Commit Graph

10 Commits

Author SHA1 Message Date
xarmian 010af13abd fix(ci): gofmt internal/models/workspace.go to unbreak Go job (BUG-1911) (#784)
The mid-struct doc comment added in #781 split the Workspace struct
into two gofmt alignment groups; the file landed without re-running
gofmt, leaving golangci-lint red on main and every PR since.

Claude-Session: https://claude.ai/code/session_01CL1pBjNpPUX6SWkuAuYXHS
2026-07-02 22:45:01 -04:00
xarmian 584ac9a806 fix(web): treat CLI/MCP-created workspaces as agent-connected (BUG-1557) (#781)
`pad init` connects an agent (installs the skill, stores credentials) and
creates a workspace, but the web UI still showed the "connect an agent"
banner and onboarding launchpad. The only signal for "agent connected" was
has_agent_activity — an item existing with source cli/mcp — and a fresh
pad-init workspace has zero items, so the UI nagged to connect an agent the
user already had.

Give the server a truthful signal: a workspace created through an agent
surface already has an agent wired up before it creates its first item. Add
a `source` column to workspaces (web/cli/mcp), attributed authoritatively
server-side from the request auth shape (actorFromRequest) — never from the
request body, so a web client can't spoof "cli" to self-suppress the
prompts. The dashboard ORs source in (cli,mcp) into has_agent_activity when
the cheap item check comes up empty.

- migrations 069 (sqlite) / 047 (postgres): workspaces.source NOT NULL
  DEFAULT '' (legacy rows stay "unknown", never treated as agent-created)
- models.Workspace.Source + WorkspaceCreate.Source (json:"-", server-set)
- thread source through the CreateWorkspace INSERT + all 7 workspace scan
  sites (workspaces.go, workspace_members.go)
- handleCreateWorkspace derives source from actorFromRequest
- OnboardingLaunchpad step 1 collapses to "Agent connected" when the agent
  is already wired up, shifting emphasis to "tell it to set up"

Web modal and cloud-signup auto-create flows are unchanged and still
correctly prompt to connect (source web / empty).

Tests: store source round-trip across reads; dashboard reports
agent-connected for a cli-created workspace with zero items; web-created
stays not-connected until an agent item exists; a web body-spoofed source
is ignored.

Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
2026-07-01 23:25:24 -04:00
xarmian 157ca4e88f chore: bump Go toolchain to 1.26 (TASK-763) (#247)
* chore: bump Go toolchain to 1.26 (TASK-763)

Bump Go from 1.25 to 1.26 across all toolchain pins:

- go.mod — go 1.25.0 → go 1.26.0
- Dockerfile — golang:1.25-alpine → golang:1.26-alpine
- .github/workflows/ci.yml — three setup-go steps (Go, Go-Postgres, E2E jobs)
- .github/workflows/release.yml — release pipeline

No `toolchain` directive: the repo is pre-launch with no external
contributors yet, so we set the floor where we want it (hard requirement).

Verified locally before commit:
- golangci-lint v2.11.4 builds and runs under Go 1.26.2 (pinned in CI)
- golang:1.26-alpine and 1.26.2-alpine images present on Docker Hub
- go build ./... clean
- go vet ./... clean
- go test ./... all pass

Parent: PLAN-644 (OSS Repo Hygiene and Launch Polish).

* chore: gofmt -w under Go 1.26 (TASK-763)

Apply Go 1.26's gofmt to the codebase. ~41 files reformatted, all
struct-tag whitespace realignment — no semantic changes. Verified:

- gofmt -l ./cmd ./internal returns empty after
- go build ./... still clean
- go test ./... still passes (run before commit)

Bundling the gofmt diff with the toolchain bump in the same PR because
the formatting drift is a direct consequence of moving from 1.25 to
1.26; splitting them creates a mandatory two-PR ordering for no value.

Parent: PLAN-644.

* docs: bump documented Go floor to 1.26 (TASK-763)

Match go.mod's hard 1.26.0 requirement in the source-build instructions.
Caught by Codex review round 1 on PR #247.

- README.md:158 — "Go 1.25+" → "Go 1.26+"
- CONTRIBUTING.md:9 — "Go 1.25+" → "Go 1.26+"
2026-04-25 11:35:19 -04:00
xarmian c6d19837c8 feat: collection & item grants, guest access, share dialog (PLAN-407 Phase 3) (#87)
* feat: collection and item grants tables + permission resolution

Add grant tables, CRUD operations, and permission resolution for
guest access and member overrides (TASK-417).

Data model:
- collection_grants table (id, collection_id, workspace_id, user_id,
  permission, granted_by) with CASCADE on collection/user delete
- item_grants table (same structure, references items)
- Indexes for user/collection/item lookups

Store methods:
- Create/Get/List/Delete for both collection and item grants
- ListUserGrants: all grants for a user across a workspace
- RevokeAllUserGrants: bulk delete for member removal
- ResolveUserPermission: full 5-step resolution per DOC-406
  (owner → item grant → collection grant → membership → deny)

API endpoints:
- GET/POST/DELETE /collections/{coll}/grants — collection grant CRUD
- GET/POST/DELETE /items/{slug}/grants — item grant CRUD
- GET /users/{userID}/grants — all grants for a user in workspace

All grant endpoints are owner-only for creation/deletion.

* feat: grant revocation + member removal with grant choice

Update member removal to support D4: owner chooses whether to revoke
all grants when removing a member (TASK-489).

- DELETE /members/{userID}?revoke_grants=true → remove membership AND
  all collection/item grants (full removal)
- DELETE /members/{userID} (or revoke_grants=false) → remove membership
  but keep grants (user becomes a guest with existing access)
- Audit log records whether grants were revoked
- CASCADE DELETE on collection/item deletion already handles cleanup
  (via ON DELETE CASCADE in the grants migration)

* feat: share dialog UI for items and collections + grant types

Add a share dialog component for managing grants on items and
collections, plus TypeScript types and API client methods (TASK-419).

Frontend:
- ShareDialog.svelte: reusable modal for listing/creating/revoking
  grants, with email input, permission select, and revoke buttons
- Item detail page: "Share" button in meta-actions (owner-only)
- Collection page: "Share" button in header actions (owner-only)

TypeScript:
- CollectionGrant and ItemGrant types added
- API client: grants.listCollectionGrants, createCollectionGrant,
  deleteCollectionGrant, listItemGrants, createItemGrant,
  deleteItemGrant, listUserGrants

Guest home screen (TASK-418) deferred — requires layout-level guest
detection which will be implemented when guest routing is built.

* feat: guest access — grants-based workspace access for non-members

Allow authenticated users with grants (but no workspace membership)
to access workspaces as guests (TASK-418).

Backend:
- UserHasGrantsInWorkspace: checks if user has any collection/item
  grants in a workspace
- GuestVisibleCollectionIDs: returns collections visible to a guest
  via collection grants + collections containing granted items
- RequireWorkspaceAccess: after member-nil check, falls through to
  grant check; sets role to "guest" if grants exist
- VisibleCollectionIDs: non-members now checked for guest grants
  instead of returning empty
- GetUserWorkspaces: includes guest workspaces (is_guest flag)
- GetWorkspacesBySlugForUser: JOINs on grants tables so workspaces
  resolve for guests
- roleLevel: "guest" = 0 (below viewer, blocks role-gated actions)

Frontend:
- Workspace.is_guest field in TypeScript type
- Sidebar: hides Dashboard, Roles, Activity, Settings, and "New
  collection" button for guests; shows "Shared with you" header

* feat: wiki-link rendering with locked icon for hidden items

Update wiki-link rendering to show a 🔒 locked icon when the linked
item is in a collection the user can't see (TASK-420).

- renderMarkdown accepts optional visibleCollectionSlugs parameter
- Items in hidden collections render as "🔒 Title" with tooltip
- Unresolved links still render as broken (no change)
- Username param added to renderMarkdown for correct URL construction
- TimelineCommentCard and CommentThread accept username prop

* fix: harden grant security — 9 findings from Codex review

- Item grants no longer leak collection-wide read access; guests with
  item-level grants see only their granted items, not the full collection
  (GuestVisibleResources two-level filter + ItemIDs in ListItems SQL).
- Edit grants are now enforced: mutating handlers (create/update/delete
  items, comments, reactions, links, versions) resolve grant-based
  permissions for guests via requireEditPermission + ResolveUserPermission.
- Grant list endpoints restricted to owners (collection/item grants) or
  owner-or-self (user grants) to prevent metadata/email enumeration.
- Guests blocked from listing workspace members; invitation details
  restricted to owners only.
- Grant deletion scoped to workspace_id to prevent cross-workspace
  deletion by guessing grant IDs.
- Member removal now revokes grants by default (opt-out with
  ?revoke_grants=false) and propagates revocation errors instead of
  silently discarding them.
- Guest workspace listing properly propagates DB errors instead of
  swallowing them.
- PostgreSQL subquery alias added to UserHasGrantsInWorkspace to fix
  silent guest-access failures on Postgres deployments.

* fix: harden item-level grant isolation — 7 findings from Codex re-review

- /changes endpoint now filters by item-level grants so guests with one
  item grant no longer receive updates for every item in that collection.
- Search results filtered by item-level grants (new ItemIDs field in
  SearchParams) so guests can't discover other items via search.
- Relationship/summary endpoints (item links, children, progress,
  activity, dashboard) all apply item-level visibility checks via
  isItemVisibleToGuest(), preventing metadata leakage through related
  item titles, statuses, and counts.
- Grants now work as member overrides: a viewer with an edit grant can
  edit the granted item (requireEditPermission falls back to
  ResolveUserPermission for members below editor role).
- handleMoveItem now requires edit permission on the target collection,
  not just visibility, preventing guests from moving items into
  view-only collections.
- Member removal + grant revocation is now atomic via
  RemoveWorkspaceMemberAndRevokeGrants() which wraps both operations
  in a single database transaction.
- Guest-access DB errors in middleware now return 500 with slog.Error
  instead of being silently collapsed into a 403 forbidden response.

* fix: close remaining grant isolation gaps — 10 findings from Codex round 3

- Workspace token endpoints (create/list/delete) now require owner role,
  preventing guests from enumerating or revoking API tokens.
- Legacy document endpoints (list, get, context, bulk-read, backlinks,
  links) now require at least viewer role, blocking guests entirely
  since documents are outside the grants model.
- Global search no longer relies on workspaceRole() (which is unset
  outside RequireWorkspaceAccess); detects guests via IsWorkspaceMember
  and applies item-level filtering. Multi-workspace search now uses
  GuestVisibleResources for guest workspaces.
- SSE event filtering now checks item IDs for guests with item-level
  grants, not just collection slugs, preventing live event leaks.
- Role board passes ItemIDs through RoleBoardParams so guests only
  see items they have grants on, not the entire collection.
- VisibleCollectionIDs for members with "specific" collection access
  now merges direct grants (collection + item grants), so grant
  overrides work for restricted members.
- Plans-progress endpoint filters plan items and children by item-level
  grants for guests, preventing one plan grant from exposing all plans.
- Webhook listing now requires owner role since URLs may contain secrets.
- Agent role item counts use item-level filtering for guests.
- Link deletion checks item-level visibility on both endpoints, not
  just collection-level.

* fix: close member grant escalation and remaining edge cases — round 4

- Item grants for restricted members no longer escalate to collection-
  wide visibility. VisibleCollectionIDs now merges only direct collection
  grants (not item-derived collections) into member access. Item-level
  filtering (guestResourceFilter, isItemVisibleToGuest, requireItemVisible)
  now applies to both guests AND restricted members with item grants,
  closing the gap where a member with specific collection access plus
  one item grant could see/edit all items in that collection.
- Guests blocked from workspace-level activity feed (/activity) which
  exposed audit events (member invites, role changes) with operational
  metadata. Requires at least viewer role.
- Global search no longer returns zero results for item-only guests.
  Store.Search early-return now checks both CollectionIDs and ItemIDs
  are empty before short-circuiting, so item-level grants work in
  global (multi-workspace) search.
- UserHasGrantsInWorkspace now excludes item grants on soft-deleted
  items, preventing phantom guest access to a workspace shell with
  no visible content when the only granted item is archived.

* fix: prevent grant filter from overriding member access, close SSE/dashboard/collection leaks — round 5

- guestResourceFilter now returns nil/nil for members with "all"
  collection access, preventing item grants from accidentally replacing
  their full visibility. Only guests and members with "specific"
  collection access get item-level filtering applied. This fixes a
  regression where a normal member receiving one item grant would lose
  access to all other items.
- requireItemVisible uses guestResourceFilter (with the same scoping)
  instead of raw GuestVisibleResources, so the member-access check is
  consistent throughout all code paths.
- SSE event filtering now denies collection-less events (workspace
  updates, legacy document events) for guests, preventing metadata
  leakage through realtime event payloads.
- Dashboard recent activity filters out workspace-level entries (no
  DocumentID) for guests, preventing audit metadata leakage.
- All grant visibility queries (UserHasGrantsInWorkspace,
  GuestVisibleCollectionIDs, GuestVisibleResources) now join the
  collections table and require deleted_at IS NULL, so grants on
  soft-deleted collections no longer provide phantom access.

* fix: make item grants additive for restricted members, close write/search/SSE gaps — round 6

- guestResourceFilter now merges member_collection_access + system
  collections + collection grants into fullCollIDs for restricted members,
  making item grants additive to existing access. Previously, item grants
  replaced the member's normal collections, causing members with one item
  grant to lose all their other collection visibility.
- Added ListSystemCollectionIDs store method for system collection lookup.
- Search (both global and workspace-scoped) now applies item-level
  filtering for restricted members with item grants, not just guests.
  Previously VisibleCollectionIDs included item-granted collections as
  full-access, leaking all items in those collections via search.
- SSE event filtering now builds item-level filters for restricted
  members with item grants (previously only for non-members/guests),
  and merges member collections into the full-access set.
- Role board reorder now uses requireItemVisible + requireEditPermission
  per item instead of collection-only visibility check, preventing
  restricted editors from reordering items in item-granted collections.
- View create/update/delete now check requireEditPermission on the
  collection (via requireViewEditable), not just collection visibility.
- GetUserWorkspaces guest query now joins collections/items tables to
  exclude grants on soft-deleted resources, matching the behavior of
  UserHasGrantsInWorkspace.

* fix: block guests from legacy doc versions/activity, fix ListItems early return, SSE fail-closed — round 7

- Legacy document version handlers (handleListVersions, handleGetVersion)
  and document activity handler (handleListDocumentActivity) now require
  at least viewer role, blocking guests from reading version history and
  activity for unrelated legacy documents.
- ListItems early return now checks both CollectionIDs and ItemIDs are
  empty before short-circuiting, matching the fix already applied to
  Search. This fixes item-only guests seeing zero results from /items,
  dashboard, role board, and agent-role counts.
- SSE item-grant filtering now fails closed on GuestVisibleResources
  errors: installs empty item/collection filter sets instead of falling
  through with nil (which would pass all events through).
- Role board reorder removed top-level requireMinRole("editor") so the
  per-item grant-aware requireEditPermission checks can run for guests
  and viewers with edit grants, consistent with other mutating handlers.
2026-04-11 14:46:24 -04:00
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 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 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 23a7fc2be1 feat(workspaces): add CLI and API context support for TASK-130 (#46) 2026-04-02 16:10:09 -04:00
xarmian c1a9cd1533 feat(workspaces): add typed context schema for TASK-129 (#47) 2026-04-02 16:09:10 -04:00
xarmian 81579847c6 Initial release
Pad — project management for developers and AI agents.
Single Go binary with embedded SvelteKit web UI, SQLite storage,
CLI, and Claude Code /pad skill integration.

https://getpad.dev
2026-03-26 01:52:36 +00:00