Tjue

https://code.bas.es/tjue/tjue last activity · 5m

Ideas

History

2026-05-22
2026-05-04
2026-05-02
2026-04-29
2026-04-28
show earlier entries
2026-04-25
  • Pattern-compliance pass over the existing htmx 2.0.4 surface, following an audit of all hx-* usage. Five items planned, four implemented (one deliberately skipped — see below).

    Changes

    1. Lead handlers branch on HX-Request

    Both `POST /api/leads` (public landing form) and `POST /admin/spor/{id}` (admin lead row toggle) now check `HX-Request` and choose between fragment + `HX-Push-Url` (htmx) or `303 See Other` (no-JS POST-Redirect-GET). The forms gained `action=` / `method=` and a hidden `lang` field so the redirect target follows the page language.

    `GET /` and `GET /en` now read `?lead=ok` / `?lead=err&msg=...` and render the success or error block server-side, so a refresh after a non-JS submit lands on the right state.

    Without this change, submitting either form without JS would dump a tiny HTML fragment as the entire page response.

    2. Dokument autosave uses htmx

    `admin_dokument_ny.html` and `admin_dokument_rediger.html` previously used hand-rolled `fetch()` for debounced autosave. Replaced with `hx-post` on the textarea + `hx-trigger="input changed delay:2s"` + `hx-swap="none"`. Status text driven by `hx-on::before-request` / `::after-request`. The "X min sidan" tick stays in a small inline script (htmx wouldn't simplify time-formatting). `beforeunload` `sendBeacon` is preserved.

    The create handler returns `HX-Redirect` for htmx clients and `303 See Other` otherwise.

    3. Styleguide tab nav uses out-of-band swap

    The previous `htmx:afterSwap` listener that re-applied `.is-active` to the clicked tab is gone. Tab requests now return the tab content plus a copy of `<nav id="design-nav" hx-swap-oob="true">` with the active class baked in server-side. Pure hypermedia.

    Skipped

    Innboks quote toggle (`onclick="this.closest('.post').classList.toggle('is-expanded')"`) — converting this to `hx-on:click` would require loading htmx (~14KB) on the innboks_thread page, which currently uses no other htmx. Cost outweighs the cosmetic win.

    htmx 4

    We discussed bundling an htmx 4 upgrade. Skipped — htmx 4 is alpha (beta mid-2026, stable late 2026 / early 2027), and we just shipped the first pull-based auto-deploy with v0.1.0. Not the time to also flip the JS runtime. Will revisit when 4 hits stable.

    Tests

    • `internal/leads/handlers_test.go`: htmx + non-htmx variants for create, validation, and admin update; lang redirect.
    • `internal/dokument/http_test.go`: htmx (HX-Redirect) and non-htmx (303) paths for create.
    • `internal/web/router_smoke_test.go`: design partial response now asserted to include OOB nav with the right is-active.

    `go test ./...` and `go test -tags=integration ./internal/updater` both pass. Manually smoke-tested via curl against a local server (lead post in both modes + langs, design tab partial response includes OOB nav).

    See on code.bas.es →

  • bootstrap.sh's curl calls to the Forgejo releases API and asset URLs were unauthenticated. The repo is private, so prod bootstrap 404'd before any binary made it onto the host. Reuse UPDATER_REPO_TOKEN (already required in the env file) for those calls when set.

    Verified during the v0.1.0 prod bootstrap on fismen — without this change the script halts at step 7.

    See on code.bas.es →

  • Summary

    Implements the 2026-04-24-ci-cd-preview plan and brings preview.tjue.net online via a Woodpecker pipeline + tjue-updater pull loop.

    • Woodpecker pipeline (.woodpecker.yaml) — runs on push to main/preview or any tag. Stages: test, build, publish-preview (gated to branch: preview, force-overwrites the preview-latest release), publish-tag (gated to refs/tags/v*).
    • tjue-updater — sidecar daemon that polls Forgejo Releases for the channel's tag (preview-latest or latest semver v*), verifies a SHA256SUMS file, atomically swaps the tjue binary, restarts the service, and rolls back on health-check failure. Now accepts an optional UPDATER_REPO_TOKEN so it can read private repos.
    • Bootstrap (deploy/bootstrap/) — idempotent OpenRC + env setup for new containers; pre-creates /var/log/*.log with tjue:tjue ownership, writes TJUE_EMAIL_WORKER_* and UPDATER_REPO_TOKEN from host env.
    • Misc fixes from rolling this out: auth: TestDecodeRejectsTamper was flaky (~25% of runs) due to a degenerate base64-tail tamper; replaced with a deterministic payload tamper.

    Test plan

    • [x] CI green on preview branch.
    • [x] tjue-updater polls successfully with token, swaps binaries, restarts tjue, health-checks pass.
    • [x] preview.tjue.net returns 200; /health returns 200.
    • [x] internal/updater tests pass, including new TestFetchReleaseSendsToken.
    • [ ] Prod (tjue.tjue.net) — separate session: cut a v0.1.0 tag, run bootstrap.sh tjue prod tjue.net against it.

    🤖 Generated with Claude Code

    See on code.bas.es →

2026-04-23
  • Summary

    Reorganises /design/ from a single-scroll page into seven HTMX-navigated tabs, each with a deep-linkable URL.

    • New internal/web/design.go owns the embed, DesignRenderer, and a chi sub-router. Preview file <title> tags are parsed once at init to drive the Sider tab.
    • /design/ → 301 → /design/oversyn. Seven tabs: oversyn, typografi, fargar, stilar, komponentar, notat, sider. Unknown slugs return 404; /design/preview/*, /design/specs/*, and /design/icon.svg stay as static fallthroughs.
    • Full render on direct visits, partial render on HTMX requests (per HX-Request header). Tab nav uses hx-push-url so the browser URL always matches the visible tab.
    • Existing styleguide content ported into the tab templates. New prose written for Oversyn (brand overview) and Notat (design rationale covering mobile header, safe-area-inset, .row__link, push state machine).
    • TestDesignRoute updated for the new redirect; new TestDesignTabs covers full render, partial render, 404, and static fallthrough.

    Spec: `internal/web/design/specs/2026-04-23-styleguide-tabs.md` Plan: `internal/web/design/specs/2026-04-23-styleguide-tabs-plan.md`

    Test plan

    • [ ] `go test ./...` — full suite passes
    • [ ] Browse `/design/` — lands on Oversyn tab
    • [ ] Click through each tab — URL updates via `hx-push-url`, content swaps without full reload, active tab highlights
    • [ ] Paste a deep link like `/design/fargar` — lands on Colors tab with correct nav highlighting
    • [ ] `/design/preview/landing.html` — still renders

    🤖 Generated with Claude Code

    See on code.bas.es →

  • Summary

    Introduces --new: #B24A2C to the CSS palette for semantic "new / unread / needs attention" states. Token-only change:

    • Added to :root in internal/web/static/css/tjue.css next to --accent.
    • Documented in the styleguide (internal/web/design/index.html) as the ninth swatch, with a Norwegian note clarifying it isn't applied anywhere yet.

    Live-template application (nav counts, thread rows, etc.) is deferred to a follow-up branch so we can nail down the application design separately.

    Spec: internal/web/design/specs/2026-04-23-color-new.md Plan: internal/web/design/specs/2026-04-23-color-new-plan.md

    Test plan

    • [ ] `go test ./...` passes
    • [ ] `/design/` renders with the new swatch + note under the color tokens section

    🤖 Generated with Claude Code

    See on code.bas.es →

  • Summary

    Integrate the previously-standalone design/ styleguide into the main tjue Go binary:

    • Moved design/internal/web/design/ and deleted the parallel style.css, fonts/, and theme.jsinternal/web/static/css/tjue.css is now the single source of truth.
    • New public route /design served by web.DesignHandler() (embedded FS, 5-minute Cache-Control).
    • Rewrote 22 HTML <link> hrefs to /static/css/tjue.css.
    • TestDesignRoute pins both the 200 on /design/ and the 301 on /design.
    • Two audits appended to the spec to feed a follow-up "extract shared Go template partials" branch:
      • CSS parity findings — three live-only components documented in the styleguide (thread-row header, row__link, push-section state machine), eight cross-cutting rules noted, one design-only orphan (.thread__compose-hint).
      • Preview-mock gaps — two missing previews, two stale mocks, seven drifted pairs with structural specifics.
    • CLAUDE.md updated: design-system paragraph reflects the new layout, and /design* added to the routing-shape list.

    Spec: internal/web/design/specs/2026-04-23-design-system-integration.md Plan: internal/web/design/specs/2026-04-23-design-system-integration-plan.md

    Test plan

    • [ ] `go test ./...` — full suite passes
    • [ ] `go run ./cmd/tjue serve` locally, then:
      • [ ] `curl -si http://localhost:8080/design/ | head -1` returns 200
      • [ ] `curl -si http://localhost:8080/design | head -1` returns 301 to `/design/`
      • [ ] Browse the styleguide at `http://localhost:8080/design/` and confirm CSS renders
      • [ ] Open a preview, e.g. `/design/preview/admin-tavla.html`

    🤖 Generated with Claude Code

    See on code.bas.es →

2026-04-22
  • Three-word names (ASF / ØSH) now read distinctly on the oppgåver owner chip. Two-word names unchanged.

    See on code.bas.es →

  • What

    Three overlapping fixes on tavla / admin mobile UX plus a feature (markdown rendering in tavla posts).

    Mobile refinements

    • Sticky admin-nav on mobile so the section header doesn't scroll away
    • Two-tone rubber-band overscroll (paper-2 on top, paper on bottom)
    • .row stacks vertically on mobile with a hairline-separated action bar
    • .row__time { margin-right: auto } handles 1–3 trailing actions

    Tavla reply-count redesign

    • Reply count is now inline in the thread-row header (counts replies, not posts)
    • Hidden when a thread has only the opening post
    • sub helper in the template funcmap

    Tavla markdown rendering

    • New internal/markdown package (shared goldmark + bluemonday renderer with config for extensions, policy, unsafe, hard-wraps, heading offset, cache)
    • internal/dokument migrated onto the shared package — behavior unchanged, 5 existing tests pass byte-identical
    • internal/tavla/render.go uses a strict policy + heading offset +1 (so #<h2>, doesn't collide with thread <h1>)
    • {{md .Body}} on thread detail; {{truncate 640 (mdstrip .OpeningBody)}} on the list preview
    • .post__body--md typography block scoped so innboks stays unbordered
    • Existing posts re-render through goldmark on next read — no migration

    Tests

    go test ./... green across 13 packages. 13 new tests in internal/markdown, 3 in internal/tavla. Dokument's 5 existing tests unchanged.

    Specs

    • docs/superpowers/specs/2026-04-22-mobile-refinements.md
    • docs/superpowers/specs/2026-04-22-tavla-reply-count-design.md
    • docs/superpowers/specs/2026-04-22-tavla-markdown-design.md
    • docs/superpowers/plans/2026-04-22-tavla-markdown.md

    Already deployed to tjue.net.

    See on code.bas.es →

2026-04-17
  • Four small fixes on production pages.

    • Mail thread sticky header safe area.thread__head now adds env(safe-area-inset-top) to its top padding so the iOS status bar stops overlaying the subject on iPhone.
    • Cmd/Ctrl + Enter submit — the hint spans were misleading (no handler existed). Removed the hints; added a keydown listener on the compose textarea in both innboks and tavla threads that submits on meta/ctrl+Enter.
    • Dokument overview — dropped the row__sub line that echoed auto-generated slugs like "dokument-1"; it just duplicated the title row.
    • Leikegrind "Ny app" — now uses the default green btn instead of btn-ghost, matching Tavla and Oppgåver.

    Already built and deployed to fismen, just papering over with the actual source of truth.

    See on code.bas.es →

  • Admin-only markdown knowledge base under /admin/dokument/.

    Summary

    • Single-textarea editor. First line of the body is the title; no separate field. Slug derived at creation from the title (lowercase, Ø→o / Æ→ae / Å→aa, NFD+Mn strip, - for everything else) and frozen thereafter.
    • Debounced autosave. 2 s debounce, fetch POST to /admin/dokument/{slug}/rediger returning 204. Status strip ticks relative time; beforeunload fires a final sendBeacon.
    • Versioning rule. 5-minute buckets within the last 24h; beyond 24h, compacted to the latest version per calendar day. Prune runs in-transaction on every save. Noop when body unchanged.
    • Soft archive, hidden URL. ?arkiv=1 reveals archived docs; no UI link.
    • View + history. Rendered with goldmark + bluemonday UGCPolicy (GFM task-list <input> attrs explicitly allowed). Per-version HTML cache keyed on version ID. Old-version view gets a banner.
    • Schema: new documents + document_versions tables, migration 0011.
    • Worker contract: unchanged.

    Test plan

    • [x] go test ./... -race green (40+ new tests in internal/dokument).
    • [x] Migration 0011 applied in prod; tables present; backup at .predokument.
    • [x] Empty-state page loads, Nytt dokument button visible.
    • [x] Create → redirects to editor; autosave ticks correctly.
    • [x] Edit → reload → body preserved.
    • [x] Render page shows markdown with Plex Serif column.
    • [x] History + old-version view work; banner shows on old.
    • [x] Archive via hidden POST works.

    🤖 Generated with Claude Code

    See on code.bas.es →

2026-04-16
  • Ground-up redesign of the Innboks feature.

    Summary

    • Threads are tickets. Status is open or closed — no more per-user read state. Tjue replies close by default (override via Ferdig etter dette svaret toggle on the composer); inbound messages reopen closed threads.
    • Thread identity is the canonical recipient set. CanonicalKey(from, to, cc) = sorted, lowercased, comma-joined external (non-@tjue.net) addresses. Group threads work; duplicate/case variants dedupe.
    • Outbound From is per-user. Arne · Tjue <arne@tjue.net>, driven by a new users.local_part column. hei@tjue.net stays receive-only + automated.
    • Subject handling. Normalized Re:/Sv:/AW:/Antw.: stripping. Thread carries a current_subject that advances to the latest non-reply-prefixed subject; stream renders a Nytt emne … marker when it changes.
    • Quote stripping on ingest. StripQuote splits body into body_visible + body_quoted using sig-delimiter, attribution patterns, and ≥3-line >-quote runs. Disclosure button (sitat) in the thread view toggles the hidden half.
    • Admin UI rebuilt. Overview uses the .page/.list/.row vocabulary with a scope switch (Ope/Avslutta/Alle) and the Ferdig row toggle. Thread view gets a sticky .thread__head, subject-change markers, outbound post--out accent stripe, inline composer, scroll-to-bottom on load. New compose page.
    • Schema: new email_threads table, thread_id + body_visible + body_quoted on emails, users.local_part, dropped email_thread_reads + emails.counterparty. Migration 0010 backfills one thread per existing counterparty.
    • Worker contract: cc field added to the inbound JSON payload (already deployed).

    Test plan

    • [x] go test ./... -race green
    • [x] Migration applies on prod DB; backfill intact
    • [x] Direct webhook probes (single, group, subject-change, reopen) land correctly
    • [x] Real inbound via Worker → backend
    • [x] Real outbound with per-user From; Ferdig toggle closes; keep-open preserves
    • [ ] Post-merge: delete the .preinnboks DB backup after a soak period

    🤖 Generated with Claude Code

    See on code.bas.es →

  • Summary

    Adds the email domain per docs/superpowers/specs/2026-04-16-email-domain-design.md.

    • New internal/email package: Store, InboundHandler, WorkerSender, admin Handler with Routes().
    • DB migration 0009_email.sql: emails, email_thread_reads, notify_new_email column.
    • /admin/innboks mounted in the admin router; sidebar gets "Innboks" entry with unread badge.
    • Public POST /api/email/inbound accepts JSON from the Cloudflare Worker (auth via X-Tjue-Email-Secret, hmac.Equal).
    • Counterparty-based threading; URL-encoded counterparties handled correctly (incl. + aliases).
    • Konto page gains a third toggle: notify_new_email.
    • Lead rows on /admin/spor get a "Send svar"-button that links to the composer prefilled with the lead's email.
    • Smoke test extended to cover /admin/innboks and /admin/innboks/ny.

    Worker

    Built in parallel in the tjue/email repo (commits 6621988 scaffold + d4e5895 source). Worker handles inbound (email() event → POST to backend) and outbound (fetch() request from backend → env.SEND_EMAIL.send(...)).

    Required env vars (production)

    • TJUE_EMAIL_WORKER_URLhttps://tjue-mail.<account>.workers.dev
    • TJUE_EMAIL_WORKER_SECRET — same value as the Worker's BACKEND_SECRET
    • TJUE_EMAIL_FROM_ADDRESS (default hei@tjue.net)
    • TJUE_EMAIL_FROM_NAME (default Tjue)

    Cutover

    1. Deploy this Go branch with the new env vars set (Worker URL points at the deployed Worker; Email Routing rule still forwards to current destination — backend has no traffic yet).
    2. Deploy the Worker via wrangler deploy from the tjue/email repo. Set secrets.
    3. In the Cloudflare dashboard, change the Email Routing catch-all from "Forward" to "Send to Worker tjue-mail".
    4. Send a test email from a personal address to hei@tjue.net. Confirm it lands in /admin/innboks and the push notification fires.
    5. Reply from the inbox UI. Confirm Cloudflare delivers it.

    Test plan

    • [x] go test ./... green
    • [x] go vet ./... green
    • [ ] Manually: log in, click through /admin/innboks (empty state expected before cutover)
    • [ ] Manually: toggle the new notify_new_email pref on /admin/konto, confirm persists
    • [ ] Manually after cutover: send mail in, verify push + thread
    • [ ] Manually after cutover: reply, verify Cloudflare delivers it

    Out of scope (per spec)

    Attachments, async bounce ingestion, lead↔email FK link, multi-recipient threading.

    See on code.bas.es →

  • Summary

    • Push notifications now fan out on every internal create action (new Tavla thread, Tavla reply, new oppgåve, new Leikegrind app), not just replies. New `notify_admin_activity` pref replaces `notify_tavla_reply`.
    • Konto page gets a state-aware push-enable section (needs-install / can-enable / subscribed) so iPhone users can actually subscribe from the UI.
    • iPhone home-screen install now shows a real icon ("20" in IBM Plex Serif on a dark tile) instead of a blank white square — `apple-touch-icon` and `apple-mobile-web-app-*` meta tags added to base.html; stub 1x1 PNGs replaced with real 192/512 renders.
    • Logout redirects to /admin/login instead of the marketing homepage.

    Plan

    docs/superpowers/specs/2026-04-16-push-pwa-logout-design.md docs/superpowers/plans/2026-04-16-push-pwa-logout.md

    Test plan

    • [x] `go test ./...` green (migration + four handler hook tests + logout redirect test)
    • [x] Deployed to prod from branch; served HTML has all apple-mobile-web-app tags; /static/icons/icon-192.png is a real 192x192 PNG
    • [ ] Manual iPhone check: Add to Home Screen shows the "20" icon
    • [ ] Manual iPhone check: /admin/konto Slå på varsel flow works inside installed PWA
    • [ ] Manual iPhone check: logout lands on /admin/login
    • [ ] Manual check: creating a new oppgåve / Leikegrind app / Tavla thread pushes to subscribed devices (author excluded)

    See on code.bas.es →

  • Derive passkey labels from authenticator AAGUID at registration (curated map, unknown → 'Passkey'), and let users rename any passkey inline on /admin/konto (scoped to the session user).

    See on code.bas.es →

2026-04-15
  • 2 % understated the real cost of a tilsett. New multiplier 1.216.

    See on code.bas.es →

  • The Tavla and Oppgåver rows on /admin rendered as plain <li class=row> with no anchor, so clicks went nowhere. Wrap in <a class=row href=...> per the Leikegrind pattern, linking to the thread/task detail page. Spor rows stay unlinked (no per-lead detail page). Also bumps tjue.css?v=6.

    See on code.bas.es →

  • Plain-JS port of the React prototype, served as a single static HTML at /static/leikegrind/lonnskalkulator.html. No build step.

    After merge, create a Leikegrind row pointing at that URL (status=live) and the Oversyn/Leikegrind row links jump straight into it.

    See on code.bas.es →

  • Replaces the static landing page with a Go server that:

    • Serves the existing NO and EN landing pages with an HTMX contact form (/ and /en)
    • Captures leads into SQLite with honeypot + per-IP rate limiting
    • Gates /admin behind WebAuthn passkeys (supports multiple devices per admin)
    • Admins review/handle leads inline (HTMX) with editable notes
    • PWA with Web Push (VAPID) — admins opt in per device; new leads fan out to all subscribed devices
    • CLI: `tjue admin add`, `tjue vapid generate`, `tjue serve`
    • Deploy recipe: systemd unit + Caddyfile under `deploy/`

    Design spec: `docs/superpowers/specs/2026-04-15-tjue-go-app-design.md` Implementation plan: `docs/superpowers/plans/2026-04-15-initial-go-app.md`

    Test plan

    • [ ] Deploy to staging at `https://tjue.net` (Caddy + systemd)
    • [ ] Landing page loads and contact form creates a lead
    • [ ] `tjue admin add` + passkey enrollment on a supported browser
    • [ ] Subsequent login lands on `/admin`
    • [ ] Enabling notifications creates a row in `push_subscriptions` and new leads deliver a push

    Admin UI is intentionally plain — design polish comes in a follow-up via the frontend-design skill.

    See on code.bas.es →