Norma

repo: ~/Projects/Norma · 62 commits · 0 in last 24h, 3 in last 7d ·

Search the build

62 commits indexed

  1. b9d1c04 2026-07-23 instagram-agent fully LIVE: real token installed, monitor/discover/report un-stubbed (Steve-authorized), Graph v21 metrics fixed
  2. be52823 2026-07-23 auto-save: 2026-07-23T08:19:35 (2 files) — agents/instagram-agent/skills/discover.js agents/instagram-agent/skills/monitor.js
  3. e6864e6 2026-07-23 auto-save: 2026-07-23T07:19:17 (1 files) — scripts/gmail-backfill.mjs
  4. 05b28cd 2026-07-14 instagram-agent: env-configurable Graph host (IG_GRAPH_HOST) — support Facebook-Login flow (page token + IG business id) alongside Instagram-Login
  5. 68552ce 2026-07-13 instagram-agent: make reel skill account-aware (close cross-brand IG-token leak)
  6. 5d84a60 2026-07-13 chore: v0.1.3 (dashboard CSS refactor)
  7. 47943fb 2026-07-13 refactor(agent-base): hoist static dashboard CSS to a module const
  8. c7157cf 2026-07-13 chore: version bump (session close — SSRF guard + security P0 verification)
  9. cb9d893 2026-07-12 docs(security): record P0#1-3 + P1 SSRF remediation as resolved in April audit
  10. 10f2f97 2026-07-12 security(P1): add shared SSRF guard blocking private/reserved/loopback hosts on user-URL fetches
  11. dab8014 2026-07-12 chore: version bump (session close — Norma platform + agent recovery, login/schema-drift fixes, dashboard)
  12. b184c10 2026-07-11 Fix tier_credentials schema drift in users + gmail routes
  13. 86dc7d4 2026-07-11 Fix tier_credentials schema drift in auth + impersonate routes
  14. 9ba27ad 2026-07-11 Fix tier_credentials schema drift in settings CRUD routes
  15. c421505 2026-07-11 auto-save: 2026-07-11T08:42:28 (1 files) — app/api/auth/login/route.ts
  16. 2439b98 2026-07-10 dashboard: harden escaping for attribute context (contrarian gate)
  17. a1a4ebc 2026-07-10 Norma dashboard: enrich GET / with insights + media grid (cache-backed)
  18. 46eb638 2026-07-10 Norma instagram-agent: add RECOVERY.md (deps-location gotcha, auth, durability)
  19. 3dbea14 2026-07-10 agent-base: fix route shadowing + server-render status dashboard
  20. 6f74352 2026-07-10 Norma agents: add read-only status dashboard at GET / (behind auth)
  21. 3b2f226 2026-06-01 Global search: mount on Supervisor + Pulse tiers
  22. eb3fcd7 2026-05-31 fix: add rel=noopener noreferrer to 32 target=_blank links in inbox mockup index; expand .gitignore backup patterns
  23. 81abdbc 2026-05-30 security: strip hardcoded dw_admin DSN password -> env-first (.mjs straggler). No rotation/deploy.
  24. bde9449 2026-05-30 security: strip hardcoded dw_admin DSN password -> env-first/passwordless. No rotation/deploy.
  25. 1033d7f 2026-05-30 security: strip hardcoded dw_admin DSN password -> env-first (DATABASE_URL); passwordless localhost fallback. No rotation/deploy.
  26. 36d0cb1 2026-05-28 wire Morning Batch + weekly_intelligence to PM2 cron — fixes 77d/72d stale tables
  27. 0713748 2026-05-28 norma-scraper: load .env.local via interpreter_args so SESSION_SECRET reaches the cron — fixes 401 loop
  28. 44cdbed 2026-05-21 defensive: explicit body[data-inbox-theme='classic-dense'] selector
  29. 76b8e92 2026-05-21 feat(theme): hoist InboxThemeBanner + AgeThemeSlider into app/layout.tsx
  30. 59324fb 2026-05-20 docs(sessions): log brute-force-protection learnings (commit 2c9def0)
  31. 2c9def0 2026-05-20 feat(auth): brute-force-resistant login rate-limit (10 fails/15min per IP, 30min lockout)
  32. adb6d64 2026-05-20 tasks: mark #52 (admin impersonation UI) DONE
  33. 1c1e1df 2026-05-20 feat(impersonate): wire the View-as dropdown + Acting-as banner into AppShell
  34. e284feb 2026-05-20 session 2026-05-20: log roles+perms+modal-fanout+gmail-per-user+disk-save+admin-union
  35. 11bcabc 2026-05-20 docs+infra: gmail connect note (password redacted) + linter-touched messages route
  36. c498d02 2026-05-20 fix(theme): apply 32 inbox-theme skins at body level so dropdown clicks visibly re-skin the whole page
  37. ce205d8 2026-05-20 fix(theme): apply inbox-theme skins at body level so dropdown clicks re-skin the whole page immediately
  38. 82446f2 2026-05-20 feat(theme): full-width inbox-theme banner at top of AppShell
  39. 70fa5dc 2026-05-20 feat(theme): hoist inbox theme switcher to global AppShell header
  40. 8f71b14 2026-05-20 fix(gmail): scope Gmail CRM to current user's mailbox, not hardcoded natalia
  41. ee9135a 2026-05-20 feat(modals): fan out ResizableModal to 22 modals across 15 files
  42. dfa8907 2026-05-20 feat(inbox): +16 themes (v17-v32) — total 32 live + dropdown-wired
  43. 93205f4 2026-05-20 feat(inbox): 10 more theme mockups (v7-v16) + live theme dropdown in GmailCRMTab
  44. b163e4d 2026-05-20 session 2026-05-20: log roles/perms/modals + infra fires + 4 user seed
  45. 68f7b63 2026-05-20 feat(mockups): 6 inbox UI variants — Classic Dense / Notion Cards / Linear Keyboard / Apple Mail / Front Conversation / Hey Zero-Distraction
  46. d433048 2026-05-20 feat(modals): shared ResizableModal with full-screen toggle + drag-resize
  47. 38145df 2026-05-20 feat(gmail): per-message Assign-to + per-user read receipts + inline-expand layout
  48. f6b2dba 2026-05-20 feat(auth): intern role + user fields + admin-editable role × feature permissions matrix
  49. 189ac33 2026-05-20 feat(admin): comprehensive API/Token/MCP registry + age-adaptive theme slider
  50. 2b2fc56 2026-05-20 ux(gmail): land with full-width inbox — default showContacts to false so users see all mail first; Contacts toggle stays in toolbar for outreach drilldown
  51. 4161ee6 2026-05-20 session 2026-05-20: log 9 task entries (petition debounce, modal video, 4 pay-down todos)
  52. fb319b1 2026-05-20 docs: modal training one-pager for SDCC staff (companion to walkthrough video)
  53. e46e6f9 2026-05-20 fix(editor): set immediatelyRender:false on Tiptap useEditor — Next 15 + React 19 SSR hydration error blocked petition pipeline editor render
  54. 4f1d74b 2026-05-20 fix(pipeline/reorder): cast org_id param as ::uuid not ::text — pipeline drag-drop was 500-ing for staff role with non-null orgId due to same uuid=text operator mismatch already fixed in pipeline/stats + settings
  55. 9619a2a 2026-05-20 petition create: debounce double-submit via useRef guard + 30-min server-side merge window
  56. eb21d0d 2026-05-20 fix(api): cast org_id param as ::uuid not ::text in pipeline/stats + settings
  57. c0eed7c 2026-05-20 docs: morning wakeup brief — DB password fix + verification path
  58. 52ff6c8 2026-05-20 fix(pulse-public): silence 401 spam, unblock topics for guests, hide Apple OAuth stub, add sign-in CTA
  59. 720506c 2026-05-20 fix(types): unblock SocialTab render, IntegrationsTab metadata, credentials rowCount null
  60. ddf268e 2026-05-19 instagram-agent: enable live reel/story/post publishing via graph.instagram.com (Instagram Login API)
  61. 479708b 2026-05-13 snapshot: 12 file(s) changed, ~12 modified
  62. 4ad1c15 2026-05-06 initial scaffold (gitify-all 2026-05-06)

Authors

Agents used

  • none detected

Skills used

  • /auth7
  • /messages7
  • /gmail6
  • /login5
  • /mockups5
  • /inbox5
  • /pulse4
  • /index4
  • /petitions4
  • /skills3
  • /route3
  • /story3
  • /page3
  • /deploy3
  • /settings3
  • /stats3
  • /discover2
  • /instagram-agent2
  • /reserved2
  • /assign2
  • /shared2
  • /status2
  • /supervisor2
  • /search2
  • /layout2
  • /cron2
  • /daily2
  • /impersonate2
  • /session2
  • /social2

Creative ideas + design notes

Commits with substantial prose (≥120 chars) — the rationale behind each move.

68552ce · 2026-07-13 · instagram-agent: make reel skill account-aware (close cross-brand IG-token leak)
- resolveAccount(account): 'dw' → IG_USER_ID/IG_ACCESS_TOKEN (unchanged);
  other id X → IG_USER_ID_<X> / IG_ACCESS_TOKEN_<X> (X=upper, dashes→underscores),
  mirroring dw-marketing-reels publish-social.mjs IG_AGENT_AUTH_<ID> naming.
- non-dw account with NO token now returns { status:'no-creds-for-account',
  account, note } instead of silently posting from the DW account (the bug).
- echoes resolved account in response + logs; dw single-account simulation/live
  path 100% intact. node --check passes; verified via in-process skill calls.
47943fb · 2026-07-13 · refactor(agent-base): hoist static dashboard CSS to a module const
Behavior-preserving — moves the 26-line inline <style> block out of the
per-request GET / handler into a DASHBOARD_CSS const (defined once, not
rebuilt per page load). Rendered HTML identical; verified node --check +
norma-instagram boots + GET / 200 with CSS intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10f2f97 · 2026-07-12 · security(P1): add shared SSRF guard blocking private/reserved/loopback hosts on user-URL fetches
Wire lib/ssrf-guard.assertPublicUrl() into /api/nonprofit/enrich and
/api/email-analyzer/fetch-url before the outbound fetch. Blocks non-http(s)
protocols, localhost, RFC1918 (10/172.16-31/192.168), loopback (127/::1),
link-local + cloud-metadata (169.254.0.0/16 incl. 169.254.169.254), and other
reserved ranges; resolves DNS so hostnames pointing at private IPs are also
blocked. Verified: private/reserved/file:// -> 400, public URL -> 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b184c10 · 2026-07-11 · Fix tier_credentials schema drift in users + gmail routes
users: drop full_name/email/is_active; name from display_name, email from
email-shaped username, no is_active filter.
gmail messages/[id]/assign/oauth: swap full_name->display_name in COALESCE,
resolve email from username (no tier_credentials.email), drop is_active
filter on the assign target lookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86dc7d4 · 2026-07-11 · Fix tier_credentials schema drift in auth + impersonate routes
login: remove dead is_active block + swap row.full_name -> display_name.
session: drop full_name/email from SELECT (email inferred from username).
impersonate: drop is_active from SELECT + remove disabled-account guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9ba27ad · 2026-07-11 · Fix tier_credentials schema drift in settings CRUD routes
Drop full_name/email/is_active/last_login_at/created_by column refs that
do not exist in this deployment. SELECT/INSERT/dynamic-UPDATE/RETURNING
now match the real schema; admin-disable guard removed (no is_active).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2439b98 · 2026-07-10 · dashboard: harden escaping for attribute context (contrarian gate)
Contrarian flagged attribute-context XSS: esc() escaped &<> but not quotes,
while permalink->href and thumb->src render in double-quoted attributes; in
live mode discover() pulls hashtag-search results (other users' captions/
permalinks) and helmet CSP is disabled, so an injection would be unmitigated.
Fixes: esc() now also escapes " and ', and href/src accept only http(s) URLs
(blocks javascript: scheme). Proven via headless Playwright with a hostile
payload: no dialog, window.__pwned=false, 0 rogue img[onerror]/script nodes,
malicious permalink -> href='#', caption rendered as literal escaped text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a1a4ebc · 2026-07-10 · Norma dashboard: enrich GET / with insights + media grid (cache-backed)
Steve: dashboard was 'very little info, no images'. Now:
- shared agent-base gains a generic dashboard() hook: renders a metrics[]
  stat-card row + a media[] thumbnail grid + live/simulation badge. Base
  stays generic (no IG specifics); agents supply the data.
- instagram-agent supplies dashboard(): reads cache-backed monitor insights
  (reach/impressions/followers/etc) + discover media. DTD-committed (2/2)
  CACHE design: crons persist latest result to data/*.json; GET / reads the
  cache and NEVER fires a metered Graph API call from a page view. Simulation
  computes on-empty (free/local). Non-loading sim media_url -> placeholder tile.
- data/ gitignored (runtime cache).

Verified (simulation): mode badge, 6 metric cards, media grid w/ captions,
cache files written, GET / 200 authed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
46eb638 · 2026-07-10 · Norma instagram-agent: add RECOVERY.md (deps-location gotcha, auth, durability)
Documents the shared/node_modules dependency-resolution gotcha the contrarian
flagged: npm install in agents/shared/ (not the agent dir) when non-dotenv deps
go missing. Also records the .env/empty-password trap + reboot durability + the
dw-uptime-probe watchlist follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3dbea14 · 2026-07-10 · agent-base: fix route shadowing + server-render status dashboard
Contrarian gate caught two real defects:
- app.use('*') 404 catch-all was registered inside createAgentServer,
  shadowing caller-added routes: instagram-agent /api/skill/reel + /story
  were 404. Now deferred to start() (called last) -> custom routes win.
  Verified reel/story now 200, bogus routes still 404.
- GET / dashboard fetched /api/status client-side, which was fragile re:
  Basic-auth on sub-requests. Now server-renders data inline (const S).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6f74352 · 2026-07-10 · Norma agents: add read-only status dashboard at GET / (behind auth)
Renders /api/status + cron + recent audit client-side; no write controls.
Fixes the 'open it in a browser -> 401/404' gap. Shared base, so all 11
agents inherit it; only norma-instagram is currently live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3b2f226 · 2026-06-01 · Global search: mount on Supervisor + Pulse tiers
Norma had GlobalSearch wired only into AppShell (admin) and NikkiShell
(staff). Extend to the remaining two tiers:

- Supervisor: import GlobalSearch and render it top-right of
  app/supervisor/page.tsx. onNavigate pushes /?tab=<id>&id=<id>; teach
  AppShell to read those params on mount so deep-links land on the right
  tab.

- Pulse: build a new PulseSearch component that matches the Pulse
  Georgia-serif / cream / dark-green / gold theme (the existing
  GlobalSearch is dark-themed and would clash). Hits a new
  /api/search/public route that requires no auth and only returns
  entities safe for end-users: petitions, politicians, organizations,
  topics, articles. Intentionally excludes journalists, contacts,
  grants, foundations — those are staff/admin tooling.

Files:
  app/api/search/public/route.ts  (new — 5-entity public search)
  components/PulseSearch.tsx       (new — Pulse-themed search bar + modal)
  components/AppShell.tsx          (read ?tab=&id= deep-link params)
  app/supervisor/page.tsx          (mount GlobalSearch top-right)
  app/pulse/layout.tsx             (mount PulseSearch in nav)

Cmd+K / Ctrl+K opens search on both surfaces. Esc closes, arrow keys
navigate, Enter selects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
36d0cb1 · 2026-05-28 · wire Morning Batch + weekly_intelligence to PM2 cron — fixes 77d/72d stale tables
Both writers existed but had no scheduled trigger:
- /api/cron/daily (Morning Batch / sessions table) was never hit on prod
- scripts/compute-intelligence.ts (weekly_intelligence table) wasn't in any
  cron or ecosystem entry despite header saying 'Nightly cron script'

Adds:
- scripts/daily-cron.js: POSTs to /api/cron/daily with x-cron-secret (mirrors
  credential-health-cron.js pattern)
- ecosystem.config.js: norma-morning-batch (cron 0 14 * * * = 6 AM PT)
- ecosystem.config.js: norma-intelligence (cron 0 8 * * * = midnight PT)

Both load .env.local via --env-file so CRON_SECRET/GEMINI_API_KEY reach the
procs. Surfaced by yolo task 49.
0713748 · 2026-05-28 · norma-scraper: load .env.local via interpreter_args so SESSION_SECRET reaches the cron — fixes 401 loop
scripts/scraper-cron.js builds an inline session token using SESSION_SECRET.
Without --env-file, the cron's secret was empty, signature mismatched the
server's HMAC, server returned 401, cron exited, pm2 restarted it every
~2 min. norma-credential-health already had this fix; norma-scraper didn't.

Mirrors the norma-credential-health pattern (interpreter+interpreter_args).
2026-05-28 fix surfaced by yolo task 49.
44cdbed · 2026-05-21 · defensive: explicit body[data-inbox-theme='classic-dense'] selector
Previously the classic-dense theme relied on the :root base via *absence*
of an override — switching FROM another theme BACK to classic-dense
worked because CSS cascade restores :root values when overrides aren't
matched, but it's fragile: if a future theme sets a var that no other
theme touches, switching back to classic-dense would leak that var.

Now classic-dense is a positive reset (32 explicit selectors + the :root
base = 33 layers of belt-and-suspenders). All token values mirror the
:root defaults exactly.
76b8e92 · 2026-05-21 · feat(theme): hoist InboxThemeBanner + AgeThemeSlider into app/layout.tsx
Both controls now render on every page including /login, /signup, /pulse —
not just inside the authenticated AppShell. Matches the original intent
('set theme and age at the start' from earlier in the session).

- TopThemeBar.tsx: thin client wrapper bundling both components
- layout.tsx (server component): renders <TopThemeBar /> before children
- AppShell.tsx: removed duplicate banner + slider renders
- Both still write to <body data-…>, so behavior is unchanged for
  authenticated users; new behavior is that pre-auth visitors can also
  set both. Persisted in localStorage.
59324fb · 2026-05-20 · docs(sessions): log brute-force-protection learnings (commit 2c9def0)
- Why dedicated check/record/clear helpers vs the existing combined limiter.
- Threshold-crossing extends reset to the lockout window, not the rolling window.
- NextRequest.ip not stable across Next 16 runtimes — XFF header is the source.
- Observation: sdcc.agentabrams.com nginx vhost currently proxies :9720
  (Express norma-sdcc-pitch), not norma-email :7400. Smoke test ran against
  127.0.0.1:7400 directly on prod via ssh.
- Verified 10/15min/30min behavior end-to-end on the Next.js app.
2c9def0 · 2026-05-20 · feat(auth): brute-force-resistant login rate-limit (10 fails/15min per IP, 30min lockout)
- Add checkLoginAttempt / recordLoginFailure / clearLoginCounter helpers to
  lib/rate-limit.ts, sharing the existing in-memory Map under a 'login:<ip>'
  namespace (per learnings.md 2026-04-13 part 2).
- Only HTTP 401 'Invalid credentials' responses increment the counter;
  successful login clears it; 400/403/500 leave it untouched.
- Once 10 failures land inside a 15-minute window, the IP is locked for 30
  minutes — extended reset on threshold-crossing, not just window expiry.
- 429 responses now carry Retry-After header + {retryAfter} JSON field.
- Client IP extracted x-forwarded-for first (nginx in front) → x-real-ip →
  'unknown'. NextRequest.ip intentionally not used (unstable across Next 16
  runtimes).
- No new deps; no Redis; the existing /api/v1/* API-key limiter is untouched.
1c1e1df · 2026-05-20 · feat(impersonate): wire the View-as dropdown + Acting-as banner into AppShell
Backend was already shipped at /api/admin/impersonate (GET state,
POST {username} to start, DELETE to exit; preserves the real admin's
session in `norma-imp-by` cookie). This adds the UI:

- components/ImpersonateMenu.tsx
  - variant="dropdown" — renders a "View as…" button in the AppShell
    header, admin-only, lists every user with role chip + email; click
    POSTs and hard-reloads
  - variant="banner" — renders an amber sticky bar at the very top of
    the chrome while impersonating, shows "Impersonating <user> · role
    + Exit impersonation" button (DELETE + hard-reload)
  - Single component, two render modes so the same fetch + state logic
    powers both

- components/AppShell.tsx — mounts <ImpersonateMenu variant="banner" />
  above the InboxThemeBanner (sticky-top), and <ImpersonateMenu
  variant="dropdown" /> in the header next to <ApiRegistryButton/>.

Smoke-verified end-to-end:
- admin → POST {ryan} → session reports user=ryan role=intern
- /api/admin/impersonate GET while impersonating reports impersonating:true
- DELETE → session restored to admin
c498d02 · 2026-05-20 · fix(theme): apply 32 inbox-theme skins at body level so dropdown clicks visibly re-skin the whole page
Selectors rewritten from
  body[data-inbox-theme='X'] .gmail-crm-tab { … }
to
  body[data-inbox-theme='X'] { … }

The skin overrides --color-bg/--color-text/--color-primary/etc, which
already cascade through every component via var(--color-*), so the
entire app re-themes on click instead of silently changing nothing
when the user isn't on the Gmail CRM tab.

classic-dense is the implicit default (no explicit override) → 31
explicit body-level rules + the base = 32 functional themes.
ce205d8 · 2026-05-20 · fix(theme): apply inbox-theme skins at body level so dropdown clicks re-skin the whole page immediately
Previous selectors scoped down to body[data-inbox-theme] .gmail-crm-tab,
which meant picking a theme produced no visible change unless the user
was already on the Gmail CRM tab. Steve hit that confusion ('nothing
happens when dropdown item is clicked').

Rewrote all 32 selectors to body[data-inbox-theme=...] — the skin sets
CSS custom properties (--color-bg, --color-text, --color-primary, font)
which cascade through every component that already uses var(--color-*),
so the entire app re-themes on click. The 'inbox theme' name is now
a historical artifact; effectively a global Norma theme.
82446f2 · 2026-05-20 · feat(theme): full-width inbox-theme banner at top of AppShell
Replace the small dropdown in the header right-side cluster with a
prominent sticky banner at the very top of the page:

- ◀ prev / ▶ next cycle buttons (wraps around the 32-theme set)
- 'N / 32' counter in monospace
- 🎲 random button
- direct-jump dropdown
- current theme name + description caption
- 'Gallery ↗' link to /mockups/inbox/index.html

Sits sticky above the AgeThemeSlider. Writes <body data-inbox-theme>,
CSS skins in globals.css still scope to .gmail-crm-tab so the visual
effect stays inbox-only. Persists to localStorage.
70fa5dc · 2026-05-20 · feat(theme): hoist inbox theme switcher to global AppShell header
- lib/inbox-themes.ts: single source of truth for the 32-entry registry
- components/InboxThemeSwitcher.tsx: header dropdown that writes
  document.body.dataset.inboxTheme + persists to localStorage
- AppShell.tsx renders <InboxThemeSwitcher /> next to API Registry button
- GmailCRMTab.tsx: removed duplicate state + dropdown + data-attribute
  (no more 'find the toggle inside the Gmail CRM tab'); kept the
  .gmail-crm-tab className so CSS skins still scope down
- globals.css: 32 selectors rewritten from
    .gmail-crm-tab[data-inbox-theme='…']
  to
    body[data-inbox-theme='…'] .gmail-crm-tab
  so the toggle on <body> propagates down to the inbox tab
8f71b14 · 2026-05-20 · fix(gmail): scope Gmail CRM to current user's mailbox, not hardcoded natalia
The Gmail CRM header read 'Gmail CRM — natalia@studentdebtcrisis.org'
for EVERY logged-in user, and /api/gmail/messages always returned
natalia's mailbox regardless of who was logged in. So info@ saw natalia's
inbox, which is the wrong sharing model.

- /api/auth/session now returns fullName + email pulled from
  tier_credentials (with username fallback when the username itself is
  email-shaped).
- /api/gmail/messages now resolves the mailbox via the authed user's
  email (case-insensitive). When the user has no connected mailbox it
  returns {messages: [], needs_connect: true} instead of leaking another
  user's mail. Admin can opt-out via ?mailbox=all to see the union.
- GmailCRMTab header now shows the logged-in user's email + a
  "Gmail not connected" badge when needs_connect=true.

Also added placeholder mailbox rows for info@ + SabrinaAshley@ with
needs_reauth=true so the upcoming Connect-Gmail flow has a target row.
ee9135a · 2026-05-20 · feat(modals): fan out ResizableModal to 22 modals across 15 files
Dispatched 4 parallel claude --print workers via the max-it skill. Each
worker refactored a batch of modal-bearing components to use the shared
<ResizableModal modalId="…"/> wrapper, picking up full-screen toggle,
corner drag-resize, and per-modalId localStorage size persistence.

Modals converted (22):
  ApiCredentialsSection (4)  — api-key-create / api-platform-connect /
                               api-key-revoke-confirm / api-platform-disconnect-confirm
  ManageTabsModal           — manage-tabs
  AddGrantModal             — grant-create
  AddPetitionModal          — petition-create
  ScheduleModal             — pipeline-schedule
  MemberEmailModal          — member-email-generator
  BulkScheduleModal         — social-bulk-schedule
  CompareModal              — email-analyzer-compare-versions
  ContactsTab               — contacts-import-csv
  InTheNewsTab              — intelligence-discover-articles
  JournalistHub (2)         — journalist-article-detail / journalist-add
  OldLeadsTab               — leads-rekindle-letter
  ApiRegistryModal          — api-registry
  LibraryTab                — library-item-create
  AdvocacyTargets           — advocacy-petition-template
  XSessionsTab              — xsessions-new-scan
  StatementsTab             — statements-generate
  PartnersTab               — partners-add
  OnBoardTab (2)            — onboard-trigger-discovery / onboard-nonprofit-signup
  OutreachPlaybookTab (2)   — outreach-add-pipeline-contact / outreach-generate-letter
  SocialTab (2)             — social-add-stream / social-compose-post
  PlatformPreview (2, prev) — platform-preview-save-confirm / platform-preview-publish-confirm
  ComposeModal (prev)       — email-compose

Skipped (intentionally, per ResizableModal rules):
  PetitionDraftModal        — side-drawer, not centered modal
  PipelineTab/PetitionsTab/GrantsTab/NewsTab/EmailSendsTab/EmailAnalyzer/
    ApiRegistryButton        — no inline modals (delegate to external Modal*)
  DashboardTab, PulseFeed, TopicRadar, NetworkGraph3D, EnhancedMindMap,
    PulseGlobe                — no centered modals (inline panels only)

Pre-existing TS errors in lib/auth.ts (SESSION_SECRET narrowing) +
QueryResultRow generics in unrelated files left untouched.
dfa8907 · 2026-05-20 · feat(inbox): +16 themes (v17-v32) — total 32 live + dropdown-wired
v17 Ottoman Tile · v18 Brutalist Concrete · v19 Library Catalog ·
v20 Wabi-Sabi · v21 Cyberpunk Rain · v22 Bauhaus · v23 Memphis 80s ·
v24 Vellum Manuscript · v25 Solarpunk · v26 Frutiger Aero ·
v27 Crossword Grid · v28 8-Bit Pixel · v29 Watercolor · v30 Sakura ·
v31 Forest Walk · v32 Botanical Print

Each: standalone HTML mockup + matching .gmail-crm-tab[data-inbox-theme=...]
CSS skin in globals.css + entry in INBOX_THEMES dropdown. All 32 selectable
live from the GmailCRMTab toolbar. Persisted in localStorage as
norma.inbox-theme.

Gallery updated to 32 cards at /mockups/inbox/index.html.
93205f4 · 2026-05-20 · feat(inbox): 10 more theme mockups (v7-v16) + live theme dropdown in GmailCRMTab
10 new visual directions, all wired as live themes:
  v7  Spotify Dark        — pure black, green accent
  v8  Minimal Mono        — black on white, Helvetica
  v9  Newsprint           — sepia broadsheet, drop-cap serif body
  v10 Glassmorphism       — frosted panels, cyan+pink neon on purple
  v11 Terminal            — green phosphor, monospace, scanline overlay
  v12 Mochi Pastel        — blush+lavender+mint, Nunito rounded
  v13 High-Contrast A11y  — Atkinson Hyperlegible, 18px floor, ≥48px tap
  v14 Material 3          — purple primary, rounded, FAB send button
  v15 Synthwave           — magenta+cyan on indigo, retro-grid horizon
  v16 Kanban Lanes        — Trello palette (lane layout in mockup only)

All 16 (v1-v16) now selectable from a Theme dropdown in the GmailCRMTab
toolbar. Each option rewrites the Norma CSS tokens FOR THE INBOX ONLY
via .gmail-crm-tab[data-inbox-theme='…'] in globals.css — the rest of
the app keeps its base theme. Choice persists to localStorage as
norma.inbox-theme.

Note: structural layouts (3-pane v4, kanban v16, conversation v5) are
palette+typography swaps in the live toggle; the structural reshape
remains a future-prop component refactor. Full pixel-perfect mockups
stay at /mockups/inbox/v1-v16.html for reference.

Gallery: /mockups/inbox/index.html with all 16 in a side-by-side grid.
68f7b63 · 2026-05-20 · feat(mockups): 6 inbox UI variants — Classic Dense / Notion Cards / Linear Keyboard / Apple Mail / Front Conversation / Hey Zero-Distraction
Each variant ships every feature from the live GmailCRMTab (assigned-to dropdown,
per-user read receipts as pills, inline-expanded reading pane, AI draft reply)
under a distinct visual treatment so Steve can pick a direction.

- index.html: side-by-side scaled-iframe gallery with one-line descriptions
  + badges (familiar/dense/keyboard-first/team-inbox/etc.) for fast comparison.
- v1-classic-dense: today's pattern, refined dark theme, tight rows.
- v2-notion-cards: light theme, elevated cards w/ generous padding.
- v3-linear-keyboard: monochrome, status-machine (Triage/Working/Replied/Snoozed),
  J/K-style keyboard nav prompts.
- v4-apple-mail: 3-pane (mailboxes | list | reading), multi-account ready.
- v5-front-conversation: assignee-first, group inbox, internal-note threads,
  collision detection ('Cody is also viewing').
- v6-hey-zero: editorial Source Serif, Imbox/Feed/Paper Trail buckets, slow-mail vibe.

Browse at http://localhost:7400/mockups/inbox/index.html once norma-email
is back up. To promote one to Figma frames: kick the mcp__figma__authenticate
OAuth flow.
d433048 · 2026-05-20 · feat(modals): shared ResizableModal with full-screen toggle + drag-resize
- components/shared/ResizableModal.tsx — drop-in wrapper providing:
  - Maximize/Restore icon in header → expand to full viewport
  - Bottom-right corner drag handle → freeform resize
  - localStorage per modalId remembers size + maximized state
  - Esc-to-close, click-backdrop-to-close, smooth border-radius transition
- Retrofit both modals in CredentialsSection (User Create/Edit, Delete
  Confirm) as the proof point. Other Norma modals can adopt incrementally
  by swapping their wrapper div for <ResizableModal modalId="…" />.
38145df · 2026-05-20 · feat(gmail): per-message Assign-to + per-user read receipts + inline-expand layout
DB (migration 025_email_assign_read.sql):
- gmail_messages.assigned_user_id / assigned_at / assigned_by (FK tier_credentials)
- gmail_message_reads (message_id, user_id, read_at) — one row per (msg, user)

API:
- GET /api/users — assignable users (admin/staff/intern) for the dropdown
- POST /api/gmail/messages/[id]/assign {userId} — admin-only assignment
- POST /api/gmail/messages/[id]/mark-read — current user records read (idempotent)
- GET /api/gmail/messages — now returns assigned_name + read_by[] for each msg
- GET /api/gmail/messages/[id] — auto-records read receipt for the CURRENT user
  on view (per-user, replaces the legacy global is_read auto-flip)

UI (GmailCRMTab):
- Row footer chips: 'Assigned to <name>' (indigo) + 'Seen ×N' or 'Seen by X' (emerald)
- Detail panel: Owner dropdown (admin-only) + Seen-by pill list with read-at tooltips
- Layout: stacked column instead of 45/55 split — detail expands inline below the
  list rather than as a sibling side-pane, per Steve's 'open inline with expanding
  area' directive.
- Auto mark-as-read happens server-side on GET /messages/[id]; no extra client call.
f6b2dba · 2026-05-20 · feat(auth): intern role + user fields + admin-editable role × feature permissions matrix
- migration 024: extend tier_credentials role enum with 'intern'; add full_name,
  email, is_active, last_login_at, created_by; create role_permissions table
  (22 seeded feature_keys across 8 categories — gmail/drafts/petitions/contacts/
  news/grants/outreach/social/settings).
- lib/auth.ts: UserRole union type + ROLE_LABEL + ALL_ROLES exports.
- lib/permissions.ts: hasPermission(role, feature) reader w/ 60s in-process cache.
  admin implicitly allowed for every feature.
- /api/admin/permissions GET/PUT — admin-only matrix editor.
- /api/settings/tier-credentials POST/PUT now accept full_name/email/is_active;
  admin row can't be disabled or demoted.
- /api/auth/login: blocks is_active=false accounts (after pw check, so we
  don't leak which usernames exist); bumps last_login_at; returns full_name.
- CredentialsSection: new collapsible per-user permissions row showing every
  feature the user can access. Add User modal grew Full Name / Email /
  Active toggle / Intern role option.
- PermissionsMatrix.tsx: admin-editable feature × role checkbox grid in
  Settings, mounted directly under Users & Roles.

Also widened the 'admin'|'staff'|'pulse' role literal in AuthProvider,
Sidebar, and pulse layout to include 'intern'.
189ac33 · 2026-05-20 · feat(admin): comprehensive API/Token/MCP registry + age-adaptive theme slider
- lib/api-registry.ts: ~30 integrations across 10 categories (Gmail/Google OAuth,
  Gemini, Congress/ProPublica/BLS/Census/FRED/Scorecard, Action Network, Twitter/X,
  BlueSky, Reddit, Facebook/IG, Discord, Twitch, Slack, SMTP, internal infra, MCPs).
  Each entry: env vars, purpose, signup URL, docs URL, OAuth scopes, pricing,
  numbered get-this-token steps.
- /api/registry route: admin-gated, returns set/missing status per env var.
  Values NEVER leak — only booleans.
- ApiRegistryModal: searchable, status-filterable, grouped by category. Per-item
  card expands to show env vars (with copy buttons), scopes, pricing,
  step-by-step instructions, and signup/docs links.
- ApiRegistryButton in header (admin only) opens the modal.
- AgeThemeSlider on top of every page: rewrites CSS tokens through 8 bands
  (toddler/kid/tween/teen/adult/mature/senior/elder), persisted in localStorage.
  CSS bands appended to globals.css — typography + palette only, NOT a WCAG
  substitute.
4161ee6 · 2026-05-20 · session 2026-05-20: log 9 task entries (petition debounce, modal video, 4 pay-down todos)
Adds 2026-05-20 dated section (#33-#41) covering today's work:
- #33 petition double-submit debounce (commit 9619a2a)
- #34 sweep empty petition stubs
- #35 promote ready drafts + feature 3 heroes
- #36 modal training one-pager (commit fb319b1)
- #37 narrated 9-modals walkthrough video w/ cloned voice

Plus 4 pending pay-down items surfaced during this session:
- #38 ComposeModal mounts 3 stub drafts before user types (same anti-pattern
  as the petition create-page bug; same fix shape)
- #39 CompareModal needs ≥2-version seed for future live demo capture
- #40 ScheduleModal reused across 4 tabs — document canonical demo path
- #41 'Select organization' login-step gate missing from CLAUDE.md
fb319b1 · 2026-05-20 · docs: modal training one-pager for SDCC staff (companion to walkthrough video)
Covers all 9 admin-tier modals — Manage Tabs, Add Petition, Petition Draft
(AI), Add Grant, Compose Email, AI Member Email, Schedule, Bulk Schedule,
Compare. Each section: purpose, where to open, key fields, what saves to
PG, gotchas. Common patterns + troubleshooting + data-flow table at the end.

Pairs with the modal walkthrough video being built by app-demo-video skill
(output at ~/.claude/skills/app-demo-video/output/norma-modals-tour/).
9619a2a · 2026-05-20 · petition create: debounce double-submit via useRef guard + 30-min server-side merge window
Two-layer fix for the create-page double-submit bug that produced empty
stub + real-content dupe rows (SAVE Plan drafts incident, 2026-05-20).

Client (create/page.tsx): useRef inFlight guard runs synchronously inside
handleSubmit so React strict-mode double-fire and rapid clicks can't sneak
past the state-based 'submitting' gate.

Server (api/pulse/petitions/route.ts): before INSERT, look for a same-title
active petition created in the last 30 minutes. If found, MERGE the new
submission's missing fields into it (body/description/target/category/tags/
goal — only fills empties) and return the existing row with idempotent:true.
Catches rapid double-clicks (no-op merge) AND the publish-empty-then-publish-
filled pattern (stub gets filled in).

Smoke-tested locally: 3 submits with same title -> 1 row, all idempotent.
eb21d0d · 2026-05-20 · fix(api): cast org_id param as ::uuid not ::text in pipeline/stats + settings
petition_pipeline.org_id, drafts.org_id, news_items.org_id, collaborations.org_id,
partners.org_id, old_leads.org_id are all uuid columns. The previous $1::text NULL
guard caused pg to infer the bound param as text, then the org_id = $1 comparison
threw 'operator does not exist: uuid = text' for any non-null orgId.

/api/pipeline/stats was 500-ing for SDCC staff. /api/settings would have done the
same had the .catch() not been swallowing it silently into a 0-row default.

Cast both occurrences as ::uuid so the null-guard and the comparison agree.
c0eed7c · 2026-05-20 · docs: morning wakeup brief — DB password fix + verification path
Generated overnight while Steve was asleep. Single doc at repo root
covering: (1) the 5-second psql ALTER USER fix that unlocks everything,
(2) what's already working, (3) what's still broken until the fix runs,
(4) the 7 fixes committed overnight (CATEGORY_COLORS / IntegrationsTab /
credentials.rowCount + pulse-public UX), (5) the click-through audit
artifacts at /tmp/, (6) post-fix curl verification commands.

Surfaces the exact dw_admin password reset command Mac2 needs without
touching Kamatera's identical-named role.
52ff6c8 · 2026-05-20 · fix(pulse-public): silence 401 spam, unblock topics for guests, hide Apple OAuth stub, add sign-in CTA
5 fixes for unauthenticated Pulse visitor UX (per click-through audit
2026-05-20 at /tmp/norma-audit-2026-05-20.md):

1. /api/auth/session: return 200 + {authenticated:false} for guests
   instead of 401. Every Pulse page calls this on mount; the 401 was
   spamming red errors in browser DevTools + triggering Next.js prefetch
   retries. Client code already checks data.authenticated.

2. /api/petitions/topics: drop the requireRole gate. Route was 401'ing
   guests despite middleware allow-listing /api/petitions/* for public
   access. Topics list has no user-specific logic and should render
   pre-signup so guests can browse before creating an account.

3. /login: hide 'Continue with Apple' button behind NEXT_PUBLIC_APPLE_OAUTH_ENABLED
   env flag. /api/auth/apple route does not exist, so clicking the button
   gave a 404. Handler kept for when Apple OAuth ships; just no UI render.

4. /pulse/activity: error banner with 'please sign in' message now renders
   a green-themed card with an inline 'Sign in' CTA button linking to
   /login. Previously was a generic red error box with no path forward
   for guests.
720506c · 2026-05-20 · fix(types): unblock SocialTab render, IntegrationsTab metadata, credentials rowCount null
3 fixes for issues found via tsc pre-flight before tomorrow's QA:

1. components/social/SocialTab.tsx — define local CATEGORY_COLORS map at top
   of file. Was being referenced at lines 3298,3299 without import; would
   throw ReferenceError on social tab render. Same palette as the existing
   defs in components/graph/PartnerTree.tsx + IdeaMindMap.tsx. If we expand
   the palette later, lift this to a shared lib/category-colors.ts.

2. components/IntegrationsTab.tsx:257 — wrap metadata.workspace_name
   conditional in ternary instead of && so React's ReactNode contract is
   satisfied. The && form was leaving 'unknown' in the children array.

3. lib/credentials.ts:178 revokeApiKey — handle nullable rowCount via
   nullish coalescing. Previous code would TypeError if pg returned null
   rowCount on an UPDATE-RETURNING with zero matches.
ddf268e · 2026-05-19 · instagram-agent: enable live reel/story/post publishing via graph.instagram.com (Instagram Login API)
App is Instagram-Login-only (Facebook Login disabled), so all calls use graph.instagram.com not graph.facebook.com. Adds _ig-api.js helper (container create/poll/publish + long-lived token exchange) and setup-ig-token.js one-shot token installer.

File tree

870 files tracked. Click any to browse the source at HEAD.

Other build journals

← NationalPaperHangers  ·  all 4 projects  ·  Patty →

Export

commits.csv · feed.atom · project.json · commits.json

rendered in 2ms