From eee7ef60557e31b9fe255bab3a5717b0de434631 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Fri, 10 Jul 2026 21:56:33 +0200 Subject: [PATCH 1/4] chore(ops): restart:unless-stopped so the stack survives host reboots The Pi stack stayed down after a reboot (services had no restart policy). Add restart:unless-stopped to postgres/backup/api/frontend (not the init-keys one-shot). Also commits todo.md (running follow-ups: drafts fetch, live OAuth/subdomain, sent re-sync). Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 4 ++++ todo.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 todo.md diff --git a/docker-compose.yml b/docker-compose.yml index 4deff9b..669deb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: postgres: + restart: unless-stopped # pgvector-enabled Postgres 16 (semantic search). Drop-in for postgres:16 data; # the 'vector' extension is created by the AddEmbeddingColumn migration. image: pgvector/pgvector:pg16 @@ -27,6 +28,7 @@ services: # encrypted disk and be included in your off-machine backup regime (see SECURITY.md). # Restore: docker compose exec -T postgres psql -U inboxintel -d inboxintel < backups/.sql backup: + restart: unless-stopped image: pgvector/pgvector:pg16 entrypoint: /bin/sh command: @@ -64,6 +66,7 @@ services: - keys:/keys api: + restart: unless-stopped build: context: . dockerfile: src/InboxIntel.Api/Dockerfile @@ -98,6 +101,7 @@ services: - "127.0.0.1:8080:8080" frontend: + restart: unless-stopped build: context: ./frontend dockerfile: Dockerfile diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..70cd248 --- /dev/null +++ b/todo.md @@ -0,0 +1,54 @@ +# InboxIntel — TODO / follow-ups + +Outstanding items after the 2026-07-04 work session (UI refactor, category fixes, login fix, +Pi deployment, mockups). Nothing here is blocking; these are the known loose ends. + +## Sync — Sent / Drafts folders +The PHASE 2 category fix (PR #38) is correct — after a re-sync, **Spam and Trash now populate** +(Trash 1363, Spam 112 on staging). But: + +- [ ] **Uncapped re-sync to confirm Sent.** Sent is still empty on staging only because of the + 2000-message dev cap (`GmailSync:MaxMessages=2000`) + Gmail's newest-first ordering — the + newest 2000 are dominated by trash/promotions, so Sent/Draft/most Inbox mail falls outside + the window. An uncapped sync (`MAX_MESSAGES=0`, real deployment) should include Sent. + To re-run: `DELETE FROM sync_states` for the user, then reload the dashboard (auto-fires a + full sync). Note: the capped re-sync also left staging's Inbox sparse (~5) — staging-only. + +- [ ] **Drafts fix.** User added a drafts scope in Google Cloud console. Code side: `gmail.readonly` + already grants draft *read*, but `messages.list` doesn't return drafts — add a `users.drafts.list` + fetch in `GmailService`/`SyncService` and link the DRAFT label. (Add the new scope to + `GoogleOAuth:Scopes` in appsettings if compose/modify is needed for drafts too.) + +- [ ] Optional: with `IncludeSpamTrash=true`, a *capped* sync gets flooded by trash. If caps are + kept, consider prioritising non-trash or making the cap per-folder. + +## Pi deployment — real login +InboxIntel is live on the homelab at http://192.168.50.13:8090 (auto-deploys from `develop`). + +- [x] Live message cap removed — `MAX_MESSAGES=0` in the Pi `deploy/.env` (takes effect on next sync). +- [ ] **Google login on the Pi.** Subdomain: **inboxintel.cesnimda.uk** (user wiring traefik+TLS manually). + Redirect URI (in the new live Google OAuth client): `https://inboxintel.cesnimda.uk/signin-google`. + Once traefik is up: set `FRONTEND_ORIGIN=https://inboxintel.cesnimda.uk` in the Pi `deploy/.env`, + set `AllowedHosts`, and put the **new live** `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` in the Pi + `deploy/.env` (see below), then redeploy. +- [ ] **Where the live OAuth ID/secret go: the Pi `/opt/inboxintel/deploy/.env`** (git-ignored) — NOT + Gitea secrets. The Pi auto-deploys via a cron `git pull` + `docker compose`, which reads + `deploy/.env` directly; there's no Gitea Actions runner on the Pi, so Gitea secrets wouldn't be + injected. (Gitea secrets only matter for the staging CD runner.) + +## Release / housekeeping +- [ ] Decide on promoting `develop` → `main` and cutting a release tag (e.g. v0.1.0). `main` is + stale; all current work lives on `develop`. Stale PR #6 (develop→main) can be closed/redone. +- [ ] Deferred by user: "Clean up my inbox" natural-language chat feature (do last). +- [ ] Accessibility pass before launch (green accent must not be colour-only) — noted as nice-to-have. + +## Done this session (for reference) +- Login 500 fixed (root-owned DataProtection keys volume) — PR #37 +- All 7 smart-category bugs fixed at source — PR #38 +- Email dashboard UX: split-view, custom checkbox, row polish, bulk/keyboard, dashboard + heatmap→category-bar, skeletons/empty states — PR #44 +- Dead backend heatmap removed — PR #45 +- .NET 10 migration, Renovate, DB backups, OpenTelemetry, hybrid semantic search, keyset + pagination, feature-flags foundation, Semgrep SAST (earlier PRs) +- Resume mockups → F:\Pictures\website\inboxintel +- Live Docker deployment + cron auto-deploy on pi@192.168.50.13 -- 2.52.0 From c9f1437860df0ae73b3ee4a8c5c9fb02d29dfc2a Mon Sep 17 00:00:00 2001 From: cesnimda Date: Fri, 10 Jul 2026 22:03:23 +0200 Subject: [PATCH 2/4] fix(proxy): honor upstream X-Forwarded-Proto so OAuth redirect is https behind traefik The SPA nginx overwrote the reverse proxy's X-Forwarded-Proto with $scheme (http on the traefik->frontend hop), so the API built http:// OAuth redirects that Google rejects. Map the incoming proto through (fallback to $scheme for direct access). Needed for login via https://inboxintel.cesnimda.uk behind traefik. Co-Authored-By: Claude Opus 4.8 --- frontend/nginx.conf | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 938d8d0..082f5a3 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,3 +1,11 @@ +# Honor a reverse proxy's X-Forwarded-Proto (e.g. traefik terminating TLS) so the API sees +# https and builds https OAuth redirects; fall back to the connection scheme for direct access. +map $http_x_forwarded_proto $fwd_proto { + default $scheme; + https https; + http http; +} + server { listen 80; server_name _; @@ -27,7 +35,7 @@ server { proxy_pass http://api:8080; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $fwd_proto; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header Cookie $http_cookie; } @@ -38,7 +46,7 @@ server { proxy_pass http://api:8080; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $fwd_proto; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header Cookie $http_cookie; } -- 2.52.0 From cb740c051533d18b4c6782db9e12810597a8f098 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 12 Jul 2026 15:20:10 +0200 Subject: [PATCH 3/4] fix(dashboard): grid fills available width instead of fixed 1200px Wrap GridLayout with WidthProvider (already ships with react-grid-layout, no new dep) so tiles reflow to actual container width on resize/sidebar-collapse instead of leaving dead space or overflowing on a hardcoded 1200px canvas. compactType=vertical already auto-packs the bento-style variable-size tiles. Co-Authored-By: Claude Sonnet 5 --- frontend/src/pages/Dashboard.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 057f020..8114f48 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import GridLayout from 'react-grid-layout'; +import GridLayoutBase, { WidthProvider } from 'react-grid-layout'; +const GridLayout = WidthProvider(GridLayoutBase); import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import { AnalyticsApi, LayoutApi, ExportApi, SyncApi } from '../api/client.js'; @@ -137,7 +138,6 @@ export default function Dashboard() { layout={visibleLayout} cols={12} rowHeight={60} - width={1200} isResizable isDraggable resizeHandles={['se']} -- 2.52.0 From be715e3cea7162cdc8644c49a0546bc1c0dd78b7 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 12 Jul 2026 15:53:35 +0200 Subject: [PATCH 4/4] fix(ci): semgrep install fails on Gitea Act runner (no sudo/pipx) Runner has no sudo binary but already runs as root, and no pipx. Call apt-get directly and pip install --user semgrep instead. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/security.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml index cf69641..a86c0f2 100644 --- a/.gitea/workflows/security.yml +++ b/.gitea/workflows/security.yml @@ -62,10 +62,12 @@ jobs: - uses: actions/checkout@v4 # The runner image lacks pip, and a semgrep job-container lacks the node that # actions/checkout needs — so install pip via apt on the standard image. + # Gitea's Act runner has no sudo binary but already runs as root, so call + # apt-get directly; pip install --user needs no pipx. - name: Install semgrep run: | - sudo apt-get update -qq && sudo apt-get install -y -qq python3-pip pipx - pipx install semgrep + apt-get update -qq && apt-get install -y -qq python3-pip + python3 -m pip install --user --break-system-packages semgrep || python3 -m pip install --user semgrep - name: Semgrep scan run: | export PATH="$HOME/.local/bin:$PATH" -- 2.52.0