Commit Graph

189 Commits

Author SHA1 Message Date
xarmian f80876a52e feat: add username column to users table
Add username field to the user data model as the foundation for
the multi-user permissions system (PLAN-407, TASK-408).

- SQLite migration 029 and Postgres migration 009 add username column
  with partial unique index (WHERE username != '')
- User, UserCreate, UserUpdate Go structs updated
- Store: CreateUser, UpdateUser, scanUser, userColumns updated
- New GetUserByUsername store method (case-insensitive lookup)
- All auth handler JSON payloads include username field
- WorkspaceMember struct and ListWorkspaceMembers query include username
- TypeScript User type and API client inline types updated

Column is empty string by default; TASK-482 will backfill existing
users and TASK-409 will add validation/registration flow support.
2026-04-10 22:40:07 +00:00
xarmian 4326ab5e04 Merge pull request #84 from xarmian/fix/timeline-duplicate-field-key
fix: timeline duplicate field key crash
2026-04-10 16:57:29 -04:00
xarmian bc8c7090bc fix: use index key for timeline activity changes to avoid duplicate field keys
Activity entries can have the same field changed multiple times (e.g.
status: active → completed; status: completed → planned). The {#each}
block was keyed by change.field which caused each_key_duplicate errors
in Svelte. Switch to array index key since duplicate fields are valid.
2026-04-10 20:57:09 +00:00
xarmian ced8428802 Merge pull request #83 from xarmian/fix/resolve-old-refs-after-move
fix: resolve old item refs after collection move
2026-04-10 16:39:25 -04:00
xarmian 2c9c7e8552 fix: resolve old item refs after move via number-only fallback
After moving an item between collections, the old PREFIX-NUMBER ref
(e.g. PLAN-382) would 404 because the prefix no longer matches.
Since item numbers are now workspace-unique, GetItemByRef falls back
to a number-only lookup when the exact prefix+number doesn't match.

This means PLAN-382 still resolves to the item even after it became
TASK-382, preventing frontend 404 cascades during collection moves.
2026-04-10 20:39:04 +00:00
xarmian 11d1534162 Merge pull request #82 from xarmian/feat/workspace-global-item-numbering
feat: workspace-global item numbering
2026-04-10 16:34:57 -04:00
xarmian a203df3863 feat: workspace-global item numbering with automatic migration
Switch item_number from per-collection to per-workspace scope so items
keep their number when moved between collections (IDEA-42 → BUG-42).

- CreateItem: counter query scopes to workspace_id instead of collection_id
- MoveItem: preserves item_number, only updates collection_id and fields
- One-time migration: detects old (collection_id, item_number) index,
  renumbers all items per-workspace in a single transaction, swaps to
  new UNIQUE(workspace_id, item_number) index. Fully transactional —
  rolls back on failure, retries on next startup.
- Import: assigns fresh sequential numbers instead of using exported
  values, fixing compatibility with old per-collection exports.
- Concurrent create safety: retry loop on unique constraint violation
  (up to 3 attempts) for parallel inserts in the same workspace.

Closes IDEA-330 (formerly IDEA-118).
2026-04-10 20:34:34 +00:00
xarmian 7432ffb1ec fix: emoji picker dropdown escapes dialog overflow clipping
Portal the dropdown into the nearest <dialog> element so it stays in
the browser's top layer, and temporarily set overflow:visible on the
dialog while the picker is open so it isn't clipped.
2026-04-10 20:06:19 +00:00
xarmian cabb552faf feat: add EmojiPickerButton and replace plain-text emoji inputs
Create a reusable EmojiPickerButton component that wraps EmojiPicker
in a compact dropdown toggle. Replace the plain <input type="text">
emoji fields in quick action icons (EditCollectionModal) and role
icons (roles page) with the new picker for a consistent UX.
2026-04-10 19:24:46 +00:00
xarmian 44f249994e feat: add Pad logo wordmark to topbar
Add PadLogo component to the left side of the workspace top bar.
Shows "Pad" in bold accent-blue; includes a hidden "Cloud" badge
variant for future Pad Cloud branding (TASK-205). Reserve left/right
padding in the topbar so workspace buttons never overlap the logo
or user menu.
2026-04-10 19:20:39 +00:00
xarmian 50975e7b91 UI housekeeping: editor stability, sidebar UX, board DnD, quick-add (#81)
* fix: sidebar + button for new collections, desktop sidebar reopen affordance, auto-resizing title editor

- Add "+" button next to Collections header in sidebar to open CreateCollectionModal (IDEA-130)
- Show a chevron tab at the left edge when sidebar is hidden on desktop so users can reopen it without knowing the keyboard shortcut (IDEA-131)
- Switch item title editor from single-line input to auto-resizing textarea so long titles are fully visible while editing (BUG-27)

* fix: reduce editor save jitter by increasing debounce and wiring up SSE guards

Three root causes for BUG-25 (page jumps, stutters, lost keystrokes while typing):

1. Debounce too short (500ms → 1200ms): fast typists frequently pause
   ~500ms between words, triggering saves mid-thought
2. Item page never updated editorStore.lastSaveTime or dirty flag, so
   the SSE handler's 2-second guard never activated — every self-triggered
   save caused an SSE item_updated → re-fetch → store update cycle
3. saveStatus was set to 'saving' on every keystroke (before debounce),
   causing unnecessary re-renders; now only set when save actually fires

Also sets collectionStore.activeItem from the item page so the SSE
handler's active-item guard works correctly.

* fix: suppress timeline refresh during active content editing

The ItemTimeline's SSE handler was re-fetching the entire timeline on
every item_updated event, including self-triggered content saves. This
caused visible re-rendering/shakiness in the timeline section each time
the debounce fired. Now skips item_updated events within 3 seconds of
the last editor save, matching the existing SSE guard pattern.

* fix: eliminate spurious network requests from self-triggered SSE events

Each content save was causing 3 network requests instead of 1: the save
itself, plus /collections and /children re-fetches triggered by the SSE
item_updated event echoing back from our own save.

- Workspace layout SSE handler: skip all side-effects (loadCollections,
  item refetch) for self-triggered content saves using editorStore.dirty
  and lastSaveTime guards
- ChildItems SSE handler: skip item_updated events from self-saves since
  content edits can't affect children
- Timeline SSE handler: remove item_updated from relevant events entirely
  (version diffs appear on next natural refresh); debounce remaining
  events to prevent rate-limit errors from SSE replay on reconnect

* feat: collapsible topbar, sidebar close buttons, quick-add modal, rate limit bump

- Topbar: centered workspace list, collapsible via chevron button or
  Cmd-\, hover-reveal expand tab when hidden, persisted to localStorage
- Sidebar: added close button in footer for independent hiding
- Cmd-\ now toggles both sidebar and topbar together
- Quick-add: sidebar + buttons and "New Item" button open a modal with
  auto-resizing textarea for title input instead of creating "Untitled"
  items (IDEA-132, IDEA-133)
- Dashboard "New Idea"/"New Task" buttons now link to /new form page
- API rate limit bumped from 100/min to 600/min — more appropriate for
  a local-first tool with SSE-driven UI cascading refreshes

* fix: board view drag-and-drop snap-back and re-render cascade

Root cause: isDragging was set to false before the async onStatusChange
API call, triggering a reactive $effect that overwrote columnData with
stale positions — item snapped back to the original column then bounced
to the new one. Additionally, handleReorder fired per-item API calls
that each triggered SSE events, causing cascading re-renders.

- Add dropCooldown flag that freezes columnData for 2s after a drop,
  preventing the $effect from overwriting the visual state while API
  calls and SSE events settle
- Only persist sort_order for items whose order actually changed
- On failed moves: skip reorder, immediately drop cooldown so the
  original state restores cleanly to the correct position
- handleStatusChange re-throws on failure so BoardView can distinguish
  success from failure

* feat: add theme toggle and quick-add modal to sidebar

- Light/dark mode toggle button in sidebar footer row (sun/moon icon),
  to the left of the notification bell (IDEA-134)
- Quick-add modal with auto-resizing textarea for title input, triggered
  from per-collection + buttons and "New Item" button (IDEA-132, IDEA-133)
- Removed old "Untitled" item creation flow from sidebar

* fix: remove all /new page references, use quick-add modal and inline create

- Empty collection "Create" button now opens the inline quick-create
  input instead of navigating to /new (BUG-29)
- Cmd-N opens the sidebar quick-add modal (defaults to active collection
  or Tasks) instead of navigating to /new
- Dashboard "New Idea"/"New Task" buttons trigger quick-add modal
- Onboarding checklist links go to collection pages instead of /new
- Cleaned up dead quickCreate function and unused imports from dashboard

* chore: remove dead /new page route

All item creation now goes through the sidebar quick-add modal or
collection page inline create. The /new form page is no longer
referenced anywhere.
2026-04-10 11:33:12 -04:00
xarmian 1d26c2b542 feat: add workspace top bar with drag-to-reorder (#80)
* feat: add workspace top bar with drag-to-reorder

Replace the sidebar WorkspaceSwitcher dropdown with a dedicated top bar
that provides fast workspace switching and a user menu.

Desktop:
- Horizontal bar above sidebar + content with workspace icons (colored
  first-letter circles) and names as real <a> links
- Drag-and-drop reorder via svelte-dnd-action
- User avatar on right with dropdown (settings, theme toggle, sign out)
- "+" button to create new workspaces

Mobile:
- Full-width fixed bar at top when sidebar opens (above sidebar/backdrop)
- Tap workspace to navigate and close sidebar
- Reorder button opens full-screen vertical list with drag handles
- Sidebar starts below the top bar with adjusted positioning

Backend:
- Migration 028: add sort_order to workspace_members (per-user ordering)
- GET /workspaces now returns workspaces in user's sort order
- PUT /workspaces/reorder endpoint for persisting order

Sidebar simplified:
- Removed WorkspaceSwitcher component, user section, theme toggle
- Theme initialization moved to root layout
- Cleaner footer with search, settings, and notification bell

Implements IDEA-129, relates to IDEA-126.

* fix: address codex review findings (P1+P2)

- Remove unsupported `direction` option from svelte-dnd-action dndzone
- Add Postgres migration 008 for workspace_members.sort_order
- Handle sql.ErrNoRows gracefully in reorder endpoint for admins who
  aren't members of all workspaces
- Restore mobile sign-out: add user name + logout button to sidebar
  footer on mobile (was only in desktop TopBar user menu)
2026-04-10 01:22:49 -04:00
xarmian fbf2b60ce7 Merge pull request #79 from xarmian/fix/tab-resume-sync
fix: replace scattered tab-resume refetches with layered sync system
2026-04-10 01:08:32 -04:00
xarmian 7ef4506cfa fix: replace scattered tab-resume refetches with layered sync system
When the browser tab lost focus and regained it, 5 independent
onTabResume callbacks all fired simultaneously, flooding the server
with redundant requests. This replaces that pattern with a 4-layer
sync architecture:

1. Replay buffer — per-workspace ring buffer stores recent events with
   monotonic IDs. On SSE reconnect, missed events are replayed via
   Last-Event-ID so the client is already caught up.

2. Last-Event-ID support — SSE handler reads the header, replays from
   buffer, or sends sync_required if the gap is too large.

3. Incremental sync — new /changes?since=<ms> endpoint returns only
   modified/deleted items since a timestamp, including archived items
   for view consistency.

4. Centralized sync coordinator — single decision tree replaces 5
   scattered callbacks. Short absences skip sync entirely, SSE-covered
   gaps need no API calls, and full refresh is a last resort.

Key robustness details:
- Global event IDs via Redis INCR for multi-instance safety
- Server-time cursors to avoid client clock skew
- Safe cursor management (only advances on confirmed sync)
- 9 new tests for replay buffer and event ID behavior

Fixes BUG-26.
2026-04-10 04:15:43 +00:00
xarmian 4117d94b5b Merge pull request #78 from xarmian/fix/plan-view-crashes
fix: resolve plan view crash and startup migration error
2026-04-09 10:49:27 -04:00
xarmian 9d90308e20 fix: resolve plan view crash from duplicate children and tiptap link conflict
Three fixes:

1. GetChildItems returned duplicate rows when an item was linked to a parent
   via multiple link types (e.g. both "parent" and "implements"), causing
   Svelte's {#each} to throw each_key_duplicate. Added SELECT DISTINCT.

2. StarterKit v3.20.4 now includes Link by default, conflicting with our
   custom SafeLink extension. Disabled StarterKit's built-in link.

3. Migration runner now tolerates "duplicate column name" errors on
   ALTER TABLE ADD COLUMN, making migrations idempotent when partially
   applied (e.g. server crash mid-migration).
2026-04-09 14:49:03 +00:00
xarmian 239ce19d1f ci: reduce CI usage by ~60-70% with optimized workflow
- Add concurrency groups to cancel superseded in-progress runs
- Move race detector to main-only (saves ~9.5 min per PR run)
- Merge go-vet into go job (eliminates separate VM)
- Remove redundant full-build job (release.yml handles real builds)
- Add binary build+verify as steps in go job for smoke testing
2026-04-09 00:27:55 +00:00
xarmian 3f29055f5c Merge feat/totp-two-factor-auth: TOTP two-factor authentication (#77) 2026-04-08 21:55:56 +00:00
xarmian 0dc3f0b61f fix: address Codex review findings for TOTP 2FA
- Reject API token auth on 2FA enrollment endpoints (setup, verify,
  disable) to prevent account takeover via leaked tokens (P1)
- Re-read 2FA challenge secret after persisting to handle multi-instance
  startup race on fresh databases (P2)
2026-04-08 21:36:48 +00:00
xarmian 10867b9210 fix: persist 2FA challenge key and prevent duplicate TOTP verification
- Persist the 2FA challenge HMAC signing key in platform_settings so
  tokens survive process restarts and work across multiple instances
- Add AND totp_enabled = false to EnableTOTP WHERE clause so concurrent
  /auth/2fa/verify calls (double-click, multi-tab) cannot both succeed
  and overwrite each other's recovery codes
2026-04-08 20:30:39 +00:00
xarmian 1ac0abc305 fix: resolve 2FA Codex review findings (Postgres bools, recovery code race, web login flow)
- Use dialect.BoolToInt() for totp_enabled updates instead of hardcoded
  1/0 integers that fail on PostgreSQL BOOLEAN columns
- Add optimistic locking to ConsumeRecoveryCode to prevent double-spend
  under concurrent requests
- Add 2FA challenge step to web login and join pages so browser login
  works for accounts with TOTP enabled
2026-04-08 20:30:39 +00:00
xarmian 5606b22007 fix: address 6 security findings from Codex review of TOTP 2FA
HIGH fixes:
- Login-verify no longer accepts bare user_id. Now requires an
  HMAC-signed, IP-bound, 5-minute challenge token issued during login
  (prevents password bypass via known user ID + TOTP code)
- Recovery codes are SHA-256 hashed before storage; plaintext is
  returned to the user once and never persisted

MEDIUM fixes:
- ConsumeRecoveryCode uses a DB transaction to prevent concurrent
  double-consumption of the same recovery code
- EnableTOTP is atomic: WHERE clause requires totp_secret match to
  prevent TOCTOU race between setup and verify calls
- /auth/2fa/login-verify now uses the strict Auth rate limiter
  (5 req/min/IP) instead of the general API limiter
- CLI login detects requires_2fa response and prompts for TOTP code
  instead of silently saving empty credentials
2026-04-08 20:30:39 +00:00
xarmian 0e32645bb5 feat: add TOTP two-factor authentication
Backend support for optional TOTP-based 2FA on user accounts:

- POST /auth/2fa/setup — generate TOTP secret, return QR code URI
- POST /auth/2fa/verify — verify code and enable 2FA with recovery codes
- POST /auth/2fa/disable — disable 2FA (requires password confirmation)
- POST /auth/2fa/login-verify — complete login with TOTP or recovery code
- Login returns {requires_2fa: true, user_id} when 2FA is enabled,
  requiring a second step via /auth/2fa/login-verify
- 8 recovery codes generated on setup for account recovery
- User model extended with totp_secret, totp_enabled, recovery_codes
- Refactored user queries with shared scanUser/userColumns for DRYness

Implements TASK-169 under PLAN-15 (Pad Cloud: Hardening).
2026-04-08 20:30:39 +00:00
xarmian beb2afd7f4 Merge feat/api-token-rotation-expiry: API token rotation, expiry defaults, and scope enforcement 2026-04-08 18:50:12 +00:00
xarmian 33f51a2bd6 fix: address Codex review findings for token rotation and scope enforcement
- Preserve backward compatibility for unrecognized token scopes (P1)
- Reject malformed JSON before destructive token rotation (P2)
- Preserve original created_at timestamp when rotating tokens (P3)
2026-04-08 18:50:08 +00:00
xarmian ba8e20c697 feat: add API token rotation, expiry defaults, and scope enforcement
- New tokens get a default 90-day expiry (configurable via platform
  settings: token_default_expiry_days, token_max_lifetime_days)
- POST /api/v1/auth/tokens/{id}/rotate generates a new secret while
  preserving token metadata; old secret is immediately invalidated
- X-Token-Expires-Soon and X-Token-Expires-At headers warn when a
  token is within 7 days of expiry
- Token scopes are now enforced: "read" restricts to GET/HEAD/OPTIONS,
  "write" and "*" allow all methods
- Existing tokens without expiry continue to work (backward compatible)

Implements TASK-170 under PLAN-15 (Pad Cloud: Hardening).
2026-04-08 18:00:43 +00:00
xarmian 30fe60d666 feat: session binding, nonce-based CSP, and auth hardening (#75)
* feat: add session binding, nonce-based CSP, and auth hardening

Security hardening for Pad Cloud (PLAN-15 / TASK-171):

- Bind sessions to User-Agent hash; mismatch invalidates session
- Store client IP on session creation for audit trail
- Increase bcrypt cost from 10 to 12
- Upgrade invitation codes to 128-bit entropy with hashed storage
- Replace CSP unsafe-inline with per-request nonce for SvelteKit scripts
- Move SecurityHeaders to main router so SPA gets headers too

* fix: enforce session binding on auth cookie fallbacks and fix invitation code uniqueness

- Add validateSessionCookie() helper that checks UA binding, replacing
  raw ValidateSession() calls in handleSessionCheck, handleGetCurrentUser,
  and handleUpdateCurrentUser that bypassed the new session binding
- Store invitation ID in code column instead of empty string to satisfy
  the NOT NULL UNIQUE constraint (previously broke on second invitation)
- Skip code/join_url in invitation listings for hashed invitations where
  the plaintext is not recoverable
2026-04-08 13:58:58 -04:00
xarmian 5f17ac6c21 Merge pull request #74 from xarmian/ui/board-view-lane-styling
ui: style board view columns to match roles page swim lanes
2026-04-07 18:01:31 -04:00
xarmian 1198f79492 ui: style board view columns to match roles page swim lanes
Add background, border, and border-radius to board columns for a
boxed card-like container look. Round header top corners to match,
tighten card gap, and bump header padding/weight for consistency
with the roles page lane styling.
2026-04-07 22:01:12 +00:00
xarmian 6ddf16ceed Merge pull request #73 from xarmian/fix/progress-bar-child-link-types
fix: count implements links as children for progress tracking
2026-04-07 17:54:25 -04:00
xarmian 62c85164f9 fix: count implements links as children for progress tracking
Progress bars, child item lists, and parent enrichment all only
counted 'parent' link types. Items connected via 'implements' links
(e.g. tasks implementing a plan) were invisible to the progress
system, showing 0/0 even when all implementing items were done.

Backend: define childLinkTypes ('parent' + 'implements') and update
all 8 SQL queries (GetItemProgress, GetAllItemProgress, GetChildItems,
PopulateHasChildren, GetParentMap, GetParentForItem, and both
terminal-status helpers) to use IN ('parent','implements').

Frontend: remove the standalone "Derived Closure" banner that
duplicated relationship info. Fold closure summary into the matching
relationship group as an inline annotation. Add implements-link
dedup so children shown in ChildItems aren't repeated in Relationships.
2026-04-07 21:54:04 +00:00
xarmian 8edb9225b4 Merge pull request #72 from xarmian/fix/case-insensitive-item-refs
fix: make item ref parsing case-insensitive (BUG-22)
2026-04-07 15:27:15 -04:00
xarmian 23af946b58 fix: make item ref parsing case-insensitive (BUG-22)
Uppercase the input in parseItemRef() so "task-5", "Task-5", and
"TASK-5" all resolve correctly. Fixes CLI lookups, API resolution,
and search matching for item references.
2026-04-07 19:26:59 +00:00
xarmian bde15d45ca Rename Phases to Plans, clean up deprecated aliases (#71)
* Rename "Phases" to "Plans" and clean up deprecated phase aliases

Renames the default "Phases" collection to "Plans" across the full stack:
- DB migration renames existing collections in-place (name, slug, prefix PLAN, icon 🗺️)
- Removes all deprecated Phase* backward-compat aliases from models and store
- Removes --phase CLI flag (use --parent instead)
- Updates convention triggers: on-phase-start/complete → on-plan-start/complete
- Updates dashboard API: active_phases → active_plans, /phases-progress → /plans-progress
- Updates all frontend components, types, and documentation

Closes IDEA-124

* Fix CSRF cookie not being cleared on logout

The SessionAuth middleware was re-issuing a CSRF cookie before the
logout handler could clear it, resulting in two Set-Cookie headers.
Skip CSRF re-issue for /api/v1/auth/ paths since auth endpoints
manage their own CSRF cookies (login sets, logout clears).

* Fix migration issues found in Codex review

- P1: Move doc_type UPDATE from migration 024 into 025, which recreates
  the table with the new CHECK constraint first (SQLite enforces CHECK
  on UPDATE, so the old constraint would reject 'plan')
- P1: Add PostgreSQL migration 005 for the collection rename (phases →
  plans) — previously only existed on the SQLite path
- P2: Recreate FTS triggers, indexes, and rebuild FTS after the table
  swap in migration 025 (DROP TABLE drops associated objects in SQLite)

* Fix parent filter field name and sync .agents skill copy

Codex review round 2 findings:
- P1: Parent filter compared against `parent_id` (wrong) instead of
  `parent_link_id` — plan filtering in collection view was broken
- P1: .agents/skills/pad/SKILL.md still had old --phase flags and
  "Phases" references — synced from the updated .claude copy
- P2: Accept legacy 'phase' filter key for backward compat with
  existing saved views that serialized the old key name

* Fix PG migration JSONB casting and add slug collision guards

Codex PR review bot findings:
- P1: PostgreSQL REPLACE/LIKE don't work on JSONB columns — cast
  schema::text and fields::text before string ops, then back to ::jsonb
- P1: If a workspace already has a custom 'plans' collection, the
  rename hits UNIQUE(workspace_id, slug) — added NOT EXISTS guard
  to both SQLite and PostgreSQL migrations
2026-04-07 14:55:23 -04:00
xarmian 063ff92d00 feat: generalized parent/child items with progress tracking (#70)
* feat: generalize parent/child items — any item can have children with progress tracking

Replaces the phases-only task widget with a generalized parent/child system.
Any item (Phase, Idea, Doc, Task, etc.) can now be a parent of child items,
getting automatic progress bars, burndown charts, status grouping, drag-drop
reordering, and recursive expand/collapse up to 3 levels deep.

DB: migrate link_type 'phase' → 'parent' (migration 023)
Store: generalized methods (GetChildItems, GetItemProgress, SetParentLink
  with cycle detection), drop collection filters, per-child terminal status
API: new /items/{slug}/children and /items/{slug}/progress endpoints
Frontend: ChildItems, ChildChart, NestedChildren components replace PhaseTasks
CLI: --parent flag (--phase kept as hidden alias), list/show/changelog updated
Docs: CLAUDE.md, SKILL.md, pad-web updated for parent/child model

Full backward compatibility: old 'phase' link_type, --phase flags, phase_id/
phase_ref JSON fields all still work as deprecated aliases.

Closes PHASE-16 (9 tasks).

* fix: update collection list page to use item_id from phasesProgress response

The TS client return type changed from phase_id to item_id but the
collection list page still referenced p.phase_id, causing svelte-check
type errors in CI.

* fix: address Codex review findings — PG migration, terminal statuses, metrics, CSRF resilience

- Add PostgreSQL migration 003 to rename 'phase' links to 'parent'
- Use schema-defined terminal statuses in GetAllItemProgress instead of hardcoded defaults
- Pass computed terminal statuses from page to ChildItems component
- Only special-case 'parent' field key when not defined in collection schema
- Move MetricsMiddleware before Recoverer so panics are counted
- Always mount ChildItems for SSE subscriptions even with 0 children
- Exclude soft-deleted children from has_children enrichment query
- Re-issue CSRF cookie when session is valid but cookie is missing
- Show actual API error messages in create-item toasts
2026-04-07 09:52:31 -04:00
xarmian 889c341dc0 Merge pull request #69 from xarmian/phase-14/production-ops
Phase 14: Production Ops — Metrics, SSE Limits, Audit Trail, Deploy & Backup
2026-04-06 21:30:43 -04:00
xarmian 34ebf31fdf fix: resolve Codex review findings across SSE, audit, metrics, and deployment
- Make SSE limit checks atomic with subscription via SubscribeIfAllowed to
  prevent TOCTOU races where concurrent requests bypass connection caps
- Replace fmt.Sprintf JSON assembly with json.Marshal (auditMeta helper) in
  all audit log call sites to prevent silent JSONB insert failures on
  PostgreSQL when metadata contains special characters
- Pass database credentials via PGDATABASE env var instead of pg_dump/psql
  command-line args to avoid leaking passwords in ps/proc output
- Fix audit-log query builder to rebind placeholders once after all filters
  are appended, preventing duplicate $1 placeholders on PostgreSQL
- Replace per-workspace SSE GaugeVec with a single Gauge to avoid unbounded
  Prometheus label cardinality in multi-tenant deployments
- Fix prod Docker Compose: override PAD_REDIS_URL with password and add
  authenticated Redis healthcheck when REDIS_PASSWORD is set
2026-04-07 01:13:44 +00:00
xarmian 77d756c677 fix: address Codex review findings for backup and audit trail
P1 fixes:
- Use --dbname=URL for pg_dump/psql so SSL params, timeouts, and
  other connection options from PAD_DATABASE_URL are preserved
- Add --clean --if-exists to pg_dump so restores can overwrite an
  existing database without duplicate-key errors

P2 fixes:
- Add logAuditEventForUser() to pass explicit user ID for auth events
  (login, register, bootstrap, password_reset) where the request
  context doesn't yet have the authenticated user
- Change audit log --actor filter to match user_id column instead of
  actor type, so filtering by specific user actually works
2026-04-06 02:22:23 +00:00
xarmian 1d26283752 feat: add PostgreSQL backup, restore, and migration CLI commands
- pad db backup: wraps pg_dump with --output and --cron flags
- pad db restore: wraps psql with confirmation prompt and --force
- pad db migrate-to-pg: one-time SQLite→PostgreSQL migration using
  application-level export/import for all workspace data
- docs/backup.md: comprehensive backup strategy guide covering SQLite,
  PostgreSQL, cloud snapshots, and disaster recovery
2026-04-06 02:01:51 +00:00
xarmian af2755d5af docs: add deployment documentation, Docker Compose, and K8s manifests
Provide production-ready deployment configurations:
- docker-compose.yml: Pad + PostgreSQL + Redis single-command setup
- docker-compose.prod.yml: production overlay with resource limits
- deploy/k8s/: Kubernetes manifests (deployment, service, ingress, HPA)
- deploy/Caddyfile: Caddy reverse proxy with auto-TLS
- deploy/nginx.conf: nginx config with SSE-friendly proxy settings
- docs/deployment.md: environment variable reference, architecture
  diagram, quick start, production checklist
2026-04-06 01:58:04 +00:00
xarmian 872f08aa84 feat: add compliance audit trail with IP/UA tracking
Extend the activities table to capture IP address and user agent for all
state-changing operations. Add audit events for auth (login, logout,
register, bootstrap, password changes), workspace management (member
invite/remove, role changes), token lifecycle, and admin settings.

- SQLite migration recreates activities table with nullable workspace_id,
  new ip_address/user_agent columns, and relaxed CHECK constraints
- PostgreSQL migration adds columns and drops constraints
- New ListAuditLog store method with action/actor/workspace/date filters
- GET /api/v1/audit-log endpoint (admin-only)
- CLI: pad workspace audit-log [--days N] [--actor X] [--action X]
2026-04-06 01:55:06 +00:00
xarmian 82e93d6014 feat: implement SSE connection limits
Add configurable global and per-workspace SSE connection limits to
prevent memory exhaustion from unbounded connections. Returns HTTP 429
when limits are reached. Logs warnings at 80% capacity.

Configurable via PAD_SSE_MAX_CONNECTIONS (default 1000) and
PAD_SSE_MAX_PER_WORKSPACE (default 100), or config.toml.

Adds WorkspaceSubscriberCount to EventBus interface for per-workspace
tracking (MemoryBus iterates subscribers, RedisBus uses existing
wsCounts map).

Resolves TASK-165
2026-04-06 01:23:34 +00:00
xarmian 20fbb45de9 feat: add Prometheus metrics and /metrics endpoint
Instrument the Go server with Prometheus metrics for production
monitoring. Adds HTTP request count/duration/size histograms (by
method, route pattern, status), SSE connection gauges per workspace,
event bus publish/subscriber counts, and database connection pool
stats via callback collector. Go runtime metrics included.

The /metrics endpoint is unauthenticated (standard for Prometheus
scraping), separated from the auth middleware via chi router groups.

Resolves TASK-164
2026-04-06 01:13:40 +00:00
xarmian 190481f2d1 Merge pull request #68 from xarmian/phase-13/cloud-infrastructure
feat: Cloud infrastructure & scaling (PHASE-13)
2026-04-05 20:36:27 -04:00
xarmian 754033f559 fix: use dialect booleans for view and webhook creation on PostgreSQL
- views.is_default: replace literal 0 with s.dialect.BoolToInt(false)
- webhooks.active: replace literal 1 with s.dialect.BoolToInt(true)

Both columns are BOOLEAN in the PostgreSQL schema; integer literals
cause type errors with pgx.
2026-04-06 00:29:35 +00:00
xarmian 935b3d7b0e fix: PG agent role reorder, JSONB tag filters, Redis credential leak
- Rebind prepared statements in agent role and card reordering
  so PostgreSQL receives $1/$2 instead of ? placeholders
- Add JSONArrayContains dialect method: SQLite uses LIKE, PostgreSQL
  uses jsonb @> operator — fixes tag filtering on JSONB columns
- Redact Redis credentials from startup log: log addr+db only,
  not the full connection URL which may contain passwords
2026-04-06 00:20:20 +00:00
xarmian fa3aee6561 fix: SQL injection in field filters, document search ranking, item search ordering
- Sanitize field-filter keys from query params before interpolating
  into JSON path expressions — prevents SQL injection via crafted
  query parameter names (affects both SQLite and PostgreSQL)
- Fix document search ORDER BY rank on PostgreSQL — the PG query
  path doesn't expose a `rank` column; use ts_rank() with DESC
- Fix item search rank ordering in ListItems and SearchItems for
  PostgreSQL — ts_rank() needs DESC (higher = more relevant)
2026-04-05 23:59:53 +00:00
xarmian 55101e9680 fix: remaining PostgreSQL boolean/ranking issues from Codex review
- Convert isDiff int→bool in version inserts (items.go, documents.go)
  using s.dialect.BoolToInt() for cross-driver compatibility
- Fix boolean parameter writes in export.go ImportWorkspace
  (is_default, pinned, is_diff all used int literals)
- Fix webhook CASE expression: return FALSE instead of 0 for
  PostgreSQL BOOLEAN active column
- Fix search rank ordering: PostgreSQL ts_rank() uses DESC (higher
  = more relevant) vs SQLite bm25() ASC (more negative = better)
- Fix pinned WHERE clauses: use TRUE/FALSE instead of 1/0 for
  PostgreSQL BOOLEAN compatibility
2026-04-05 20:55:54 +00:00
xarmian 63d5cb7b73 fix: address Codex review findings for PostgreSQL compatibility
P1: Wrap store helper queries (uniqueSlug, uniqueSlugExcluding,
backfillItemNumbers) with s.q() for placeholder rebinding.
P1: Replace boolToInt() with s.dialect.BoolToInt() so pgx receives
native booleans instead of 0/1 integers.
P1: Change boolean scan variables from int to bool to match
PostgreSQL's native boolean type.
P1: Fix FTS table aliases in PostgreSQL search branches.
P1: Make api_tokens.workspace_id nullable for user-scoped tokens.
P2: Move eventBus.Close() before srv.Shutdown() so SSE handlers
drain before the HTTP server shutdown deadline.
2026-04-05 20:45:38 +00:00
xarmian 313bc48419 fix: allow SvelteKit inline scripts in CSP to prevent white screen
The script-src 'self' CSP directive blocked SvelteKit's inline bootstrap
scripts, causing a white screen on mobile browsers which enforce CSP
strictly. Add 'unsafe-inline' as a temporary fix until nonce-based CSP
is implemented (TASK-163).
2026-04-05 20:10:58 +00:00