Wave 6. Swaps react-scripts' build/dev tooling for Next.js while
keeping the app's actual routing/rendering model unchanged -- the app
is almost entirely behind auth with no proven SSR/SEO need, so a real
App Router rewrite would touch ~90 files for zero user-visible benefit.
- next.config.js: output:'export' (static HTML+JS, same "single
index.html served by nginx with try_files fallback" deploy as CRA).
- app/layout.tsx + app/page.tsx: root shell ports public/index.html's
<head>, mounts the whole existing App tree client-only (ssr:false)
since it reads window/localStorage during initial render and Next's
static prerender would otherwise execute that on the server.
- Renamed src/pages/ -> src/views/ (Next's Pages Router auto-detects
any `pages/` dir under the app root and tried to build our React
Router page components as its own routes).
- REACT_APP_* -> NEXT_PUBLIC_* across code, .env.development,
Dockerfile, docker-compose.yml build args.
- Replaced the CRA SVGR import (`ReactComponent` from .svg, unsupported
under Turbopack) with a small inline JobbjaktMark component.
- TypeScript 4.9 -> 5.9 (MUI v8's type-checked build needs syntax
4.9's parser rejects; CRA never hit this because babel doesn't
type-check).
- Dropped CRA-only files (index.tsx, reportWebVitals, react-app-env.d.ts,
public/index.html); kept react-scripts as the Jest test runner only
(next/jest migration not needed -- the existing config already works).
Verified: `next build` static export succeeds, `next dev` serves the
landing page and client-side routes (login etc.) correctly, all 57
frontend tests + 172 backend tests still green.
Known caveat: deep-linking straight to a sub-route (e.g. /login) 404s
in `next dev` since there's no server route for it -- the app only
ever mounts at "/". Production is unaffected: nginx's existing
try_files fallback still serves index.html for any path.
b2 of the multi-provider email roadmap. Mirrors the Gmail provider's shape
end-to-end so the two stay structurally interchangeable:
- MicrosoftGraphConnection model + table (reconciler pattern, SQLite+MySQL,
same shape as GmailConnection: encrypted refresh/access token, sync state).
- MicrosoftGraphOAuthService: auth-code + offline-access flow against
login.microsoftonline.com, encrypted token storage via IDataProtector,
message search/thread/detail fetch against Microsoft Graph (conversationId
stands in for Gmail's threadId), attachment listing.
- MicrosoftGraphProvider implements IEmailProvider — no contract changes;
the existing seam was already provider-neutral.
- MicrosoftGraphController: connect-url/oauth/callback/status/disconnect,
mirrors GmailController's OAuth surface exactly (including the popup
postMessage handshake). Job-matching/review endpoints stay Gmail-only for
now, per the roadmap — generalising those needs the frontend provider
picker work, not this slice.
- Registered in DI + IEmailProviderRegistry (multi-registration of
IEmailProvider, resolved by ProviderKey).
- Config: Microsoft:ClientId/ClientSecret/TenantId/RedirectUri, wired through
docker-compose.yml + .env.example alongside the existing Google:Gmail* keys.
- Tests: MicrosoftGraphControllerTests (OAuth lifecycle) +
MicrosoftGraphProviderTests (DTO mapping onto the neutral contract).
147/147 green (135 existing + 12 new).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts from main's Wave 0 (PR #1) landing after this branch was cut:
- useViewResource.ts: main's e352aae already fixes the render loop the same way
(load in a ref, dropped from deps) — took main's canonical version. My
independent fix is superseded (my branch predated e352aae, which is why the
loop reproduced live).
- JobApplicationsController.cs: keep BOTH main's IJobCvMatchService and my
AnalyticsService (ctor gets both optional params). GetAnalyticsOverview stays
delegated to AnalyticsService.
- Fold main's H3 additions into the extracted AnalyticsService: pipeline-driven
funnel (JobPipeline.Normalize/Stages) + time-in-stage (StageAnalytics) and add
StageDurationDto + TimeInStage to Models/AnalyticsDtos.cs, preserving the API
contract the frontend expects.
Build clean; backend suite 135/135 green.
The structured /cv/* calls funnel through a provider router so production can
offload a weak local GPU (GTX 1060) to a cloud provider without any .NET change.
Default stays "ollama" (keyless/local) and /summarize remains local distilbart.
- AI_PROVIDER=ollama|gemini|groq dispatch inside _ollama_generate_json/_text
(entry-point names kept, so no call sites change; Ollama path is byte-identical).
- Gemini (x-goog-api-key header, not URL query) and Groq (OpenAI-compatible
chat/completions) added via stdlib urllib — zero new dependencies.
- /health reports ai_provider + ai_provider_configured.
- Keys read from env only; never logged/committed.
- Compose + .env.example pass AI_PROVIDER/GEMINI_*/GROQ_* through.
Tests: 11 passed (default Ollama unchanged, Gemini/Groq dispatch, missing-key 503,
health reports provider).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The compose file shipped its own ollama service, so 'docker compose pull'
during deploy re-downloaded the Ollama image and a deploy that starts the
AI stack would spin up a second Ollama alongside an existing/shared one.
- ollama service moved behind a 'bundled-ollama' compose profile, so it is
excluded from the default pull/up (no duplicate, no re-download)
- ai-service no longer depends_on ollama and is documented to point at a
shared instance via OLLAMA_BASE_URL (e.g. http://<host>:11435)
- deploy.sh no longer names ollama in 'compose up'
To run a self-contained Ollama: docker compose --profile bundled-ollama up
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CRA's build runs fork-ts-checker in a forked process whose IPC needs
more than Docker's default 64MB /dev/shm; too little segfaults
'npm run build' (RpcIpcMessagePortClosedError / SIGSEGV) with no compile
error. Set shm_size on the frontend image build so production deploys
don't hit this. (CI runners need the same via their container options.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>