Skip to content

docs: add macOS setup gotchas (npm prefix, ffmpeg, Groq key format)#45

Open
impactworks-dev wants to merge 146 commits into
earlyaidopters:mainfrom
impactworks-dev:main
Open

docs: add macOS setup gotchas (npm prefix, ffmpeg, Groq key format)#45
impactworks-dev wants to merge 146 commits into
earlyaidopters:mainfrom
impactworks-dev:main

Conversation

@impactworks-dev

Copy link
Copy Markdown

Summary

Adds a short "macOS setup notes" subsection in Step 1 of the README capturing the real issues a fresh Mac setup hits that aren't obvious from the current docs.

What's documented

  • npm i -g fails with EACCES on /usr/local/... — macOS ships an npm prefix owned by root. Docs the user-writable $HOME/.npm-global workaround so @anthropic-ai/claude-code installs without sudo.
  • Local voice output needs ffmpeg — the say + ffmpeg TTS fallback silently falls back to text without it. Adds brew install ffmpeg.
  • Groq API key format — only gsk_... values work; an org_... identifier returns HTTP 401 invalid_api_key.
  • Secrets belong in .env, not the shell — pasting a key at the zsh prompt produces command not found, easy to misread as a setup failure.
  • launchd PATH must include the Claude CLI dir — otherwise the bot starts but can't spawn Claude Code subprocesses (common when claude lives in $HOME/.npm-global/bin).

Also refreshes package-lock.json to match a current npm install.

Why

These were encountered during a real end-to-end setup on a clean Mac. Writing them in Step 1 up front saves newcomers from the same debug loop.

Testing

  • Ran npm install, npm run build, npm run setup equivalent config generation, launchd install, and npm run status — reports "All systems go."
  • Verified Groq STT end-to-end via transcribeAudio() in src/voice.ts (HTTP 200, correct transcript).
  • Verified local TTS end-to-end via synthesizeSpeech() (produces a valid OGG Opus buffer; round-trips back to text through Groq).

Conversation

Generated with assistance from Warp (conversation: https://app.warp.dev/conversation/7f78a63a-b2fe-461c-b0e0-794ba9f23ae3)

impactworks and others added 7 commits April 20, 2026 08:35
Captures real issues encountered during a fresh macOS setup walkthrough:
- npm -g EACCES workaround via user-writable prefix
- brew install ffmpeg required for local say + ffmpeg TTS fallback
- Groq API keys are gsk_* (not org_*) to avoid 401 invalid_api_key
- Clarify secrets live in .env, not the shell
- launchd PATH must include Claude CLI dir to spawn subprocesses

Also refreshes package-lock.json to match npm install.

Co-Authored-By: Oz <oz-agent@warp.dev>
Document the one-time local setup for:
- Obsidian Brain vault integration (path, registry, cleanup)
- GitHub SSH ed25519 key with Keychain-backed auto-load
- kepano/obsidian-skills install layout and user-level symlinks
- Agent template vault configuration
- launchd PATH requirements for npm-global binaries

No secrets are recorded; machine-specific values stay in .env.

Co-Authored-By: Oz <oz-agent@warp.dev>
Add a new chat_settings table to store per-chat preferences keyed by
chat_id and key. Use it to persist the voice-reply toggle so the bot
remembers each chat's voice mode across restarts.

Changes:
- db.ts: add chat_settings table and get/set/delete/getAllByKey helpers
- bot.ts: load voice-enabled chats on startup; persist /voice toggle
- index.ts: invoke loadVoiceEnabledChats() at startup and log result

Co-Authored-By: Oz <oz-agent@warp.dev>
Add .gitignore entries for files that are either machine-specific or
managed by tooling:
- .agents/ and matching .claude/skills/* symlinks (managed by `skills`)
- cli/ (third-party repo cloned for reference)
- memory/ (personal knowledge base)
- dashboard.html (generated from src/dashboard-html.ts)
- TASKS.md (personal task tracker)
- launchd/com.claudeclaw.tunnel.plist (absolute home paths)

Also track skills-lock.json so the skill install set is reproducible.

Co-Authored-By: Oz <oz-agent@warp.dev>
Introduces the warroom/ Python package: a WebSocket voice server that
bridges browser audio to ClaudeClaw agents via either Gemini Live
(default) or Deepgram STT + Cartesia TTS.

Built against pipecat-ai 1.0.0:
- WebsocketServerTransport owns the WebSocket server (host/port).
- SileroVADAnalyzer is attached via WebsocketServerParams.vad_analyzer.
- PipelineTask accepts params as a keyword-only argument.
- LLMContext + LLMContextAggregatorPair replace the old OpenAILLMContext
  + llm.create_context_aggregator() pattern.
- GeminiLiveLLMService replaces GeminiMultimodalLiveLLMService.
- Cartesia/Deepgram service imports use their .tts / .stt submodules.
- CartesiaTTSService uses Settings(voice=...) instead of deprecated
  voice_id kwarg.

Also ignores warroom/.venv/ and warroom/__pycache__/ from VCS.

Co-Authored-By: Oz <oz-agent@warp.dev>
The test 'truncates last_result to 500 chars' asserted
toHaveLength(500) against a 1000-char input, but updateTaskAfterRun
in src/db.ts truncates via result.slice(0, 4000). The input was
therefore never truncated and the length check was unreachable.

Align the test with the current runtime behavior: feed 5000 chars and
assert the stored value is exactly 4000.

Co-Authored-By: Oz <oz-agent@warp.dev>
Add War Room voice server on pipecat 1.0

@rubening rubening left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (Overnight Agent, Night #97)

Thanks for the setup docs content -- the macOS gotchas section is genuinely useful. However, this PR has several issues that make it unmergeable as-is:

  1. CLAUDE.md deletion -- This PR replaces the entire production CLAUDE.md with a generic template. CLAUDE.md contains the bot's personality, environment config, skills, special commands, and all operational instructions. Deleting it would break the bot.
  2. Unrelated code changes -- src/bot.ts, src/db.ts, src/index.ts modifications for voice persistence, and an entire warroom/ Python package, are bundled with what's labeled as a docs PR.
  3. Mixed commits -- 7 unrelated commits covering docs, code features, config changes, and a merge commit from your fork.

Recommendation: Please submit a clean PR with only the setup docs changes (a new docs/SETUP_NOTES.md and the relevant README additions). The voice persistence feature and warroom package are interesting but should be separate PRs with their own review.

The headRefName being main suggests these were committed directly to your fork's main -- consider using feature branches for future PRs to keep changes isolated.

impactworks and others added 22 commits May 19, 2026 20:13
- Add interactive dashboard with HTML UI and TypeScript backend
- Add War Room client bundle, avatars, and HTML interface
- Add Google OAuth authentication flow (scripts/google-auth.ts)
- Update package dependencies (package.json, package-lock.json)
- Add launchd service plists and restart-main.sh helper
- Various src/ module updates and new TypeScript sources

Co-Authored-By: Oz <oz-agent@warp.dev>
Ports five osrepo PRs onto the manual-port snapshot:

- PR earlyaidopters#51 (security/dashboard): timing-safe token comparison + CORS
  Origin allowlist + War Room WebSocket upgrade auth gate
- PR earlyaidopters#52 (security/dashboard-html): escape agent.name on 3 innerHTML
  render sites
- PR earlyaidopters#57 (fix/scheduler): pass agentDefaultModel to runAgent for
  scheduled tasks (mission tasks already correct)
- PR #67 (fix/orchestrator): pass target agent's model to runAgent
  in delegateToAgent (was undefined, ignored per-agent model)
- PR #69 (fix/scheduler): wire memory ingestion into mission and
  scheduled task completion

Skipped from PR earlyaidopters#51: CSRF middleware block (not present in snapshot),
requireToken helper (not present in snapshot).

Files: src/dashboard.ts, src/dashboard-html.ts, src/scheduler.ts,
src/orchestrator.ts. +77 / -10.
Adds the full v2 dashboard frontend from osrepo/main:
- web/ (55 files): index.html, main.tsx, App.tsx, 14 pages, 19 components,
  16 lib helpers, public/brain.glb (1.6MB)
- vite.config.ts: Vite root=web/, outDir=dist/web/, proxies /api+/ws+
  /warroom-* to backend in dev, vendor chunk split
- package.json + package-lock.json: adds preact, vite, three,
  monaco-editor, tailwindcss v4, dompurify, marked, lucide-preact,
  wouter-preact, @preact/signals, @preact/preset-vite, @types/three,
  @types/dompurify, @tailwindcss/vite

Backend wiring in src/dashboard.ts:
- Token middleware now gates only /api/*. SPA shell at / is public so
  token-stripped URLs still load the app and the SPA reads ?token= from
  window.location itself
- requireToken() helper for legacy HTML routes that embed DASHBOARD_TOKEN
- / route serves dist/web/index.html when present, falls back to legacy
  inline HTML if DASHBOARD_LEGACY=true or bundle is missing
- /assets/* serves Vite-built JS/CSS/source-maps with immutable cache
- /:filename.{glb|gltf|bin|ktx2|wasm|svg|png|webp|ico} serves top-level
  static files (e.g. /brain.glb for 3D Hive Mind)
- /warroom now calls requireToken() inline (was global middleware)

Known gaps vs osrepo/main (SPA pages will 404 on these endpoints):
- /api/dashboard/settings PATCH (Settings page)
- /api/security/kill-switch POST (Settings -> Security)
- /api/warroom/unpin POST (War Room)
These depend on src/kill-switches.ts and other osrepo modules not yet
ported. UI loads, most pages work, three buttons fail. Iterate later.

Files: src/dashboard.ts, package.json, package-lock.json, web/* (55),
vite.config.ts.
…, war room text)

Dashboard fixes (earlier today):
- token persisted in localStorage (no more 401 lockouts on fresh tabs/bookmarks)
- SPA history fallback for deep routes (no more 404 on refresh of /mission etc.)
- War Room voice WebSocket token fix (warroom-html.ts)

Feature parity ported from cached osrepo/main:
- /api/dashboard/settings (GET+PATCH), /api/security/kill-switch (POST)
- /api/agents/suggestions (4 routes), /api/warroom/text/* (11 routes) + /warroom/text page
- new modules: kill-switches, env-write, warroom-tool-policy, warroom-text-*
- security.ts getScrubbedSdkEnv, state.ts abortByPrefix, memory-ingest extractViaClaude
- buildMemoryContext opts param, AgentConfig.warroomTools (additive, backward compatible)

db.ts:
- new tables: dashboard_settings, agent_suggestions, agent_file_history
- warroom_meetings meeting_type + chat_id migrations
- 26 parity helper functions; getWarRoomTranscript/addWarRoomTranscript upgraded (back-compat)

chore: harden .gitignore against untracked OAuth client_secret + personal working files
- Multi-stage Dockerfile with Python venv + Claude Code CLI baked in
- docker-entrypoint.sh bridges Fly secrets to /app/.env, restores Claude creds
- fly.toml: app config, persistent volume, single machine, no HTTP healthcheck
- Run as non-root node user (Claude Code refuses --dangerously-skip-permissions as root)
- agents/*/agent.yaml: obsidian vault path → /app/store/obsidian-brain (syncthing target)
- .github/workflows/fly-deploy.yml: auto-deploy on push to main
- scripts/fly-*.sh: setup, data migration, cutover, syncthing helpers
- FLY-MIGRATION.md: end-to-end runbook
Includes daily brief, memory-to-tasks dispatcher, sidebar rebrand, dashboard updates
that were running locally on Mac before cutover but missed the first CI commit.
Without this, every deploy wipes /home/node/.claude/projects/ and stored
session IDs become orphans. Symlink the dir onto the Fly volume so sessions
survive container restarts. No more manual DELETE FROM sessions needed.
Bumps machine to 2GB to fit 5 node processes + Gemini bursts.
Each sub-agent uses its own Telegram bot token from /app/.env.
When main exits, sub-agents die with the bash shell, supervisor restarts.
Mobile Safari ITP clears localStorage after 7 days of inactivity, causing
all API calls to return 401 (dashboardToken becomes empty string).

Frontend (api.ts):
- On every load, persist token to BOTH localStorage AND a 30-day cookie
  (claw_token; Secure; SameSite=Lax). Cookie is also read as final fallback
  so cleared localStorage or private browsing never breaks auth.
- All fetch helpers now send Authorization: Bearer <token> header in addition
  to the existing ?token= query param.

Backend (dashboard.ts):
- API auth middleware now accepts the token from (in order):
  1. ?token= query param  (original, always present when token is known)
  2. Authorization: Bearer header  (new - sent by every SPA fetch)
  3. claw_token cookie  (new - set by SPA on first authenticated load)
This means once a mobile browser visits with ?token= once, subsequent visits
work indefinitely without re-injecting the token.
Static token entry page at /auth.html. On any api 401/403, the SPA hops
there with a return param. Form verifies the token via /api/health and
persists to localStorage + cookie. Token already cached → silently
redirects back. Force re-entry with ?forceLogin=1.
Adds refreshing state to useFetch so pages can distinguish first-load (loading)
from background revalidation. Button now disables, swaps copy to Refreshing...,
and spins the RefreshCw icon (Tailwinds animate-spin) while a fetch is in flight.
Claude Code SDK with OAuth (Pro/Max subscription) only populates usage on
the final result event, not on per-assistant-message events. Without a
fallback, lastCallCacheRead and lastCallInputTokens stayed 0 so the
token_usage table got context_tokens=0 every turn, breaking /convolife
percent calculation and the dashboard usage view.

Fall back to the result event aggregate when per-message events were empty.
SPA calls /api/tokens, /api/health, /api/memories without ever sending
a chatId. Backend filtered by empty string, returning zeros even though
the DB has 254 rows for the real chat. Default to ALLOWED_CHAT_ID so
the page shows the primary authorized chats data when no chatId is
explicitly provided.
impactworks and others added 30 commits June 8, 2026 09:24
Order intake forms have business_name often blank, literal "null", or stale.
Pull the canonical name from /businessLocations resource (same source the
Vendasta Partner Center UI uses) and override any order-form name with it.

Also filter literal "null" / "undefined" / empty strings as not-a-name.

Recovers ~47 previously-unnamed customers including Reesource Pest,
Data Check Systems, ZAGG Phone Repair, NEATCap Medical, etc.
Adds optional full per-account customer breakdown (retail MRR, wholesale,
margin) to the revenue endpoint. Default response unchanged — only top 5
to keep dashboard payload slim. ?full=1 returns the whole list for
report generation.
Phase A+B of the QBO Class-based attribution build.

- revenueByAccount now joins each AG-ID against the CRM list_records
  endpoint to attach a marketSlug (pwps = ImpactWorks, default = Rocket
  Local). Surfaces both byAccount[].marketSlug AND a pre-aggregated
  byMarket totals block.

- CleanedRevenue now exposes a brands[] array with per-brand
  customerRetailMRR / wholesaleMonthly / grossMargin / marginPct /
  customerCount / retailShare. Internal accounts (Pest WebPros + Rocket
  Local internal) are excluded just like the top-line numbers.

This is the data layer that powers the upcoming Mission Control tile,
monthly Settlement Slip generator, and Telegram-approved QBO journal
entry write.
Slim view (id, name, type, balance, active) used by the upcoming
Settlement Slip generator to resolve the 5 QBO account IDs needed
for the per-brand Vendasta journal entry.
…(Phases D+E)

Phase D — scripts/settlement-slip.mjs:
  Pulls per-brand split from /api/vendasta/revenue, resolves the 5 QBO
  account IDs from /api/qb/accounts, builds a balanced journal entry,
  saves it as a pending slip in store/pending-slips.json, generates a
  Google Doc, prints a Telegram-ready summary with a slip ID.

Phase E — connectors/quickbooks/server.mjs + scripts/post-settlement-slip.mjs:
  - New qbo_create_journal_entry tool: POSTs JournalEntry to QBO with
    balance validation (rejects unbalanced entries before sending).
  - New qboPost helper alongside qboGet.
  - post-settlement-slip.mjs: loads pending slip by ID (or latest), calls
    qbo_create_journal_entry, stores QBO entry ID + link, updates slip
    status to posted. Called by Nikki when Dante replies post slip <id>
    in Telegram.

Books structure (no Classes, separate accounts per brand):
  DR Bank
  DR ImpactWorks - Vendasta Wholesale (COGS)
  DR Rocket Local - Vendasta Wholesale (COGS)
  CR Income: ImpactWorks
  CR Income: Rocket Local
Tomorrows test fire is scheduled (task 59407a3d, June 9 9am EST). After
it fires, Nikki deletes the one-shot task and the monthly recurring
task (a1f38408, 5th of month at 9am EST) remains.
…nt cap

Added the people-map.json entries Nikki uses to interpret incoming
iMessages with proper context (family vs client vs friend).

Roster: family (wife, son, stepson, daughter, stepdaughter, mother,
father, mother-in-law, uncle, aunt, cousin), close circle (best friend
since high school, mentor, ex-roommate + NEW founder), and clients
(Reesource Pest, ZAGG, John Fenrich, Ralph Caparotti).

Also bumped /recent cap from 200 to 5000 so future tagging passes see
more of the message history.
…disambiguation

- Auto-added 25 email handles from contacts.json to people-map for all
  previously-tagged contacts (Jennifer, Ralph, Emily, John, Jim, Bill
  Warner, Nicole, Jeffrey, Cam, etc.)

- Disambiguated Vincent Sr (father, name Silvio Vincent Crescenzi) from
  Vincent Jr (son, same full name, goes by Vincent).
- Re-tagged vincent@impactworks.com from father to son (its actually
  the sons work email).

- Re-tagged dante+ai@impactworks.com from business line to self.
- Added dante@impactworks.com + finance@impactworks.com as self.
src/people-resolver.ts:
  - Loads relay/people-map.json (priority) + relay/contacts.json (gaps)
  - resolvePerson(handle) returns { name, relationship?, org?, notes?, source }
  - categorize(person) returns family|inner-circle|self|client|vendor|...
  - mtime watcher reloads files on change (no restart needed)
  - 78 manually-tagged entries + 2248 contact-only handles indexed

Wired into:
  - email-data.ts buildBriefEmailBlock: senders now show
    name (relationship) with a category badge (👪 ⭐ 💼 🔄)
  - New /api/people/stats + /api/people/resolve?handle=...
    admin endpoints for ad-hoc lookups

Smoke tested against 12 sample handles — all resolve to expected
category. Father/son disambiguation works (vincent@impactworks.com →
son, svc@cbmcpa.com → father).
…Inbox tile

Server enrichment: shapeRow() now resolves the sender via people-resolver
and attaches senderPerson { name, relationship, org, category } to every
EmailRow. Single in-memory cache lookup per row, no extra latency.

Frontend: InboxCard.tsx EmailRowView renders
  👪 Audra Crescenzi (wife)
  ⭐ Jim Roberts (close friend / business community)
  💼 Ralph Caparotti (client / business partner)
  🔄 Dante Crescenzi (self)
instead of raw email addresses.

Badge legend:
  👪 family · ⭐ inner circle · 💼 client · 🔄 self/business
  🏷️ vendor · 🎓 professional · (none) other/unknown
People-resolver was returning 0 entries on Fly because the Dockerfile
did not COPY the relay/ data files. The TS code was deployed but the
data files were not.

Also adding contacts.json to git (was created locally from vCard import
but never committed). Repo is private — these are committed alongside
all other personal data files already in store/ etc.

After this deploy:
  /api/people/stats should return 78 peopleMapEntries + 2248 contactHandles
STOCKS:
  Stooq added a JavaScript proof-of-work browser-verification challenge
  to their CSV endpoint — every server-side fetch returns 404. Switched
  the quote fetch from Stooq to Twelvedata /quote endpoint. We were
  already using Twelvedata for chart history so this consolidates on
  one provider. Free tier (800/day) + 5min cache = safe headroom.

AI NEWS:
  Each NewsItem now carries iconUrl (Googles s2/favicons proxy keyed to
  the publishers domain) and sourceDomain (extracted from the <source
  url=> attribute in the RSS feed). Frontend renders a 28px circular
  badge next to each story with the publisher logo, falling back to the
  source name initial if the favicon fails to load.
CONNECTOR: new plaid_get_holdings tool that calls /investments/holdings/get
across every linked Plaid item, aggregating accounts + securities +
holdings. Items without the investments product attached are skipped
quietly via per-item error capture rather than throwing.

LINK FLOW: plaid_create_link_token now requests both transactions AND
investments products. Plaid auto-disables products an institution does
not support, so this is safe for banks like Novo while unlocking
brokerage support for Schwab / Vanguard / Stash / Robinhood / etc.

DATA LAYER: src/investments-data.ts. Aggregates per-account current
value + day change, top-10 holdings sorted by value with weight %,
gross totals. 30-min cache on Fly volume.

ENDPOINT: /api/investments (?force=1 bypasses cache).

DASHBOARD: Investments tile rendered between cash-pulse and cash-pipeline.
Shows hero portfolio value + day delta, per-institution list with mask +
day change, top-5 holdings with ticker / value / weight. Empty state
nudges user to /cash/connect to link their first brokerage.

Section order bumped to v4 to add investments slot.
… brokerage

CONNECTOR: plaid_get_holdings now resolves institution_name from Plaid
when missing. Calls /institutions/get_by_id with the institution_id
returned on the item, persists the name back to items.json so subsequent
calls hit the cached value. Fixes accounts linked tonight that came
through without an institution_name (Plaid Link didnt forward it).

FRONTEND: Per-account list on the Investments tile now GROUPS by
institution. Each group shows VANGUARD / STASH / etc. as the bold header
with the brokerages subtotal, then the individual accounts indented
underneath with their name + subtype + mask + value. Much easier to
tell which accounts came from where.
…oint.sh helper

Lets Mac/Cowork sessions push a high-salience pinned session checkpoint
directly into Nikki's memory table on Fly. Closes the gap where the
local save-nikki-checkpoint.py only wrote to the Mac copy.

POST /api/memory/import-checkpoint
  Auth: DASHBOARD_TOKEN (Bearer or ?token=)
  Body: { summary, raw_text, chat_id?, agent_id?, entities?, topics?, importance? }
  Defaults: chat_id=ALLOWED_CHAT_ID, agent_id=main, importance=0.95
  Always source='session_checkpoint', pinned=1.
Root cause: all 5 agents (main + 4 sub) called initDatabase() simultaneously
on container boot with no SQLite busy_timeout, causing SQLITE_BUSY crashes.

Fixes:
1. db.ts: busy_timeout=30000 before journal_mode=WAL so SQLite retries on BUSY
2. docker-entrypoint.sh: WAL checkpoint at boot + main starts first, sub-agents
   after 5s delay + wait MAIN_PID instead of exec
3. scripts/settlement-slip.mjs: fix hostname + slim revenue endpoint
Completes the WAL-contention fix (55a6853). The entrypoint reorder +
busy_timeout stopped the simultaneous-open crash; this closes the remaining
hole in the single-instance lock itself.

Before: on boot, acquireLock SIGTERM'd whatever PID was in the lock file then
proceeded after a fixed 1s sleep — opening the DB even if the previous instance
was still alive and holding the WAL (the contention window), and wasting that
1s on an already-dead PID after a crash.

After:
- isAlive(pid) probe (kill -0) distinguishes a live holder from a stale PID.
- Live holder: SIGTERM, then poll until it actually exits (10s grace) before
  reclaiming, escalating to SIGKILL if it ignores SIGTERM. We never open the DB
  while the prior instance still holds it.
- Stale/crashed PID: reclaimed instantly — the real crash-recovery path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move apiGet call from render body into useEffect with cleanup
  (prevents multiple concurrent fetches on parent re-renders and
  ensures state updates fire correctly)
- Pass e.id (message ID) to the API instead of e.threadId — the
  gmail-cli read command requires a message ID, not a thread ID;
  for multi-message threads these differ and the old code would 500
- Keep threadId on the modal for the Gmail deep-link URL (correct)
- Add cancellation flag so stale callbacks don't stomp state after
  the modal closes
1) Inbox modal showed blank email content because getThread() read j.from / j.bodyText but gmail-cli wraps the payload in j.message. Now reads from j.message.* and treats missing message as an error.

2) Latest brief header showed the Sun at night because it used the stored brief_kind from the most recent brief regardless of how old it was. Now uses the briefs kind ONLY if it matches the current time-of-day bucket, otherwise falls back to themeFromCurrentHour().
- Prefer bodyHtml over bodyText so newsletters and marketing emails render with their original layout and images (was showing the bland plain-text version even when HTML existed).
- Add sanitizeEmailHtml() helper: strip script tags, event handlers, javascript: URLs, form/input elements. Cap at 2MB so a malicious large email cant lock up the browser.
- Add scoped .email-html-body CSS in main.css to undo Tailwind preflight inside the email body: images max-width 100%, links use accent color, tables/lists/blockquotes/headings render with sensible defaults.
… memory

Journal:
- New SQLite table journal_entries (date PK, gratitude_1/2/3, great_today_1/2/3, affirmation, highlight_1/2/3, learned, completion timestamps)
- 4 API endpoints: /api/journal/today, /journal/entry/:date GET+POST, /journal/recent
- Daily rotating gratitude quote (30 quotes, deterministic by date)
- Streak tracking (current + longest consecutive morning entries)
- JournalCard component: compact morning gratitude entry on Founder Dashboard with sunrise gradient + quote + streak indicator
- Journal full page at /journal: morning section (gratitude, what would make today great, daily affirmation) + evening section (highlights, what I learned), date navigation, sun/moon decorations
- Whimsical aesthetic via Caveat (handwritten cursive) + Cormorant Garamond (italic serif), parchment tones, dashed dividers
- Light + dark mode variants

Brief mark feedback wiring:
- /api/brief/:id/mark now writes a memory note when action is set
- acted: low-importance memory tagging which brief drove activity
- ignored: high-importance pinned memory so Nikki notices format may need rethink
…mode

- Removed all [data-mode=dark] overrides on .journal-* selectors
- Morning section: peach/apricot wash, sunrise hero, gold accents
- Evening section: lavender/rose wash, dusk colors (purple text + caret), distinct from morning
- Sun icon stays gold, moon icon goes lavender for visual identity
- Used !important on the journal-specific properties so future theme tokens cant bleed in
- Page background stays a consistent warm cream canvas under the cards
Tone:
- Replaced bright peach/lavender with calm warm-linen + dusty-sage + dusty-mauve palette inspired by The Way app (Henry Shukman)
- Single CSS token set as :root vars on .journal-card and .journal-page
- Toned-down radial gradients, lower saturation

Meditation:
- Added meditation_minutes / meditation_sessions / meditation_last_at to journal_entries with backward-compat migration
- POST /api/journal/meditation/:date increments todays sit count
- MeditationSession modal: 5 duration chips (3/5/10/15/20), breath orb expanding 4s, holding 2s, contracting 4s, with cycling cue text
- Soft sine-wave bell on session start + end via Web Audio API
- Done-early button logs partial sit
- 15 Henry Shukman / Thich Nhat Hanh / Zen quotes added to rotation

Practice rings + gamification:
- New PracticeRings component: three thin SVG progress rings (Gratitude / Sit / Reflect)
- 28-day path-of-stones grid on the Journal page
- Streak math expanded to journal, meditation, and practice tracks (practice = lenient combined)
- Removed fire emoji in favor of italic Cormorant Garamond
- New Meditation section between Morning and Evening on the Journal page
Two causes of 'can't access Nikki' after successful boot:

1. Telegram 409 crash loop:
   When the container restarts, the previous instance's 30-second
   getUpdates long-poll is still open. The new instance immediately
   tries to poll → 409 Conflict → GrammyError propagates to main().catch
   → process.exit(1) → restart → 409 again → hits Fly max_retries=10
   → machine permanently stopped.
   Fix: wrap bot.start() in a retry loop. On 409, log a warning and
   wait 35s (> the 30s poll window) before retrying. Up to 6 retries
   (3.5 min total). Any other error still crashes as before.

2. Fly default restart policy stops machine after 10 failures:
   fly.toml had no [restart] block so Fly used 'on-failure / max=10'.
   Fix: add [restart] policy = 'always' so Fly keeps trying forever
   with exponential backoff, same as any production service should.
flyctl Config.restart is typed as []appconfig.Restart (a Go slice),
so TOML requires array-of-tables [[restart]] not a plain [restart] object.

Previous commit broke CI with:
  json: cannot unmarshal object into Go struct field Config.restart
  of type []appconfig.Restart
When Plaid credentials expire, the connector was hanging for 60-150s
before throwing, blocking /api/founder and making the dashboard appear
blank on refresh.

Two fixes:
1. Add timeout: 10_000 to execFileAsync in plaidCall() — credentials
   errors now surface in ~10s instead of 60-150s.
2. readStaleCache() fallback — if the live call fails and there's any
   prior cache on disk (even expired), return it rather than an empty
   cash section. Stale data is better than a blank dashboard.
The pre-flight pipecat import check used a 10s timeout, but pipecat pulls
in heavy ML/audio deps and can take 20-30s to import on a cold container.
When the timeout fires, the code thought deps were missing and sent a
Telegram message every time. Combined with the 409-conflict retry loop
(triggered by a second Telegram poller), this produced periodic spam.

- Bump timeout to 60s
- Distinguish a real ModuleNotFoundError (worth alerting on) from a cold-
  import timeout (silent log + skip War Room this boot, next boot will
  likely succeed)
- Only Telegram-spam on genuine missing deps
Connector extension (read-only):
- vendasta_platform_list_campaigns
- vendasta_platform_get_campaign
- vendasta_platform_list_email_templates
- vendasta_platform_list_automation_runs

Resource paths are best-guess from naming conventions (marketingCampaigns,
emailTemplates, automationRuns). Marked PROBING in the code — verify on
first call and adjust if Platform API returns 404.

Data layer (src/vendasta-campaigns.ts):
- 10 min cache on Fly volume
- Aggregates totalRecipients, emailsDelivered, weighted avg open rate
- Counts published/ongoing/draft
- Surfaces top 5 by emailsDelivered

API: GET /api/vendasta/campaigns

Founder Dashboard CampaignsCard:
- Inserted between attention and real-mrr in the section order
- Hero stats: reach, weighted open rate, recipients
- Top 3 performing campaigns with delivered + open rate
- Open-in-Vendasta link to partners.vendasta.com/marketing/campaigns/all
- Section order bumped to v6
…ting

Verified at developers.vendasta.com that the Partner Platform REST API
exposes: Platform / Business / CRM / Advertising Intelligence /
CalendarHero / Customer Voice / Local SEO / Reputation AI / Social
Marketing / SCIM — but NOT Marketing Campaigns.

The /marketing/campaigns/all UI uses Vendasta internal/private API
that is not part of the documented partner integration surface.

Changes:
- Connector: list_campaigns / get_campaign / list_email_templates now
  return a documented unsupported_by_vendasta_public_api error instead
  of attempting 404 calls. list_automation_runs kept as a probe since
  /automations itself works.
- Data layer: passes the unsupported state through cleanly.
- Founder Dashboard: removed campaigns from DEFAULT_SECTION_ORDER (v7).
  The CampaignsCard component stays in the repo in case the API ever
  opens up.
- Memory: vendasta-campaigns-api-limitation.md documents the dead-end
  so future sessions dont retry.

Workaround if Dante asks again: infer campaign activity from the CRM
activity feed via vendasta_list_records on activities.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants