diff --git a/.agents/skills/coding-style/SKILL.md b/.agents/skills/coding-style/SKILL.md index 31aef55..643181f 100644 --- a/.agents/skills/coding-style/SKILL.md +++ b/.agents/skills/coding-style/SKILL.md @@ -2,275 +2,10 @@ 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 +class componets disconnected from state, in automat. -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} brands All items (full list, decoupled from page UI). - * @property {Array} 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 ( - 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: ``. - ---- - -## 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). +Reference: +Component: src/routes/artikel/marken.jsx +it's Automat: src/automats/brandAutomat.js \ No newline at end of file