Implementation History¶
A chronological log of completed development sessions and the work shipped in each.
2026-07-03 — Certification Sync (Credly, Databricks, MS Learn)¶
PRs: #606 Branch: feat/certification-sync
Certification Sync Service (PR #606)¶
Added automated syncing of earned professional certifications from three sources into the portfolio database. Migration 0011 adds seven new columns to the certifications table: source, external_id, badge_image_url, issued_at, expires_at, last_synced_at, and sync_enabled (with a unique constraint on (source, external_id)). The sync_enabled=False flag lets specific rows opt out of overwriting (used for the three manual fallback entries).
Credly is fetched via the public badges.json API with no auth. Databricks uses Playwright headless Chromium to load the Accredible wallet page and intercept the api.accredible.com/v1/.../user_wallet API response — direct HTTP calls fail because the API uses client-side request signing (x-timestamp + x-signature headers computed by the browser's JS). Playwright is installed to /opt/playwright-browsers in the Docker image (world-readable so appuser can access it). Microsoft Learn has no public API for earned credentials; the infrastructure is wired (refresh token + client ID via Key Vault → env vars in both backend CA and cert-sync job) but activation requires a one-time device-code OAuth flow documented in scripts/get_mslearn_token.py and docs/planning/mslearn-org-reporting.md.
An Azure Container Apps scheduled job (Microsoft.App/jobs@2023-05-01) runs the sync every Monday 06:00 UTC, reusing the backend image with entrypoint python -m api.jobs.sync_certifications. A POST /api/admin/sync-certifications endpoint (10/hour rate limit) allows on-demand triggering. Sort order: manual entries first (by sort_order), then synced by source group (credly → microsoft → databricks), each group sorted by issued_at DESC. The frontend certifications-section.tsx was updated to show badge images (with SVG icon fallback), issued/expiry dates, and dims expired cards at opacity-50.
Key fixes during development: (1) the ACA job entrypoint must call db_engine.init_engine() itself since api.main is not imported; (2) ORM datetime objects are not directly assignable to Optional[str] Pydantic fields — a field_validator coerces them to ISO strings; (3) shared/api.types.ts must be prettier-formatted after types:generate to match CI output.
2026-06-29 — Stable Release v0.21.0 + Release Process Documentation¶
PRs: #585 Branch: fix/release-main-merge → release
Stable Release v0.21.0 (PR #585)¶
Promoted v0.21.0-alpha to stable by merging main into release. The merge conflicted in three files (.release-please-manifest.json, package.json, CHANGELOG.md) — all resolved by taking main's version string. Release-please was triggered manually via workflow_dispatch on the release branch but produced an incorrect v0.22.0-alpha PR (because the manifest held 0.21.0-alpha, so it computed a new alpha rather than stripping the suffix). That PR was closed; the manifest was reset to 0.20.0 (the previous stable) and re-triggered, but release-please still found no conventional commits on the release branch (merge commits are chore: and ignored). The stable tag v0.21.0 was therefore created manually with git tag -a, pushed to origin, and a GitHub release was created via gh release create. The manifest was then updated to 0.21.0 and pushed so future release-please runs compute correctly.
The root cause is structural: release-please requires feat:/fix: conventional commits on the target branch. When main is merged into release as a single squash/merge commit (chore:), release-please sees no version-bumping work and does nothing. This means stable releases will always require the manual 6-step process; release-please is effectively unused on the release branch.
Release Workflow Documentation¶
Rewrote docs/planning/release-workflow.md to accurately reflect the 6-step manual stable release process. Key additions: explanation of why release-please cannot auto-tag on the release branch, the conflict resolution table for the three always-conflicting files, and a warning not to trigger release-please via workflow_dispatch on release (it will either do nothing or propose the wrong version). A feedback memory was also written to the Claude memory system.
2026-06-11–12 — Content & Presentations Section (v0.21.0-alpha)¶
PRs: #556, #559, #560, #562 Branch: feat/content-section
CI/CD Fixes: Docs Submodule PAT + Experience Title (PRs #556, #559)¶
Two CI fixes landed before the content work. PR #556 fixed the docs submodule PAT authentication in deploy-infrastructure.yml (was using a token without submodule scope) and migrated the W+B role title in seed data via Alembic migration 0010. PR #559 scoped the PAT more tightly in the frontend workflow and fixed an experience section title rendering issue in the frontend.
Content & Presentations Section (PR #560)¶
Added a full Content & Presentations section to the portfolio. A new Strapi content-post collection type was created with fields: title, slug, content_type (article|presentation), excerpt, cover_image (media), publish_date, tags (JSON), external_url, attachment (media), source, featured, body (Markdown text), and embeds (JSON array of {marker, url, caption}). Two OneDNA articles were seeded on Strapi bootstrap: a DataOps article (Dutch) and a Databricks article (Dutch), both with full Markdown bodies.
On the frontend: useContentPosts fetches from /cms/api/content-posts with 5-minute cache, placeholderData fallback to two static entries, and retry: 0. useContentPost(slug) fetches a single post by slug with the same fallback strategy. The /content landing page shows a spotlight card (first featured post) plus a compact grid with tilt/glow/reveal animations. The /content/:slug detail page renders Markdown body via ReactMarkdown + remark-gfm, displays cover hero image, tags, and a PDF attachment download block. Articles navigate to /content/:slug; presentations open a ContentModal (YouTube/Vimeo embed or PDF iframe). A ContentPreviewSection home teaser was added. Navigation gained a Content link in both desktop pill nav and mobile nav.
Blog Images, Webinar Seed, Interactive Embeds (PR #562)¶
Expanded the content section with richer media. Eight embedded images were added to seed-images and wired into article bodies via SEED_IMG_ placeholders that the bootstrap script resolves to Strapi media IDs at runtime. A dbt lunchsessie webinar was seeded as a presentation content-post with a PDF attachment (webinar-dbt-lunchsessie-2023-08-24.pdf). Three interactive embeds were added to the DataOps article via EMBED_n markers in the body — rendered as 16:9 aspect-ratio iframes that break out of the max-w-3xl container to full viewport width using 100vw + calc(50% - 50vw) margin.
A critical nginx fix was included: the /cms/uploads/ location was added with ^~ prefix (higher priority than the regex static-file location block) so .jpg/.png Strapi media served via the /cms/ proxy are not intercepted by nginx's static file regex. useContentPosts was also switched from initialData to placeholderData so the CMS fetch always fires (initialData marks data as fresh, suppressing the network request).
2026-06-02 — Strapi CMS Dev Environment: Auth Fixes, Plugins, Custom Domain¶
PRs: #507, #508, #509, #510, #511, #512, #514, #515, #516 Branch: various fix/ and feat/ branches
CMS Postgres Managed Identity Auth Fix Chain (PRs #507–#510)¶
A chain of four bugs prevented the CMS from authenticating to Postgres using its Managed Identity. #507 fixed the psql schema init step to use an AAD token instead of password auth. #508 rewrote the IMDS token fetch in cms/config/database.js from fetch() (unavailable in Node 18) to Node's built-in http.request(). #509 switched the Postgres username from the MI client ID UUID to the MI principal name (dna-dev-portfolio-we-id-cms), which is what Postgres Entra auth requires, and added a GRANT role step to the schema init. #510 registered the CMS MI as a Postgres Entra admin in the deploy-infrastructure.yml post-deploy step — without this registration, Entra auth for the CMS was never enabled on the server.
E2E UIGen URL Fix (PR #511)¶
The build-and-deploy job in deploy-application.yml was missing the uigen_url output declaration, causing the post-deploy E2E job to try connecting to localhost:4400 instead of the deployed UIGen URL. Fixed by adding the output declaration so the E2E job receives the real URL and skips UIGen-specific tests when the URL is not a localhost address.
CMS Custom Domain — Stages 2 and 3 (PRs #512, #516)¶
Stage 2 (PR #512): updated infra/parameters.dev.bicepparam to set cms_custom_domain='cms.dev.sven-relijveld.com', which registers the hostname on the Container App Environment (bindingType: Disabled). Stage 3 (PR #516): set deploy_cms_custom_domain=true, triggering Azure to issue a managed TLS certificate and enable HTTPS binding (bindingType: SniEnabled). The CMS is now reachable at https://cms.dev.sven-relijveld.com.
Dockerfile.strapi Build Fixes (PRs #514, #515)¶
PR #514 added --legacy-peer-deps to the npm ci command in Dockerfile.strapi because strapi-plugin-duplicate-button@2.0.0 pins react-router-dom@6.22.3 as a peer dep but the project uses 6.30.x. This resolved the ERESOLVE build failure but exposed a second issue: with --legacy-peer-deps, npm does not hoist esbuild to the top-level node_modules, causing esbuild-register (used by Strapi's build step) to fail with "Cannot find module 'esbuild'". PR #515 added esbuild@^0.28.0 as an explicit dependency to ensure it is always available. Both fixes were verified locally with docker build before pushing.
Strapi Plugins (commit 15628df)¶
Seven Strapi plugins installed and configured in cms/config/plugins.js: @strapi/plugin-documentation (Swagger UI), @strapi/plugin-color-picker (color field type), @strapi/plugin-graphql (GraphQL endpoint), strapi-plugin-config-sync (git-tracked config exports), strapi-plugin-preview-button (frontend preview from editor), strapi-plugin-schema-visualizer (visual ER diagram), strapi-plugin-duplicate-button (duplicate entries). config/admin.js updated with watchIgnoreFiles for config-sync.
2026-05-22–30 — UIGen v0.16.0, Release Channels, Auth Split, Dependabot Bulk¶
PRs: #463, #469, #471, #472, #474, #475, #459, #465, #466, #468, #478, #479, #480 Branch: various fix/ and chore/ branches
UIGen v0.16.0 Upgrade + Create-Projects Auth Guard (PR #474)¶
Upgraded UIGen from v0.15.x to v0.16.0 in Dockerfile.uigen and .uigen/config.yaml. v0.16.0 introduced a form rendering regression where class-component overrides (e.g. custom error boundaries) fail silently because React is not defined in the override bundle scope. Worked around this by replacing the class-component error boundary with a functional auth-gate component (create-projects-guard.tsx) that proactively checks authentication state before rendering the create-projects form. The known issue is documented in docs/infrastructure/known-issues.md.
Fix: Split enable_auth into enable_frontend_auth and enable_uigen_auth (PR #469)¶
The single enable_auth Bicep parameter was split into two independent flags: enable_frontend_auth (controls EasyAuth on the frontend Container App) and enable_uigen_auth (controls EasyAuth and Key Vault secret injection on the UIGen Container App). This allows UIGen auth to be enabled on prod independently of the frontend EasyAuth gate (which remains off on prod). Changes in infra/main.bicep, infra/modules/container_apps.bicep, and both .bicepparam files. PR #463 preceded this by adding the correct prod param value so the UIGen shim received its Entra client secret.
Fix: Environment Context on Validate Job for OIDC (PR #471)¶
The validate job in deploy-infrastructure.yml was missing environment: context, causing OIDC token requests to fail when the job ran in isolation (e.g. workflow_dispatch). Added environment: ${{ needs.determine-environment.outputs.environment }} to the validate job so it requests a scoped OIDC token matching the deployment environment.
Feat: Alpha and Stable Release Channels (PR #472)¶
Configured release-please for two release tracks: main branch publishes alpha prereleases (e.g. v0.19.0-alpha), release branch publishes stable releases (e.g. v1.0.0). Updated release-please-config.json with branches array defining channel: alpha on main and channel: latest on release. This enables a formal stable release track without disrupting the current rapid-iteration alpha workflow.
CMS Plan Revision — Strapi v5 Only (PR #475)¶
Revised the content management architecture plan from a 27-hour dual-backend (FastAPI + Strapi + bidirectional sync) to a 23-hour Strapi-v5-only implementation. Dropped the sync job entirely; Strapi becomes the single CMS with its own Azure Container App, managed identity auth to shared Postgres, and native Draft & Publish for staging. Plan documented at docs/planning/plan-content-management.md.
Dependabot Bulk + Gitignore Cleanup (#459, #465, #466, #468, #478, #479, #480)¶
Sequential Dependabot merges: idna 3.15 (security), qs 6.15.2, ws 8.21.0, pip all-patch group (8 packages), npm minor-patch group (27 packages), axios 1.16.1. Also added *.local.md to .gitignore and untracked ERRORS.local.md (#480).
2026-05-13–19 — UIGen Auth Loop, Hero/Nav Redesign, Experience Content Update¶
PRs: #437, #438, #439, #443–#451, #452, #454, #455, #456 Branch: fix/content_experience (content); various hotfix branches
UIGen EasyAuth + Auth Router Fixes (PRs #438, #439)¶
UIGen's EasyAuth block was intercepting /auth/token and /auth/callback paths, preventing the OAuth PKCE flow from completing. Fixed in infra/modules/container_apps.bicep by adding those paths to the EasyAuth exclusion list. Separately, the auth router in api/main.py was mounted at /api/auth but UIGen's shim proxy forwarded to /auth/me (no /api prefix), causing 404s. Fixed by also registering auth_router at the root path so both /auth/me and /api/auth/me resolve.
UIGen OAuth Login Loop Fix Series (PRs #443–#451)¶
A chain of six bugs caused an infinite OAuth login loop in UIGen's admin portal. Fixed in sequence: (1) landing page added to resolve /dashboard routing error (#443); (2) sessionValidationEndpoint path corrected to avoid double /api prefix (#444); (3) Bearer token from session cookie injected on /api/auth/me (#446); (4) userInfoUrl set to /api/auth/me to avoid Microsoft Graph 401 (#447); (5) userInfoUrl changed to absolute HTTPS URL (#448); (6) PyJWT validation updated + sessionStorage bearer injection added so UIGen's client-side auth check sees the token (#451). Also fixed Entra app registration: requestedAccessTokenVersion=2 to emit v2.0 tokens (#449) and moved localhost redirect to SPA platform for PKCE local dev (#452).
Hero + Nav Redesign (PR #454)¶
Added type-in treatment to the hero tagline (cycles through role names with cursor animation), a sliding pill indicator on the navigation bar, and a new SkillBar component (client/src/components/ui/skill-bar.tsx) for visualising skill proficiency. Navigation gained OneDNA logo variants (colour and white). Role cycler is wired behind the feature flags context. All new components have Storybook stories and Vitest tests.
UIGen Feature Flags Override Panel (PR #455)¶
Added a feature-flags override UI inside the UIGen shim (/.uigen/src/feature-flags.tsx). The panel reads current flag state from /api/feature-flags, lets the admin toggle flags, and writes changes via PATCH /api/feature-flags. Fixed proxy routing so the override panel's API calls reach the backend correctly. Docker-compose updated with FEATURE_* env vars wired to the backend service. Also fixed TTL cache busting after writes and granted App Configuration Data Owner (not just Reader) to the backend MI so writes succeed.
Experience Content Update (PR #456)¶
Added Cloud Architect & DevOps Engineer role at Witteveen+Bos (November 2025–present) as a new top-level work entry. Marked the Databricks Data & AI role as completed (December 2025). Split the Albron engagement into two: HAI (March 2026–present, mentoring focus, part-time) and Albron (January–April 2025, paused). Changes applied across api/data/seed.json, client/src/data/experience.ts, Alembic migrations 0008 and 0009, and both English and Dutch CV content files. Dutch CV template (cv/templates/dutch.md.j2) also updated with a comprehensive translation dictionary for month names and period labels.
2026-05-05 — Arjan Refactor Phases 2–6¶
PRs: #375, #377, #379, #380, #381
Branch series: refactor/api-phase2 → refactor/api-phase3 → refactor/api-phase4 → refactor/api-phase5 → refactor/api-phase6
Completion of the six-phase ArjanCodes-inspired backend refactor. Each phase was an independently shippable PR with no behavior change until Phase 4.
Phase 2 — Pydantic v2 Response Schemas (PR #375)¶
Added api/schemas/responses.py with 11 Pydantic v2 response schemas using
model_config = ConfigDict(from_attributes=True): ProjectCard, ProjectDetail, ProjectPhase,
WorkEntry, Skill, SkillCategory, Award, Certification, ContactResponse,
HealthResponse, FeatureFlagsResponse. All _x_to_y hand-converter functions were deleted.
ORM rows now flow into response schemas via Schema.model_validate(orm_obj) — no manual field
copying.
Phase 3 — Per-Domain Routers + Domain Exceptions (PR #377)¶
Split main.py route handlers into 9 per-domain router files under api/routers/: health,
projects, experience, skills, awards, certifications, contact, cv, feature_flags. Added a domain
exception hierarchy in api/core/exceptions.py (PortfolioError, NotFoundError,
ConflictError, ServiceUnavailableError) plus register_exception_handlers() which maps domain
exceptions to HTTP status codes at the edge — handlers focus on the happy path only. Added a
shared Limiter instance in api/core/limiter.py (slowapi, in-memory, single-replica). main.py
shrunk to a ~150-line create_app() factory.
Phase 4 — Repository + Service Layers (PR #379)¶
Added 5 repository modules in api/repositories/ (projects, experience, skills, awards,
certifications). Each accepts db: AsyncSession | None and falls back to seed.load_*() when
None, eliminating the duplicated if AsyncSessionLocal is None branches from every handler.
Added EmailService (Jinja2 + SMTP) in api/services/email.py and generate_cv() using
asyncio.create_subprocess_exec (non-blocking, replacing blocking subprocess.run) in
api/services/cv.py. Added get_optional_db() to api/db/engine.py.
Phase 5 — Seed JSON + ProjectPhase Table (PR #380)¶
Created api/data/seed.json as the single source of truth for all static content (projects,
work history, skills, awards, certifications). api/data/seed.py provides an lru_cache loader
(load_projects(), load_work_entries(), etc.) used by repository fallback paths. Added
ProjectPhase ORM model with FK to projects, ordinal, name, and description. Migration
0005_project_phases_table.py replaces the timeline_encoded ARRAY(Text) hack with a proper
child table, migrating existing data by parsing the "name|||description" encoding. Projects
repository uses selectinload(Project.phases) to avoid N+1 queries.
Phase 6 — Structured Logging + Request IDs + DB Token Store (PR #381)¶
Added loguru structured logging via configure_logging() in api/core/logging.py. An
InterceptHandler captures stdlib, uvicorn, and SQLAlchemy logs into loguru; JSON sink in
production, pretty sink in development. Added RequestIdMiddleware in api/core/middleware.py:
reads or generates X-Request-Id per request, binds to a ContextVar, echoes in the response
header, and loguru pulls it into every log line. Added a DB-backed UIGen token store:
uigen_tokens table, UigenToken ORM model, and api/services/uigen_tokens.py with issue(),
validate(), revoke() using SHA-256 hashing. Migration 0006_uigen_tokens_table.py creates the
table. Falls back to the in-process dict when the DB is unavailable (preserving local dev
behavior). Documented the slowapi single-replica limitation in the architecture docs.
2026-04-26 — Postgres MI Auth Bug Chain, UIGen Fixes, Dependabot¶
PRs: #259, #261, #265–#267, #269–#274, #276–#277, #299–#300, #304
Branch series: hotfix chain from main
Postgres Managed Identity Auth Bug Chain (PRs #265–#274)¶
A chain of five bugs prevented the backend from connecting to PostgreSQL in Azure. Fixed in sequence:
- PR #265 — IMDS retry:
_get_token()retried up to 5× with exponential backoff (1s/2s/4s/8s) and bumped per-attempt timeout to 10s, since IMDS at169.254.169.254is unreachable during Container App cold start. - PR #267 — Alembic CMD retry: the
Dockerfile.backendCMD now retriesalembic upgrade headup to 3× with a sleep, since it runs before uvicorn and hits the same IMDS cold-start window. - PR #269 — azure-identity: replaced the raw
urllibIMDS call entirely withManagedIdentityCredentialfromazure-identity(already used infeature_flags.py). Raw urllib never worked reliably on ACA. - PR #270 — MI principal name: Bicep was writing the Postgres
DATABASE_URLwith the MI's client ID UUID as the username. PostgreSQL AAD auth requires the MI's principal name (e.g.dna-prd-portfolio-we-id-be), not the UUID. Fixed ininfra/modules/container_apps.bicep. - PR #273 + #274 — Workflow overwrite fix:
deploy-infrastructure.ymlwas overwriting the corrected Key Vault secret with the old UUID-based URL after Bicep ran. Also the env short name wasprodinstead ofprdin resource lookups, silently skipping the AAD admin registration step entirely.
UIGen CI Cache + 0.5.0 Bump (PR #261)¶
ARG CACHE_BUST in Dockerfile.uigen was never used in a RUN layer, so Docker never invalidated the cache. Added RUN echo "Cache bust: ${CACHE_BUST}" to force cache invalidation. Bumped UIGen packages from 0.4.0 → 0.5.0.
UIGen Theme + IMDS Combined Hotfix (PR #265)¶
Also fixed the UIGen theming bug in prod: Dockerfile.uigen was only copying config.yaml, not theme.css. UIGen loads .uigen/theme.css from {specDir}/.uigen/ — without a volume mount in ACA the file was absent. Now copied at build time and seeded on startup.
Smoke Test HTTP Code Fix (PR #266)¶
The smoke test was checking / on the frontend (which redirects via EasyAuth) and / on the backend (which returns 404 by design). Fixed: frontend now accepts 401, backend now tests /api/health.
Feature Flags Retry After EasyAuth (PR #277)¶
On dev, EasyAuth intercepts /api/feature-flags before the user authenticates. The fetch was failing silently, leaving cvGeneration: false permanently. Fixed by retrying the fetch up to 5× with 2s backoff in FeatureFlagsContext — once EasyAuth sets the session cookie the retry succeeds.
Revision Health Check Schedule (PR #276)¶
Reduced health check cron from */15 * * * * to twice-daily (0 6,18 * * *). Per-deploy health alerting will be tracked separately as deploy-failure-alerting.
API Annotations for UIGen (PR #299)¶
Added x-uigen-view OpenAPI extension hints to public endpoints so UIGen renders appropriate widgets (action buttons, cards, forms) instead of plain API rows. Fixed UIGen not updating in PR previews (was always deploying :latest instead of SHA-tagged image). Fixed all issues from post-commit code review.
App Config RBAC Fix (PR #300)¶
main.bicep has an app_config_rbac module that assigns App Configuration Data Reader to the backend MI, but it was guarded by !empty(app_config_resource_id). This param was missing from parameters.dev.bicepparam and parameters.prod.bicepparam, so the module never ran and the MI had no RBAC on the App Configuration store — causing feature flag reads to fail silently.
zod v3→v4 + ecdsa Security Fix (PR #304)¶
Bumped zod 3.24.2 → 4.3.6 (major). Usage is limited to createInsertSchema + z.infer via drizzle-zod (supports both v3 and v4). Also bumped zod-validation-error 3→5 for peer dep compatibility. Pinned ecdsa>=0.19.2 in pyproject.toml to resolve a medium-severity DoS CVE — ecdsa is a transitive dep via sendgrid with no upstream version constraint.
Docs: Foldable Endpoint Callouts + ADR-007 (PR #259)¶
Converted all endpoint sections in docs/api/endpoints.md to foldable ??? example callouts (collapsed by default). Added ADR-007-feature-flags-app-config.md to the ADR nav. Committed UIGen shim proxy and OneDNA pink theme to the repo.
Bulk Dependabot Updates (PRs #278–#294, #301, #303)¶
Routine patch/minor dependency updates: actions/setup-python 5→6, azure/login 2→3, docker/build-push-action 6→7, docker/setup-buildx-action 3→4, fastapi, pydantic-settings, psycopg2-binary, azure-appconfiguration, @vitest/coverage-v8, @typescript-eslint/parser, eslint-plugin-react-hooks, express, @types/express, @radix-ui/react-separator, plotly, mkdocs-same-dir, pytest-asyncio, uv.
2026-04-21 — v0.5.0-alpha: App Config, UIGen, CI Robustness¶
PRs: #243, #247, #253, #254, #255, #257
Branch series: feat/azure-app-config → feat/app-config-feature-flags → hotfixes → fix/llamapr-ci-robustness
Azure App Configuration Feature Flags (PRs #243, #247)¶
Introduced Azure App Configuration as the feature flag store. A shared-RG store (dna-portfolio-we-appcs, Free tier) was added via infra/modules/app_configuration.bicep. The cv-generation flag has per-environment labels (dev=on, prod=off). The backend reads flags via managed identity (App Configuration Data Reader RBAC) with a 30-second TTL cache in api/feature_flags.py, falling back to the FEATURE_CV_GENERATION env var locally. Endpoints: GET /api/feature-flags (public) and GET /api/cv/status.
UIGen custom domain infra was also shipped in #247: admin.sven-relijveld.com (prod) and admin.dev.sven-relijveld.com (dev) with managed certificates. DNS TXT/CNAME records added to infra/modules/dns_zone.bicep.
Backend Crash Loop + UIGen Guest Routing Fix (PR #253)¶
Fixed two production issues: (1) Alembic env.py was using a bare create_async_engine instead of the IMDS-aware _make_engine(), causing the migration step to fail on startup in Azure. Fixed by threading _make_engine() through env.py. (2) UIGen guest routing was broken — unauthenticated users hitting the admin domain were not being redirected correctly. Fixed the Node.js auth shim routing logic.
IMDS client_id Fix (PR #254)¶
User-assigned managed identities require &client_id=<objectId> on the IMDS token request (169.254.169.254/...). Without it, the IMDS endpoint returns a 400. The MANAGED_IDENTITY_CLIENT_ID env var (injected by Bicep from backend_identity.properties.clientId) is now appended to the token request URL in api/db/engine.py.
Prod Always Full Redeploy — Artifact-Based Env Detection (PR #255)¶
The workflow_run trigger in deploy-application.yml and deploy-docs.yml previously used github.ref to detect whether to deploy to prod, which was always refs/heads/main for manual dispatches. Both workflows now query the GitHub API for the triggering run's uploaded artifact name: if it matches deployment-prod-*, deploy to prod; otherwise dev. This ensures manual workflow_dispatch infra runs to prod correctly propagate through the chain.
Docs Update Post v0.5.0-alpha (PR #257)¶
Updated backend.md, frontend.md, pipeline-overview.md, and resource-overview.md to reflect all v0.5.0-alpha changes: UIGen token endpoint, feature flags, App Config backing, IMDS client_id fix, artifact-based env detection.
2026-04-19 — Promotion Model + Local Stack Tooling¶
PRs: #194, #195, #237, #242
Branch: feat/main-deploys-to-dev
Fix CV Download in Dev Stack (PR #194)¶
Fixed CV download endpoint failing in the local docker-compose stack. Added test coverage for the download dialog component and improved coverage reporting setup.
Local Stack Tooling: Docs Service + Coverage + act (PR #195)¶
Extended docker-compose to run the MkDocs docs service locally (portfolio-docs, port 8080). Added VITE_DOCS_URL=http://localhost:8080 so the "Docs" nav link points to the local instance. Added npm run test:coverage and npm run coverage:open scripts. Documented how to simulate CI jobs locally using act (nektos), including known limitations on Windows ARM64 (esbuild crashes under QEMU emulation).
CI EasyAuth Fixes (PRs #237, #242)¶
Added missing EasyAuth env vars to the post-deploy Playwright job (#237). Fixed the deploy-application.yml post-deploy E2E to skip when frontend_url is empty (#242), preventing spurious failures when the deploy step was skipped.
2026-04-15 — CV Polish, Cost Dashboard, Cline Skills¶
PRs: #189, #190, #191
Cline Kanban Sync Skills (PR #189)¶
Added two Claude Code skills: sync-clineboard-to-memory (reads board.json backlog/in_progress columns → updates MEMORY.md "Next Up") and sync-memory-to-clineboard (reads MEMORY.md completions → moves cards to trash in board.json + updates roadmap.md + wip.md).
Weekly Azure Cost Report (PR #190)¶
Added cost-report.yml GitHub Actions workflow (scheduled weekly, Monday 07:00 UTC) that queries Azure Cost Management for both dev and prod resource groups, renders a Markdown cost table, and commits it to docs/infrastructure/cost-dashboard.md with [skip ci]. The Cost Management Reader role is assigned at subscription scope via Bicep in infra/main.bicep.
CV Template Polish (PR #191)¶
Completed the CV template redesign: dark header band (68mm), circular photo blob, two-column cover page (cover_left/cover_right HTML columns), dot-rating matrix for skill proficiency, skills grid layout, pink spacer bar, dark footer, and typography refinements. Cover page toggled by frontpage: true in frontmatter.
2026-04-13 — PostgreSQL Phase 3, CV Generation Pipeline, CI Consolidation¶
PRs: #183, #184, #186, #187
CV Generation Pipeline (PR #183)¶
Implemented a publishpipe-based CV build pipeline using Bun + pagedjs-cli. cv/build.ts wraps the render() API with --no-sandbox for CI environments. Custom Nunjucks template renders to PDF via headless Chromium. FastAPI feature-flagged endpoints added: GET /api/cv/status and GET /api/cv/generate?lang=english|nederlands.
PostgreSQL CRUD — Phase 3: Tests + Admin Endpoints (PR #184)¶
Added pytest fixtures with a real test database (postgres service container in CI), conftest.py for async engine setup, and full test coverage for all DB-backed endpoints. Admin write endpoints (POST /api/admin/projects|experience|skills) protected by Authorization: Bearer <api-admin-token>. Token stored in Key Vault (api-admin-token secret), injected via Bicep.
Parallel CI Test Workflow (PR #186)¶
Consolidated test-api.yml, test-unit.yml, typecheck.yml, and check-links.yml into a single test.yml with four parallel jobs (unit, api, typecheck, links) and a gate job as a required status check. fail-fast: false so all four jobs always run; the gate fails if any job failed.
Fix CV pagedjs-cli Deps (PR #187)¶
Fixed a Node.js runtime compatibility issue with pagedjs-cli in the CI container. Pinned the nodejs version and resolved the dependency conflict.
2026-04-12 — PostgreSQL Phases 1–2, Devcontainer, E2E Typing¶
PRs: #163, #178, #180, #181
End-to-End Typing (PR #163)¶
Introduced pydantic-settings Settings class for backend config (replaces bare os.getenv). Added field constraints to ContactRequest (name min 2, message max 5000). Set up openapi-typescript to generate shared/api.types.ts from the live FastAPI OpenAPI schema. Added ESLint no-explicit-any rule. Added CI type-drift guard: regenerates types and fails if the committed file differs.
PostgreSQL CRUD — Phase 1: DB Layer (PR #178)¶
Added api/db/engine.py (async SQLAlchemy engine with IMDS token callback), ORM models (Project, SkillCategory, WorkEntry), Alembic migration infrastructure, and seed data. All three content endpoints (/api/projects, /api/experience, /api/skills) now read from PostgreSQL with a static in-memory fallback. Docker-compose extended with a postgres:16-alpine service.
Devcontainer (PR #180)¶
Added .devcontainer/devcontainer.json + postCreate.sh for GitHub Codespaces. Features: Node 20, Python 3.11, Azure CLI + Bicep, Docker-in-Docker. Ports forwarded: 80 (frontend), 8000 (backend), 8080 (MkDocs), 5432 (postgres). PYTHONPATH=/workspaces/portfolio set so pytest tests/ works from repo root.
PostgreSQL CRUD — Phase 2: Bicep Infra (PR #181)¶
Added infra/modules/postgres.bicep: Azure PostgreSQL Flexible Server with AAD-only auth (no password), the backend managed identity as the AAD administrator, a portfolio database, and firewall rule for Azure services. DATABASE_URL format: postgresql+asyncpg://<mi-clientId>@<fqdn>/portfolio?ssl=require. PostgreSQL deployed to northeurope (West Europe quota-restricted for Flexible Server). Wired through main.bicep with deploy_postgres param (default false).
2026-04-10 — CI/CD Hardening (Items 5–8)¶
PRs: #159, #160, #161, #162
Branch series: claude/item-7-docker-cache → claude/item-8-runbook → claude/item-5-race-condition → claude/item-6-notifications
Item 7 — Docker Layer Caching (PR #159)¶
Added ACR registry-mode Docker layer caching to all container build steps.
Changes:
deploy-pr-preview.yml: replaced baredocker build+docker pushwithdocker/setup-buildx-action@v3+docker/build-push-action@v6; cache tags scoped per PR (cache-pr-<N>) to avoid branch bleeddeploy-application.yml: same migration; sharedcachetags for prod builds- Added
cleanup-pr-cachejob triggered onpull_request: closed— deletesportfolio-frontend:cache-pr-<N>andportfolio-backend:cache-pr-<N>tags from the dev ACR to prevent unbounded tag accumulation
Impact: Cold builds gain layer reuse across runs; PR builds don't pollute the prod cache namespace.
Item 8 — Runbook: Secret Rotation & Redeploy (PR #160)¶
Created a comprehensive operations runbook covering all secret rotation procedures.
New file: docs/infrastructure/runbook-secret-rotation.md
Sections:
| Section | Content |
|---|---|
| Secret inventory | All secrets with source, target (GitHub / Key Vault), and rotation frequency |
| Email credentials | SMTP password rotation steps + redeploy |
| Entra ID client secret | Azure Portal → App Registration → new secret → update GitHub Secrets |
| OIDC federated credentials | When to rotate vs. when they self-rotate; steps to add/remove credentials |
| Full redeploy from scratch | End-to-end procedure: Bicep base → containers → docs; when to use |
| Validation checklist | Smoke test commands after rotation |
Added nav entry to docs/infrastructure/.nav.yml.
Item 5 — DeploymentActive Race Condition Fix (PR #161)¶
Eliminated the Azure ARM DeploymentActive collision between concurrent GitHub Actions runs.
Root cause: Three workflows (deploy-infrastructure.yml, deploy-application.yml, deploy-pr-preview.yml) each called az deployment sub create against the same subscription scope but used disjoint GitHub Actions concurrency groups, so two runs could hold different locks while both hitting Azure.
Changes:
- Unified concurrency groups across all three workflows to
azure-deploy-dev/azure-deploy-prod, so only one workflow run per environment can proceed at a time - ARM pre-flight poll improved in all four poll sites: distinguishes between a failed
azcommand (exit non-zero → log error, retry) and an empty result (no running deployments → proceed); previously a command failure silently appeared asACTIVE=""and proceeded anyway - PR comment enrichment (
deploy-infrastructure.yml): fixed step output references (steps.deploy-env.outputs.*instead of the non-existentsteps.deploy.outputs.*that only runs whendeploy_containers=true), preventing empty placeholder values in PR comments - HTML marker added to PR comment body (
<!-- PORTFOLIO_DEPLOYMENT_COMMENT -->) for reliable upsert matching - Removed duplicate validate/what-if steps that had been added to the
deployjob indeploy-infrastructure.yml
Item 6 — Deploy Failure Notifications via GitHub Issues (PR #162)¶
Replaced action-triggered email notifications with GitHub Issues, deduplicated per workflow.
Changes applied to all five workflow notify-failure jobs:
deploy-infrastructure.ymldeploy-application.ymldeploy-pr-preview.ymldeploy-docs.ymlsmoke-test-prod.ymldeploy-infrastructure-shared.yml
Notification logic:
// Dedup: update existing open issue for same workflow instead of creating a new one
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'deployment-failure',
state: 'open',
});
const existing = issues.find(i => i.title.startsWith(`Deployment failure: ${workflow}`));
if (existing) {
// Add a "Recurrence" comment to the existing issue
await github.rest.issues.createComment({ ... });
} else {
// Create a new labeled issue
await github.rest.issues.create({ ... });
}
Failure info captured per notification:
- Workflow name, git ref, commit SHA (linked), failed job names, run URL
Hardening: All five scripts wrapped in try { ... } catch (error) { core.warning(...) } so a GitHub API failure in the notification job does not obscure the actual deployment failure that triggered it.
Permissions: Added issues: write to each workflow's permissions block.
2026-04-08 — Frontend Modernisation¶
PRs: #143–#148, #150–#151, #154–#156
| PR | Item |
|---|---|
| #143 | Migrate to TypeScript 6 |
| #144 | Migrate to Tailwind CSS 4 |
| #145 | Migrate to React 19 |
| #146 | Upgrade Vite 5→6 |
| #147 | Fix Claude Code review workflow permissions |
| #148 | Fix duplicate Experience navbar item |
| #150 | Fix bufferutil lock file mismatch in Vitest CI |
| #151 | Consume /api/projects from frontend (React Query hooks, skeleton loading) |
| #154 | Fix prev/next navigation, local images, API proxy, FastAPI upgrade |
| #155 | Fix dark mode toggle in Tailwind CSS 4 (@custom-variant dark) |
| #156 | Add related project links to experience full page; Add IaC Data Platform Framework project |
2026-04-01 — API & Tooling¶
PRs: #119–#120
| PR | Item |
|---|---|
| #119 | Add Claude Code GitHub Workflow |
| #120 | Dynamic OpenAPI servers block + local-first Swagger UI |
2026-03-26 — Dev Cost & Dependabot¶
PRs: #96–#118
| PR | Item |
|---|---|
| #96 | Fix dev Container Apps cost leak: single revision mode + PR close cleanup |
| #97 | Add dependabot-review skill + fix dependabot.yml config |
| #98–#118 | Resolve Dependabot security alerts (bulk) |
2026-03-22 — Architecture Docs & Infrastructure¶
PRs: #88–#93
| PR | Item |
|---|---|
| #88 | Force Container App revision + EasyAuth session fix + dev-docs SWA slot + pin uv |
| #89 | Add /api/health route, dev server in OpenAPI, fix EasyAuth CI bypass |
| #90 | draw.io architecture diagram |
| #93 | C4 Context + Container diagrams, deployment diagram, Azure cloud diagram, architecture index, MkDocs glightbox, consolidate deps, fix draw.io PNG export in CI |
2026-03-20 — Docs & SSO¶
PRs: #73–#82
| PR | Item |
|---|---|
| #73–#80 | Entra ID SSO for dev surfaces (redirect URIs + secrets) |
| #81 | Docs restructure + metadata tables |
| #82 | Add Swagger UI + ADR section to docs |
2026-03-04 — Content Extraction¶
PRs: #60
| PR | Item |
|---|---|
| #60 | Extract hardcoded content into typed data modules |
This page is updated at the end of each development session. See Roadmap for planned future work and Work in Progress for active tasks.