ReviewOS

also looking at this

loghqorg/loghq

Promote main to production: signed-out chrome and the subscriber tables

#17
Merged glennmichael123 wants to merge main into production
54 files +4252 -1353

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

config/crosswind.tsmodified+17-0
Changes to config/crosswind.ts
@@ -242,6 +242,23 @@ export default {
242242 'err-box': 'bg-[color-mix(in_srgb,#ef4444_12%,transparent)] text-[#ef4444]',
243243 'ok-box': 'bg-[color-mix(in_srgb,#22c55e_12%,transparent)] text-[#16a34a]',
244244
245 /*
246 * The box those two sit in. Ten sites across six views spelled
247 * `px-3 text-sm rounded-lg` out as three separate utilities, identically in
248 * every one, beside whichever of the pair gave it a colour.
249 *
250 * Kept apart from err-box and ok-box rather than folded into them, so the
251 * composite reads the same way in all three apps that have it: colour is
252 * one class, geometry is another, and `class="mb-4 py-2 err-box
253 * notice-box"` says which is which. It also means the confirmation and the
254 * error share a box instead of each carrying their own copy of it.
255 *
256 * The vertical padding stays at the call site: errors sit at py-2 and
257 * confirmations at py-3 here, in bughq and in analyticshq alike, so it is a
258 * real axis rather than an oversight.
259 */
260 'notice-box': 'px-3 text-sm rounded-lg',
261
245262 // SiteFooter's repeated link and column heading. Four columns and ~28
246263 // links, so these earn a name rather than being repeated on each element.
247264 'footer-head': 'm-0 mb-[0.35rem] font-mono text-[0.7rem] font-semibold '
resources/views/fix.stxmodified+2-2
Changes to resources/views/fix.stx
@@ -217,7 +217,7 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri
217217 </section>
218218
219219 @if (formError)
220 <div role="alert" class="mb-6 px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
220 <div role="alert" class="mb-6 py-2 err-box notice-box">{{ formError }}</div>
221221 @endif
222222
223223 @if (!enabled)
@@ -232,7 +232,7 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri
232232 @endif
233233
234234 @if (run && run.status === 'failed')
235 <div class="mb-6 px-3 py-3 text-sm rounded-lg err-box">
235 <div class="mb-6 py-3 err-box notice-box">
236236 The last analysis stopped. {{ run.error || 'No further detail was recorded.' }}
237237 </div>
238238 @endif
resources/views/forgot-password.stxmodified+2-2
Changes to resources/views/forgot-password.stx
@@ -53,7 +53,7 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) =>
5353 </div>
5454
5555 @if (sent)
56 <div role="status" class="px-3 py-3 text-sm rounded-lg ok-box">
56 <div role="status" class="py-3 notice-box ok-box">
5757 If an account exists for that email, a reset link is on its way. It expires in 60 minutes.
5858 </div>
5959 <p class="mt-6 text-center text-sm text-subtle">
@@ -63,7 +63,7 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) =>
6363 {{-- Posts to itself; the action is in the server block. --}}
6464 <form action="/forgot-password" method="POST" class="space-y-4">
6565 @if (formError)
66 <div class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
66 <div class="py-2 err-box notice-box">{{ formError }}</div>
6767 @endif
6868
6969 <p class="text-muted text-sm">Enter the email you signed up with and we will send you a link to reset your password.</p>
resources/views/login.stxmodified+2-2
Changes to resources/views/login.stx
@@ -139,7 +139,7 @@ async function onVerify(e: Event): Promise<void> {
139139
140140 {{-- The shared error box for both steps. `:text` overwrites the (empty)
141141 static label after hydration and never cloaks. --}}
142 <div id="login-error" role="alert" :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div>
142 <div id="login-error" role="alert" :show="!!error()" :text="error()" class="mb-4 py-2 err-box notice-box"></div>
143143
144144 {{-- Step 1: credentials. A same-origin fetch, not a native POST — the
145145 submit is intercepted so the browser never navigates to /login itself. --}}
@@ -177,7 +177,7 @@ async function onVerify(e: Event): Promise<void> {
177177 </form>
178178
179179 @if (oauthError)
180 <div role="alert" class="mt-4 px-3 py-2 text-sm rounded-lg err-box">{{ oauthError }}</div>
180 <div role="alert" class="mt-4 py-2 err-box notice-box">{{ oauthError }}</div>
181181 @endif
182182
183183 @if (anyOauth)
resources/views/projects/new.stxmodified+1-1
Changes to resources/views/projects/new.stx
@@ -106,7 +106,7 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri
106106 {{-- Posts to itself; the action is in the server block. --}}
107107 <form action="/projects/new" method="POST" class="space-y-4">
108108 @if (formError)
109 <div id="form-error" role="alert" class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
109 <div id="form-error" role="alert" class="py-2 err-box notice-box">{{ formError }}</div>
110110 @endif
111111
112112 <div>
resources/views/register.stxmodified+1-1
Changes to resources/views/register.stx
@@ -95,7 +95,7 @@ async function onRegister(e: Event): Promise<void> {
9595 <p class="mb-3.5 invite-hint">You've been invited to a project — sign up to join it.</p>
9696 @endif
9797
98 <div id="form-error" role="alert" :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div>
98 <div id="form-error" role="alert" :show="!!error()" :text="error()" class="mb-4 py-2 err-box notice-box"></div>
9999
100100 {{-- A same-origin fetch, not a native POST — the submit is intercepted. --}}
101101 <form action="/api/auth/register" method="POST" @submit="onRegister" class="space-y-4">
resources/views/reset-password.stxmodified+2-2
Changes to resources/views/reset-password.stx
@@ -72,7 +72,7 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) =>
7272 </div>
7373
7474 @if (done)
75 <div class="px-3 py-3 text-sm rounded-lg ok-box">
75 <div class="py-3 notice-box ok-box">
7676 Your password has been reset. You have been signed out everywhere; sign in with the new password.
7777 </div>
7878 <p class="mt-6 text-center text-sm text-subtle">
@@ -85,7 +85,7 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) =>
8585 <input type="hidden" name="token" value="">
8686 <input type="hidden" name="email" value="">
8787 @if (formError)
88 <div id="form-error" role="alert" class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
88 <div id="form-error" role="alert" class="py-2 err-box notice-box">{{ formError }}</div>
8989 @endif
9090
9191 <div>
tests/unit/notice-box.test.tsadded+93-0
Changes to tests/unit/notice-box.test.ts
@@ -0,0 +1,93 @@
1/**
2 * The inline form notice: the error banner above a form, and the confirmation
3 * that replaces it.
4 *
5 * Ten sites across six views spelled `px-3 text-sm
6 * rounded-lg` out as three separate utilities, identically in every one. They
7 * carry `notice-box` now, composed with the colour class (`err-box` / `ok-box`, both
8 * shortcuts of their own) and the vertical padding, which genuinely varies.
9 *
10 * Why this is a test and not just a config entry: a shortcut reaches the page
11 * only through the Crosswind merge. Rename or drop the entry and every one of
12 * these banners loses its padding, its type size and its corners while keeping
13 * its red tint, and nothing else fails. So the assertions are on the GENERATED
14 * declarations, driven from markup through the engine's own extractor, which is
15 * the path `buddy serve` takes per request.
16 */
17import { describe, expect, test } from 'bun:test'
18import { readFileSync } from 'node:fs'
19import { join } from 'node:path'
20import theme from '../../config/crosswind'
21// The installed STX package does not export these subpaths by bare specifier.
22import { loadCssEngine } from '../../node_modules/@stacksjs/stx/dist/dev-server/ts-css.js'
23import { mergeCssConfig } from '../../node_modules/@stacksjs/stx/dist/ts-css-config.js'
24
25const ROOT = join(import.meta.dir, '../..')
26const read = (p: string) => readFileSync(join(ROOT, p), 'utf8')
27
28const CALLERS = [
29 'resources/views/login.stx',
30 'resources/views/register.stx',
31 'resources/views/reset-password.stx',
32 'resources/views/forgot-password.stx',
33 'resources/views/projects/new.stx',
34 'resources/views/fix.stx',
35]
36
37describe('the shared inline form notice', () => {
38 test('every caller uses the shortcut and none spells the utilities out', () => {
39 let total = 0
40 for (const path of CALLERS) {
41 const source = read(path)
42 total += (source.match(/\bnotice-box\b/g) ?? []).length
43 expect(source).not.toContain('px-3 py-2 text-sm rounded-lg')
44 expect(source).not.toContain('px-3 py-3 text-sm rounded-lg')
45 }
46 expect(total).toBe(10)
47 })
48
49 test('the colour and the vertical padding stay at the call site', () => {
50 /*
51 * notice-box is geometry only. `err-box` and `ok-box` are shortcuts of their own and
52 * carry the tint, so a notice that lost its colour class would be a plain
53 * grey box; and errors sit at py-2 while confirmations sit at py-3, which
54 * is a real axis rather than something to fold in.
55 */
56 for (const path of CALLERS) {
57 for (const match of read(path).matchAll(/class="([^"]*\bnotice-box\b[^"]*)"/g)) {
58 expect(match[1]).toMatch(/\bpy-[23]\b/)
59 expect(match[1]).toMatch(/\b(err-box|ok-box)\b/)
60 }
61 }
62 })
63
64 test('the generated rule is the three declarations the call sites carried', async () => {
65 const engine = await loadCssEngine()
66 if (!engine)
67 throw new Error('The installed STX CSS engine is required')
68 const config = mergeCssConfig(engine.defaultConfig || engine.config, theme).config
69
70 const extract = engine.extractClasses
71 if (!extract)
72 throw new Error('The installed STX CSS engine is required')
73 const extracted = [...extract('<div class="mb-4 py-2 err-box notice-box">x</div>')]
74 expect(extracted).toContain('notice-box')
75
76 const generator = new engine.CSSGenerator(config)
77 for (const name of extracted)
78 generator.generate(name)
79 const css = generator.toCSS(false, false)
80
81 expect(css).toContain('.notice-box {')
82 // px-3
83 expect(css).toContain('padding-left: 0.75rem;')
84 expect(css).toContain('padding-right: 0.75rem;')
85 // text-sm, both halves: a hand-written composite loses the line-height
86 expect(css).toContain('font-size: 0.875rem;')
87 expect(css).toContain('line-height: 1.25rem;')
88 // rounded-lg
89 expect(css).toContain('border-radius: 0.5rem;')
90 // and NOT the colour, which belongs to .err / .ok
91 expect(css).not.toMatch(/\.notice-box \{[^}]*background/)
92 })
93})