max / goingson
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
101 files changed,
+214 insertions,
-30682 deletions
| @@ -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 |
| @@ -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` | |