Files
dashfront/.agents/skills/coding-style/SKILL.md
2026-10-11 02:36:46 +02:00

12 KiB

name, description
name description
coding-style 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
Canonical Reference View: 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:

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:

/**
 * @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:

/**
 * @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.
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:

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:

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:

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 (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/.
  • 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.