This commit is contained in:
seb
2026-10-11 02:36:46 +02:00
parent ce072a3731
commit 3379a63baf
6 changed files with 356 additions and 105 deletions

View 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).

View File

@@ -0,0 +1 @@
coding-style