Commit Graph

7 Commits

Author SHA1 Message Date
xarmian 076fb9b2e7 feat(comments): comment editing backend — user_id, UpdateComment, PATCH, SSE (TASK-1663) (#665)
* feat(comments): comment editing backend — user_id, UpdateComment, PATCH, SSE (TASK-1663)

Foundation for comment editing (PLAN-1662). No migration — comments.user_id
already exists (012_users.sql) but was never written or exposed.

- Populate user_id on create/reply: CreateComment takes an explicit userID
  param (passed from currentUserID by the handlers, not via the request body
  so it can't be spoofed). Expose user_id on models.Comment + all comment
  SELECTs/scans. The workspace export path is left as-is — imported comments
  keep NULL user_id (admin-only edit), matching the pre-identity fallback.
- Store.UpdateComment(id, body): replaces body + bumps updated_at; the
  comments_fts_update trigger re-indexes.
- PATCH /workspaces/{ws}/comments/{commentID}: author-or-admin only
  (canEditComment), rejects empty body. Editing is an authorship op, distinct
  from delete (item editors). NULL user_id → admin-only.
- comment_updated SSE event: broadcast from the handler; added to the web
  sse allowlist + ItemTimeline refresh set.
- web: api.comments.update(), Comment.user_id type.

Tests: author edits own (200), non-author non-admin (403), admin edits
anyone (200), empty body (400), NULL-user_id comment is admin-only.

Parent: PLAN-1662.

* fix(account): detach authored comments on account deletion per Codex review (round 1)

Now that TASK-1663 populates comments.user_id (FK to users.id),
DeleteAccountAtomic would fail on the FK for any user who authored a
comment. Null comments.user_id for the user before deleting the row —
comments live on in soft-deleted/other workspaces; the display-name
author is preserved and the comment just becomes admin-only to edit.
Regression test added.
2026-05-30 12:38:57 -04:00
xarmian 6e4c7f617b fix(timeline): drop \xff cursor sentinel that broke Postgres pagination (BUG-1086) (#391)
The timeline handler defaulted the cursor's beforeID to the literal byte
"\xff" as a sentinel intended to "sort after any UUID". SQLite tolerates
that in TEXT columns, but Postgres rejects it as an invalid UTF-8 byte
sequence (SQLSTATE 22021 — "invalid byte sequence for encoding 'UTF8':
0xff"), causing every timeline tab load on pad cloud to return 500.

Reproduced empirically against the test Postgres with a one-line probe
that issues a TEXT-typed bind of "\xff" — same error string the bug
reported.

The fix removes the sentinel and distinguishes three cursor cases in
the handler:

  1. Neither `before` nor `before_id` (true first page) → store gets
     beforeID = "" and drops the id tie-breaker from the WHERE clause
     entirely. Just `WHERE created_at < ?`.

  2. Both supplied (normal cursor pagination) → unchanged.

  3. `before` supplied without `before_id` (anomalous but possible
     for external clients) → use "g" as a UUID-safe upper-bound
     sentinel. Lowercase-hex UUIDs are bounded by "f", so "g" sorts
     above them in every reasonable collation while remaining valid
     UTF-8. This preserves the legacy semantics of including
     same-second rows that the naive `created_at < ?` would drop —
     a regression Codex caught on round 1 of review.

The id-predicate branching is applied symmetrically across all three
*BeforeTime store functions: ListCommentsBeforeTime,
ListDocumentActivityBeforeTime, ListItemVersionsBeforeTime.

New test file internal/store/timeline_pagination_test.go covers:

  - No-cursor first-page path (the broken one)
  - Real (timestamp, id) cursor pagination
  - Same-second cursor with sentinel id (regression guard)
  - Limit respected
  - Activity and version BeforeTime no-cursor paths

All six pass on SQLite and on real Postgres via `make test-pg`. Full
./internal/store and ./internal/server suites stay green on Postgres.
2026-05-03 00:12:50 -04:00
xarmian 7cda0d7896 feat: rebrand to Perpetual Software + new tagline (IDEA-832) (#273)
Migrates from xarmian/pad to PerpetualSoftware/pad across the entire
repo and updates the product subtitle to "Collaborate with your AI
agents".

Go module rename
- go.mod: github.com/xarmian/pad → github.com/PerpetualSoftware/pad
- All Go imports updated across cmd/pad, internal/{cli,server,store,
  models,collections,items,events,metrics,webhooks} (~130 files)
- Test fixtures with the literal repo slug ("xarmian/pad" in JSON
  shapes, SSH/HTTPS git URL strings, workspace_context fixtures)
  also updated, including the secondary repo entry
  (xarmian/pad-web → PerpetualSoftware/pad-web — pad-web was also
  moved to the org per branch context)

Docs / config
- README badges, install instructions, brew tap, Docker image, source
  build path, sponsor link (sponsor link kept as personal @xarmian)
- Subtitle: "Project management for developers and AI agents." →
  "Collaborate with your AI agents." (README, manifests, web layout
  meta, .goreleaser homebrew description)
- CONTRIBUTING.md, SECURITY.md, skills/INSTALL.md
- .goreleaser.yaml: homebrew_casks owner, GHCR image, release github
  owner, cosign cert-identity regex, comments
- .github/workflows/release.yml: tap/release comments
- deploy/k8s/deployment.yaml: container image
- docs/deployment.md: clone URL
- web/static/{site.webmanifest,manifest.json}: description
- web/src/routes/+layout.svelte: meta description + og:description

Brew tap path is PerpetualSoftware/tap/pad (CamelCase, matches
GitHub user case). GHCR image is ghcr.io/perpetualsoftware/pad
(lowercased per GHCR's URL normalization). CODEOWNERS @xarmian and
FUNDING.yml github: xarmian intentionally retained — those are the
personal maintainer / sponsor account, separate from the org repo.

Verification: go build ./..., go test ./... (all pkgs pass), web
build, and make install all clean (TASK-844, TASK-845).
2026-04-28 12:26:39 -04:00
xarmian a4a701367a feat: add PostgreSQL support with dual-driver store layer (TASK-157)
- Create Dialect abstraction for SQLite/PostgreSQL SQL differences
  (JSON ops, FTS, placeholders, datetime, aggregation)
- Add Store.NewPostgres() constructor with connection pooling
- Create consolidated PostgreSQL schema (pgmigrations/001_initial.sql)
  with tsvector FTS, JSONB columns, and GIN indexes
- Refactor all store queries (~150) to use s.q() for placeholder rebinding
- Replace hardcoded json_extract/FTS5/GROUP_CONCAT with dialect methods
- Support PAD_DB_DRIVER=postgres + PAD_DATABASE_URL env vars
- Keep SQLite as the default for local/self-hosted mode
- Add dialect unit tests (rebind, SQLite, PostgreSQL)
2026-04-05 18:50:50 +00:00
xarmian 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 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