Files
devplacepy/quality_software.md
T
retoorandClaude Sonnet 5.5 f55c7ee5f4
DevPlace CI / test (push) Failing after 31m42s
Add community node protocol spec, Agent Skills, and make the failing tests hermetic
Community nodes (Phase 1 of plan.md): the protocol specification in
services/nodes/spec.py as the single source of truth, GET /nodes/spec.json,
a generated protocol docs page, the Community nodes API docs group, and the
implementation plan plus the residential proxy research. The no-open-proxy rule
is enforced in the spec (open_proxy cannot be set).

Agent Skills: register the Skills API docs group, cascade skills on account
deletion, add the nested CLAUDE.md, README and routes docs, and cover the
parser, the fetcher (end to end against a mock origin), the data layer and the
routes with 73 unit and 16 api tests.

Tests made hermetic with the repo's existing patterns instead of ambient
environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the
workspace tests install FakeBackend via runtime.set_backend, and the skills
install tests use run_async. DockerCliBackend now returns 127 (command not
found) when the CLI is missing instead of raising FileNotFoundError, so a host
without docker answers with a clean error rather than a 500. The Providers tab
empty state no longer names the Model routes tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 07:13:25 +02:00

517 lines
26 KiB
Markdown

<!-- doc: quality_software.md | version: 1.2.1 | updated: 2026-10-11 -->
# Quality Software Playbook: How to Build a Site as Good as DevPlace
This is a generic, reusable method distilled from how DevPlace was built.
It is written so you can apply it to a completely different site, for
example a hosting company (accounts, products, orders, provisioning,
support tickets, status pages), and get the same stability and quality.
It is not about copying features. It is about copying structure,
constraints, and verification habits.
## 1. Core philosophy
1. Boring technology, smart edges. One backend language, one database
file, server-rendered HTML, plain JavaScript. Put complexity where
you can see it (request handlers, templates), keep storage dumb.
2. Fewer moving parts. Every dependency is a way to fail. Prefer the
standard library, one process type, one data root.
3. Server-rendered first. The page works without JavaScript. JavaScript
only upgrades live behavior (polling, websockets, optimistic UI).
4. One data source, many faces. Every domain object is served as HTML
for browsers, JSON for API clients, an agent/tool action for
automation, and a documented endpoint. Design the data once, then
expose all four.
5. Exact, minimal diffs. Edit existing files, reuse existing helpers,
match existing visual structure. Zero tolerance for a second way
of doing something that already has one way.
6. Measure, do not theorize. Performance claims come from runs on real
machines. If there is no measurement, say so.
7. Loud skips beat silent corruption. Reject unverifiable input with a
clear reason and a counter. Never store what you cannot vouch for.
Hosting example: a VPS order is one row in one table. The same handler
renders the order page (HTML), returns the order (JSON), exposes a
`order_create` automation action, and is documented in your API docs.
## 2. Reference stack (proven shape)
You can swap brands, but keep the shape:
- Backend: one typed web framework (DevPlace: Python + FastAPI).
- Templates: server-side templates with one shared instance
(DevPlace: Jinja2, single `templates` object imported everywhere).
- Frontend: no framework, no build step. Plain ES modules, one class
per file, small shared utilities (`Http`, `Poller`, modal, toast).
- Database: one embedded relational file with WAL mode for small to
medium scale (DevPlace: SQLite via `dataset`, WAL, busy timeout,
mmap). Synchronous local calls are fine when they are
sub-millisecond; do not make the data layer async for fashion.
- Auth: session cookie for browsers plus API key and Basic for
machines, resolved in one `get_current_user` choke point. No JWT
unless you need third-party verification.
- Static assets: content-addressed or boot-versioned URLs served
immutable (`/static/v<ts>/...`), one helper builds every URL.
- Background work: one in-process queue with inline fallback so tests
stay deterministic and production stays fast.
Hosting example: keep customers, services, invoices, tickets in one
SQLite/Postgres schema behind one data module. Serve the control
panel as HTML, the reseller API as JSON from the same handlers.
## 3. Repository layout that scales
Mirror URLs with code paths, one segment per directory:
- `routers/` mirrors the URL tree. Flat file for one resource
(`feed.py`), package directory for sub-resources (`projects/`).
Each leaf owns an `APIRouter`, the package `__init__.py` aggregates.
- `templates/` mirrors pages, extends one `base.html`. Page CSS in
`extra_head`, page JS in `extra_js`. Shared partials for repeated
UI (avatar link, user link, pagination, report button).
- `static/js/` mirrors behavior, one class per file, instantiated as
`app`. Shared utilities live once and are reused, never forked.
- `static/css/` uses design tokens, no literal colors in page files,
one closed breakpoint set, one z-index scale.
- `services/` holds background services behind one base class and one
manager (start/stop/status/logs). Each service owns its tables and
its admin controls.
- `tests/` mirrors the same tree by path: `tests/api/...` for HTTP,
`tests/e2e/...` for browser, `tests/unit/...` for pure logic.
Rule: if you cannot guess the file from the URL, the layout is wrong.
## 4. Single data root and single config module
All runtime artifacts live under one root (`DATA_DIR`), every path
derived from it in one config module, created at boot:
- Database file, uploads, attachments, generated archives, keys,
locks, backups, service state.
- Stored DB values are relative (`directory` + `stored_name`), so a
layout move never needs a DB rewrite.
- One `DATA_PATHS` registry lists every path; one `ensure_data_dirs()`
creates them before anything writes.
Hosting example: `data/` holds `app.db`, `uploads/`, `invoices/`,
`backups/`, `locks/`, `keys/`. Point `DATA_DIR` at a volume in prod.
## 5. Request pipeline (same for every route)
1. Single app factory: mount `/static`, register routers, install
middleware in fixed order (timing outermost, then security
headers, rate limit, maintenance gate, error handlers).
2. One auth resolution: cookie, API key header, Bearer, Basic,
all in `get_current_user`. Guards: public read
(`get_current_user`), member write (`require_user`), admin
(`require_admin`).
3. One response helper (`respond()`): same context renders HTML for
browsers and JSON for `Accept: application/json` via a response
schema. A context key missing from the schema is dropped from
JSON by design, so schemas are explicit contracts.
4. One validation style: Pydantic form models bound from forms,
one global validation-error handler (re-render or redirect).
5. One timing mechanism: outermost middleware stamps start time and
sets `X-Response-Time`. Never re-time per route.
6. Rate limit only mutations (`POST/PUT/PATCH/DELETE`), keyed by
real IP, tunable from settings. Reads stay fast and exempt.
## 6. Data conventions that prevent entire bug classes
- Primary keys are opaque string IDs (`uid`), slugs accept slug or
bare ID via one resolver. Slugs embed the random tail of the ID,
never a timestamp prefix (prefix sharding funnels writes).
- Soft delete everywhere user content is removable: `deleted_at` +
`deleted_by`, live rows are `deleted_at IS NULL`. Every insert
writes both columns, every list filters them. Only garbage
collection hard-deletes. One admin Trash restores/purges by event.
- Ownership rule: edits are owner-only, deletes are owner-or-admin,
checked by one helper. Admin seniority: junior admins cannot act
on senior admins.
- No N+1: batch helpers (`get_users_by_uids`, counts by ID lists,
paginators) are the only way to load related rows in lists.
- Idempotent schema init: ensure full column set before indexes,
because auto-schema tools create partial tables from first insert.
- Toggles revive, never duplicate: look up ignoring deleted flag,
stamp on off, clear on on, under one shared stamp for cascades.
Hosting example: canceling a server is a soft delete with one stamp
covering service, DNS rows, and scheduled jobs, restorable from Trash
until GC purges it.
## 7. Frontend rules that keep UI consistent
- One base template owns nav, head, modals, toasts, timing badge.
Nav active state is one global, never inline path checks.
- Never hardcode `/static/...`. One helper builds versioned URLs in
templates and one in JS.
- Server renders content that exists at request time (SEO, no-JS,
speed). Client pipeline is only for live content (chat bubbles,
streamed results), always sanitized fail-closed.
- Dates stored as UTC ISO, rendered as `<time data-dt>` and
reformatted to viewer timezone client-side, one format for users
(DevPlace: DD/MM/YYYY).
- Modals, lightbox, toasts, floating windows, scroll memory: one
implementation each, reused by class name. No inline JS.
## 8. Background jobs and services (one pattern)
- One generic fire-and-forget queue: `submit(fn, *args)`. When the
consumer runs, it defers; when it does not (tests, full queue),
it runs inline so behavior stays deterministic.
- One base service class: `run` loop, supervised start/stop, status,
logs, settings. One manager registers all services.
- One async-job pattern for slow work (archives, audits, reports):
enqueue (`POST .../run`), poll (`GET .../{uid}`), read report
(`GET .../{uid}/report`), expire and prune by CLI.
- Multi-worker safety: schema work behind one init lock, background
services elected by one service lock so exactly one worker owns
them. Never assume single-process in job code.
Hosting example: provisioning, backups, SSL renewal, and usage
metering are all job services with run/poll/report plus
`cli <domain> prune` retention commands.
## 9. Testing: three tiers, one tree, no skips
- Tiers by fixture, not by folder debate:
- `unit`: pure in-process, no HTTP, no browser.
- `api`: HTTP integration against a live test server + temp DB.
- `e2e`: Playwright browser, real user flows.
- Test server runs on its own port with its own temp DB and data
dir, background services disabled. One serial process, no
parallelism, so results are stable.
- Every run reports every failure (`-rf`), triage cheapest first:
unit+api (~minutes), then browser tier, then `--last-failed`
loop until empty.
- Required browser habits: explicit `domcontentloaded` on
navigations, visible-state waits over sleeps, scoped selectors,
global settings restored in `try/finally`.
- Gate before suite: clean app import, per-language syntax check,
linter on touched files (catches missing imports that compile
checks miss), no forbidden characters or patterns.
Quality bar: the full suite (all three tiers, every test) passes
before a change is done. A red test covering your change is the
requirement, not a stale artifact. Never narrow a run to make it
pass.
## 10. Extra verification for money, state, and races
Example-based tests are blind to wrong formulas at untried inputs,
long-sequence invariant breaks, and races. For spendable resources
(credits, coins, quotas), bounded state machines (tiers, levels),
or read-then-write mutations reachable from two requests, add four
disposable layers against a temp DB (not committed tests unless
asked):
1. Property checks over the full input domain (monotonicity,
bounds, non-negativity) across hundreds of values.
2. Stateful fuzzing: randomized action sequences across many
entities, invariants re-checked after every action.
3. Real-process concurrency: separate OS processes (not threads),
fresh production-like rows (never pre-zero NULL columns),
exact success counts and exact final totals. Fix races with one
atomic conditional `UPDATE ... WHERE <precondition>` checked by
real `rowcount` at the chokepoint.
4. Static analysis on every touched file (`pyflakes`/`ruff`), which
catches runtime `NameError` paths the other layers may miss.
Hosting example: apply this to credit spend, plan upgrades,
affiliate payouts, and concurrent provisioning of the last IP or
stock-limited node.
## 11. Operations: deploy, verify, prune
- One-command environments: `install`, `dev` (reload, one worker),
`prod` (multi-worker, no template reload, real backlog), `test*`,
`coverage`, load test. Every Python target uses the project venv.
- No bytecode in repo: `PYTHONDONTWRITEBYTECODE=1` for all targets,
`clean` removes venv and stray artifacts.
- CI runs the full suite serially on every push/PR, publishes
coverage and failure screenshots. CI green before merge.
- Production DB is never touched without explicit written
confirmation plus a two-factor token; disposable scripts always
target a scratch DB via env override.
- Retention is a feature: every expirable domain gets
`prune` (expired only) and `clear` (everything) CLI commands,
wired to the same GC the server uses.
- Backups are first-class: targets, worker, schedules, offload,
download restricted to the primary admin, prune missing files.
## 12. Security, moderation, and trust checklist
- Passwords hashed with a slow KDF (PBKDF2-SHA256), session tokens
random 256-bit, per-process TTL cache keyed by token.
- Ownership checked server-side on every mutation; deletes confirm
explicitly and audit; destructive automation actions require a
two-step confirm flag declared in their schema.
- Every user-visible content table is reportable by construction:
registry entry, object URL resolver, report button in the action
bar. Private-only tables are listed as unreportable with a reason,
enforced by a test that fails on unclassified tables.
- Outbound HTTP goes through one stealth/guarded client (consistent
TLS + headers + proxy + SSRF guard), never bare clients per call.
- Audit log is append-only, never raises into the caller, with one
catalogue of event keys. Admin-only read.
- Privacy docs and store declarations update in the same change as
any new personal-data collection or third-party transmission.
Hosting example: tickets, reviews, and public server comments are
reportable; invoices and API tokens are private-by-design with
written reasons; provisioning calls go through the one guarded
client with proxy support.
## 13. Docs, SEO, and API surface
- Every public/auth endpoint gets prose docs plus an interactive
API entry (params, auth, sample response). Public pages get SEO
context and JSON-LD; sitemap and robots are served, not static.
- One docs registry (`DOCS_PAGES`) owns prose pages; API docs own
endpoint entries. New route without docs is incomplete.
- README stays product-facing (routes, config, env vars, features).
Nested `CLAUDE.md` files hold subsystem mechanics next to the code
so the always-loaded root doc stays small.
- Version static assets, canonical URLs per listing, breadcrumbs,
related content, and one default OG image.
## 14. Development flow (the daily loop)
Every feature follows the same eight steps in data-flow order, and a
failed step blocks the rest (never skip ahead):
1. Understand. Read the router, template, matching tests, and the
subsystem doc. Trace input model to router to data helper to
response (HTML and JSON). Find a relatable existing example and
match it structurally and visually; if none exists, stop and agree
on the new pattern before building.
2. Data layer. Query/batch helpers first (never inline N+1), input
model, JSON response schema (every exposed key declared or it is
silently dropped), idempotent schema/index changes.
3. Server layer. Handler with the right guard (public/member/admin),
specific paths before catch-alls, HTML+JSON via one responder.
4. View layer. Extend base, reuse partials and globals, page CSS/JS
in the standard blocks, one ES class per file.
5. Safety layer. Reportable/private classification, ownership checks,
confirm gates, audit events, privacy-doc update if new personal
data or third-party transmission appears.
6. Agent + docs layer. Tool action per user-askable operation,
endpoint doc entry, prose page if user-facing, SEO context and
sitemap entry if indexable. This step is the most-forgotten; do
not skip it.
7. Document. Product README for user-visible change, subsystem doc
for new mechanics or pitfalls, root doc only for new global rules.
8. Validate. Clean import, per-language syntax check, linter on
touched files, full three-tier suite green (see section 9), plus
the money/state/race procedure where it applies.
Failure triage is cheapest-first: fast tiers (unit+api, minutes)
before browser tier, then re-run only failures (`--last-failed`)
until empty. One pass reports every failure; fix the list, not one
bug per pass.
Session rule: commit, deploy, verify in the same session, in that
order. A change that is not running in production is not done. After
deploys, verify live; green locally means nothing until prod
confirms it.
## 15. Workflows and automation
- One command surface. Every repeated action is a named target or CLI
command (`dev`, `prod`, `test`, `test-fast`, `test-failed`,
`coverage`, load test, `prune`, backups). No tribal shell
one-liners; if it is run twice, it gets a name.
- DTAP pipeline. Development (reload, one worker) to Test (CI serial
suite + coverage + failure screenshots, green before merge) to
production (multi-worker, no template reload, real backlog).
- Scheduled hygiene inside the system, not in heads: retention,
cleanup, quota resets, and health checks run on loops or CLI
commands with dry-run defaults and narrow scopes for destructive
ops. Destructive operations are dry-run first, explicitly
confirmed, date-guarded or allowlisted.
- Load testing is a target, not an event: scripted Locust run
against a scratch DB with fixed users, spawn rate, and run time,
producing an HTML report.
- Production guardrails are enforced, not remembered: a hook denies
commands reaching the live DB without explicit written
confirmation plus a second approval; disposable scripts always
target scratch databases via env override.
Hosting example: `provision`, `suspend`, `backup-run`,
`ssl-renew`, `usage-meter`, `invoice-prune` are all named commands
with `--dry-run`; nightly loops handle expiry, renewal, and status
checks with human-readable pressure states.
## 16. Agents: in-product assistant, services, and coding help
DevPlace quality comes partly from treating agents as first-class
consumers of the same routes humans use:
- In-product assistant (DevPlace: Devii). A conversational operator
over the real API: per-owner sessions and channels (web, terminal,
chat bridge), a tool catalog where each tool mirrors one route
with matching auth, confirmation gates on destructive tools (first
call refused, agent shows exact target, second call passes
`confirm=true`), per-user cost attribution and quotas through one
AI gateway, and usage/ledger pages per user. New feature rule: if
a user could ask the assistant to do it, it gets a tool.
- Gateway pattern. Every AI/model call flows through one internal
gateway (usage ledger, provider/model routing, per-call cost
headers), so spend is attributable and every consumer (correction,
modifier, grading, chat) shares quotas and audit.
- Background service agents. Crawlers, importers, reconcilers, and
persona bots run as supervised services under one manager with
start/stop/status/logs and admin pages (DevPlace: news importer,
bot fleet with live monitor, container reconciler, audit writer).
Exactly one worker owns them via a service lock.
- Coding-agent support. Nested subsystem docs next to the code so an
agent loads only what it touches; strict conventions (no second
implementation, named helpers, closed token/breakpoint/z-index
sets) so generated code matches; tool catalog and docs registry
so the agent can act and document through the same contracts.
- Human authority. Agents propose and execute reversible work;
destructive, cross-user, or policy-crossing actions need explicit
human confirmation, seniority checks where roles exist, and an
audit trail. When a floor is hit (retention minimum, protected
path), the agent holds and pages a human instead of deleting.
Hosting example: a support assistant with tools like `server_reboot`
(public read tools open, mutating tools auth-gated, destructive
tools confirm-gated), all spend metered per customer through your
gateway, provisioning and renewal running as supervised services.
## 17. Documenting: what lives where, and when
Documentation is layered by audience and distance from the code, and
every layer updates in the same change as the code it describes:
- Source code: no comments, no docstrings. Code is self-documenting
through names, small helpers, and one obvious way per task. A
decision worth recording goes in the change description or the
subsystem doc, never as deliberation text in the source.
- Subsystem docs (DevPlace: nested `CLAUDE.md` next to the code).
Each directory that owns a mechanic holds its own doc: helpers,
pitfalls, cross-layer wiring, worked examples. An agent or human
loads only the docs for the area being touched, so the always-
loaded cost stays flat as the project grows.
- Root doc (DevPlace: `CLAUDE.md`, `ARCHITECTURE.md`). Global rules,
commands, and architecture only. A per-feature detail never lands
here; a new global rule always does. The architecture atlas is
measured from the running app and schema (counts, diagrams), not
written from memory, and disagreements between docs and code are
recorded explicitly.
- Product README. User-visible surface: routes, config, env vars,
features, quick start. Updated when behavior a user can see
changes.
- User docs site (DevPlace: `/docs` with a `DOCS_PAGES` registry).
Prose pages per audience tier plus an interactive API reference
where every endpoint entry carries params, auth, and a sample
response. A new public or auth endpoint without a doc entry is
incomplete by definition.
- Privacy and store declarations. Any new personal-data category or
third-party transmission updates the privacy doc and platform
declarations in the same change, never as a follow-up.
- Event and reason registries. Audit event keys, report reasons, and
similar closed sets live in one catalogue each, so every client
and test renders the same list.
Rule: docs state what the system does today, with gaps listed next
to the code that will close them. No fiction, no roadmap prose.
Hosting example: `billing/CLAUDE.md` documents proration and retry
rules next to the billing code; the root doc only says billing
exists and which commands run it; `/docs` gains a prose page plus
one API entry per new endpoint in the same change.
## 18. Roles, permissions, and ownership
One small role set, enforced server-side at every surface (web UI,
REST API, and agent tools alike):
- Roles (DevPlace: guest, member, admin, primary admin). Stored as
exact strings, tested through one `is_admin()` helper, never
hand-rolled compares. Any write to a role invalidates the cached
role lookups.
- Guests read public pages; members write their own content;
admins moderate and operate. Public reads resolve the viewer but
never require login; every write names its guard explicitly.
- Ownership first: edits are owner-only, deletes are owner-or-admin,
checked by one helper comparing resource owner to actor. Admins
can remove abuse but cannot rewrite someone else's words.
- Seniority among admins: a junior admin (later registration) cannot
mutate a more senior admin's account or content. Every per-user
admin mutation is gated by one seniority predicate, denials are
audited, and the gate is server-side so it also covers agent
tools. One primary admin (the first admin account) is the single
exception with full visibility and full control, including
private-by-admin content and restricted downloads.
- Visibility predicates are single functions, never inline checks:
e.g. `can_view(project, user)` decides private-content access in
one place (owner sees own, admins see members' private rows, but
an admin's own private rows stay visible only to that admin
except for the primary admin). Container and workspace isolation
get their own stricter predicates on the same pattern.
- Machines authenticate as users: every account holds an API key,
plus issuable scoped tokens; the same guards apply regardless of
credential type, so the REST API cannot do what the UI forbids.
- Agents act as the asking user: tool auth mirrors route guards,
destructive tools add confirmation gates, and every admin tool
passes through the same seniority and audit path as the web UI.
Rule: if a permission check appears twice in different words, one
of them is a bug. Hoist it into the single predicate and call it
from both places.
Hosting example: guest (pricing, status), customer (own servers and
invoices), support (view customers' servers, restart but not delete;
junior support cannot touch senior staff accounts), owner (full
access plus backup downloads and billing refunds). One
`can_view_server` / `can_manage_server` pair gates panel, API, and
assistant tools together.
## 19. Replication checklist for your hosting site
Use this order; it is the data-flow order DevPlace follows:
1. Scaffold: app factory, config with one data root, shared
templates, response helper, auth choke point, middleware stack.
2. Data: users, sessions, settings, then one domain (products).
Add soft-delete columns, batch helpers, idempotent init.
3. Server: public reads first (HTML+JSON via one handler), then
member writes with guards, then admin views.
4. View: base template, tokens, partials, one JS utility set.
Server-render listings and detail pages.
5. Safety: reportable/private classification, ownership checks,
confirm gates, audit events.
6. Automation + docs: tool action per user-askable operation,
API doc entry per endpoint, SEO context per public page.
7. Verification: import gate, lint touched files, full three-tier
suite green, plus the four-layer money/state/race procedure
where it applies.
8. Ops: venv targets, CI serial suite, prune/clear CLIs, backups,
production-DB guard, deploy then verify live.
## 20. What to copy literally vs adapt
Copy literally: single data root, soft-delete columns and filters,
batch loaders, one response helper, one auth resolver, test tiers
and temp-DB server fixture, prune/clear per domain, audit-never-
raises, versioned static URLs.
Adapt to hosting: table list (customers, plans, servers, invoices,
tickets, status incidents), provisioning job services, payment and
registrar integrations behind the one outbound client, usage
metering and quota rules, status page and notifications.
If you keep the constraints and the verification loop, the domain
can change completely and the quality transfers with it.
## Doc history
| Version | Date | Change |
|---------|------|--------|
| 1.0.0 | 2026-10-11 | Created: philosophy, stack, layout, data, pipeline, frontend, jobs, testing, verification, ops, security, docs, checklist. |
| 1.1.0 | 2026-10-11 | Added development flow, workflows/automation, and agents sections. |
| 1.2.0 | 2026-10-11 | Added documenting and roles/permissions sections. |
| 1.2.1 | 2026-10-11 | Added doc version header and history per quality_versioning.md. |