## Slice A — explicit dark/light toggle projax now ships with two palettes and a 1y cookie to remember the choice. Dark is the new default; ☀ button in the header nav flips to light and writes projax_theme=light. Server reads the cookie via themeFromRequest(r) and injects Theme + ThemeColor into every template via the centralised render(w, r, …) path, so first paint never flashes the wrong theme. Inline JS in layout.tmpl handles the toggle without a server roundtrip. Every panel colour now lives in a CSS variable under :root[data-theme=dark|light]; the only hardcoded hex values left are inside those two :root blocks. A future palette tweak is one edit, not 30 selectors. Graph node colours, kind-badges, highlights and warn/ok/bad all have parallel dark/light values picked for contrast. Standalone SVG download bakes the light palette inline because the downloaded asset has no parent :root providing vars — m's existing snapshots stay print-friendly regardless of his current cookie. Login page keeps its embedded dark CSS — it's the gateway, intentionally always dark. Tests: TestThemeDefaultIsDark, TestThemeCookieRoundTrips, TestThemeCookieUnknownFallsBackToDark, TestThemeTogglePagesShareSameTheme, TestThemeToggleScriptPresent, TestThemeColorMetaHelper. Full suite green. ## Slice B — file-upload permanently out of scope (m, 2026-05-17) docs/design.md moves "File uploads / in-projax storage" from the §3c parked list to a permanent "Out of scope (decided 2026-05-17)" clause with the rationale: PER is the cross-reference index, not the file system. docs/standards/per.md gains the same explicit clause so future shifts working from the PER standard see the constraint where they look. Memory note filed so future workers don't re-propose multipart uploads, attachments tables, or documents buckets. ## docs/design.md §13 Theming Documents the toggle approach, cookie semantics, palette table, the standalone-SVG carve-out, the login-page exception, and the 4b out-of-scope (prefers-color-scheme detection, per-page overrides, transitions on swap).
8.3 KiB
projax External Reference (PER) Standard
Version: v0.1 Status: locked Authors: m + head Date: 2026-05-15
A projax External Reference (PER) is a short dotted string that m drops into letters, emails, invoices, bank transfers, PDF filenames, physical filing labels — anywhere a project (or a project-event) needs an unambiguous citation. The PER doubles as m's personal Aktenzeichen.
Format
<area>[.<project>[.<sub-project>...]][.<YYMMDD>][.<collision-tag>]
- Lowercase internally (slug, used in DB, URLs, search index).
- Display in m's preferred camelCase (
mFin.house1.260515). - Lookup is case-insensitive — typing
mfin.house1resolves the same row asmFin.house1. - Slugs are vowel-elided where natural (
prjx,mFin,mBrn). - Humanly readable UID is the explicit design goal: each PER is unique within projax (after collision-tag, if needed) AND legible at a glance to a human reading a letter.
Components
| Segment | Required | Example | Notes |
|---|---|---|---|
| Area | yes | dev, mFin, home |
Root-level area slug. |
| Project + sub-projects | optional, dotted | house1, springClean.bathroom |
Nests to any depth. |
| Date stamp | optional | 260515 |
YYMMDD. Anchors the PER to an event/document date. |
| Collision tag | only when needed | a, b, … |
Alpha sequence appended when the same <path>[.<date>] would otherwise repeat. See below. |
Collision handling
When more than one artifact would otherwise share the same PER (same project path on the same date — or same project path with no date), append a single-letter tag in registration order: .a, .b, .c, … .z, then .aa, .ab if anyone ever needs it.
- The first artifact gets the bare PER (no tag).
- The second collision triggers
.afor the new one. The first one retroactively gets.aonly if there's a real risk of ambiguity in already-written documents (otherwise leave the first bare and start the second at.a— the registry resolves both). - Alpha (not numeric) is chosen so the tag never reads as an ordinal (.1 / .2 could be misread as "first / second meeting on that day" — alpha avoids that connotation).
Examples:
| PER | Meaning |
|---|---|
mFin.house1.260515 |
the (only) thing about house1 on that day |
mFin.house1.260515.a |
one of two+ things; this is the first letter / the buyer's notary draft |
mFin.house1.260515.b |
the second one; the seller's response on the same day |
Work.upc.deadlines.a |
one of two undated reference notes on this sub-project |
Examples
| PER | Meaning |
|---|---|
mFin |
the finances area at large |
mFin.house1 |
the project |
mFin.house1.260515 |
the project on 2026-05-15 (a letter, an invoice, a meeting note from that day) |
Work.upc.deadlines.260520 |
a deadline reference on 2026-05-20 |
Home.springClean.bathroom |
a sub-project — no date |
dev.prjx.260515 |
this very document, cited |
Properties
- Rename stability — when a slug is renamed, the previous slug lands in
projax.items.aliases[]. Letters citingmFinanzen.house1.260515continue to resolve after the rename tomFin. - Reverse search — drop a PER into projax search and it resolves the project (and any dated artifacts linked to that project on that date).
- No primary-key role — PERs are NOT stored as primary keys. They're projections of
(items.path, optional item_links.event_date). Canonical identity remains the UUID. - Stable over time — UUIDs are immutable. Slugs change; the PER stays valid via
aliases[]resolution.
Where to use
- Letter Aktenzeichen
- Invoice "Reference" field
- Bank transfer Verwendungszweck
- Email subject prefix —
[mFin.house1.260515] Kaufpreiszahlung - Physical document filing labels
- PDF filenames —
mfin.house1.260515.pdf - Calendar event titles where the project is the topic
Where NOT to use
- Sensitive / external-facing contexts where revealing the project tree structure is undesirable (PERs leak m's life areas).
- Form fields with strict character limits below ~25–30 chars.
- Anywhere ambiguity is a feature (intentionally vague references).
Schema implications
projax.item_linkscarries an optionalevent_date datecolumn (migration0011_item_links_event_date.sql, shipped 2026-05-15). Dated artifacts linked to an item — CalDAV todos, Gitea issues, document references, PER-cited letters — sit here with a date.- Day granularity is intentional. Time-of-day is not part of the PER standard.
- Existing
aliases text[]onprojax.itemsis the rename-stability backbone. Don't drop on archive. - PER resolution = parse the string → match
(area-walk-path, optional date)→ return matchingitems_unifiedrow + linkeditem_linksrows withevent_date = parsed-date.
Implementation (v0.1, shipped Phase 3c)
- Backing columns:
(projax.items.paths[], projax.item_links.event_date). The path is the canonical lookup key; the date narrows to a specific dated artifact. - Ref-type convention for the artifacts surfaced under a project's Documents section:
document— generic pointer (URL, file path, Drive link, …); ref_id is the pointernote— short text snippet; ref_id is the body or a hash, full body innotecolumn ormetadata.bodyurl— bookmarked link; the UI renders ref_id as an<a>opening in a new tab- Existing typed refs (
caldav-list,gitea-repo,gitea-issue,mai-project, …) keep their meaning and can also carry anevent_date.
- Collision tags are render-time only. When two links share
(item_id, event_date), the UI appends.a/.b/… increated_atorder. The first one stays bare. We never store the tag — re-rendering after a delete naturally rebalances the assignments. - URL routing:
/i/<path>.<YYMMDD>first tries the literal path; if 404 and the trailing segment is a validYYMMDD, retries against the stripped path and surfaces the date as a render hint so the Documents row gets.highlight. Invalid dates (Feb 30, etc.) hit the original 404 path. - MCP:
mcp__projax__add_linkaccepts an optionalevent_date: "YYYY-MM-DD".linkView.event_datesurfaces the stored value on responses. - Conflict policy: on
(item_id, ref_type, ref_id, rel)duplicates the upsert usesCOALESCE(new, old)fornoteandevent_date, so a callable that re-adds a link without a date doesn't clobber a pre-set date. - Cross-references: see
docs/design.md§"Documents / dated artifacts (Phase 3c)" for the schema delta and UI integration.
Display & UI
- The backend stores lowercase. The frontend renders PERs in m's preferred camelCase by reading
items.title(or a deriveddisplay_slugfield if titles drift far from slugs). - Search input is normalized to lowercase before matching, so users can type either casing.
Out of scope (v0.1)
- Document-type suffixes (
.kauf,.notar,.mtg,.rcpt, etc.). Considered and dropped: the<area>.<project>.<YYMMDD>[.<collision>]form is enough granularity for m's use today, and adding a taxonomy of doc-types adds memorization overhead for marginal value. If multiple artifacts share<path>.<date>, disambiguation is handled by the collision tag (.a,.b, …), not by encoding a type into the PER string. Reserved for a future v0.2 if real usage surfaces the need. - Time-of-day suffixes — date granularity is enough. If m needs to distinguish "morning meeting" vs "evening call" on the same project on the same day, that's an internal metadata concern, not a PER concern.
- Cross-PER references / linking syntax — Phase 2+.
Out of scope (permanent — decided 2026-05-17)
- In-projax file storage / uploads. A PER names a document; it does not contain one.
ref_idis a pointer (URL, file path, Drive link, mFin location, …) to where the file lives elsewhere. projax never grows a multipart-upload endpoint, an attachments table, or a documents bucket. m killed that direction explicitly on 2026-05-17; do not re-propose it. The source of truth for the bits stays wherever m already keeps it — projax is the cross-reference index, not the file system.
Versioning
PER is a forward-compatible additive format. A v0.2 may add suffixes or qualifiers; v0.1 PERs MUST continue to resolve under any future spec. No backwards-incompatible changes without bumping the major version.