diff --git a/.order b/.order new file mode 100644 index 0000000..a4a406a --- /dev/null +++ b/.order @@ -0,0 +1,9 @@ +architecture +principles +local-setup +repository-layout +api +web-frontend +markdown-and-editor +extending +testing-and-contributing diff --git a/README.md b/README.md deleted file mode 100644 index 7737b7b..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# developer-guide - diff --git a/api/index.md b/api/index.md new file mode 100644 index 0000000..3e4e838 --- /dev/null +++ b/api/index.md @@ -0,0 +1,77 @@ +--- +id: api +title: The API +description: Route groups, authentication, and the review workflow's SHA contract. +tags: [api, architecture] +lang: en +--- + +# The API + +`apps/api` is a Fastify server. It is the only part of f451 that checks +permissions, runs the review workflow and writes to the index — see +[[principles]]. Everything below is registered in `src/app.ts`. + +## Route groups + +| Group | Examples | Notes | +|---|---|---| +| System | `GET /healthz`, `GET /readyz` | Unauthenticated, for orchestrators. | +| Docs | `GET /api/docs`, `GET /api/openapi.json` | Swagger UI and the generated OpenAPI spec, open without a session. | +| Auth | `/auth/*` (OIDC login/callback/logout), `/auth/connect/*` (link a provider account) | Open — a login flow can't require a session to start one. | +| Session & tokens | `GET /api/me`, `/api/tokens/*` | Personal API token management for agents (see [[extending]]). | +| Read | `GET /api/spaces/:space/*` (pages, tree, search, broken links, graph, metadata schema, templates), `GET /api/pages/:id`, `GET /api/pages/:id/versions*` | Filtered by the caller's read access to the underlying repository. | +| Write / drafts | `POST /api/pages`, `.../draft`, `.../draft/media`, `.../locks`, `.../move`, `.../unarchive`, `DELETE /api/pages/:id`, `PUT /api/spaces/:space/order` | Require a linked provider account with write access; committed under the user's own token. | +| Review workflow | `/api/pages/:id/draft`, `/api/pages/:id/review`, `/api/pages/:id/review/request-changes`, `/api/pages/:id/release` (MCP tools `request_review` / `release_page`, see [[extending]]) | Turns a draft branch into a pull request, then merges it. | +| Media | `/media/*` | Binary asset delivery, gated like page reads. | +| Webhooks | `/webhooks/*` | Provider push events trigger incremental reindexing; HMAC-verified, no session. | +| Admin | `/admin/reindex`, `/admin/status`, `/admin/backfill-ids` | Bearer-token gated (`F451_ADMIN_TOKEN`), fail-closed if the token isn't configured. | + +## Authentication + +Two independent mechanisms protect `/api/*`, `/admin/*` and `/media/*`: + +- **Session cookies**, established via OIDC login (Entra, Forgejo, or any + other OIDC provider). This is how the browser UI authenticates. +- **Personal API tokens** (`f451_pat_…`), issued through `/api/tokens` and + used by the MCP service and other automation. Tokens carry a scope — + `read` or `write` — and a read-scoped token gets a `403` on anything but + `GET`. + +`/admin/*` additionally accepts a separate bearer token +(`F451_ADMIN_TOKEN`) instead of a session, so ops automation can call +`POST /admin/reindex` from a plain script. + +A CSRF origin check runs on every state-changing request (`POST`, `PUT`, +`PATCH`, `DELETE`): a present-but-foreign `Origin` header is rejected. A +missing header is allowed through deliberately — that's the shape of +legitimate non-browser clients (CLI, CI, webhook senders), which +`SameSite=Lax` cookies don't protect against by themselves. + +## `404` is deliberately ambiguous + +Read routes never distinguish "this page doesn't exist" from "you don't +have access to it" — both are a plain `404`. See [[principles]] for why. + +## The review workflow and its SHA contract + +Nothing writes to the published version directly. The sequence is always: + +1. **Create a draft** — a branch, from the current published state. +2. **Write** — save content to the draft branch, repeatedly. +3. **Request review** — opens a pull request. +4. **Release** — merges the pull request. +5. **Reindex** — the merge webhook (or a manual `/admin/reindex`) updates + the Postgres index from the new Git state. + +Every write to a draft names a `baseSha`: the commit SHA the writer started +from. If the draft has moved on since — someone else saved a change, or a +previous save from the same writer landed — the API responds `409` with +the *current* SHA and content instead of silently overwriting it. The +caller is expected to re-fetch, reconcile, and retry with the new +`baseSha`. + +> [!IMPORTANT] +> This is the same mechanism whether the writer is a human in the browser +> editor or an AI agent calling `update_page_draft` through MCP — see +> [[extending]]. There is no privileged, SHA-check-free path for agents. diff --git a/architecture/index.md b/architecture/index.md new file mode 100644 index 0000000..c7ab59f --- /dev/null +++ b/architecture/index.md @@ -0,0 +1,49 @@ +--- +id: architecture +title: Architecture +description: The three applications and four packages, and how a request travels through them. +tags: [architecture, overview] +lang: en +--- + +# Architecture + +f451 is a pnpm monorepo on Node 22. Three applications and four shared +packages divide the work cleanly along one line: **`apps/api` is the only +place that checks permissions, runs the review workflow and writes to the +index.** Everything else either renders what the API gives it, or talks to +the API on someone's behalf. + +## Request flow + +``` +Browser ──► apps/web (Next.js) ──► apps/api (Fastify) ──► Forgejo / GitHub + │ (content, permissions) + └──► PostgreSQL (index, sessions) + +AI agent ──► apps/mcp (MCP↔HTTP) ──────────► apps/api (same HTTP API) +``` + +Nothing skips `apps/api`. The web app never talks to Git directly, and the +MCP service never talks to Postgres or Git directly — both reach content +and permissions exclusively through the API's HTTP surface. + +## The three applications + +| App | Role | +|---|---| +| `apps/api` (Fastify) | Indexing, read and write endpoints, webhooks, drift reconciliation, admin. The only place with permission checks, the review workflow and the index. | +| `apps/web` (Next.js App Router) | The user interface. Proxies `/api`, `/auth`, `/admin`, `/media` to the API, `/drawio` to the draw.io container, `/mcp` to the MCP service. Server Components by default; client islands stay small. | +| `apps/mcp` | Access for AI agents: a stateless translator between MCP and the HTTP API. It wraps only the HTTP API, holds no secret of its own, and passes the calling user's token through on every request. | + +## The four packages + +| Package | Role | +|---|---| +| `packages/markdown` | Renders Markdown to sanitized HTML. Shared by reading and writing so both sides agree on what a construct means. | +| `packages/editor` | The ProseMirror schema behind the WYSIWYG editor. Round-trip tests with `packages/markdown` keep reading and editing aligned — a document that survives Markdown → editor → Markdown unchanged. | +| `packages/design-tokens` | The catalog of design tokens that drives the look and feel — see [[web-frontend]]. | +| `packages/git-provider` | One interface, two implementations (Forgejo, GitHub): read/write files, commits, branches, pull requests, reviews. | + +See [[principles]] for why the lines are drawn exactly here, and +[[repository-layout]] for the directories underneath each of these. diff --git a/extending/index.md b/extending/index.md new file mode 100644 index 0000000..ce9e91b --- /dev/null +++ b/extending/index.md @@ -0,0 +1,103 @@ +--- +id: extending +title: Extending f451 +description: Recipes for the most common kinds of change — an API route, a UI string, a design token, a Markdown construct, an MCP tool, a Git provider. +tags: [howto, architecture] +lang: en +--- + +# Extending f451 + +Six recipes for changes that come up repeatedly. Each one names where the +change starts and what it touches on the way. + +## Add an API route + +1. Add a `register*Routes` function under `apps/api/src/routes/`, following + the existing pattern: it takes `{ db, spaces, providerRegistry, access, + canWrite, getUserProvider }` as needed, not the raw request context. +2. Register it in `apps/api/src/app.ts`, in the block that only runs when + `db && opts.spaces && opts.providerRegistry` are present — read-only + routes need `access`; anything that writes needs `canWrite` and + `getUserProvider` as well, since writes commit under the calling user's + own provider token (see [[principles]]). +3. Decide read or write semantics up front: read routes are filtered by + `access.canRead`, write routes additionally need a linked provider + account and go through the draft/SHA-contract machinery described in + [[api]] — don't bypass it for a "simple" new route. +4. Add a Swagger/JSON schema for the response; the OpenAPI spec at + `/api/openapi.json` is generated from it. + +## Add a UI string + +Add the key to both `apps/web/lib/i18n/messages/en/` and the matching +`de/` file in the same change — f451's interface is bilingual, and a +string that exists in only one language is a regression, not a partial +feature. See [[web-frontend]]. + +## Add a design token + +1. Add the value to `packages/design-tokens/src/tokens.ts`. +2. Describe it in `catalog.ts`: its level (`theme` / `derived` / + `structure` / `switch`), its settings-page group, its role in plain + language, and whether a user theme is allowed to set it (`settable`, + with a `lockReason` if not). +3. Update the token-count tests for the affected level and group — they + are pinned deliberately, so the update should be a conscious edit, not + a number you copy without reading why it existed. See [[web-frontend]]. + +## Add a Markdown construct + +Full recipe in [[markdown-and-editor]]. In short: parse and render it in +`packages/markdown`, make `stringify.ts` reproduce the same syntax, add the +matching node to `packages/editor` and its two conversion directions, then +add a round-trip test that proves Markdown → editor → Markdown is +lossless. + +## Add an MCP tool + +MCP tools in `apps/mcp/src/tools/` are thin wrappers: each one calls the +HTTP API (`apps/mcp/src/client.ts`) with the token passed through from the +calling agent, and does not hold state or a secret of its own. The +existing set is a useful map of scope: + +- **Read** (`tools/read.ts`): `search_wiki`, `list_spaces`, `get_tree`, + `read_page`, `get_page_source`, `get_graph`, `list_broken_links`. +- **Write** (`tools/write.ts`): `create_page`, `edit_page`, + `update_page_draft`, `discard_page_draft`, `request_review`, + `release_page`, `request_changes` — the review workflow from [[api]], + exposed one step at a time. +- **Attachments** (`tools/attachments.ts`): `save_diagram`, `attach_file`. + +A new tool follows the same shape: register it with `server.registerTool`, +give it a clear `title` and input schema, and implement it by calling an +existing API route — never by reaching around the API into the database or +Git directly. If no suitable API route exists yet, add one first (see +above); the MCP service is not the place to grow API-shaped logic of its +own. + +## Add a Git provider + +`packages/git-provider/src/types.ts` defines one interface, +`GitProvider` — file reads (`readFile`, `readFileBinary`, `listTree`), +writes (`writeFile`, `writeFileBinary`, `deleteFile`, the batched +`commitFiles`), branches, and the pull request lifecycle +(`createPullRequest`, `requestReviewers`, `submitPullRequestReview`, +`mergePullRequest`). `forgejo.ts` and `github.ts` are the two existing +implementations. + +A third provider means a new file implementing every method of +`GitProvider`, throwing the same three error types +(`NotFoundError`/`ConflictError`/`ProviderError`) the existing +implementations use — callers in `apps/api` branch on those errors, not on +provider-specific ones. `commitFiles` is worth reading closely before +implementing it: it exists because writing many files one call at a time +was measured to take minutes on a large move, and its batching behavior +(splitting large change sets across multiple underlying commits, returning +the last commit's SHA, treating an empty list as a no-op) is part of the +contract, not an implementation detail. + +> [!TIP] +> None of these recipes need a browser or a full test run to make progress +> on. See [[testing-and-contributing]] for what to actually check before +> opening a pull request. diff --git a/index.md b/index.md new file mode 100644 index 0000000..d931770 --- /dev/null +++ b/index.md @@ -0,0 +1,46 @@ +--- +id: overview +title: Developer Guide +description: How to understand, run and extend f451, the git-native documentation wiki. +tags: [overview, getting-started] +lang: en +--- + +# f451 Developer Guide + +f451 is a wiki whose content lives entirely in Git. Every space is a +repository on Forgejo or GitHub, every page a Markdown file with YAML +frontmatter. PostgreSQL only holds a derived index — search vectors, +rendered HTML, the knowledge-graph edges — and can always be rebuilt from +Git. This demo space is for developers who want to run f451 locally, +understand how the pieces fit together, and add something to it. + +> [!NOTE] +> This is a demo space, not the project's own documentation. It exists to +> show what a well-structured f451 space looks like while explaining f451 +> itself. + +## Where to start + +- [[principles]] — the four architectural rules that shape every decision + in the codebase, and why they exist +- [[architecture]] — the three applications and four packages, and how a + request travels through them +- [[local-setup]] — get a working stack on your machine +- [[repository-layout]] — what lives where in the monorepo + +## Going deeper + +- [[api]] — route groups, authentication, the review workflow and its SHA + contract +- [[web-frontend]] — routing, the proxy, internationalization, styling and + design tokens +- [[markdown-and-editor]] — the Markdown pipeline and the editor that has + to stay in sync with it + +## Contributing + +- [[extending]] — recipes for the six most common kinds of change +- [[testing-and-contributing]] — what to run before you open a pull + request, what CI runs, and the license terms your contribution falls + under diff --git a/local-setup/index.md b/local-setup/index.md new file mode 100644 index 0000000..657faf2 --- /dev/null +++ b/local-setup/index.md @@ -0,0 +1,84 @@ +--- +id: local-setup +title: Local setup +description: Get a working f451 stack on your machine, and the commands you'll use daily. +tags: [setup, getting-started] +lang: en +--- + +# Local setup + +f451 needs Node 22 and pnpm 9, plus Docker (or Podman) for the two stacks +it runs locally: a Forgejo instance for content and sign-in, and the wiki +itself. + +> [!NOTE] +> This mirrors the top-level `README.md`. If the two ever disagree, the +> README is authoritative — this page exists so the setup steps sit next +> to the rest of the developer documentation. + +## Why not `localhost` + +The browser and the containers must reach Forgejo and the wiki under the +*same* address, because inside a container `localhost` means the container +itself, not your machine. Pick your machine's LAN address once and use it +everywhere below (called ``). + +## Steps + +1. **Start Forgejo first.** It must be running before the wiki stack, since + the wiki authenticates against it. + + ``` + cat > deploy/git/docker-compose.override.yml <:3300/ + EOF + docker compose -f deploy/git/docker-compose.yml up -d + ``` + +2. **Run the setup script.** It creates an admin account, an organization, + an OAuth app and a demo space, and writes `deploy/wiki/.env`: + + ``` + FORGEJO_URL=http://:3300 WEB_BASE=http://:8080 ./scripts/dev-local-setup.sh + ``` + +3. **Start the wiki:** + + ``` + cd deploy/wiki && docker compose up -d --build + ``` + +4. **Open** `http://:8080`, sign in through Forgejo + (`wiki-admin` / `admin1234`), then link your Forgejo account under + *Settings → Connections*. Spaces only become visible and writable once + your account is linked — this is the "permissions come from the Git + provider" rule from [[principles]] in practice. + +> [!WARNING] +> This setup is for local development only: plain HTTP, insecure cookies. +> It is not meant to be exposed beyond your machine. + +Database migrations do not run automatically after an update; run +`docker compose run --rm api node dist/db/migrate-cli.js`. + +## Everyday commands + +Once the stack is up, most development happens outside Docker: + +| Purpose | Command | +|---|---| +| Type-check every package | `pnpm -w typecheck` | +| Run every test | `pnpm -w test` | +| Run one test file | `pnpm --filter @f451/web test -- lib/urls.test.ts` | +| API in dev mode (port 3001) | `pnpm --filter @f451/api dev` | +| Web in dev mode (port 3000) | `pnpm --filter @f451/web dev` | +| Generate a database migration | `pnpm --filter @f451/api db:generate` | +| CSS metrics with bounds | `pnpm css:inventar` | + +For what to actually run before opening a pull request, see +[[testing-and-contributing]] — it is a much shorter list than "everything +above". diff --git a/markdown-and-editor/index.md b/markdown-and-editor/index.md new file mode 100644 index 0000000..dc97466 --- /dev/null +++ b/markdown-and-editor/index.md @@ -0,0 +1,76 @@ +--- +id: markdown-and-editor +title: Markdown and the editor +description: The Markdown pipeline, its constructs, and adding a construct in both packages. +tags: [markdown, editor, architecture] +lang: en +--- + +# Markdown and the editor + +Two packages share the job of understanding a page's content: +`packages/markdown` turns Markdown into sanitized HTML for reading; +`packages/editor` provides the ProseMirror schema behind the WYSIWYG +editor. They have to agree, because a page can be opened in either mode at +any time and must come back unchanged. + +## The Markdown pipeline + +`packages/markdown/src/`: + +- `parse.ts` / `render.ts` / `stringify.ts` — Markdown → AST → HTML, and + back to Markdown text. +- `frontmatter.ts` / `frontmatter-split.ts` / `frontmatter-metadata.ts` — + the YAML header every page carries. +- `alerts.ts` — the `> [!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` + callouts used throughout this space. +- `image-size.ts`, `youtube.ts` — extra constructs beyond plain CommonMark. +- `slug.ts`, `diff.ts`, `version.ts` — heading slugs, and support for page + version comparison. + +## The editor + +`packages/editor/src/`: + +- `extensions.ts` — assembles the ProseMirror schema. +- `nodes/alert.ts`, `nodes/image.ts`, `nodes/wiki-link.ts`, + `nodes/youtube-embed.ts` — one node per construct that needs editor + support beyond stock ProseMirror. +- `from-markdown.ts` / `to-markdown.ts` — the two directions of + conversion between editor document and Markdown text. + +## Constructs shared by both packages + +Wikilinks (`[[page-id]]`, `[[page-id|label]]`) and callouts are the two +constructs every page in this space uses, and both need a matching node in +`packages/editor` (`wiki-link.ts`, `alert.ts`) so that opening a page in +the WYSIWYG editor round-trips it correctly. + +## Adding a construct to both packages + +A new Markdown construct (say, a new kind of callout, or a new inline +element) needs changes in four places, in this order: + +1. **`packages/markdown`** — teach the parser to recognize the syntax and + the renderer to produce sanitized HTML for it. +2. **`packages/markdown`** — teach `stringify.ts` to produce the same + syntax back out, so a document that was never touched in the editor is + byte-for-byte stable. +3. **`packages/editor`** — add a node (or mark) under `nodes/` and wire it + into `extensions.ts`, then extend `from-markdown.ts` and + `to-markdown.ts` for the two conversion directions. +4. **Round-trip test** — Markdown → editor document → Markdown must + reproduce the original. This is the test that actually catches drift + between the two packages; skipping it is how "reading shows X, editing + shows Y" bugs get in. + +> [!WARNING] +> If the sanitizer (used by `packages/markdown`'s HTML output) doesn't know +> about the new construct's tags or attributes, it will silently strip +> them. This is exactly the failure mode the diagram sanitizer test guards +> against for `.drawio.svg`/`.excalidraw.svg` — the `content` attribute and +> wrapping text elements must survive sanitization, or diagrams lose their +> source and their full labels. + +See [[extending]] for the same recipe phrased as a short checklist, and +[[api]] for how the rendered result reaches the reader. diff --git a/principles/index.md b/principles/index.md new file mode 100644 index 0000000..e089708 --- /dev/null +++ b/principles/index.md @@ -0,0 +1,65 @@ +--- +id: principles +title: Principles +description: The four rules that shape f451's design, and why they exist. +tags: [architecture, principles] +lang: en +--- + +# Principles + +Four rules recur throughout the codebase. They are not style preferences — +each one closes off a class of bugs or design mistakes that the project has +already made once. + +## Git is the source of truth, Postgres is only an index + +Every space is a Git repository; every page is a Markdown file with +frontmatter. Postgres holds the rendered content, the search vector, the +knowledge-graph edges, sessions and encrypted provider tokens — nothing +that can be derived from Git is allowed to live *only* in the database. + +> [!IMPORTANT] +> Losing `pg-data` is recoverable: `POST /admin/reindex` or the drift job +> rebuilds the index from Git. Losing `forgejo-data` is not — that is the +> one thing backups exist for. If you find yourself adding a column that +> holds information not derivable from the Git content, stop and ask +> whether it belongs in Git instead. + +## Permissions come from the Git provider + +f451 has no role system of its own. Whether someone can see a space or +write to it is decided by their permissions on the underlying repository. +Writers always write with **their own** provider token — the API never +commits under a service account. Without a linked provider account, every +write attempt fails with 403; that is a precondition, not a bug. + +This also shapes how "not found" behaves: a `404` deliberately means +*"doesn't exist, or you don't have access"* — never distinguish the two. +Doing so would let an unauthorized caller learn that a page exists, which +is exactly the kind of existence oracle this design avoids. + +## Writing always goes through the review workflow + +There is no route that writes directly to the published version — not for +people, not for agents. The path is always: create a draft (a branch) → +write → open a review (a pull request) → approve (merge) → reindex. + +Draft routes carry a SHA contract: whoever writes names the `baseSha` of +the version they started from. If the content has changed underneath them, +they get a `409` back with the current state instead of a silent +overwrite. See [[api]] for the routes that implement this. + +## Diagrams carry their own source + +`.drawio.svg` and `.excalidraw.svg` files under `/_media/` are +plain SVGs that also carry the diagram's source XML in a `content` +attribute. One file is both the picture people see and the thing the +diagram editor reopens. The SVG sanitizer keeps that attribute and the +wrapping text elements deliberately — stripping either would make the +diagram unreadable or unopenable. + +> [!TIP] +> These four rules are the first thing to check when a design choice looks +> surprising: it is very likely a direct consequence of one of them, not an +> arbitrary decision. diff --git a/repository-layout/index.md b/repository-layout/index.md new file mode 100644 index 0000000..2296b57 --- /dev/null +++ b/repository-layout/index.md @@ -0,0 +1,44 @@ +--- +id: repository-layout +title: Repository layout +description: What lives where in the monorepo. +tags: [architecture, reference] +lang: en +--- + +# Repository layout + +A pnpm workspace with three applications and four packages, plus +deployment and operational tooling around them. + +## `apps/` + +| Directory | Contents | +|---|---| +| `apps/api` | Fastify server: route registration in `src/app.ts`, routes under `src/routes/`, the indexer under `src/indexer/`, draft/workflow logic under `src/drafts/`, auth under `src/auth/`. See [[api]]. | +| `apps/web` | Next.js App Router UI. Proxy configuration in `next.config.ts`, styles in `app/styles/*.css`, i18n messages in `lib/i18n/messages/`. See [[web-frontend]]. | +| `apps/mcp` | The stateless MCP↔HTTP translator. Tools live under `src/tools/` (`read.ts`, `write.ts`, `attachments.ts`), diagram generation under `src/diagram/`. | + +## `packages/` + +| Directory | Contents | +|---|---| +| `packages/markdown` | Parsing, rendering and stringifying Markdown (`parse.ts`, `render.ts`, `stringify.ts`), plus the shared constructs: `alerts.ts` (callouts), `frontmatter.ts`, `youtube.ts`, `image-size.ts`, `slug.ts`, `diff.ts`. | +| `packages/editor` | The ProseMirror schema: `extensions.ts`, `nodes/` (`alert.ts`, `image.ts`, `wiki-link.ts`, `youtube-embed.ts`), and the two directions of conversion, `from-markdown.ts` / `to-markdown.ts`. | +| `packages/design-tokens` | `tokens.ts` (generated values) and `catalog.ts` (one entry per token: level, settings-page group, role, whether a user theme may override it). | +| `packages/git-provider` | The `GitProvider` interface (`types.ts`) and its two implementations, `forgejo.ts` and `github.ts`. | + +## Everything else + +| Directory | Contents | +|---|---| +| `deploy/git` | Forgejo stack — must be started before `deploy/wiki`. | +| `deploy/wiki` | The wiki stack (web, api, drawio, mcp, Postgres). | +| `docs/` | Domain vocabulary, architecture decisions, agent-facing docs, design specs. | +| `scripts/` | Local setup (`dev-local-setup.sh`), deployment E2E, CSS metrics, seed content for the local demo space. | +| `.github/workflows/ci.yml` | The CI pipeline — see [[testing-and-contributing]]. | + +> [!TIP] +> When you're not sure which package owns a piece of behavior, start from +> [[architecture]]'s table of roles rather than guessing from file names — +> the boundaries are drawn by responsibility, not by technology. diff --git a/testing-and-contributing/index.md b/testing-and-contributing/index.md new file mode 100644 index 0000000..eb2854e --- /dev/null +++ b/testing-and-contributing/index.md @@ -0,0 +1,64 @@ +--- +id: testing-and-contributing +title: Testing and contributing +description: Targeted tests over full runs, what CI checks, and the license your contribution falls under. +tags: [testing, contributing, license] +lang: en +--- + +# Testing and contributing + +## Run targeted checks, not the full suite + +For a given change, run type-checking across the workspace and only the +test files of the modules you touched: + +``` +pnpm -w typecheck +pnpm --filter @f451/web test -- lib/urls.test.ts +``` + +Not the full `pnpm -w test` for every change — its output is the actual +cost driver on a change of any size, and it re-proves things your change +didn't touch. Full-suite runs and browser/E2E verification are what CI and +review are for, not every local iteration. + +## What CI checks + +`.github/workflows/ci.yml` runs on every push to `main`/`dev` and on every +pull request: + +``` +pnpm install --frozen-lockfile +pnpm typecheck +pnpm test +pnpm build +``` + +Two further jobs exist but only run on manual dispatch, because they are +expensive: a GitHub live-contract test against `packages/git-provider` +(needs real credentials), and a Playwright end-to-end read-flow test +(needs a browser install and container startup). + +## Before opening a pull request + +- Type-check, and run the tests for the files you changed (above). +- Keep the change to what the task asked for; unrelated cleanup belongs in + its own pull request. +- New code, comments, commit messages and documentation are written in + English. f451's UI itself stays bilingual — see [[web-frontend]] — but + written material *about* the project is English going forward. + +## License + +f451 is source-available, not Open Source in the OSI sense: noncommercial +use, modification and redistribution are free, with attribution to rote +code fraktion and a link to the project, both in the code and in the +running application's interface. Commercial use requires a separate +license on request. Full terms, including named exclusions, are in +`LICENSE.md` at the repository root — read it before assuming a use case +is covered. + +> [!NOTE] +> Contribution guidelines beyond this page live in `CONTRIBUTING.md` at the +> repository root. diff --git a/web-frontend/index.md b/web-frontend/index.md new file mode 100644 index 0000000..52bc124 --- /dev/null +++ b/web-frontend/index.md @@ -0,0 +1,59 @@ +--- +id: web-frontend +title: The web frontend +description: Routing, the proxy, internationalization, and styling with design tokens. +tags: [frontend, architecture] +lang: en +--- + +# The web frontend + +`apps/web` is a Next.js App Router application. Server Components are the +default; client islands are kept as small as the interaction requires +(theme toggle, search dialog, tree expansion are the reference examples). + +## The proxy + +`apps/web` is not a client of the API in the usual sense — it *proxies* +several path prefixes straight through, configured in `next.config.ts`: + +| Prefix | Destination | +|---|---| +| `/api/:path*` | `apps/api` | +| `/auth/:path*` | `apps/api` | +| `/admin/:path*` | `apps/api` | +| `/media/:path*` | `apps/api` | +| `/drawio/:path*` | the draw.io container | +| `/mcp` | the MCP service | + +Because these are rewrites, not client-side fetches, requests to them carry +the API's own response headers (its `onSend` security hook, or the media +route's sandboxed CSP) rather than anything `apps/web` adds — `headers()` +in `next.config.ts` only applies to responses Next.js generates itself. + +## Internationalization + +The interface is bilingual (German/English). UI strings live under +`apps/web/lib/i18n/messages/{de,en}/`. This is separate from page content +language: a page's own `lang` frontmatter field (as in this space, `en`) +describes the content, not the chrome around it. + +## Styling and design tokens + +`apps/web/app/globals.css` contains no rules of its own — only the list of +imports from `app/styles/*.css`. **That import order is the cascade**; the +numeric prefixes in the filenames are not what determines it. + +The token values themselves are generated, not hand-maintained: `sync-tokens` +runs automatically as `predev`/`prebuild` and produces the CSS variables +from `packages/design-tokens`. See [[markdown-and-editor]] for the +package's counterpart on the content side, and [[extending]] for the steps +to add a token. + +> [!NOTE] +> `packages/design-tokens/src/catalog.ts` separates *values* (`tokens.ts`) +> from *description* (`catalog.ts`: which settings-page group a token +> belongs to, its role in plain language, and whether a user theme is +> allowed to override it). Tests pin the count of tokens per level and +> group, so adding one means updating those counts deliberately, not +> incidentally.