Project structure — a file-by-file map
This file is the map of the repository: what every directory and file is for, what depends on it, in what order server.js wires the pieces together and why that
This file is the map of the repository: what every directory and file is for, what depends on it, in what order server.js wires the pieces together and why that order matters, and where a new feature belongs. Everything here was read from the source on 2026-09-30; counts in the tables are the real ones.
The top level
swiftpoint-hub/
server.js Express app, entry point ("main": "server.js")
package.json 9 runtime dependencies, no devDependencies, no build step
.env / .env.example configuration; .env is git-ignored
routes/ HTTP: one router per surface
lib/ plain data and rendering, no Express and no session
public/ 12 static HTML pages, js/ (27 files), css/ (15 files), oled/
docs/ the guides, as markdown, plus the recorded walkthroughs
test/ 47 test files on node:test, plus two DOM helpers
scripts/ one generator: sync-header.js
data/ the SQLite file (git-ignored)
Other things in the root that are inputs rather than code: z3.md (the Z3 research note the guides were written from), z3-profile-viewer.html and assets/ (the reference the checker was built against), seo-review.json (a one-off audit record), google6a47ec6ec477047a.html (the Search Console verification token), and swiftpoint-hub-old/ (a previous version of the app, kept but not loaded).
There is no build step and no bundler. npm install, npm start. npm test runs check:header first and then node --test. The whole dependency list is hand-picked and the code comments say so repeatedly — see lib/spcfParser.js and lib/session-store.js, which each explain why a package was not added.
The runtime dependencies
| Package | Version | Why it is here |
|---|---|---|
express |
^4.19.2 |
The HTTP layer and the router. |
better-sqlite3 |
^11.3.0 |
The database. Synchronous, which is why the data modules are synchronous too. WAL and foreign_keys are set at open (lib/db.js). |
express-session |
^1.18.0 |
Sessions, with a custom store rather than the default memory store. |
markdown-it |
^15.0.2 |
Both markdown engines are built on it. |
markdown-it-container |
^4.0.0 |
The ::: blocks. |
highlight.js |
^11.12.0 |
Server-side syntax highlighting for fenced code. |
multer |
^2.0.0 |
The one multipart endpoint: the profile upload. 2 MB cap, memory storage. |
dotenv |
^16.4.5 |
Loads .env at the top of server.js. |
@napi-rs/canvas |
^1.0.5 |
Server-side PNG rendering: the OpenGraph cards, the per-guide cards and /profiles/:id/share.png. |
routes/ — HTTP and permissions
Nothing in routes/ reads process.env for anything except the few base-URL constants, and none of them talks to the filesystem except where it has to. Each router is mounted in server.js and exports either the router or, for two of them, a router plus a small set of helpers.
| File | Mount point | What it does | Depends on |
|---|---|---|---|
routes/auth.js |
/api/auth |
Discord OAuth2 login and callback, logout, /me, and the three guards requireUser, requireModerator, requireAdmin plus safeReturnTo. |
lib/db, lib/admin, lib/notify, lib/paths |
routes/profiles.js |
/api/profiles |
Search, sort, filter, tag and game lists, stats, one profile, download, vote, upload, edit, replace the file, comments. Re-exports groupsForRow from lib/profile-groups. |
lib/db, lib/spcfParser, lib/profile-groups, lib/notify, lib/settings, lib/games, routes/auth |
routes/notifications.js |
/api/notifications |
The list, the unread count, the SSE stream, and marking read. | lib/db, lib/events |
routes/reports.js |
/api/reports |
Filing a report (any signed-in user) and the report queue (admin). | lib/db, lib/notify, routes/auth |
routes/admin.js |
/api/admin |
Overview statistics, settings, staff CRUD, profile transfer, and mounts docs-write.js as a sub-router. |
lib/db, lib/settings, lib/admin, lib/notify, lib/analytics, lib/docs/write, routes/docs-write |
routes/users.js |
/api/users |
One public member profile and the bio editor. | lib/db, routes/auth |
routes/forum.js |
/api/forum |
Categories, threads, posts, edit/delete, pin/lock/move, ticket status, the ticket queue, preview, subscriptions, search, and the updates endpoints. | lib/db, lib/forum, lib/updates, lib/markdown, lib/notify, routes/auth |
routes/records.js |
/api/records |
The recordings CRUD, publish and unpublish. Every route is requireAdmin. |
lib/records, routes/auth |
routes/docs.js |
BASE_PATH (root) |
The guides site: /docs, /docs/<slug>, guide images, /docs/og/<slug>.png, .rec.json files, and the JSON endpoints /api/docs-tree, /api/guides, /api/docs-graph, /api/docs/<slug>/raw, /api/docs/<slug>/discord. |
lib/docs (the engine), lib/docs-og, lib/records, lib/analytics, lib/header |
routes/docs-write.js |
inside /api/admin |
The guides editor API: list, create folder, create document, save, rename, delete, and preview. Every route individually requireAdmin. |
lib/docs/write, lib/docs/index, routes/auth |
routes/seo.js |
BASE_PATH (root) |
Every HTML page, the OpenGraph images, robots.txt, sitemap.xml, the four UMD module routes (/js/spcfParser.js, which concatenates lib/nesting.js ahead of itself, plus /js/nesting.js, /js/spcfWriter.js and /js/step-diff.js), and serveShell. |
lib/db, lib/analytics, lib/share-image, lib/og-card, lib/header, lib/footer, lib/paths, lib/legal, lib/forum, lib/updates, lib/docs |
lib/ — data and rendering
The rule the modules follow: no Express, no session, plain data in and out. lib/forum.js and lib/updates.js say so in their own headers, and the tests drive them directly instead of over HTTP.
| File | What it is for | Depends on | Used by |
|---|---|---|---|
lib/db.js |
Opens the SQLite file (creating the directory), sets journal_mode = WAL and foreign_keys = ON, and runs the whole schema: users, profiles, comments, notifications, reports, pageviews, votes, settings, forum_categories, forum_threads, forum_posts, forum_ticket_events, forum_subscriptions, updates, staff, recordings. |
better-sqlite3 |
Almost everything. |
lib/session-store.js |
A four-method express-session Store over the same SQLite file. Sweeps expired rows on construction and every 100 writes. |
lib/db |
server.js |
lib/admin.js |
The single place the staff allowlists are read. .env is the stronger claim; the staff table is the weaker one. Exposes isAdminDiscordId, isModeratorDiscordId, listStaff, addStaff, setStaffRole, removeStaff, knownPeople, reload. |
lib/db (lazily) |
routes/auth, routes/admin, lib/notify, routes/seo |
lib/paths.js |
BASE_PATH and GALLERY_PATH (/profiles/hub), plus galleryPath(). |
— | routes/seo, lib/header, lib/footer, routes/auth |
lib/header.js |
The site header, generated once: row 1 with the nav, row 2 the per-page tool strip. Exports headerHtml, NAV, START, END, SUPPORT_URL. |
lib/paths |
scripts/sync-header.js, routes/docs.js, routes/seo.js |
lib/footer.js |
The shared footer, the link list and the disclaimer note. | lib/paths |
scripts/sync-header.js, routes/seo.js |
lib/legal.js |
/privacy and /imprint, generated from the implementation so the claims are checkable. |
lib/db |
routes/seo.js |
lib/analytics.js |
First-party pageview counter. trackPageView(ip, path) HMAC-hashes the IP; trafficOverview() returns 1 h / 24 h / 7 d / 30 d windows. Prunes rows older than 30 days every 200 inserts. |
crypto, lib/db |
routes/seo, routes/docs, routes/admin |
lib/events.js |
In-process pub/sub for the SSE notification stream: subscribe, unsubscribe, publish, keyed by user id. |
— | lib/notify, routes/notifications |
lib/notify.js |
Writes notification rows and publishes them. notifyAdmins, notifyStaff, notifyUser, notifyMany, isAdminUser. |
lib/db, lib/events, lib/admin |
routes/profiles, routes/forum, routes/admin, routes/reports, routes/auth |
lib/spcfParser.js |
UMD. Reads a .spcf and produces the display model: groups, sub-groups, device settings from the profileChanged system event, button mappings, tilt mappings, the input-nesting tree (nesting, keyed by raw ActionMappings index) and OLED frames. Enforces MAX_GROUPS 100, MAX_MAPPINGS 1000, MAX_NESTING 20. Also exports describeTrigger, extractProfileSettings, buttonLabel and indexInList — the last is the only sanctioned raw-index → display-list bridge. |
Buffer.from(value, 'base64') and lib/nesting |
routes/profiles, routes/seo, lib/share-image, lib/profile-groups, public/js/check.js and public/js/editor.js via /js/spcfParser.js |
lib/nesting.js |
UMD, zero dependencies. The nesting core: buildTreeFromValues, walk, flatten, validate, pathToRoot, isSingleButtonStack, the mutation helpers insertChild / removeNode / moveNode, and MAX_NEST_DEPTH = 20. Shape-agnostic (a count plus a parent-index callback), so the parser and the Builder share it. Zero dependencies is a hard requirement: routes/seo.js serves the file verbatim to the browser. Defensive by contract — a dangling or self-referential link makes that node a root, a cycle is detached, and every index is visited exactly once. |
— | lib/spcfParser, public/js/editor.js, test/nesting-tree.test.js |
lib/profile-groups.js |
groupsForRow(row) — the groups a stored profiles row is rendered from. parsed_summary is a cache of JSON.stringify(parseSpcf(file).groups) and all 48 rows predate the nesting shape, so a cached copy without a nesting block is re-parsed from raw_json. Never throws: an unparseable body degrades to the row as stored. |
lib/spcfParser |
routes/profiles.js (re-exported as router.groupsForRow), lib/share-image.js |
lib/spcfWriter.js |
UMD. The other direction: builds the raw .NET DataContract JSON nodes the Control Panel writes, including the $type class-name catalogue. applyNesting(values) regenerates the recursive Predecessor copies from PredecessorId; it is verified byte-identical on all 60 groups of all 48 stored profiles. |
— | public/js/editor.js via /js/spcfWriter.js, and the tests |
lib/step-diff.js |
UMD. Semantic diff between two .spcf documents, phrased in the guides' own vocabulary. Caps at CHANGES_PER_STEP = 2. |
public/js/export-helpers.js, lib/spcfParser.js |
public/js/recorder.js via /js/step-diff.js |
lib/markdown.js |
The forum markdown engine. html: false, a URL scheme allowlist, rel="noopener nofollow ugc" on external links, a fr-* class prefix, and the ::: blocks: callouts, changelog entries, spoiler, steps, video. CHANGELOG (added/changed/fixed/removed/security) is exported and imported by the docs engine. |
markdown-it, markdown-it-container, highlight.js |
lib/forum.js, lib/updates.js, routes/forum.js |
lib/docs/index.js |
The guides engine. Front matter (title, description, tags, order), wikilinks, relative links and images, Obsidian callouts rewritten into :::, tabs, steps, video, spoiler, changelog blocks, heading anchors, a TOC, outgoing/incoming link sets, graphData(), apiTree(), tagsStats(), toDiscord(). Owns DOCS_ROOT = docs and siteUrl(). |
fs, markdown-it, markdown-it-container, highlight.js, lib/records, lib/markdown (for CHANGELOG) |
routes/docs.js, routes/docs-write.js, routes/seo.js |
lib/docs/write.js |
Writing guides to the repository. absFor() is the one path guard and every operation goes through it. Front matter is preserved, writes are temp-file + rename, and folder deletion asks first (409 with a file count). Caps: name 60, title 140, description 300, body 200 000, tags 12. |
fs, path, lib/docs/index.js |
routes/docs-write.js |
lib/docs-og.js |
The 1200×630 per-guide OpenGraph card. | @napi-rs/canvas, lib/docs |
routes/docs.js |
lib/og-card.js |
The site's own social cards: homeCard, galleryCard, toolCard, userCard, and WIDTH/HEIGHT. |
@napi-rs/canvas |
routes/seo.js |
lib/share-image.js |
Server-side render of /profiles/:id/share.png, reusing the same UMD modules the browser uses, so the served image and the in-page Share download are identical. Reads its groups through lib/profile-groups.js, so a stored parsed_summary from before the nesting rework still renders a tree. |
@napi-rs/canvas, public/js/export-helpers.js, public/js/share-render.js, lib/profile-groups, lib/db |
routes/seo.js |
lib/forum.js |
The forum data layer: categories, threads, posts, subscriptions, the ticket statuses and the trail. STATUSES is the canonical list. |
lib/db, lib/markdown, lib/records |
routes/forum.js, routes/seo.js |
lib/updates.js |
The updates data layer. KINDS (website, mouse) and ACCENTS (lime, blue, teal, violet, orange, critical). |
lib/db, lib/markdown |
routes/forum.js, routes/seo.js |
lib/games.js |
The canonical game taxonomy: canonicalGame(), and migrate(), which rewrites stored spellings at boot. |
lib/db |
routes/profiles.js, server.js |
lib/records.js |
The recordings data layer. Validates a .rec.json payload, keeps the document in the recordings table, and maps recordings/<slug>.rec.json to a slug. MAX_BYTES 50 MB because the screenshots are data URLs. |
fs, path, lib/db |
routes/records.js, routes/docs.js, lib/docs/index.js, lib/forum.js |
lib/settings.js |
Two functions over the settings table: getSetting(key, fallback) and setSetting(key, value). |
lib/db |
routes/profiles.js, routes/admin.js |
public/ — the front end
Twelve HTML files, each of which is a shell: the server rewrites the title, the description, the canonical and og:* tags, and the client script fills in the data. None of them contains an inline script.
| Page file | Served at | Rendered by | Stylesheets | Client script |
|---|---|---|---|---|
index.html |
/ |
routes/seo.js |
tokens, base, hdr, ui, home-beta |
theme.js, chrome.js, hero.js |
gallery.html |
/profiles/hub |
routes/seo.js |
+ gallery |
app.js, auth.js, card.js |
profile-beta.html |
/profiles/:id |
routes/seo.js |
+ profile-beta |
profile-beta.js, share-render.js, export.js |
user.html |
/users/:id |
routes/seo.js |
+ user |
user.js |
forum.html |
/forum |
routes/seo.js |
+ forum |
forum-common.js, forum.js |
thread.html |
/forum/t/:id |
routes/seo.js |
+ forum |
forum-common.js, thread.js, steps-player.js |
updates.html |
/updates |
routes/seo.js |
+ updates |
forum-common.js, updates.js |
check.html |
/check |
routes/seo.js |
+ check |
check.js, export-helpers.js |
editor.html |
/editor |
routes/seo.js (adds data-can-record) |
+ editor |
spcfParser.js, spcfWriter.js, step-diff.js, editor.js, recorder.js (admin), steps-player.js |
oled.html |
/oled |
routes/seo.js |
+ oled/oled-editor |
oled/oled-editor.js, oled/spcf-inject.js |
records.html |
/records |
routes/seo.js (admin only) |
+ records |
records.js, steps-player.js |
admin.html |
/admin |
routes/seo.js (admin only) |
+ admin |
admin.js, admin-guides.js, markdown-editor.js |
public/js/ — 27 classic scripts, all IIFEs or UMD modules, none of them a module system:
| File | What it is for |
|---|---|
theme.js |
The theme switch. A separate file loaded without defer so data-theme is set before first paint. |
chrome.js |
Header behaviour: theme, mobile drawer, buildFooter() as a fallback for any page not yet synced. |
auth.js |
The one place the client asks about the session. Exports SPHAuth; four page scripts used to each carry their own copy. |
app.js |
The gallery: load, search, sort, filter, cards, upload. |
card.js |
The parts of a profile card both the gallery and the dashboard need: game colour, DPI range, big numbers, avatar fallback. |
profile-beta.js |
The profile detail page: section nav with scroll-spy, layer switcher, live binding filter, votes, comments, reports. |
user.js |
The member page. |
forum-common.js |
Fetch wrapper, HTML escaper, toasts, modals, "3 minutes ago" — shared by the three forum-family pages. |
forum.js |
The forum index. |
thread.js |
One thread: posts, composer, ticket status buttons, trail, pin/lock/move/delete. |
updates.js |
The updates list and its kind filter. |
markdown-editor.js |
The one markdown editor: new thread, reply, post edit, updates form. Toolbar, preview, localStorage draft. |
notifications.js |
The unread badge and the toast feed. Also polls the unread count every 30 s. |
check.js |
The client-side .spcf viewer. Parses with the shared parser; "the file never leaves the device". |
editor.js |
The Profile Builder. The largest file in the repository. Three panels, the mouse diagram, the binding composer, the layers rail, the localStorage draft. |
recorder.js |
The walkthrough recorder. Hangs off window.EDHooks; gated on document.body.dataset.canRecord. |
records.js |
The recordings admin page: list, edit, delete, publish, embed. |
steps-player.js |
The ::: steps player. Self-contained: it fetches the .rec.json behind the box it is in. Used by the guides and the forum. |
export-helpers.js |
UMD. Pure helpers for describing profile data, shared by Node (share images) and the browser. |
export.js |
UMD. The clean Markdown listing of a profile's bindings, for the download and the clipboard. |
share-render.js |
UMD. The canvas renderer for the share image, identical in Node and the browser. |
hero.js |
The front page's 64×32 OLED marquee preview and the scroll reveal. Both decorative. |
docs.js |
The guides page: tree collapse, search, outline scroll-spy, lightbox, mobile drawer, keyboard shortcuts. The page itself is server-rendered. |
docs-graph.js |
The notes map. |
video-player.js |
The chapter chips and ranges for a ::: video block. |
admin.js |
The admin panel: statistics, settings, staff, profiles, reports. |
admin-guides.js |
The guides editor inside the panel. Writes files; keeps no copy. |
public/oled/ — the OLED editor lives in its own directory with its own stylesheet:
| File | What it is for |
|---|---|
oled/css/oled-editor.css |
The full-viewport app shell for the pixel editor. |
oled/js/oled-editor.js |
The editor itself: 64×32, 1-bit, frames, layers, keyframes, onion skin, text, symbols, undo, and every export. |
oled/js/spcf-inject.js |
Takes one of your own .spcf files, attaches the current animation as a chain of oledFlash actions, and hands the file back. Entirely client-side. |
public/css/ — 15 stylesheets, layered in a fixed order, and test/ui-css-coverage.test.js fails if a page uses a class that a stylesheet it does not load does not style:
| File | Layer | What it owns |
|---|---|---|
tokens.css |
1 | Design tokens, single source of truth, loaded first on every page. |
base.css |
2 | Reset, element defaults, layout primitives, accessibility defaults. |
hdr.css |
3 | The shared site chrome: header rows, nav, mobile drawer. Nothing else may restyle it. |
ui.css |
4 | The component kit: buttons, fields, cards, chips, tabs, tables, empty states, toasts. |
gallery.css |
page | The gallery layout. |
home-beta.css |
page | The front page. |
profile-beta.css |
page | The profile detail layout: identity sidebar, section nav. |
forum.css |
page | The forum index, the thread page, and .st-<status> colours. |
updates.css |
page | The updates list, and .k-<kind> colours. |
check.css |
page | The checker. |
editor.css |
page | The Profile Builder shell and workspace. |
records.css |
page | The recordings page. |
admin.css |
page | The admin panel. |
user.css |
page | The member page. |
docs.css |
page | The guides viewer: explorer, article, outline rail, and the d-* block classes. |
oled/css/oled-editor.css |
page | The OLED editor (loaded from its own directory). |
docs/, test/ and scripts/
docs/ is the guides tree the site serves, and it is the whole of it — there is no second level in between any more. Four things live in it:
| Path | What it is | Served at |
|---|---|---|
docs/profile-builder/ |
Eleven markdown files: README.md plus the ten Player guides |
/docs/profile-builder/<slug> |
docs/testing.md |
One guide at the root, with the new-profile recording in it | /docs/testing |
docs/recordings/ |
Every .rec.json there is, and nothing else |
/docs/recordings/<slug>.rec.json |
docs/about/ |
The twenty-five files you are reading now | /docs/about/<slug> — closed, see below |
There is no docs/wiki/; the tree used to be nested inside it and the URLs never were, so the folder only ever added a level that had to be argued about. recordings/ is at the root rather than inside a guide's folder because a recording is a shared asset, not something a guide owns: two guides may embed the same walkthrough, and it must not live in either of them. See Walkthrough recorder.
docs/about/ is part of the served tree — it is inside DOCS_ROOT, it renders, and it has addresses — but a doc_visibility row closes the folder to staff, so a public reader gets the same "no such page" answer as for a file that is not there. See 15 Auth and roles. Everything in docs/ is owned by the repository, edited by hand or from the admin panel, and rendered by lib/docs/index.js.
test/ is 47 files on node:test plus two helpers (test/helpers/editor-dom.mjs, a stand-in DOM good enough to boot the markdown editor, and test/helpers/md-dom.js, the equivalent for the markdown blocks). They fall into: the HTTP surfaces (forum, docs-api, docs-write, auth-roundtrip, records, staff, admin-access), the pure modules (game-taxonomy, step-diff, blocks, session-store, spcf-format, profile-format), the front end against a fake DOM (editor-boot, editor-draft, editor-first-run, editor-flow, editor-preview, dom-contract, script-order, ui-shape, video-player, docs-player), the chrome and the CSS (header, footer, mobile-header, ui-css-coverage, gallery-links, front-page, forum-pages, forum-render, docs-tags, docs-empty-folders, notes-map, legal-pages, ui-language), and the cross-cutting invariants (http-headers, limits, robots, social-card, profile-seo, public-api-privacy, step-titles, auth, docs-slug, docs-steps). 22 Testing covers what each group protects.
scripts/sync-header.js is the only generator. npm run sync:header writes the header row 1 and the footer into the twelve static pages; npm run check:header fails if any page is stale, and pretest runs it. The tool strip (.tbar) is authored in each page and left alone.
The mount order in server.js, and why it matters
1 express.json({ limit: '64mb' })
2 express-session cookie `swiftpoint.sid`, 12 h, SQLite store
3 security headers CSP, nosniff, X-Frame-Options, Referrer-Policy, HSTS
4 cache-control by file extension, unless a route already set one
-- lib/games.migrate() runs here, before the first request
5 /api/auth
6 /api/profiles
7 /api/notifications
8 /api/reports
9 /api/admin also mounts routes/docs-write.js → /api/admin/docs/…
10 /api/users
11 /api/forum
12 /api/records
13 routes/docs.js /docs and everything under it
14 routes/seo.js every HTML page, the OG images, robots, sitemap
15 express.static(public)
16 error handler LIMIT_FILE_SIZE → 413, entity.too.large → 413
Four reasons the order is load-bearing:
- The API mounts come before the page mounts on purpose.
/api/*never falls through to a page. The64 MBJSON limit is set first so a recording POST with per-step screenshot data URLs is parsed at all, and it is large for that reason and no other. /profiles/hubis registered above/profiles/:idinroutes/seo.js, so the word "hub" is not read as a profile id.lib/paths.jsholds the constant because the redirect for a signed-in non-admin, the sitemap, the header, the footer and the client scripts all have to agree on it.routes/docs.jsregisters its specific patterns before the generic one:/docs/og/<slug>.pngis matched before the image matcher, and the.rec.jsonroute is registered before the catch-all/docs/(.+)— otherwise a recording would be resolved as a guide slug. TheserveShellbehaviour has to be in place beforeexpress.staticso a generated page is never shadowed by a same-named file, and/oled.htmlredirects to/oledfor the same reason./api/adminis one surface with one gate, anddocs-write.jsis mounted inside it rather than separately.test/admin-access.test.jsreadsroutes/admin.jsand asserts that every admin route is behindrequireAdmin; a sub-router the check cannot see is a sub-router nobody is checking. Each route indocs-write.jstherefore carries its ownrequireAdmin— it writes files, and one of them deletes them.
The boot-time lib/games.migrate() sits between the API mounts and the page mounts so the canonical spellings are in place before any request can read them. It is idempotent and logs Canonicalised N of M game names when it changed something.
Where a new feature goes
| If you are adding… | Put it in | And do not forget |
|---|---|---|
| An HTTP endpoint | routes/<surface>.js |
The guard: requireUser, requireModerator or requireAdmin, from routes/auth.js. |
| A query, a rule or a renderer | lib/<concern>.js |
Keep it free of Express and the session so the tests can drive it directly. |
| A new table or column | lib/db.js |
CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS, so an existing database picks it up on boot. |
| A page | public/<name>.html |
A route in routes/seo.js that serves it, plus npm run sync:header if it needs the shared header or footer. |
| A style | public/css/<page>.css |
And check the page actually loads it — ui-css-coverage.test.js enforces that. |
| A guide | docs/<folder>/<slug>.md |
Front matter with at least title and description; order decides the reading order, tags make it filterable. |
| A recording | docs/recordings/<slug>.rec.json |
Published from /records, not written by hand. Every guide's ::: steps resolves into that one directory. |
| A test | test/<thing>.test.js |
node --test picks it up with no registration. |
npm run sync:header after editing a static page is not optional: the header and the footer are generated, and npm test fails when a page has drifted.
Adding a change without breaking things
- Run
npm testbefore and after.pretestrunscheck:header, so a page whose header is stale fails before your test even starts. - Do not add an inline script or an
onclick=.script-src 'self'means it will not run, and the failure is silence.test/script-order.test.jsexists because adeferrace once made a whole admin panel look like an auth failure. - Do not read the allowlists anywhere but
lib/admin.js.test/admin-access.test.jsfails if a second module parsesADMIN_DISCORD_IDSitself. - Do not change the
.spcfwriter's output shape casually.test/profile-format.test.jsasserts that every profile the hub actually stores still round-trips through the parser, because a field that stops being written is invisible in a diff and fatal in a download. - Do not widen a limit in one place only.
test/limits.test.jsexists because the editor's counter, the server's cap and the preview endpoint's cap were three numbers that had to agree. - Restart the service after deploying. A running process only loads code at start, so a new route answers
Cannot GET …untilsudo systemctl restart swiftpoint-hub.service. This is the single most confusing thing for a new contributor, and it is not a proxy problem.
The three extension points the project README calls out
These are the places where one value is deliberately declared in more than one file, and where adding to them means editing all of them:
-
A new ticket status. Add it to
STATUSESinlib/forum.js, toSTATUSESinpublic/js/thread.js(so the buttons appear), and give it a.st-<value>rule inpublic/css/forum.css(so it has a colour).test/forum-pages.test.jsfails when one of the three is missing.lib/forum.js STATUSES = [ { value, label, hint }, … ] STATUS_SHORT = { … } public/js/thread.js STATUSES public/css/forum.css .st-open .st-in_progress .st-on_hold .st-closed -
A new update kind. Add it to
KINDSinlib/updates.jsand give it a.k-<value>rule inpublic/css/updates.css. The existing two arewebsiteandmouse; the split is the feature, so a third kind has to be as distinct as those two are. -
A new staff member. The allowlist is only ever read in
lib/admin.js. An.enventry is the stronger claim: astafftable row cannot demote or remove it, and the last admin cannot be removed or demoted at all —guardNotLastAdmin()returns the refusal andtest/staff.test.jscovers it.
Next: 03 Pages and routes.