diff --git a/docs/frontend-architecture.md b/docs/frontend-architecture.md deleted file mode 100644 index 01ed8e3..0000000 --- a/docs/frontend-architecture.md +++ /dev/null @@ -1,183 +0,0 @@ -# Frontend architecture — the shared `src/` skeleton - -**This is the source of truth for how a pathscale frontend app is laid out**, for humans -and agents alike. Seven apps share it: `24x.ai`, `honey.id`, `pathscale.com`, -`pays.online`, `support.cafe`, `web3.trading`, `nofilter.io`. They link here rather than -each keeping a copy — one skeleton, one description, no drift. - -Scope: **where code goes and why**. Two neighbours cover the rest: - -- [`ui-usage.md`](ui-usage.md) — how to use `@pathscale/ui` (components, theming, forms). -- [`frontend-services-contract.md`](frontend-services-contract.md) — how a backend - endpoint is wired from contract to hook. - -Anything a single app does differently belongs in **that app's** -`docs/frontend-conventions.md`, under its "Deviations" section — not here. - -## The skeleton - -Present in **all seven** apps, without exception: - -``` -src/ - api/ config/ features/ layouts/ models/ services/ styles/ - assets/ constants/ hooks/ lib/ scripts/ stores/ utils/ -``` - -Plus five root files, also in all seven: `App.tsx`, `config.ts`, `env.d.ts`, -`index.css`, `index.tsx`. - -Optional, present in some: - -| directory | in | -|---|---| -| `pages/` | honey.id, pathscale.com, pays.online, web3.trading, nofilter.io | -| `routes.ts` | same five | -| `types/` | same five | -| `schemas/` | honey.id, web3.trading, nofilter.io | -| `routing/` (dir) | pathscale.com, nofilter.io — `pays.online` has `routing.ts` as a file | -| `test/` | pathscale.com, nofilter.io | -| `contexts/` | honey.id only | -| `callapp/`, `realtime/`, `webrtc.d.ts` | nofilter.io only | - -`24x.ai` and `support.cafe` are the lean end — skeleton only, no `pages/`, no route table. - -Note `config.ts` (file) and `config/` (directory) coexist in every app. They are not -duplicates: see below. - -## What belongs where - -### `api/` — transport wiring, nothing else - -`configure.ts` builds the `@pathscale/wss-adapter` configuration — remote URLs, the -method map, `onError`, `onDisconnect` — and calls `wssAdapter.configure()`. `index.ts` -exports the ready-to-call session objects (`authApi`, `apiApi`, …) that hooks import. - -`api/services/` is **generated output**, not hand-written wiring: one JSON method map per -service, emitted by `bun run schema`. Do not edit it, and do not add code files to it. - -**Does not belong:** business logic, retries with domain meaning, anything importing from -`features/`. - -### `models/` — generated DTOs, never hand-edited - -Every type describing a backend payload. Generated from the services JSON; edits are -silently destroyed on the next `bun run schema`. - -Two layouts are in the wild. Newer apps group by contract first -(`src/models/api/userApi/GetUsersDto.ts` in `honey.id`); older ones are flat by service -(`src/models/paymentApi/` in `pays.online`). Both are generated; follow whichever your -repo already uses. - -**The exception worth knowing:** a few hand-written files sit at the *top level* of -`models/` — `honey.id` has `src/models/roles.ts` and -`src/models/supportCafeChatMessage.ts`. The never-hand-edit rule applies to the generated -*subdirectories*, not to these. Generated files carry a "DO NOT MODIFY IT BY HAND" banner; -if a file has no banner, it is hand-written. - -### `services/` — business logic and error normalisation - -Where an API call becomes a domain operation: sequencing multiple calls, normalising -errors into something a component can act on, owning connection lifecycle. - -Two roles live here in practice: - -- **`serviceStore.ts`** — connection and session lifecycle. Present in all seven. Hooks - import it for the `servicesInitialized` readiness gate. -- **`authServices.ts` / `authErrorNormalize.ts`** — domain logic proper. - -**Not every call passes through here.** A plain read often goes hook → `api/` directly. -`services/` is entered when there is logic to own, not as a mandatory relay layer. - -**Does not belong:** JSX, component imports, route knowledge. - -### `hooks/` — what components actually consume - -One hook per endpoint or per interaction, wrapping `@tanstack/solid-query` and returning -reactive state. Subdirectory grouping differs by app and both are fine: by domain -(`src/hooks/users/`, `src/hooks/metrics/` in `honey.id`) or by service -(`src/hooks/userApi/`, `src/hooks/adminApi/` in `support.cafe`). - -Query keys are centralised in `src/constants/queryKeys.ts` — never inline a key string. - -**Does not belong:** JSX, direct DOM work, endpoints that aren't in the services JSON. - -### `components/` vs `features/` — the split that matters most - -- **`components/`** — reusable and domain-agnostic. If it would still make sense in a - different app, it goes here. Grouped by kind (`components/buttons/`, - `components/tables/`, `components/skeletons/`). -- **`features/`** — one directory per product area, owning its own slice end to end. The - convention inside is `components/`, `hooks/`, `pages/`, plus whatever that feature needs - (`honey.id` has `features/auth/utils/`, `features/auth/schema/`, - `features/admin/skeletons/`). - -The test: **would this make sense outside this product area?** Yes → `components/`. -No → that feature's folder. A feature may import from `components/`; `components/` must -never import from `features/`. - -### `stores/` vs `contexts/` — global state vs tree-scoped values - -- **`stores/`** — module-level `createSignal` singletons, imported directly, no provider. - This is the default and covers nearly everything: `authStore.ts`, `connectionStore.ts`, - `i18nStore.ts` recur across apps. -- **`contexts/`** — Solid `createContext`, only for values that *cannot* be global because - they belong to one subtree. The single example across all seven is `honey.id`'s - `src/contexts/DashboardNavContext.tsx`, which passes a DOM element reference down one - layout. - -Reach for a store first. A context is justified only when a value is genuinely per-subtree. - -### The small ones - -- **`config/`** (directory) — app configuration modules: `routes.ts` (path constants), - `featureFlags.ts`, `i18n.ts`. **`config.ts`** (root file) is the environment/runtime - config entry — different thing, both exist everywhere. -- **`constants/`** — fixed values, no logic: `queryKeys.ts`, `connectionSettings.ts`. -- **`lib/`** — self-contained utilities with real internal complexity, often a - subdirectory of their own (`lib/secureStorage/`, `lib/theme.ts`). -- **`utils/`** — small stateless helpers, one concern per file (`formatTimestamp.ts`, - `roleUtils.ts`). If it grows a subdirectory and internal state, it belongs in `lib/`. -- **`layouts/`** — page shells composed by the router (`AppLayout.tsx`, `AuthLayout.tsx`, - `RootLayout.tsx`), exported through an `index.ts`. -- **`styles/`** — global CSS and theme files. Component styling is Tailwind utilities plus - `@pathscale/ui` tokens; see [`ui-usage.md`](ui-usage.md). -- **`scripts/`** — build and codegen scripts run through `package.json`, notably - `schema.js`. Not application code; never imported by `src/`. -- **`assets/`** — static files (images, icons). - -## Routing - -Three layers. They stack — a later one never replaces an earlier one. - -**1. Path constants — `src/config/routes.ts`. Present in all seven, always.** Exports a -`ROUTES` object and any path helpers. Every path string in the app comes from here; never -hard-code a route string in a component. - -**2. The route table — `src/routes.ts`.** Exports `routes: RouteConfig[]`, mapping paths to -components, layouts and guards. Five apps have it; `24x.ai` and `support.cafe` instead -declare `` elements inline in `App.tsx`, with guards as wrapper components -(`RequireAuth`, `RedirectIfAuth`, `RequireRole`). - -**3. Derived groupings — `src/routing/`.** Filters the route table into groups the shell -renders (`routeGroups.ts`), and in `nofilter.io` also access policy -(`src/routing/accessPolicy.ts`). This layer **imports** `~/routes` — it is a consumer of -layer 2, not an alternative to it. `pays.online` does the same thing in a single -`src/routing.ts` file. - -So "`routes.ts` or `routing/`?" is a false choice: if an app has `routing/`, it has -`routes.ts` too, and both sit on `config/routes.ts`. - -## Adding something new — where does it go? - -| you are adding | it goes in | -|---|---| -| a backend call | `hooks/` (see [`frontend-services-contract.md`](frontend-services-contract.md)) | -| a screen | that feature's `pages/`, registered in `routes.ts` / `App.tsx` | -| a widget used by one feature | that feature's `components/` | -| a widget used by three features | `components/` | -| global state | `stores/` | -| a value scoped to one subtree | `contexts/` | -| a pure helper | `utils/`, or `lib/` if it needs its own directory | -| a path | `config/routes.ts`, always | -| a type describing backend data | nowhere by hand — regenerate `models/` | diff --git a/docs/frontend-services-contract.md b/docs/frontend-services-contract.md deleted file mode 100644 index c97cd1b..0000000 --- a/docs/frontend-services-contract.md +++ /dev/null @@ -1,148 +0,0 @@ -# Frontend services contract — contract to hook - -**This is the source of truth for how a backend endpoint reaches a component**, for humans -and agents alike, across `24x.ai`, `honey.id`, `pathscale.com`, `pays.online`, -`support.cafe`, `web3.trading` and `nofilter.io`. They link here rather than each keeping -a copy. - -This documents the **wiring pattern**. It is deliberately not an endpoint list — the -endpoints are already machine-readable in each app's `docs/*.services.json`, and any list -here would be stale within a week. - -Neighbours: [`frontend-architecture.md`](frontend-architecture.md) for where code lives, -[`ui-usage.md`](ui-usage.md) for `@pathscale/ui`. - -## The rule that comes before everything - -**`docs/*.services.json` is the contract, and it is authoritative.** Endpoints, -parameters, returns, error variants, roles — if it is not in there, it does not exist as -far as the frontend is concerned. - -Do not invent an endpoint, add a parameter the contract doesn't declare, or assume a -return shape because it would be convenient. When something you need is missing, **say so -and stop** — that is a backend conversation, not a frontend workaround. Guessing produces -code that typechecks, passes review, and fails against a real server. - -Apps carry one or more contracts. `honey.id` has three — `docs/api.services.json`, -`docs/auth.services.json`, `docs/support.services.json` — which is why its `src/models/` -has three subtrees. Most apps have one. - -## The chain - -``` -docs/.services.json the contract — authoritative, hand-maintained - │ - ├─(bun run schema)─> src/models//… generated DTOs + enums + errorCatalog - └─(bun run schema)─> src/api/services//… generated method maps (JSON) - -src/api/configure.ts reads docs/*.services.json directly, configures @pathscale/wss-adapter - └─> src/api/index.ts exports callable sessions (authApi, apiApi, …) - └─> src/hooks//useX.ts solid-query wrapper — what components consume - └─> src/services/… only where there is domain logic to own -``` - -Two things about this diagram are easy to get wrong, so they are worth stating plainly: - -**`src/api/services/` is generated, and `configure.ts` does not read it.** The method map -`configure.ts` passes to the adapter is built in-process from `docs/*.services.json` -(`buildMethods()`). The JSON under `src/api/services/` is a *separate emission* of the same -information, and is imported by hand in only a couple of places — in `honey.id`, just -`src/features/auth/reauth/reauthSession.ts`. Do not treat that directory as the wiring. - -**`src/services/` is not a mandatory hop.** A plain read goes hook → `api/`. The -`services/` layer is entered when there is real logic — multi-step flows, error -normalisation, connection lifecycle. Routing every call through it "for consistency" adds -a pass-through file that does nothing. - -## Worked trace — `GetUsers` in `honey.id` - -**1. The contract.** `docs/api.services.json` declares endpoint code `10000`, name -`GetUsers`, parameters `appPublicId`, `page`, `pageSize`. - -**2. Generation.** `bun run schema` (which is `bun run src/scripts/schema.js` in every one -of the seven apps) reads the contract and writes: - -- `src/models/api/userApi/GetUsersDto.ts` — `GetUsersParams` and `GetUsersResponse`, - carrying a "DO NOT MODIFY IT BY HAND" banner -- `src/models/api/userApi/index.ts` — barrel re-export -- `src/models/api/enums/` — the contract's enums, one file each, plus an `index.ts` -- `src/api/services/api/userApi.json` — `{"10000": {"name": "GetUsers", "parameters": [...]}}` - -**3. Transport.** `src/api/configure.ts` builds the adapter configuration — remote URL per -service, the method map, `timeout`, an `onError` handler and per-service `onDisconnect` — -and calls `wssAdapter.configure()`. `src/api/index.ts` exports the resulting session -objects. - -**4. The hook.** `src/hooks/users/useGetUsers.ts` is the whole consumer-facing surface: - -```ts -export const useGetUsers = (params: GetUsersParams, options?: { enabled?: boolean }) => - useQuery(() => ({ - queryKey: userApiKeys.users(params.appPublicId, keyParams), - enabled: (options?.enabled ?? true) && serviceStore.servicesInitialized && !!params.appPublicId, - queryFn: async () => { - const res = await apiApi.GetUsers(params); - const payload = (res as { params?: GetUsersResponse }).params ?? res; - return { ...payload, data: (payload.data ?? []) as UserListItem[] }; - }, - })); -``` - -Four conventions are visible in those few lines, and all four are load-bearing: - -- **Types come from `models/`**, never hand-declared alongside the hook. -- **The key comes from `src/constants/queryKeys.ts`** (`userApiKeys.users(...)`), never - inlined. -- **`enabled` gates on `serviceStore.servicesInitialized`.** Without it the query fires - before the socket is up and fails on first paint. -- **The response is unwrapped defensively** — `res.params ?? res` — because the adapter - may hand back either shape. - -**5. Components** call `useGetUsers(...)` and read `.data` / `.isLoading`. They never touch -`apiApi`. - -## Adding an endpoint - -1. **Confirm it exists in `docs/*.services.json`.** If not, stop — nothing below is valid. -2. **Regenerate:** `bun run schema`. Never hand-write the DTO. -3. **Commit the generated output** alongside your change; it is checked in. -4. **Add a query key** to `src/constants/queryKeys.ts`. -5. **Write the hook** in `src/hooks//`, mirroring the nearest existing one — - same unwrapping, same `enabled` gate. -6. **Add a `services/` function only if there is logic to own.** Otherwise skip it. -7. **Consume the hook** from the component. - -Regenerated, never hand-edited: `src/models/**` and `src/api/services/**`. -Hand-written: everything in `hooks/`, `services/`, `constants/queryKeys.ts`, and -`api/configure.ts`. - -## How errors surface - -`src/services/authErrorNormalize.ts` in `honey.id` is the worked example, and it closes the -loop back to the contract: it imports `AUTH_ERROR_CATALOG` and `ERROR_CODE_NAME_BY_VALUE` -from `src/models/auth/errorCatalog.ts` — themselves generated from the `errors` blocks in -`docs/auth.services.json`. Backend error variants are therefore typed, not guessed at from -strings. - -Four rules it encodes, all worth copying: - -- **Never branch on a message string.** Control flow keys off `kind` and `code`; the - `NormalizedAuthError.message` field is commented "never use for control flow" precisely - because messages are display text and will change. -- **Allowlist what reaches the UI.** `SAFE_PARAM_KEYS` passes through timing and count - fields (`retryAfter`, `attemptsRemaining`, …) and drops everything else, so a backend - that starts attaching a token to an error payload cannot leak it into a component. -- **Never surface the raw error.** It is kept on `raw` for logging and not rendered. -- **Distinguish known from unknown.** `isKnownServiceError` separates a declared contract - variant from anything else, so unknown failures get a generic message rather than an - unhandled branch. - -Transport-level failures are handled once, centrally, in `configure.ts`'s `onError`: -auth failures invalidate the session through `authStore`, and everything else is logged. -Individual hooks do not re-implement that. - -## When the contract and the code disagree - -The contract wins. If `src/models/` looks wrong, regenerate before you debug — the most -common cause is a contract that moved and generated files that didn't. If the contract -itself looks wrong, that is a backend conversation. Do not paper over it in a hook. diff --git a/docs/ui-usage.md b/docs/ui-usage.md index de6d74f..694dda7 100644 --- a/docs/ui-usage.md +++ b/docs/ui-usage.md @@ -1,9 +1,8 @@ # @pathscale/ui — Usage Reference **This is the source of truth for `@pathscale/ui` usage conventions**, for humans and -agents alike, and for every consuming app (24x.ai, honey.id, pathscale.com, -pays.online, support.cafe, web3.trading, nofilter.io). Those repos link here rather -than keeping their own copy — one library, one set of conventions, no drift. +agents alike, and for every consuming application. Those apps link here rather than +keeping their own copy — one library, one set of conventions, no drift. SolidJS component library, HeroUI-parity API, daisyUI-style theming. ~104 components.