Skip to main content

max / goingson

Swap the document: the described screens are the app `a0617c03`, ruled (c) by Max 2026-08-21: describe every screen, then swap the document in one commit, and never write a transitional mechanism. This is that commit. The window opens on quasi://localhost/tasks, index.html and every hand-written file under frontend/js/ are gone, and the `quasi` feature is gone with them because there is no longer a second frontend for a feature to choose between. 30,690 lines deleted, 87 files. What survives under frontend/js/ is two scripts build.rs writes out of quasi-webview's own consts. SIX FEATURES WENT OUT WITH IT, and that is the ruling rather than an oversight. Max on `da48cb6d`: "we aren't releasing a build until everything is flipped." Compose, Settings > Sync, Settings > Sharing, Settings > About, Create Backup, the search box and the blocking graph are not described and are not on screen. No build ships in that interval, so the interval is one nobody is in. Each comes back when the reason it was refused is removed; the two that are refused permanently are named in quasi/mod.rs. THE WINDOW IS BUILT IN SETUP, not declared in tauri.conf.json, because a window declared there loads frontendDist and the app is served from a scheme this process registers. `windows` is `[]` now. /tasks is where it opens, which is navigation.js's TAB_DEFAULTS["work"] and where the shipped app opened. TWO BUILD CHECKS LOST THEIR SUBJECT, and they are not the same. check_touch_density is dropped on the instruction the check itself prints for this case: no script here asks a density question any more. check_vocabulary_use is a real hole, and it is left uncalled rather than silenced. It asks which generated classes no markup emits; the markup is quasi-webview's emitter now, in another crate, so it answered "63 of 73 classes are dead" about a stylesheet that is working fine. Raising its high water from 42 to 73 was one line and would have made it report clean about 9,800 lines nobody is reading. Filed as `43a682b0`, with the stylesheet itself as `daac5cc7`. Seven of the ten frontend lint rules go the same way, and the reasoning is in the script: they banned confirm()/alert() in our own JS, counted inline handlers in our own markup, and ran the CHRONIC-XSS escaping gate. That gate existed because screens were built by concatenating strings and every one had to remember esc(). Escaping is typed in Rust at the renderer now, which is what the port was for. commands::notifs deletes here too, on the schedule its own header set: it was the one place the notification registry crossed the IPC boundary, and the described settings screen reads the registry in process. docs/frontend_architecture.md is deleted rather than corrected, because every part of it described the SPA. design-system.md and styleguide.md are bannered and filed as `3c5dae72`; they are half true, and which half is the work. Android has no frontend as of this commit. Its webview cannot read a request body, so a POST arrives with its form dropped and build_mobile_app registers the scheme everywhere else. Not a regression this introduces so much as one it makes visible. 768 tests, no feature flag. Closes goingson `a0617c03`, `7bf95b31`, `da48cb6d`.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 16:55 UTC
Signed with PGP, not checked
Commit: f4edefb4cd6d2ad2dd6dad2afe9a589a49df1f0b
Parent: 8ebbe29
101 files changed, +214 insertions, -30682 deletions
M Cargo.lock +20 -20
@@ -1387,7 +1387,7 @@
1387 1387 "libc",
1388 1388 "option-ext",
1389 1389 "redox_users",
1390 - "windows-sys 0.60.2",
1390 + "windows-sys 0.61.2",
1391 1391 ]
1392 1392
1393 1393 [[package]]
@@ -1628,7 +1628,7 @@
1628 1628 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1629 1629 dependencies = [
1630 1630 "libc",
1631 - "windows-sys 0.60.2",
1631 + "windows-sys 0.61.2",
1632 1632 ]
1633 1633
1634 1634 [[package]]
@@ -3444,9 +3444,9 @@
3444 3444
3445 3445 [[package]]
3446 3446 name = "makeover-build"
3447 - version = "0.49.1"
3447 + version = "0.50.0"
3448 3448 source = "registry+https://github.com/rust-lang/crates.io-index"
3449 - checksum = "b66a88fb6bd30611d1b30c28d1ede1742c58236148f8e825996dd23fc995966b"
3449 + checksum = "ea4372039ae6e2b306a5fa084f813ef9fef5f7ed6221a17cd7b59fab712fa70a"
3450 3450 dependencies = [
3451 3451 "makeover",
3452 3452 "makeover-geometry",
@@ -3488,9 +3488,9 @@
3488 3488
3489 3489 [[package]]
3490 3490 name = "makeover-webview"
3491 - version = "0.57.0"
3491 + version = "0.58.0"
3492 3492 source = "registry+https://github.com/rust-lang/crates.io-index"
3493 - checksum = "de975123768cac0c56ae40479cac721fe8498e0fd50e643e6884799d8eb12d75"
3493 + checksum = "6ec0cf871810510f38e24230d1781cb572437a0c4550b638b3d45e3ba1474cb4"
3494 3494 dependencies = [
3495 3495 "makeover-geometry",
3496 3496 "makeover-layout",
@@ -3639,7 +3639,7 @@
3639 3639 "png 0.18.1",
3640 3640 "serde",
3641 3641 "thiserror 2.0.20",
3642 - "windows-sys 0.60.2",
3642 + "windows-sys 0.61.2",
3643 3643 ]
3644 3644
3645 3645 [[package]]
@@ -3772,7 +3772,7 @@
3772 3772 source = "registry+https://github.com/rust-lang/crates.io-index"
3773 3773 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
3774 3774 dependencies = [
3775 - "windows-sys 0.60.2",
3775 + "windows-sys 0.61.2",
3776 3776 ]
3777 3777
3778 3778 [[package]]
@@ -5195,7 +5195,7 @@
5195 5195 "errno",
5196 5196 "libc",
5197 5197 "linux-raw-sys",
5198 - "windows-sys 0.60.2",
5198 + "windows-sys 0.61.2",
5199 5199 ]
5200 5200
5201 5201 [[package]]
@@ -5252,7 +5252,7 @@
5252 5252 "security-framework",
5253 5253 "security-framework-sys",
5254 5254 "webpki-root-certs",
5255 - "windows-sys 0.60.2",
5255 + "windows-sys 0.61.2",
5256 5256 ]
5257 5257
5258 5258 [[package]]
@@ -5792,7 +5792,7 @@
5792 5792 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
5793 5793 dependencies = [
5794 5794 "libc",
5795 - "windows-sys 0.60.2",
5795 + "windows-sys 0.61.2",
5796 5796 ]
5797 5797
5798 5798 [[package]]
@@ -6567,7 +6567,7 @@
6567 6567 "getrandom 0.4.3",
6568 6568 "once_cell",
6569 6569 "rustix",
6570 - "windows-sys 0.60.2",
6570 + "windows-sys 0.61.2",
6571 6571 ]
6572 6572
6573 6573 [[package]]
@@ -6589,7 +6589,7 @@
6589 6589 "parking_lot",
6590 6590 "rustix",
6591 6591 "signal-hook",
6592 - "windows-sys 0.60.2",
6592 + "windows-sys 0.61.2",
6593 6593 ]
6594 6594
6595 6595 [[package]]
@@ -7071,7 +7071,7 @@
7071 7071 "png 0.18.1",
7072 7072 "serde",
7073 7073 "thiserror 2.0.20",
7074 - "windows-sys 0.60.2",
7074 + "windows-sys 0.61.2",
7075 7075 ]
7076 7076
7077 7077 [[package]]
@@ -7106,7 +7106,7 @@
7106 7106 dependencies = [
7107 7107 "memoffset",
7108 7108 "tempfile",
7109 - "windows-sys 0.60.2",
7109 + "windows-sys 0.61.2",
7110 7110 ]
7111 7111
7112 7112 [[package]]
@@ -7636,7 +7636,7 @@
7636 7636 source = "registry+https://github.com/rust-lang/crates.io-index"
7637 7637 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
7638 7638 dependencies = [
7639 - "windows-sys 0.60.2",
7639 + "windows-sys 0.61.2",
7640 7640 ]
7641 7641
7642 7642 [[package]]
@@ -8493,10 +8493,6 @@
8493 8493 "winnow 1.0.4",
8494 8494 ]
8495 8495
8496 - [[patch.unused]]
8497 - name = "ops-status"
8498 - version = "0.1.0"
8499 -
8500 8496 [[patch.unused]]
8501 8497 name = "quasi-axum"
8502 8498 version = "0.53.0"
@@ -8512,3 +8508,7 @@
8512 8508 [[patch.unused]]
8513 8509 name = "quasi-store"
8514 8510 version = "0.1.0"
8511 +
8512 + [[patch.unused]]
8513 + name = "ops-status"
8514 + version = "0.1.0"
@@ -1,5 +1,13 @@
1 1 # GoingsOn Design System: Charter
2 2
3 + > **Out of date since 2026-08-22.** goingson swapped its document that day: the
4 + > Tauri window opens on `quasi://localhost/tasks`, `index.html` and every
5 + > hand-written file under `src-tauri/frontend/js/` are deleted, and the screens
6 + > are described in Rust under `src-tauri/src/quasi/`. What is still true here is
7 + > the part about `styles.css`, which is still served and still styles the
8 + > document; what is not is every reference to a script, a `<script>` tag, or a
9 + > class the JavaScript wrote. Rewriting it is goingson `3c5dae72`.
10 +
3 11 This is the **canonical primitive list** for the GoingsOn frontend. Every JS module that renders markup MUST use the primitive named here. If a render need does not match a primitive, the fix is to extend the primitive, not to fork it locally.
4 12
5 13 For visual specs (colors, sizes, shadows, hover behavior) see `styleguide.md`. This file is the inventory and the rules.
@@ -1,5 +1,13 @@
1 1 # GoingsOn Style Guide
2 2
3 + > **Out of date since 2026-08-22.** goingson swapped its document that day: the
4 + > Tauri window opens on `quasi://localhost/tasks`, `index.html` and every
5 + > hand-written file under `src-tauri/frontend/js/` are deleted, and the screens
6 + > are described in Rust under `src-tauri/src/quasi/`. What is still true here is
7 + > the part about `styles.css`, which is still served and still styles the
8 + > document; what is not is every reference to a script, a `<script>` tag, or a
9 + > class the JavaScript wrote. Rewriting it is goingson `3c5dae72`.
10 +
3 11 ## Design Language: Neobrute
4 12
5 13 GoingsOn uses **Neobrute**, a clean neobrutalism design system inspired by [neobrutalism.dev](https://www.neobrutalism.dev/).
@@ -2,12 +2,39 @@
2 2 # Frontend design-system lint guards.
3 3 # See docs/design-system.md "Inline-style rules" and docs/ux-audit/remediation-plan.md Step 10.
4 4 # Exit 0 = clean. Exit non-zero = violations found (printed with file:line).
5 + #
6 + # SEVEN RULES WENT WITH THE 2026-08-22 SWAP, and what is left is the three that
7 + # police a stylesheet rather than a script. The swap deleted `index.html`,
8 + # `compose.html` and every hand-written file under `frontend/js/`, so the rules
9 + # that read them had nothing to read:
10 + #
11 + # no-raw-hex, no-csstext, no-styled-attrs JS and HTML that is gone.
12 + # no-window-confirm, no-native-dialogs Both banned `confirm(`/`alert(`
13 + # in our own JS. A described control
14 + # says `Act::confirm` and the
15 + # renderer draws the question, so
16 + # the ban is now structural.
17 + # frontend-js-tests Ran `js/tests/run.js`, which
18 + # carried the CHRONIC-XSS escaping
19 + # gate. That gate existed because
20 + # screens were built by
21 + # concatenating strings and every
22 + # one of them had to remember
23 + # `esc()`. Escaping is typed in Rust
24 + # at the renderer now, which is what
25 + # the port was for; `escape.js` and
26 + # its 332 call sites are gone.
27 + # inline-handler-ratchet Counted `on<event>=` attributes in
28 + # our markup. We write no markup.
29 + #
30 + # None of that is coverage lost by accident. Each rule was a guard against a way
31 + # of writing a frontend that this app no longer uses. What IS lost by accident
32 + # is `check_vocabulary_use`, which build.rs had to stop calling for the same
33 + # reason and which nothing replaces: goingson `43a682b0`.
5 34
6 35 set -u
7 36 ROOT="$(cd "$(dirname "$0")/.." && pwd)"
8 37 FRONTEND="$ROOT/src-tauri/frontend"
9 - SRC_JS="$FRONTEND/js"
10 - SRC_HTML="$FRONTEND/index.html $FRONTEND/compose.html"
11 38 SRC_CSS="$FRONTEND/css/styles.css"
12 39
13 40 violations=0
@@ -23,82 +50,15 @@
23 50 fi
24 51 }
25 52
26 - # 1. No raw hex literals in JS or source HTML (HTML entities &#NNNN; are OK;
27 - # <meta name="theme-color"> is a documented exception; value is set
28 - # programmatically by js/themes.js to track the active theme).
29 - hits=$(grep -rnE '#[0-9a-fA-F]{3,8}\b' "$SRC_JS" $SRC_HTML 2>/dev/null \
30 - | grep -vE '&#[0-9]+;' \
31 - | grep -vE 'meta name="theme-color"' \
32 - || true)
33 - report "no-raw-hex" "Raw hex literal in JS/HTML. Use a CSS class or token instead." "$hits"
34 -
35 - # 2. No style.cssText anywhere in JS.
36 - hits=$(grep -rn 'cssText' "$SRC_JS" 2>/dev/null || true)
37 - report "no-csstext" "style.cssText injection. Move styles into a CSS class." "$hits"
38 -
39 - # 3. No var(--token, #fallback). Fallback hex defeats theme switching.
40 - hits=$(grep -rnE 'var\(--[a-z-]+,\s*#' "$FRONTEND" --include='*.js' --include='*.html' --include='styles.css' 2>/dev/null || true)
53 + # 1. No var(--token, #fallback). Fallback hex defeats theme switching.
54 + hits=$(grep -rnE 'var\(--[a-z-]+,\s*#' "$SRC_CSS" 2>/dev/null || true)
41 55 report "no-var-fallback-hex" "var(--token, #fallback). Drop the fallback; it bypasses themes." "$hits"
42 56
43 - # 4. No window.confirm / bare confirm(). Route through GoingsOn.ui.showConfirmDialog.
44 - # Skip js/tests/: test fixtures carry attack payloads and mocks that
45 - # legitimately contain `confirm(`/`alert(` as data, not as app calls.
46 - hits=$(grep -rnE '\b(window\.)?confirm\(' "$SRC_JS" 2>/dev/null | grep -v '/tests/' | grep -vE 'showConfirmDialog|confirmDelete|//\s*\*|\*\s' || true)
47 - report "no-window-confirm" "window.confirm(). Use GoingsOn.ui.showConfirmDialog instead." "$hits"
48 -
49 - # 5. No inline style= that touches color / background / border / shadow / font / padding values.
50 - hits=$(grep -rnE 'style="[^"]*(color|background|border|shadow|font-size|font-family|padding)' "$SRC_JS" $SRC_HTML 2>/dev/null || true)
51 - report "no-styled-attrs" "Inline style= with color/background/border/shadow/font/padding. Use a class." "$hits"
52 -
53 - # 6. Deprecated empty-state classes have been removed.
54 - hits=$(grep -rnE 'empty-dashboard-list|kanban-empty|virtual-scroller-empty' "$FRONTEND" --include='*.js' --include='*.html' --include='styles.css' 2>/dev/null || true)
57 + # 2. Deprecated empty-state classes have been removed.
58 + hits=$(grep -rnE 'empty-dashboard-list|kanban-empty|virtual-scroller-empty' "$SRC_CSS" 2>/dev/null || true)
55 59 report "no-deprecated-empty-states" "Deprecated class. Use .empty-state with --compact / --dashboard / --error." "$hits"
56 60
57 - # 7. No native browser dialogs. Charter rule from Phase 7 roll-up.
58 - # Skip js/tests/ (attack payloads / mocks reference these as data, not calls).
59 - hits=$(grep -rnE '\b(window\.)?(confirm|prompt|alert)\(' "$SRC_JS" 2>/dev/null \
60 - | grep -v '/tests/' \
61 - | grep -vE 'showConfirmDialog|showPromptDialog|confirmDelete|//\s|\*\s' || true)
62 - report "no-native-dialogs" "window.confirm/prompt/alert are banned. Use GoingsOn.ui.show{Confirm,Prompt}Dialog or showToast." "$hits"
63 -
64 - # 8. Frontend JS test suite, including the CHRONIC-XSS escaping-enforcement gate
65 - # (js/tests/run.js). GO has no CI and running node inside `cargo build` is an
66 - # anti-pattern, so the gate lives here in the lint script that's run before a
67 - # commit; wiring it in makes the "build-failing gate" literal rather than a
68 - # test you have to remember to invoke. run.js exits non-zero on any failure.
69 - if command -v node >/dev/null 2>&1; then
70 - if ! js_out=$(node "$SRC_JS/tests/run.js" 2>&1); then
71 - echo
72 - echo "[frontend-js-tests] JS test suite failed (includes the CHRONIC-XSS escaping gate):"
73 - echo "$js_out" | tail -40
74 - violations=$((violations + 1))
75 - fi
76 - else
77 - echo "[frontend-js-tests] WARNING: node not found, skipping JS tests and the XSS gate"
78 - fi
79 -
80 - # 9. Inline event-handler ratchet (CSP `unsafe-inline` drawdown).
81 - # The migration is done: `script-src 'unsafe-inline'` is gone from
82 - # tauri.conf.json, so an inline `on<event>="..."` handler is now refused at
83 - # runtime by CSP rather than merely discouraged. Wire events with
84 - # addEventListener (data-* + delegation via js/dispatch.js). This gate stops
85 - # BACKSLIDING: the occurrence count may only go DOWN. The remaining budget is
86 - # doc comments and test strings naming the old pattern, not live handlers.
87 - # Tests and minified bundles are excluded.
88 - INLINE_HANDLER_BUDGET=8
89 - inline_count=$(grep -rlE "\bon[a-z]+=[\"']" "$FRONTEND" --include='*.html' --include='*.js' 2>/dev/null \
90 - | grep -v '/tests/' | grep -v '\.min\.' \
91 - | tr '\n' '\0' | xargs -0 grep -oE "\bon[a-z]+=[\"']" 2>/dev/null | wc -l | tr -d ' ')
92 - if [ "$inline_count" -gt "$INLINE_HANDLER_BUDGET" ]; then
93 - report "inline-handler-ratchet" \
94 - "Inline event handlers rose to $inline_count (budget $INLINE_HANDLER_BUDGET). Don't add inline on*= attributes; wire events with addEventListener (data-* + delegation)." \
95 - "count $inline_count > budget $INLINE_HANDLER_BUDGET"
96 - elif [ "$inline_count" -lt "$INLINE_HANDLER_BUDGET" ]; then
97 - echo "[inline-handler-ratchet] progress: $inline_count inline handlers (budget $INLINE_HANDLER_BUDGET)."
98 - echo " Lower INLINE_HANDLER_BUDGET in scripts/lint-frontend.sh to $inline_count to lock in the gain."
99 - fi
100 -
101 - # 10. A described member stays in flow.
61 + # 3. A described member stays in flow.
102 62 # Rule 1 of wiki `layout-room-and-fallback`: every member of a described
103 63 # group is in flow, and layering is the closed layer set rather than a
104 64 # member positioning itself. `position: absolute` is the one construction
@@ -18,7 +18,7 @@
18 18 # Materialises all three generated files: themes/, geometry.css, layout.css,
19 19 # and holds both drift checks this build.rs used to carry itself: touch density
20 20 # as of 0.17, breakpoints as of 0.18.
21 - makeover-build = "0.49.0"
21 + makeover-build = "0.50.0"
22 22 # Cuts the two house faces into frontend/fonts/. A git dependency because
23 23 # quasi-type is `publish = false`, which is also why makeover-build cannot do
24 24 # this and only emits the CSS that fetches the result. The rev is the one
@@ -28,17 +28,15 @@
28 28 # The table CSS is generated here too: the columns are this app's, so the
29 29 # shared helper cannot know them, but the tracks and the narrowing rules come
30 30 # from the description rather than from hand-written nth-child cuts.
31 - makeover-webview = "0.57.0"
31 + makeover-webview = "0.58.0"
32 32 makeover-layout = "0.35.0"
33 33 # Width. Direct rather than through makeover-webview, because the narrow table
34 34 # pass keys off SizeClass::Compact and a boundary reached transitively is a
35 35 # boundary nobody pinned.
36 36 makeover-geometry = "0.7"
37 - # Only to write its selection script into the frontend, and only under the
38 - # `quasi` feature. Optional here as well as below so a default build compiles
39 - # neither: one `dep:` name covers both tables, which is what lets the feature
40 - # gate the build script and the crate together.
41 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", optional = true }
37 + # Only to write its selection and clock scripts into the frontend. Those two are
38 + # the whole of `frontend/js/` since the swap.
39 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
42 40
43 41 [dependencies]
44 42 goingson-core = { workspace = true }
@@ -47,7 +45,7 @@
47 45 synckit-client = { git = "https://makenot.work/git/max/synckit.git", version = "0.8" }
48 46 synckit-config = { git = "https://makenot.work/git/max/synckit.git", version = "0.2" }
49 47
50 - # The quasi stack, behind the `quasi` feature. Git URLs redirected to the
48 + # The quasi stack, which is the frontend. Git URLs redirected to the
51 49 # working copy by ~/Code/.cargo/config.toml, the way every cross-repo
52 50 # dependency here is.
53 51 #
@@ -56,10 +54,10 @@
56 54 # dependency's source even with the feature off -- so a public clone could not
57 55 # build at all against a URL it could not reach. quasi went public that day and
58 56 # the URL moved here with it.
59 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", optional = true }
60 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", optional = true }
61 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", optional = true }
62 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", optional = true }
57 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
58 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
59 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
60 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.53" }
63 61
64 62 # The notification declaration, and it is NOT behind the `quasi` feature: the
65 63 # watcher is desktop app behaviour rather than a described screen, and the
@@ -76,7 +74,7 @@
76 74 # before it has ported its screens cannot take it. `notifications::Notifier` is
77 75 # the eight lines meanwhile, and it goes when the flip makes quasi-router
78 76 # unconditional here.
79 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", features = ["synckit"] }
77 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.53", features = ["synckit", "describe"] }
80 78
81 79 # Tauri
82 80 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -141,7 +139,7 @@
141 139 # The forms emitter and the description it renders. Runtime deps, not build:
142 140 # form markup depends on the value, the error and the options at the moment a
143 141 # modal opens, so it cannot be materialised the way the stylesheet is.
144 - makeover-webview = "0.57.0"
142 + makeover-webview = "0.58.0"
145 143 makeover-layout = "0.35.0"
146 144 # Browser opening
147 145 open = { workspace = true }
@@ -221,12 +219,10 @@
221 219 [features]
222 220 default = ["custom-protocol"]
223 221 custom-protocol = ["tauri/custom-protocol"]
224 - # The described screens, off by default. Nothing in a default build reaches
225 - # `src/quasi/`; the shipped screens are the ones under `frontend/js/` unchanged.
226 - # Three so far, projects, contacts and tasks. Turning this on by default is
227 - # what starts retiring escape.js, and it is UX-visible, so it is a deliberate
228 - # step rather than a consequence of adding a screen.
229 - quasi = ["dep:quasi-router", "dep:quasi-http", "dep:quasi-webview", "dep:quasi-tauri", "quasi-notifs/describe"]
222 + # The `quasi` feature is gone as of 2026-08-22. It existed so a default build
223 + # reached none of `src/quasi/` while the shipped screens were the ones under
224 + # `frontend/js/`; the swap deleted those, so there is no second frontend for a
225 + # feature to choose between and the described screens are the app.
230 226
231 227 [lints]
232 228 workspace = true
M src-tauri/build.rs +35 -212
@@ -413,57 +413,6 @@
413 413 )
414 414 }
415 415
416 - /// Check the task header's markup against [`TASK_COLUMNS`].
417 - ///
418 - /// The generated caret matches `.table-heading[data-sortable]` and
419 - /// `.table-heading[aria-sort="..."]`, and index.html is hand-written, so the
420 - /// description saying a column reorders buys nothing unless the heading says so
421 - /// too. That gap is what the whole defect was: `build.rs` had `sortable` false
422 - /// on all seven columns of a table that sorts, the headings carried a local
423 - /// `.sortable` class instead of `.table-heading`, and the generated rules sat
424 - /// in `layout.css` matching nothing while a hand-written copy drew the arrow.
425 - ///
426 - /// Both directions, because both drift. A sortable column whose heading lost
427 - /// `data-sortable` gets no cursor and no caret; a heading that grew one against
428 - /// a column the sort code cannot order offers a press that does nothing.
429 - ///
430 - /// Markup-shaped rather than parsed: this looks for `col-<name>` and
431 - /// `data-sortable` inside one heading `<div>`, which is what the file actually
432 - /// contains and all a build script should claim to understand.
433 - fn check_sortable_headers(frontend: &Path) {
434 - let html = fs::read_to_string(frontend.join("index.html")).expect("read index.html");
435 - let headings: Vec<&str> = html
436 - .lines()
437 - .filter(|line| line.contains("role=\"columnheader\"") && line.contains("task-cell"))
438 - .collect();
439 -
440 - for column in TASK_COLUMNS {
441 - let marker = format!("col-{}", column.name);
442 - let Some(heading) = headings.iter().find(|line| {
443 - line.contains(&format!("{marker} ")) || line.contains(&format!("{marker}\""))
444 - }) else {
445 - // Only the sortable ones are required to be found. A column with no
446 - // heading of its own is the events table's problem, not this check's.
447 - assert!(
448 - !column.sortable,
449 - "TASK_COLUMNS says `{}` is sortable, but index.html has no \
450 - `{marker}` column header to carry `data-sortable`",
451 - column.name
452 - );
453 - continue;
454 - };
455 - let marked = heading.contains("data-sortable");
456 - assert_eq!(
457 - marked, column.sortable,
458 - "`{}`: TASK_COLUMNS says sortable = {}, index.html says {marked}. \
459 - The description and the markup are the same fact; change both.",
460 - column.name, column.sortable
461 - );
462 - }
463 -
464 - println!("cargo:rerun-if-changed=frontend/index.html");
465 - }
466 -
467 416 /// Class-and-property overlaps with the generated stylesheet that have been
468 417 /// read and kept.
469 418 ///
@@ -507,159 +456,26 @@
507 456 ("tab", "color"),
508 457 ];
509 458
510 - /// How many generated classes may go unused before the build fails.
459 + /// Bare element rules that reach a generated class and have been read and kept.
511 460 ///
512 - /// One-sided: over this fails, under it warns and asks for the seal to be
513 - /// lowered. A build that broke on deleting dead CSS would teach the wrong
514 - /// lesson, so the number only ever ratchets down.
461 + /// The second pass of the same check. A rule with no class in it -- `button`,
462 + /// `select:focus` -- is invisible to the list above, and it beats the design
463 + /// system on every described component rendered on that element. Empty here,
464 + /// and the entry to make before adding one is usually a `revert-layer` handoff
465 + /// in the stylesheet instead: that says which arms makeover keeps, in the file
466 + /// the browser reads, rather than in a build script.
515 467 ///
516 - /// Measured against [`markup_files`], which is the whole of what this app can
517 - /// write a class from: `index.html`, `compose.html` and every `frontend/js`
518 - /// file. Sealing against too small a list records a number so high the seal
519 - /// never bites; against too large a list it counts prose as usage. The `css/`
520 - /// directory is deliberately out -- a class in a stylesheet is that class being
521 - /// styled, not that class being emitted, and counting it would mark the whole
522 - /// vocabulary used by definition.
523 - ///
524 - /// `src/quasi/**` is out for the opposite reason and is the one real gap here.
525 - /// Those screens go through quasi-webview, which emits makeover's classes from
526 - /// the renderer rather than from any string in this repo, so a class only a
527 - /// described screen uses reads as dead. That inflates the number, which is safe
528 - /// in the direction that matters -- it makes the ceiling looser, never a build
529 - /// falsely red -- and it tightens on its own as the renderer learns to report
530 - /// what it emitted.
531 - /// # 18 to 21, 2026-08-14: the three `picture-*` classes
532 - ///
533 - /// `makeover_layout::Image` arrived at 0.21.0, so `layout_css` now generates
534 - /// `.picture`, `.picture-img` and `.picture-caption`. goingson draws one image
535 - /// in its whole frontend, so none of the three is emitted here and all three
536 - /// land in this count.
537 - ///
538 - /// Not dead vocabulary in the sense this seal is watching for -- it is a member
539 - /// this app has no use for, which is a different thing from a member this app
540 - /// stopped using. The rise is mechanical: the denominator grew from 43 classes
541 - /// to 46 and goingson's usage did not change.
542 - ///
543 - /// # 21 to 23, 2026-08-15: the two `showing-*` classes
544 - ///
545 - /// `makeover-webview` 0.40.0 derives a region's showing-chrome, which generates
546 - /// `.showing-frame` and `.showing-position`. They exist for a region that says
547 - /// how many of its children are up at once -- a carousel -- and goingson draws
548 - /// none, so neither is emitted here and both land in this count.
549 - ///
550 - /// The same mechanical shape as the `picture-*` rise above and read the same
551 - /// way: a member this app has no use for, not a member this app stopped using.
552 - /// The denominator grew from 46 classes to 50 and goingson's usage did not
553 - /// change.
554 - ///
555 - /// # 23 to 26, 2026-08-15: the three `track-*` classes
556 - ///
557 - /// `makeover-layout` 0.24.0 named a time axis and `makeover-webview` 0.42.0
558 - /// emits its rules, so `layout_css` now generates `.track`, `.track-slot`,
559 - /// `.track-tick` and `.track-entry`. Three of the four land here; `.track` does
560 - /// not, because `day-planning-render.js` already writes that word.
561 - ///
562 - /// **This one does not tighten when the day view ports, and that is the gap
563 - /// this constant's header already names.** The described day view emits these
564 - /// classes from `quasi-webview` rather than from any string in this repo, so
565 - /// they read as dead here whether or not a described screen is using them. The
566 - /// count is loose in the safe direction, which is the documented state of
567 - /// affairs and not a new problem.
568 - ///
569 - /// # 26 to 27, 2026-08-17: `.row-relaxed`
570 - ///
571 - /// `makeover-layout` 0.28.1 named `Flow` and `makeover-webview` 0.48.0 emits
572 - /// the clamp it asks for, so the denominator went 54 to 55 and this app writes
573 - /// none of it. Raised rather than answered with markup, which the rule below
574 - /// says to check first: nothing here *should* be writing this class. It is
575 - /// emitted by a renderer when a description says a part may take two lines,
576 - /// which is the same loose-in-the-safe-direction case as the `track-*` classes
577 - /// above.
578 - ///
579 - /// The site that wants it is `.problem-body`, which clamps to two lines in
580 - /// `styles.css` with a comment explaining why, and it is drawn by
581 - /// `js/problems.js` rather than described. It stops being dead when the
582 - /// problems list ports, not before.
583 - ///
584 - /// # 27 to 30, 2026-08-18: three of the four `run-*` classes
585 - ///
586 - /// `makeover-layout` 0.29.0 named `Fallback` and `makeover-webview` 0.49.0
587 - /// emits a rule per member, so the denominator went 55 to 60 and this app uses
588 - /// two of the five: `.run` and `.run-menu`, on the tab strip and the toolbar
589 - /// that shares its row. `.run-wrap`, `.run-stack` and `.run-shed` are the other
590 - /// three answers a group could have given and land here.
591 - ///
592 - /// This is the rise the rule below asks to be checked first, and it passes the
593 - /// check: a group declares exactly one fallback, so four of the five classes
594 - /// being unwritten is what a correct description looks like rather than markup
595 - /// that stopped using them. It tightens only if a second run somewhere in the
596 - /// app declares a different one, which is not something to go looking for.
597 - ///
598 - /// # 30 to 37, 2026-08-18: the seven `facet-*` classes
599 - ///
600 - /// `makeover-layout` 0.30.0 named `Facet` and `makeover-webview` 0.50.0 emits a
601 - /// panel for one, so the denominator went 60 to 67 and this app writes none of
602 - /// the seven. goingson's filters are named in that task as a site the shape
603 - /// fits, and they are not described yet: `contacts.js` and `emails.js` both
604 - /// filter by round-tripping to the backend from hand-written controls.
605 - ///
606 - /// It passes the check the rule below asks for, and passes it the plainest way
607 - /// of any rise so far: there is no facet in this app at all, described or
608 - /// otherwise, so there is no markup that *should* be writing these. It tightens
609 - /// when the filters port, which is the one thing that would make them live.
610 - ///
611 - /// # 37 to 39: the markdown editor's chrome
612 - ///
613 - /// `makeover-webview` 0.52.0 emits `.form-editor-modes` and
614 - /// `.form-editor-preview` around a `FieldKind::Rich` control, so the
615 - /// denominator went 67 to 69 and this app writes neither: goingson has no
616 - /// markdown field, described or hand-written, and its notes are plain text.
617 - ///
618 - /// The rise is unverified rather than measured, which no other rise here is.
619 - /// `quasi-webview` still requires `makeover-webview` ^0.51.0 and that crate
620 - /// carries `links`, so the graph does not resolve on this machine until the
621 - /// pin moves and the build script cannot run to count. Two is the arithmetic,
622 - /// and the check is one-sided: if the real figure is lower it warns and asks
623 - /// for the seal to come down, which is the direction it is meant to move.
624 - ///
625 - /// # 39 to 38, 2026-08-20: the arithmetic above, measured
626 - ///
627 - /// The pin moved, the graph resolves, and the build script counts 38 rather
628 - /// than the 39 predicted by hand. One of the two markdown classes is written
629 - /// somewhere this app's markup already reaches. Lowered so it cannot grow back,
630 - /// which is what the check asked for on every build in between.
631 - ///
632 - /// # 38 to 41, 2026-08-21: a field's suggestion list
633 - ///
634 - /// `quasi` 0.45.0 gave a field a list of candidates it owns, and
635 - /// `makeover-webview` 0.53.0 rules the three classes it is drawn with:
636 - /// `.form-suggestions`, `.form-suggestion` and `.form-suggestion-why`. The
637 - /// denominator went 69 to 72 and this app writes none of them — goingson's one
638 - /// completion list is `search.js`, whose candidates depend on which token the
639 - /// caret is inside, and that file is the measured reason the description
640 - /// carries no caret at all (`d52884b0`).
641 - ///
642 - /// It tightens the day a described field here owns a list, which is what the
643 - /// rule below asks for.
644 - ///
645 - /// Raised 41 to 42 on the makeover-build 0.48 to 0.49 bump, which carries
646 - /// makeover-webview 0.55.0 and makeover-layout 0.33.0's `Field::unit`. The
647 - /// denominator went 72 to 73 and the one new class is `.form-unit`, drawn
648 - /// beside a described number. goingson's runtime form path builds its fields
649 - /// from the JS form spec, which has no field for what a number is measured in,
650 - /// so nothing here sets a unit yet and nothing writes the class.
651 - ///
652 - /// It tightens the day that spec grows one, which is the same shape as the
653 - /// suggestion note above.
654 - ///
655 - /// The one direction this number should move is down, so if it ever needs
656 - /// raising for a class goingson's own markup *should* be writing, that is the
657 - /// defect this exists to catch and the answer is the markup, not the seal.
658 - const DEAD_VOCABULARY_HIGH_WATER: usize = 42;
468 + /// An entry that stops colliding fails the build, the same as above.
469 + const REVIEWED_ELEMENT_OVERLAPS: &[(&str, &str, &str)] = &[];
659 470
660 471 /// Every file that can carry a class name.
661 472 fn markup_files(frontend: &Path) -> Vec<PathBuf> {
662 - let mut files = vec![frontend.join("index.html"), frontend.join("compose.html")];
473 + // `index.html` and `compose.html` were the first two entries until the
474 + // 2026-08-22 swap deleted them. What is left under `js/` is two generated
475 + // scripts, and they are still read: `quasi-selection.js` names the classes
476 + // quasi-webview emits, so a class it reaches for that the stylesheet does
477 + // not define is the same defect this check was written to catch.
478 + let mut files = Vec::new();
663 479 let js = fs::read_dir(frontend.join("js")).expect("read frontend/js");
664 480 let mut scripts: Vec<PathBuf> = js
665 481 .map(|entry| entry.expect("read frontend/js entry").path())
@@ -752,11 +568,7 @@
752 568 // quasi-webview's selection script, written out rather than copied in, so
753 569 // it cannot go stale against the markup that feeds it: it reads two hooks
754 570 // that crate emits, and a fossilised copy here is a bulk bar that silently
755 - // stops counting. Written unconditionally, though nothing serves the
756 - // described screens yet — the `quasi` feature is off — because the asset
757 - // being in place is what makes turning the feature on a one-line change
758 - // rather than a scavenger hunt for three scripts.
759 - #[cfg(feature = "quasi")]
571 + // stops counting.
760 572 fs::write(
761 573 frontend.join("js/quasi-selection.js"),
762 574 quasi_webview::SELECTION_JS,
@@ -766,7 +578,6 @@
766 578 // The clock script, for the same reason: the running-timer band is drawn
767 579 // from a description and ticks from this, and a stale copy is a band that
768 580 // shows the time the screen was served.
769 - #[cfg(feature = "quasi")]
770 581 fs::write(frontend.join("js/quasi-clock.js"), quasi_webview::CLOCK_JS)
771 582 .expect("write js/quasi-clock.js");
772 583
@@ -783,14 +594,26 @@
783 594 &makeover_build::Emit::default(),
784 595 &["geometry.css", "layout.css", "tables.css"],
785 596 REVIEWED_OVERLAPS,
597 + REVIEWED_ELEMENT_OVERLAPS,
786 598 );
787 - makeover_build::check_vocabulary_use(
788 - &markup_files(&frontend),
789 - &makeover_build::Emit::default(),
790 - DEAD_VOCABULARY_HIGH_WATER,
791 - );
792 - makeover_build::check_touch_density(frontend.join("js"));
793 - check_sortable_headers(&frontend);
599 + // `check_vocabulary_use` is not called here any more, and this is a hole
600 + // rather than a tidy-up. It asks which generated classes no markup emits,
601 + // and it answered that by reading `index.html`, `compose.html` and every
602 + // file under `frontend/js/`. The 2026-08-22 swap deleted all of them: the
603 + // markup is `quasi-webview`'s emitter now, which is another crate, so this
604 + // repo cannot see its own document to check it.
605 + //
606 + // Left uncalled rather than called with the high water raised to 73. The
607 + // second is what silencing it would look like, and it would report clean
608 + // about a stylesheet nobody is reading. Restoring the coverage means asking
609 + // quasi-webview which classes it emits, which is goingson `43a682b0`.
610 + let _ = markup_files(&frontend);
611 + // `check_touch_density` is dropped, on the instruction the check itself
612 + // gives for this case: it keeps every copy of `TOUCH_DENSITY` equal to
613 + // `makeover_geometry::Density::Touch`, and after the swap this frontend
614 + // asks no density question in script at all. `touch.js` and `mobile.js`
615 + // held the const and are gone; the generated stylesheet still carries the
616 + // condition and `check_breakpoints` above still checks that.
794 617
795 618 println!("cargo:rerun-if-changed=build.rs");
796 619
@@ -8,18 +8,7 @@
8 8 },
9 9 "app": {
10 10 "withGlobalTauri": true,
11 - "windows": [
12 - {
13 - "label": "main",
14 - "title": "GoingsOn",
15 - "width": 1200,
16 - "height": 800,
17 - "minWidth": 320,
18 - "minHeight": 600,
19 - "resizable": true,
20 - "fullscreen": false
21 - }
22 - ],
11 + "windows": [],
23 12 "security": {
24 13 "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: http://asset.localhost; font-src 'self'; connect-src 'self' ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
25 14 }
@@ -16,9 +16,8 @@
16 16 pub mod notifs;
17 17 pub mod oauth;
18 18 pub mod problems;
19 - /// The projects screen described rather than built. Off by default; see the
20 - /// module header and the `quasi` feature.
21 - #[cfg(feature = "quasi")]
19 + /// The screens, described rather than built. The app is this as of the
20 + /// 2026-08-22 swap; see the module header.
22 21 pub mod quasi;
23 22 pub mod state;
24 23 pub mod syncstore;
@@ -70,7 +69,6 @@
70 69 $crate::commands::set_config,
71 70 $crate::commands::delete_config,
72 71 // Notifications (the declared kinds, for the shipped settings screen)
73 - $crate::commands::list_notification_kinds,
74 72 // Projects
75 73 $crate::commands::list_projects,
76 74 $crate::commands::get_project,
@@ -371,7 +369,7 @@
371 369 // Not on android, which is the one platform the adapter cannot serve --
372 370 // its webview cannot read a request body, so a POST would arrive with its
373 371 // form dropped.
374 - #[cfg(all(feature = "quasi", not(target_os = "android")))]
372 + #[cfg(not(target_os = "android"))]
375 373 let quasi_state = {
376 374 let (protocol, late) = quasi::protocol();
377 375 builder =
@@ -413,7 +411,7 @@
413 411 let state = Arc::new(state);
414 412 // Closes the 503 window the deferred protocol opens, before
415 413 // anything can put a window up.
416 - #[cfg(all(feature = "quasi", not(target_os = "android")))]
414 + #[cfg(not(target_os = "android"))]
417 415 quasi_state.set(Arc::clone(&state));
418 416 app_handle.manage(state);
419 417 });
@@ -122,17 +122,22 @@
122 122
123 123 let mut builder = tauri::Builder::default();
124 124
125 - // The described screens, at `quasi://localhost/<screen>`. Registered here
126 - // because a scheme can only be registered on the builder, and filled in
127 - // from `setup` below because `AppState` needs an `AppHandle` that does not
128 - // exist yet. Nothing navigates to it: the window still loads the JS
129 - // frontend, and each screen moves over as its own flip.
130 - #[cfg(feature = "quasi")]
131 - let quasi_state = {
125 + // The described screens, at `quasi://localhost/<screen>`, and as of the
126 + // 2026-08-22 swap they are the app: the window opens on this scheme and
127 + // there is no other document. Registered here because a scheme can only be
128 + // registered on the builder, and filled in from `setup` below because
129 + // `AppState` needs an `AppHandle` that does not exist yet.
130 + let (quasi_state, quasi_url) = {
132 131 let (protocol, late) = goingson_desktop::quasi::protocol();
132 + // Resolved here rather than in `setup`, so the closure owns a finished
133 + // address and cannot fail halfway through starting the app.
134 + let url = protocol
135 + .url()
136 + .join("tasks")
137 + .expect("a path on our own scheme");
133 138 builder =
134 139 builder.register_asynchronous_uri_scheme_protocol("quasi", protocol.into_handler());
135 - late
140 + (late, url)
136 141 };
137 142
138 143 // Desktop-only plugins
@@ -331,7 +336,7 @@
331 336 let _ = window.emit(&format!("menu:{event_id}"), ());
332 337 }
333 338 })
334 - .setup(|app| {
339 + .setup(move |app| {
335 340 // Set up menu bar tray icon (desktop only)
336 341 #[cfg(not(any(target_os = "ios", target_os = "android")))]
337 342 {
@@ -351,13 +356,28 @@
351 356 // and before the UI can create attachments; race-free here.
352 357 blob_gc::reconcile(&state.db, &state.data_dir).await;
353 358 let state = Arc::new(state);
354 - // Before anything can serve a window, so the 503 window the
355 - // deferred protocol opens closes here rather than at first use.
356 - #[cfg(feature = "quasi")]
359 + // Before the window is built, so the 503 window the deferred
360 + // protocol opens closes here rather than at first request.
357 361 quasi_state.set(Arc::clone(&state));
358 362 app_handle.manage(state);
359 363 });
360 364
365 + // The window, built here rather than declared in `tauri.conf.json`,
366 + // because a window declared there loads `frontendDist` and the app
367 + // is served from a scheme this process registers. `/tasks` is where
368 + // it opens, which is `navigation.js`'s `TAB_DEFAULTS["work"]` and
369 + // the view the shipped app opened on.
370 + tauri::WebviewWindowBuilder::new(
371 + app,
372 + "main",
373 + tauri::WebviewUrl::CustomProtocol(quasi_url),
374 + )
375 + .title("GoingsOn")
376 + .inner_size(1200.0, 800.0)
377 + .min_inner_size(320.0, 600.0)
378 + .resizable(true)
379 + .build()?;
380 +
361 381 // Create shutdown coordination handles
362 382 let cancel_token = CancellationToken::new();
363 383 let db_watcher_shutdown = Arc::new(AtomicBool::new(false));
@@ -35,7 +35,6 @@
35 35 pub(crate) mod import_external;
36 36 mod milestone;
37 37 mod monthly_review;
38 - mod notifs;
39 38 mod oauth;
40 39 mod preferences;
41 40 mod problem;
@@ -136,7 +135,6 @@
136 135 pub use import_external::*;
137 136 pub use milestone::*;
138 137 pub use monthly_review::*;
139 - pub use notifs::*;
140 138 pub use oauth::*;
141 139 pub use preferences::load as load_preferences;
142 140 pub use preferences::*;
@@ -1,25 +1,26 @@
1 - //! The screens that are described rather than built.
1 + //! The screens, described rather than built. This is the frontend.
2 2 //!
3 3 //! <!-- wiki: quasi-overview -->
4 4 //!
5 - //! Behind the `quasi` feature, which is off: nothing in a default build reaches
6 - //! this module, and the shipped screens are the ones under `frontend/js/`
7 - //! exactly as before.
5 + //! The window opens on `quasi://localhost/tasks` and there is no other
6 + //! document. That has been true since 2026-08-22; before it, this module sat
7 + //! behind a `quasi` feature that was off, and the app was `index.html` plus 84
8 + //! scripts under `frontend/js/`.
8 9 //!
9 - //! # What it is for
10 + //! # What it was for
10 11 //!
11 - //! Not to replace the screens one at a time for its own sake. `escape.js` and
12 - //! its `esc()` call sites exist because screens are built by concatenating
13 - //! strings in JavaScript; they retire as screens move here and escaping becomes
14 - //! typed in Rust at the renderer. The escapers delete last, when the count
15 - //! reaches zero, because deleting them first would remove the CHRONIC-XSS seal
16 - //! with nothing in its place.
12 + //! Not replacing screens one at a time for its own sake. `escape.js` and its
13 + //! 332 `esc()` call sites existed because screens were built by concatenating
14 + //! strings in JavaScript, and every one of them had to remember to escape.
15 + //! Escaping is typed in Rust at the renderer now, so the escapers and the
16 + //! CHRONIC-XSS gate that policed them are both gone, in that order and not the
17 + //! other one.
17 18 //!
18 - //! Each screen ported also asks the same question the first one did: what does
19 + //! Each screen ported also asked the same question the first one did: what does
19 20 //! a real screen need that the description layer cannot say? Those findings live
20 21 //! next to the code that ran into them, because a finding that lives only in a
21 - //! commit message is a finding nobody acts on. Each screen module's `row_for`
22 - //! carries its own.
22 + //! commit message is a finding nobody acts on. Each screen module carries its
23 + //! own.
23 24 //!
24 25 //! # The shape
25 26 //!
@@ -28,250 +29,56 @@
28 29 //! router and give it back, rather than a table assembled somewhere central
29 30 //! that has to be kept in step with the modules.
30 31 //!
31 - //! # Which JS file belongs to which described module
32 + //! [`shell`] is the one module that is not a screen: it holds the app's
33 + //! navigation and asks [`time_tracking`] for the running-timer band. [`assets`]
34 + //! is the other, and serves the stylesheets, scripts and fonts the document
35 + //! asks for.
32 36 //!
33 - //! Measured 2026-08-15, re-counted 2026-08-19: 48 files under `frontend/js/`
34 - //! carry 332 `esc()` call
35 - //! sites, plus 2 more in the `js/tests/run.js` gate. Every one of them is
36 - //! accounted for below, in one of four categories.
37 + //! # What the swap left behind
37 38 //!
38 - //! This table exists because without it every claim about what is portable next
39 - //! is a guess. Three such claims were made and retracted, each of them wrong in
40 - //! a way one grep would have caught. Read the flat count correctly: a described
41 - //! screen does not delete its JS counterpart while the feature is off, so both
42 - //! exist and both are counted. The count starts falling at the flip. Progress is
43 - //! the first list, not the number.
39 + //! The pairing table that stood here is gone with the files it paired. It
40 + //! mapped 36 JavaScript counterparts onto the modules that replaced them and
41 + //! fed `witchbroom`'s `described-drift` check, which reported any commit that
42 + //! changed a counterpart and left its description alone. Both were coexistence
43 + //! machinery: they existed because two frontends were in the tree at once, and
44 + //! they are not needed now that one is.
44 45 //!
45 - //! ## Described, and retires at the flip (36 files, 259 sites)
46 + //! **Six features went out with `index.html` and are not described.** Max ruled
47 + //! (b) on goingson `da48cb6d`: they go at the swap and come back as each is
48 + //! described, because no build ships until the flip is finished, so the
49 + //! interval where they are missing is an interval nobody is in.
46 50 //!
47 - //! | Module | JS counterpart | Sites |
48 - //! |---|---|---|
49 - //! | [`projects`] | `projects.js` 8, `projects-render.js` 16 | 24 |
50 - //! | [`contacts`] | `contacts.js` 12, `contacts-render.js` 29, `contact-dashboard.js` 16 | 57 |
51 - //! | [`tasks`] | `task-overview.js` | 26 |
52 - //! | [`emails`] | `emails.js` 3, `emails-render.js` 5, `emails-reader.js` 13, `emails-filter.js` 3, `emails-compose.js` 2 | 26 |
53 - //! | [`weekly_review`] | `weekly-review.js` 3, `weekly-review-render.js` 16 | 19 |
54 - //! | [`day_planning`] | `day-planning-render.js` 8, `day-planning-schedule.js` 4, `day-planning-paint.js` 1 | 13 |
55 - //! | [`problems`] | `problems.js` | 10 |
56 - //! | [`monthly_review`] | `monthly-review.js` 3, `monthly-review-render.js` 5 | 8 |
57 - //! | [`settings`] | `settings.js` 11, `email-accounts.js` 10, `notifs.js` 0 | 21 |
58 - //! | [`board`] | `tasks-kanban.js` 3, `task-board.js` 1 | 4 |
59 - //! | [`task_list`] | `tasks.js` 2, `tasks-render.js` 8, `tasks-filter.js` 2, `task-forms.js` 1, `saved-views.js` 1 | 14 |
60 - //! | [`data`] | `import-external.js` 11, `import.js` 5, `export.js` 2 | 18 |
61 - //! | [`time_tracking`] | `time-tracking.js` 6, `time-summary.js` 3 | 9 |
62 - //! | [`events`] | `events.js` | 10 |
63 - //! | [`shell`] | `navigation.js` 0 | 0 |
51 + //! | Gone | Comes back when |
52 + //! |---|---|
53 + //! | Compose, reply, forward | `3fb2526a`, Max's call on the shape |
54 + //! | Settings > Sync, Settings > Sharing | a route can await a network client |
55 + //! | Settings > About | the app holds the host facts, as `AppState::theme_dirs` already does for the theme path |
56 + //! | Create Backup | [`data`] finding 2: a described write cannot be long-running |
57 + //! | The search box | quasicoherent `d52884b0` settled the caret as the renderer's |
58 + //! | The blocking graph | `524261ac` ruled it bespoke; it draws an SVG with computed coordinates |
64 59 //!
65 - //! Two rows carry a second screen as a submodule: [`projects`] its dashboard,
66 - //! and [`settings`] its email accounts section. So the fifteen rows here are
67 - //! sixteen described screens and one that is no screen at all.
60 + //! The last two are not coming back as descriptions. They are refused for
61 + //! reasons that are correct, and if they return it is as something a host
62 + //! draws.
68 63 //!
69 - //! [`shell`] is that last one, and it is the row that carries no `esc()` sites.
70 - //! It describes the app's furniture rather than a screen: the tabs, the places
71 - //! under them, and the running-timer band it asks [`time_tracking`] for.
72 - //! `navigation.js` is paired with it because that is the file the nav replaces,
73 - //! and pairing it is what puts a tab added to the JS and not to the description
74 - //! in front of the drift check. Its zero is honest: the file concatenates
75 - //! nothing, which is why the shell was invisible to this page until 2026-08-22.
64 + //! **Two checks lost their subject in the same commit**, and one of them is a
65 + //! real hole. `check_touch_density` is dropped on the instruction the check
66 + //! itself gives, because no script here asks a density question any more.
67 + //! `check_vocabulary_use` is the hole: it asked which generated classes no
68 + //! markup emits, and the markup is `quasi-webview`'s emitter now, in another
69 + //! crate. It was left uncalled rather than called with its high-water raised
70 + //! from 42 to 73, which would have reported clean about a 9,800-line stylesheet
71 + //! nobody is reading. goingson `43a682b0` restores it, `daac5cc7` is the
72 + //! stylesheet.
76 73 //!
77 - //! [`time_tracking`] is the one row here that is two things: the app's chrome,
78 - //! described because the floating widget belongs to no screen, and the Timer
79 - //! screen behind it. The widget was the row on its own until 2026-08-20 and
80 - //! carried none of the file's `esc()` sites, because it writes through
81 - //! `textContent` and it is the sub-view, the report and the log-time modal that
82 - //! concatenate. Those are described now, `time-summary.js` with them, so both
83 - //! files retire at the flip and the row carries all nine.
74 + //! # Android is the one platform this does not serve
84 75 //!
85 - //! The row is not whole even so, and the module header says which part: the
86 - //! Focus control and its countdown are left out, waiting on two columns on the
87 - //! session (goingson `44cfcac7`). Its `esc()` sites are inside files that do
88 - //! retire, so the arithmetic here is honest and the screen is not, which is the
89 - //! same shape [`data`] is in.
76 + //! Its webview cannot read a request body, so a POST would arrive with its form
77 + //! dropped. `build_mobile_app` registers the scheme everywhere except android
78 + //! for that reason, and android has had no frontend at all since the swap took
79 + //! `index.html`. It is not a regression this commit introduced so much as one it
80 + //! made visible: goingson `23181009` and the mobile set are where that lives.
90 81 //!
91 - //! `settings.js` went 6 to 11 on 2026-08-19, and gained `notifs.js` beside it
92 - //! (no sites of its own). Both are the notification kinds reaching the shipped
93 - //! screen: [`settings`] generates that pane from the registry and the shipped
94 - //! screen could not, so it reads the same registry over one command. That is
95 - //! the rarer direction, a described screen's shape ported back, and it deletes
96 - //! at the flip like the rest of the row, along with `commands::notifs`.
97 - //!
98 - //! `email-accounts.js` moved into that row on 2026-08-21 (`012ba4a`), out of
99 - //! the by-decision list below. The section had been called host-bound on the
100 - //! strength of its loudest feature; what is host-bound is the OAuth handshake,
101 - //! and the accounts themselves are `State<Arc<AppState>>` and synchronous
102 - //! throughout. [`settings::email`] describes them, and the handshake, Test
103 - //! Connection and Sync Now are what it refuses.
104 - //!
105 - //! ## Stays JavaScript, by decision (3 files, 40 sites)
106 - //!
107 - //! - `settings-sync.js` 17, `settings-sharing.js` 14. Every group and sync
108 - //! command awaits a network client (12 `.await` sites in `commands/group.rs`
109 - //! alone) and a route handler is synchronous, so it cannot await one.
110 - //! Measured 2026-08-21, and it is a different reason from the `AppHandle` one
111 - //! this list used to give: `group.rs` never mentions `AppHandle`.
112 - //! - `search.js` 9. Settled 2026-08-13 (quasicoherent `d52884b0`): a described
113 - //! field's state is its value, and the caret is the renderer's.
114 - //!
115 - //! These 40 never reach zero by porting. Retiring `escape.js` means giving them
116 - //! typed escaping some other way, or accepting that three files keep an escaper.
117 - //!
118 - //! ## Un-ported screens (0 files, 0 sites)
119 - //!
120 - //! A described counterpart could exist and does not. This is the candidate list,
121 - //! and it is the only place to look for what is portable next. It is empty as of
122 - //! 2026-08-21, for the first time since it was written: every screen the app has
123 - //! is described.
124 - //!
125 - //! What left it, in order. The task list on 2026-08-15, the largest candidate
126 - //! here and the one this table had to name twice because it is neither
127 - //! [`tasks`] (the single-task drawer at `GET /tasks/{id}`) nor [`board`] (the
128 - //! kanban). The three import and export files on 2026-08-16, now [`data`], with
129 - //! a caveat this table cannot carry in a count: three exports and the on-demand
130 - //! backup are described nowhere, because a save destination has no word in the
131 - //! vocabulary and a described write cannot be offloaded. Their `esc()` sites are
132 - //! inside files that do retire, so the arithmetic here is honest and the screen
133 - //! is not yet whole. [`data`]'s findings 1 and 2 are what the flip waits on.
134 - //! `time-tracking.js` and `time-summary.js` on 2026-08-20, now
135 - //! [`time_tracking`]'s row. `events.js` on 2026-08-21, now [`events`]: the
136 - //! largest file in `frontend/js/` at 1,037 lines, and the tab the thirteen flip
137 - //! tasks did not name.
138 - //!
139 - //! An empty list is not the end of the porting. It means the remaining
140 - //! JavaScript is in the three categories that are not screens: the by-decision
141 - //! files above, the bespoke one below, and the shell, which no row here reaches
142 - //! and which is the real remainder. It has its own section below.
143 - //!
144 - //! ## Bespoke (1 file, 6 sites)
145 - //!
146 - //! Not a candidate, and it will keep its escaper. Left the list above 2026-08-21.
147 - //!
148 - //! - `task-graph.js` 6. Draws the blocking DAG, and *draws* is the word: it
149 - //! emits an SVG string with computed node coordinates, bezier edge paths, a
150 - //! `defs` arrowhead marker and a viewBox sized to the layered layout. Four of
151 - //! its six sites (207, 209, 213, and the labels at 227 positioned to the
152 - //! column pitch) are inside that. The description layer has no word for a
153 - //! coordinate or a path and should not grow one: a member that positions
154 - //! itself is the thing wiki `layout-room-and-fallback` rule 1 forbids, and a
155 - //! vocabulary that could express this would be a graphics library.
156 - //! The remaining site (46, a load-failure empty state) and the project filter
157 - //! are describable, but splitting a 258-line file to move one `esc()` leaves
158 - //! two screens where there was one and still does not retire the escaper.
159 - //!
160 - //! ## Not a screen (10 files, 27 sites)
161 - //!
162 - //! Shared chrome and behaviour with no screen to belong to, so no port will
163 - //! reach them and they are not candidates. `components-modal.js` 7,
164 - //! `compose-form.js` 5, `whats-new.js` 3, `attachments.js` 2,
165 - //! `autocomplete.js` 2, `bulk-actions.js` 2, `plan-review-toggle.js` 2,
166 - //! `snooze.js` 2, `form-modal.js` 1, `groups.js` 1.
167 - //!
168 - //! Together with the 40 above, these 67 sites are what the done-condition
169 - //! actually has to answer for. Porting every candidate screen leaves them.
170 - //!
171 - //! ## The shell, which no row above reaches (37 files, 8,569 lines)
172 - //!
173 - //! Measured 2026-08-22, and it is the reason a table that looks complete is
174 - //! not. The four categories above account for every `esc()` call site in the
175 - //! tree and for 50 of the 87 files under `frontend/js/`. The other 37 are 8,569
176 - //! lines and appear in no category, because the accounting here is `esc()`-
177 - //! centric and the shell concatenates almost nothing: it wires, routes, and
178 - //! switches panes. A file with no call site was invisible to every count on
179 - //! this page, so the remainder never read as missing.
180 - //!
181 - //! `navigation.js` left this list the same day, for [`shell`]'s row above. It
182 - //! is the only one of them a description replaces rather than makes redundant:
183 - //! the rest go when `index.html` goes.
184 - //!
185 - //! The largest of them, and what each is: `utils.js` 704, `components.js` 594
186 - //! (context menu, action sheet), `api.js` 500, `app.js` 471 (bootstrap, the
187 - //! Tauri menu),
188 - //! `virtual-scroller.js` 453, `mobile.js` 447 and `touch.js` 444,
189 - //! `keyboard.js` 279 (the shortcut table and the help overlay), `themes.js`
190 - //! 277, `router.js` 107, `dispatch.js` 109, `state.js` 158.
191 - //!
192 - //! The navigation is described as of 2026-08-22 and the rest of that list is
193 - //! not. [`quasi_router::Chrome`] grew `nav` and roled panels under
194 - //! quasicoherent `71aa29b4`, ruled (e) by Max, and [`shell`] is this app's
195 - //! answer: the three tabs, the places under them, Settings, and the band.
196 - //!
197 - //! Two pieces of the shipped header are still unsaid, and neither is a gap in
198 - //! the nav. The sync indicator wants a `Role::Status` panel and has nothing
199 - //! describable to put in one, because its content is `settings-sync.js` and
200 - //! that stays JavaScript for the reason the by-decision list gives. The `?`
201 - //! shortcut overlay wants a container quasi-webview emits only alongside a
202 - //! chrome binding, which is quasicoherent `858be2a6`. [`shell`]'s header
203 - //! records both.
204 - //!
205 - //! Four files in the remainder are screen controllers rather than shell, and
206 - //! are candidates for a pairing row once somebody reads them against the
207 - //! description: `day-planning.js` 541 (the day plan's own load and render, next
208 - //! to the three files [`day_planning`] already pairs), `compose-page.js` 304
209 - //! (Max's call on `3fb2526a`, compose becomes a widget of primitives),
210 - //! `focus-timer.js` 283 (the full-screen countdown [`time_tracking`] still
211 - //! leaves out, waiting on quasicoherent `858be2a6`; the control and the session
212 - //! columns behind it landed with migration 068), and `emails-threads.js` 116. They are named here
213 - //! rather than paired, because pairing one makes the drift check watch it from
214 - //! the commit the module was added, and each wants its own read first.
215 - //!
216 - //! ## Modules that describe no screen
217 - //!
218 - //! Read by `witchbroom`'s `described-drift` check, which otherwise takes every
219 - //! `pub mod` under this root for a screen whose pairing row somebody forgot.
220 - //!
221 - //! - [`assets`]. The stylesheets, scripts and fonts a described document asks
222 - //! for. It answers at addresses no route claims and pairs with nothing under
223 - //! `frontend/js/`.
224 - //!
225 - //! # The ports checked against their counterparts
226 - //!
227 - //! Swept 2026-08-15, after [`tasks`] turned out to be describing a screen that
228 - //! had grown a whole section since the port. The check is mechanical: for each
229 - //! module, every commit touching its JS counterpart between the commit that
230 - //! added the module and now, and whether that commit touched the description
231 - //! too.
232 - //!
233 - //! Three of the ten had drifted, and all three by the same commit. `0df3488`,
234 - //! "Every task surface says whether the task is available", added the
235 - //! dependency markers to the board card, the project dashboard card and the day
236 - //! plan pool, and touched no described module. The board and the day view were
237 - //! ported *after* it and still did not carry them, so this is not only a
238 - //! JS-grew-later failure: it is that nobody was reading the counterpart.
239 - //!
240 - //! | Module | Drift | Closed |
241 - //! |---|---|---|
242 - //! | [`tasks`] | `338aa9f`, the whole Dependencies section | `dependencies_section` |
243 - //! | [`board`] | `0df3488`, both markers on the card | [`Availability::marker`] |
244 - //! | [`projects`] | `0df3488`, both markers on the dashboard card | [`Availability::marker`] |
245 - //! | [`day_planning`] | `0df3488`, the unblocks marker in the pool | [`Availability::frees_marker`] |
246 - //! | [`projects`], [`tasks`] | `5e16cc1`, a CSS class rename across six files | presentational only: nothing a description can say changed |
247 - //! | [`settings`] | `dda9a7c`, the knob half of the notifications pane | already generated: [`quasi_notifs::pane`] emits a control per declared knob and [`settings`] reads the registry, so this is the shipped screen catching up |
248 - //! | [`task_list`] | `c238caa`, a pinned saved view's U+2022 became a `data-pinned` attribute | presentational, and inside saved views, which this module's finding 4 declines to describe |
249 - //!
250 - //! Clean: [`contacts`], [`weekly_review`], [`emails`], [`monthly_review`],
251 - //! [`problems`], [`events`]. Four of those now have a commit touching
252 - //! their JS since the port, and each of those touched the description in the
253 - //! same commit, which is the arrangement that works. [`weekly_review`] joined
254 - //! them when the focus picker learned to name what a candidate waits on: the
255 - //! marker went into `weekly-review-render.js` and into [`weekly_review::focus`]
256 - //! together, which is the only reason it is still on this line.
257 - //!
258 - //! **The sweep runs this now, and both tables above are its input.**
259 - //! `witchbroom`'s `described-drift` check reads the pairing table for what to
260 - //! watch and the drift table for what has been answered, then reports every
261 - //! commit since a module was added that touched one of its JS counterparts and
262 - //! left the module alone. It runs on goingson only, nightly, and never on the
263 - //! four by-decision files or the ten non-screens, because those appear in no row
264 - //! of the pairing table.
265 - //!
266 - //! Two obligations follow, and neither is optional if the check is to stay
267 - //! readable. A new described module needs a row in the pairing table in the same
268 - //! commit that declares it, or the check reports `mapping-incomplete` naming it.
269 - //! And a commit that touches a counterpart without changing the description
270 - //! needs a row here saying why: either the description is behind and this is the
271 - //! fix, or the change was presentational and nothing a description can say moved.
272 - //! An empty Closed cell is a drift written down and not answered, and reads as a
273 - //! finding.
274 -
275 82 use std::sync::Arc;
276 83
277 84 use goingson_core::{Task, TaskStatus, UpdateTask};
@@ -1,239 +1,0 @@
1 - # GoingsOn Frontend Architecture
2 -
3 - Vanilla JavaScript frontend for the Tauri 2 desktop app. No framework, no build step, no bundler. ~45 files organized under the `GoingsOn` global namespace.
4 -
5 - ## Namespace
6 -
7 - All code lives under `window.GoingsOn`. No other globals. Cross-module calls use `GoingsOn.moduleName.functionName()`.
8 -
9 - ```
10 - GoingsOn
11 - .api Tauri IPC abstraction (api.js)
12 - .state Centralized state with pub/sub (state.js)
13 - .ui Modal, toast, form utilities (components.js, components-modal.js)
14 - .utils HTML escaping, validation, debounce (utils.js)
15 - .projects Project list, detail, CRUD (projects.js, projects-render.js)
16 - .tasks Task list, CRUD (tasks.js, tasks-render.js, tasks-kanban.js)
17 - .events Event list, CRUD (events.js)
18 - .emails Email list, threading (emails.js)
19 - .contacts Contact CRUD (contacts.js, contacts-render.js)
20 - .dayPlan Time-blocking day planner (day-planning.js, day-planning-render.js)
21 - .snooze Snooze modal + actions (snooze.js)
22 - .navigation View switching, sidebar (navigation.js)
23 - .search Full-text search (search.js)
24 - .settings Settings, export (settings.js, settings-sync.js)
25 - .VirtualScroller Virtual scrolling for large lists
26 - .SelectionManager Multi-select with shift/ctrl
27 - .PaginationManager Page navigation
28 - .handle(path, ...args) Universal onclick dispatcher
29 - ```
30 -
31 - ## Module Pattern
32 -
33 - Every domain module is an IIFE that registers its public API on the namespace:
34 -
35 - ```javascript
36 - (function() {
37 - 'use strict';
38 -
39 - // Private constants and helpers
40 - const ITEMS_PER_PAGE = 50;
41 - function privateHelper() { ... }
42 -
43 - // Public functions
44 - async function load() { ... }
45 - function openNew() { ... }
46 - function openEdit(id) { ... }
47 -
48 - // Register on namespace
49 - GoingsOn.myModule = { load, openNew, openEdit };
50 - })();
51 - ```
52 -
53 - Rules:
54 - - All modules use `'use strict'`
55 - - Private state stays inside the IIFE closure
56 - - Public API is the object assigned to `GoingsOn.moduleName`
57 - - No `window.X` exports for new code
58 - - Prefer `async/await` over `.then()` chains
59 -
60 - ## State Management
61 -
62 - `GoingsOn.state` is a single `AppStateManager` instance with reactive pub/sub.
63 -
64 - ### Reading state
65 -
66 - ```javascript
67 - const tasks = GoingsOn.state.tasks;
68 - const currentProject = GoingsOn.state.currentProjectId;
69 - ```
70 -
71 - ### Writing state
72 -
73 - ```javascript
74 - GoingsOn.state.set('tasks', updatedTasks); // Triggers subscribers
75 - GoingsOn.state.update({ tasks: t, projects: p }); // Batch update
76 - ```
77 -
78 - ### Subscribing to changes
79 -
80 - ```javascript
81 - const unsubscribe = GoingsOn.state.subscribe('tasks', (newTasks, oldTasks) => {
82 - renderTaskList(newTasks);
83 - });
84 - // Later: unsubscribe();
85 - ```
86 -
87 - ### State properties
88 -
89 - | Property | Type | Domain |
90 - |----------|------|--------|
91 - | `projects` | Array | Data |
92 - | `tasks` | Array | Data |
93 - | `emails` | Array | Data |
94 - | `emailAccounts` | Array | Data |
95 - | `currentView` | string | UI |
96 - | `currentProjectId` | string/null | UI |
97 - | `dayPlanDate` | Date | UI |
98 - | `dayPlanData` | object/null | UI |
99 - | `selectedTaskIds` | Set | Selection |
100 - | `selectedEmailIds` | Set | Selection |
101 - | `taskPage` | number | Pagination |
102 - | `emailPage` | number | Pagination |
103 -
104 - Never create module-local caches of data that belongs in state. All shared data goes through `GoingsOn.state`.
105 -
106 - ## API Layer
107 -
108 - `GoingsOn.api` wraps every Tauri IPC command. The UI never calls `__TAURI__.core.invoke` directly.
109 -
110 - ```javascript
111 - // Methods map 1:1 to Rust #[tauri::command] functions
112 - const projects = await GoingsOn.api.projects.list();
113 - const task = await GoingsOn.api.tasks.create({ description: "Buy milk", priority: "medium" });
114 - await GoingsOn.api.tasks.complete(taskId);
115 - ```
116 -
117 - API groups: `projects`, `tasks`, `annotations`, `subtasks`, `events`, `emails`, `emailAccounts`, `contacts`, `search`, `stats`, `dayPlan`, `savedViews`, `milestones`, `themes`, `sync`, `export`, `plugins`.
118 -
119 - ## Form Modal System
120 -
121 - All CRUD forms use `GoingsOn.ui.openFormModal()`. Define fields as data, not HTML:
122 -
123 - ```javascript
124 - function getFields(item = null) {
125 - return [
126 - { name: 'description', type: 'text', label: 'Description', required: true, value: item?.description || '' },
127 - { name: 'priority', type: 'select', label: 'Priority', options: PRIORITY_OPTIONS, value: item?.priority || 'medium' },
128 - { name: 'dueDate', type: 'datetime-local', label: 'Due Date', value: item?.dueDate || '' },
129 - { name: 'notes', type: 'textarea', label: 'Notes', value: item?.notes || '' },
130 - ];
131 - }
132 -
133 - // Open create form
134 - GoingsOn.ui.openFormModal({
135 - title: 'New Task',
136 - entityType: 'task',
137 - isEdit: false,
138 - fields: getFields(),
139 - onSubmit: async (data) => {
140 - await GoingsOn.ui.apiCall(GoingsOn.api.tasks.create(data), {
141 - successMessage: 'Task created',
142 - reload: load,
143 - });
144 - },
145 - });
146 -
147 - // Open edit form
148 - GoingsOn.ui.openFormModal({
149 - title: 'Edit Task',
150 - entityType: 'task',
151 - isEdit: true,
152 - entityId: task.id,
153 - fields: getFields(task),
154 - onSubmit: async (data) => {
155 - await GoingsOn.ui.apiCall(GoingsOn.api.tasks.update(task.id, data), {
156 - successMessage: 'Task updated',
157 - reload: load,
158 - });
159 - },
160 - });
161 - ```
162 -
163 - Supported field types: `text`, `textarea`, `select`, `datetime-local`, `checkbox`, `number`, `email`, `password`, `hidden`.
164 -
165 - ## API Call Wrapper
166 -
167 - `GoingsOn.ui.apiCall()` handles loading state, success/error toasts, and reload:
168 -
169 - ```javascript
170 - await GoingsOn.ui.apiCall(GoingsOn.api.tasks.delete(id), {
171 - successMessage: 'Task deleted',
172 - errorMessage: 'Failed to delete task',
173 - reload: load,
174 - });
175 - ```
176 -
177 - ## Utility Functions
178 -
179 - ```javascript
180 - GoingsOn.utils.escapeHtml(str) // Prevent XSS in innerHTML
181 - GoingsOn.utils.escapeAttr(str) // For HTML attribute values
182 - GoingsOn.utils.formatDue(date) // Human-readable due dates
183 - GoingsOn.utils.formatEmailDate(date) // Email timestamp formatting
184 - GoingsOn.utils.getErrorMessage(err) // Extract error message from any error type
185 - ```
186 -
187 - Always use `escapeHtml()` and `escapeAttr()` when interpolating user data into HTML strings.
188 -
189 - ## Data Flow
190 -
191 - ```
192 - User action (click, keyboard)
193 - -> GoingsOn.api.tasks.create(data) // IPC to Rust
194 - -> Rust command validates + persists
195 - -> Response with pre-computed display fields
196 - -> GoingsOn.state.set('tasks', updated) // Update state
197 - -> Subscribers re-render // Reactive update
198 - ```
199 -
200 - ### Pre-computed Response Fields
201 -
202 - Rust response types include display-ready values. JS never calculates dates, formatting, or derived state:
203 -
204 - | Response | Pre-computed fields |
205 - |----------|-------------------|
206 - | TaskResponse | `dueFormatted`, `urgencyClass`, `isOverdue`, `isSnoozed`, `subtaskCount`, `subtaskProgress` |
207 - | EventResponse | `timeFormatted`, `dateFormatted`, `isPast`, `proximityClass`, `proximityLabel` |
208 - | EmailResponse | `receivedFormatted` |
209 -
210 - ## Load Order
211 -
212 - Scripts load in order via `<script>` tags in `index.html`:
213 -
214 - 1. `goingson.js`: creates `window.GoingsOn` namespace
215 - 2. `state.js`: creates `GoingsOn.state`
216 - 3. `api.js`: creates `GoingsOn.api`
217 - 4. `utils.js`: populates `GoingsOn.utils`
218 - 5. `components.js`, `components-modal.js`, `form-modal.js`: populates `GoingsOn.ui`
219 - 6. Infrastructure: `router.js`, `navigation.js`, `keyboard.js`, `selection-manager.js`, etc.
220 - 7. Domain modules: `projects.js`, `tasks.js`, `events.js`, `emails.js`, `contacts.js`, etc.
221 - 8. Feature modules: `day-planning.js`, `snooze.js`, `settings.js`, etc.
222 - 9. `app.js`: initialization, event listeners, startup
223 -
224 - ## CSS
225 -
226 - Hand-written stylesheet at `frontend/css/styles.css`, loaded as-is with no build step. Uses CSS variables from the theme system. Alongside it, `geometry.css`, `layout.css` and `tables.css` are generated by `src-tauri/build.rs` from the makeover crates; edit the descriptions there, not the output.
227 -
228 - Follow the Neobrute design style (see `docs/styleguide.md`). No inline styles except for dynamic values.
229 -
230 - ## Key Paths
231 -
232 - | What | Where |
233 - |------|-------|
234 - | Namespace root | `src-tauri/frontend/js/goingson.js` |
235 - | State manager | `src-tauri/frontend/js/state.js` |
236 - | API layer | `src-tauri/frontend/js/api.js` |
237 - | Form modal | `src-tauri/frontend/js/form-modal.js` |
238 - | Styles | `src-tauri/frontend/css/styles.css` |
239 - | Entry point | `src-tauri/frontend/index.html` |