Commit Graph
69 Commits
Author SHA1 Message Date
jjbrunton 2c533fa52a Verify Reddit identity before Matrix capture 2026-08-08 16:03:54 +01:00
jjbrunton 2fd92bf82c Gate bot logic on full account authentication 2026-08-08 16:01:06 +01:00
jjbrunton 4db6de8bf5 Stop Matrix refresh loop when login is unavailable 2026-08-08 15:41:02 +01:00
jjbrunton 674cd89cd7 Use full Chrome for interactive Reddit login 2026-08-08 15:30:24 +01:00
jjbrunton 70d62b2da3 feat(dashboard): route settings and access 2026-08-08 15:26:40 +01:00
jjbrunton 79de6aa2a1 feat: add settings and access dashboard slice 2026-08-08 15:25:34 +01:00
jjbrunton dcf2a1200c Clear stale Chromium profile locks on startup 2026-08-08 15:22:20 +01:00
jjbrunton 364b4db200 docs: document dashboard auth and typed events 2026-08-08 15:20:31 +01:00
jjbrunton 271f2d1fb5 Handle non-string image content types 2026-08-08 15:18:28 +01:00
jjbrunton 51438aa5db Keep x11vnc alive for remote login 2026-08-08 15:16:51 +01:00
jjbrunton 89f7376b1b fix(dashboard): deduplicate realtime log replay 2026-08-08 15:15:54 +01:00
jjbrunton e003b0d8b2 feat(dashboard): route performance and operations 2026-08-08 15:11:48 +01:00
jjbrunton 5917b63755 feat(dashboard): add performance and operations slices 2026-08-08 15:10:52 +01:00
jjbrunton 20fa83f245 feat(dashboard): route conversations and media 2026-08-08 15:06:19 +01:00
jjbrunton 768470204b feat: add private media dashboard slice 2026-08-08 15:05:30 +01:00
jjbrunton 8d4eae7ab5 feat(dashboard): add conversations workspace 2026-08-08 15:05:08 +01:00
jjbrunton 40e5f4c818 chore(deps): apply audited dependency fixes 2026-08-08 15:02:23 +01:00
jjbrunton 9919b12aab feat(web): bridge logs to typed event stream 2026-08-08 15:00:02 +01:00
jjbrunton d6d29badcd feat(dashboard): align contracts and production build 2026-08-08 14:58:27 +01:00
jjbrunton 69d560d5c5 feat(web): add Express SSE event adapter 2026-08-08 14:56:37 +01:00
jjbrunton 9f4f198800 fix: align realtime event contracts 2026-08-08 14:54:09 +01:00
jjbrunton 51e3e28a9b feat(dashboard): add React foundation and overview 2026-08-08 14:53:53 +01:00
jjbrunton 59c3a61a88 feat: add shared dashboard API contracts 2026-08-08 14:52:06 +01:00
jjbrunton 939cf408a0 feat(web): add typed realtime event core 2026-08-08 14:50:15 +01:00
jjbrunton 78e84fe36b Fix noVNC WebSocket routing 2026-08-08 14:48:58 +01:00
jjbrunton 316dc3dd3e Show Reddit recovery status in dashboard 2026-08-08 14:42:19 +01:00
jjbrunton d2b3a1b31d Retry Matrix capture after chat login redirect 2026-08-08 14:34:10 +01:00
jjbrunton bffda95251 Improve remote Matrix token refresh 2026-08-08 14:30:40 +01:00
jjbruntonandClaude Opus 4.7 b9df8459e3 Atomic chat-cache writes + stop tests clobbering prod cache
Three intertwined fixes for the torn-write that corrupted the prod
data/chat-cache.json mid-session:

  - ChatCache.save / persistCooldown now write via temp file + rename.
    rename(2) is atomic on the same filesystem, so a crashed or
    concurrent writer can never leave NULs in the middle of the file.
    A small serialising write queue stops two concurrent saves from
    racing to the same temp name.

  - ChatCache throws if constructed without a filePath under NODE_ENV=test.
    The old default (./data/chat-cache.json) silently routed test
    writes into the production cache — exactly how the corruption
    happened. Future test that forgets to pass a tmp path fails loudly
    instead of stomping data.

  - Cooldown / delta-media / username-persist tests now pass an
    explicit tmp ChatCache so the new guard is satisfied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:48:33 +01:00
jjbruntonandClaude Opus 4.7 71548ec2ad Resolve invitees via /state and accept 'invite' membership
For chats where we sent an opening message but the recipient never
accepted, /sync drops the room with no heroes and the recipient is
in state as m.room.member with membership:'invite' — not 'join'.
chat.reddit.com's web UI labels these rooms with the invitee anyway,
because it reads the full room state.

  - roomToActiveChat now accepts 'invite' membership in addition to
    'join' when scanning member-join state events.
  - enrichUsernames pass 2 hits /_matrix/client/v3/rooms/{id}/state
    first (cheap, returns all member events including invites) and
    only falls back to /messages history if /state yields nothing.
  - Added MatrixClient.getRoomFullState to back that lookup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:41:31 +01:00
jjbruntonandClaude Opus 4.7 cf7fc41761 Fix username resolver: use www.reddit.com, not api.reddit.com
api.reddit.com/api/user_data_by_account_ids returns 404 without an
OAuth token — which is why every chat in the dashboard sat on
t2_xxx or unknown. The same path on www.reddit.com (with a .json
suffix) is served unauthenticated and returns the expected user
map. Switch the resolver to that host so enrichUsernames actually
succeeds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:30:01 +01:00
jjbruntonandClaude Opus 4.7 01ea8fd36b Mine t2 IDs from timeline events and /messages history
Reddit's homeserver does not expose /_matrix/client/v3/rooms/{id}/joined_members
(it returns 'page not found'), so the previous fallback was always a 404.
Two replacements:

  - roomToActiveChat: include timeline event senders as candidate MXIDs
    alongside heroes / member-join state. /sync omits heroes for idle
    rooms but always carries full sender MXIDs in any timeline events
    it does return, so we recover a t2 ID whenever the delta sees a
    message.

  - enrichUsernames: split into three passes — apply known mappings
    first (so re-created delta chats get upgraded without a restart),
    then page /messages 'b' once per still-unknown room to mine a t2
    from the recipient's history, then resolve any remaining raw t2s
    via the bulk user_data endpoint. Failed history lookups are cached
    in unresolvableRooms so we don't retry every sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:15:27 +01:00
jjbruntonandClaude Opus 4.7 1df7b680ee Stop re-attempting /joined_members on rooms we can't read
Reddit's homeserver returns 404 from /_matrix/client/v3/rooms/{id}/joined_members
for rooms we've left, been kicked from, or never fully joined — which is
roughly every chat that wasn't accepted by the recipient. The previous
enrichUsernames pass retried these on every delta sync, flooding the
dashboard log with debug-level 404s. Track failures in an in-memory
unresolvable set so each room is tried at most once per process.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:06:33 +01:00
jjbruntonandClaude Opus 4.7 ad5c63fc50 Fall back to /joined_members when delta sync omits the other party
Reddit's homeserver sometimes returns a room in /sync with no
m.heroes and no joined m.room.member state events, leaving us with
no t2 ID to resolve and the chat stuck on 'unknown'. enrichUsernames
now hits /_matrix/client/v3/rooms/{roomId}/joined_members for any
chat whose username is 'unknown' or a bare MXID, stamps the t2 ID,
and lets the existing resolver fill in the real Reddit username.

Also treats a cached 'unknown' username as stale in roomToActiveChat
so subsequent deltas re-attempt the lookup instead of pinning it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:01:06 +01:00
jjbruntonandClaude Opus 4.7 2edbc7dbf7 Resolve t2 usernames after every delta sync
enrichUsernames() was only invoked once at startup, so any rooms that
arrived via a later delta /sync stayed pinned to their raw t2_xxx
identifier — which the dashboard then rendered as a bare Matrix-style
ID. Call enrichUsernames() at the tail of syncOnce so new rooms get
resolved to real Reddit usernames before the UI reads them.

Also pass the unformatted username to openConversation() and format
the modal title in the frontend, so lookups by username keep working
after the display sanitisation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:52:04 +01:00
jjbruntonandClaude Opus 4.7 d9b9a79bdd Make subreddit list editable from the dashboard
Adds an inline subreddit editor on the dashboard that lets you add,
remove, and edit (tone, type, weight) the subreddit list, with changes
persisted to config.yaml on disk so they survive restarts.

Backend
- src/config/persistence.ts: round-trips config.yaml via
  YAML.parseDocument, mutating existing row nodes in place so their
  trailing inline `# ...` comments survive an edit; new rows are
  appended as flow-style maps to match the on-disk idiom; removed rows
  are dropped. Default values ('default' tone, 'image' type) are
  omitted on write so the file stays clean. Atomic via tmp+rename.
- src/web/routes/config.ts: GET /api/config now also returns the
  current subreddit list and the available tone keys. PUT /api/config
  accepts a `subreddits` field, validates with the existing
  SubredditConfigSchema, rejects duplicates, persists, then mutates
  ctx.config.reddit.subreddits in place so the processor picks up the
  new tone immediately without a restart.

Frontend
- public/index.html: replaces the read-only subreddit chip list in the
  Configuration snapshot section with a grid of editable rows (name,
  type, tone dropdown, weight, remove). + Add / Save buttons with
  inline status. Tones are sourced from /api/config so the catalog
  drives the dropdown.

Tests
- tests/config-persistence.test.ts: covers in-place mutation comment
  preservation, removal, appends, reorder, default omission, and
  atomic write cleanup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:43:05 +01:00
jjbruntonandClaude Opus 4.7 46a213e3e0 Add subreddit tone profiles for message style guidance
Introduces a tone-profile system that lets the processor adapt outgoing
message style per-subreddit (and per-flair) instead of using one tone
for everything. Profiles act as a "tone mic" layered after the base
prompt and strategy override, so they shape voice without redefining
the whole message.

- src/config/tone-profiles.ts defines the catalog (default, dominant,
  submissive, playful, direct, ...) and a resolveTone() helper that
  falls back to the default profile for unknown keys.
- config.yaml maps subreddits to tone keys; the schema in
  src/config/index.ts validates it.
- src/services/processor.ts threads the resolved tone into the prompt
  build, picking based on subreddit and optional link flair.
- src/reddit/api.ts adds link_flair_text to RedditPost so the
  processor can use it for tone selection.
- src/ai/openrouter.ts accepts and forwards the tone-shaped prompt.
- public/index.html surfaces the active tone in the dashboard.
- Tests cover the catalog, config validation, and processor integration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:29:01 +01:00
jjbruntonandClaude Opus 4.7 0c7df6e1d3 Add Matrix credential upload route + local seed script
Closes the noVNC-unavailable gap: when Dokploy's reverse proxy strips
WebSocket upgrades for noVNC, there's no way to do a one-time
interactive Reddit login in the deployed container. Without that,
extractMatrixCredentials inside the container hits the /chat/ login
wall and never sniffs a bearer.

Add POST /api/debug/matrix-credentials that accepts a credentials JSON
body, persists it to data/matrix-credentials.json, and hot-swaps it
into the running ChatService via setMatrixClient. Also export
saveCredentials from the chat module so the route can reuse the same
cache-file logic as the in-container sniff.

scripts/seed-matrix-creds.ts opens a headed Chromium on the dev's
machine, waits for an interactive Reddit login (detected by polling
/api/me.json from inside the page), opens www.reddit.com/chat/, sniffs
the first authed matrix.redditspace.com request, resolves user_id via
/whoami, and POSTs the result to BOT_URL/api/debug/matrix-credentials.
End-to-end seeding without ever touching the container.

Usage:
  BOT_URL=https://red.apps.jbrunton.co.uk tsx scripts/seed-matrix-creds.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:20:53 +01:00
jjbruntonandClaude Opus 4.7 e58bccf6b5 Sniff Matrix token via www.reddit.com/chat/ + cookie dump endpoint
chat.reddit.com now redirects to www.reddit.com/chat/, and that SPA was
bouncing automated logins to the /login/ wall (confirmed via the new
matrix-debug.png endpoint — the captured screenshot shows the login UI,
not the chat surface, despite isLoggedIn() returning true seconds
earlier). Going direct to the canonical URL means the session cookie set
by the automated /login form-fill is applied to the very first request
instead of only after a cross-subdomain redirect, which is where it was
getting dropped. Switch goto from awaited domcontentloaded on the old
URL to a non-awaited domcontentloaded on the new URL so the request
listener can resolve the moment the SPA fires its first authed call to
matrix.redditspace.com.

Adds GET /api/debug/cookies as a temporary diagnostic — returns cookies
visible to www.reddit.com, chat.reddit.com, and matrix.redditspace.com
(values hashed by default; ?reveal=1 to dump raw). Lets us compare what
the browser holds vs. what /chat/ rejects if this doesn't fix it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:05:32 +01:00
jjbruntonandClaude Opus 4.7 2bf8989ec7 Pipe winston logs to dashboard + add Matrix sniff debug route
The dashboard's /api/logs buffer only ever saw lines explicitly pushed
via broadcaster.push, so winston output (including the Matrix
token-extractor's timeout diagnostics) was invisible from the deployed
container. Attach a winston transport that forwards every entry into the
LogBroadcaster before the rest of bootstrap runs, so the matrix sniff's
`finalUrl=... hosts=[...]` line shows up in /api/logs.

Also serve data/matrix-debug.png (the screenshot the extractor drops on
timeout) at /api/debug/matrix-debug.png so we can see what
chat.reddit.com looked like in the headed browser without shell access
to the Dokploy host.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:57:33 +01:00
jjbrunton 705d88dca1 Revert "Publish noVNC on host:16080 to bypass WS-stripping reverse proxy"
This reverts commit 65a8fcb163.
2026-05-22 21:32:12 +01:00
jjbruntonandClaude Opus 4.7 65a8fcb163 Publish noVNC on host:16080 to bypass WS-stripping reverse proxy
The upstream openresty proxy in front of Dokploy strips the
WebSocket Upgrade header, which kills noVNC. Publishing noVNC
directly on a host port (16080) avoids the proxy entirely.
Auth is still gated by VNC_PASSWORD. The dashboard continues
to go through Traefik on its existing domain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:30:14 +01:00
jjbruntonandClaude Opus 4.7 9091eed7d1 Use a named docker volume for /app/data on Dokploy
A ./data bind mount points at the deploy directory, which Dokploy
rebuilds on every redeploy — wiping the SQLite db, the persistent
browser session, and the cached Matrix credentials. That forced the
slow re-login + chat.reddit.com token sniff on every container start.

Switch to a named docker volume (reddit_bot_data) so the data
directory survives rebuilds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:19:22 +01:00
jjbruntonandClaude Opus 4.7 4a3b44bc87 Don't crash bootstrap on Matrix token capture failure
If chat.reddit.com sniffing fails, fall through with a placeholder
credential set so the post-monitoring/processor side of the bot
(and the web UI) stay up. Chat features are disabled until the
next successful token refresh.

Also dump diagnostics on sniff timeout: final page URL, every host
the page touched (with request counts + whether any carried an
Authorization header), and a full-page screenshot to data/matrix-debug.png.
This makes the three failure modes (page didn't load / loaded but
never hit Matrix / hit Matrix without auth) distinguishable from logs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:09:38 +01:00
jjbruntonandClaude Opus 4.7 665ff8b1f3 Clear stale X display lock before starting Xvfb
A bot crash leaves /tmp/.X99-lock behind; the next container start
hits "Server is already active for display 99" and the entrypoint
previously fell through with only a warning, so Chromium then died
with "Missing X server or $DISPLAY" in a tight crash loop. Remove
the stale lock up-front and fail hard if Xvfb still won't come up,
so the failure mode is "container won't start" rather than "bot
crashes forever".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:04:28 +01:00
jjbruntonandClaude Opus 4.7 1b6dfd67e3 Persist t2→username mappings in reddit_users table
Adds a durable mapping so the UI never has to show raw t2_xxx IDs or
MXIDs just because the in-memory cache is cold after a restart.
ChatService seeds from the table on construction and writes through
every resolution path; cached chats whose username is still t2_xxx
get upgraded from the DB on loadCache.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:34:06 +01:00
jjbruntonandClaude Opus 4.7 160172403b Stop publishing ports 3000/6080 to the host
On Dokploy, traffic is routed through Traefik via the configured
domain — publishing host ports caused a collision with another
service already bound to 3000 on the same host. Switch to `expose`
so the ports are reachable on the internal Docker network only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:29:37 +01:00
jjbruntonandClaude Opus 4.7 eb7bc7c045 Install x11-utils and wait reliably for Xvfb in entrypoint
The Xvfb readiness loop in entrypoint.sh used xdpyinfo, which isn't
in the Playwright base image. On busy hosts the loop fell through
before Xvfb was ready, so Chromium launched with no X server and
crashed with "Missing X server or \$DISPLAY".

Add x11-utils (provides xdpyinfo) and extend the readiness loop to
10 s with explicit logging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:01:32 +01:00
jjbruntonandClaude Opus 4.7 90d81b840f Pin DNS to 1.1.1.1/8.8.8.8 in docker-compose
Chromium in the container was failing with ERR_INTERNET_DISCONNECTED
when navigating to Reddit. Override container DNS to public resolvers
so the browser can reach reddit.com / chat.reddit.com.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:00:01 +01:00
jjbruntonandClaude Opus 4.7 9389c6258a Set DEBIAN_FRONTEND=noninteractive in Dockerfile
tzdata was prompting for a timezone interactively during apt-get install,
which hangs the Dokploy build. Pin TZ=Etc/UTC and skip the prompt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:51:40 +01:00
jjbruntonandClaude Opus 4.7 05c95db6bd Record sent chats in messages repo and blur gallery by default
Threads MessagesRepository through ChatService so first outgoing chat
messages are persisted for stats, and extends the repo + DB client to
support chat-room-keyed lookups. Adds tests covering the new behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:48:14 +01:00
jjbruntonandClaude Opus 4.7 2260388dc8 Blur gallery images by default, sync progress UI, and add chat/monitor tests
- Blur NSFW media gallery images until hover/reveal
- Surface chat sync progress in the dashboard header
- Extend chat service, cache, and monitor with delta-media handling
- Add tests for chat cooldown, delta media, and monitor flows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:01:09 +01:00
jjbrunton 633bb0ec04 Restructure chat service, web routes, and add test suite
Split monolithic chat service into modular structure with Matrix client
support, extract web server into per-resource route modules, introduce
bootstrap/main-loop separation, and add comprehensive vitest coverage
across services, repositories, and utilities. Adds project docs and
CLAUDE.md agent instructions.
2026-05-21 01:32:10 +01:00
jjbruntonandClaude Opus 4.6 dfa0580859 Update source code and remove runtime data from tracking
Add gitignore rules for browser session data, SQLite databases,
media downloads, and build artifacts. Remove previously tracked
runtime files from the repository.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 18:54:07 +01:00
jjbrunton e90213d1ff Update 2026-01-05 15:15:16 +00:00
jjbrunton 00934fecbb Update 2026-01-05 15:13:59 +00:00
jjbrunton 2a78f65b09 Update 2026-01-05 15:11:35 +00:00
jjbrunton cad1f7b842 Update 2026-01-05 15:06:58 +00:00
jjbrunton 9602403f8a Update 2026-01-05 15:04:16 +00:00
jjbrunton 58bb6b6dfc Update 2026-01-05 14:44:55 +00:00
jjbrunton 2cf474899c Update 2026-01-05 14:34:20 +00:00
jjbrunton 0158103f64 Update docker-compose.yml 2026-01-05 12:45:14 +00:00
jjbrunton 7b3ea37127 Update 2026-01-05 00:17:06 +00:00
jjbrunton b780bff850 Update 2026-01-04 20:26:41 +00:00
jjbrunton c460dd9715 Initial 2026-01-04 19:33:33 +00:00
jjbrunton 0ca98deaf5 Update 2026-01-04 19:30:51 +00:00
jjbrunton 24b197c2c2 Update 2026-01-04 19:26:50 +00:00
jjbrunton 0c5ffc5001 Update 2026-01-04 19:24:56 +00:00
jjbrunton c552956516 Initial 2026-01-04 19:16:31 +00:00