Commit Graph

12 Commits

Author SHA1 Message Date
xarmian bf5ab5b366 chore: clear staticcheck SA + U1000 findings on main (TASK-764) (#249)
* chore: clear cosmetic staticcheck findings (TASK-764)

Apply zero-behavior-change fixes for 8 staticcheck findings on main:

- SA4023 cmd/pad/main.go:431 — drop always-true `if eventBus != nil`
  guard. eventBus is wrapped in metrics.NewInstrumentedBus a few lines
  above, which returns a concrete *InstrumentedBus that is never nil.
- SA1019 cmd/pad/main.go:3926 — replace deprecated strings.Title with
  golang.org/x/text/cases.Title(language.English).String. golang.org/x/text
  was already an indirect dep; now promoted to direct.
- SA4031 internal/server/handlers_changes.go:130 — delete dead
  `if updatedItems == nil { ... }` block. make([]T, n) always returns
  non-nil; the JSON marshalling already produced [] not null.
- SA9003 cmd/pad/init.go:351 — delete empty if branch and fold its
  intent into the surrounding comment.
- SA9003 internal/server/handlers_dashboard.go:125 — replace empty
  `if err == nil { ... }` branch with `_ = json.Unmarshal(...)` to
  match the sibling settings parse and document the best-effort intent.
- SA4006 internal/cli/format.go:153 — drop the dead initial
  `titlePart := item.Title` (overwritten in both branches below);
  declare titlePart with `var` instead.
- SA4006 internal/store/workspaces.go:70 — drop the dead first call
  to s.uniqueSlug; only the workspace-specific uniqueWorkspaceSlug
  is meaningful (workspace slugs are globally unique, not workspace-
  scoped like collection/item slugs).
- SA4000 internal/store/store_test.go:99 — remove always-true outer
  `if idx := len(connStr) - len(connStr); idx >= 0` and unindent the
  inner '?' query-string split.

go.mod side effects from `go mod tidy` under Go 1.26: golang.org/x/text
moves to direct (used directly now); pquerna/otp, prometheus/client_*
and trustelem/zxcvbn move from indirect to direct (they were already
used directly — Go 1.26's tidy correctly classifies them).

Verified:
- `go build ./...` clean
- `go vet ./...` clean
- `go test ./...` all pass (including the replaceDBName test path)
- `staticcheck -checks "SA1019,SA4000,SA4006,SA4023,SA4031,SA9003"` clean
  except for handlers_dashboard.go:221 (SA4006, dashboard visibility-
  filter dead block — handled in TASK-765)

Parent: PLAN-644.

* fix: clear SA5011 nil-deref in buildReconcileFindings (TASK-764)

extractItemStatus(item.Fields) on the first line of the function would
have panicked on a nil item before the `if item != nil && item.CodeContext
== nil` guard could fire. Staticcheck SA5011 flagged the inconsistency.

Drop the (item != nil) half of the guard — the function now documents
its non-nil contract in the doc comment. All callers (reconcile.go:204
plus three sites in cmd/pad/reconcile_test.go) already pass non-nil,
so this is documentation, not behaviour change.

Verified:
- `go build ./...` clean
- `go test ./cmd/pad/...` passes (the existing reconcile tests cover the
  contract)
- `staticcheck -checks SA5011 ./...` clean

Parent: PLAN-644.

* chore: silence SA4017 false positive in watchCmd SSE loop (TASK-764)

cmd/pad/main.go SSE keepalive branch:

    if strings.HasPrefix(line, ":") {
        continue
    }

Staticcheck SA4017 reports "HasPrefix doesn't have side effects and
its return value is ignored" — but the return value IS used as the
if condition. Two sibling strings.HasPrefix calls earlier in the same
for-loop body (matching "event: " and "data: " prefixes) are not
flagged, which strongly suggests an SSA-analysis quirk specific to
this branch rather than a real defect.

Suppress the finding with a //lint:ignore directive that explains
the false positive in-place. Rewriting to a different form (extract
to a bool var, comma-OK on a synthetic value, etc.) would be uglier
than the suppression comment.

Verified:
- `staticcheck -checks SA4017 ./...` clean
- `go build ./...` clean

Parent: PLAN-644.

* chore: delete dead code flagged by U1000 (TASK-764)

Pre-launch (no external contributors yet) — no consumer fork can be
relying on these unreferenced symbols, so we delete them rather than
carry the maintenance burden into v1.

## Helpers (14 functions, 1 type)

cmd/pad/main.go
- progressBar — never called

internal/cli/format.go
- stripHTMLTags — never called

internal/server/handlers_dashboard_test.go
- updateItem (test helper) — never called from any test

internal/server/handlers_items.go
- publishItemEvent — wrapper over publishItemEventWithName; all 5 call
  sites use the *WithName variant directly.
- resolveRelationFields — never called.
- resolveRelationFieldFiltersForWorkspace, resolveRelationFieldFilters,
  relationFilterKeys, resolveRelationFilterValue — closed loop of dead
  helpers (each one only called by another dead one in the family).
- extractStatus — never called (cmd/pad/reconcile.go has its own copy).

internal/server/handlers_versions.go
- handleGetDiff (HTTP handler) — never wired into setupRouter.
- diffsToChanges, diffChange (type) — only used by handleGetDiff above.
- Removes now-unused imports `strconv` and `dmp` (sergi/go-diff).

internal/server/middleware_ratelimit.go
- writeTooManyRequests — never called; the live ratelimit middleware
  uses a dedicated 429 path with Retry-After-Bucket headers.

internal/server/server.go
- guestVisibleItemIDs — never called. handlers_events.go had a
  comment cross-reference; updated to drop the reference.

## Constants

internal/events/redis_bus.go
- reconnectDelay — never read.

internal/store/api_tokens.go
- defaultTokenExpiryDays — never read.

## Out of scope
The 5 unwired handlers in internal/server/handlers_documents.go are
left alone: they are the subject of TASK-769 (a product decision —
wire up vs. delete — that may want different treatment per handler).

The two SA4006/SA4010 findings on internal/server/handlers_dashboard.go
visibility-filter block are similarly left for TASK-765.

## Verified
- `go build ./...` clean
- `go vet ./...` clean
- `go test ./...` all pass
- `staticcheck -checks "SA*,U1000" ./...` clean except the two TASK-
  765 / TASK-769 follow-ups noted above.

Parent: PLAN-644.

* docs: correct caller name in buildReconcileFindings doc (TASK-764)

Codex round 1 caught: the doc comment named the caller `reconcileSingle`
but the actual function is `reconcileItem` (cmd/pad/reconcile.go:204).
Fix the contract comment so it doesn't go stale on the first git blame.
2026-04-25 11:53:31 -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 c5183d2a4b feat: add SSE events for item star/unstar (#121)
* feat: add SSE events for item star/unstar

Emit real-time events for multi-tab sync (PLAN-564, TASK-571):

- New event types: item_starred, item_unstarred
- Emitted from handleStarItem and handleUnstarItem after success
- Includes item ID, title, collection, actor, and source
- Follows existing publishItemEventWithName pattern

* fix: scope star/unstar SSE events to the acting user

Star events are user-specific state, not workspace-wide. Changes:

- Add UserID field to Event struct for user-scoped events
- SSE handler filters events with UserID, only delivering them to
  the user who triggered the action (multi-tab sync without leaking
  star actions to other workspace members)
- Star/unstar handlers set UserID when publishing events
2026-04-14 21:03:37 -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 34ebf31fdf fix: resolve Codex review findings across SSE, audit, metrics, and deployment
- Make SSE limit checks atomic with subscription via SubscribeIfAllowed to
  prevent TOCTOU races where concurrent requests bypass connection caps
- Replace fmt.Sprintf JSON assembly with json.Marshal (auditMeta helper) in
  all audit log call sites to prevent silent JSONB insert failures on
  PostgreSQL when metadata contains special characters
- Pass database credentials via PGDATABASE env var instead of pg_dump/psql
  command-line args to avoid leaking passwords in ps/proc output
- Fix audit-log query builder to rebind placeholders once after all filters
  are appended, preventing duplicate $1 placeholders on PostgreSQL
- Replace per-workspace SSE GaugeVec with a single Gauge to avoid unbounded
  Prometheus label cardinality in multi-tenant deployments
- Fix prod Docker Compose: override PAD_REDIS_URL with password and add
  authenticated Redis healthcheck when REDIS_PASSWORD is set
2026-04-07 01:13:44 +00:00
xarmian 82e93d6014 feat: implement SSE connection limits
Add configurable global and per-workspace SSE connection limits to
prevent memory exhaustion from unbounded connections. Returns HTTP 429
when limits are reached. Logs warnings at 80% capacity.

Configurable via PAD_SSE_MAX_CONNECTIONS (default 1000) and
PAD_SSE_MAX_PER_WORKSPACE (default 100), or config.toml.

Adds WorkspaceSubscriberCount to EventBus interface for per-workspace
tracking (MemoryBus iterates subscribers, RedisBus uses existing
wsCounts map).

Resolves TASK-165
2026-04-06 01:23:34 +00:00
xarmian b9d0a89195 feat: add Redis pub/sub EventBus for multi-instance SSE (TASK-158)
- Extract EventBus interface (Subscribe, Unsubscribe, Publish, Close)
- Rename Bus → MemoryBus, keeping it as the default for single-instance
- Add RedisBus implementation with per-workspace channel subscriptions
- Lazy Redis subscribe/unsubscribe as SSE clients connect/disconnect
- Configure via PAD_REDIS_URL env var; falls back to in-memory without it
- Update Server.SetEventBus to accept the EventBus interface
2026-04-05 15:16:57 +00:00
xarmian e7f4448028 feat: add readiness probe and structured logging (TASK-161)
- Add /health/live (liveness) and /health/ready (readiness with DB check) endpoints
- Add Store.Ping() for database connectivity verification
- Create internal/logging package using stdlib log/slog
- Support PAD_LOG_LEVEL (debug/info/warn/error) and PAD_LOG_FORMAT (text/json) env vars
- Add structured request logging middleware replacing chi's default Logger
- Migrate all log.Printf calls to slog with proper levels and key-value attrs
- Exempt health probe endpoints from auth middleware
2026-04-05 15:02:54 +00:00
xarmian dab6d6c9c9 feat: implement graceful shutdown with request draining (TASK-159)
- Add signal handler for SIGINT/SIGTERM with 30s grace period
- Add Server.Shutdown() for graceful HTTP connection draining
- Add EventBus.Close() to cleanly terminate SSE subscribers
- Configure HTTP server timeouts (read: 15s, header: 5s, idle: 120s)
- Fix SetWebUI nil router panic by calling ensureRouter()
- Add Server.Handler() for httptest compatibility
2026-04-05 14:56:56 +00:00
xarmian 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 4003772538 feat: show user names in activity feeds and real-time events (#17)
Activity entries now display the authenticated user's name instead of
generic "user"/"You" labels. The user_id (already present in the
activities table from migration 012) is persisted when logging activity
and joined against the users table when querying, so actor_name flows
through the API without extra lookups. Falls back to the actor field
value when no user is associated (e.g. pre-auth activities or deleted
users).

Backend:
- Activity model gains UserID and ActorName fields
- Store queries LEFT JOIN users to populate actor_name
- logActivityWithMeta now records currentUserID on every activity
- Dashboard API includes actor_name in recent_activity
- SSE Event struct carries ActorName for real-time toasts
- Item-level activity endpoint added (GET /items/{slug}/activity)

Frontend:
- Activity page shows user name badge (green) instead of "web"
- Dashboard recent activity shows user name inline
- ActivityFeed component displays user name instead of "You"
- SSE toast notifications show user name instead of "CLI"
2026-03-28 19:11:42 -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