Skip to content

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-phase2refactor/api-phase3refactor/api-phase4refactor/api-phase5refactor/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:

  1. 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 at 169.254.169.254 is unreachable during Container App cold start.
  2. PR #267 — Alembic CMD retry: the Dockerfile.backend CMD now retries alembic upgrade head up to 3× with a sleep, since it runs before uvicorn and hits the same IMDS cold-start window.
  3. PR #269 — azure-identity: replaced the raw urllib IMDS call entirely with ManagedIdentityCredential from azure-identity (already used in feature_flags.py). Raw urllib never worked reliably on ACA.
  4. PR #270 — MI principal name: Bicep was writing the Postgres DATABASE_URL with 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 in infra/modules/container_apps.bicep.
  5. PR #273 + #274 — Workflow overwrite fix: deploy-infrastructure.yml was overwriting the corrected Key Vault secret with the old UUID-based URL after Bicep ran. Also the env short name was prod instead of prd in 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-configfeat/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-cacheclaude/item-8-runbookclaude/item-5-race-conditionclaude/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 bare docker build + docker push with docker/setup-buildx-action@v3 + docker/build-push-action@v6; cache tags scoped per PR (cache-pr-<N>) to avoid branch bleed
  • deploy-application.yml: same migration; shared cache tags for prod builds
  • Added cleanup-pr-cache job triggered on pull_request: closed — deletes portfolio-frontend:cache-pr-<N> and portfolio-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 az command (exit non-zero → log error, retry) and an empty result (no running deployments → proceed); previously a command failure silently appeared as ACTIVE="" and proceeded anyway
  • PR comment enrichment (deploy-infrastructure.yml): fixed step output references (steps.deploy-env.outputs.* instead of the non-existent steps.deploy.outputs.* that only runs when deploy_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 deploy job in deploy-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.yml
  • deploy-application.yml
  • deploy-pr-preview.yml
  • deploy-docs.yml
  • smoke-test-prod.yml
  • deploy-infrastructure-shared.yml

Notification logic:

javascript
// 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.