Compare commits

..

11 Commits

56 changed files with 1046 additions and 1360 deletions
+4
View File
@@ -3,3 +3,7 @@
The deployment script and Gitea workflow live in this directory and `.gitea/workflows/`. The current deployment, health/readiness, rollback, and secret-handling guidance is in the repository [infrastructure guide](../docs/infrastructure/README.md), especially [deployment](../docs/infrastructure/deployment.md) and [disaster recovery](../docs/infrastructure/disaster-recovery.md).
Keep the server `.env` outside the repository. Do not run production deployment, restore, migration, or credential rotation as part of local documentation work.
MariaDB backup connection parsing uses `scripts/parse-backup-connection.py` (Python 3, already required by deployment preflight). Keys are case-insensitive and support the documented host/user/password/database aliases. Single/double-quoted values may contain semicolons and doubled quote characters. The dump contract requires one TCP host, a database and user, with port 1-65535 (default 3306); an empty password is retained. Duplicate aliases, control characters, host lists/socket paths and database path separators are rejected before dumping. This deliberately bounded parser is not the full MySqlConnector configuration engine. Additional connection options do not change existing dump-client TLS behavior; that qualification remains separate.
Run `python scripts/test-deploy-backup.py` for the offline synthetic dump-boundary regression. It extracts only the backup function and never executes the deployment entry point, contacts a database or reads the production environment.
+69 -28
View File
@@ -326,19 +326,19 @@ backup_database() {
return 1
fi
# Parse the ADO.NET connection string. Keys are case-insensitive in .NET, so match that.
local db_host db_port db_name db_user db_pass
db_host="$(sed -n 's/.*[Ss]erver=\([^;]*\).*/\1/p' <<<"$cs")"
db_port="$(sed -n 's/.*[Pp]ort=\([^;]*\).*/\1/p' <<<"$cs")"
db_name="$(sed -n 's/.*[Dd]atabase=\([^;]*\).*/\1/p' <<<"$cs")"
db_user="$(sed -n 's/.*[Uu]ser[ ]*[Ii]*[Dd]*=\([^;]*\).*/\1/p' <<<"$cs")"
db_pass="$(sed -n 's/.*[Pp]assword=\([^;]*\).*/\1/p' <<<"$cs")"
db_port="${db_port:-3306}"
if [ -z "$db_host" ] || [ -z "$db_name" ] || [ -z "$db_user" ]; then
echo "Could not parse host/database/user from JOBTRACKER_CONNECTION_STRING. Aborting deploy."
# Keep secrets off argv; parse aliases/quoted separators without evaluating shell text.
local db_host db_port db_name db_user db_pass parsed_connection
local -a connection_fields=()
if ! parsed_connection="$(printf '%s' "$cs" | python3 scripts/parse-backup-connection.py)"; then
echo "Could not parse JOBTRACKER_CONNECTION_STRING for backup. Aborting deploy."
return 1
fi
mapfile -t connection_fields <<< "$parsed_connection"
db_host="${connection_fields[0]}"
db_port="${connection_fields[1]}"
db_name="${connection_fields[2]}"
db_user="${connection_fields[3]}"
db_pass="${connection_fields[4]}"
local target="$BACKUP_DIR/jobtracker-${db_name}-${BACKUP_STAMP}.sql.gz"
echo "Backing up MariaDB database '${db_name}' on ${db_host}:${db_port} to ${target}"
@@ -525,12 +525,14 @@ apply_backup_retention() {
return 1
fi
local month monthly_root monthly_dir
local month monthly_root monthly_dir complete_tmp
month="$(TZ="$timezone" date +%Y-%m)"
monthly_root="$BACKUP_DIR/monthly"
monthly_dir="$monthly_root/$month"
mkdir -p "$monthly_root"
chmod 700 "$monthly_root"
if ! mkdir -p "$monthly_root" || ! chmod 700 "$monthly_root"; then
echo "Could not prepare monthly backup directory. Aborting deploy."
return 1
fi
local current_files=("$BACKUP_DIR"/jobtracker-*"-$BACKUP_STAMP".*)
if [ ! -e "${current_files[0]}" ]; then
@@ -539,22 +541,47 @@ apply_backup_retention() {
fi
if [ ! -f "$monthly_dir/.complete" ]; then
mkdir -p "$monthly_dir"
chmod 700 "$monthly_dir"
cp --preserve=mode,timestamps -- "${current_files[@]}" "$monthly_dir/"
printf '%s\n' "$BACKUP_STAMP" > "$monthly_dir/.complete"
chmod 600 "$monthly_dir/.complete"
if ! mkdir -p "$monthly_dir" || ! chmod 700 "$monthly_dir" ||
! cp --preserve=mode,timestamps -- "${current_files[@]}" "$monthly_dir/"; then
echo "Could not retain monthly recovery files. Aborting deploy."
return 1
fi
if ! complete_tmp="$(mktemp "$monthly_dir/.complete.XXXXXX")"; then
echo "Could not create monthly recovery marker. Aborting deploy."
return 1
fi
if ! printf '%s\n' "$BACKUP_STAMP" > "$complete_tmp" ||
! mv -T -- "$complete_tmp" "$monthly_dir/.complete"; then
rm -f -- "$complete_tmp"
echo "Could not publish monthly recovery marker. Aborting deploy."
return 1
fi
echo "Monthly recovery set retained: ${monthly_dir}"
fi
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'jobtracker-*' -mtime "+$daily_days" -delete
if ! find "$BACKUP_DIR" -maxdepth 1 -type f -name 'jobtracker-*' -mtime "+$daily_days" -delete; then
echo "Daily backup retention failed. Aborting deploy."
return 1
fi
local monthly_dirs=()
mapfile -t monthly_dirs < <(find "$monthly_root" -mindepth 1 -maxdepth 1 -type d -name '20??-??' | sort)
local monthly_dirs=() monthly_listing
# Check the pipeline directly; mapfile cannot report process-substitution failures.
if ! monthly_listing="$(find "$monthly_root" -mindepth 1 -maxdepth 1 -type d -name '20??-??' | sort)"; then
echo "Could not enumerate monthly backups. Aborting deploy."
return 1
fi
if [ -n "$monthly_listing" ]; then
mapfile -t monthly_dirs <<< "$monthly_listing"
fi
while [ "${#monthly_dirs[@]}" -gt "$monthly_count" ]; do
local oldest="${monthly_dirs[0]}"
case "$oldest" in
"$monthly_root"/20??-??) rm -rf -- "$oldest" ;;
"$monthly_root"/20??-??)
if ! rm -rf -- "$oldest"; then
echo "Monthly backup retention failed. Aborting deploy."
return 1
fi
;;
*) echo "Refusing to prune unexpected monthly backup path: $oldest"; return 1 ;;
esac
monthly_dirs=("${monthly_dirs[@]:1}")
@@ -574,14 +601,20 @@ upload_current_backup_set() {
remote="${remote%/}"
local current_files=("$BACKUP_DIR"/jobtracker-*"-$BACKUP_STAMP".*) file
for file in "${current_files[@]}"; do
rclone copyto "$file" "$remote/daily/$(basename "$file")" --immutable
if ! rclone copyto "$file" "$remote/daily/$(basename "$file")" --immutable; then
echo "Daily off-host backup upload failed. Aborting deploy."
return 1
fi
done
local month="$(TZ="${BACKUP_TIMEZONE:-Europe/Oslo}" date +%Y-%m)"
if [ -d "$BACKUP_DIR/monthly/$month" ]; then
rclone copy "$BACKUP_DIR/monthly/$month" "$remote/monthly/$month" --immutable
if ! rclone copy "$BACKUP_DIR/monthly/$month" "$remote/monthly/$month" --immutable; then
echo "Monthly off-host backup upload failed. Aborting deploy."
return 1
fi
fi
echo "Current recovery set copied to configured encrypted off-host remote."
echo "Current recovery set copied to configured off-host remote."
}
resolve_existing_web_proxy_subnet
@@ -642,10 +675,18 @@ finish_deploy() {
if [ "$status" -ne 0 ] && [ "$replacement_started" = true ]; then
if [ -n "$previous_backend_ref" ] && [ -n "$previous_frontend_ref" ]; then
echo "Deployment failed after core replacement; restoring previous healthy images."
echo "Deployment failed after core replacement; restarting previous core images."
if BACKEND_IMAGE="$previous_backend_ref" FRONTEND_IMAGE="$previous_frontend_ref" \
compose up -d --force-recreate --remove-orphans backend frontend; then
echo "Previous core release restored. Deployment remains failed; inspect the logs above."
if curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 "${APP_PUBLIC_BASE_URL%/}/" >/dev/null &&
curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 "${APP_PUBLIC_BASE_URL%/}/health" >/dev/null &&
curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 "${APP_PUBLIC_BASE_URL%/}/ready" >/dev/null; then
echo "Previous core images restarted and public core checks passed. Deployment remains failed; inspect the logs above."
else
echo "Previous core images restarted, but rollback health verification failed. Manual recovery is required."
compose ps || true
compose logs --tail=200 backend frontend || true
fi
else
echo "Automatic core rollback failed. Manual recovery is required."
compose ps || true
+11 -3
View File
@@ -1,5 +1,11 @@
# First production deployment runbook
> Historical July 2026 baseline. Current release preparation follows
> [the release qualification matrix](../docs/release-readiness/14_RELEASE_ACCEPTANCE_MATRIX.md) and
> [recovery specification](../docs/release-readiness/11_BACKUP_RESTORE_ROLLBACK.md).
> Reconcile the examples below with the exact candidate, migration history and isolated rehearsal
> before operational use. Current rollback health checks are source-tested offline.
> Written 2026-07-19 against the actual implementation, not against the other docs. Every command and
> failure mode below was exercised locally against MariaDB 11 containers. No production data was used.
>
@@ -220,9 +226,11 @@ everything written since the dump — including anything users did during and af
**Restore the database only if the data itself is wrong.** If the backend simply will not start, the
code rollback is almost certainly the whole fix.
Why a code rollback is safe here: every Phase 4/5 migration is a no-op — the reconciler owns those
tables — so reverting code never leaves migration history ahead of the schema. The reconciler is
additive and never drops a table holding rows, so the older code simply ignores the newer tables.
The July 2026 baseline used no-op Phase 4/5 migrations around reconciler-owned tables. Current
migrations also perform schema changes, including `AddJobRevision` adding the application Version
column and `AddUserOperationEvents` creating its table and indexes. Qualify the exact old/new image
pair against the current migration history and representative post-upgrade writes. The recovery
specification above includes ownership, revision, document and restore checks for that rehearsal.
## Procedure
-20
View File
@@ -1,20 +0,0 @@
# Public content architecture — proposed, not implemented
2 November 2026. Independent preparation under the owner's current direct-roadmap instruction. No visual concept selected, public route added, marketing claim published or application source changed. Final layout/section order remains an owner decision.
Reuse the current Next static-export build, React Router private shell, MUI design tokens, shared plan ownership and en/nb translation contracts. Avoid a CMS or second application shell. Public descriptive content should be emitted as crawlable static HTML from repository-managed, locale-aware content; authenticated routes retain their current client-only providers. Do not turn private account data, feature flags or arbitrary query strings into build-time content.
Separate content domains:
- Feature catalogue: stable feature ID and links to implementation, scoped acceptance, deployed identity, entitlement, market, locale, approved screenshot and marketing approval. Missing evidence withholds a current-availability claim. Existing MARKETING_CLAIM_VALIDATION.md remains the authority; do not duplicate it into a competing status list.
- Page copy: complete title, description, headings, feature narratives, links, FAQ and accessible text in approved locale resources. Whole sentences and explicit placeholders; reuse terminology and distinguish canonical Career Profile from a CV variant.
- Market: available product/provider capabilities, actual billing/currency, legal links and applicable languages. A market is not a language, nationality or automatic grant to fetch jobs.
- Assets: genuine synthetic product captures with source commit/date/surface/theme/viewport, rights and alt-text context. The five application design concepts are conceptual prototypes, not screenshots of a released product. No unlicensed images or invented customer testimonials.
Draft profession-neutral content options for later copy review: “Your job search, organised.” / “Jobbsøkingen din, organisert.” Supporting proposition: “Keep applications, career information and documents together.” / “Samle søknader, karriereinformasjon og dokumenter på ett sted.” These are unpublished candidate copy, not released-capability approval or native-language acceptance. Avoid promising universal import accuracy, live international sources, automatic employer actions, AI availability or privacy certifications.
Candidate narrative modules (not an approved order): clear outcome and product preview; tracking and next steps; Career Profile/document workflow; optional import review; how it works; verified plan information; factual FAQ; supported registration path; legal/support footer. Where registration is disabled, the CTA must use the existing truthful sign-in/access path rather than promise account creation. Legal destinations and final FAQ answers require verified content; never invent links or policies.
Implementation gate: after final visual approval, use the approved composition with static public copy and a small client interaction boundary for preference/navigation. Preserve signed-in redirect, protected-route return state and the existing dashboard-first optional onboarding. No second onboarding flow. Reverting future content is an ordinary version-controlled content rollback; no database migration is proposed by this architecture.
Open: final visual choice, actual released feature/plan/provider evidence, production origin/deployed identity, public asset provenance and native copy review. Full European catalogue work does not automatically publish locale pages.
-22
View File
@@ -1,22 +0,0 @@
# LAUNCH-01 homepage concepts
Open the [comparison gallery](concepts/index.html) from this checkout, or open any full-page desktop and phone image below. These are standalone English design previews. The registration and sign-in treatments scroll to an explanation in the prototype. Plans show layout and the existing catalogue labels only. Public copy, pricing, legal destinations, localisation and feature availability need release validation.
| Concept | View | Strength | Tradeoff and accessibility focus |
| --- | --- | --- | --- |
| A · Modern Minimalist | [Responsive](concepts/minimal.html) · [Desktop](concepts/renders/minimal-desktop.png) · [Phone](concepts/renders/minimal-phone.png) | A direct split hero leads with the application tracker. Violet accents connect it to the application. | The restrained treatment puts more weight on the headline. On phones, the screenshot is a cropped preview with a full-capture link. |
| B · Premium SaaS | [Responsive](concepts/premium.html) · [Desktop](concepts/renders/premium-desktop.png) · [Phone](concepts/renders/premium-phone.png) | A large dashboard stage explains the organisation workflow through the product itself. | The desktop composition uses more vertical space. The phone layout puts a left-aligned headline and actions before the capture. Recheck text contrast if the background treatment changes. |
| C · Friendly and Human | [Responsive](concepts/human.html) · [Desktop](concepts/renders/human-desktop.png) · [Phone](concepts/renders/human-phone.png) | Sage and Georgia display type support an original illustration spanning education, care and trades. A separate teacher CV capture shows the actual product. | Product evidence follows the introduction. The illustration has an alternative description and is separate from the development capture. |
| D · Modern Editorial | [Responsive](concepts/editorial.html) · [Desktop](concepts/renders/editorial-desktop.png) · [Phone](concepts/renders/editorial-phone.png) | A navy masthead and asymmetric Career Workspace frame emphasise the experience applicants bring to the search. | Display typography needs locale expansion checks. On phones, the masthead and application preview stack in reading order. |
Each page has the same navigation destinations, basic feature and workflow copy, provisional Free/Pro slots and footer. This keeps the comparison focused on hierarchy, imagery and type. Review the hero, navigation, typography, colour, illustration balance, feature presentation, calls to action and phone layout. The owner can combine parts across concepts and identify missing sections.
## Verification
Chromium passed 20 local browser cases across the four prototypes and the gallery at 1440, 768, 390 and 320 px. Checked image loading, no horizontal page overflow, local link destinations, keyboard skip navigation, inactive signup flow, minimum visible link size and axe WCAG A/AA tags. No page errors, external requests or tagged violations occurred. The first run exposed a small review link. Its hit area was enlarged and the full matrix reran. The eight full-page desktop and phone renders were visually inspected. [Browser receipts](concepts/renders/checks.json)
See [screenshot and asset provenance](SCREENSHOT_AND_ASSET_MANIFEST.md). No production feature, provider, pricing, signup or availability claim was verified. Native accessibility, translated copy and production performance remain open.
## Owner selection gate
LAUNCH-01 is ready for owner review. LAUNCH-02 starts when the owner selects one or more directions. Final implementation requires separate explicit approval of the refined design.
-39
View File
@@ -1,39 +0,0 @@
# LAUNCH-00 source audit - partial checkpoint
## Design pattern and asset inputs
- Linear shows the workspace near the top and lets specific workflow examples explain the product. The Premium concept explores a genuine Jobjakt dashboard in a similar position using its own visual treatment. [Linear homepage](https://linear.app/)
- Notion groups its feature presentation around recognizable actions. All four Jobjakt concepts use three short outcomes tied to tracking, Career Profile and CV preparation. [Notion product overview](https://www.notion.com/product)
- Basecamp combines a direct product explanation with a distinct tour action. The Jobjakt hero prototypes separate a proposed registration action from the feature walkthrough. [Basecamp homepage](https://basecamp.com/)
The [four rendered concepts](DESIGN_CONCEPTS.md) use verified synthetic product captures and one original illustration. Signup submission, degraded auth, baseline performance and native accessibility remain open in LAUNCH-00. A live commercial claim or current price still needs independent evidence and approval.
## Rendered baseline reconciliation, 10 November 2026
The separately published A3 evidence at `bd34a3504f7e849c41b48db444f0e1f16324a16f` includes `docs/ui-modernisation/visual-correction/LANDING_BASELINE_REVIEW.md` and `evidence/landing-baseline/BROWSER.json` under the same directory. The runtime was `e8e7d83958587e8b5a4b93d03d3e736896df5a48`. This closes the rendered anonymous navigation and raw export observations at that baseline.
12 fixture journeys covered 320, 768 and 1440 px in EN/NB and both themes. Keyboard feature navigation, local language selection and entry into existing sign-in and registration routes passed. No registration was submitted. All three preview SVGs loaded. All 12 states reported missing landmark best practices, and the raw static response lacked a hero heading, canonical and locale alternates. The anonymous language switch attempted 24 authenticated preference writes that the fixture rejected. Public language changes should use the existing local choice until the user has a session. The dashboard SVG email was anonymised in the capture fixture only.
The captured images are browser evidence of the existing homepage. The preview SVGs inside it are still illustrations. For LAUNCH-01, source potential real product captures from the published A3 evidence viewer, including `dashboard`, `applications-selection`, `career` and the existing CV editor captures. Before using an image, inspect the pixels and bind it to its actual route, source commit, language, viewport, fixture and asset rights. The A3 development branch has outstanding visual acceptance. Its captures are candidates for review mockups. Market, provider and pricing claims require the separate claim review.
Open audit inputs include contemporary design pattern research, asset rights, realistic registration and degraded-auth behaviour, measured performance and native review. The four LAUNCH-01 concepts remain to be rendered.
2026-11-02. `feature/launch-site-redesign`, accepted conditional baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Full authoritative homepage prompt read. LAUNCH-00 IN_PROGRESS; LAUNCH-01 concepts NOT YET PRODUCED. No final design selected or implemented.
| Finding | Source evidence | User implication / proposed direction |
| --- | --- | --- |
| Main Get started opens sign-in | `LandingPage.tsx` passes `goToLogin` to hero CTA; Free-plan CTA separately goes to `/register` | Registration intent has an extra mode-selection step. Propose direct registration while preserving sign-in return state |
| Existing signup owner should be reused | `App.tsx:522` maps `/register` to `LoginPage initialMode=register`; LoginPage respects auth config and registration availability | No second onboarding implementation; final CTA must reflect actual enabled registration |
| Product previews are conceptual assets | `LandingPage.tsx` uses `/mockups/dashboard.svg`, `pipeline.svg`, `workspace.svg` | Replace with provenance-bound synthetic captures in approved design; never label current artwork a genuine screenshot |
| Implementation details occupy hero copy | LandingPage renders React/TypeScript/ASP.NET/EF/FastAPI/Gmail text | Replace with plain user outcomes across professions; framework names do not explain job-search value |
| Marketing content depends on client execution and auth probe | `app/page.tsx` loads ClientShell; `ClientShell.tsx` uses `ssr:false`; LandingPage shows loading until `/auth/me` resolves/rejects | Crawlable HTML and loading/failure behavior need measured review; cannot assume static export includes full marketing copy |
| Metadata is generic | `app/layout.tsx` has global title/description and initial `lang=en` | Design market/content-locale metadata and canonical/alternate rules without disturbing authenticated routing; no SEO score claimed |
| Brand continuity exists | Indigo/cyan hero, shared MUI/theme, custom en/nb translations | Explore distinct compositions while preserving identity; avoid rebuilding app shell |
| Product presentation is limited | Six feature cards, six steps and three conceptual images; no actual redesigned CV/import/gallery imagery | Prioritise actual accepted tracking, canonical Career Profile and reviewed document workflow; do not imply new Global Jobs is shipped |
| Pricing shows capabilities, not amounts | `planCatalog.ts` defines Free/Pro, explicitly delegates commercial terms to configured checkout | Do not invent amounts/currencies/trials or promise active Pro AI; release and entitlement validation required |
| Footer lacks privacy/legal links in this component | LandingPage footer contains copyright text and sign-in button | Verify existing legal destinations and approved copy before adding links; no invented compliance claims |
| Responsive layouts are declared | MUI breakpoints change hero controls, feature grids, plan columns and footer | Source intent only; mobile/tablet/keyboard/reflow/contrast require actual rendered checks |
Preserve signed-in redirect to `/dashboard`, interrupted protected-route return state, shared language behavior, plan policy ownership and existing registration constraints. New public market language must not overwrite job-ad or document output language. Existing custom en/nb implementation is not full European locale support.
Not yet checked: current rendered desktop/mobile site, external design-pattern research, runtime signup, slow/offline auth probe, keyboard/AT/contrast, image loading/performance, crawled output, real deployment availability and final asset rights. These omissions keep LAUNCH-00 open. No product tests were rerun for this Markdown-only source checkpoint.
-11
View File
@@ -1,11 +0,0 @@
# Marketing locale integration — proposed contract
2 November 2026. EN/Bokmål source support is existing; no additional locale is released by this report. Reuse I18N-00 registry/catalogue proposals and their review gates instead of a second country-language mapping. Full European locale identities, script distinctions and native review remain the localisation workstream's responsibility.
Keep marketing content locale, selected market, application UI language, ad-language preferences and document language distinct. A marketing selector may offer a deliberate explicit application-language action after sign-in; it must not silently write ad/document preferences. Current shared UI preference behavior must be inspected and migrated compatibly before introducing a separate persisted marketing choice. No persistence change is made here.
Supported choices show native and English names, retain English access, and allow another supported language independent of market. Bokmål/nb/nb-NO/no legacy compatibility does not grant Nynorsk support. Unreleased languages may be described truthfully as forthcoming only with approved copy; they do not receive public routes, hreflang or sitemap entries. User-generated CV/job text is not translated by changing chrome language.
Complete each published variant's visible, form, error, accessibility, metadata, social and legal text. Validate keys/placeholders, fallback behavior, interpolation escaping, long labels, plural/number/date/currency rules and dirty-form switching. Native terminology review is separate from syntactic coverage. Currency display follows real billing capability, not a selected country. Preserve provider names, original job language, rights/attribution and the distinction between source descriptions and any reviewed translation.
Acceptance remains BLOCKED for additional released locales until architecture entry, coverage, translation rights/proficient review, actual market capabilities, approved content and integrated QA are satisfied. This does not prevent independent catalogue, fixture, content and SEO planning. See SEO_STRATEGY.md for published-only URL constraints and CONTENT_ARCHITECTURE.md for content ownership.
@@ -1,20 +0,0 @@
# Preliminary claims and content boundaries
All claims below are **WITHHELD from public release approval**. Source implementation and conditional development acceptance do not establish production availability. No production inspection was authorised or performed.
| Proposed subject | Implementation/development evidence | Missing marketing proof |
| --- | --- | --- |
| Track applications in table/Kanban | Existing LandingPage/plan catalogue and conditional pre-P10 handoff | Deployed version, applicable account entitlement, genuine synthetic capture and approved copy |
| Career Profile and CV variants | Canonical profile/document contracts in architecture and CV handoff | Current deployed behavior and accurate plan/theme availability |
| Import, review and correct CV facts | Pre-P10 accepted deterministic supported-fixture workflow | No universal parsing or AI-accuracy claim; actual released formats/languages and limitations |
| CV templates and PDF | Scoped development appearance/metric approval retained | Physical-print limits, deployed design/version and screenshots |
| Reminders/mail/integrations | Existing source owners; provider qualification separate | Actual enabled countries/providers/consent/delivery; avoid blanket automation promises |
| International job search | GJ-00 planning separately published; later implementation blocked | Accepted implementation, provider rights and per-country deployed capabilities |
| AI assistance | Some contracts implemented; real CV model quality/activation blocked | Accepted model compatibility/accuracy, privacy/consent and enabled deployment |
| Free/Pro | Shared public catalogue exists; no price encoded here | Server policy/checkout consistency, active billing, tax/currency wording and approved current prices |
| Languages | Existing en/nb source catalogues; I18N audit separate | Native review and per-surface release state; additional locale candidates are not supported language packs |
| Privacy/security | Existing controls and conditional security evidence | Approved truthful wording; no certification/compliance guarantee |
Each final catalogue entry needs feature ID, implementation commit, acceptance artifact, deployed version, entitlement, market, locale, screenshot provenance and owner marketing approval. Missing fields deny availability claims. This is a documentation contract, not a new executable publishing system.
Proposed section sequence for concept exploration: outcome-led hero; genuine tracking preview; career/document journey; broad-profession story; how it works; verified plans; FAQ; registration CTA; legal/support footer. Final order remains a design choice. No fabricated customer quotes, statistics, discounts or job-provider coverage.
-20
View File
@@ -1,20 +0,0 @@
# Partial homepage planning handoff
10 November concept successor. [LAUNCH-01 comparison](DESIGN_CONCEPTS.md) contains four responsive prototypes and eight full-page renders with synthetic development captures and a source manifest. Twenty local browser cases passed. The owner can select or combine directions before LAUNCH-02. Final visual approval, public copy, production rendering and launch acceptance remain separate. Earlier pending statements below describe the previous checkpoint.
10 November: the published A3 landing baseline now supplies 12 rendered navigation states, raw HTML findings and screenshot provenance candidates. See [homepage audit](HOMEPAGE_AUDIT.md). Next, complete the outstanding audit inputs and render the four concept pairs. Final homepage selection, application implementation and launch acceptance retain their separate gates.
Continuation 2 November: CONTENT_ARCHITECTURE.md, INTERNATIONALISATION.md and SEO_STRATEGY.md add independent design-neutral content/routing/publication proposals. Byte-matched existing build-shell evidence confirms raw export lacks descriptive marketing HTML, canonical and hreflang. No public UI/routes/metadata implementation or final visual choice made. These source/build checks are not live SEO, performance or release qualification. The five application UI concepts remain separate from LAUNCH-01's required homepage exploration; no sixth application concept created.
Branch `feature/launch-site-redesign`, baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Documentation-only source audit. LAUNCH-00 remains IN_PROGRESS. LAUNCH-01 four actual desktop/mobile concepts are PENDING, not replaced by written descriptions. No owner design decision requested until reviewable concepts exist.
Next authorised work:
1. Inspect rendered homepage/signup and research contemporary patterns using credible public sources. Preserve existing routing and privacy boundaries.
2. Obtain provenance-bound synthetic screenshots from accepted evidence or an isolated test environment. The committed pre-P10 visual manifest contains real PDF previews/contact sheets, not a complete set of dashboard/CV editor/mobile product captures. Inspect any image before reuse; do not touch the separate pre-p10-ux worktree.
3. Produce four distinct, viewable desktop/mobile concepts and an asset/licence manifest. No external image/font acquisition without appropriate rights. Concept placeholders cannot be represented as real application screenshots.
4. Present concrete concepts for mix-and-match owner feedback, then stop final implementation until explicit final-design approval. Design approval does not authorise publication.
No assets, app code, tests, routes, packages, production services or authenticated dashboard were changed. Rendered/browser/performance/accessibility/SEO checks NOT RUN. No production feature/pricing/language availability was asserted.
The delegated design task stopped at a service usage limit before saving deliverables. Parent continued the source audit and preserved this checkpoint. No design agent is running in the background.
@@ -1,14 +0,0 @@
# Launch screenshot and asset manifest
The eight product images in `concepts/assets/` are unchanged copies of real browser captures of the exported A3 React application using intercepted synthetic API fixtures. Every image was inspected and its SHA-256 matched the published evidence manifest at `49e79875d9681efab0880d7e812bb0e536b7d30c`. The prototypes show the top of each capture and link to the full source image. The fixture CV preview uses controlled HTML. These captures establish visual provenance for design review. Public reuse requires confirmation on the accepted release and separate marketing approval.
| Asset | Route and fixture | Runtime |
| --- | --- | --- |
| Applications | `/jobs`, saved community nurse role | `1246994cca9a3ba7cb89eb4605b3dc77b5d7c322` |
| Dashboard | `/dashboard`, synthetic nurse, teacher and engineer opportunities | `a7d726738b74d13fb805f7bd4f5780bdf19110d0` |
| CV editor | `/career/builder/3`, synthetic teacher CV and fixture preview | `b31ce6176bcb8f6a70d3e29ad4057b7cb74a1f53` |
| Career Workspace | `/career`, synthetic profile and import review | `b31ce6176bcb8f6a70d3e29ad4057b7cb74a1f53` |
[Exact source paths, widths and hashes](concepts/assets/provenance.json)
`concepts/professions.svg` is original vector artwork for this design review. It depicts fictional people and uses no external images, marks or fonts. Prototype type uses local platform fonts and no bundled font files. No photography was sourced or licensed in this checkpoint. Any later photography, generated artwork and public screenshot variants need their own approval and asset record.
-22
View File
@@ -1,22 +0,0 @@
# SEO baseline and design-neutral proposal
2 November 2026. Source/available-build review only; no deployment, crawl, search-engine ranking, Lighthouse or performance score claimed.
## Measured raw-export baseline
Reused the successful local I18N static build, checking byte identity of launch-branch `app/layout.tsx`, `app/page.tsx` and `app/[[...slug]]/ClientShell.tsx` first. Their SHA256 values respectively: `8e4f0cd2776d821c958480aec5c93943674acb0fe28cd60d0822df40009cb262`, `bf661f17c1c7c06ff8fda41b1afa44144bce694c815d52c3d5a927e32063b1b1`, `0afd4379fbfaeb2059cfa860b10523b212a20dc19c64f961de77bb8572ee6791`. ClientShell disables SSR for the application. This is reused build evidence for unchanged shell sources, not a fresh launch-branch build or proof of every bundle.
A standard-library HTML parser excluding script/style text found only `Jobbjakt` and `You need to enable JavaScript to run this app.` in out/index.html; canonical link count 0, hreflang count 0. This confirms missing descriptive raw HTML, not an assertion that JavaScript-capable crawlers cannot index the client UI. Root metadata is generic and document language initially en. Record post-implementation comparisons against this narrow baseline.
## Proposed contract after design/routing approval
- Emit approved public headings, descriptions, feature text and FAQs in static HTML. Keep private routes and provider/account content outside the prerendered marketing boundary.
- Give each published page a descriptive locale-specific title/description, semantic heading hierarchy, Open Graph metadata and approved social asset. Use factual SoftwareApplication/Organization structured data only when supported by the actual page; no fabricated ratings, offers or testimonials.
- Obtain the production public origin from release-approved configuration. Do not guess a hostname or use localhost as a public canonical. Canonicals must refer to the intended published page, with query/tracking variants normalised.
- Derive canonical/reciprocal alternate/sitemap entries from the same approved published market/content-locale inventory. Include only fully supported published variants; self and reciprocal links must agree. An unreviewed locale is absent, not an empty indexable page. Use valid language/script/region tags; country NO must not be treated as Bokmål nb.
- Decide URL shape separately from visuals: proposed explicit `/market/{market}/{contentLocale}/...` or another approved unambiguous scheme. This is a proposal, not an added route. Preserve existing auth and application links and static-host fallback behavior; do not implement speculative duplicate country pages.
- Keep incomplete/pre-release previews outside production index publication. Robots directives are not an authorisation boundary or protection for private content. Do not expose private or admin routes through sitemap generation.
## Required verification before acceptance
Parse exported HTML per approved locale/market; assert meaningful non-JS content, title/description/lang, one intended h1, canonical origin, reciprocal published-only alternates and sitemap membership. Check no unpublished variants, private URLs or unsupported prices/providers. Follow links in an isolated browser; test auth redirect/registration configuration, language persistence and separation from ad/document preferences. Then measure responsive/accessibility/performance budgets on realistic test conditions, preserving manual/device/native-language and deployed-origin gates. No executable SEO publishing infrastructure is introduced by this report.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

@@ -1,101 +0,0 @@
{
"evidenceCommit": "49e79875d9681efab0880d7e812bb0e536b7d30c",
"images": [
{
"target": "assets/applications-1440.png",
"source": "docs/ui-modernisation/visual-correction/evidence/applications-selection/ordinary-en-light-1440.png",
"runtime": "1246994cca9a3ba7cb89eb4605b3dc77b5d7c322",
"manifest": "docs/ui-modernisation/visual-correction/APPLICATION_SELECTION_SOURCE_MANIFEST.json",
"route": "/jobs",
"width": 1440,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "98898ed330f5bb08946477367e1c95e448ee1bfea26c978c13792985de1febda"
},
{
"target": "assets/applications-390.png",
"source": "docs/ui-modernisation/visual-correction/evidence/applications-selection/ordinary-en-light-390.png",
"runtime": "1246994cca9a3ba7cb89eb4605b3dc77b5d7c322",
"manifest": "docs/ui-modernisation/visual-correction/APPLICATION_SELECTION_SOURCE_MANIFEST.json",
"route": "/jobs",
"width": 390,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "9937bb7f328b1c0b719f46b54bf73f1cd0694c348ffe4899449d0ffcf5124d7f"
},
{
"target": "assets/dashboard-1440.png",
"source": "docs/ui-modernisation/visual-correction/evidence/dashboard/dashboard-en-light-1440.png",
"runtime": "a7d726738b74d13fb805f7bd4f5780bdf19110d0",
"manifest": "docs/ui-modernisation/visual-correction/DASHBOARD_DISCOVERY_SOURCE_MANIFEST.json",
"route": "/dashboard",
"width": 1440,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "c19a6b87686d32102db22fe61e4ed7b697f674774f45872d0bf2f2ac64066d80"
},
{
"target": "assets/dashboard-390.png",
"source": "docs/ui-modernisation/visual-correction/evidence/dashboard/dashboard-en-light-390.png",
"runtime": "a7d726738b74d13fb805f7bd4f5780bdf19110d0",
"manifest": "docs/ui-modernisation/visual-correction/DASHBOARD_DISCOVERY_SOURCE_MANIFEST.json",
"route": "/dashboard",
"width": 390,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "a6ae5fdaa0815e3785234ed3db9237cb3cd8418f5dbd5853b4cfc15de1442256"
},
{
"target": "assets/cv-editor-1440.png",
"source": "docs/ui-modernisation/visual-correction/evidence/career/cv-editor-en-light-1440.png",
"runtime": "b31ce6176bcb8f6a70d3e29ad4057b7cb74a1f53",
"manifest": "docs/ui-modernisation/visual-correction/CAREER_DOCUMENT_SOURCE_MANIFEST.json",
"route": "/career/builder/3",
"width": 1440,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "e90a1c32709d376417d9af8b00d8007d53aed102f5d017a4c55cdc7371e402bc"
},
{
"target": "assets/cv-editor-390.png",
"source": "docs/ui-modernisation/visual-correction/evidence/career/cv-editor-en-light-390.png",
"runtime": "b31ce6176bcb8f6a70d3e29ad4057b7cb74a1f53",
"manifest": "docs/ui-modernisation/visual-correction/CAREER_DOCUMENT_SOURCE_MANIFEST.json",
"route": "/career/builder/3",
"width": 390,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "b26258fe02e24f34b107739f0139391d766b3bf8a3e63a5864063e06f40a68e8"
},
{
"target": "assets/career-1440.png",
"source": "docs/ui-modernisation/visual-correction/evidence/career/overview-en-light-1440.png",
"runtime": "b31ce6176bcb8f6a70d3e29ad4057b7cb74a1f53",
"manifest": "docs/ui-modernisation/visual-correction/CAREER_DOCUMENT_SOURCE_MANIFEST.json",
"route": "/career",
"width": 1440,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "637f8dbc0cd01d4382dd409fc0b3870c40e69bbbcb2d6efbbb3e3c6c3b8a46d2"
},
{
"target": "assets/career-390.png",
"source": "docs/ui-modernisation/visual-correction/evidence/career/overview-en-light-390.png",
"runtime": "b31ce6176bcb8f6a70d3e29ad4057b7cb74a1f53",
"manifest": "docs/ui-modernisation/visual-correction/CAREER_DOCUMENT_SOURCE_MANIFEST.json",
"route": "/career",
"width": 390,
"locale": "en",
"theme": "light",
"fixture": "synthetic",
"sha256": "21f367b3848fae24dfe4abf4271c5c9c73fbf4c2a8f1aba5dd6faff61ed6cee9"
}
]
}
-558
View File
@@ -1,558 +0,0 @@
:root {
font-family: "Segoe UI",Arial,sans-serif;
line-height: 1.5;
color: #241b35;
background: #f7f6fa;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
a {
color: inherit;
text-underline-offset: .22em;
}
a:focus-visible {
outline: 3px solid currentColor;
outline-offset: 4px;
}
h1,h2,h3,p,figure {
margin: 0;
}
p {
max-width: 62ch;
}
img {
display: block;
width: 100%;
height: auto;
}
h1 {
font-size: clamp(2.8rem,6.6vw,7rem);
line-height: .99;
letter-spacing: -.065em;
font-weight: 760;
}
h2 {
font-size: clamp(1.85rem,3.4vw,3.3rem);
letter-spacing: -.04em;
line-height: 1.05;
}
h3 {
font-size: 1.18rem;
line-height: 1.25;
}
.wrap {
width: min(100% - 72px,1240px);
margin-inline: auto;
}
.review {
background: #241b35;
color: #fff;
font-size: .85rem;
}
.review .wrap {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
gap: .7rem;
flex-wrap: wrap;
padding-block: .35rem;
}
.top,.nav,.actions,.bottom {
display: flex;
align-items: center;
gap: 1.25rem;
flex-wrap: wrap;
}
.top,.bottom {
justify-content: space-between;
padding-block: 1.1rem;
}
.brand {
display: inline-flex;
align-items: center;
gap: .55rem;
text-decoration: none;
font-size: 1.2rem;
font-weight: 770;
}
.mark {
display: grid;
place-items: center;
width: 1.8rem;
height: 1.8rem;
border-radius: .5rem;
background: #704fa6;
color: #fff;
font-weight: 800;
}
.nav {
font-size: .9rem;
}
.nav a {
display: inline-flex;
align-items: center;
min-height: 44px;
}
.hero,.section {
padding-block: clamp(2.7rem,6vw,6rem);
}
.hero-copy {
display: grid;
gap: 1.35rem;
justify-items: start;
align-content: center;
}
.lede {
font-size: clamp(1.07rem,1.7vw,1.34rem);
max-width: 38ch;
}
.button {
display: inline-flex;
align-items: center;
min-height: 44px;
border: 1px solid transparent;
padding: .55rem 1rem;
border-radius: .45rem;
text-decoration: none;
font-weight: 700;
background: var(--accent,#563282);
color: white;
}
.button.secondary {
background: none;
border-color: currentColor;
color: inherit;
}
.product {
min-width: 0;
}
.product a {
display: block;
overflow: hidden;
border: 1px solid #aaa0b9;
border-radius: .65rem;
background: white;
}
.product img {
aspect-ratio: 1.7;
object-fit: cover;
object-position: top;
}
.product figcaption,.fine {
font-size: .88rem;
margin-top: .55rem;
}
.product .phone {
display: none;
}
.eyebrow {
font-size: .9rem;
font-weight: 700;
}
.stack {
display: grid;
gap: 1rem;
}
.section h2 {
max-width: 20ch;
}
.grid,.steps,.plans {
display: grid;
gap: 1rem;
grid-template-columns: repeat(3,minmax(0,1fr));
margin-top: 1.75rem;
}
.steps {
padding-left: 1.6rem;
}
.steps li,.feature,.plan {
padding: 1rem;
border-top: 1px solid currentColor;
}
.steps li::marker {
font-weight: 750;
}
.feature p,.plan p,.steps p {
margin-top: .45rem;
}
.plans {
grid-template-columns: repeat(2,minmax(0,1fr));
}
.plan {
border: 1px solid currentColor;
border-radius: .55rem;
}
.section-alt {
background: var(--soft,#eee8f6);
}
.foot {
border-top: 1px solid #aaa0b9;
}
.bottom a {
display: inline-flex;
min-height: 44px;
align-items: center;
}
.skip {
position: absolute;
left: 1rem;
top: -5rem;
z-index: 10;
background: white;
color: #241b35;
padding: .7rem;
}
.skip:focus {
top: .5rem;
}
.minimal {
--accent: #543586;
--soft: #edf0f8;
background: #fcfcfe;
color: #25213b;
}
.minimal .hero-inner {
display: grid;
grid-template-columns: minmax(0,.86fr) minmax(0,1.14fr);
gap: clamp(2rem,5vw,5rem);
align-items: center;
}
.minimal h1 {
max-width: 10ch;
}
.minimal .hero {
border-top: 1px solid #dad6e7;
}
.minimal .product {
box-shadow: 0 28px 70px -50px #352450;
}
.premium {
--accent: #6c3da1;
--soft: #ece5f8;
color: #26163c;
background: #fbf9ff;
}
.premium .hero {
background: radial-gradient(ellipse at 50% 100%,#61428c 0,#32214d 47%,#241733 78%);
color: #fff;
padding-bottom: 3.5rem;
}
.premium .hero-copy {
text-align: center;
justify-items: center;
}
.premium h1 {
max-width: 13ch;
}
.premium .hero .button {
background: #f4ebff;
color: #301b48;
}
.premium .hero .secondary {
color: white;
background: none;
}
.premium .product {
max-width: 930px;
margin: 2rem auto 0;
}
.premium .product a {
border: 9px solid #9073ba;
border-radius: 1.25rem;
}
.premium .feature {
border: 1px solid #c4addd;
border-radius: .8rem;
padding: 1.35rem;
}
.human {
--accent: #285247;
--soft: #e4eee6;
color: #183a31;
background: #fbfaf4;
}
.human .mark {
background: #285247;
}
.human .hero {
background: #e4eee6;
}
.human .hero-inner {
display: grid;
grid-template-columns: minmax(0,.95fr) minmax(0,1.05fr);
gap: clamp(2rem,4vw,4rem);
align-items: center;
}
.human h1 {
font-family: Georgia,serif;
font-weight: 700;
letter-spacing: -.055em;
max-width: 12ch;
}
.human .art {
border-radius: 3rem .4rem 3rem .4rem;
overflow: hidden;
}
.human .showcase {
display: grid;
grid-template-columns: minmax(0,.75fr) minmax(0,1.25fr);
gap: 2rem;
align-items: center;
}
.human .feature {
padding-inline: 0;
}
.editorial {
--accent: #183761;
--soft: #e9eff8;
background: #f8fafc;
color: #183761;
}
.editorial .mark {
background: #183761;
}
.editorial .hero {
padding-top: 2rem;
}
.editorial .hero-inner {
display: grid;
grid-template-columns: minmax(0,1.1fr) minmax(0,.9fr);
gap: 2rem;
border-top: 3px solid currentColor;
padding-top: 1.5rem;
align-items: end;
}
.editorial h1 {
font-family: Georgia,serif;
font-size: clamp(3rem,8.2vw,9.2rem);
font-weight: 600;
max-width: 10ch;
}
.editorial .product {
padding-top: 1.5rem;
border-top: 1px solid currentColor;
}
.editorial .product a {
border-radius: 0;
}
.editorial .section .wrap {
border-top: 2px solid currentColor;
padding-top: 1rem;
}
.editorial .grid {
grid-template-columns: 1fr 1fr;
}
.editorial .feature:first-child {
grid-column: 1/-1;
}
.gallery {
padding-block: 3rem;
}
.gallery h1 {
font-size: clamp(2.5rem,5vw,5rem);
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(2,minmax(0,1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.gallery-card {
background: #fff;
padding: 1rem;
border: 1px solid #aaa0b9;
border-radius: .6rem;
min-width: 0;
}
.gallery-card img {
aspect-ratio: 1.6;
object-fit: cover;
object-position: top;
border: 1px solid #ddd4ea;
}
.gallery-card h2 {
font-size: 1.6rem;
margin-block: .8rem .4rem;
}
.gallery-card .nav {
gap: .9rem;
margin-top: .5rem;
}
@media(max-width:780px) {
.wrap {
width: calc(100% - 36px);
}
.minimal .hero-inner,.human .hero-inner,.human .showcase,.editorial .hero-inner {
grid-template-columns: 1fr;
gap: 1.6rem;
}
.top {
gap: .45rem;
padding-block: .55rem;
}
.nav {
gap: .8rem;
}
.top .nav {
width: 100%;
justify-content: space-between;
}
.product a {
max-width: 380px;
}
.product img {
aspect-ratio: 4/5;
}
.product .desktop {
display: none;
}
.product .phone {
display: block;
}
.premium .product a {
margin-inline: auto;
}
.premium .product figcaption {
text-align: center;
margin-inline: auto;
}
.premium .hero-copy {
text-align: left;
justify-items: start;
}
.human .art {
max-width: 580px;
}
.editorial .hero-inner {
align-items: start;
}
.editorial .product {
padding-top: 1rem;
}
.gallery-grid {
grid-template-columns: 1fr;
}
}
@media(max-width:520px) {
.grid,.steps,.plans,.editorial .grid {
grid-template-columns: 1fr;
gap: .55rem;
}
.editorial .feature:first-child {
grid-column: auto;
}
.top .nav {
justify-content: flex-start;
}
.review .wrap {
padding-block: .45rem;
}
.actions {
gap: .65rem;
}
.bottom {
align-items: flex-start;
}
.gallery {
padding-block: 2rem;
}
.premium .hero .product a {
border-width: 6px;
}
}
.review a {
display: inline-flex;
align-items: center;
min-height: 32px;
}
-68
View File
@@ -1,68 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>D · Modern Editorial | Jobjakt design review</title>
<link rel="stylesheet" href="concepts.css">
</head>
<body class="editorial">
<a class="skip" href="#main">Skip to content</a>
<div class="review"><div class="wrap"><span>Concept D · Modern Editorial · Design review</span><a href="index.html">Compare concepts</a></div></div>
<header class="top wrap">
<a class="brand" href="#main"><span class="mark" aria-hidden="true">✓</span>Jobjakt</a>
<nav class="nav" aria-label="Main"><a href="#features">Features</a><a href="#steps">How it works</a><a href="#plans">Plans</a><a href="#registration">Sign in</a></nav>
</header>
<main id="main">
<section class="hero">
<div class="hero-inner wrap">
<div class="hero-copy">
<h1>Put your experience to work.</h1>
<p class="lede">A practical workspace for the applications, CVs and career details behind your next role.</p>
<div class="actions"><a class="button" href="#registration">Create an account</a><a class="button secondary" href="#features">Explore Jobjakt</a></div>
</div>
<figure class="product">
<a class="desktop" href="assets/career-1440.png" aria-label="Open full desktop product screenshot"><img src="assets/career-1440.png" alt="Jobjakt Career Workspace with sections and an import awaiting review"></a>
<a class="phone" href="assets/career-390.png" aria-label="Open full phone product screenshot"><img src="assets/career-390.png" alt="Jobjakt Career Workspace with sections and an import awaiting review"></a>
<figcaption>Development screenshot with synthetic records. Select it for the full capture.</figcaption>
</figure>
</div>
</section>
<section class="section" id="features">
<div class="wrap">
<div class="stack">
<div class="stack"><p class="eyebrow">The Jobjakt guide</p><h2>The work behind a job search.</h2></div>
</div>
<div class="grid">
<article class="feature"><h3>Track applications</h3><p>Save opportunities, review your progress and keep a record of what comes next.</p></article>
<article class="feature"><h3>Keep career details together</h3><p>Build a Career Profile with experience you can refer to when preparing a CV.</p></article>
<article class="feature"><h3>Prepare a CV</h3><p>Review a document in the editor before deciding what to send.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="steps">
<div class="wrap"><h2>How it works</h2>
<ol class="steps">
<li><h3>Add your experience</h3><p>Start with your Career Profile and record the details you want to reuse.</p></li>
<li><h3>Prepare your CV</h3><p>Put a document together and review it before exporting.</p></li>
<li><h3>Keep track of applications</h3><p>Record the roles you are pursuing, the status and your next step.</p></li>
</ol>
</div>
</section>
<section class="section" id="plans">
<div class="wrap"><h2>Plans and pricing</h2>
<div class="plans">
<article class="plan"><h3>Free</h3><p>Plan layout preview. Entitlements and availability require release verification.</p></article>
<article class="plan"><h3>Pro</h3><p>Plan layout preview. Price, billing terms and eligibility require approval.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="registration">
<div class="wrap stack"><h2>Ready to start your next search?</h2><p>Design preview. Registration and sign-in are inactive in this mockup.</p></div>
</section>
</main>
<footer class="foot"><div class="bottom wrap"><span class="brand"><span class="mark" aria-hidden="true">✓</span>Jobjakt</span><span>Privacy and terms links pending approval</span><a href="index.html">Compare concepts</a></div></footer>
</body>
</html>
-68
View File
@@ -1,68 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>C · Friendly and Human | Jobjakt design review</title>
<link rel="stylesheet" href="concepts.css">
</head>
<body class="human">
<a class="skip" href="#main">Skip to content</a>
<div class="review"><div class="wrap"><span>Concept C · Friendly and Human · Design review</span><a href="index.html">Compare concepts</a></div></div>
<header class="top wrap">
<a class="brand" href="#main"><span class="mark" aria-hidden="true">✓</span>Jobjakt</a>
<nav class="nav" aria-label="Main"><a href="#features">Features</a><a href="#steps">How it works</a><a href="#plans">Plans</a><a href="#registration">Sign in</a></nav>
</header>
<main id="main">
<section class="hero">
<div class="hero-inner wrap">
<div class="hero-copy">
<h1>A little more room for your next chapter.</h1>
<p class="lede">Applying to a school, clinic, workshop or somewhere entirely new? Put your experience to work with a space to organise the search.</p>
<div class="actions"><a class="button" href="#registration">Create an account</a><a class="button secondary" href="#features">Explore Jobjakt</a></div>
</div>
<figure class="art"><img src="professions.svg" alt="Illustration of three people working in teaching, healthcare and electrical trades" width="720" height="525"></figure>
</div>
</section>
<section class="section" id="features">
<div class="wrap">
<div class="showcase">
<div class="stack"><p class="eyebrow">From experience to application</p><h2>Bring your experience with you.</h2></div>
<figure class="product">
<a class="desktop" href="assets/cv-editor-1440.png" aria-label="Open full desktop product screenshot"><img src="assets/cv-editor-1440.png" alt="Jobjakt CV Builder open on a synthetic teacher CV"></a>
<a class="phone" href="assets/cv-editor-390.png" aria-label="Open full phone product screenshot"><img src="assets/cv-editor-390.png" alt="Jobjakt CV Builder open on a synthetic teacher CV"></a>
<figcaption>Development screenshot with synthetic records. Select it for the full capture.</figcaption>
</figure>
</div>
<div class="grid">
<article class="feature"><h3>Track applications</h3><p>Save opportunities, review your progress and keep a record of what comes next.</p></article>
<article class="feature"><h3>Keep career details together</h3><p>Build a Career Profile with experience you can refer to when preparing a CV.</p></article>
<article class="feature"><h3>Prepare a CV</h3><p>Review a document in the editor before deciding what to send.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="steps">
<div class="wrap"><h2>How it works</h2>
<ol class="steps">
<li><h3>Add your experience</h3><p>Start with your Career Profile and record the details you want to reuse.</p></li>
<li><h3>Prepare your CV</h3><p>Put a document together and review it before exporting.</p></li>
<li><h3>Keep track of applications</h3><p>Record the roles you are pursuing, the status and your next step.</p></li>
</ol>
</div>
</section>
<section class="section" id="plans">
<div class="wrap"><h2>Plans and pricing</h2>
<div class="plans">
<article class="plan"><h3>Free</h3><p>Plan layout preview. Entitlements and availability require release verification.</p></article>
<article class="plan"><h3>Pro</h3><p>Plan layout preview. Price, billing terms and eligibility require approval.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="registration">
<div class="wrap stack"><h2>Ready to start your next search?</h2><p>Design preview. Registration and sign-in are inactive in this mockup.</p></div>
</section>
</main>
<footer class="foot"><div class="bottom wrap"><span class="brand"><span class="mark" aria-hidden="true">✓</span>Jobjakt</span><span>Privacy and terms links pending approval</span><a href="index.html">Compare concepts</a></div></footer>
</body>
</html>
-44
View File
@@ -1,44 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>Homepage concepts | Jobjakt design review</title>
<link rel="stylesheet" href="concepts.css">
</head>
<body class="">
<a class="skip" href="#main">Skip to content</a>
<main id="main" class="gallery wrap">
<h1>Homepage concepts</h1>
<p class="lede">Compare four approaches and choose any combination of hero, navigation, typography, imagery and layout.</p>
<p class="fine">Static design review. The product captures show synthetic development fixtures.</p>
<div class="gallery-grid">
<article class="gallery-card">
<a href="minimal.html" aria-label="Open concept A: Modern Minimalist"><img src="renders/minimal-desktop.png" alt="Desktop mockup of Modern Minimalist"></a>
<h2>A · Modern Minimalist</h2>
<p>A split hero with clear registration and a genuine application preview.</p>
<div class="nav"><a href="minimal.html">Responsive page</a><a href="renders/minimal-desktop.png">Full desktop</a><a href="renders/minimal-phone.png">Full phone</a></div>
</article>
<article class="gallery-card">
<a href="premium.html" aria-label="Open concept B: Premium SaaS"><img src="renders/premium-desktop.png" alt="Desktop mockup of Premium SaaS"></a>
<h2>B · Premium SaaS</h2>
<p>A theatre-style dashboard preview below a centred violet hero.</p>
<div class="nav"><a href="premium.html">Responsive page</a><a href="renders/premium-desktop.png">Full desktop</a><a href="renders/premium-phone.png">Full phone</a></div>
</article>
<article class="gallery-card">
<a href="human.html" aria-label="Open concept C: Friendly and Human"><img src="renders/human-desktop.png" alt="Desktop mockup of Friendly and Human"></a>
<h2>C · Friendly and Human</h2>
<p>Illustrated professions lead into a genuine teacher CV editing example.</p>
<div class="nav"><a href="human.html">Responsive page</a><a href="renders/human-desktop.png">Full desktop</a><a href="renders/human-phone.png">Full phone</a></div>
</article>
<article class="gallery-card">
<a href="editorial.html" aria-label="Open concept D: Modern Editorial"><img src="renders/editorial-desktop.png" alt="Desktop mockup of Modern Editorial"></a>
<h2>D · Modern Editorial</h2>
<p>A typographic masthead and an asymmetric Career Workspace showcase.</p>
<div class="nav"><a href="editorial.html">Responsive page</a><a href="renders/editorial-desktop.png">Full desktop</a><a href="renders/editorial-phone.png">Full phone</a></div>
</article>
</div>
</main>
</body>
</html>
-68
View File
@@ -1,68 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>A · Modern Minimalist | Jobjakt design review</title>
<link rel="stylesheet" href="concepts.css">
</head>
<body class="minimal">
<a class="skip" href="#main">Skip to content</a>
<div class="review"><div class="wrap"><span>Concept A · Modern Minimalist · Design review</span><a href="index.html">Compare concepts</a></div></div>
<header class="top wrap">
<a class="brand" href="#main"><span class="mark" aria-hidden="true">✓</span>Jobjakt</a>
<nav class="nav" aria-label="Main"><a href="#features">Features</a><a href="#steps">How it works</a><a href="#plans">Plans</a><a href="#registration">Sign in</a></nav>
</header>
<main id="main">
<section class="hero">
<div class="hero-inner wrap">
<div class="hero-copy">
<h1>Your job search, organised.</h1>
<p class="lede">Keep applications, career details and CVs together, so you can see what needs your attention next.</p>
<div class="actions"><a class="button" href="#registration">Create an account</a><a class="button secondary" href="#features">Explore Jobjakt</a></div>
</div>
<figure class="product">
<a class="desktop" href="assets/applications-1440.png" aria-label="Open full desktop product screenshot"><img src="assets/applications-1440.png" alt="Jobjakt application table with a saved community nurse role"></a>
<a class="phone" href="assets/applications-390.png" aria-label="Open full phone product screenshot"><img src="assets/applications-390.png" alt="Jobjakt application table with a saved community nurse role"></a>
<figcaption>Development screenshot with synthetic records. Select it for the full capture.</figcaption>
</figure>
</div>
</section>
<section class="section" id="features">
<div class="wrap">
<div class="stack">
<div class="stack"><p class="eyebrow">A workspace for the whole job search</p><h2>Keep the next step in view.</h2></div>
</div>
<div class="grid">
<article class="feature"><h3>Track applications</h3><p>Save opportunities, review your progress and keep a record of what comes next.</p></article>
<article class="feature"><h3>Keep career details together</h3><p>Build a Career Profile with experience you can refer to when preparing a CV.</p></article>
<article class="feature"><h3>Prepare a CV</h3><p>Review a document in the editor before deciding what to send.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="steps">
<div class="wrap"><h2>How it works</h2>
<ol class="steps">
<li><h3>Add your experience</h3><p>Start with your Career Profile and record the details you want to reuse.</p></li>
<li><h3>Prepare your CV</h3><p>Put a document together and review it before exporting.</p></li>
<li><h3>Keep track of applications</h3><p>Record the roles you are pursuing, the status and your next step.</p></li>
</ol>
</div>
</section>
<section class="section" id="plans">
<div class="wrap"><h2>Plans and pricing</h2>
<div class="plans">
<article class="plan"><h3>Free</h3><p>Plan layout preview. Entitlements and availability require release verification.</p></article>
<article class="plan"><h3>Pro</h3><p>Plan layout preview. Price, billing terms and eligibility require approval.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="registration">
<div class="wrap stack"><h2>Ready to start your next search?</h2><p>Design preview. Registration and sign-in are inactive in this mockup.</p></div>
</section>
</main>
<footer class="foot"><div class="bottom wrap"><span class="brand"><span class="mark" aria-hidden="true">✓</span>Jobjakt</span><span>Privacy and terms links pending approval</span><a href="index.html">Compare concepts</a></div></footer>
</body>
</html>
-68
View File
@@ -1,68 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>B · Premium SaaS | Jobjakt design review</title>
<link rel="stylesheet" href="concepts.css">
</head>
<body class="premium">
<a class="skip" href="#main">Skip to content</a>
<div class="review"><div class="wrap"><span>Concept B · Premium SaaS · Design review</span><a href="index.html">Compare concepts</a></div></div>
<header class="top wrap">
<a class="brand" href="#main"><span class="mark" aria-hidden="true">✓</span>Jobjakt</a>
<nav class="nav" aria-label="Main"><a href="#features">Features</a><a href="#steps">How it works</a><a href="#plans">Plans</a><a href="#registration">Sign in</a></nav>
</header>
<main id="main">
<section class="hero">
<div class="hero-inner wrap">
<div class="hero-copy">
<h1>Make the next move with a plan.</h1>
<p class="lede">Save the roles you want to pursue, prepare documents and see your applications move forward.</p>
<div class="actions"><a class="button" href="#registration">Create an account</a><a class="button secondary" href="#features">Explore Jobjakt</a></div>
</div>
<figure class="product">
<a class="desktop" href="assets/dashboard-1440.png" aria-label="Open full desktop product screenshot"><img src="assets/dashboard-1440.png" alt="Jobjakt dashboard showing synthetic nursing, teaching and engineering applications"></a>
<a class="phone" href="assets/dashboard-390.png" aria-label="Open full phone product screenshot"><img src="assets/dashboard-390.png" alt="Jobjakt dashboard showing synthetic nursing, teaching and engineering applications"></a>
<figcaption>Development screenshot with synthetic records. Select it for the full capture.</figcaption>
</figure>
</div>
</section>
<section class="section" id="features">
<div class="wrap">
<div class="stack">
<div class="stack"><p class="eyebrow">The workspace</p><h2>See what needs your attention.</h2></div>
</div>
<div class="grid">
<article class="feature"><h3>Track applications</h3><p>Save opportunities, review your progress and keep a record of what comes next.</p></article>
<article class="feature"><h3>Keep career details together</h3><p>Build a Career Profile with experience you can refer to when preparing a CV.</p></article>
<article class="feature"><h3>Prepare a CV</h3><p>Review a document in the editor before deciding what to send.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="steps">
<div class="wrap"><h2>How it works</h2>
<ol class="steps">
<li><h3>Add your experience</h3><p>Start with your Career Profile and record the details you want to reuse.</p></li>
<li><h3>Prepare your CV</h3><p>Put a document together and review it before exporting.</p></li>
<li><h3>Keep track of applications</h3><p>Record the roles you are pursuing, the status and your next step.</p></li>
</ol>
</div>
</section>
<section class="section" id="plans">
<div class="wrap"><h2>Plans and pricing</h2>
<div class="plans">
<article class="plan"><h3>Free</h3><p>Plan layout preview. Entitlements and availability require release verification.</p></article>
<article class="plan"><h3>Pro</h3><p>Plan layout preview. Price, billing terms and eligibility require approval.</p></article>
</div>
</div>
</section>
<section class="section section-alt" id="registration">
<div class="wrap stack"><h2>Ready to start your next search?</h2><p>Design preview. Registration and sign-in are inactive in this mockup.</p></div>
</section>
</main>
<footer class="foot"><div class="bottom wrap"><span class="brand"><span class="mark" aria-hidden="true">✓</span>Jobjakt</span><span>Privacy and terms links pending approval</span><a href="index.html">Compare concepts</a></div></footer>
</body>
</html>
-40
View File
@@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 525">
<rect width="720" height="525" fill="#d3e2d7"/>
<path d="M0 340C115 300 200 364 327 337S550 276 720 324V525H0Z" fill="#b3cebf"/>
<circle cx="572" cy="121" r="71" fill="#f6db97"/>
<path d="M40 102h157v105H40z" fill="#48776b" stroke="#f2f1e6" stroke-width="8"/>
<path d="M60 129h104m-104 24h81m-81 24h38" stroke="#f2f1e6" stroke-width="4" stroke-linecap="round"/>
<path d="M478 114h114v126H478z" fill="none" stroke="#eef4ed" stroke-width="4"/>
<path d="M494 145h28v-24h20v24h28v19h-28v25h-20v-25h-28z" fill="#eef4ed"/>
<path d="M647 195l-23 42h18l-8 29 32-43h-21l13-28z" fill="#36595a"/>
<ellipse cx="357" cy="467" rx="312" ry="29" fill="#91b7a5"/>
<!-- Teacher, with an open book. -->
<path d="M67 458l15-94c8-48 31-76 63-83l64 1c35 9 59 38 66 84l13 92z" fill="#bb674f"/>
<path d="M158 262h43v50c-13 14-31 14-43 0z" fill="#a65d48"/>
<path d="M125 218c0-41 22-68 55-68 36 0 60 32 58 68l-11 52c-12 21-29 33-48 33-31 0-53-24-54-57z" fill="#c87e5a"/>
<path d="M121 225c-18-44 8-85 56-88 38-3 66 22 65 64-28 2-53-12-68-32-10 22-29 35-53 37z" fill="#392638"/>
<path d="M116 370l61 21 67-21-6 72-61 22-58-21z" fill="#f7e4ab"/>
<path d="M177 391v73m-45-59 32 11m23-1 42-12" stroke="#aa8551" stroke-width="3"/>
<!-- Nurse, with scrubs and badge. -->
<path d="M240 461l20-103c11-50 36-82 76-91l65 1c44 9 74 41 82 90l15 103z" fill="#4a72a4"/>
<path d="M337 248h49v52l-24 36-25-36z" fill="#855042"/>
<path d="M302 205c0-39 25-65 60-65 36 0 62 27 61 65l-7 44c-12 30-31 44-55 44-27 0-47-17-56-44z" fill="#a86854"/>
<path d="M298 219c-17-57 15-94 62-94 47 0 74 28 67 73-26-6-45-19-58-41-19 24-42 36-71 41z" fill="#282c3b"/>
<path d="M324 276l38 52 38-52" fill="none" stroke="#e6ecf7" stroke-width="5" stroke-linecap="round"/>
<rect x="385" y="353" width="45" height="30" rx="5" fill="#f4f0e0"/>
<path d="M396 367h23" stroke="#4a72a4" stroke-width="3" stroke-linecap="round"/>
<!-- Tradesperson, with a work jacket and hard hat. -->
<path d="M441 460l13-91c10-52 35-81 73-90l64 1c36 8 66 38 75 89l13 91z" fill="#36595a"/>
<path d="M536 262h48v51l-25 21-23-21z" fill="#b77957"/>
<path d="M503 222c0-36 25-65 57-65 33 0 59 29 59 65l-6 38c-12 28-28 41-54 41-24 0-44-15-54-41z" fill="#d39873"/>
<path d="M500 211c-2-40 21-65 60-65 37 0 62 26 61 65z" fill="#e9b85c"/>
<path d="M489 213h143m-72-65v51" stroke="#9d6d34" stroke-width="5" stroke-linecap="round"/>
<path d="M514 311l-20 151m105-151 25 151" stroke="#e8bd6e" stroke-width="15"/>
<path d="M554 356h12l-8 17h12l-21 25 7-20h-12z" fill="#f6db97"/>
<g fill="#30283a">
<circle cx="162" cy="229" r="3.2"/><circle cx="204" cy="229" r="3.2"/>
<circle cx="341" cy="219" r="3.2"/><circle cx="381" cy="219" r="3.2"/>
<circle cx="539" cy="231" r="3.2"/><circle cx="580" cy="231" r="3.2"/>
</g>
<path d="M175 251q8 6 17 0m162-8q8 6 17 0m181 12q8 6 17 0" fill="none" stroke="#663d42" stroke-width="3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

@@ -1,146 +0,0 @@
{
"checks": [
{
"page": "minimal",
"width": 1440,
"height": 1000,
"passed": true,
"axeViolations": []
},
{
"page": "minimal",
"width": 768,
"height": 1024,
"passed": true,
"axeViolations": []
},
{
"page": "minimal",
"width": 390,
"height": 844,
"passed": true,
"axeViolations": []
},
{
"page": "minimal",
"width": 320,
"height": 720,
"passed": true,
"axeViolations": []
},
{
"page": "premium",
"width": 1440,
"height": 1000,
"passed": true,
"axeViolations": []
},
{
"page": "premium",
"width": 768,
"height": 1024,
"passed": true,
"axeViolations": []
},
{
"page": "premium",
"width": 390,
"height": 844,
"passed": true,
"axeViolations": []
},
{
"page": "premium",
"width": 320,
"height": 720,
"passed": true,
"axeViolations": []
},
{
"page": "human",
"width": 1440,
"height": 1000,
"passed": true,
"axeViolations": []
},
{
"page": "human",
"width": 768,
"height": 1024,
"passed": true,
"axeViolations": []
},
{
"page": "human",
"width": 390,
"height": 844,
"passed": true,
"axeViolations": []
},
{
"page": "human",
"width": 320,
"height": 720,
"passed": true,
"axeViolations": []
},
{
"page": "editorial",
"width": 1440,
"height": 1000,
"passed": true,
"axeViolations": []
},
{
"page": "editorial",
"width": 768,
"height": 1024,
"passed": true,
"axeViolations": []
},
{
"page": "editorial",
"width": 390,
"height": 844,
"passed": true,
"axeViolations": []
},
{
"page": "editorial",
"width": 320,
"height": 720,
"passed": true,
"axeViolations": []
},
{
"page": "index",
"width": 1440,
"height": 1000,
"passed": true,
"axeViolations": []
},
{
"page": "index",
"width": 768,
"height": 1024,
"passed": true,
"axeViolations": []
},
{
"page": "index",
"width": 390,
"height": 844,
"passed": true,
"axeViolations": []
},
{
"page": "index",
"width": 320,
"height": 720,
"passed": true,
"axeViolations": []
}
],
"externalRequests": [],
"device": "Chromium desktop emulation"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

@@ -0,0 +1,22 @@
# Preliminary release discovery
2026-11-02. Branch `planning/release-readiness-discovery`; accepted conditional development baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Stage 1 IN_PROGRESS. No release candidate nominated. Production NO-GO. This report records source inspection and a bounded offline check, not full release qualification.
| Component / inspected owner | Dependency / failure boundary | Required release evidence |
| --- | --- | --- |
| `job-tracker-ui/package.json`, `Dockerfile`, `nginx.conf.template` | Next static export, React/MUI and React Router; nginx serves `out`, proxies `/api/`, `/health`, `/ready`; configured host and forwarded-header handling | Exact build/image, browser journeys and deployed proxy/TLS identity |
| `JobTrackerApi/Program.cs`, `JobTrackerApi.csproj` | net9.0, EF 9.0.14, Pomelo 9.0.0; authentication/authorization then rate limiting/session middleware and controllers | Endpoint-by-endpoint ownership/session/CSRF/error tests; source registration alone is not security proof |
| `JobTrackerApi/Data/DatabaseConfiguration.cs` | Defaults to MariaDB; missing connection throws; SQLite requires explicit transitional selection | Actual MariaDB migration/upgrade/rollback and preservation evidence; retain legacy paths |
| `docker-compose.database.yml` | Development persistent volume and separate integration tmpfs service; MariaDB 12.2.2 pinned digest; loopback ports | Isolated provider tests; no claim of equality with production |
| Career/CV domain and PDF | Program registers CareerProfileService, ApplicationAssetsService, CV/letter renderers and Playwright exporter | Canonical facts, null-as-inherit, revisions, historical artifacts and physical print remain separate contracts |
| Background services | Program registers backup, rules, reminders, saved search, export, enrichment, summarizer probe, AI operation, retention, email recovery and account deletion workers | Verify configuration, failure recovery and tenant isolation for enabled workers; registration is not runtime activation |
| Auth/mail/provider integrations | Architecture documents Google/Microsoft identity, Gmail/Graph/IMAP/SMTP, NAV, optional translation, local/external AI, Stripe | Synthetic local tests separate from licensed/provider/account delivery and consent |
| `.gitea/workflows/ci-deploy.yml` | Test pipeline; deployment limited by source condition to push on `refs/heads/main` | Fresh hosted artifact/commit/runner evidence; development branch push is not hosted qualification |
| `.gitea/workflows/mariadb-integration.yml` | Isolated runner label, explicit initial exit-1 isolation gate | Operator isolation evidence and authorised workflow review before enabling; do not remove gate during discovery |
| Existing backup/deployment runbooks | Database, attachments/CV artifacts, keys and deletion tombstones; code rollback differs from data restore | Disposable restore with independent content/decryption/deletion checks; approved RPO/RTO and failure rehearsal |
Public boundary currently inspected: nginx host routing, `/health`, `/ready`, `/api/`; API controllers are mapped but a complete anonymous/authenticated endpoint census is still PENDING. External mail/job/AI requests, uploaded documents, browser credentials, database records and stored artifacts are separate trust boundaries. No live endpoints, Docker engines, databases, credentials, models or production services were accessed.
Local development, test and production are not interchangeable: integration uses a generated project and disposable service; production needs the external network, server-side environment, actual ingress and operator controls. Current architecture/runbooks describe these differences; exact intended staging/production configuration and image identity remain unverified.
The latest handoff's source-bound PDF metric/visual approvals supersede the older acceptance paragraph describing unchanged S01 coordinate failures. They are preserved for the approved development scope. Physical print, device, AT and language review remain open. No export was rerun because no renderer/font/fixture/application source changed.
@@ -0,0 +1,11 @@
# Discovery baseline and actual checks
Date 2026-11-02. Source `5cb903b989742b7a96cb22ddf4c0e24993b18980`; application freeze `ba04d817fed26d659bfa2a56b089d6cac851dc34`. No accepted post-roadmap release candidate, build identifier or release image exists in this discovery checkpoint.
Fresh check: `rtk proxy python scripts/test-mariadb-integration.py` exited 0; **5 tests passed** in 0.030 seconds. This existing offline suite mocks subprocess execution. The printed cleanup failure/exit 17 and setup error/exit 1 are intentional negative-test outcomes asserted by the suite, not a real container failure. No MariaDB/Docker execution occurred. This proves five runner guards, not database behavior, hosted isolation or a full regression.
Read-only source checks confirmed the MariaDB default/missing-connection fail-closed behavior and the hosted workflow's explicit isolation blocker. Refer to SYSTEM_INVENTORY for inspected paths. `git diff --check` is a documentation hygiene check, not product qualification.
Historical test totals, PDF exports, security scans and provider runs remain attributed to their original evidence. They are not fresh runs here. `docs/automation/handoffs/pre-phase10/FINAL_DEVELOPMENT_ENTRY.md` records the latest approved unchanged-source applicability and narrow PDF metric/visual closure. Direct chat does not adopt or activate its historical autonomous-controller instructions.
NOT RUN: full backend/frontend/browser/Python/provider suites, builds, container validation, security scan/advisory refresh, performance, native review, deployment smoke, restore or rollback rehearsal. These require the actual release-stage source/environment and their own applicable gates. Discovery does not mark any mandatory release gate PASS merely because historical tests exist.
@@ -0,0 +1,23 @@
# Release discovery · endpoint and environment census
Source baseline `f19e7ebf4aeae350b43fa84cf115c393030280f0` on `codex/release-backup-parser`. Read-only source census. The 41 unique controller route prefixes are grouped below. Controller and action attributes, fallback authorisation, proxy policy, and runtime responses are separate checks. Repeat endpoint-level runtime verification on the accepted release candidate.
| Surface | Route owners under `/api` | Qualification owner and required failure cases |
| --- | --- | --- |
| Identity and accounts | `auth`, `auth/2fa`, `auth/sessions`, `account-lifecycle`, `export` | Auth/session, account lifecycle and export tests. Cover invalid/expired/revoked session, CSRF for cookie writes, rate limits, recent sign-in, owner isolation and final deletion policy. |
| Job tracking and discovery | `jobapplications`, `jobapplications/{jobId:int}` and its `checklist`, `focus-plan`, `workspace`, `ai` paths, `calendar`, `companies`, `jobimport`, `job-discovery` | Ownership, RD21, application/workspace, calendar and import/provider tests. Cover foreign/missing IDs, stale writes, parser/source denial and documented provider rights. |
| Career and files | `career/evidence`, `career/profile`, `profile-cv`, `cv`, `documents`, `attachments`, `public-cv`, `backup` | CV revision, import, document, public CV and backup tests. Cover frozen revisions, public opt-in and withdrawal, private downloads, upload limits, physical renderer qualification and scoped restoration. |
| Mail | `correspondence`, `email`, `email/drafts`, `gmail`, `imap`, `microsoft-graph` | Email ownership, Gmail, provider and draft tests. Verify OAuth state, consent/expiry, mailbox ownership, idempotent send, uncertain outcome recovery and real delivery separately. |
| Personal processing | `ai/settings`, `ai/usage`, `notification-settings`, `notifications`, `operations`, `rules` | Tenant/worker/AI tests. Verify entitlement and account isolation, no model start by default, queue and lease recovery, and redacted failures. |
| Administration and billing | `admin/audit`, `admin/processing`, `admin/system`, `users`, `billing`, `client-errors` | AdminProcessing, Users, AuthAndSystem, Billing and ClientErrors tests. Cover role denial, minimisation, signed webhooks, safe input and rate/size controls. Client error intake relies on the configured fallback auth policy, so verify both supported configurations. |
Outside the controllers, `GET /health` reports process/build liveness and `GET /ready` checks database connectivity with a terse 503 on failure. They cannot stand in for authenticated application smoke. OpenAPI is mapped only in Development. Explicit anonymous controller paths include auth configuration and entry/recovery flows, the 2FA challenge, provider callbacks, Stripe webhook and opted-in public CV HTML/PDF. They still need route-specific proof of token, signature, consent or data scope as applicable. Public CV HTML carries a noindex header and the PDF action has a named rate limit. Source inspection does not establish actual ingress controls.
| Environment | Source contract | Evidence required at the gate |
| --- | --- | --- |
| Local dev | Development compose has backend, frontend and optional local AI service. Development MariaDB uses a persistent dev volume. | Confirm isolated config, version and selected service flags. Keep developer data and credentials out of shared artifacts. |
| Disposable integration | Database compose pins MariaDB 12.2.2 by digest and declares a separate test service. | Prove ephemeral storage, port and project isolation, migrations and cleanup. Keep the hosted isolation gate until operator evidence exists. |
| Staging and recovery rehearsal | The release procedure requires an isolated, approved target. No running target or staging identity was inspected here. | Freeze image/config/migration IDs, scope credentials and network, use synthetic or approved sanitised data, and prove rollback/restore and alert behaviour. |
| Production | Production compose separates frontend, backend, AI service and Ollama. Exact deployed versions, ingress, mounted data and service activation are external evidence. | Verify proxy and TLS, only intended exposed routes, build identity, readiness, provider/worker boundaries, and the operator's approved release decision. |
No API, hosted CI, staging, production, Docker, provider or model operation was executed for this census. No configuration secret values were read. Stage 1 has a source-bound surface map. Exact endpoint/runtime evidence, candidate identity and recovery qualification remain open.
@@ -0,0 +1,99 @@
# Recovery implementation review and rehearsal plan
2026-11-02; source `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Read-only discovery. The entire 765-line `deploy/deploy.sh` was inspected, never executed or sourced. No operational keys, credentials, environment files, databases, containers or remote storage were accessed.
## Distinct recovery mechanisms
| Owner | Verified source behavior | Qualification boundary |
| --- | --- | --- |
| `SqliteDatabaseBackupRunner` | SQLite-only VACUUM snapshot and count retention; explicitly unsupported for MariaDB | Does not prove MariaDB backups exist |
| `BackupController` | Authenticated, encrypted user-data export using application Data Protection; includes structured records and attachment metadata | Not a server-wide database/files/keys recovery set; do not use it as disaster recovery proof |
| `deploy/deploy.sh:backup_database` | Requires explicit provider; SQL dump for MariaDB, volume archive for SQLite; rejects missing/empty/malformed/trailerless dumps | Format checks do not restore or compare records; emergency skip exists and is not approved here |
| `backup_recovery_volumes` | MariaDB additionally archives required application data volume; independently archives tombstones when present | Consistency between dump/files/tombstones and current key availability still needs isolated rehearsal |
| Retention/off-host copy | Daily/monthly retention and optional rclone copy | Source message calls remote encrypted but source does not establish actual remote encryption configuration; off-host configuration and recovery remain unverified |
| `finish_deploy` | On failure after replacement, restarts previous backend/frontend references, checks public frontend/liveness/readiness with bounded retries and retains failed exit | Offline stubs verify control flow. Actual recovery, image identity, database reversal and previous AI image restoration remain unqualified |
| Tombstone store and replay | Validates ledger records; Program invokes `StageRestoredAccountsAsync` at startup; deleted identities are staged again | Test restored data with latest independent tombstones; corrupt ledger must fail closed. Existing AccountDeletionTests include restore replay, not a complete operational restore |
Build retry paths prune Docker build/system cache and deployment may warm an Ollama model. Therefore even a proposed "local deploy smoke" must not run this script against the shared workstation. No such action was taken.
## RR-11: backup connection parser mismatch - reproduced
The script's host `sed` expression accepts `Server=` but returns no host for `SERVER=` and `Host=`. The latter is a documented MySqlConnector alias ([official connection options](https://mysqlconnector.net/connection-options/), checked 2026-11-02). The script then fails its required-host check before dump execution. Impact: a valid application connection spelling can prevent deployment backup/deployment; no observed data loss or actual production impact. Engineering priority proposed P2; not a security vulnerability rating.
Three synthetic cases used only loopback and dummy identifiers. Exact source expression was extracted from the `db_host` assignment, then executed through Git Bash with `echo <shell-quoted synthetic input> | sed -n <shell-quoted source expression>`. Results: canonical `Server` -> `127.0.0.1`; uppercase `SERVER` -> empty; alias `Host` -> empty. Assertions confirmed all three results. No connection was opened. Initial direct Windows-to-sed invocations failed the positive-control assertion and were discarded as invalid harness evidence; the shell-native rerun passed its positive control.
Proposed future remediation: parse with the same supported connection-string semantics as the API, or explicitly validate/document a narrower deploy contract before any operational step. Cover aliases/case, quoted separators, duplicate keys, missing required values and secret-redaction behavior. Do not print real input. Any implementation belongs to an authorised release remediation scope, not P10 cleanup.
## Isolated rehearsal specification - NOT RUN
1. Bind accepted candidate, image digests, migration history and disposable host ownership. Use synthetic accounts and a separate temporary MariaDB/files/key/tombstone set; never copy production data here without separate authority.
2. Seed two owners with canonical profiles, CV overrides, letter history, submitted packages, attachments, encrypted provider placeholders and completed deletion tombstones. Record independent counts and artifact hashes, without recording plaintext secrets.
3. Produce the actual supported dump/files/tombstone recovery set. Demonstrate a failed dump or missing required volume prevents promotion. Verify off-host retrieval/decryption separately if that integration is approved.
4. Restore into a second isolated environment. Independently compare rows/ownership/revisions/artifact bytes; exercise application decryption and document reopening. Restore an older database with newer tombstones and prove erased accounts cannot regain access. Inject a malformed ledger and assert fail-closed startup.
5. Rehearse prior-image rollback after failed replacement, then after a representative additive migration and writes. Check `/health`, `/ready`, exact version and authenticated read/write preservation. Document when image rollback is insufficient and database restore would lose newer writes.
6. Measure recovery duration and data-loss window against owner-approved RTO/RPO. No target or measurement exists in this checkpoint. Preserve cleanup and evidence identities; no production execution or release approval implied.
Fresh checks: Git Bash `bash -n deploy/deploy.sh` PASSED (syntax only); `python scripts/test-mariadb-workflow.py` PASSED 4/4 (source quarantine guards only). These are separate from the earlier 5/5 offline runner tests. Restore, failure injection, hosted isolation, live encryption and rollback health remain NOT TESTED.
## RR-12 offline failure injection
Tested the unchanged `finish_deploy` function and EXIT trap from baseline `47c6834cee192a5f1f054ddcd500b4d45c48c985`, `deploy/deploy.sh` blob `8341110de5a827698efc6eeb6951f296770562da`. The test extracts only the handler and trap and supplies synthetic image refs and mock Compose and cleanup functions. The full deploy script was never sourced or executed.
| Case | Observed behavior |
| --- | --- |
| Replacement started, both previous core image references exist | Recreates backend and frontend with those refs only. The failed deployment status is preserved. No health probe follows the successful Compose call. |
| Compose rejects rollback | Attempts status and backend/frontend log collection. The original failure status is preserved, even when the rollback command returns a different status. |
| Either or both previous references are missing | Skips automatic recreation and reports that no complete prior core release is available. |
| Deployment fails before replacement, or succeeds | Skips automatic recreation. |
Both auth-config temporary-file cleanup calls occur before the rollback decision. The test stubs deletion and checks the exact call sequence.
- `python scripts/test-deploy-rollback.py -v` **PASSED 5/5**, including three missing-reference combinations.
- `python scripts/test-deploy-backup.py -v` **PASSED 5/5** and `python scripts/test-mariadb-workflow.py -v` **PASSED 4/4**. Git Bash `bash -n deploy/deploy.sh` passed syntax validation.
RR-12 remains open for an authorised isolated rehearsal. After the prior images are recreated, verify exact image identity, `/health`, `/ready`, and authenticated reads and writes against representative migrated and post-write data. A successful Compose call does not establish service health or recover schema, data, or the AI image. RR-13 still needs authorised off-host retrieval/decryption and recovery-set consistency verification. No Docker, network service, database, remote storage, deployment or restore was invoked by these checks.
## RR-13 off-host upload failure handling
Reproduced two transfer failures in the deployment gate using the real `upload_current_backup_set` function, synthetic files and a stub `rclone`. The caller uses `! upload_current_backup_set`, so Bash suppresses errexit inside that function. When either daily `copyto` or monthly `copy` returned 23, the old code still printed success and the gate returned 0. The healthy control selected only current-stamp daily files, then the monthly directory.
The bounded fix explicitly returns failure on either copy error. A daily error stops before subsequent files and the monthly copy. A monthly error stops before the deployment continues. The successful path still uses immutable transfers and no longer claims the configured remote is encrypted. A failed upload may leave some remote files already transferred.
- Red run: **2 failures and 1 passing control**. Green run: **8/8 backup tests** including the three upload cases. The independent rollback handler and hosted quarantine guard checks passed **5/5** and **4/4**. Bash syntax validation passed.
- Encrypted off-host retrieval, decryption, completeness and consistent restore of database/files/keys/tombstones remain required. No actual transfer, restore, production configuration, keys or backup files were accessed.
## RR-13 monthly completion marker
The same conditional Bash behavior applies to `apply_backup_retention`. With a stubbed partial file copy returning 23, the old function wrote `.complete`, reported the month retained and continued through the mocked pruning, upload and deployment boundary. The successful control retained byte-identical synthetic files and the expected stamp.
The fix explicitly checks preparation and copy. It writes the stamp to a temporary file in the monthly directory and renames it to `.complete` only after the copy and stamp write succeed. A failed rename blocks later steps and attempts to remove the temporary marker. Existing completed snapshots are preserved. A failed copy can leave unmarked files, and previously written markers still require verification. A marker records the selected stamp and successful local copy; it does not attest to recovery-set integrity.
- Red run: **2 failures and 1 passing control** against the pre-fix function. The partial-copy case directly reproduced false completion; the marker-publication case required an atomic rename the old function did not perform.
- Green run: **12/12 backup tests** including four monthly cases (successful copy and stamp, partial copy, failed marker rename, existing completed month). Rollback handler and hosted quarantine checks passed **5/5** and **4/4**. Bash syntax validation passed.
- Only temporary synthetic files were created or copied. Pruning and upload were stubbed. Approved rehearsal still needs independent checks of artifacts for the marker stamp, encryption, retrieval and actual restore before any release decision.
## RR-12 post-rollback core health checks
11 November 2026. Source `a39ac847298e1fbe7667cf42671e7ec265500fcd` on `codex/release-rollback-health`, based on `24a62997`. After a successful previous-image restart, the handler now checks the configured public frontend, `/health` and `/ready`. Each curl request has connection/request timeouts and bounded retries for startup failures. A failed check reports that manual recovery is required and collects status/logs. The deployment's original nonzero status is retained even if diagnostics fail. The success message is limited to restarted core images and passing public core checks.
The red test run executed six methods and reported four assertion failures because the old handler performed no probes. The green run passed six methods, including failed-probe subcases for all three endpoints, Compose failure, incomplete references, pre-replacement failure and successful deployment. Probe failures and Compose failure also use failing diagnostic stubs to verify original-status preservation. Twelve backup tests and shell syntax validation passed separately. Earlier five-test receipts remain historical and overlap this run.
Only the extracted exit handler/trap ran with shell functions substituting Compose, Docker, curl and cleanup. The full deployment script was never sourced or executed, and no network/containers/models were used. Raw red/green/backup/syntax receipts are in `evidence/rollback-health/`. Actual startup timing, proxy routing, exact image identity, post-migration writes and database/files/keys/tombstone recovery still need the authorised isolated rehearsal. Passing public probes alone does not establish recovery correctness.
## RR-13 retention failure propagation
11 November 2026. Based on `3cca441874abfab6ad581c5a07913a6d66622423` on `codex/release-rollback-health`. Bounded offline remediation under prompt 11, sections 13 and 16. Engineering priority P2: retention failures could falsely report success and permit upload/deployment, leaving retention incomplete; no actual data loss or production impact was observed. No security severity or release-risk acceptance is implied.
The real conditional deployment caller suppresses Bash errexit within `apply_backup_retention`. Previously a failed daily `find -delete` or monthly `rm` was masked by the final success message. Monthly `find | sort` failures were also hidden by process substitution feeding `mapfile`. Seven retention test methods reproduced four failing assertions: daily prune, monthly enumeration, sorting and monthly prune all returned success and reached the mocked upload/deployment boundary.
Daily pruning and each monthly removal now explicitly return failure. Monthly enumeration is captured in a checked command substitution under the script's existing `pipefail`, then split into the same ordered array; an empty listing produces an empty array. Failure prevents later monthly removals, upload and deployment, and suppresses the final retention-success message. Existing daily age/count defaults, monthly path guard, copy/atomic-marker behavior, archive/dump/upload functions and backend error handling remain unchanged. Pruning can already have removed some eligible old files before a command fails; this change does not undo prior removals or prove recoverability.
Fresh evidence, Windows / Python 3.12.10 / Git Bash:
- `python scripts/test-deploy-backup.py BackupRetentionTests -v`: red run **7 methods, 4 assertion failures** (monthly enumeration has two failing subcases). Four existing methods passed. An additional empty-list control was added afterward.
- `python scripts/test-deploy-backup.py -v`: final **16/16 methods PASSED**, including 8 retention, 5 connection-parser and 3 upload methods. Healthy retention requests removal of the two oldest synthetic months in order, preserves current copied bytes/stamp and an existing completed snapshot, and reaches upload/deployment. Daily failure prevents enumeration; either pipeline failure prevents monthly pruning; first monthly removal failure prevents further removals. Empty-list control makes no removal request. The red and earlier green totals overlap; do not add them.
- `python scripts/test-deploy-rollback.py -v`: **6/6 PASSED** independently; original failure statuses, rollback diagnostics and health-check gates retained.
- Git Bash `bash -n deploy/deploy.sh`: **PASSED**, syntax only. Raw receipts and SHA-256 source identities: [retention-failures evidence](evidence/retention-failures/source-identity.json), alongside `red.txt`, `green-backup.txt`, `rollback.txt`, and `syntax.txt`.
- `graphify update .`: AST-only update completed, 18,539 nodes / 45,993 edges. Optional SQL extractor missing and unsupported/no-symbol files reported; no dependency installed or semantic/model extraction run. Generated graph remains untracked local navigation material.
Tests execute only extracted functions/caller with synthetic temporary files. Every pruning `find`, `rm` and upload operation is a shell stub; sorting uses only mocked listing text. The full deployment script was never executed or sourced. No real backup, deletion, network transfer, containers, database, migration, providers, model, infrastructure repair or production operation was performed. Product/browser/full MariaDB/hosted/security suites were not rerun for this shell-only package. Actual Linux-host pruning, encrypted retrieval/decryption, consistent restore, post-write rollback and agreed RTO/RPO remain unqualified. RR-09/RR-12/RR-13 recovery gates and production NO-GO remain open. Reverting this bounded commit restores the prior failure-masking behavior; no schema or user-data reversal is needed.
@@ -0,0 +1,22 @@
# Preliminary release blocker reconciliation
Discovery only, 2026-11-02. These are qualification gaps unless a concrete defect is stated. No inferred P0/P1 vulnerability ratings or owner risk acceptance.
| ID | Historical item / current finding | Status / closure evidence needed |
| --- | --- | --- |
| RR-01 | Phase 9/10 and production NO-GO | CLEAN-20 implementation and verification are published. Independent Phase 10 acceptance and later roadmap acceptance remain open. BLOCKED for final RC |
| RR-02 | Old Linux 11/24 and later S01 failure narratives | Latest handoff records approved S01 36/36 per platform and owner visual acceptance for unchanged development output. Do not reopen these metrics without affected change; physical print/device remain NOT TESTED |
| RR-03 | Python advisory/native parser/image qualification | Latest scoped conditional disposition retained; current advisories and production-equivalent native isolation NOT TESTED. No new scan or risk waiver |
| RR-04 | Hosted MariaDB/isolation | Source explicitly fails before execution until isolated runner proof. BLOCKED; fresh local 5 guard tests do not close it |
| RR-05 | Human/device/provider | Actual AT/contrast, physical devices/print, native language and live provider accounts not qualified here. BLOCKED/NOT TESTED by lane |
| RR-06 | Old known-issues F19 SQLite fallback statement | Current DatabaseConfiguration defaults to MariaDB and throws without connection. Old wording is superseded for this source. SQLite retirement still BLOCKED on transfer/restore/parity gates |
| RR-07 | Old F01 stale-write statement | MariaDB acceptance documents bounded RD21 resolution; later handoff carries it. Do not reopen all old F01 findings based on stale index wording; final affected API census/regression remains required |
| RR-08 | MySQL TLS / dependency / production identity | Latest handoff retains MYSQL-TLS-01 and OPS-01. No live configuration read; current advisory disposition and deployed equivalence unverified |
| RR-09 | Backup/restore and rollback | Existing procedures and historical seeded rehearsal are not a current restore. Need isolated recovery-set completeness, decryption, tombstone behavior, post-write rollback compatibility and agreed RPO/RTO |
| RR-10 | AI quality/activation | Deterministic Docling fixtures are distinct from real-model semantic accuracy. Optional unqualified adapter remains disabled; no AI invocation authorised here |
| RR-11 | Backup host parsing rejected supported `Host=` alias and uppercase spelling | FIXED AND OFFLINE-TESTED. See [bounded remediation](RR-11_IMPLEMENTATION.md). Real dump/restore qualification remains open |
| RR-12 | Previous handler omitted post-rollback health checks; schema/data/AI recovery remains separate | Health probes FIXED in `a39ac847` and six offline handler methods passed, including failed endpoints and diagnostics. See [health follow-up](11_BACKUP_RESTORE_ROLLBACK.md#rr-12-post-rollback-core-health-checks). Actual image identity, startup timing and recovery after migration/writes still require isolated rehearsal |
| RR-13 | Off-host encryption and recovery-set consistency need independent proof. Failed transfer, partial monthly copy and retention errors previously let deployment continue | Transfer/marker gates and explicit daily prune, monthly enumeration/sort and removal failure propagation FIXED AND OFFLINE-TESTED. Final backup suite 16/16 methods; four retention failure assertions reproduced first. See [retention follow-up](11_BACKUP_RESTORE_ROLLBACK.md#rr-13-retention-failure-propagation). Actual encryption, retrieval/decryption and database/files/key/tombstone consistency require approved isolation |
| RR-14 | July first-deploy runbook inferred rollback safety from its no-op migration baseline, while current migration Up methods add schema | Historical scope and current qualification links clarified. `AddJobRevision` adds Version and `AddUserOperationEvents` creates a table/indexes. Exact old/new image compatibility after migration/writes remains an isolated rehearsal gate |
Continue evidence work against the [source endpoint and environment census](03_ENDPOINT_ENVIRONMENT_CENSUS.md). Actual qualification requires an accepted candidate and isolated environment. Every fix needs minimal scope, targeted verification and the applicable stage approval.
@@ -0,0 +1,26 @@
# Preliminary acceptance matrix
2026-11-02. **NO-GO for production.** This is a discovery snapshot, not Stage 7 final acceptance. No accepted final RC or production-equivalent build is nominated. PASS applies only to an executed gate, not historical source evidence.
| Mandatory gate | Current release verdict | Missing release evidence / dependency |
| --- | --- | --- |
| Functional regression | BLOCKED | Accepted final roadmap source; fresh applicable critical journeys |
| Frontend/UX | BLOCKED | Accepted design/implementation and final source-bound browser evidence |
| Accessibility | NOT TESTED | Actual AT/device/contrast/manual evidence plus automation |
| Authentication/authorization | NOT TESTED | Final endpoint/tenant/session qualification |
| Security assessment | BLOCKED | Dedicated security initiative start and predecessor gates; no scan run |
| Backend APIs | NOT TESTED | Final API census and applicable regression |
| MariaDB/migrations | BLOCKED | Hosted isolation and actual RC upgrade/recovery evidence; five offline guards are not this gate |
| CV/PDF fidelity | BLOCKED | Final RC source equivalence and retained scoped approvals; physical print still missing |
| AI integrations | BLOCKED | Explicit isolated-model qualification and semantic evidence |
| Email/OAuth | BLOCKED | Actual configured provider/consent/account verification |
| Performance/reliability | NOT TESTED | Approved load targets and bounded final-source measurements |
| Docker/infrastructure | NOT TESTED | Exact images/environment/configuration/ingress parity |
| CI/CD | BLOCKED | Hosted isolation and fresh actual run/artifacts |
| Backup/restore | NOT TESTED | Disposable complete recovery rehearsal with content assertions |
| Deployment/rollback | NOT TESTED | Authorised isolated rehearsal; production execution remains prohibited |
| Monitoring | NOT TESTED | Failure detection, delivery and redaction evidence |
| Documentation | NOT TESTED | Complete release-source/runbook reconciliation, beyond this partial discovery |
| Human/device/provider | BLOCKED | Real reviewers/devices/accounts and source-bound records |
No gate is marked FAIL without fresh evidence of unacceptable behavior. No missing qualification is marked PASS. Historical source-specific acceptance may be reused where permitted and unchanged, but cannot establish final release readiness by itself.
+23
View File
@@ -0,0 +1,23 @@
# Release discovery handoff
11 November. Branch `codex/release-rollback-health`, baseline `24a62997`, source `a39ac847` adds bounded post-rollback frontend/liveness/readiness probes and failing-diagnostics coverage while preserving the original exit status. Six extracted-handler tests, twelve backup tests and shell syntax validation passed. Full deploy, Docker, network, models and real restore were not executed. [Evidence and remaining qualification](11_BACKUP_RESTORE_ROLLBACK.md#rr-12-post-rollback-core-health-checks).
RR-14 clarifies that the first-production runbook is a July baseline. Current Up migrations add the Version column and operation-event schema. Its old no-op argument cannot qualify a current old/new image pair. Current candidate, migration/write and recovery rehearsal requirements are now linked from that runbook. This was source/document review only.
RR-12 offline failure injection passed 5/5 against the unchanged deploy exit handler. The evidence covers synthetic prior-image recreation, failed rollback diagnostics, original exit status and conditions that skip rollback. [Recovery report](11_BACKUP_RESTORE_ROLLBACK.md#rr-12-offline-failure-injection) distinguishes this from a real post-migration/post-write rehearsal. RR-13 off-host retrieval/decryption and consistent restore remain open. No production qualification.
RR-13 transfer guard [reproduced and fixed offline](11_BACKUP_RESTORE_ROLLBACK.md#rr-13-off-host-upload-failure-handling). Daily and monthly copy errors stop deployment, and the success log makes no encryption claim. Actual remote encryption and restore remain open.
RR-13 [monthly completion guard](11_BACKUP_RESTORE_ROLLBACK.md#rr-13-monthly-completion-marker) now blocks the gate after a partial copy or marker publication failure. The successful path preserves artifact bytes and the stamp in synthetic fixtures. Existing markers and real restoration remain unverified.
10 November: [endpoint and environment census](03_ENDPOINT_ENVIRONMENT_CENSUS.md) maps 41 source route families, anonymous exceptions and failure cases to qualification owners on the backup-parser branch. No runtime qualification. Carry the map into Stage 2 only after the release candidate and environment are accepted.
Branch `planning/release-readiness-discovery`, baseline `5cb903b989742b7a96cb22ddf4c0e24993b18980`; documentation only. Stage 1 IN_PROGRESS, not complete. Full authoritative release prompt read. No final GO/NO-GO completion report or deployable release claimed.
Completed: initial source inventory, historical blocker reconciliation, preliminary 18-gate matrix and five fresh offline MariaDB runner guard tests. Preserved scoped PDF acceptance and separate model/provider/human/hosted/production gates.
Next authorised work: complete endpoint and environment census, map each failure boundary to existing test/runbook owners, and inspect remaining backup/restore/deployment implementation without executing it. Then prepare a concrete isolated-rehearsal plan. Final baseline/qualification depends on accepted roadmap source; do not run thousands of unchanged tests to fill this report.
Continuation: recovery implementation inspection and isolated rehearsal specification are now recorded in 11_BACKUP_RESTORE_ROLLBACK.md. RR-11 parser mismatch reproduced using three synthetic cases; RR-12/13 retain rollback/encryption qualification limits. Four source quarantine guard tests and shell syntax check passed. No deployment script execution. Next: endpoint/environment census, then remaining independent I18N/homepage audit work. Stage 1 remains IN_PROGRESS.
No production credentials/environment files were opened; no infrastructure, models, main branch or autonomous orchestrator changed. The earlier delegated researcher was interrupted by a service limit; parent supplied this partial checkpoint. No background agent remains active.
@@ -0,0 +1,19 @@
# RR-11 bounded remediation
Authorised by the owner's Continuous Roadmap Execution instruction explicitly prioritising this fix. Branch `codex/release-backup-parser`, based on published discovery `653f8d04d469dcd52c883d4b98bea76b8adacada`. Separate from CLEAN-20, full release qualification and infrastructure operations.
Replaced five inconsistent sed expressions with one stdlib Python parser and a shell-safe field transfer. Case-insensitive aliases, quoted separators/doubled quotes, Unicode and empty passwords survive to the dump command. Invalid ports, incomplete/ambiguous/control-bearing values and unsupported multi-host/socket/path forms fail before dumping. No eval; connection input travels via stdin, not argv; errors never include input. No dependency added. Existing dump/TLS/retention/rollback behavior is otherwise unchanged.
## Evidence
- Regression first: four test methods ran against the original backup function; eight subcase failures reproduced alias/case rejection and quoted-password truncation. Canonical/negative controls executed. No real dump or deployment.
- Final targeted suite: **5/5 methods PASSED**, including 7 host spellings, quoted semicolon/equal/doubled quote/shell characters, Unicode/single quotes, empty password, and six invalid/ambiguous input cases. Tests run the actual backup function and mock only external dump/Docker boundaries.
- Full offline script unittest set: **31/31 PASSED**, including targeted cases, ingress Compose rendering with synthetic env, workflow/runner guards, retry guard, supply-chain fixtures and offline model-evaluation helpers. No model invocation. Printed exit17/exit1 are expected mocked runner negative cases. Do not add targeted five to the overlapping total.
- Ordinary unittest discovery found zero tests because repository filenames contain hyphens; recorded as NOT A PASS. Explicit importlib loading of every `scripts/test-*.py` unittest module (excluding standalone Docker topology runner) executed the 31 tests. Docker topology qualification NOT RUN.
- Git Bash `bash -n deploy/deploy.sh`: PASSED.
- Graphify AST update completed locally: 18,504 nodes / 45,920 edges; missing optional PDF/SQL extractors reported. Newly generated untracked graph output retained locally, not a runtime verification or introduced repository artifact.
- Product/backend/frontend/full database/browser suites NOT RUN: this package changes only deploy parsing and its offline tests. Real dump/restore, Linux-host operational behavior, hosted CI and production validation remain NOT TESTED.
The first Unicode test attempt failed in the Windows test harness encoding before reaching product code; the harness was corrected to UTF-8, then passed. Parser stdin/stdout are explicitly UTF-8. No application user data or production credentials used.
Rollback: revert this bounded implementation commit to restore the prior parser (and its known limitation). No database/schema change needs reversal. RR-11 is FIXED AND OFFLINE-TESTED on this development branch; RR-12/13 and production remain open. No main merge/deploy authorised.
@@ -0,0 +1,23 @@
test_empty_password_is_preserved (__main__.BackupConnectionTests.test_empty_password_is_preserved) ... ok
test_invalid_or_ambiguous_input_never_invokes_dump_or_exposes_input (__main__.BackupConnectionTests.test_invalid_or_ambiguous_input_never_invokes_dump_or_exposes_input) ... ok
test_quoted_separators_and_shell_characters_remain_data (__main__.BackupConnectionTests.test_quoted_separators_and_shell_characters_remain_data) ... ok
test_single_quoted_unicode_password_and_database_alias_are_preserved (__main__.BackupConnectionTests.test_single_quoted_unicode_password_and_database_alias_are_preserved) ... ok
test_supported_host_alias_and_case_reach_dump_unchanged (__main__.BackupConnectionTests.test_supported_host_alias_and_case_reach_dump_unchanged) ... ok
test_completed_monthly_snapshot_contains_both_artifacts_and_stamp (__main__.BackupRetentionTests.test_completed_monthly_snapshot_contains_both_artifacts_and_stamp) ... ok
test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy) ... ok
test_empty_monthly_enumeration_does_not_request_removal (__main__.BackupRetentionTests.test_empty_monthly_enumeration_does_not_request_removal) ... ok
test_marker_publish_failure_does_not_expose_complete_marker_or_proceed (__main__.BackupRetentionTests.test_marker_publish_failure_does_not_expose_complete_marker_or_proceed) ... ok
test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy) ... ok
test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy) ... ok
test_partial_monthly_copy_cannot_mark_snapshot_complete_or_proceed (__main__.BackupRetentionTests.test_partial_monthly_copy_cannot_mark_snapshot_complete_or_proceed) ... ok
test_preserves_the_existing_completed_monthly_snapshot (__main__.BackupRetentionTests.test_preserves_the_existing_completed_monthly_snapshot) ... ok
test_daily_copy_failure_stops_before_remaining_files_and_monthly_copy (__main__.BackupUploadTests.test_daily_copy_failure_stops_before_remaining_files_and_monthly_copy) ... ok
test_monthly_copy_failure_blocks_deploy (__main__.BackupUploadTests.test_monthly_copy_failure_blocks_deploy) ... ok
test_uploads_only_current_daily_files_then_monthly_snapshot (__main__.BackupUploadTests.test_uploads_only_current_daily_files_then_monthly_snapshot) ... ok
----------------------------------------------------------------------
Ran 16 tests in 6.842s
OK
PROCESS_EXIT=0
@@ -0,0 +1,84 @@
test_completed_monthly_snapshot_contains_both_artifacts_and_stamp (__main__.BackupRetentionTests.test_completed_monthly_snapshot_contains_both_artifacts_and_stamp) ... ok
test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy) ... FAIL
test_marker_publish_failure_does_not_expose_complete_marker_or_proceed (__main__.BackupRetentionTests.test_marker_publish_failure_does_not_expose_complete_marker_or_proceed) ... ok
test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy) ...
test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy) (operation='monthly-find') ... FAIL
test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy) (operation='sort') ... FAIL
test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy) ... FAIL
test_partial_monthly_copy_cannot_mark_snapshot_complete_or_proceed (__main__.BackupRetentionTests.test_partial_monthly_copy_cannot_mark_snapshot_complete_or_proceed) ... ok
test_preserves_the_existing_completed_monthly_snapshot (__main__.BackupRetentionTests.test_preserves_the_existing_completed_monthly_snapshot) ... ok
======================================================================
FAIL: test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-backup.py", line 258, in test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
AssertionError: 1 != 0 : Monthly recovery set retained: C:/Users/Connor/AppData/Local/Temp/tmp_6t6unbl/monthly/2099-01
PRUNING WOULD START
CLEANUP REQUEST -rf --
MONTHLY PRUNE 2098-11
CLEANUP REQUEST -rf --
MONTHLY PRUNE 2098-12
Backup retention applied: 30 days daily, 1 monthly sets.
UPLOAD WOULD START
DEPLOY WOULD CONTINUE
MONTHLY ENUMERATION
======================================================================
FAIL: test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy) (operation='monthly-find')
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-backup.py", line 270, in test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
AssertionError: 1 != 0 : Monthly recovery set retained: C:/Users/Connor/AppData/Local/Temp/tmpk3z7klep/monthly/2099-01
PRUNING WOULD START
CLEANUP REQUEST -rf --
MONTHLY PRUNE 2098-11
CLEANUP REQUEST -rf --
MONTHLY PRUNE 2098-12
Backup retention applied: 30 days daily, 1 monthly sets.
UPLOAD WOULD START
DEPLOY WOULD CONTINUE
MONTHLY ENUMERATION
======================================================================
FAIL: test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy) (operation='sort')
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-backup.py", line 270, in test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
AssertionError: 1 != 0 : Monthly recovery set retained: C:/Users/Connor/AppData/Local/Temp/tmp6bu45jcv/monthly/2099-01
PRUNING WOULD START
Backup retention applied: 30 days daily, 1 monthly sets.
UPLOAD WOULD START
DEPLOY WOULD CONTINUE
MONTHLY ENUMERATION
======================================================================
FAIL: test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy (__main__.BackupRetentionTests.test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-backup.py", line 279, in test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
AssertionError: 1 != 0 : Monthly recovery set retained: C:/Users/Connor/AppData/Local/Temp/tmpesmkwjd9/monthly/2099-01
PRUNING WOULD START
CLEANUP REQUEST -rf --
MONTHLY PRUNE 2098-11
CLEANUP REQUEST -rf --
MONTHLY PRUNE 2098-12
Backup retention applied: 30 days daily, 1 monthly sets.
UPLOAD WOULD START
DEPLOY WOULD CONTINUE
MONTHLY ENUMERATION
----------------------------------------------------------------------
Ran 7 tests in 1.787s
FAILED (failures=4)
PROCESS_EXIT=1
@@ -0,0 +1,13 @@
test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery (__main__.DeployRollbackTests.test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery) ... ok
test_failed_replacement_recreates_only_prior_core_images (__main__.DeployRollbackTests.test_failed_replacement_recreates_only_prior_core_images) ... ok
test_failure_before_replacement_does_not_recreate_any_service (__main__.DeployRollbackTests.test_failure_before_replacement_does_not_recreate_any_service) ... ok
test_incomplete_previous_release_does_not_recreate_any_service (__main__.DeployRollbackTests.test_incomplete_previous_release_does_not_recreate_any_service) ... ok
test_rollback_failure_retains_original_status_and_collects_diagnostics (__main__.DeployRollbackTests.test_rollback_failure_retains_original_status_and_collects_diagnostics) ... ok
test_success_does_not_recreate_any_service (__main__.DeployRollbackTests.test_success_does_not_recreate_any_service) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.369s
OK
PROCESS_EXIT=0
@@ -0,0 +1,40 @@
{
"baseline": "3cca441874abfab6ad581c5a07913a6d66622423",
"python": "3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]",
"files": {
"deploy/deploy.sh": "f25723f68aa93f2e1ad6afd467facba5d0fd97004122f02db3f77f967237f334",
"scripts/test-deploy-backup.py": "a9d3a69214888c76407b9a47c9e5394144f4a5f01dba134717d0ea9ff8f40ef1",
"scripts/test-deploy-rollback.py": "4d3ec3074ca7582fda52960285909e5f4fbad1e497343b9dbe54a8d8acc90fc4"
},
"checks": [
{
"name": "green-backup",
"command": [
"python",
"scripts/test-deploy-backup.py",
"-v"
],
"exit": 0
},
{
"name": "rollback",
"command": [
"python",
"scripts/test-deploy-rollback.py",
"-v"
],
"exit": 0
},
{
"name": "syntax",
"command": [
"C:/Program Files/Git/bin/bash.exe",
"-n",
"deploy/deploy.sh"
],
"exit": 0
}
],
"red_deploy_sha256": "e5290bf766627517701c269c866398d6466843c05d1dcb832a866815c228291d",
"red_note": "Seven retention methods against baseline deploy source; empty-list control added afterward. Raw worktree SHA-256 identities preserve exact tested bytes, including line endings."
}
@@ -0,0 +1,2 @@
PROCESS_EXIT=0
@@ -0,0 +1,5 @@
............
----------------------------------------------------------------------
Ran 12 tests in 5.884s
OK
@@ -0,0 +1,73 @@
FFFF....
======================================================================
FAIL: test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery (__main__.DeployRollbackTests.test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery) (path='/')
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-rollback.py", line 72, in test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery
self.assertEqual(self.probes()[:index + 1], calls[1:-2])
AssertionError: Lists differ: ['CALL curl -fsS --connect-timeout 5 --max[97 chars]id/'] != []
First list contains 1 additional elements.
First extra element 0:
'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 https://synthetic.invalid/'
+ []
- ['CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 '
- '--retry-connrefused --retry-max-time 30 https://synthetic.invalid/']
======================================================================
FAIL: test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery (__main__.DeployRollbackTests.test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery) (path='/health')
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-rollback.py", line 72, in test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery
self.assertEqual(self.probes()[:index + 1], calls[1:-2])
AssertionError: Lists differ: ['CALL curl -fsS --connect-timeout 5 --max[247 chars]lth'] != []
First list contains 2 additional elements.
First extra element 0:
'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 https://synthetic.invalid/'
+ []
- ['CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 '
- '--retry-connrefused --retry-max-time 30 https://synthetic.invalid/',
- 'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 '
- '--retry-connrefused --retry-max-time 30 https://synthetic.invalid/health']
======================================================================
FAIL: test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery (__main__.DeployRollbackTests.test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery) (path='/ready')
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-rollback.py", line 72, in test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery
self.assertEqual(self.probes()[:index + 1], calls[1:-2])
AssertionError: Lists differ: ['CALL curl -fsS --connect-timeout 5 --max[396 chars]ady'] != []
First list contains 3 additional elements.
First extra element 0:
'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 https://synthetic.invalid/'
+ []
- ['CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 '
- '--retry-connrefused --retry-max-time 30 https://synthetic.invalid/',
- 'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 '
- '--retry-connrefused --retry-max-time 30 https://synthetic.invalid/health',
- 'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 '
- '--retry-connrefused --retry-max-time 30 https://synthetic.invalid/ready']
======================================================================
FAIL: test_failed_replacement_recreates_only_prior_core_images (__main__.DeployRollbackTests.test_failed_replacement_recreates_only_prior_core_images)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Connor\.codex\worktrees\release-readiness-discovery\Job tracker\scripts\test-deploy-rollback.py", line 55, in test_failed_replacement_recreates_only_prior_core_images
self.assertEqual([
AssertionError: Lists differ: ['CAL[121 chars]ous]', 'CALL curl -fsS --connect-timeout 5 --m[398 chars]ady'] != ['CAL[121 chars]ous]']
First list contains 3 additional elements.
First extra element 1:
'CALL curl -fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30 https://synthetic.invalid/'
Diff is 808 characters long. Set self.maxDiff to None to see it.
----------------------------------------------------------------------
Ran 6 tests in 0.420s
FAILED (failures=4)
@@ -0,0 +1 @@
Git Bash bash -n deploy/deploy.sh exited 0. Full script was not executed.
@@ -0,0 +1,5 @@
......
----------------------------------------------------------------------
Ran 6 tests in 0.374s
OK
+68
View File
@@ -0,0 +1,68 @@
"""Read a deployment connection string from stdin; emit five fields plus a sentinel.
This is a single-host dump contract, not a replacement for MySqlConnector. Reject
ambiguous aliases/control characters instead of choosing a different backup target.
Never include input values in errors or pass the connection string as an argument.
"""
import re
import sys
ALIASES = {
**dict.fromkeys(['server', 'host', 'data source', 'datasource', 'address', 'addr', 'network address'], 'host'),
**dict.fromkeys(['user id', 'userid', 'username', 'uid', 'user name', 'user'], 'user'),
**dict.fromkeys(['password', 'pwd'], 'password'),
**dict.fromkeys(['database', 'initial catalog'], 'database'),
'port': 'port',
}
PAIR = re.compile(r'''\s*([^=;]+?)\s*=\s*(?:"((?:""|[^"])*)"|'((?:''|[^'])*)'|([^;]*))\s*(?:;|$)''')
def parse(connection):
if any(ord(c) < 32 or ord(c) == 127 for c in connection):
raise ValueError('Control characters are unsupported in backup connection settings.')
fields = {}
position = 0
while position < len(connection):
if connection[position] == ';' or connection[position].isspace():
position += 1
continue
match = PAIR.match(connection, position)
if not match:
raise ValueError('Malformed backup connection settings.')
key, double, single, plain = match.groups()
if double is not None:
value = double.replace('""', '"')
elif single is not None:
value = single.replace("''", "'")
else:
value = plain.strip()
if value.startswith(('"', "'")):
raise ValueError('Malformed quoted backup connection value.')
canonical = ALIASES.get(key.strip().lower())
if canonical:
if canonical in fields:
raise ValueError('Duplicate backup connection setting or alias.')
fields[canonical] = value
position = match.end()
if any(not fields.get(key, '').strip() for key in ['host', 'database', 'user']):
raise ValueError('Backup connection requires host, database and user.')
port = fields.get('port', '3306')
if not re.fullmatch(r'[0-9]+', port) or not 1 <= int(port) <= 65535:
raise ValueError('Backup connection port must be between 1 and 65535.')
if any(c in fields['host'] for c in ',/\\'):
raise ValueError('Backup requires one TCP host, not a host list or socket path.')
if any(c in fields['database'] for c in '/\\'):
raise ValueError('Database name cannot contain backup path separators.')
return [fields['host'], str(int(port)), fields['database'], fields['user'], fields.get('password', '')]
if __name__ == '__main__':
try:
values = parse(sys.stdin.buffer.read().decode('utf-8'))
except (ValueError, UnicodeError):
print('Invalid or unsupported backup connection settings.', file=sys.stderr)
sys.exit(1)
sys.stdout.reconfigure(encoding='utf-8', newline='\n')
# Sentinel keeps an empty final password intact through shell command substitution.
sys.stdout.write('\n'.join([*values, 'END']) + '\n')
+298
View File
@@ -0,0 +1,298 @@
"""Exercise deployment backup functions with synthetic commands; never deploy."""
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
BASH = 'C:/Program Files/Git/bin/bash.exe' if os.name == 'nt' else shutil.which('bash')
class BackupConnectionTests(unittest.TestCase):
def backup(self, connection):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('backup_database() {')
function = source[start:source.index('\n}\n', start) + 3]
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
script = '\n'.join([
'set -euo pipefail',
'BACKUP_DIR=' + shlex.quote(folder.as_posix()),
'BACKUP_STAMP=synthetic',
'DATABASE_PROVIDER=mariadb',
'JOBTRACKER_CONNECTION_STRING=' + shlex.quote(connection),
'python3() { ' + shlex.quote(Path(sys.executable).as_posix()) + ' "$@"; }',
'docker() { echo "Unexpected Docker call" >&2; return 99; }',
'mariadb-dump() { printf "%s\\0" "$@" > "$BACKUP_DIR/args"; '
'printf "%s" "$MYSQL_PWD" > "$BACKUP_DIR/password"; '
'printf "CREATE TABLE synthetic (id int);\\n-- Dump completed\\n"; }',
'verify_sql_backup() { gzip -t "$1"; }',
function,
'backup_database',
])
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True, cwd=ROOT,
env={**os.environ, 'DEPLOY_SKIP_DB_BACKUP': 'false'})
arguments = (folder / 'args').read_bytes().split(b'\0')[:-1] if (folder / 'args').exists() else None
password = (folder / 'password').read_text(encoding='utf-8') if (folder / 'password').exists() else None
return result, arguments, password
def test_supported_host_alias_and_case_reach_dump_unchanged(self):
for host in ['Server', 'SERVER', 'Host', 'Data Source', 'Address', 'Addr', 'Network Address']:
with self.subTest(host=host):
result, args, password = self.backup(f'{host}=127.0.0.1;DATABASE=synthetic;UID=reader;PWD=dummy-secret')
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertEqual([b'--host=127.0.0.1', b'--port=3306', b'--user=reader',
b'--single-transaction', b'--routines', b'--events', b'--quick', b'synthetic'], args)
self.assertEqual('dummy-secret', password)
self.assertNotIn('dummy-secret', result.stdout + result.stderr)
def test_quoted_separators_and_shell_characters_remain_data(self):
result, args, password = self.backup('Server=127.0.0.1;Port=3307;Database=synthetic;User ID=reader;Password="semi;eq=quote""$(false)"')
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertIn(b'--port=3307', args)
self.assertEqual('semi;eq=quote"$(false)', password)
def test_empty_password_is_preserved(self):
result, args, password = self.backup('Server=127.0.0.1;Database=synthetic;User ID=reader;Password=')
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertIsNotNone(args)
self.assertEqual('', password)
def test_single_quoted_unicode_password_and_database_alias_are_preserved(self):
result, args, password = self.backup("Host=127.0.0.1;Initial Catalog=synthetic;Username=reader;Password='søknad你好;it''s';SslMode=VerifyFull")
self.assertEqual(0, result.returncode, result.stderr + result.stdout)
self.assertEqual(b'synthetic', args[-1])
self.assertEqual("søknad你好;it's", password)
def test_invalid_or_ambiguous_input_never_invokes_dump_or_exposes_input(self):
for connection in [
'Host=127.0.0.1;Database=synthetic;Uid=reader;Port=0;Pwd=dummy-secret',
'Host=127.0.0.1;Database=synthetic;Uid=reader;Port=65536;Pwd=dummy-secret',
'Host=127.0.0.1;Server=other;Database=synthetic;Uid=reader;Pwd=dummy-secret',
'Host=127.0.0.1;Database=synthetic;Uid=reader;Pwd="dummy-secret',
'Host=127.0.0.1;Database=synthetic;Uid=reader;Pwd=dummy-secret\nline',
'Host=127.0.0.1;Uid=reader;Pwd=dummy-secret',
]:
with self.subTest(connection=connection.replace('dummy-secret', 'REDACTED')):
result, args, _ = self.backup(connection)
self.assertNotEqual(0, result.returncode)
self.assertIsNone(args)
self.assertNotIn('dummy-secret', result.stdout + result.stderr)
class BackupUploadTests(unittest.TestCase):
def upload(self, fail_operation=''):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('upload_current_backup_set() {')
function = source[start:source.index('\n}\n', start) + 3]
gate_start = source.index('if ! apply_backup_retention || ! upload_current_backup_set; then')
gate = source[gate_start:source.index('\nfi\n', gate_start) + 4]
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
for filename in ['jobtracker-db-synthetic.sql.gz', 'jobtracker-files-synthetic.tar.gz',
'jobtracker-db-older.sql.gz']:
(folder / filename).touch()
(folder / 'monthly' / '2099-01').mkdir(parents=True)
script = '\n'.join([
'set -euo pipefail',
'BACKUP_DIR=' + shlex.quote(folder.as_posix()),
'BACKUP_STAMP=synthetic',
'BACKUP_RCLONE_REMOTE=fixture:encrypted/',
'FAIL_OPERATION=' + shlex.quote(fail_operation),
'date() { echo 2099-01; }',
'apply_backup_retention() { return 0; }',
'rclone() {',
' printf "UPLOAD %s %s %s\\n" "$1" "$3" "$4";',
' if [ "$1" = "$FAIL_OPERATION" ]; then return 23; fi;',
'}',
function,
gate,
'echo "DEPLOY WOULD CONTINUE"',
])
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True,
cwd=ROOT, timeout=10,
env={key: value for key, value in os.environ.items()
if key.upper() in {'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'}})
calls = [line for line in result.stdout.splitlines() if line.startswith('UPLOAD ')]
return result, calls
def test_uploads_only_current_daily_files_then_monthly_snapshot(self):
result, calls = self.upload()
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertEqual([
'UPLOAD copyto fixture:encrypted/daily/jobtracker-db-synthetic.sql.gz --immutable',
'UPLOAD copyto fixture:encrypted/daily/jobtracker-files-synthetic.tar.gz --immutable',
'UPLOAD copy fixture:encrypted/monthly/2099-01 --immutable',
], calls)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertIn('Current recovery set copied to configured off-host remote.', result.stdout)
def test_daily_copy_failure_stops_before_remaining_files_and_monthly_copy(self):
result, calls = self.upload('copyto')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertEqual([
'UPLOAD copyto fixture:encrypted/daily/jobtracker-db-synthetic.sql.gz --immutable',
], calls)
self.assertIn('Daily off-host backup upload failed', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Current recovery set copied', result.stdout)
def test_monthly_copy_failure_blocks_deploy(self):
result, calls = self.upload('copy')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertEqual(3, len(calls))
self.assertEqual('UPLOAD copy fixture:encrypted/monthly/2099-01 --immutable', calls[-1])
self.assertIn('Monthly off-host backup upload failed', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Current recovery set copied', result.stdout)
class BackupRetentionTests(unittest.TestCase):
def retain(self, fail_operation='', *, previous_snapshot=False):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('apply_backup_retention() {')
function = source[start:source.index('\n}\n', start) + 3]
gate_start = source.index('if ! apply_backup_retention || ! upload_current_backup_set; then')
gate = source[gate_start:source.index('\nfi\n', gate_start) + 4]
fixtures = {'jobtracker-db-synthetic.sql.gz': b'database fixture',
'jobtracker-files-synthetic.tar.gz': b'files fixture'}
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
for name, contents in fixtures.items():
(folder / name).write_bytes(contents)
monthly = folder / 'monthly' / '2099-01'
if previous_snapshot:
monthly.mkdir(parents=True)
(monthly / 'jobtracker-db-earlier.sql.gz').write_bytes(b'earlier fixture')
(monthly / '.complete').write_bytes(b'earlier\n')
script = '\n'.join([
'set -euo pipefail',
'BACKUP_DIR=' + shlex.quote(folder.as_posix()),
'BACKUP_STAMP=synthetic',
'BACKUP_DAILY_RETENTION_DAYS=30',
'BACKUP_MONTHLY_RETENTION_COUNT=1',
'FAIL_OPERATION=' + shlex.quote(fail_operation),
'date() { echo 2099-01; }',
'upload_current_backup_set() { echo "UPLOAD WOULD START"; }',
'find() {',
' if [[ "$*" == *-delete* ]]; then',
' echo "PRUNING WOULD START";',
' [ "$FAIL_OPERATION" != daily-find ]; return;',
' fi;',
' echo "MONTHLY ENUMERATION" >&2;',
' if [ "$FAIL_OPERATION" = empty-enumeration ]; then return 0; fi;',
' printf "%s/monthly/%s\\n" "$BACKUP_DIR" 2098-12 "$BACKUP_DIR" 2099-01 "$BACKUP_DIR" 2098-11;',
' [ "$FAIL_OPERATION" != monthly-find ];',
'}',
'sort() { if [ "$FAIL_OPERATION" = sort ]; then return 25; fi; command sort "$@"; }',
'rm() {',
' echo "CLEANUP REQUEST $1 $2";',
' if [ "$1" = -rf ]; then',
' echo "MONTHLY PRUNE ${3##*/}";',
' [ "$FAIL_OPERATION" != monthly-rm ];',
' fi;',
'}',
'cp() {',
' if [ "$FAIL_OPERATION" = cp ]; then',
' printf partial > "${@: -1}/partial"; return 23;',
' fi;',
' command cp "$@";',
'}',
'mv() { if [ "$FAIL_OPERATION" = mv ]; then return 24; fi; command mv "$@"; }',
function,
gate,
'echo "DEPLOY WOULD CONTINUE"',
])
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True,
cwd=ROOT, timeout=10,
env={key: value for key, value in os.environ.items()
if key.upper() in {'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'}})
artifacts = {file.name: file.read_bytes() for file in monthly.iterdir()} if monthly.exists() else {}
return result, artifacts
def test_completed_monthly_snapshot_contains_both_artifacts_and_stamp(self):
result, artifacts = self.retain()
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertEqual({'jobtracker-db-synthetic.sql.gz': b'database fixture',
'jobtracker-files-synthetic.tar.gz': b'files fixture',
'.complete': b'synthetic\n'}, artifacts)
self.assertIn('PRUNING WOULD START', result.stdout)
self.assertEqual(['MONTHLY PRUNE 2098-11', 'MONTHLY PRUNE 2098-12'],
[line for line in result.stdout.splitlines() if line.startswith('MONTHLY PRUNE ')])
self.assertIn('UPLOAD WOULD START', result.stdout)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_partial_monthly_copy_cannot_mark_snapshot_complete_or_proceed(self):
result, artifacts = self.retain('cp')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertEqual({'partial': b'partial'}, artifacts)
self.assertIn('Could not retain monthly recovery files', result.stdout)
self.assertNotIn('PRUNING WOULD START', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_marker_publish_failure_does_not_expose_complete_marker_or_proceed(self):
result, artifacts = self.retain('mv')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertNotIn('.complete', artifacts)
self.assertIn('CLEANUP REQUEST -f --', result.stdout)
self.assertIn('Could not publish monthly recovery marker', result.stdout)
self.assertNotIn('PRUNING WOULD START', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_preserves_the_existing_completed_monthly_snapshot(self):
result, artifacts = self.retain('cp', previous_snapshot=True)
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertEqual({'jobtracker-db-earlier.sql.gz': b'earlier fixture', '.complete': b'earlier\n'}, artifacts)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
def test_daily_prune_failure_blocks_monthly_pruning_upload_and_deploy(self):
result, _ = self.retain('daily-find')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertIn('Daily backup retention failed', result.stdout)
self.assertNotIn('MONTHLY ENUMERATION', result.stderr)
self.assertNotIn('MONTHLY PRUNE', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Backup retention applied', result.stdout)
def test_monthly_enumeration_failure_blocks_pruning_upload_and_deploy(self):
for operation in ['monthly-find', 'sort']:
with self.subTest(operation=operation):
result, _ = self.retain(operation)
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertIn('Could not enumerate monthly backups', result.stdout)
self.assertNotIn('MONTHLY PRUNE', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Backup retention applied', result.stdout)
def test_monthly_prune_failure_stops_before_remaining_months_upload_and_deploy(self):
result, _ = self.retain('monthly-rm')
self.assertEqual(1, result.returncode, result.stdout + result.stderr)
self.assertIn('Monthly backup retention failed', result.stdout)
self.assertIn('MONTHLY PRUNE 2098-11', result.stdout)
self.assertNotIn('MONTHLY PRUNE 2098-12', result.stdout)
self.assertNotIn('UPLOAD WOULD START', result.stdout)
self.assertNotIn('DEPLOY WOULD CONTINUE', result.stdout)
self.assertNotIn('Backup retention applied', result.stdout)
def test_empty_monthly_enumeration_does_not_request_removal(self):
result, _ = self.retain('empty-enumeration')
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
self.assertNotIn('MONTHLY PRUNE', result.stdout)
self.assertIn('Backup retention applied', result.stdout)
self.assertIn('UPLOAD WOULD START', result.stdout)
self.assertIn('DEPLOY WOULD CONTINUE', result.stdout)
if __name__ == '__main__':
unittest.main()
+105
View File
@@ -0,0 +1,105 @@
"""Exercise the deployment exit handler with synthetic image references and no Docker."""
import os
from pathlib import Path
import shutil
import subprocess
import unittest
ROOT = Path(__file__).resolve().parents[1]
BASH = 'C:/Program Files/Git/bin/bash.exe' if os.name == 'nt' else shutil.which('bash')
class DeployRollbackTests(unittest.TestCase):
def exit_deploy(self, status, *, replaced=True, backend=True, frontend=True, compose_status=0, failed_probe='', diagnostics_status=0):
source = (ROOT / 'deploy/deploy.sh').read_text(encoding='utf-8')
start = source.index('finish_deploy() {')
handler = source[start:source.index('\ntrap finish_deploy EXIT\n', start) + len('\ntrap finish_deploy EXIT\n')]
script = '\n'.join([
'set -euo pipefail',
'exec 3>&1',
'APP_PUBLIC_BASE_URL=https://synthetic.invalid/',
f'replacement_started={str(replaced).lower()}',
'previous_backend_ref=' + ('synthetic/backend:previous' if backend else ''),
'previous_frontend_ref=' + ('synthetic/frontend:previous' if frontend else ''),
'auth_config_body_file=synthetic-body',
'auth_config_headers_file=synthetic-headers',
'rm() { printf "CALL rm %s\\n" "$*"; }',
'docker() { echo "CALL unexpected docker"; return 99; }',
'curl() {',
' printf "CALL curl %s\\n" "$*" >&3;',
f' if [ "${{@: -1}}" = "https://synthetic.invalid{failed_probe}" ]; then return 22; fi;',
' return 0;',
'}',
'compose() {',
' printf "CALL compose %s [%s] [%s]\\n" "$*" "${BACKEND_IMAGE:-}" "${FRONTEND_IMAGE:-}";',
f' if [ "$1" = up ]; then return {compose_status}; fi;',
f' return {diagnostics_status};',
'}',
handler,
f'exit {status}',
])
# Exclude inherited deployment settings from the fixture.
environment = {key: value for key, value in os.environ.items()
if key.upper() in {'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'}}
result = subprocess.run([BASH, '--noprofile', '--norc'], input=script,
text=True, encoding='utf-8', capture_output=True,
cwd=ROOT, env=environment, timeout=10)
self.assertEqual(status, result.returncode, result.stdout + result.stderr)
self.assertEqual('', result.stderr)
calls = [line for line in result.stdout.splitlines() if line.startswith('CALL ')]
self.assertEqual(['CALL rm -f synthetic-body', 'CALL rm -f synthetic-headers'], calls[:2])
return result.stdout, calls[2:]
def test_failed_replacement_recreates_only_prior_core_images(self):
stdout, calls = self.exit_deploy(41)
self.assertEqual([
'CALL compose up -d --force-recreate --remove-orphans backend frontend '
'[synthetic/backend:previous] [synthetic/frontend:previous]',
*self.probes(),
], calls)
self.assertIn('public core checks passed', stdout)
self.assertIn('Deployment remains failed', stdout)
@staticmethod
def probes():
flags = '-fsS --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 2 --retry-connrefused --retry-max-time 30'
return [f'CALL curl {flags} https://synthetic.invalid{path}' for path in ['/', '/health', '/ready']]
def test_each_failed_health_probe_preserves_failure_and_requires_manual_recovery(self):
for index, path in enumerate(['/', '/health', '/ready']):
with self.subTest(path=path):
stdout, calls = self.exit_deploy(41, failed_probe=path, diagnostics_status=17)
self.assertEqual(self.probes()[:index + 1], calls[1:-2])
self.assertEqual(['CALL compose ps [] []', 'CALL compose logs --tail=200 backend frontend [] []'], calls[-2:])
self.assertIn('rollback health verification failed', stdout)
self.assertIn('Manual recovery is required', stdout)
self.assertNotIn('public core checks passed', stdout)
def test_rollback_failure_retains_original_status_and_collects_diagnostics(self):
stdout, calls = self.exit_deploy(41, compose_status=19, diagnostics_status=17)
self.assertEqual([
'CALL compose up -d --force-recreate --remove-orphans backend frontend '
'[synthetic/backend:previous] [synthetic/frontend:previous]',
'CALL compose ps [] []',
'CALL compose logs --tail=200 backend frontend [] []',
], calls)
self.assertIn('Manual recovery is required', stdout)
def test_incomplete_previous_release_does_not_recreate_any_service(self):
for backend, frontend in [(False, True), (True, False), (False, False)]:
with self.subTest(backend=backend, frontend=frontend):
stdout, calls = self.exit_deploy(41, backend=backend, frontend=frontend)
self.assertEqual([], calls)
self.assertIn('no complete previous core release', stdout)
def test_failure_before_replacement_does_not_recreate_any_service(self):
_, calls = self.exit_deploy(41, replaced=False)
self.assertEqual([], calls)
def test_success_does_not_recreate_any_service(self):
_, calls = self.exit_deploy(0)
self.assertEqual([], calls)
if __name__ == '__main__':
unittest.main()