Files
pad/web
xarmian cf3e64caf4 feat(web): add legal footer + consent notice on auth pages (TASK-714) (#229)
* feat(web): add legal footer + consent notice on auth pages (TASK-714)

Pad Cloud (cloudMode=true) needs visible Terms / Privacy / Sub-processors
links so Stripe Live-mode compliance is defensible and users know what
they're agreeing to. Self-hosted installs (cloudMode=false) don't need
these — the legal docs at getpad.dev are Perpetual Software LLC's TOS
for the hosted service, not the user's own instance.

Changes:
- New LegalFooter.svelte component: renders Terms · Privacy · Sub-processors
  links to https://getpad.dev/{terms,privacy,subprocessors}. Gated on a
  cloudMode prop so self-hosted installs see nothing.
- login, register, forgot-password pages: render <LegalFooter> below the
  card. Each page already fetches session; register/forgot-password now
  persist session.cloud_mode into a local $state so the footer can be
  reused consistently.
- register page adds a "By creating an account, you agree to Terms and
  Privacy Policy" consent notice directly under the Create account button
  (only when cloudMode=true). This is the signup touchpoint for
  Stripe Live mode.
- Auth-page containers switched to flex-direction: column so the card
  and footer stack cleanly centered.

Cookie banner intentionally deferred: the privacy policy already asserts
"strictly-necessary cookies only, no banner required" and app.getpad.dev
only sets first-party session/CSRF cookies. Stripe Checkout runs on
billing.stripe.com (separate origin) so its cookies don't apply here.

Parent: PLAN-645 (Pad Cloud Beta Readiness). This covers the main
launch-blocking legal bullet (Stripe Live mode compliance); the pad-web
marketing site already hosts the actual legal content.

* fix(web): read cloudMode from authStore; add link affordance (Codex round 1)

Addresses PR #229 review findings:

MEDIUM — register and forgot-password pages were fetching session via
api.auth.session() specifically to derive cloudMode, duplicating the
root layout's authStore.load() and adding a silent failure path (if the
extra request failed, the legal footer/consent would vanish even on
Pad Cloud). Switched to authStore.cloudMode in both pages. register
still calls api.auth.session() for its pre-existing setup_required +
authenticated checks, but no longer reads cloud_mode from that call.
forgot-password no longer needs onMount at all — its only addition
was the cloudMode fetch.

LOW — legal footer links had no affordance until hover: muted color,
no underline, nothing for keyboard users. Added persistent subtle
underline (1px, 2px offset) and :focus-visible outline so the links
are discoverable for touch and keyboard users.

Also: login page intentionally left untouched. It has a pre-existing
local cloudMode state used for OAuth buttons + sign-up link + legal
footer; unifying it with authStore is a separate refactor and outside
this PR's scope.

* fix(auth): add authStore.ensureLoaded for post-logout auth nav (Codex round 2)

Addresses PR #229 round 2 finding:

MEDIUM — After logout, authStore.session is cleared and the root layout
doesn't re-run onMount on SPA navigation, so subsequent visits to
/register or /forgot-password would read authStore.cloudMode=false and
silently hide the legal footer/consent on Pad Cloud.

Fix: add authStore.ensureLoaded() which returns the cached session when
present or fetches it otherwise. register's onMount now routes its
pre-existing session fetch through ensureLoaded (so the same call
populates authStore for downstream components like LegalFooter).
forgot-password calls ensureLoaded() on mount — cheap no-op when the
store is already populated, one fetch when it's been cleared.

This keeps the single-source-of-truth benefit from round 1 while
handling the logout-then-navigate case Codex flagged.

* fix(auth): coalesce concurrent session loads (Codex round 3)

Addresses PR #229 round 3 finding:

MEDIUM — authStore.ensureLoaded() did a bare 'if (session)' check, so a
hard page load that fired both the root layout's authStore.load() and
the page's ensureLoaded() could issue two /auth/session requests. If
one succeeded and the other failed, the later catch path (session =
null) would overwrite the good session, leaving cloudMode=false after
a successful fetch.

Fix: add an inflight Promise in auth.svelte.ts. load() returns the
inflight promise when one exists; the then/catch/finally chain runs
exactly once per fetch and clears inflight in finally. ensureLoaded()
keeps its cached-session short-circuit and otherwise delegates to
load(), so concurrent callers all await the same underlying request.

* fix(auth): guard stale session fetches with generation counter (Codex round 4)

Addresses PR #229 round 4 findings:

MEDIUM — clear() did not invalidate a pending inflight load, so a
pre-logout /auth/session call could resolve after logout and resurrect
the logged-out user's session. Subsequent ensureLoaded() calls would
also attach to that stale promise.

LOW — inflight was only cleared in the promise's finally, so a
permanently-hanging fetch wedged loading=true and every subsequent
load()/ensureLoaded() returned the same never-settling promise.

Fix: introduce a 'generation' counter that bumps on clear(). load()
captures the current generation at fetch start and only writes session
/ clears loading / clears inflight when the generation is still
current. clear() now also drops the inflight reference and resets
loading=false, so the next ensureLoaded() fires a fresh request and
the UI is not left hanging. Late callbacks from pre-logout fetches
still resolve in the background but cannot mutate authStore state.
2026-04-23 23:33:42 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
      activity/              ActivityFeed
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens