Commit Graph

177 Commits

Author SHA1 Message Date
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
xarmian a4a701367a feat: add PostgreSQL support with dual-driver store layer (TASK-157)
- Create Dialect abstraction for SQLite/PostgreSQL SQL differences
  (JSON ops, FTS, placeholders, datetime, aggregation)
- Add Store.NewPostgres() constructor with connection pooling
- Create consolidated PostgreSQL schema (pgmigrations/001_initial.sql)
  with tsvector FTS, JSONB columns, and GIN indexes
- Refactor all store queries (~150) to use s.q() for placeholder rebinding
- Replace hardcoded json_extract/FTS5/GROUP_CONCAT with dialect methods
- Support PAD_DB_DRIVER=postgres + PAD_DATABASE_URL env vars
- Keep SQLite as the default for local/self-hosted mode
- Add dialect unit tests (rebind, SQLite, PostgreSQL)
2026-04-05 18:50:50 +00:00
xarmian b9d0a89195 feat: add Redis pub/sub EventBus for multi-instance SSE (TASK-158)
- Extract EventBus interface (Subscribe, Unsubscribe, Publish, Close)
- Rename Bus → MemoryBus, keeping it as the default for single-instance
- Add RedisBus implementation with per-workspace channel subscriptions
- Lazy Redis subscribe/unsubscribe as SSE clients connect/disconnect
- Configure via PAD_REDIS_URL env var; falls back to in-memory without it
- Update Server.SetEventBus to accept the EventBus interface
2026-04-05 15:16:57 +00:00
xarmian e7f4448028 feat: add readiness probe and structured logging (TASK-161)
- Add /health/live (liveness) and /health/ready (readiness with DB check) endpoints
- Add Store.Ping() for database connectivity verification
- Create internal/logging package using stdlib log/slog
- Support PAD_LOG_LEVEL (debug/info/warn/error) and PAD_LOG_FORMAT (text/json) env vars
- Add structured request logging middleware replacing chi's default Logger
- Migrate all log.Printf calls to slog with proper levels and key-value attrs
- Exempt health probe endpoints from auth middleware
2026-04-05 15:02:54 +00:00
xarmian dab6d6c9c9 feat: implement graceful shutdown with request draining (TASK-159)
- Add signal handler for SIGINT/SIGTERM with 30s grace period
- Add Server.Shutdown() for graceful HTTP connection draining
- Add EventBus.Close() to cleanly terminate SSE subscribers
- Configure HTTP server timeouts (read: 15s, header: 5s, idle: 120s)
- Fix SetWebUI nil router panic by calling ensureRouter()
- Add Server.Handler() for httptest compatibility
2026-04-05 14:56:56 +00:00
xarmian 8aa6481421 PHASE-12: Security Hardening for Pad Cloud (#67)
* feat: enforce RBAC role checks on all mutation endpoints (TASK-150)

Add requireMinRole helper and role enforcement to 30+ mutation handlers.
Viewers are now blocked from all state-changing operations, editors can
mutate items/docs/comments/views but not collections/webhooks/workspace
settings, and only owners can perform administrative operations.

Includes 11 integration tests with real auth covering viewer/editor/owner
access across items, collections, documents, comments, agent roles,
item links, and workspace operations.

* fix: scope search results to user's workspaces (TASK-151)

Search without a ?workspace= param previously returned results from all
workspaces in the database. Now the handler resolves the authenticated
user's workspace memberships and passes their IDs to the store query,
ensuring results only include items from workspaces the user belongs to.

Fresh installs (no users) retain unscoped search for backward compat.
Includes integration test proving cross-workspace isolation.

* fix: add webhook URL validation and SSRF protection (TASK-152)

Webhook creation now validates URLs before accepting them: only HTTP(S)
schemes allowed, embedded credentials rejected, private/reserved IPs
blocked (loopback, RFC1918, link-local, cloud metadata 169.254.169.254),
and hostnames are DNS-resolved to verify they don't point to private IPs.

Defense-in-depth check also added to the dispatcher's deliver function
so existing webhooks with unsafe URLs are blocked at delivery time.

* feat: add CSRF protection with double-submit cookie pattern (TASK-153)

Implements CSRF middleware that validates X-CSRF-Token header matches
the pad_csrf cookie on all state-changing API requests. Bearer token
auth, auth endpoints, and fresh installs are exempt. The frontend
client reads the CSRF cookie and attaches the header on mutations.

* feat: add per-endpoint rate limiting middleware (TASK-154)

Adds IP-based rate limiting for auth endpoints (5/min login, 3/hr
password reset, 5/hr registration) and user-based limits for API
(100/min) and search (30/min). Uses golang.org/x/time/rate with
automatic stale-entry cleanup. Adds chi RealIP middleware for
correct client IP behind proxies. Returns 429 with Retry-After.

* fix: sanitize error responses and remove PII from logs (TASK-155)

Replace all writeError(500, err.Error()) calls with writeInternalError
that logs the real error server-side and returns a generic message to
clients. Remove email addresses, user IDs, and password reset tokens
from log output to prevent PII leakage.

* feat: add security headers, configurable CORS, and secure cookies (TASK-160)

Add SecurityHeaders middleware (CSP, X-Frame-Options, nosniff,
Referrer-Policy, Permissions-Policy). Make CORS origins configurable
via PAD_CORS_ORIGINS env var. Add PAD_SECURE_COOKIES for TLS
deployments (sets Secure flag on session/CSRF cookies and enables
HSTS). Also adds X-CSRF-Token to CORS allowed headers.

* fix: address PR review — lazy router init and trusted IP for rate limits

Fix two issues flagged by Codex:

1. CORS/HSTS config was ignored because setupRouter() ran in New()
   before SetCORSOrigins/SetSecureCookies were called. Now uses
   sync.Once to lazily build the router on first ServeHTTP/Listen.

2. Rate limiter read X-Real-IP directly from untrusted headers,
   allowing clients to spoof IPs. Now uses RemoteAddr only (which
   chimiddleware.RealIP already sanitizes from trusted proxy headers).
2026-04-05 10:26:00 -04:00
xarmian 367116b3a0 Unify relation fields and item links into single dependency system (#66)
* feat: unify relation fields and item links into single dependency system

Phase membership (Task→Phase) was previously stored as a UUID in the
item's fields JSON, separate from the item_links table used for
blocks/related/implements relationships. This unifies both into the
item_links table so all item relationships use one system.

Backend:
- Add 'phase' link type to item_links constants
- Migration 021: migrate existing phase field values to item_links,
  strip phase from fields JSON, remove phase field from tasks schema
- Rewrite GetPhaseProgress, GetAllPhasesProgress, GetTasksForPhase
  to JOIN on item_links instead of json_extract(fields, '$.phase')
- Add SetPhaseLink, ClearPhaseLink, GetPhaseForItem, GetTaskPhaseMap
  store helpers with single-phase constraint enforcement
- Create/update handlers intercept 'phase' in fields and route through
  links system; enrich item responses with phase_id/ref/title
- Dashboard orphan detection uses batch GetTaskPhaseMap lookup
- Add PhaseID filter to ItemListParams for link-based list filtering

Frontend:
- Remove relation field type from FieldEditor (no longer needed)
- Add link CRUD UI to item detail page: "Add relationship" inline form
  with link type picker + item search, delete buttons on existing links
- Phase links appear in Relationships section as "In phase"/"Phase"
- ItemCard reads phase from item.phase_title instead of fields.phase
- FilterBar phase filter uses item.phase_id for client-side filtering
- Add api.links.delete to frontend API client
- Fix duplicate {#each} key on dashboard attention list

Implements IDEA-106.

* fix: remove relationLabels prop from BoardView, ListView, TableView

ItemCard no longer accepts relationLabels (phase info now comes from
item.phase_title), so remove the prop from all parent view components
that were passing it through. Also remove unused .cell-relation CSS.

* fix: address PR review — atomic SetPhaseLink, migration safety, error handling

1. Migration 021: remove deleted_at filters so archived tasks and tasks
   pointing to archived phases also get their phase links migrated.

2. SetPhaseLink: wrap delete+insert in a transaction so a failed insert
   doesn't leave the item with no phase link (previously non-atomic).

3. Create/update handlers: return proper HTTP errors when phase link
   operations fail instead of logging warnings and returning 200 OK.
2026-04-04 19:47:57 -04:00
xarmian e21da3c6a4 fix: schema-driven terminal statuses replace hardcoded lists (BUG-17) (#65)
Add `terminal_options` to collection field schemas so each collection
declares which statuses are terminal/finalized. Replaces 10+ inconsistent
hardcoded status lists across backend, CLI, and frontend.

- Add TerminalOptions to FieldDef and centralized helpers in models/terminal.go
- Populate terminal_options on all default and template collections
- Replace hardcoded isDoneStatus/isTerminalItemStatus with schema-aware lookups
- Fix phase progress to count all terminal statuses (not just "done")
- Add terminal status toggle UI in collection field editor (Settings → Fields)
- Redesign collection field editor for cleaner layout and alignment
- Move Platform settings tab before Danger Zone tab
2026-04-04 18:56:30 -04:00
xarmian 8d00ae822d fix: relation field UUID display + link bubble auto-show (BUG-18, BUG-10) (#64)
* fix: show phase title instead of UUID in relation fields

FieldEditor only loaded relation items when the dropdown was opened,
so the initial render showed the raw UUID. Now eagerly fetches relation
items on mount when the field has a value, and shows a loading state
while fetching.

Fixes BUG-18.

* fix: prevent link bubble from auto-showing on page load

The EditorLinkPopover subscribed to the transaction event which fires
during initial document load. If the cursor landed inside a link, the
popover appeared without user interaction. Removed the transaction
listener — selectionUpdate alone correctly handles user-initiated
cursor changes.

Fixes BUG-10.
2026-04-04 16:27:59 -04:00
xarmian 405ef8b629 fix: align editor block drag handle with text and checkboxes (#63)
The drag handle was vertically misaligned because the offset was
hardcoded to -12px (assuming 24px half-height) but the handle is 32px
tall. Also, for flex containers like task list items, the handle
aligned to the full <li> height instead of the checkbox row.

Now uses dynamic handle height and computed line-height for centering,
and for flex/grid containers measures the first child element so the
handle aligns with the checkbox/content row.

Fixes BUG-21.
2026-04-04 16:10:52 -04:00
xarmian 83f3d99b70 fix: support searching by item ref (BUG-20) (#62)
* fix: support searching by item ref (e.g. TASK-5, BUG-8)

Search only used FTS5 on title/content/tags, but item refs are computed
from collection prefix + item number and were never indexed. Additionally,
FTS5 interpreted hyphens as NOT operators, causing SQL errors for
ref-like queries.

Add direct ref lookup before FTS so exact ref matches always appear
first, and sanitize FTS queries by quoting tokens to prevent special
character interpretation.

Fixes BUG-20.

* fix: return all ref matches across workspaces in search

Address Codex review: use Query instead of QueryRow for ref lookup so
unscoped searches (no workspace filter) return matching refs from all
workspaces, since refs are not globally unique.
2026-04-04 15:56:44 -04:00
xarmian edcf2ae8b0 Board view improvements: independent scrolling, unified cards, lane reorder, new-item modal (#61)
* feat: independent board scrolling, unified card style, lane reordering, and new-item modal

- BoardView: switch from CSS grid to flex layout with independent per-column
  scrolling, matching the Roles board UX
- ItemCard: redesign to match Roles board card style — top row with optional
  collection badge + ref, compact meta row with colored status/priority text
- Roles board: replace inline card markup with shared ItemCard component,
  add HTML5 drag-and-drop lane reordering (persisted via new API endpoint),
  rename "Highlight Mine" to "Mine", add "+ New" button with collection
  picker modal
- Backend: add PUT /roles/board/lane-order endpoint and UpdateAgentRoleOrder
  store method for batch role sort_order updates
- Collection page: board view now fills viewport height so columns have
  bounded scroll areas

* fix: resolve svelte-check type error and remove unused CSS selectors

- Add null guard on lane.role in openEditModal onclick
- Remove unused .role-edit-actions, .role-btn-create, .role-btn-cancel CSS

* fix: correct lane reorder insert index when dragging forward

After splicing out the source lane, downstream indices shift left by one.
Adjust the insert index when srcIdx < dstIdx to place the lane at the
correct drop target position.
2026-04-04 13:02:12 -04:00
xarmian 2f9e61ad9c fix: within-lane reorder now persists sort order
The early return for same-lane drops (oldKey === key) was skipping
the sort persistence code entirely. Restructured handleDndFinalize
so the cross-lane role update is a nested conditional, and the sort
order persistence always runs regardless of whether a cross-lane
move occurred.
2026-04-04 14:10:13 +00:00