u
This commit is contained in:
276
.agents/skills/coding-style/SKILL.md
Normal file
276
.agents/skills/coding-style/SKILL.md
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
name: coding-style
|
||||
description: >-
|
||||
Coding and project style conventions for the Dashfront repository.
|
||||
Use this skill whenever authoring, modifying, or reviewing code, creating or updating
|
||||
automats, writing React PureComponents, integrating dashgui components, or structuring
|
||||
backend API and SSE communication. Names src/automats/brandAutomat.js as the canonical reference automat.
|
||||
---
|
||||
|
||||
# Dashfront Coding & Project Style Guide
|
||||
|
||||
This skill defines the canonical coding standards, architectural patterns, and project conventions for the `dashfront` codebase.
|
||||
|
||||
> **Canonical Reference Automat**: [src/automats/brandAutomat.js](file:///home/seb/src/dashfront/src/automats/brandAutomat.js)
|
||||
> **Canonical Reference View**: [src/routes/artikel/marken.jsx](file:///home/seb/src/dashfront/src/routes/artikel/marken.jsx)
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Architectural Principles
|
||||
|
||||
1. **State Lives Outside React**: Domain and configuration state lives in module-scoped `react-automat` stores (`src/automats/*`), independent of component lifecycles.
|
||||
2. **Zero Blank Flash**: Components read initial state **synchronously** in the `constructor` from the automat before subscribing in `componentDidMount`. Never show empty flicker when data is already cached.
|
||||
3. **PureComponents Over Hooks for Stateful Views**: Views and widgets subscribe directly via `automat.subscribe(this, selector)` in React `PureComponent` classes. Do **not** use React hooks (`useState`, `useEffect`) inside `PureComponent` classes.
|
||||
4. **No React Context for Data**: Never create React Context providers for domain models. Components import the automat or `src/api` namespace directly.
|
||||
5. **No Mock Backends**: Never mock API routes or fake endpoints. The real Express backend runs locally on port `4208` (`dashback`), proxied via Vite.
|
||||
6. **Silent SSE Updates**: Real-time server-sent events trigger **silent reloads** (`actions.reload()`), preserving visible UI state without resetting to `'loading'`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Canonical Reference Automat: `brandAutomat.js`
|
||||
|
||||
All configuration and list automats in `src/automats/` must follow the structure and conventions exemplified by [src/automats/brandAutomat.js](file:///home/seb/src/dashfront/src/automats/brandAutomat.js):
|
||||
|
||||
```
|
||||
src/automats/brandAutomat.js
|
||||
├── 1. File JSDoc with Upstream Triggers documentation
|
||||
├── 2. Imports (react-automat, api, authAutomat, sseAutomat)
|
||||
├── 3. JSDoc State Typedef & initialState
|
||||
├── 4. Pure Helper Functions (sorting, filtering)
|
||||
├── 5. Concurrency control (inflightLoad guard, pagination)
|
||||
├── 6. State Updater Helper (updateStateWithBrands)
|
||||
├── 7. Actions Object (filter, load, reload, CRUD, reset)
|
||||
├── 8. Automat Singleton Instantiation (new Automat(initialState, actions, { name }))
|
||||
├── 9. Upstream Subscriptions (authAutomat lifecycle, sseAutomat events)
|
||||
└── 10. Default Export
|
||||
```
|
||||
|
||||
### Key Patterns Illustrated by `brandAutomat.js`
|
||||
|
||||
#### A. Upstream Triggers Documentation
|
||||
Always document the trigger contract at the top of the file:
|
||||
```js
|
||||
/**
|
||||
* @file brandAutomat.js
|
||||
* @description Manages catalog brands and manufacturers for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
* - `sseAutomat`: Subscribes to the single `'brand'` event carrying
|
||||
* `{ sender, brandId }` — any create/rename/delete/picture change triggers
|
||||
* `actions.reload()`, which silently re-fetches the list. Events sent by this tab
|
||||
* (matching `sender`) are ignored.
|
||||
* - UI / Settings: Mutating actions (`create`, `rename`, `remove`, `addImage`, `deleteImage`).
|
||||
*/
|
||||
```
|
||||
|
||||
#### B. Predictable State Shape & Status Progression
|
||||
Define state via JSDoc `@typedef` with standardized status progression:
|
||||
```js
|
||||
/**
|
||||
* @typedef {Object} BrandState
|
||||
* @property {Array<object>} brands All items (full list, decoupled from page UI).
|
||||
* @property {Array<object>} filteredBrands Filtered items matching active filter.
|
||||
* @property {string} filter Active search/filter string.
|
||||
* @property {number} total Unpaged count.
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
const initialState = {
|
||||
brands: [],
|
||||
filteredBrands: [],
|
||||
filter: '',
|
||||
total: 0,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
```
|
||||
|
||||
#### C. Inflight Load Deduplication & Silent Reload
|
||||
- Guard fetches with `let inflightLoad = null` to coalesce concurrent load/reload calls into a single HTTP promise.
|
||||
- `load()` sets `status: 'loading'` for initial loads.
|
||||
- `reload()` silently updates state without setting `status: 'loading'`, keeping existing data on screen to prevent flicker.
|
||||
|
||||
```js
|
||||
let inflightLoad = null;
|
||||
|
||||
const actions = {
|
||||
/** Initial load with visible loading state */
|
||||
async load() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
brandAutomat.setState({ status: 'loading', error: null });
|
||||
return (inflightLoad = fetchAllBrands()
|
||||
.then(({ brands }) => updateStateWithBrands(brands, { status: 'succeeded' }))
|
||||
.catch((err) => {
|
||||
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
||||
throw err;
|
||||
})
|
||||
.finally(() => { inflightLoad = null; }));
|
||||
},
|
||||
|
||||
/** Silent re-fetch on SSE signal or background sync */
|
||||
async reload() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
return (inflightLoad = fetchAllBrands()
|
||||
.then(({ brands }) => updateStateWithBrands(brands, { status: 'succeeded', error: null }))
|
||||
.catch((err) => {
|
||||
brandAutomat.setState({ error: err?.message ?? String(err) });
|
||||
throw err;
|
||||
})
|
||||
.finally(() => { inflightLoad = null; }));
|
||||
},
|
||||
|
||||
reset() {
|
||||
brandAutomat.setState(initialState);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### D. Auth Lifecycle Coordination
|
||||
Automatically tie the automat lifecycle to `authAutomat`:
|
||||
```js
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) void brandAutomat.actions.load();
|
||||
else if (auth.status === 'anon') brandAutomat.actions.reset();
|
||||
});
|
||||
```
|
||||
|
||||
#### E. SSE Echo Suppression
|
||||
When handling backend SSE events, always verify `sender` against `getSseClientId()` to suppress redundant reloads for mutations dispatched by the current browser tab:
|
||||
```js
|
||||
sseAutomat.actions.on('brand', (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event?.data ?? '{}');
|
||||
if (payload?.sender && payload.sender === getSseClientId()) return;
|
||||
} catch { }
|
||||
void brandAutomat.actions.reload().catch(() => { });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Reference UI Component Pattern
|
||||
|
||||
Components consuming automats must follow the pattern established in [src/routes/artikel/marken.jsx](file:///home/seb/src/dashfront/src/routes/artikel/marken.jsx):
|
||||
|
||||
```jsx
|
||||
import { PureComponent } from 'react';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
|
||||
export class ArtikelMarkenSearchField extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// 1. Synchronous initial read — zero blank flash!
|
||||
this.state = {
|
||||
input: brandAutomat.state.filter ?? '',
|
||||
submitting: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// 2. Fine-grained selector subscription
|
||||
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
||||
input: state.filter,
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// 3. Always clean up subscription
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
onInputChange(input) {
|
||||
this.setState({ input });
|
||||
brandAutomat.actions.filter(input);
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
const name = this.state.input.trim();
|
||||
if (!name || this.state.submitting) return;
|
||||
|
||||
this.setState({ submitting: true });
|
||||
try {
|
||||
const row = await brandAutomat.actions.create({ name });
|
||||
enqueueSnackbar(`Marke „${row.name || name}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ input: '', submitting: false });
|
||||
brandAutomat.actions.filter('');
|
||||
} catch (err) {
|
||||
this.setState({ submitting: false });
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { input, submitting } = this.state;
|
||||
return (
|
||||
<SingleLineTextEdit
|
||||
value={input}
|
||||
disabled={submitting}
|
||||
onChange={(next) => this.onInputChange(next)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Rules
|
||||
1. **Always unsubscribe**: Save the return value of `automat.subscribe(this, ...)` in `this.unsub` and invoke `this.unsub?.()` in `componentWillUnmount()`.
|
||||
2. **Synchronous constructor reads**: Read directly from `automat.state` or helper selector (e.g. `getArticle(sku)`) in the constructor.
|
||||
3. **Selector efficiency**: The second argument to `subscribe(this, selector)` should return an object with only the slice of state needed by this component.
|
||||
4. **No hooks in PureComponents**: Never call `useState`, `useEffect`, or `useMemo` inside `PureComponent`.
|
||||
|
||||
---
|
||||
|
||||
## 4. UI Library Integration (`dashgui` & MUI)
|
||||
|
||||
1. **Leverage `dashgui` Components**:
|
||||
- `PanelTile` & `PanelTileGrid`: Container cards and responsive layouts.
|
||||
- `SingleLineTextEdit`: Editable single-line text inputs (supports `search`, `clear`, `size="small"`).
|
||||
- `RecordList`: High-performance list view for tabular and recordset data.
|
||||
- `ElevatedPill`: Badges, chips, and status pills.
|
||||
- `FillSpace`: Layout spacer.
|
||||
2. **Theme Tokens & Color Constants**:
|
||||
- Import shared palette constants from [src/colors.js](file:///home/seb/src/dashfront/src/colors.js) (`MAIN_BACKGROUND`, `PURPLE_PANEL`, etc.).
|
||||
- Use standard MUI `Box`, `Button`, `Typography`, `CircularProgress` with `sx` props for layout spacing (`gap: 1`, `pb: 2`).
|
||||
3. **Notifications**:
|
||||
- Success: `enqueueSnackbar('Erfolgreich gespeichert', { variant: 'success' })` from `notistack`.
|
||||
- Error: `notifyError(err)` from `src/utils/notify`.
|
||||
- Page/Section Error Display: `<BackendError error={...} />`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Backend Communication & Pricing Rules
|
||||
|
||||
1. **API Namespaces**:
|
||||
- Always import from `src/api` (`import { brand, article, delivery } from '../api'`).
|
||||
- The `http()` client automatically sets `credentials: 'include'`.
|
||||
- **Never pass `googleAuthId`** in request payloads; the backend extracts identity directly from the HTTP-only cookie.
|
||||
2. **Pricing Convention**:
|
||||
- Prices sent to and received from the API/database are **net prices** (`net`).
|
||||
- Gross prices displayed to end users must be calculated using tax rates from `companyAutomat` (`selectCompanyTaxrate()`, `selectCompanyReducedTaxrate()`).
|
||||
3. **Binary / Media Assets**:
|
||||
- Compress images client-side into AVIF format (`@jsquash/avif`) before uploading.
|
||||
- Use `ThumbHash` for instant blur placeholders before loading full images from `/api/thumb/{hash}` or `/api/image/{hash}`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Checklist for New Automats
|
||||
|
||||
When adding a new domain or configuration automat:
|
||||
|
||||
- [ ] Place file in [src/automats/](file:///home/seb/src/dashfront/src/automats/).
|
||||
- [ ] Add JSDoc file header documenting **Upstream Triggers** (auth, SSE, UI).
|
||||
- [ ] Model the state with JSDoc `@typedef`, including `status: 'idle'|'loading'|'succeeded'|'failed'` and `error`.
|
||||
- [ ] Implement `inflightLoad` promise guard for request deduplication.
|
||||
- [ ] Implement both `load()` (visible loading) and `reload()` (silent background refresh).
|
||||
- [ ] Subscribe to `authAutomat` to auto-load on `'authed'` with `selectedCompanyId` and `reset()` on `'anon'`.
|
||||
- [ ] Subscribe to `sseAutomat` with `getSseClientId()` echo check to trigger `actions.reload()`.
|
||||
- [ ] Instantiate `new Automat(initialState, actions, { name: '...' })`.
|
||||
- [ ] Export singleton as default and re-export in [src/automats/index.js](file:///home/seb/src/dashfront/src/automats/index.js).
|
||||
1
.agents/skills/project-style
Symbolic link
1
.agents/skills/project-style
Symbolic link
@@ -0,0 +1 @@
|
||||
coding-style
|
||||
Reference in New Issue
Block a user