* feat: add pagination and sorting to search API
Extend the search endpoint with limit/offset pagination and sort options.
The response now includes total count (from a separate count query) so
frontends can paginate properly.
- Add Limit, Offset, Sort, Order to SearchParams with Normalize() defaults
- Return SearchResponse struct with total/limit/offset metadata
- Count query runs alongside results query for accurate totals
- Sort options: relevance (default), created_at, updated_at, title
- Add --sort, --limit, --offset flags to CLI search command
- Update frontend SearchFilters and SearchResponse types
- Add TestSearchPagination and TestSearchSorting integration tests
* fix: count ref hits in search totals and handle empty pages
- Ensure total is never less than actual results when direct ref
matches (e.g. "TASK-5") aren't captured by the FTS count query
- Handle empty page in CLI output: show "No results on this page"
instead of an invalid descending range like "Showing 11-10 of 5"
Addresses codex review on PR #123.
* fix: paginate ref hits correctly and add sort tie-breaker
- Ref hits now occupy slots on page 0 only; FTS limit/offset adjusted
so combined results respect the requested pagination contract
- On subsequent pages, ref hits are excluded (already shown on page 0)
- Add i.id as deterministic tie-breaker to all ORDER BY clauses to
prevent duplicate/missing items across paginated pages
Addresses codex review on PR #123.
* feat: add collection and field filtering to search API
Extend the /search endpoint to support scoping by collection slug and
filtering by structured field values (status, priority, and generic
field.* params). Works on both SQLite FTS5 and PostgreSQL tsvector.
- Add Collection and FieldFilters to SearchParams (store layer)
- Parse collection, status, priority, field.* query params (handler)
- Add SearchFilters type and update api.search() signature (frontend)
- Add --collection, --status, --priority flags to CLI search command
- Add integration tests for collection, field, and combined filtering
* fix: validate field filter keys to prevent SQL injection
Reject field filter keys containing special characters before they
reach JSONExtractText, which interpolates keys directly into SQL.
Keys must match ^[a-zA-Z][a-zA-Z0-9_-]*$ — validation is applied
in both the handler and the store layer as defense in depth.
Addresses codex review on PR #122.
Add star/unstar/starred CLI commands (PLAN-564, TASK-570):
- pad item star <ref> — star an item
- pad item unstar <ref> — unstar an item
- pad item starred [--all] [--format json] — list starred items
Client methods: StarItem, UnstarItem, ListStarredItems.
* feat: restore `pad init` as smart multi-step entry point
Adds a top-level `pad init` command that detects the current state and
walks through each setup step as needed: configure connection, start
server, bootstrap admin account, authenticate, create/link workspace,
and install/update AI skill files.
Safe to re-run anytime — skips completed steps and shows a status
summary when everything is healthy.
Also refactors `pad workspace init` to use shared helpers and adds a
hint pointing users to `pad init` when prerequisites are missing.
Ref: IDEA-499, PLAN-546
* fix: validate --template flag before workspace creation in pad init
Adds the same preflight template validation that workspace init has,
preventing a partially initialized workspace from being created when
an invalid template name is passed.
Ref: PR #99 review feedback
The db commands were PostgreSQL-only, which made them useless for
self-hosted users on the default SQLite setup. Now both commands
auto-detect the database driver and do the right thing:
- SQLite (default): file copy of ~/.pad/pad.db with WAL/SHM handling
- PostgreSQL: existing pg_dump/psql behavior (unchanged)
Replace the email/password terminal prompt in `pad auth login` with a
browser-based auth flow. The CLI creates a pending session, prints a URL
the user opens in their browser (works for localhost, remote VPS, or
Pad Cloud), and polls until the session is approved.
- Add CLI auth session endpoints (create, poll, approve)
- Add browser approval page at /auth/cli/{code}
- Rewrite `pad auth login` to use browser flow by default
- Keep `pad auth login --interactive` as email/password fallback
- Add login page redirect param support for post-login bounce-back
- Add SQLite and PostgreSQL migrations for cli_auth_sessions table
Closes PLAN-539, IDEA-404
* feat: email unsubscribe for non-transactional emails
Add CAN-SPAM compliant unsubscribe support:
- New email_optouts table (by email address, not user ID) so
uninvited recipients can opt out without an account
- HMAC-signed unsubscribe tokens (derived from Maileroo API key)
so links work without authentication
- GET /api/v1/unsubscribe endpoint with simple HTML confirmation page
- Invitation emails now include unsubscribe footer link
- Welcome emails accept unsubscribe URL parameter
- Before sending invitation emails, check opt-out table and silently
skip opted-out addresses (prevents invite spam)
- Password reset emails are exempt (transactional, user-initiated)
Fixes BUG-256.
* fix: hide "Copy invite link" when code is unrecoverable
For hashed invitations the plaintext code can't be recovered, so the
button was copying a broken URL. Now shows "Sent via email" label
instead. Only shows the copy button when join_url or code is available.
Fixes BUG-255.
Add the foundation for running Pad as a hosted service at app.getpad.dev.
Same binary in cloud mode with a thin sidecar for OAuth and Stripe.
Cloud mode (PAD_CLOUD=true):
- PAD_CLOUD flag with cloud secret for sidecar communication
- Account-level billing: plan field on users, CheckLimit enforcement
- Free/Pro tiers with configurable limits stored in platform_settings
- Three-tier limit resolution: user overrides → DB defaults → hardcoded fallback
- Plan enforcement on workspace, item, member, webhook, and token creation
Authentication & security:
- OAuth login endpoint (POST /api/v1/auth/oauth-login) with cloud secret gate
- Verified email requirement for OAuth, 2FA bypass protection
- Cloud secret rotation support (comma-separated keys)
- TOTP secret encryption at rest (AES-256-GCM via PAD_ENCRYPTION_KEY)
- Rate limiting on OAuth login endpoint
- Bootstrap disabled in cloud mode
- Password max length enforcement (128 chars)
- Config file written with 0600 permissions
Admin & billing:
- Admin user management API (list, detail, update plan/overrides)
- Configurable plan limits API (GET/PATCH /api/v1/admin/limits)
- Platform stats endpoint
- Admin plan endpoint for sidecar to set user plans
- GDPR: account deletion and data export endpoints
Console UI (cloud mode only):
- /console — workspace list with owned/shared sections
- /console/new — create workspace wizard with slug preview
- /console/settings — profile, password, API tokens
- /console/billing — plan status, upgrade/manage links
- /console/admin — user management, plan overrides, limits editor
- OAuth buttons (GitHub/Google) on login page in cloud mode
Auto-create default workspace on signup in cloud mode.
Migration 035: plan, plan_expires_at, stripe_customer_id, plan_overrides on users.
- Fix UUID-shaped workspace slugs: resolveWorkspace now falls back to
slug-based lookup when a UUID doesn't match any workspace ID
- Fix imported workspaces: ImportWorkspace accepts ownerID, handler sets
authenticated user as owner and adds workspace membership
- Fix generated username collisions: add EnsureUniqueUsername to append
suffixes (-2, -3, etc.) when auto-generated usernames already exist
- Fix handler-level UUID resolution: workspace CRUD handlers now use
getWorkspace helper (reads middleware-resolved ID from context) instead
of raw URL params with slug-only store methods
- 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
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
* 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
* 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
- 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
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
- 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
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]
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
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
- 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
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.
- 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)
- 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
- 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
* 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).
* 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.
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
* feat: role board — cross-collection view, dashboard breakdown, agent bindings (#PHASE-11)
Add a standalone role board page showing all work organized by agent
role across every collection. This is the "human orchestrator" view —
see at a glance what's queued for each capability and who's working it.
Agent bindings:
- Add `tools` text field to agent_roles table (migration 019)
- CLI: `pad role create "Implementer" --tools "Claude Code + Sonnet"`
- Lightweight notes about preferred tools — no per-user binding table
Dashboard role breakdown:
- `pad project dashboard` now includes `by_role` section
- Shows item count, assigned users, and tools per role
- CLI renders role summary table with icons
Role board API:
- `GET /workspaces/{ws}/roles/board` — items from all collections grouped by role
- Filters terminal-status items (done, cancelled, etc.)
- Supports `?assigned_user_id=X` for "my work" filtering
- Returns role info, items, and assigned user list per lane
Web UI:
- New page at /{workspace}/roles with horizontal lane layout
- Collection badges on cards (items span collections)
- "My Work" toggle to filter by current user
- Empty states for no roles and empty lanes
- Sidebar nav: 🎭 Roles link added
- Responsive: stacks vertically on mobile
Skill:
- References role board in greeting and "who's working on what" patterns
* feat: add assignment picker to item detail page
Replace read-only assignment display with interactive dropdowns for
assigning users and roles directly from the item detail page.
- User dropdown populated from workspace members
- Role dropdown populated from agent roles
- Either can be set or cleared independently
- Saves immediately on change via PATCH API
- Added assigned_user_id/agent_role_id/clear_* to ItemUpdate type
* feat: add role management UI to roles page
Add a "Manage" toggle in the role board header that reveals an inline
panel for creating, editing, and deleting roles directly from the UI.
- Role cards show icon, name, description, tools, and item count
- Edit inline: name, icon, description, tools
- Create new roles with a dashed card form
- Delete with confirmation dialog
- Board auto-refreshes after changes
* refactor: replace inline role management with dialog modal
The inline horizontal card grid was cramped and hard to use. Replace
with a proper <dialog> modal that opens from the ⚙ Manage button.
- Vertical list of role rows with icon, name, description, tools, item count
- Inline edit mode per row with labeled fields
- Create new role form at the bottom with clear field labels
- Click backdrop or ✕ to close, board refreshes on close
- Native dialog handles backdrop, escape key, and focus trapping
* fix: role board mobile layout matches collection kanban, unassigned first
- Unassigned lane now appears first (before role lanes)
- Mobile: horizontal swipe with scroll-snap at 75vw columns, matching
the collection BoardView pattern (no vertical stacking)
* feat: add drag-and-drop between role board lanes
Items can now be dragged between role lanes to reassign their role.
Uses svelte-dnd-action matching the collection BoardView pattern.
- Drag items between role lanes to change role assignment
- Drag to Unassigned lane to clear role
- Drop target highlight on hover
- Touch support with 500ms delay (same as collection board)
- Haptic feedback on mobile drag start
- Board refreshes after drop to sync server state
* fix: auto-assign user on drag to role lane, show unassigned in My Work
- When dragging an unassigned item into a role lane, automatically
assign the current user alongside the role
- "My Work" filter now shows items assigned to you OR items with no
user assignment, so unassigned work remains visible and claimable
* fix: three-state filter on role board — All, My Work, Unassigned
Replace the My Work toggle with a segmented button group offering
three filter modes:
- All: show everything (default)
- My Work: items explicitly assigned to the current user
- Unassigned: items with no user assignment
* fix: replace filter buttons with Highlight Mine toggle
Remove the three-state filter (All/My Work/Unassigned) and replace
with a single "Highlight Mine" toggle that dims cards not assigned
to the current user. All items remain visible and draggable — your
items just visually pop while others fade to 35% opacity (hovering
restores to 70%).
* fix: resolve undefined loadBoard and myWorkOnly in role board page
Replace 6 references to nonexistent `loadBoard()` with `loadData()`
(the actual data-loading function), and replace `myWorkOnly` with
`highlightMine` (the actual state variable). Fixes svelte-check errors
that caused CI Web Build to fail.
* fix: role breakdown pointer aliasing and terminal status filtering
P1: Copy role.ID to a local variable before taking its address in
GetRoleBreakdown, avoiding potential pointer aliasing from the range
variable (safe in Go 1.22+ but clearer with an explicit copy).
P2: Add terminal status exclusion to the GetRoleBreakdown SQL query
so dashboard counts match the board view. Previously, done/completed/
cancelled items were included in role counts, inflating active load.
Addresses Codex review comments on PR #60.
* feat: agent roles — role-based (user, role) assignment for items (#PHASE-9)
Introduce agent roles as a first-class concept for human-agent work
assignment. Roles describe capability specializations (Planner,
Implementer, Reviewer, etc.) and items can be assigned to a (user, role)
pair, enabling natural handoff workflows between different AI tools.
Migration:
- New `agent_roles` table (workspace-scoped, slug-unique)
- `assigned_user_id` + `agent_role_id` columns on `items` with FKs
- Removed legacy `assignee` text field from Tasks schema
Backend:
- AgentRole model + full CRUD store/API
- All item queries updated with LEFT JOINs to resolve assignment
- Item list filtering by assigned_user_id and agent_role_id
- Role transitions tracked in activity feed metadata
CLI:
- `pad role list/create/delete` commands
- `--role` and `--assign` flags on item create/update/list
- Assignment displayed in `pad item show` output
Web:
- TypeScript types + API client for agent roles
- Role badge on item cards in list/board views
- Assignment display on item detail page
* fix: enforce workspace-scoped assignments and fail fast on unresolved --assign filter
Addresses code review feedback from PR #58:
P1: Add validateAssignmentScope() to the store layer, called by both
CreateItem and UpdateItem. Verifies that assigned_user_id belongs to
the workspace (via IsWorkspaceMember) and agent_role_id exists in the
workspace (via GetAgentRole) before writing. Prevents cross-workspace
assignment leaks.
P2: The CLI `pad item list --assign <name>` now errors instead of
silently returning unfiltered results when the member lookup fails or
no workspace member matches the provided name.
* feat: unified item timeline with comment-on-update, threading, and reactions (IDEA-115)
Replace the separate Comments section and Version History modal on the
item detail page with a single chronological timeline that interleaves
comments, activities, and content versions.
Key changes:
- Add --comment flag to `pad item update` so agents/users can explain
status changes inline (creates a comment linked to the activity)
- Add threaded replies (parent_id on comments) with inline reply UI
- Add emoji reactions on comments (new comment_reactions table)
- New /timeline API endpoint merges comments, activities, and versions
server-side with dedup and collapsing of rapid edits
- New Svelte timeline components: ItemTimeline, TimelineCommentCard,
TimelineActivityCard, TimelineVersionCard, ReactionPicker
- Remove Implementation Notes and Decision Log inputs from web UI
- Update skill docs to encourage --comment on status changes
* fix: address review findings for PR #54 (iteration 1)
- Use ListItemVersions instead of ListVersions in timeline endpoint
so item content history renders correctly
- Add workspace validation to reply and reaction handlers to prevent
cross-workspace comment mutation
- Raise activity cap from 500 to 10000 to avoid silently truncating
long timelines
- Fix toggleReaction to always POST (idempotent) instead of incorrectly
matching other users' reactions for DELETE
- Await onReply promise before clearing draft to prevent duplicate
submissions and lost drafts on failure
- Register reaction_added/reaction_removed in SSE ITEM_EVENTS so
reactions from other sessions appear in real-time
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address review findings for PR #54 (iteration 2)
- Fix nil pointer dereference in timeline handler when item not found
- Store empty string instead of NULL for reaction user_id so UNIQUE
constraint works correctly in SQLite
- Add workspace validation to handleDeleteComment (cross-workspace
deletion was possible)
- Fix SKILL.md duplicate numbering (4. appeared twice)
- Remove || true debug artifacts from reaction conditionals
- Replace SvelteMap with plain Map in non-reactive groupReactions
- Use != null checks for timeline API params to handle offset=0
- Log warning on comment creation failure during item update
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix all 17 svelte-check warnings across 7 components
- Add tabindex to toolbar role elements (BoardView, Editor)
- Replace nested buttons with div[role=button] in ListView group headers
- Add role="none" to click-to-close backdrop overlays (Editor, slug page)
- Fix label→span for non-input field labels (CreateWorkspaceModal)
- Add keyboard handlers to interactive divs (CreateWorkspaceModal drop zone, slug page modal)
- Remove unused .slash-backdrop CSS (Editor) and input[type=text] selector (CreateWorkspaceModal)
- Fix state_referenced_locally in RawMarkdownEditor ($state init)
- Add svelte-ignore for conditional tabindex false positive (ToastContainer)
* feat: add build version, commit hash, and timestamp to CLI and web UI
Inject version info via ldflags during build (Makefile, GoReleaser,
Dockerfile). Expose version/commit/build_time in the health API
endpoint and display it in the sidebar footer. Dev builds show
"dev (abc1234 ...)", releases show "v1.2.3 (abc1234 ...)".
Track all skill installations in ~/.pad/installations.json so
`pad install --update` can update stale skill files across every
project in one shot. Also fixes false Copilot detection on projects
that have .github/ for CI but don't use Copilot.
- Add Installation registry (internal/cli/registry.go) with
record, prune, status, and update-all operations
- Record installations from all install code paths (install,
init, skills install, interactive, --all, --update)
- `pad install --list` / `pad skills status` now show tracked
installations across all projects with freshness indicators
- `pad install --update` / `pad skills update` now update stale
files globally, not just the current directory
- `pad skills update` and `pad skills status` delegate to the
same logic as `pad install --update` and `pad install --list`
- Fix Copilot detection: use .github/copilot or .github/instructions
instead of .github (which exists on most projects for CI)
- Add .codex to agents detection directories
- `pad init` on already-linked workspaces now records existing
installations in the registry
* feat: email-based password reset flow (IDEA-81)
Add full password reset flow: forgot password request, time-limited
reset tokens (1hr, single-use, SHA-256 hashed), new password form,
and automatic session creation after reset.
* fix: use all: prefix in go:embed to include _-prefixed files
Go's embed package excludes files starting with _ or . when recursing
directories. SvelteKit/Vite occasionally generates chunk filenames with
_ prefixes (e.g. _VLZtjCJ.js), causing them to be silently dropped
from the embedded filesystem and served as HTML by the SPA fallback.
The all: prefix includes everything regardless of filename prefix.
Fixed in both embed.go and the Makefile which regenerates it.
* fix: address PR review — atomic token consumption, error handling, log reset URL
- Replace ValidatePasswordReset + MarkPasswordResetUsed with atomic
ConsumePasswordReset using UPDATE ... WHERE ... RETURNING to prevent
race conditions where two concurrent requests consume the same token
- Handle DeleteUserSessions errors (log instead of silently ignoring)
- Log the full reset URL when email is not configured so the admin
CLI fallback is actually usable