Files
pad/internal/store/migrations/060_user_last_write_at.sql
T
xarmian 0a09c1dca7 feat(store): add users.last_write_at column + write-path hook (TASK-1543) (#598)
* feat(store): add users.last_write_at column + write-path hook (TASK-1543)

Engagement metrics need a "last write" signal distinct from last_active_at
(which is bumped on any authenticated request, so it includes reads). Adds:

- Migration 060: users.last_write_at + index. Backfills from activities
  table (the canonical record of who-did-what) using the action set
  that handlers_items.go / handlers_comments.go actually emit:
  created/updated/archived/restored/moved/commented.

- Store.TouchUserWrite(ctx, userID): mirrors TouchUserActivity. Same
  5-minute throttle to avoid write-amplification, silent no-op on
  empty userID so callers don't have to guard.

- Hook in logActivityWithMetaReturningID (handlers_documents.go) — every
  item-write action funnels through this single helper, so one TouchUserWrite
  call covers item create/update/archive/restore/move and comment authoring.

- Explicit hook in handlers_attachments.go after CreateAttachment, since
  uploads don't go through logActivity.

Test: TestTouchUserWrite covers empty-userID no-op, first-write set,
in-throttle suppression, and out-of-throttle advance.

Architecture note: items.last_modified_by / comments.created_by are
attribution strings ("user"/"agent"/"cli"), not user IDs. The user
identity lives in activities.user_id, populated at the handler layer
where currentUser is in scope. That's why the hook lives in handlers,
not the store layer — and why the backfill reads from activities.

Part of PLAN-1542 (admin user management enhancements).

* fix: address Codex review on TASK-1543

- Add Postgres migration 039 (pgmigrations counterpart to 060). Same
  ALTER + index + activities-backfill, using TEXT to match the existing
  last_active_at / disabled_at column types in pgmigrations/020-021.
  Without this, Postgres deployments silently no-op TouchUserWrite
  because the column doesn't exist (and the call's UPDATE error is
  swallowed by design).

- Hook TouchUserWrite in handleCreateCommentReply. The reply handler
  doesn't go through logActivity (no "commented" activity emitted for
  replies — verified by grep), so the activity-helper hook misses it.
  Explicit call after a successful CreateComment.
2026-05-20 16:39:23 -04:00

30 lines
1.3 KiB
SQL

-- Migration 060: per-user last-write timestamp (PLAN-1542 / TASK-1543).
--
-- Adds a dedicated `last_write_at` column on `users` so admin engagement
-- metrics can distinguish active writers from passive readers. The existing
-- `last_active_at` column is bumped on any authenticated request (throttled
-- by TouchUserActivity), so it can't tell us whether the user actually
-- created or modified anything.
--
-- The hook (TouchUserWrite + handler-layer call sites) is wired in the same
-- task; this migration is the data-layer half.
--
-- Backfill draws from the existing `activities` table — the canonical record
-- of who did what — rather than from items.last_modified_by (which holds
-- attribution strings like "user"/"agent", not user IDs). The action values
-- below match what handlers_items.go and handlers_comments.go pass into
-- logActivity / logActivityWithMeta.
ALTER TABLE users ADD COLUMN last_write_at TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_users_last_write_at ON users(last_write_at);
UPDATE users
SET last_write_at = (
SELECT MAX(a.created_at)
FROM activities a
WHERE a.user_id = users.id
AND a.action IN ('created', 'updated', 'archived', 'restored',
'moved', 'commented')
)
WHERE last_write_at IS NULL;