6 Commits

Author SHA1 Message Date
Jason Tudisco
fc75b27ac6 feat(kez-chat/web): light theme + Light/Dark/System toggle
Some users want light. Dark stays the default/brand; light is a
first-class option.

  • app.css: :root[data-theme="light"] overrides the @theme token values
    (deeper cyan accent so it's legible as text on white; light elevation
    ramp + text tiers + semantic colors). Every utility is var(--color-*),
    so flipping the vars flips the whole app — no per-component work.
  • lib/theme.svelte.ts: choice (light/dark/system) persisted to
    localStorage; "system" follows prefers-color-scheme live; sets
    <html data-theme> + syncs the mobile theme-color meta.
  • index.html: inline pre-paint script resolves the theme before first
    render to avoid a flash of the wrong palette.
  • Settings: new Appearance section with a Light/Dark/System segmented
    control + a hint line ("Following your device (dark)").
  • EmojiButton: picker now follows the app theme (was hardcoded white).
  • main.ts: side-effect import so the system-theme listener is always
    live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:31:27 -06:00
Jason Tudisco
7bbe336f6b fix(kez-chat/web): @theme block was being dropped — theme never applied
The deployed redesign rendered dark-bg but unstyled, low-contrast text
because none of the --color-* tokens or token utilities were in the
output CSS. Two compounding causes, both fixed:

1. A CSS @import url(google-fonts) AFTER @import "tailwindcss" becomes a
   misplaced import once Tailwind inlines itself; Lightning CSS drops it
   and everything after — including @theme. Fonts now load via <link> in
   index.html.
2. A box-drawing-unicode comment immediately before @theme stopped
   Tailwind v4 from transforming the block. Replaced with plain ASCII.

CSS 21.8KB → 26.3KB; tokens + utilities now present; theme applies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:06:55 -06:00
Jason Tudisco
60ff82b4a2 design(kez-chat/web): redesign foundation — tactical-terminal theme + tokens
Phase 0 of the redesign (see DESIGN.md). Establishes the visual
foundation; route restyling + IA reorg follow in subsequent commits.

Design direction (decided with a 3-agent design-team debate):
  • Audience: hackers, privacy absolutists, anti-surveillance, Meshtastic
    / off-grid, journalists in hostile environments.
  • Aesthetic: "muted tactical terminal" — Mullvad-calm restraint, not
    neon cyberpunk cosplay. Monospace as identity. Hard-ish edges.
  • Signature color: electric cyan #28C8E8 on neutral near-black #0B0C0E
    (chosen over signal-amber and phosphor-green — ages better, reads
    "serious infrastructure" without shouting). Verified-green reserved
    for proofs only.

Changes:
  • app.css: full Tailwind v4 @theme token set — elevation ramp, text
    tiers, accent + dim + contrast, semantic colors, Inter + JetBrains
    Mono via Google Fonts, tactical radius scale, accent glow, dark
    color-scheme, cyan text-selection, thin dark scrollbars, and the
    kez-cursor blink keyframe (respects prefers-reduced-motion).
  • Wordmark.svelte: `kez▌` mono wordmark with blinking cyan block
    cursor — the cursor is the brand mark.
  • Avatar.svelte: deterministic 5×5 symmetric identicon from the
    ed25519 key, cyan-arc hue. Every KEZ gets a stable face.
  • kez-icon.svg: amber key → cyan key-meets-cursor glyph; regenerated
    the full PWA icon set + apple-touch-icon from it.
  • manifest + index.html theme/background color → #0B0C0E.
  • DESIGN.md: the full system + IA plan as source of truth.

Note: existing route components still use light-theme utility classes
and will look inconsistent until restyled in the next phases — that
work lands next (shell/nav → Chats → Identity → Settings → Contacts).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:45:21 -06:00
Jason Tudisco
d789e872b1 feat(kez-chat/web): SW auto-reload on deploy + visible build sha in footer
Two related changes — both aimed at "can you tell what's deployed?"

1. SW auto-update (no more "refresh twice")

   The default vite-plugin-pwa autoUpdate behavior was: new SW
   downloads on first reload, activates on second reload. Users
   refresh after a deploy, still see old bundle, get confused.

   Now:
   • workbox: skipWaiting + clientsClaim → new SW activates and
     takes control of existing pages immediately on install.
   • main.ts listens for `controllerchange` and calls reload() once.
     New SW takes over → page reloads → new bundle loads.

   Net: deploys land on the FIRST refresh after the new bundle is
   reachable. (Caveat: the SW that's currently running has to
   download the new SW first, so the very first refresh after a
   deploy may serve stale + then auto-reload a beat later.)

2. Visible build sha in the footer

   vite.config.ts now runs `git rev-parse --short HEAD` at build
   time and injects __BUILD_SHA__ + __BUILD_TIME__ via Vite's
   `define`. App.svelte's footer renders the sha as a small monospace
   chip linking to the commit on gitea, with the build time on
   hover.

   "kez-chat web v0.1" → "kez-chat  [abc1234]  · source"

   So when you refresh and the chip changes value, you know the new
   build landed. When it doesn't, you know the SW is still serving
   the old bundle.

3. Killed the `apple-mobile-web-app-capable` deprecation warning by
   adding the standard `mobile-web-app-capable` next to it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:58:26 -06:00
Jason Tudisco
6c0f5e2fd5 feat(kez-chat/web): make the SPA installable as a PWA
Now installable on iOS (Safari → Share → Add to Home Screen) and
Android/desktop Chrome (install prompt or Settings → Install app).
Launches in standalone mode with a dark theme color matching the
lock-icon palette.

Stack:
  • vite-plugin-pwa with workbox in generateSW mode, registerType
    'autoUpdate' — new SW activates on next page load, no upgrade prompt
    (chat needs to stay fresh).
  • @vite-pwa/assets-generator for icon variants from a single SVG.
    Source kez-icon.svg = dark squircle (#111827) + amber key glyph,
    drawn inside the 80% maskable safe zone.

Caching:
  • Precaches the SPA shell (~635 KB inc. the zstd WASM, well under
    the 5 MB per-file cap).
  • runtimeCaching 'NetworkOnly' for /v1/* — never cache authenticated
    chat data; every poll must hit the network.
  • navigateFallback to index.html so /messages, /claims, /dashboard
    survive a refresh while offline. The /v1/, /internal/, /.well-known/
    paths are explicitly denylisted from this fallback.

Meta tags (index.html):
  • <link rel="manifest"> + theme-color for Android Chrome.
  • apple-touch-icon-180x180 + apple-mobile-web-app-* meta for iOS,
    including status-bar-style=black-translucent so the dark header
    flows into the notch area in standalone.
  • viewport-fit=cover so safe-area-inset works on notched devices.

Generated artifacts committed under web/public/:
  kez-icon.svg, pwa-{64,192,512}.png, maskable-icon-512x512.png,
  apple-touch-icon-180x180.png, favicon.ico.

Verified live: /manifest.webmanifest serves application/manifest+json,
/sw.js serves text/javascript, all icons return 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 22:23:14 -06:00
Tudisco
a9feb1b5b2 feat(kez-chat/web): Svelte SPA — account creation + claims wizard
First real UI for kez-chat. Served by the chat-server as static
files; uses the same HTTP API a native client would (dogfoods the
contract).

Stack: Svelte 5 + TypeScript + Vite + Tailwind 4 + @noble/curves +
@scure/base + canonicalize + idb-keyval + svelte-spa-router.

Bundle: 113 KB JS / 14 KB CSS (gzip: 42 KB / 4 KB).

Pages (all behind hash routing):
  /                 Landing — sign up or restore from seed
  /create           Account creation flow:
                       1. Pick handle, set passphrase
                       2. Show seed for paper backup, require ack
                       3. Confirm
                       4. POST /v1/register, save passphrase-encrypted seed
                          to IndexedDB
  /restore          Stub for restore-from-seed (v0.2: needs
                    GET /v1/by-primary endpoint on the server)
  /unlock           Enter passphrase to derive the AES-GCM key,
                    decrypt the seed, populate session state
  /dashboard       Show handle, primary, registered_at, sigchain URL
  /claims          List locally-cached claims (with publication status)
  /claims/add      Add-a-claim wizard:
                       1. Pick channel (github/dns/web/nostr/bluesky/ap)
                       2. Enter identifier
                       3. SignedClaimEnvelope built + signed in-browser
                          using Ed25519 + JCS, matching the spec exactly
                       4. Show channel-appropriate publish instructions +
                          copyable markdown or JSON artifact
                       5. User marks it published (purely a local note —
                          actual verification is the verifier's job)

Crypto / KEZ helpers (src/lib/kez.ts):
- generateIdentity / identityFromSeed (32-byte Ed25519)
- canonicalBytes (RFC 8785 JCS via the `canonicalize` package — same
  one our Node port uses; produces byte-identical output to Rust)
- signClaim, signRegistration (build envelopes; sign with
  ed25519-sha512-jcs; same alg / key / sig shape as kez-core)
- toPrettyJson, toMarkdown (the same wire encodings the CLI emits)

Key storage (src/lib/identity-store.ts):
- IndexedDB via idb-keyval
- Seed encrypted under user passphrase: PBKDF2-SHA256
  (600,000 iterations, OWASP 2024 guidance) → AES-GCM-256
- Documented limitation: browsers don't have an OS-keychain
  equivalent. Native clients (future CLI/Tauri) will use the OS
  keychain for better protection.

Bundle includes:
- Workaround for TS 5.6+ Uint8Array<ArrayBufferLike> vs ArrayBuffer
  strictness (small asBuffer() helper that copies into a plain
  ArrayBuffer for WebCrypto + Response calls).

Dockerfile updated: now multi-stage with a Node `webbuild` stage
that runs `npm run build` before the Rust binary stage. SPA dist
is copied into the runtime image at /app/web; chat-server's
KEZ_CHAT_WEB_DIR points at it so the SPA is served at /.

What works against the LIVE deployment right now (https://kez.lat):
- Open https://kez.lat → SPA loads (113 KB JS, 14 KB CSS)
- Create account → key gen happens in browser, seed shown for
  backup, encrypted under passphrase, POSTed to /v1/register
- Dashboard → shows registered handle + primary + sigchain URL
- Claims wizard → sign for any of the 6 channels, get publish
  instructions + the right wire format to copy
- Lock / unlock — passphrase-derived AES-GCM, no roundtrips

What's still TODO (v0.2):
- Restore-from-seed: needs GET /v1/by-primary on the server so the
  SPA can discover the handle from a seed
- Actual NATS chat: needs server's auth callout (currently 501) +
  nats.ws client (browser side; package is in deps but not used yet)
- Sigchain integration: append `add` event when user publishes a
  claim, upload to sig-server (needs sig.kez.lat tunnel)
- Verification: in-browser channel fetches (some channels are
  CORS-friendly, others need a server-side proxy)
- Compact (kez:z1:) form: the spec uses zstd, browsers don't have
  native zstd CompressionStream support yet. Workaround in code
  uses deflate-raw with a `kez:zd1:` prefix to make it obvious the
  output isn't spec-compliant; replace with @bokuweb/zstd-wasm or
  similar when we need true compact form in the SPA.
2026-05-25 12:29:14 -06:00