Commit Graph

109 Commits

Author SHA1 Message Date
xarmian 38e6b4e5b1 feat: rewrite web UI routing to /{username}/{workspace}/... pattern
Restructure all workspace-scoped web URLs to include the owner's
username as a prefix (TASK-411).

Route structure:
- Moved web/src/routes/[workspace]/ → [username]/[workspace]/
- All workspace pages extract both username and workspace from URL
- Auth routes (/login, /register, /join, etc.) unchanged

Backend:
- Workspace model adds OwnerUsername field (populated by JOIN)
- All workspace queries JOIN users table for owner_username
- TypeScript Workspace type updated with owner_username

Frontend (24 files updated):
- All route pages: added username derived, updated URL constructions
- Sidebar, TopBar, WorkspaceSwitcher: use owner_username for links
- ItemCard, TableView, ChildItems, NestedChildren: username in links
- CommandPalette, OnboardingChecklist, CreateWorkspaceModal: updated
- Root page redirect includes owner_username
- Wiki-link markdown utility accepts username parameter

What did NOT change:
- API client (client.ts) — still uses workspace slug for API calls
- Go API routes — unchanged
- CLI — unchanged
2026-04-10 23:47:25 +00:00
xarmian b1357799a9 feat: username validation, reserved words, and registration flow
Add username support to registration with validation, reserved words,
and real-time availability checking (TASK-409).

Backend:
- ValidateUsername() with format/length/reserved word checks
- 35+ reserved usernames (route conflicts, system terms)
- GET /auth/check-username endpoint for real-time validation
- handleBootstrap auto-generates username from name (D1)
- handleRegister accepts optional username, auto-generates if omitted

Frontend:
- Register page: username field with auto-generation from name
- Join/invite page: same username field in register mode
- Debounced availability checking (400ms) via /auth/check-username
- Inline status indicators (checking/available/taken)
- API client: register() accepts username, new checkUsername() method
2026-04-10 23:12:59 +00:00
xarmian 520a7ca27b feat: add owner_id to workspaces with backfill
Add owner_id column to workspaces table and backfill existing
workspaces from membership data (TASK-410 + TASK-480).

- SQLite migration 030 and Postgres migration 010 add owner_id column
  with indexes on (owner_id) and (owner_id, slug)
- Workspace, WorkspaceCreate models updated with OwnerID field
- All workspace SELECT queries include owner_id
- CreateWorkspace INSERT includes owner_id
- handleCreateWorkspace sets owner_id from authenticated user
- backfillWorkspaceOwners extended: sets owner_id using D3 logic
  (earliest owner member → earliest member → first admin)
- TypeScript Workspace type updated

Global UNIQUE(slug) constraint preserved for now; will be replaced
with UNIQUE(owner_id, slug) in TASK-412 when auth-scoped resolution
needs it.
2026-04-10 22:56:32 +00:00
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 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 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 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 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 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 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 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 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 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 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
xarmian 034f169a02 fix: keep isDragging true until lanes state is updated
The $effect that syncs laneData from orderedLanes was firing when
isDragging was set to false, overwriting the optimistic sort order
with stale data. Now isDragging stays true until after lanes state
is updated with the new sort order, so the $effect sees correct data.
2026-04-04 14:07:00 +00:00
xarmian 8b1b46ef22 feat: persistent card ordering within role board lanes
Add role_sort_order column to items for independent ordering in the
role board, separate from the collection sort_order.

Backend:
- Migration 020: role_sort_order INTEGER column on items
- Item model, all SELECT/INSERT/Scan queries updated
- PUT /roles/board/reorder endpoint for batch sort updates
- Board API sorts items by role_sort_order within each lane

Frontend:
- Within-lane drag reorder persists via reorder API
- Cross-lane moves also persist new sort order
- Both operations are optimistic (no page refresh)
2026-04-04 14:00:21 +00:00
xarmian 64ab4fe1d3 fix: remove assignee pills from lane headers, show on cards only
Assigned user names were shown both in the lane header as pills and
on individual item cards. Remove the lane header pills — the card
already shows the assigned user, and duplicating it in the header
added clutter without information.
2026-04-04 13:26:46 +00:00
xarmian 0cf8f34496 fix: center role dialog on mobile, shrink add-role button
- Dialog uses fixed positioning with translate(-50%, -50%) for
  reliable viewport centering on all screen sizes
- Add Role column no longer stretches to full lane width/height —
  sized to its content with align-self: flex-start
2026-04-04 13:24:37 +00:00
xarmian a3e8ccc4ca refactor: per-lane edit buttons and add-column replaces manage modal
Remove the ⚙ Manage button and its full-list modal. Instead:

- Each role lane header has a ✎ edit button (appears on hover)
  that opens a focused dialog for that role's fields + delete
- A dashed "+" column at the far right of the board opens the
  same dialog in create mode
- Empty state has a "Create your first role" CTA
- Dialog is simpler: one role at a time with name, icon,
  description, tools fields, save/cancel/delete
2026-04-04 12:19:30 +00:00
xarmian e958abc20a fix: optimistic drag-and-drop — no page refresh on role reassignment
Replace the loadData() round-trip after dropping with an optimistic
local update. The item moves instantly in lanes state with updated
role fields, and the API call fires in the background. Only reloads
from server on error (revert). No scroll reset, no flash.
2026-04-04 12:06:49 +00:00
xarmian 2635823cd2 fix: role board drop zones extend to bottom of tallest column
Change lanes-container from align-items: flex-start to stretch so all
lanes match the height of the tallest column. Combined with flex: 1 on
lane-items, the drop target area fills the full remaining lane height.
2026-04-04 12:03:32 +00:00
xarmian a2b51693e1 fix: drag-and-drop now shows item in new column immediately
Keep isDragging true during the API call so the $effect doesn't
revert laneData from stale orderedLanes. The item stays visually
in the target lane while the server processes the role change,
then isDragging releases after loadData() refreshes from the server.
2026-04-04 11:59:35 +00:00
xarmian edd259b1b0 feat: role board — cross-collection view, dashboard breakdown, agent bindings (#60)
* 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.
2026-04-04 07:48:33 -04:00
xarmian be576d9e24 feat: agent roles — role-based (user, role) assignment for items (#58)
* 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.
2026-04-03 21:16:38 -04:00
xarmian 481527de02 fix: server-side timeline pagination and reaction toggle (#57)
* fix: server-side timeline pagination and reaction toggle (#55, #56)

Issue #55: Replace in-memory pagination with cursor-based approach.
The /timeline endpoint now accepts `before` (RFC3339 timestamp) and
`limit` params, fetching a small window from each source (comments,
activities, versions) instead of loading everything into memory.
Frontend gets a "Load more" button that passes the oldest entry's
timestamp as the cursor.

Issue #56: Plumb current user ID to reaction toggle. ItemTimeline
fetches the auth session on mount to get the user ID, passes it to
TimelineCommentCard. toggleReaction now checks if the user already
reacted (by matching reaction.user_id) and calls onRemoveReaction
to un-react. Own reaction chips are visually highlighted.

* fix: address review findings for PR #57 (iteration 1)

- Use <= with ID tie-breaker in cursor queries to prevent skipping
  entries at timestamp boundaries; use consistent RFC3339 format
- Treat orphaned replies (parent on different page) as top-level
  entries instead of silently dropping them
- Deduplicate by ID on "Load more" to handle boundary overlap
- SSE reload now prepends new entries and updates existing ones
  instead of clobbering all paginated state
- Parse before cursor with RFC3339Nano fallback for sub-second
  precision
- Fix dangling doc comment on ListItemVersions

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: add global auth store, remove per-component session fetch

Create authStore (web/src/lib/stores/auth.svelte.ts) following the
same pattern as workspaceStore. The root layout populates it on mount.
ItemTimeline now reads currentUserId via $derived(authStore.userId)
instead of making a separate api.auth.session() call on every mount.

* fix: address review findings for PR #57 (iteration 2)

- Add beforeID tie-breaker to all cursor queries to prevent infinite
  Load More loop when entries share the same timestamp
- Over-fetch per source (limit*3) to avoid skipping filtered entries
- SSE refresh now detects deleted entries and removes them from the
  first-page window instead of keeping stale data
- Auth store re-throws on fetch errors so layout can distinguish
  "not authenticated" from "server unreachable"
- Sidebar reads from authStore instead of making redundant session fetch

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address review findings for PR #57 (iteration 3)

- Add ID tie-breaker to buildTimeline sort to match SQL cursor ordering
  (prevents same-second entries from being skipped on Load More)
- Refresh authStore after login so Sidebar and reaction toggle have
  correct user identity without requiring a page reload
- SSE merge now tracks first-page IDs explicitly to detect deletions
  without incorrectly removing entries from older pages that share
  boundary timestamps

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 09:39:40 -04:00
xarmian 998716ae49 feat: unified item timeline with comment-on-update, threading, and reactions (#54)
* 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>
2026-04-03 07:25:56 -04:00
xarmian 35f3dd1da4 feat(conventions): add structured metadata for TASK-133 (#51)
* feat(conventions): add structured metadata for TASK-133

* fix(web): add workspace update type for CI
2026-04-02 18:39:51 -04:00
xarmian 64654846be feat(web): add workspace context editor for TASK-131 (#49) 2026-04-02 16:23:47 -04:00
xarmian 23a7fc2be1 feat(workspaces): add CLI and API context support for TASK-130 (#46) 2026-04-02 16:10:09 -04:00
xarmian f5649b912e refactor(cli): group first-release commands for TASK-127 (#45) 2026-04-02 15:28:16 -04:00
xarmian c61f4cdaaa feat(items): add structured notes for TASK-125 (#43) 2026-04-02 13:48:00 -04:00
xarmian bd9f281c81 feat(items): add first-class code metadata for TASK-123 (#41) 2026-04-02 11:15:38 -04:00
xarmian 0596ed07f4 feat(lineage): surface derived closure for TASK-122 (#40) 2026-04-02 10:46:09 -04:00
xarmian 1205fd1757 feat(web): unify setup-required auth guidance for TASK-119 (#38) 2026-04-02 05:35:20 -04:00
xarmian b59f50982f feat(auth): add local bootstrap setup for TASK-118 (#37)
* feat(auth): add local bootstrap setup for TASK-118

* fix(auth): honor setup-required bootstrap flow
2026-04-02 05:13:17 -04:00
xarmian a2a25b176a refactor(auth): make setup state explicit for TASK-117 (#36) 2026-04-01 22:21:26 -04:00
xarmian 193d694995 feat(editor): add mobile list nesting controls for TASK-111 (#30) 2026-04-01 12:38:00 -04:00
xarmian 172004f002 [codex] Fix BUG-16 quick-create button behavior and BUG-15 phase task refresh (#29)
* fix(collection): align quick-create button with Enter for BUG-16

* fix(phases): refresh linked tasks live for BUG-15
2026-04-01 12:15:27 -04:00
xarmian d136d78cfd fix(web): route empty collection new CTA to create form (BUG-14) (#27) 2026-03-31 16:43:41 -04:00
xarmian 0972347cf0 Fix svelte warnings and add build version info (#26)
* 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 ...)".
2026-03-30 11:54:45 -04:00