bilko Bilko product documentation Test Sweep 2026-05-15 Bilko Comprehensive Test Sweep — 2026-05-15 Date: 2026-05-15 Verdict: PARTIAL Mission Control: MC #100704 (preflight) through #100715 (this documentation) Orchestrated by: John (CEO request option C) Executed via: Ollama DAG (primary) + direct subagent fallback Validator pattern: /verify-fix-loop Summary A comprehensive test sweep across Bilko's 3 deployed domains (bilko.io stage, bilko.cloud UAT, bilko.company UAT) covering 9 test categories: backend unit, backend integration, frontend unit, end-to-end (×3 domains), SAST, DAST, and performance + accessibility. Consolidated verdict: PARTIAL 0 critical security findings 0 product regressions All test failures categorized as either test infrastructure harness issues (in-scope for fix-loop) or pre-existing structural defects (out-of-scope for this sweep, tracked separately) Backend unit tests: 693/693 PASS after StubCountryPlugin fix Backend integration tests: 556/576 PASS (20 failures pre-existing, enum DDL ordering) Frontend unit tests: 56/67 PASS (11 harness failures: localStorage mock, i18n stub, null guard) E2E bilko.io stage: 3/37 PASS (auth-fresh fixture does not persist access token) E2E bilko.cloud: 8/8 PASS E2E bilko.company: 8/8 PASS SAST: Detekt 0 findings, npm audit 2 HIGH (devDependencies), SonarCloud + Snyk blocked on missing tokens DAST: 0 CRITICAL, 0 HIGH, 4 MEDIUM (missing x-frame-options, CSP on bilko.cloud + bilko.company) Performance + Accessibility: 100/96/96/92 (all 3 domains) Sweep cost: TBD (to be extracted from MC aggregation) Execution method: DAG orchestration → task distribution → validator synthesis Follow-up MCs opened: 6 MCs for fix-loop iteration B (test harness repairs + header hardening) Scope Domains tested bilko.io stage — https://bilko-web-stage-dh4m46blja-lz.a.run.app (GCP Cloud Run, Next.js 15 frontend) bilko.cloud — https://bilko.cloud (Cloudflare Pages, HR market UAT landing) bilko.company — https://bilko.company (Cloudflare Pages, BA market UAT landing) Test categories (9) Backend unit tests (Kotlin/Ktor, Kotest) Backend integration tests (Kotlin/Ktor, Testcontainers PostgreSQL) Frontend unit tests (Next.js 15, Vitest, jsdom) End-to-end bilko.io stage (Playwright, auth-fresh fixture) End-to-end bilko.cloud UAT (Playwright, static landing assertions) End-to-end bilko.company UAT (Playwright, static landing assertions) SAST (Detekt, npm audit, SonarCloud, Snyk) DAST (passive header analysis, OWASP ZAP baseline) Performance + Accessibility (Lighthouse CI, 3 runs per domain) Blueprint references Test Plan: /Users/makinja/business/ALAI-Holding-AS/products/Bilko/docs/TEST-PLAN.md (v1.0, 2026-02-23) Test Strategy: /Users/makinja/business/ALAI-Holding-AS/products/Bilko/docs/testing/TEST-STRATEGY.md Build Blueprint: /Users/makinja/business/ALAI-Holding-AS/products/Bilko/BUILD-BLUEPRINT.md (top 50 lines reviewed) Results by Category Category Verdict Tests Passed Tests Total Key Numbers Evidence File BE Unit PASS 693 693 121/121 core, 29 suites api-unit-rerun-v2.json BE Integration PARTIAL 556 576 20 failures (enum DDL) api-int-rerun-v2.json FE Unit PARTIAL 56 67 14 suites pass, 6 fail web-unit.json + core-unit.json E2E bilko.io FAIL 3 37 34 unexpected (401) e2e-io/summary.json E2E bilko.cloud PASS 8 8 All assertions verified e2e-cloud/summary.json E2E bilko.company PASS 8 8 All assertions verified e2e-company/summary.json SAST PARTIAL Detekt 0 — npm audit 2 HIGH sast-summary-100711.json DAST PARTIAL 0 CRITICAL — 4 MEDIUM header gaps dast-summary-100712.json Perf + A11y PASS 100/96/96/92 3 domains All gates pass lhci-io/cloud/company.json Findings Critical Security Findings Count: 0 Product Regressions Count: 0 All test failures fall into two categories: Test infrastructure/harness issues (in-scope for fix-loop iteration 2): auth-fresh.ts fixture does not persist access token across Playwright context resets (E2E bilko.io 34/37 FAIL) localStorage mock missing in Vitest jsdom setup (FE settings.test.tsx 8 failures) i18n stub incomplete in invoices.test.tsx (raw key {invoices} rendered) MarketContext.test.tsx null guard missing (1 test TypeError) Pre-existing structural defects (out-of-scope, tracked separately): InvoiceStatus enum DDL ordering in DbTestBase.setUpDatabase (BE integration 20 failures) SonarCloud + Snyk tokens absent from Bitwarden (SAST coverage gap) x-frame-options, CSP absent on bilko.cloud + bilko.company (DAST 4 MEDIUM) npm audit 2 HIGH in devDependencies (rollup via @sentry/nextjs, tmp via @lhci/cli) Fix-Loop Iteration 2 Outcomes Status at publish time: In progress (CodeCraft B-half dispatched) Fix-loop candidates identified by validator: auth-fresh.ts fixture — inject access token into Playwright storageState (Vizu/Playwright author, effort S) localStorage mock — add vi.stubGlobal in vitest.config.ts setupFiles (CodeCraft/FE author, effort XS) i18n stub — add i18n mock provider in test wrapper (CodeCraft/FE author, effort S) MarketContext null guard — add null guard in MarketContext.tsx or test stub (CodeCraft/FE author, effort S) Follow-up MCs opened (6): MC TBD — Fix InvoiceStatus enum ordering in BE integration test schema (DbTestBase.setUpDatabase) — Priority H, owner CodeCraft MC TBD — Provision SONAR_TOKEN in Bitwarden and enable SonarCloud SAST scan — Priority M, owner FlowForge MC TBD — Provision Snyk auth token in Bitwarden and integrate Snyk into CI — Priority M, owner FlowForge MC TBD — Add _headers file to bilko.cloud and bilko.company CF Pages deployments (x-frame-options, CSP, HSTS) — Priority H, owner FlowForge MC TBD — Fix rollup HIGH CVE (GHSA-mw96-cpmx-2vgc) via @sentry/nextjs upgrade or exclusion — Priority M, owner CodeCraft MC TBD — Execute ZAP active scan against bilko.io stage after image stabilises — Priority M, owner Securion Lighthouse Scores (Performance + Accessibility) All 3 domains tested with Lighthouse CI (3 runs each, median scores): Domain Performance Accessibility Best Practices SEO bilko.io 100 96 96 92 bilko.cloud 100 96 96 92 bilko.company 100 96 96 92 Gates: perf_gt_70: PASS (all 3 domains) a11y_gt_90: PASS (all 3 domains) A11y failures (all 3 domains, same pattern): aria-allowed-role: Uses ARIA roles on incompatible elements color-contrast: Background and foreground colors do not have sufficient contrast ratio label-content-name-mismatch: Elements with visible text labels do not have matching accessible names Score remains 96/100 (within acceptable range per TEST-PLAN §7.4 target: Lighthouse Performance Score > 90). Note: axe-core WCAG 2.1 AA Playwright tests (TEST-PLAN §8.9) were not executed in this sweep — blocked by e2e_io auth-fresh fixture failure. Evidence Index All evidence files stored in /tmp/bilko-test-sweep/ with integrity checksums: File Category SHA256 (first 16 hex) Notes validator-verdict.json Validator synthesis (computed at publish) Canonical consolidated verdict api-unit-rerun-v2.json BE unit (computed) 693/693 PASS api-int-rerun-v2.json BE integration (computed) 556/576 PASS, 20 failures web-unit.json FE unit (computed) 56/67 PASS core-unit.json FE unit (core) (computed) 121/121 PASS e2e-io/summary.json E2E bilko.io (computed) 3/37 PASS e2e-cloud/summary.json E2E bilko.cloud (computed) 8/8 PASS e2e-company/summary.json E2E bilko.company (computed) 8/8 PASS sast-summary-100711.json SAST (computed) Detekt 0 findings, npm audit 2 HIGH dast-summary-100712.json DAST (computed) 0 CRITICAL, 0 HIGH, 4 MEDIUM lhci-io.json Perf+A11y bilko.io (computed) 100/96/96/92 lhci-cloud.json Perf+A11y bilko.cloud (computed) 100/96/96/92 lhci-company.json Perf+A11y bilko.company (computed) 100/96/96/92 mc-ids.json MC tracking (computed) 12 MC IDs (#100704–#100715) Screenshot evidence: /tmp/bilko-test-sweep/e2e-io/stage-login-ui.png — bilko.io stage login UI (login succeeds) /tmp/bilko-test-sweep/e2e-io/dashboard-401-failure.png — Dashboard API 401 failure (access token not forwarded) /tmp/bilko-test-sweep/e2e-cloud/bilko-cloud-home.png — bilko.cloud home page /tmp/bilko-test-sweep/e2e-cloud/bilko-cloud-pricing.png — bilko.cloud pricing section /tmp/bilko-test-sweep/e2e-company/bilko-company-home.png — bilko.company home page /tmp/bilko-test-sweep/e2e-company/bilko-company-pricing.png — bilko.company pricing section Genesis CEO ask: 2026-05-15 morning Option selected: C (comprehensive sweep across all test categories) Execution method: Ollama DAG orchestration (primary) + direct subagent fallback when DAG blocked Validator pattern: /verify-fix-loop (atomic-claim decomposition, read-only verification) 12 MCs spawned: #100704 (preflight), #100705 (BE unit), #100706 (BE integration), #100707 (FE unit), #100708 (E2E bilko.io), #100709 (E2E bilko.cloud), #100710 (E2E bilko.company), #100711 (SAST), #100712 (DAST), #100713 (Perf+A11y), #100714 (validator), #100715 (this documentation) Blueprint compliance: Met: TEST-PLAN §2.1 core unit tests: 121/121 PASS (accounting, tax, multi-currency, invoicing, chart-of-accounts) TEST-PLAN §7.4 Lighthouse Performance Score >90: 100/100 all 3 domains TEST-PLAN §8.9 Lighthouse Accessibility Score >=90: 96/100 all 3 domains TEST-STRATEGY §6 Financial logic (VAT, double-entry, currency) tested at >95% coverage (core-unit 121/121) BE unit tests 693/693 PASS (Kotlin/Ktor backend, StubCountryPlugin fix applied) E2E UAT landing pages (bilko.cloud + bilko.company): 8/8 PASS each DAST: 0 CRITICAL, 0 HIGH security findings on any target Detekt Kotlin SAST: 0 findings Netty CVE pre-remediated (MC #99531) Gaps: TEST-PLAN §3 / TEST-STRATEGY §7 PR merge gate: BE integration tests 20/576 FAIL (InvoiceStatus enum) — blocks merge gate TEST-STRATEGY §10 Production Deploy Gate: "All E2E tests pass on staging" — e2e_io 34/37 FAIL (auth-fresh fixture) TEST-PLAN §8.8 Security headers: x-frame-options, CSP absent on bilko.cloud and bilko.company TEST-PLAN §8 SAST: SonarCloud and Snyk not executed — token provisioning gap TEST-PLAN §10 coverage target: country-module unit tests (country-rs, country-ba, country-hr) explicitly listed as 0% in TEST-PLAN §10 TEST-PLAN §2.1 bank-import.test.ts listed as MISSING in blueprint — still not present axe-core WCAG 2.1 AA Playwright tests (TEST-PLAN §8.9) not executed — blocked by auth-fresh fixture failure k6 load tests (TEST-PLAN §7.2) not executed — out of scope for this sweep (Phase 2) Related Pages Bilko Test Plan v1.0 Bilko Test Strategy Mission Control #100704–#100715 Published by: Skillforge (ALAI knowledge management) Reviewed by: John (AI Director) Approved for publish: 2026-05-15 Bilko HR Roadmap — Fiscal Compliance + Feature-Enable Architecture (2026-05-29) Bilko HR Roadmap — Fiscal Compliance + Feature-Enable Architecture Created: 2026-05-29 CEO directive: B2B prvo, B2C odgođen, citav app feature-enable based (micro-frontend) za per-user per-plan delivery Status: Active roadmap, supersedes prior B2B-only scoping 1. Executive summary HR ima dva odvojena fiskalna sistema : F1 (B2C, SOAP) i F2 (B2B, Peppol) — oba mandatorna od 01.01.2026 per Zakon o fiskalizaciji NN 89/25 (porezna-uprava.gov.hr verified 2026-05-29) CEO odluka 2026-05-29: B2B prvo (Phase 0), B2C odgođeno CEO arhitektonska direktiva 2026-05-29: Feature-enable based / micro-frontend / per-user per-plan delivery — NE hackovati per feature B2B path: Storecove (~70% Bilko code već postoji, u pregovorima oko cijene) ili Sveračun (negotiated <€0.10/invoice, čekamo creds) Multi-tenant accountant view (1 računovođa → 30+ klijent orgs) zavisi od prvog B2B ACK Direktni rizik produkta ako feature-enable ne dođe prvo: plan tier leakage, ad-hoc feature flag hacking, market cross-contamination 2. Croatian fiscal legal framework Zakon o fiskalizaciji Aktuelni tekst: NN 89/25 (Narodne novine) Source: https://porezna-uprava.gov.hr/hr/fiskalizacija/3982 F1 — B2C fiskalizacija (POS / kasa) Aktivno od 2013 (Fiskalizacija 1.0), proširen 2026 (Fiskalizacija 2.0) Path: real-time SOAP ka cis.porezna-uprava.hr JIR (Jedinstveni identifikator računa) — generira Porezna uprava u SOAP response-u ZKI (Zaštitni kod izdavatelja) — MD5 hash (OIB + DateTime + InvoiceNumber + PPCode + DeviceCode + TotalAmount) , RSA-signed sa FINA cert Cert obaveza: PER-ORG FINA application certificate, bez moguće intermedijarne organizacije Receipt arhiva: 11 godina Hardware fiskalni printer: NIJE legalno mandatoran (web POS dovoljan) Source: https://porezna-uprava.gov.hr/hr/fiskalizacija-racuna-u-krajnjoj-potrosnji-b2c-poslovanje/8033 F2 — B2B eRačun Mandatorno od 01.01.2026 (svi B2B sa drugim PDV obveznicima) Format: HR-FISK CIUS (Peppol BIS Billing 3.0 sa hrvatskim ekstenzijama) Path: preko Peppol intermediara (Storecove, Sveračun, drugi) Cert NIJE per-org — intermediar pokriva (key razlika od B2C) UBL 2.1 XML (težak dio) — već implementiran u Bilko Source: https://porezna-uprava.gov.hr/hr/izdavanje-i-primanje-eracuna-i-fiskalizacija-eracuna/8047 Terminologija "F1" = B2C sistem (ne form-type / cert-type) "F2" = B2B sistem (ne form-type / cert-type) Common confusion: ovo NISU schema verzije ili cert tipovi PDV stope Standard: 25% Reduced: 13% (turizam, hospitalitet) Reduced: 5% (osnovne potrebe, mediji) Zero: 0% (izvoz, intra-EU isporuke) 3. Phased roadmap sa MC mapingom Phase 0 — B2B prvo (current focus) Phase MC Naslov Status Vlasnik 0a #102447 Storecove HR-FISK 2.0 activation (FAST PATH, 4-5 dana) open — counter-offer poslan 2026-05-28 john 0b #102398 Sveračun API sandbox onboarding (negotiated <€0.10/invoice) open — čeka CEO email za creds + računovođa intro john 0c #102481 Feature-enable based micro-frontend architecture (CEO 2026-05-29) open — kritičan prerequisite, dolazi PRIJE Phase 1 john 0d #102401 Bilko Phase 0 multi-org switcher + Securion RLS gate open — zavisi od bilo koji B2B ACK john 0e #102399 5 accountant pilot recruitment (90-day) open — CEO outreach alem 0f #102400 HR pravna osoba tracking (background) open — Sveračun potvrdio NIJE potrebno za B2B; B2C TBD alem Phase 1+ — B2C (DEFERRED per CEO 2026-05-29) Phase MC Naslov Status 1 #102478 B2C fiskalizacija MVP (ZKI + JIR + SOAP + FINA cert) DEFERRED — 21 dana effort 2 #102479 B2C POS web kasa modul DEFERRED — 19 dana effort 3 TBD Hardware integration (fiskalni printer, cash drawer, barcode) not opened 4 TBD Industry specializations (hospitality, retail, services) not opened 4. Feature-enable architecture directive CEO directive (verbatim, 2026-05-29) "citav app treba da je feature enable based (micro frontend) nadam se da mozemo lako features per user per pay deliver a ne da sve hakiramo" Problem statement B2B + B2C + accountant view + direct SMB + multi-market (HR/RS/BiH) — sve raste kao monolit Risk: per-feature ad-hoc hacking Risk: revenue leakage (Basic plan vidi Pro feature) Risk: dev na HR markeu lomi RS market Cilj — komponente architekture Central feature catalog — kanonski popis svih feature-a sa metadata (market, plan tier, dependency graf) Per-user feature resolution — funkcija od (tenant org country + plan tier + accountant role + custom overrides) Micro-frontend split — nezavisni feature modules koji se učitavaju/ne učitavaju po feature flag-u Pricing model alignment — Basic/Pro/Accountant tier = feature subset (definirano u catalog-u) Safe fallback — feature flag service down → conservative defaults (Basic, ne Pro) Audit trail — koji user kad accessovao koji feature Implementation phases (MC #102481) Phase 0: feature catalog kanonski + DB schema + resolution endpoint (~M effort) Phase 1: per-tenant feature flag application u backend (~S) Phase 2: micro-frontend split frontend (~L) Phase 3: pricing model wiring (~M) Phase 4: admin UI za enable/disable per tenant (~S) Build vs buy Datavera istražuje: LaunchDarkly (mature, $$) GrowthBook (open-source self-hosted) Unleash (open-source self-hosted) ConfigCat (mid-tier) Build-our-own (potpuna kontrola, više rada) 5. Open decisions for CEO B2B path: prihvatamo Storecove counter-offer (ako stigne s revidiranim quote) ili idemo Sveračun? HR pravna osoba: potvrđeno NIJE potrebna za B2B; otvoreno za buduće B2C (kad ga otvorimo) Feature catalog vendor: build vs buy — odluka nakon Datavera istrage Multi-market timing: kad RS (SEF) + BiH ulaze u feature catalog scope? 6. MC indeks MC Priority Status Vlasnik Naslov Rute #100332 H ready_for_review alem Sveračun CEO outreach (parent) bizdev #102447 H open john MC-A2 Storecove FAST PATH backend #102398 H open john MC-A Sveračun sandbox onboarding backend #102481 H open john Feature-enable architecture (CEO 2026-05-29) backend #102401 H open john MC-D Phase 0 multi-org switcher backend #102399 H open alem MC-B 5 accountant pilot recruit bizdev #102400 M open alem MC-C HR doo tracking bizdev #102478 H open (DEFERRED) john MC-E B2C fiskalizacija MVP backend #102479 M open (DEFERRED) john MC-F B2C POS web kasa frontend #102448 H open john Vendor email routing + auto-forward devops #102449 H open john Policy: no strategic reply bez CEO OK bizdev 7. Source artifacts /tmp/alai/bilko-hr-fiscal-full-scope-20260529.md — Datavera HR law research (640 linija) /tmp/alai/bilko-hrfisk-audit-20260528.md — CodeCraft FISK code path audit /tmp/alai/sveracun-meeting-outcome-20260528.md — Sveračun meeting outcome /tmp/alai/bilko-accountant-gtm-spec-20260528.md — Skybound accountant-led GTM spec /tmp/alai/bilko-multitenant-accountant-gap-20260528.md — CodeCraft multi-tenant tech gap /tmp/alai/storecove-counter-reply-20260528.md — Storecove counter-offer state 8. Decision log Datum Odluka Kontekst 2026-05-28 Bilko HR ide dual-track GTM (accountant + direct SMB) Sveračun meeting strategic input 2026-05-28 Storecove counter-offer poslan (no name drop Sveračun) CEO directive — leverage 2026-05-29 B2B prvo, B2C odgođen CEO scope decision 2026-05-29 Feature-enable / micro-frontend arch je obavezan prerequisite za sve daljnje builds CEO architecture directive 9. Next steps Storecove reply chase — ako nema odgovora do 2026-06-04, poslati follow-up sa ref na contract expiry 27-06-2026 Sveračun follow-up email — CEO šalje za sandbox creds + 3-5 računovođa intro (target: 2026-05-30) Datavera dispatch za feature catalog vendor comparison (LaunchDarkly vs GrowthBook vs Unleash vs build-our-own) Mehanik gate za MC #102481 (feature-enable arch) prije CodeCraft Phase 0 dispatch Update MEMORY.md sa pointer-om na ovu BookStack stranicu (skip-list ako se promijeni status) Bilko BUG-005 — revenueMTD / credit-note documentType + RLS migration landmine Bilko BUG-005 — revenueMTD negative (credit-note documentType + RLS migration landmine) MC: #103001 (child #102887) | Fixed: 2026-06-05 | Tag: v0.2.18 / PR #255 Symptom Dashboard KPI revenueMTD showed -457.50 on bilko-demo. CEO-visible. Root cause (two layers) Write-side: InvoiceService.createCreditNote insert omitted documentType . The Invoices.documentType column has no Kotlin .default , so the PG column default 'standard' applied. Every credit note was stored as STANDARD with a negative amount. The v0.2.17 read-side fix (ReportService.getRevenueForPeriod excludes CREDIT_NOTE) therefore could not exclude them. Backfill blocked by RLS: migration V65 ( UPDATE invoices SET document_type='credit_note' WHERE document_type='standard' AND invoice_number LIKE 'CN-%' ) recorded success=t but affected 0 rows . The org_isolation RLS policy returns FALSE when app.current_org_id is unset; Flyway runs as bilko with no org GUC, so RLS hid every row. No SQL error → Flyway "succeeded". Fix PR #255: createCreditNote now sets it[Invoices.documentType] = InvoiceDocumentType.CREDIT_NOTE . Historical backfill applied on the serving DB (tribal-sign-487920-k0:europe-north1:bilko-demo-db) via ALTER TABLE invoices NO FORCE ROW LEVEL SECURITY (table owner) → UPDATE (5 rows) → COMMIT → FORCE ROW LEVEL SECURITY restored. Verification (Proveo independent PASS, live) revenueMTD 375.00 (was -457.50); CN-2026-001..007 all documentType=credit_note; freshly created CN is credit_note; INV-2026-001 stays standard. Evidence: /tmp/verify-103001/proveo-validation.md. Reusable lessons Data-backfill migrations on RLS tables must bypass RLS ( ALTER TABLE … NO FORCE ROW LEVEL SECURITY wrapper) or they silently no-op while reporting success. Verify by ROW COUNT, not Flyway success. Verify by live outcome, not green build — every CI/deploy signal said "shipped" while live data was wrong. cloudbuild.yaml coverage gsutil-upload step makes every demo deploy report FAILURE despite success (follow-up). Bilko Backoffice — Backend MVP (Sentry + audit request_id + support_tickets) 1. Overview This backend slice (MC #103323, branch feat/103323-backoffice-backend , commit 6b214a00 , PR #316 ) delivers the diagnostic and intake backbone for the Bilko support fix-loop. Before this slice, when a customer hit an accounting error on app.bilko.cloud neither the platform team nor the customer had a way to identify which request failed or why. Three components address that gap: Sentry error capture — catch-all (INFRA-only) exception capture with PII scrub and Cloud Run release/serverName metadata. Inert until SENTRY_DSN secret is provisioned (OCD-1, CEO action). V71 audit_log.request_id — nullable correlation column added to every audit row, threaded from a single canonical source ( call.callId ) across all route handlers. V72 support_tickets + SupportTicketRoutes — customer intake channel (POST) and platform-admin triage queue (GET list + GET detail + PATCH status) with RLS, idempotency, and full status-transition audit trail. This slice is deploy-gated behind prod cutover MC #103300. All three components were independently verified by Proveo (Angie Jones): 12/12 AC signals PASS, integration test 3/3 PASS, unit suite 1280/1280. 2. Component Map 2.1 Sentry capture — plugins/Sentry.kt + plugins/StatusPages.kt DSN guard: configureSentry() checks SENTRY_DSN ; if absent or blank, Sentry.init is not called. The SDK stays in silent no-op mode. CI / Testcontainers / local dev never emit live Sentry events. Cloud Run metadata: K_REVISION maps to options.release ; K_SERVICE maps to options.serverName . Fallbacks: "local" and "bilko-api-local" . beforeSend PII scrub: request body ( event.request?.data = null ) and breadcrumbs ( event.breadcrumbs?.clear() ) stripped before transmission. Extra context filtered to allowlist: errorCode, requestId, orgId, httpStatus, instancePath . Single capture point — Throwable catch-all only: Sentry.captureException is placed exclusively in the exception handler in StatusPages.kt (line 237). Named typed handlers (BadRequest, Conflict, Unauthorized, Forbidden, etc.) do not call captureException — those cover 4xx user-error exceptions. Ktor StatusPages dispatches named handlers first; Throwable catch-all fires only for genuine INFRA/unexpected exceptions. AC signal: grep returns count=1 in both checks. Sentry scope tags: requestId from call.callId (CallId plugin canonical source), orgId from BilkoPrincipal.organizationId (fallback "UNKNOWN" for pre-auth crashes — mandatory), errorCode = INFRA_001 . 2.2 V71 audit_log.request_id — AuditLogService.kt + migration Migration V71: ALTER TABLE audit_log ADD COLUMN request_id TEXT; — nullable, no default. PG 11+ metadata-only operation (no table rewrite). Plain CREATE INDEX (not CONCURRENTLY) on partial index WHERE request_id IS NOT NULL . CONCURRENTLY is prohibited inside Flyway transactions (institutional memory from V70; AC signal confirms absence). Column type TEXT: chosen over UUID because clients can supply arbitrary X-Request-ID header values. Trust boundary: client-supplied, stored verbatim, correlation/debuggability only. No idempotency constraint on audit_log: one HTTP request legitimately produces multiple audit rows (e.g. impersonation start + org update in same admin session). A UNIQUE constraint would reject valid multi-row sequences. Idempotency enforced at V72 layer. AuditLogService.insert signature: added requestId: String? = null as last parameter (default null = backward compatible). Docstring: "Correlation handle for cross-system debugging only — NOT a security control. Client-supplied value stored verbatim." Single canonical requestId source: call.callId (Ktor CallId plugin) is the single authoritative source across StatusPages (Throwable catch-all), AdminPortalRoutes, ImpersonationService, and SupportTicketRoutes. Typed domain handlers retain raw header for RFC 7807 echo-back to client only — these do not call captureException and do not write to audit_log, so the split is intentional and does not break the diagnostic join. (bruce-momjian dissent resolution) 2.3 V72 support_tickets + SupportTicketRoutes — routes/SupportTicketRoutes.kt POST /support/tickets (customer, JWT-scoped): orgId and userId extracted from BilkoPrincipal only — never from request body. context_bundle server-side validated against CONTEXT_BUNDLE_ALLOWLIST before insert. app.current_org_id set via orgTransaction(principal.organizationId) so RLS WITH CHECK passes. Idempotency: duplicate (org_id, request_id) returns 409. GET /admin/support/tickets (platform-admin): paginated list, limit (default 50, max 100) + offset , optional status and orgId filters. Returns data array + meta.total/limit/offset . GET /admin/support/tickets/{id} (platform-admin): single ticket detail. PATCH /admin/support/tickets/{id} (platform-admin): enforces status transition machine, requires resolutionNote for RESOLVED/CLOSED, inserts audit_log row for every status change with requestId = call.callId . Audit write failure is non-fatal but logged to structured stderr (Cloud Logging visible). Admin GUC pattern: transaction { exec("SET LOCAL app.is_platform_admin = 'true'") } — SET LOCAL per transaction, pgBouncer transaction-mode pooling safe. 3. Data Model 3.1 support_tickets columns Column Type Notes id UUID PK gen_random_uuid() default org_id UUID NOT NULL FK to organizations(id) ON DELETE CASCADE user_id UUID NOT NULL FK to users(id) error_code TEXT Nullable; currently generic VAL/INFRA (OCD-2 open CEO decision) request_id TEXT Correlation ID of originating failed request. NOT a FK to audit_log.request_id (one request_id maps to N audit rows). Join via equality. context_bundle JSONB NOT NULL CHECK jsonb_typeof = 'object'. Allowlisted keys only (server-side enforced). customer_description TEXT Free text from customer status TEXT NOT NULL CHECK (status IN ('OPEN','TRIAGED','IN_PROGRESS','RESOLVED','CLOSED')). Default 'OPEN'. triage_json JSONB NULL = not yet triaged. V2 AI agent writes here. created_at TIMESTAMPTZ NOT NULL DEFAULT now() updated_at TIMESTAMPTZ NOT NULL DEFAULT now(); maintained by BEFORE UPDATE trigger. resolution_note TEXT Required (route-enforced) for RESOLVED/CLOSED transitions. external_ref TEXT V2 Zendesk/Linear sync. Nullable at MVP. 3.2 Indexes UNIQUE (org_id, request_id) WHERE request_id IS NOT NULL — idempotency. (org_id, status, created_at DESC) — admin list query (per-org filtered). (status, created_at DESC) — global admin list. 3.3 RLS policies All GUC SET statements use SET LOCAL (transaction-scoped) — pgBouncer transaction-mode pooling safe. support_tickets_customer_insert — FOR INSERT WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid) . support_tickets_customer_select — FOR SELECT USING (org_id = current_setting('app.current_org_id', true)::uuid OR current_setting('app.is_platform_admin', true)::boolean = true) . support_tickets_admin_all — FOR ALL USING and WITH CHECK (current_setting('app.is_platform_admin', true)::boolean = true) . Same GUC pattern as audit_log RLS (V51). Customer UPDATE/DELETE immutability: no UPDATE or DELETE policy for customers. RLS ENABLED with no such policy = deny-by-default. Customers cannot modify or delete submitted tickets. Production code audit (Proveo-confirmed): orgTransaction{} (OrgScopeSessionVariable.kt:131) always wraps SET LOCAL app.current_org_id inside transaction{} . The Testcontainers test failure (Proveo GAP-1) was caused by the test setup using a PostgreSQL superuser connection — superusers bypass RLS regardless of GUC values. Production code was never buggy. 3.4 Status transition machine From Allowed next states OPEN TRIAGED, CLOSED TRIAGED IN_PROGRESS, CLOSED IN_PROGRESS RESOLVED, CLOSED RESOLVED CLOSED CLOSED (no further transitions) Invalid transitions return HTTP 422 with code: "INVALID_TRANSITION" and allowedNext . 3.5 context_bundle allowlist Allowed keys (server-side enforced, rejection = HTTP 422): requestId, errorCode, httpStatus, instancePath, orgId, userId, appRoute, planTier, country, auditRef . IDs and codes only — never invoice content, names, amounts, or email addresses. 4. Diagnostic Join SELECT al.* FROM audit_log al JOIN support_tickets st ON al.request_id = st.request_id WHERE st.id = ''; Framing (martin-kleppmann dissent): request_id is a correlation handle for cross-system debugging only — NOT tamper-evidence. The append-only guarantee for audit_log comes from the block_audit_mutation() trigger (V51), not from request_id. Platform-admin direct DB access is outside the threat model of this column. 5. Known Gaps and Follow-ups Item Detail Status OCD-1: Sentry DSN bilko-sentry-dsn / bilko-web-sentry-dsn must be provisioned in GCP Secret Manager. Inject via --update-secrets (never --set-env-vars ). Sentry code is fully inert until then. CEO action required. Blocks production deploy; does not block feature branch merge. OCD-2: error_code taxonomy Domain errors currently fall into generic VAL/INFRA codes, making ticket triage partly blind. Domain-specific codes are V2 scope (MC #103333). CEO confirmed proceed with V72 before those codes land. Open CEO decision. V2 follow-on MC #103333. OCD-3: merge-order vs #103300 V71/V72 migration numbers must be confirmed/renumbered after #103300 merges. Open. Blocking deploy only. Positive-path RLS assertion Integration test confirms negative proof (wrong-org INSERT rejected). Positive proof (correct-org INSERT succeeds) not explicitly asserted. Proveo: completeness gap, not safety-weakening gap. Follow-up test enhancement. Non-blocking. CI runner quota Tracked as MC #103304. Separate MC. Deploy gate Deploy-gated behind MC #103300 prod cutover. Dependent on #103300. 6. Verification Evidence Proveo P2P Final Verdict: PASS — commit 6b214a00 , 12/12 AC signals pass, integration test 3/3 PASS (BUILD SUCCESSFUL in 35s, tests="3" failures="0"), unit suite 1280/1280. Evidence: /tmp/alai/p2p-pairing-evidence/proveo-103323-verdict-final.md Builder evidence bundle: /tmp/evidence-103323/verification.md PR: #316 on branch feat/103323-backoffice-backend Integration test XML SHA256: 941b588f21c8fd735c1b6f7f1b888ea2d2441ec0c5f3a2085bc00489fcc70bf7 File hashes (Proveo): StatusPages.kt fca33115361ced358dbdc56a8fd0020bc1212d58758574f540fdc46193287284 ; SupportTicketRoutes.kt 730f76a245fb0492f5f94c378e18973242e7e9a0f9c4de5353dc8be268a38b2f ; OrgScopeSessionVariable.kt 2c5c992c92c5f548c22092c171a98fb599760f3ce827d1e72db26d901c0c89f2 Bilko Backoffice — Ops Infra (Logging Views + support@ Forwarding + Preflight) Bilko Backoffice — Ops Infra (MC #103325) Branch: feat/103325-backoffice-infra | PR: #317 | Proveo verdict: PASS (2026-06-10) | Sibling: Backoffice Backend MVP (page 3100) 1. Cloud Logging Saved Views GCP project: tribal-sign-487920-k0 Bucket: _Default (global) Verified via: gcloud logging views list --bucket=_Default --location=global --project=tribal-sign-487920-k0 View ID Scope filter Intended use / Log Explorer query to add bilko-error-by-org resource.type="cloud_run_revision" AND resource.labels.service_name=~"bilko-api-(demo|stage)" Add query severity>=ERROR . Group results by orgId (parse via JSON_EXTRACT(textPayload, "$.orgId") — orgId lives in textPayload JSON, not jsonPayload). bilko-request-trace resource.type="cloud_run_revision" AND resource.labels.service_name=~"bilko-(api|web)-(demo|stage)" Add query logName=~"stdout" OR logName=~"requests" . Correlate requests end-to-end by requestId field in textPayload. bilko-5xx-demo resource.type="cloud_run_revision" AND resource.labels.service_name=~"bilko-(api|web)-demo" Add query httpRequest.status>=500 . Scoped to demo environment only. GCP constraint — view filter expressiveness GCP gcloud logging views create --log-filter only accepts log source, resource type, appHub fields, user labels, and log ID conditions. Severity comparisons ( severity>=ERROR ) and field comparisons ( httpRequest.status>=500 ) are not valid in view filters — they must be added as Log Explorer query refinements on top of the saved view scope. This is a documented GCP platform limitation. Each view description in GCP documents this explicitly. Log schema note: Bilko API logs structured data as JSON inside textPayload (not jsonPayload ). The textPayload schema is: {"requestId":"...","method":"...","path":"...","status":N,"durationMs":N,"userId":"...","orgId":"...","ip":"..."} . ERROR logs are stack traces in textPayload; orgId is present on request-completion log lines, not on exception lines. 2. support@bilko.cloud Email Forwarding MX provider — IMPORTANT bilko.cloud MX = Migadu ( aspmx1.migadu.com + aspmx2.migadu.com , confirmed via dig MX bilko.cloud ). The CF Email Routing section in DEPLOY-MAP.md is STALE and must be corrected — Cloudflare does not handle bilko.cloud email. Implemented forwarding Mail flow: support@bilko.cloud (Migadu mailbox, may_receive=true , may_send=false ) → Migadu forwarding → alem@alai.no Key Migadu design constraint: Alias destinations only accept same-domain addresses — external addresses are silently rewritten to @ . The correct mechanism for external cross-domain delivery is a forwarding on a mailbox object (not an alias). Implementation steps taken: Confirmed GET /v1/domains/bilko.cloud/mailboxes/support/forwardings — support@ was alias-only (no mailbox). Created support@bilko.cloud mailbox: may_receive=true , may_send=false , IMAP/POP3 disabled (receive-only). Added forwarding via POST /v1/domains/bilko.cloud/mailboxes/support/forwardings {"address":"alem@alai.no"} — response: is_active: true , confirmed_at: 2026-06-10T08:17:01Z , no confirmation email required. Deleted the old support@ alias (superseded by mailbox). Removed investigation-only forwarding from sales@bilko.cloud — sales@ is left untouched ( forwardings: [] ). Verified state (Proveo independent GET): GET /v1/domains/bilko.cloud/mailboxes/support/forwardings {"forwardings":[{"address":"alem@alai.no","confirmed_at":"2026-06-10T08:17:01Z","blocked_at":null,"is_active":true}]} GET /v1/domains/bilko.cloud/mailboxes/sales/forwardings {"forwardings":[]} Migadu admin path (for future changes) To modify forwarding: admin.migadu.com → bilko.cloud → Mailboxes → support → Forwardings. Do not use the Aliases section for external cross-domain targets. 3. Preflight Rollback Script File: scripts/ops/bilko-support-fix-preflight.sh (committed at 67ed0ce5 , PR #317, mode 100755 ) What it does STEP 1 — Cloud SQL backup (write, skipped in dry-run): Takes an on-demand Cloud SQL backup of the Bilko DB before any deploy action. Provides a restore point. STEP 2 — Capture current Cloud Run revision (read-only always): Records the live revision name and image SHA for both bilko-api-demo and bilko-web-demo . STEP 3 — Print rollback commands (print only, never executes): Outputs the exact gcloud run services update-traffic commands needed to roll back to the captured revisions. These are echo -wrapped — they are never executed by the script. How to run # Dry-run (safe, no writes — use to confirm rollback targets before deploy) bash scripts/ops/bilko-support-fix-preflight.sh --dry-run # Live run (takes SQL backup, captures revisions, prints rollback cmds) bash scripts/ops/bilko-support-fix-preflight.sh Deploy-fragility rule enforced All example re-deploy commands in the printed output use --update-secrets . The script documents and enforces: NEVER use --set-env-vars for Bilko Cloud Run deploys — it overwrites the Secret Manager binding and exposes secrets as plaintext environment variables. 4. Known Follow-up Item Status Owner DEPLOY-MAP.md CF Email Routing section for bilko.cloud is stale (lists Cloudflare; MX is Migadu) Open John / next infra PR PR #317 bundles MC #103323 application code (Sentry, SupportTickets, DB migrations V71+V72) — confirm separate QA validation for that scope Open (Proveo advisory) John Merge PR #318 (smoke-test fix) before PR #317, or close #318 as superseded if #317 merges first Open John Created by Skillforge for MC #103325. Evidence: /tmp/evidence-103325/verification.md + /tmp/alai/p2p-pairing-evidence/proveo-multi-317-318-verdict.md. All facts machine-verified. MC #105153 — API write bucket stage E2E rate-limit override MC #105153 — Bilko API write bucket 429 stage E2E Scope Fix authenticated api rate-limit bucket 429s on stage E2E for sentinel contact create and instant-demo write-protection probes, while preserving production defaults. Changes verified on disk apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt Added apiRateLimitConfig(perMinuteEnv) parser. RateLimitName("api") now uses API_RATE_LIMIT_PER_MINUTE with default 100 . apps/api/src/test/kotlin/no/alai/bilko/plugins/RateLimitTest.kt Added tests for default 100 , stage override 500 , invalid fallback 100 . azure-pipelines.yml Stage ACA deploy sets API_RATE_LIMIT_PER_MINUTE=500 for bilko-api-stage . .github/workflows/azure-stage.yml Stage ACA deploy sets API_RATE_LIMIT_PER_MINUTE=500 . .env.example Documents API_RATE_LIMIT_PER_MINUTE , AUTH_RATE_LIMIT_PER_MINUTE , DEMO_RATE_LIMIT_PER_MINUTE , DEMO_RATE_LIMIT_PER_HOUR defaults. infrastructure/gcp/ENV-MATRIX.md Documents stage-only API_RATE_LIMIT_PER_MINUTE=500 ; demo/prod default/absent. Verification — 2026-07-09T23:07Z Command run from apps/api : ./gradlew test --tests no.alai.bilko.plugins.RateLimitTest --rerun-tasks Result: BUILD SUCCESSFUL in 21s , 7 actionable tasks: 7 executed . JUnit XML evidence: apps/api/build/test-results/test/TEST-no.alai.bilko.plugins.RateLimitTest.xml Static presence check: grep -n "API_RATE_LIMIT_PER_MINUTE" \ apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt \ apps/api/src/test/kotlin/no/alai/bilko/plugins/RateLimitTest.kt \ azure-pipelines.yml .github/workflows/azure-stage.yml \ infrastructure/gcp/ENV-MATRIX.md .env.example Observed paths/lines: apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt:115 apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt:125 azure-pipelines.yml:527 .github/workflows/azure-stage.yml:181 infrastructure/gcp/ENV-MATRIX.md:36 .env.example:70 Production-safety note Production default remains unchanged: absent/invalid API_RATE_LIMIT_PER_MINUTE resolves to 100 , and only stage deploy config sets API_RATE_LIMIT_PER_MINUTE=500 . P2P pre-verifier PASS: mesh-thr-77df628b-c7b4-4c5e-a3ff-1825826bb7f9 / mesh-msg-f642fa23-cb94-476f-af4b-01a63f386be2 Evidence JSON: /tmp/alai/company-mesh-auto-responder/2026-07-09T23-10-17-743Z-mesh-msg-f1bd06a1-d6a6-4b9d-84b8-e60304ecfc39.json Bilko sveRačun Faza 1 — TEST-live aktivacija na demou (MC #103450, 2026-07-11) Bilko sveRačun Faza 1 — TEST-live aktivacija na demou (MC #103450, 2026-07-11) Šta je urađeno HR eRačun (sveRačun/PostLink) submit aktiviran na demou (app.bilko.cloud / app-api.bilko.cloud, ACA bilko-api-demo , rg-bilko-demo) protiv TEST sandboxa test.sveracun.hr — bez stvarne fiskalizacije, reverzibilno. Stanje (tool-verifikovano) Env ( bilko-api-demo rev 0000043): SVERACUN_HR_LIVE=true , SVERACUN_API_KEY=secretref:sveracun-api-key , SVERACUN_SENDER_VAT=HR91276104352 , DEMO_MODE=true . SVERACUN_BASE_URL nije postavljen — kod defaultira na TEST ( SveRacunHrEInvoiceAdapter.kt:70 ), a per-profile api_base_url u DB je također TEST. DB flip (bilko-demo-pg, hr_einvoice_issuer_config ): enabled=true za tačno 2 test org-a — HR demo org 00000000-0000-0029-c000-000000000001 + E2E org 1f9811d2-af38-482d-91d9-229e1acbb37e (2026-07-11 16:33 UTC). Tabela ima SAMO ta 2 reda — nijedan customer org. Gate semantika : submit path gate-uje hr_einvoice_issuer_config.enabled ( IssuerProfileRepository.kt ) + SVERACUN_HR_LIVE env. adapter_config tabela gate-uje samo Storecove (mrtav put). Stale GCP secret-ref u DB redovima je inertan ( EnvVarSecretManagerClient parsira samo ime → env var). Proveo E2E — PASS (2026-07-11) Login CIAM E2E user → org 1f9811d2; faktura INV-2026-054 → POST /invoices/{id}/submit-to-sveracun → HTTP 200, documentId 6a527117f0525843a744646a, fiscalNumber 2026-000002, SUBMITTED . GET /invoices/{id}/sveracun-status + POST poll-sveracun-status → PENDING, lastPolledAt se osvježava (živi sandbox roundtrip). UI dugme "Pošalji eRačun" ( sveracun-submit-button ) postoji i wired. Evidence: /tmp/evidence-103450/ (screenshotovi, HTTP odgovori, pre/post SELECT, verdict.json). Rollback UPDATE hr_einvoice_issuer_config SET enabled=false WHERE org_id IN ('00000000-0000-0029-c000-000000000001','1f9811d2-af38-482d-91d9-229e1acbb37e'); (+ opcionalno SVERACUN_HR_LIVE=false na bilko-api-demo za potpuni STUB.) Nalazi / follow-up MC #105259 : invoice GET/list serializer ne vraća sveracunDocumentId/InternalStatus/ExternalStatus → UI kartica pokazuje "not submitted" nakon uspješnog submita; dugme ostaje klikabilno (backend B4 idempotency brani dupli submit, UX vara). + E2E fixture treba kompletnu HR adresu. CI napomena : main-branch Bilko-CI-CD buildovi padaju na E2E UAT stage-u (poznati rep E2E sprinta 2026-07-10, 7 failova) — nevezano za ovu aktivaciju (nije bilo deploya). Prod aktivacija (stvarna fiskalizacija) = zaseban track #103443: FINA cert + prod secret + SVERACUN_HR_LIVE=true deploy odluka CEO. Kontekst lanca #104171 (Azure secret+env+runbook) → #104172 (arhiva Azure Blob) → #103452 (submit path + B4 idempotency) → #103450 (ovo) → #103443 (live aktivacija, parent). Bilko — Minimax Gap Program (2026-07-20) 1. CEO odluka i kontekst Datum CEO odluke: 2026-07-20 — "Sve odobreno" (svih 6 stavki gap analize). Povod: CEO ask — "vidi jesmo li pokrili sve iz Minimax dokumentacije; eRačun = top seller; storno; plate/odsustva; mob slikanje računa; mob time tracking." Metod: 4 paralelna read-only istraživačka agenta nad kanonskim Bilko repo-om (John, 2026-07-20), rezultat u docs/product/MINIMAX-GAP-ANALIZA-2026-07-20.md . Izvorna dokumentacija (Minimax-derived): docs/product/MINIMAX-MIGRATION-UX-BRIEF.md (P0/P1/P2 backlog), docs/product/BILKO-HR-KNOWLEDGE-ARTICLE-PLAN.md + 33 draft članka, docs/COMPETITIVE-RESEARCH.md §2.8. Program parent: MC #106102 — "PROGRAM parent — 6 CEO-odobrenih stavki gap analize 2026-07-20". Red-zone stavke (iz MC #106102): storno (novac/zakon), eRačun prod send (fiskalno). Phase-gate napomena (ključno — vidi §5): spec taskovi (#106108 payroll odsustva, #106110 time tracking, #106111 migracioni wizard) čekaju CEO potvrdu prije nego se otvore build taskovi. Nijedan od ta tri specA nije autorizovan za gradnju — samo dizajn/plan. 2. MC taskovi programa — status i vlasnik MC # Naslov Prioritet Status Vlasnik Napomena #106102 PROGRAM parent — 6 CEO-odobrenih stavki H in_progress john Parent svih ispod #106103 Storno eRačuna dovršetak: SefHttpClient.cancelInvoice (RS), HR FISK storno submission, skini PRO+ gate sa INVOICE_CREDIT_NOTE H in_progress john 🔴 Red-zone (novac/zakon) #106104 Storno E2E validacija na write org-u (HR-603 MEDIUM #6) H paused john QA za #106103 #106105 eRačun top-seller put A: sveRačun/FINA produkcijski provider onboarding H open john 🔴 Red-zone (fiskalno) #106106 eRačun top-seller put B: send kao primarni CTA u invoice flowu (web) M paused john Gated na #106105 aktivaciju #106107 eRačun top-seller put C: hero/landing poruka redesign M paused john NAKON prod aktivacije #106108 Payroll odsustva SPEC (godišnji/bolovanje/dopust) + JOPPD M paused john SPEC gotov — čeka CEO potvrdu za build #106109 Mobile capture fix paket (crop, OCR confidence, multi-page, telemetrija) M paused john Nalaz gotov, build task nije otvoren #106110 Time tracking SPEC (Tripletex parity) M paused john SPEC gotov — čeka CEO potvrdu za build #106111 Migracioni wizard MIG-01→08 SPEC + fazni plan M paused john SPEC gotov — čeka CEO potvrdu za build #106113 BookStack dokumentacija programa (ova stranica) M in_progress→done skillforge Docs task (ZAKON PLAN) Napomena: MC #106112 (RAG-first hook Phase A recorder) i #106114 (alai-hooks CLI dispatch mismatch) su susjedni ID-jevi ali NISU dio Minimax Gap programa — to su odvojeni SYSTEM taskovi (hook infrastruktura), owner john, nepovezani s Bilko gap analizom. 3. Sažetak nalaza po stavci 3.1 eRačun pozicioniranje — "jednostavno slanje eRačuna" kao top-seller Nalaz: NIJE trenutna poruka. Hero na landing-hr je "Računovodstvo koje razumije hrvatski biznis" + "Jednostavnije nego Minimax. Jeftinije nego Pantheon." + €15/mj. eRačun je svuda označen "FISK 2026 u razvoju · Beta". Zašto: produkcijsko slanje NIJE aktivno bez provider onboardinga (sveRačun aktiviran samo za 2 test org-a). U web appu "Pošalji" = email; eRačun kartica je sekundarna sekcija, ne prvi korak. Zaključak: poruka zahtijeva (a) produkcijsku aktivaciju providera, (b) send kao primarni CTA, (c) redizajn hero poruke — sekvencionisano kao putevi A/B/C (#106105/#106106/#106107). Trenutni copy je pošten (SAFE-02 guardrail), ali ne prodaje ono što CEO hoće da bude top-seller. Poznati srodni blokator: rabat/AllowanceCharge gap u HR eRačun XML (nalaz #105269, čl.79 ZPDV). 3.2 Storno poslanog eRačuna — parcijalno Nalaz: PARCIJALNO. Kreditno odobrenje (storno) postoji lokalno: POST /invoices/credit-note , tip CREDIT_NOTE , GL reversal, UBL tip 381 + BT-25/BT-26, CN-YYYY-NNN numeracija, UI dugme. HR eRačun servis MOŽE serializovati i poslati credit note kroz sveRačun. Rupe: RS/SEF: SefHttpClient.cancelInvoice() NE postoji — storno se ne šalje na SEF. HR FISK B2C storno endpoint nije implementiran (samo metadata lista tipova). BA: nema credit-note podrške uopšte. E2E neverifikovano uživo (HR-603 MEDIUM #6). Feature gate INVOICE_CREDIT_NOTE = PRO+ tier — a storno je zakonska korekcija, ne premium feature (MC #103516 odluka visi). 🔴 Red-zone: novac + zakonska obaveza. Build task #106103, validacija #106104. 3.3 Plate — odmor/permisija/bolovanje — ne postoji Nalaz: NE POSTOJI. Payroll MVP (V101) = registar zaposlenika + bruto→neto HR obračun + payslips. Nula podrške za odsustva: nema absence tabela, nema tipova, nema leave balansa, payslip uvijek uzima puni gross_salary_cents , nema JOPPD šifri za vrste odsustva. Nije ni u PAYROLL-PHASE2-PLAN.md. MARKET-READINESS-MATRIX: Payroll = NOT_IMPLEMENTED za GA. Reuse mogućnost (istraženo u SPEC-u #106108): @alai/hr-core extraction bio je planiran ali nikad izvršen do integracije — skeleton postoji (jedan commit, 2026-04-18), Bilko i Lobby ga nikad nisu wire-ovali. Reuse verdikt: DJELIMIČNO — shema/table skeleton ( LeaveTables.kt ) je reusable kao dizajn-referenca, servisna logika mora biti napisana od nule, direktno u Bilko-vom postojećem obrascu (isto kao što je payroll MVP sam građen bez hr-core). 3.4 Mobilna — slikanje računa — postoji sa slabostima Nalaz: POSTOJI ali sa konkretnim slabostima ( apps/mobile/app/(tabs)/expenses/index.tsx , RN+Expo v0.1.5): document-scanner plugin → fallback na običnu kameru sa allowsEditing: false (nema crop/ispravke) maxNumDocuments: 1 — nema višestraničnih računa kompresija 0.82 prije uploada — gubi sitni tekst prije OCR-a OCR confidence se vraća ali se NE provjerava u UI (niska pouzdanost prolazi bez upozorenja) nema on-device pregleda ekstrakta prije uploada fallback na običnu kameru je tih (nema telemetrije koliko često scanner ne radi) Ovo objašnjava CEO utisak "slikanje računa nije dobro". Fix paket praćen u #106109. 3.5 Mobilna — time tracking (Tripletex paritet) — ne postoji Nalaz: NE POSTOJI. Ni u mobile appu, ni u backendu (nema time-entry ruta/tabela), ni u MOBILE-PRD fazama 0-5. Jedino srodno: putni nalozi (web, HR). COMPETITIVE-RESEARCH bilježi Tripletex time tracking kao add-on NOK 59/user/mj. SPEC verdikt (#106110): Lobby TimerService.kt / TimerRepository.kt / TimerRoutes.kt su 85-90% portable (generic clock-in/out), ali NE pokrivaju billable-time-to-invoice linkage niti approval workflow (to dolazi iz Bilko-ovog vlastitog TravelOrder RBAC obrasca). Revidirana procjena: ~3.5-4.5 sedmica (full-stack: backend+mobile+web+pricing gate), veće od originalne grube procjene 2-3 sedmice. Preporučeni pricing: flat org-wide add-on (ne per-user metered), usklađeno sa Bilko per-org filozofijom. 3.6 Migracioni wizard (MIG-01→08) — nula koda specifično za wizard Nalaz iz gap analize: MIG-01→08 dokumentovano detaljno, nula koda u web appu; landing "Prebaci s Minimaxa" obećava "kontrolnu listu i pregled podataka". Takođe nedostaju: ONB-01→04 (onboarding), R1-01→05 (račun na firmu tok), MOB-01/03 (Today tab), 33 HR knowledge članka (svi draft), Minimax OIB→registar auto-fill pattern. SPEC korekcija (#106111, važno): tvrdnja "nula koda" NIJE tačna za backend infrastrukturu uvoza općenito — postoji direktno primjenjiv arhitekturni template ( V87__gl_bank_import.sql staging+idempotency+RLS obrazac, GlBankService.kt , InboxRoutes.kt accept/reject tok, SupportTicketRoutes.kt queue backbone). Zaključak: MVP je ~70% re-orkestracija postojećih patterna za generic CSV, ne gradnja od nule. Landing copy je već usklađen sa SAFE-01/02 (ne laže eksplicitno, ali gap je i dalje realan — nema in-app checklist/upload/wizard). Preporučeni MVP rez (F1): MIG-01+02+03 (upload u support queue, BEZ auto-parse)+MIG-07 (support queue)+SAFE-01/02/03, procjena M (~1 sprint). Puni wizard sa mapping-preview/auto-validacijom (F2/F3) zahtijeva security review (malware scan, CSV injection) i čeka odgovor na Open Question #1 (koji export formati su stvarno dostupni). 4. Linkovi i putanje na specove i evidence Dokument Putanja Glavna gap analiza (6 stavki) ~/business/ALAI-Holding-AS/products/Bilko/docs/product/MINIMAX-GAP-ANALIZA-2026-07-20.md Payroll odsustva spec (#106108, CodeCraft/Petter Graff) ~/system/evidence/106108/PAYROLL-ABSENCE-SPEC.md Time tracking spec (#106110, Skybound) ~/system/evidence/106110/TIME-TRACKING-SPEC.md Migracioni wizard plan (#106111) ~/system/evidence/106111/MIGRATION-WIZARD-PLAN.md Izvorna Minimax UX brief (MIG/ONB/SAFE kodovi) docs/product/MINIMAX-MIGRATION-UX-BRIEF.md (Bilko repo) HR knowledge article plan docs/product/BILKO-HR-KNOWLEDGE-ARTICLE-PLAN.md (Bilko repo) Competitive research (Tripletex time tracking add-on) docs/COMPETITIVE-RESEARCH.md §2.8 (Bilko repo) Payroll Phase 2 plan (JOPPD, RS/BA sekvencioniranje) docs/regulatory/PAYROLL-PHASE2-PLAN.md (Bilko repo) hr-core extraction plan (Sprint 0, orphaned) docs/research/hr-core-extraction-plan.md (Bilko repo) 5. Phase-gate napomena Specovi #106108 (payroll odsustva), #106110 (time tracking) i #106111 (migracioni wizard) su SPEC/PLAN ONLY — nema koda, nema build taskova otvorenih. Sva tri dokumenta eksplicitno navode na vrhu: "Status: SPEC ONLY, no code, no build tasks created" / "PHASE-GATE: ovaj dokument ide CEO-u na potvrdu prije nego se otvori bilo koji build task." Prije otvaranja build taskova za bilo koju od te tri stavke, potrebno je: Payroll odsustva (#106108): CEO odluka o self-service RBAC (ko označava odsustvo), approval workflow (da/ne), i [TREBA HR POREZNA VALIDACIJA] na svim statutarnim brojkama (dani bolovanja na teret poslodavca, HZZO split, carryover pravila) prije F2 (uticaj na obračun). F1 (samo evidencija, bez uticaja na obračun) može ići provizorno. Time tracking (#106110): CEO sign-off na pricing model (flat add-on vs per-user metered) i potvrda RBAC gap-a (novi "staff" role). Migracioni wizard (#106111): CEO potvrda na F1 rez (support-led, bez auto-parse) i odgovor na Open Question #1 (stvarni Minimax export format) prije F2 investicije u parser. Storno (#106103/#106104) i eRačun top-seller put A (#106105) su VEĆ u toku (in_progress/open) jer su H-prioritet red-zone stavke koje CEO odluka od 20.07 direktno pokriva bez dodatnog spec-gate koraka — ali ostaju pod istim red-zone verifikacionim standardom (adversarial review + Proveo E2E prije done). Povezani dokumenti Vidi takoder istrazivanje drzavnih poticaja i uslova zaposljavanja za novi HR d.o.o. entitet (Smart Forge): Smart Forge (HR) — drzavni poticaji i zaposljavanje — istrazivanje 2026-07-21 (odvojena tema, isti book). Smart Forge (HR) — državni poticaji i zapošljavanje — istraživanje 2026-07-21 1. Kontekst Smart Forge = novi d.o.o. u registraciji (~25.07.2026), tech/SaaS (NKD J62 racunalno programiranje), vlasnistvo ALAI grupa/Alem. Ovo je novi hrvatski (HR) entitet za Bilko. Sve nize je web-verifikovano 21.07.2026, izvori navedeni na dnu stranice (sekcija 6). Agent verdikt izvornog istrazivanja: PASS (web-verifikovano 2026-07-21). Izvor: ~/system/evidence/hr-poticaji-smartforge/RESEARCH-2026-07-21.md 2. Programi — MOZEMO / NE MOZEMO MOZEMO aplicirati / koristiti # Program Iznos / uslov Napomena 1 HAMAG-BICRO Program akceleracije ~3M EUR bespovratnih za inovativna poduzeca KANDIDAT #1 (Bilko = AI/SaaS inovacija). Pratiti rok. 2 HAMAG-BICRO zajmovi za pocetnike 25.001–100.000 EUR @ 0,5% fiksno, 10 god., pocek 12 mj. TRENUTNO ZATVOREN (utrosena sredstva) — pratiti novi krug (program do 2029). Uslov: vlasnik/suvlasnik zaposlen puno radno vrijeme u firmi (strukturna odluka!). 3 Jamstva kroz banke (HAMAG + EU) do 300.000 EUR Novi krug otvoren. 4 Podrska digitalizaciji MSP-ova do 120.000 EUR (50–85% sufinanciranje) Sjediste mora biti NUTS2 Panonska/Sjeverna/Jadranska Hrvatska — GRAD ZAGREB ISKLJUCEN . Strateski: registracija Smart Forge van Zagreba (npr. Rijeka/Osijek gdje su pilot partneri Terra/Maricic, ili Dubrovnik — vidi sekciju 3) otvara ovaj poziv. 5 Porezno automatski (bez posebne prijave) Novoosnovani d.o.o. NE placa akontacije poreza na dobit prvu godinu; porez na dobit 10% (<1M EUR prihoda); oslobodjenje doprinosa 1 god. za osobu koja se PRVI PUT zaposljava (neodredjeno); HZZ potpore za zaposljavanje/pripravnistvo Automatsko pravo, nije natjecaj. NE prolazimo (sada) # Program Iznos / uslov Zasto ne prolazimo 6 HZZ potpora za samozaposljavanje do 15.000 + 5.000 digitalno/zeleno + 7.000 povratnici = do 27.000 EUR; rok 30.09.2026 Trazi NEZAPOSLENU osobu u HZZ evidenciji koja osniva firmu. Alem nije u HZZ evidenciji. (Opcija bi postojala samo ako bi HR suosnivac iz HZZ evidencije osnivao — nije nasa postavka.) 7 Digitalni vauceri 2026 fond 3,92M EUR, otvaranje ~rujan Min 5 zaposlenih + pozitivna EBITDA — nova firma ne prolazi jos. 8 Start-up/spin-off mladih istrazivaca 66k–200k EUR Prozor prijava 15.04–15.06.2026 PROSAO; eventualno 2027. Napomene o podobnosti Strano vlasnistvo: nije prepreka za EU/HR MSP programe (bitno: HR registracija + MSP status). ALI MSP status se racuna S POVEZANIM poduzecima (ALAI grupa) — grupa je mala pa MSP status ostaje, dokumentovati strukturu pri prijavi. Iskljucene djelatnosti u zajmovima: trgovina, FINANCIJSKE djelatnosti, kripto... — Bilko/Smart Forge je SOFTVER (J62), NE financijska djelatnost — prolazi, ali paziti na NKD izbor pri registraciji. [TREBA POTVRDA konzultanta/HAMAG] tacni uvjeti povezanosti i da li holding struktura utice na "pocetnik" status. 3. Dubrovnik / NUTS2 nalaz (dopuna, CEO pitanje 21.07) Dubrovacko-neretvanska zupanija = NUTS2 JADRANSKA Hrvatska → KVALIFIKUJE se za MSP digitalizacijski poziv (do 120k EUR, 50–85%). Jedini iskljucen je Grad Zagreb. Svi nacionalni programi (HAMAG akceleracija, zajmovi, jamstva, porezne olaksice) vaze jednako — sjediste irelevantno za njih. Lokalno/zupanijski (skromno): DNZ program razvoja MSP 2025-2027 (npr. Aktivnost 4: sufinanciranje izrade poslovnog plana 50% do 1.000 EUR); Grad Dubrovnik potpore fokusirane na tradicijske obrte (nisu IT fit). DUNEA (regionalna agencija) = kanal za info/EU projekte. Napomena: Dubrovnik je RAZVIJENO podrucje (visok indeks razvijenosti) — nema uvecanih intenziteta potpore kakve dobijaju potpomognuta podrucja, ali NEMA nikakvog iskljucenja. Zakljucak: Dubrovnik radi jednako dobro kao Rijeka/Osijek za sve bitne programe; razlika je samo u sitnim lokalnim mjerama. 4. Zaposljavanje — EGP drzavljani i Djani scenariji Web-verifikovano (hzz.hr sluzbeno): NE MORA biti hrvatski drzavljanin. Drzavljani EU/EGP ( NORVESKA JE EGP ) su po zakonu IZJEDNACENI s hrvatskim drzavljanima za upis u HZZ evidenciju nezaposlenih — uslov je boraviste/prebivaliste u HR (prijava po mjestu boravista). Drzavljani trecih zemalja (npr. samo BiH pasos): upis u evidenciju moguc SAMO ako mogu raditi u HR bez dozvole za boravak i rad; inace ide sustav dozvola (test trzista rada). Kad je osoba u HZZ evidenciji, poslodavac (Smart Forge) moze koristiti HZZ potpore za zaposljavanje/pripravnistvo za tu osobu. BONUS "Biram Hrvatsku": povratak iz EGP zemalja (ukljucujuci Norvesku) nosi fiksnih 7.000 EUR (povratak iz inostranstva) uz samozaposljavanje/mobilnost. Doprinos-olaksica "prvo zaposljavanje" (1 god. oslobodjenja doprinosa na osnovicu, neodredjeno): vezana za prvo zaposlenje — osoba bez ranijeg HR staza vjerovatno kvalifikuje [TREBA POTVRDA racunovodje: da li se gleda samo HR staz ili i inostrani]. Djani scenario (dolazak iz Norveske, EGP pasos) — putevi REDOSLIJED JE KRITICAN: boraviste → HZZ evidencija → tek onda zaposlenje/osnivanje. Pogresan redoslijed = gubitak prava. Scenario Put Iznosi / uslovi A) ZAPOSLENIK u Smart Forge prijavi boraviste u HR → upis u HZZ evidenciju → Smart Forge ga zaposli uz HZZ potporu za zaposljavanje + moguca doprinos-olaksica Jednostavnije, manji iznosi. B) SUVLASNIK/OSNIVAC koji se samozaposljava potpora za samozaposljavanje (do 15k+5k digitalno/zeleno) + 7.000 EUR povratnik iz EGP; ISTOVREMENO zadovoljava HAMAG uslov "vlasnik/suvlasnik zaposlen puno radno vrijeme" za pocetnicki zajam 0,5% Potencijalno do ~27.000 EUR. [TREBA PROVJERA]: uvjeti vlasnickog udjela za potporu za samozaposljavanje + TIMING (osoba mora biti u HZZ evidenciji PRIJE osnivanja/zaposlenja — Smart Forge je vec u registraciji, mozda treba drugi subjekt ili naknadni ulazak — konsultovati HZZ/konzultanta PRIJE poteza). 5. [TREBA PROVJERA] — otvorene stavke Tacni uvjeti povezanosti (ALAI grupa) i da li holding struktura utice na "pocetnik" status — [TREBA POTVRDA konzultanta/HAMAG]. Doprinos-olaksica "prvo zaposlenje": da li se gleda samo HR radni staz ili i inostrani — [TREBA POTVRDA racunovodje]. Scenario B (Djani suvlasnik): uvjeti vlasnickog udjela za potporu za samozaposljavanje + tacan TIMING (HZZ evidencija PRIJE osnivanja/zaposlenja) — [TREBA PROVJERA HZZ/konzultanta prije poteza]. 6. Izvori Osnovno istrazivanje (fetch 2026-07-21): hamagbicro.hr — Program akceleracije (~3M EUR) hamagbicro.hr — novi krug jamstava do 300.000 EUR eufondovi.hr — mali zajmovi za pocetnike mjere.hzz.hr — Potpore za samozaposljavanje (PDF) fiskai.hr — vodic potpora za samozaposljavanje fiskai.hr — vodic porez na dobit esavjetovanja.gov.hr — digitalizacija MSP (NUTS2) eurokonzalting.com — pregled najavljenih natjecaja 2026 mojafirma.hr — olaksice i potpore za zaposljavanje 2026 Dopuna — Dubrovnik/NUTS2: glasgrada.hr — DNZ program potpore dubrovnik.hr — bespovratne potpore likemetkovic.hr — DNZ javni poziv mjere.hr — potpore Dubrovacko-neretvanska zupanija Dopuna — zaposljavanje stranaca (Djani scenario): hzz.hr — prijava stranih drzavljana u evidenciju nezaposlenih newbeta.hzz.hr — drzavljani EU/EGP mjere.hzz.hr — Biram Hrvatsku (mobilnost radne snage 2026) mjere.hzz.hr — Potpore za samozaposljavanje 2026 7. Preporuceni koraci ODLUKA PRIJE REGISTRACIJE: sjediste van Zagreba (Rijeka/Osijek ili Dubrovnik — svi rade jednako dobro za NUTS2 poziv) ako zelimo MSP digitalizacijski poziv do 120k EUR. NKD: J62.01 racunalno programiranje. Prijava na HAMAG mailing/pracenje: novi krug pocetnickih zajmova + Program akceleracije rok. Kod prvog zaposljavanja: iskoristiti oslobodjenje doprinosa (prvo zaposlenje) + HZZ potporu. Za vece bespovratne (2027): start-up istrazivaci prozor april–jun. Ako se razmatra Djani scenario: prije bilo kojeg poteza konsultovati HZZ/konzultanta oko TIMING-a (evidencija prije osnivanja/zaposlenja) i uvjeta vlasnickog udjela za scenario B. Evidencija: ~/system/evidence/hr-poticaji-smartforge/RESEARCH-2026-07-21.md (ukljucuje dopunu 1 — Dubrovnik scenario, i dopunu 2 — zaposljavanje stranaca/Djani). Objavljeno: Skillforge, 2026-07-21. Bilko Operational Runbook — Azure Container Apps Bilko — Operational Runbook Azure is canonical. Bilko runs on Azure Container Apps (swedencentral, resource group rg-bilko-demo , subscription 5b0b4d9b ). The previous non-Azure environment this runbook described is dead — legacy billing was exhausted 2026-06-14 (MC #103633). Platform-specific legacy references have been removed below. Deep architecture reference (source of truth for topology/diagrams): DEPLOY-MAP.md (root) Environment model: docs/infrastructure/ENVIRONMENTS.md Live CI/CD config: azure-pipelines.yml (root) Version: 4.0 (Azure Container Apps era — B4 doc cleanup, MC #101088) Last updated: 2026-07-28 Supersedes: v3.1 (2026-04-30, pre-Azure era) Table of Contents System Overview Pre-Deployment Checklist Deploying the API Deploying the Web Frontend Database Operations Rollback Procedures Database Server Operations (Azure PostgreSQL) Troubleshooting Guide Monitoring and Alerts Emergency Contacts and Escalation 1. System Overview Architecture Bilko is a Turborepo monorepo with two deployed apps, both running as Azure Container Apps : Web ( apps/web/ ) — Next.js 15 frontend, ACA app bilko-web-demo (+ bilko-web-stage ) API ( apps/api/ ) — Kotlin/Ktor + Exposed backend, ACA app bilko-api-demo (+ bilko-api-stage ). This is the sole canonical backend (ADR-020/ADR-021) — the old Express apps/api-express/ was deleted 2026-05-02 (MC #10493). Database — Azure PostgreSQL flexible server bilko-demo-pg (PostgreSQL 16, swedencentral) Full topology diagram (Cloudflare edge → ACA → Azure PG) lives in DEPLOY-MAP.md — do not duplicate it here; that file is kept current on every infra change. Apps in This Monorepo App Path Port (dev) Deployed to (ACA app name) Web apps/web/ 3000 bilko-web-demo / bilko-web-stage API apps/api/ 8080 bilko-api-demo / bilko-api-stage E2E apps/e2e/ — CI only (Playwright, stage FQDNs) Key Packages Package Path Purpose Database apps/api/src/main/resources/db/migration/ Flyway SQL migrations (canonical schema source) UI packages/ui/ Shared UI components CI/CD Pipeline Single canonical pipeline: azure-pipelines.yml (root), Azure DevOps Bilko-CI-CD (definitionId=1), org alai-holding , project Bilko , git remote azdo . Triggers on push to main and tag v* . GitHub Actions is dead/abandoned — do not rely on .github/workflows/* for deploys. Stages: CI_Gates (8 jobs) → Build → Flyway_Migrate → Deploy_Stage → E2E_UAT → Promote_Demo (manual CEO approval). Full stage-by-stage detail: DEPLOY-MAP.md § CI/CD Pipeline. 2. Pre-Deployment Checklist Run this before EVERY production ( Promote_Demo ) deployment: CI_Gates stage is green on the branch (Prettier, TSC, Vitest, Kotest, Trivy FS, Gitleaks, Semgrep) Build stage produced digest-pinned images in bilkodemo.azurecr.io Flyway_Migrate succeeded against bilko-demo-pg — no failed migrations in flyway_schema_history Deploy_Stage + E2E_UAT passed against bilko-*-stage If schema changed: new V{N+1}__description.sql exists in apps/api/src/main/resources/db/migration/ — NEVER edit an existing migration No open Trivy/Semgrep HIGH/CRITICAL findings introduced by this change Environment variables/secrets verified in Azure Key Vault kv-bilko-demo2 (via ADO variable group bilko-kv-demo ) ZAKON PI2 six hard checks ready to run post-deploy (see ~/system/rules/zakon-pi2-deploy-verification.md ) 3. Deploying the API API source: apps/api/ (Kotlin + Ktor + Exposed) Deploy target: Azure Container App bilko-api-demo (+ bilko-api-stage ) — resource group rg-bilko-demo , environment bilko-demo-env Build pipeline: azure-pipelines.yml Build stage — docker buildx linux/amd64 → Azure Container Registry Docker image: bilkodemo.azurecr.io/bilko-api:demo-{SHA} (production) / bilkodemo.azurecr.io/bilko-api:stage-{SHA} (stage) 3a. Automatic Deploy (Stage — on every main push) Deploy_Stage runs automatically on the bilko-selfhosted pool (FORGE agent bilko-forge-1 ) after Build + Flyway_Migrate succeed. It updates bilko-api-stage and bilko-web-stage to the digest-pinned image from Build . Monitor: az pipelines runs list --org https://dev.azure.com/alai-holding --project Bilko --pipeline-ids 1 --top 5 or the Azure DevOps pipeline UI. 3b. Promote to Production (CEO-gated manual approval) Promote_Demo requires manual CEO approval in the ADO environment bilko-demo , then rebuilds demo-{SHA} images from the same commit and shifts bilko-api-demo ingress traffic 100% to the new revision (multiple-revisions mode, MC #105368). # Trigger a manual pipeline run when Promote_Demo did not auto-open (BatchedCI can skip it) az pipelines run --org https://dev.azure.com/alai-holding --project Bilko --branch main --id 1 # Approve via Azure DevOps UI (Pipelines > Environments > bilko-demo) once queued, # or via API per CEO chat-authorization (see project memory for the API-approve pattern). 3c. Verify API Deployment # Azure Container Apps status + FQDN az containerapp list -g rg-bilko-demo \ --query "[].{name:name, state:properties.provisioningState, fqdn:properties.configuration.ingress.fqdn}" \ -o table # Latest revision az containerapp revision list -n bilko-api-demo -g rg-bilko-demo --query "[0].name" -o tsv # Health check (brand domain, via CF Worker) curl -s https://api.bilko.cloud/api/v1/health curl -s https://api.bilko.io/api/v1/health curl -s https://api.bilko.company/api/v1/health # Direct ACA FQDN health check curl -s https://bilko-api-demo.purplebeach-f004d490.swedencentral.azurecontainerapps.io/api/v1/health 3d. Required Secrets / Environment Variables (API) Managed as Azure Container Apps secrets + env vars, sourced from Key Vault kv-bilko-demo2 (variable group bilko-kv-demo in ADO). ACA write-only secrets return NULL on read — always cross-check against Key Vault, never assume a value from a stale doc. Secret / Var Description db-password bilko_admin Azure PostgreSQL password jwt-secret Access token signing secret jwt-refresh-secret Refresh token signing secret groq-api-key LLM provider key (API) resend-api-key Transactional email provider key field-encryption-key PII field-level encryption key field-hmac-key PII field-level HMAC key CORS_ORIGINS Allowed origins — see DEPLOY-MAP.md OCD-12 for the stage custom-domain gotcha 4. Deploying the Web Frontend Frontend source: apps/web/ (Next.js 15) Deploy target: Azure Container App bilko-web-demo (+ bilko-web-stage ) Build pipeline: Same azure-pipelines.yml Build stage — builds apps/web/ alongside the API in one pipeline run Production domains: app.bilko.io , app.bilko.cloud , app.bilko.company (routed through Cloudflare Worker bilko-edge-proxy , which rewrites Host/SNI to the ACA FQDN) 4a. Automatic Deploy (Stage) / 4b. Promote to Production Same pipeline and same manual-approval gate as the API (§3a/§3b) — web and API are built and promoted together from one Azure DevOps run. 4c. Required Environment Variables (Web) Managed via ACA env vars on bilko-web-demo / bilko-web-stage . Variable Description NEXT_PUBLIC_API_URL API base URL — currently baked at build time (see DEPLOY-MAP.md OCD-9 for the runtime-URL follow-up) NEXT_PUBLIC_APP_ENV production or staging 4d. Verify Web Deployment az containerapp revision list -n bilko-web-demo -g rg-bilko-demo --query "[0].name" -o tsv curl -sI https://app.bilko.cloud/ | head -1 curl -sI https://app.bilko.io/ | head -1 curl -sI https://app.bilko.company/ | head -1 5. Database Operations Migration tool: Flyway 11 (NOT Prisma — Prisma was tied to the deleted Express backend). Migration files live in apps/api/src/main/resources/db/migration/V*.sql . 5a. Running Migrations Migrations run automatically in the Flyway_Migrate pipeline stage against bilko-demo-pg on every main push, before Deploy_Stage . Manual run (rare — CI is the default path): docker run --rm \ -v "$(pwd)/apps/api/src/main/resources/db/migration:/flyway/sql" \ flyway/flyway:11 \ -url="jdbc:postgresql://bilko-demo-pg.postgres.database.azure.com:5432/bilko?sslmode=require" \ -user=bilko_admin \ -password="$DB_PASSWORD" \ migrate RULE: Never edit an existing V*.sql migration. Always create a new V{N+1}__description.sql . Reserve the version number carefully — two independent branches taking the same V number is a recurring collision (see technical_bilko_promote_manual_and_flyway_collisions_2026-07-20.md ); check git ls-tree azdo/main -- apps/api/src/main/resources/db/migration/ before picking a number. 5b. Checking Migration Status psql "host=bilko-demo-pg.postgres.database.azure.com port=5432 dbname=bilko user=bilko_admin sslmode=require" \ -c "SELECT version, description, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 20;" All rows should show success = t . 5c. Rolling Back a Migration Flyway (community edition, as used here) does not support automatic migration rollback. Option 1 (preferred): Write a new compensating migration that undoes the change (new V{N+1} file with inverse DDL/DML). Option 2 (last resort): Restore from an Azure PostgreSQL backup (§7f). Data written after the migration will be lost. 5d. Database Backup and Restore Azure PostgreSQL flexible server provides automatic backups (default retention — verify current setting via az postgres flexible-server show ). For an on-demand logical backup: pg_dump "host=bilko-demo-pg.postgres.database.azure.com port=5432 dbname=bilko user=bilko_admin sslmode=require" \ --format=custom --no-acl --no-owner \ --file=bilko-backup-$(date +%Y%m%d-%H%M%S).dump Restore: pg_restore --dbname="host=... user=bilko_admin sslmode=require" \ --no-acl --no-owner \ bilko-backup-YYYYMMDD-HHMMSS.dump 6. Rollback Procedures 6a. API Rollback (Azure Container Apps) # List revisions, newest first az containerapp revision list --name bilko-api-demo --resource-group rg-bilko-demo \ --query "[].{name:name,created:properties.createdTime,active:properties.active}" -o table # Shift 100% traffic to the last known-good revision az containerapp ingress traffic set --name bilko-api-demo --resource-group rg-bilko-demo \ --revision-weight =100 6b. Web Rollback (Azure Container Apps) az containerapp revision list --name bilko-web-demo --resource-group rg-bilko-demo \ --query "[].{name:name,created:properties.createdTime,active:properties.active}" -o table az containerapp ingress traffic set --name bilko-web-demo --resource-group rg-bilko-demo \ --revision-weight =100 6c. Edge Rollback (Cloudflare) # Worker (bilko-edge-proxy) rollback cd apps/edge-proxy && wrangler rollback # CF Pages (marketing landings) — via dashboard: # Pages > project (bilko-io / bilko-cloud / bilko-company) > Deployments > "Rollback to this deployment" 6d. Database Rollback IMPORTANT: Database rollbacks are destructive if data was written after the migration ran. Always confirm with the team before rolling back the database. Steps: Take a current-state backup first (§5d) Write a compensating migration (preferred over restore — see §5c Option 1) Only restore from backup if data loss is acceptable and a pre-migration backup exists 7. Database Server Operations (Azure PostgreSQL) Database: Azure PostgreSQL flexible server bilko-demo-pg , B1ms, PostgreSQL 16, region swedencentral, resource group rg-bilko-demo Schema source of truth: Flyway migrations, apps/api/src/main/resources/db/migration/ App user: bilko_admin (NOT bilko ) — password in ACA secret db-password / Key Vault kv-bilko-demo2 Shared instance: stage and demo currently share this one server (see DEPLOY-MAP.md OCD-3 for the isolation tradeoff) 7a. Connecting (Local / Admin) psql "host=bilko-demo-pg.postgres.database.azure.com port=5432 dbname=bilko user=bilko_admin sslmode=require" Firewall: FORGE's public IP is allow-listed (rule forge-ci-runner ); Microsoft-hosted CI agents use the allow-azure-services rule. Connecting from an unlisted IP will time out — add a temporary firewall rule via az postgres flexible-server firewall-rule create rather than widening allow-azure-services . 7b. Running the RLS Audit # Via test suite (preferred) ./gradlew :apps:api:test --tests "*RlsAudit*" Tables that MUST have RLS enabled include (non-exhaustive — see the audit test for the authoritative list): organizations , users , invoices , invoice_items , expenses , transactions , bank_accounts , bank_transactions , contacts , logged_actions . 7c. Verifying RLS Policies in PostgreSQL -- List all RLS policies SELECT schemaname, tablename, policyname, cmd, qual FROM pg_policies ORDER BY tablename, policyname; -- Check which tables have RLS enabled SELECT relname AS table_name, relrowsecurity AS rls_enabled FROM pg_class WHERE relnamespace = 'public'::regnamespace AND relkind = 'r' ORDER BY relname; 7d. Enabling RLS on a Table ALTER TABLE public.table_name ENABLE ROW LEVEL SECURITY; ALTER TABLE public.table_name FORCE ROW LEVEL SECURITY; -- Example org-scoped policy CREATE POLICY org_isolation ON public.table_name USING (organization_id = current_setting('app.current_org_id')::uuid); 7e. Backup and Restore (Azure PostgreSQL server-level) # Server status and current config az postgres flexible-server show -g rg-bilko-demo -n bilko-demo-pg \ --query "{name:name, state:state, version:version, location:location}" -o table # Point-in-time restore to a new server (does NOT overwrite the source — verify before promoting) az postgres flexible-server restore \ --resource-group rg-bilko-demo \ --name bilko-demo-pg-restored \ --source-server bilko-demo-pg \ --restore-time "2026-07-28T00:00:00Z" Also see the logical pg_dump / pg_restore path in §5d for a single-database, portable backup. 8. Troubleshooting Guide API Down Symptoms: GET /api/v1/health returns non-200 or times out. Steps: Check Azure status for the region: az containerapp show -n bilko-api-demo -g rg-bilko-demo --query "properties.provisioningState" Check container logs: az containerapp logs show -n bilko-api-demo -g rg-bilko-demo --tail 100 Check recent revisions for a bad deploy: az containerapp revision list -n bilko-api-demo -g rg-bilko-demo -o table If a bad revision is live, roll back (§6a) Check the Cloudflare Worker ( bilko-edge-proxy ) isn't misrouting — curl the direct ACA FQDN to isolate edge vs. origin If not resolved in 10 minutes: rollback (§6) and open a P1 per §10 High Error Rate Symptoms: More than 5% of API requests returning 5xx errors. Steps: az containerapp logs show -n bilko-api-demo -g rg-bilko-demo --tail 200 — look for error patterns Check Application Insights / Azure Monitor (if wired for this service) for new error groups Health check: curl https://api.bilko.cloud/api/v1/health Check if a Flyway migration ran recently — look for schema mismatches in the logs Check for too many connections errors (see §8 DB Connection Pool Exhausted below) Restart the revision if connection-pool errors persist: az containerapp revision restart -n bilko-api-demo -g rg-bilko-demo --revision Database Unreachable Symptoms: Health endpoint shows "database": "error" , connection errors in logs. Steps: az postgres flexible-server show -g rg-bilko-demo -n bilko-demo-pg --query "state" Verify the db-password secret / connection string is current: az containerapp secret list -n bilko-api-demo -g rg-bilko-demo (values are NULL by design — cross-check against Key Vault kv-bilko-demo2 , not against this doc) Test connection directly: psql "host=bilko-demo-pg.postgres.database.azure.com ... sslmode=require" -c "SELECT 1;" Check firewall rules haven't been tightened/removed: az postgres flexible-server firewall-rule list -g rg-bilko-demo -n bilko-demo-pg -o table Check connection pool exhaustion (below) DB Connection Pool Exhausted Symptoms: too many connections errors, requests queuing indefinitely. Steps: Check current connections: SELECT count(*), state FROM pg_stat_activity GROUP BY state; Find long-running queries: SELECT pid, now() - query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' AND now() - query_start > interval '30 seconds'; Kill a stuck query if safe: SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = target_pid; Restart the API revision to release the connection pool ( az containerapp revision restart ) Consider PgBouncer / connection-pooling middleware if this recurs under load Slow Responses Symptoms: p95 response time above 2 seconds. Steps: Check for slow queries: SELECT query, calls, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 20; Check for missing indexes on frequently filtered columns Look for N+1 query patterns in Exposed usage Use EXPLAIN ANALYZE on suspect queries SSL Certificate Issues echo | openssl s_client -connect api.bilko.cloud:443 2>/dev/null | openssl x509 -noout -dates Cloudflare-fronted domains ( app.* , api.* , bilko.io/.cloud/.company ): CF-managed, auto-renews — check Cloudflare dashboard if issues. Stage custom domains ( web-stage.bilko.cloud , api-stage.bilko.cloud ): ACA-managed certs ( SniEnabled ), direct DNS-only CNAME (not proxied) — check az containerapp hostname list -n bilko-api-stage -g rg-bilko-demo for cert binding state. Deployment Fails in CI Common causes: Failure Fix CI_Gates lint/TSC fail Fix and push — see which of the 8 jobs failed in the ADO run Trivy HIGH/CRITICAL Update the dependency or image base, re-run Flyway_Migrate fails Bad SQL or a version-number collision (see §5a) — never edit an applied migration Deploy_Stage health check fails New revision not passing /api/v1/health or / within the 20s wait — check logs E2E_UAT skips Usually a stale BILKO_E2E_TOKEN_SECRET in bilko-kv-demo — see DEPLOY-MAP.md OCD-1 9. Monitoring and Alerts See monitoring/ directory: monitoring/uptime.json — uptime check configuration monitoring/alert-rules.json — alert thresholds and routing monitoring/metrics.md — key metrics with targets Quick health commands: # Production API (brand domains) curl -s https://api.bilko.cloud/api/v1/health curl -s https://api.bilko.io/api/v1/health curl -s https://api.bilko.company/api/v1/health # Production Web curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" https://app.bilko.cloud # Stage curl -s https://api-stage.bilko.cloud/api/v1/health curl -sI https://web-stage.bilko.cloud/ | head -1 Log locations: Component Log location API (ACA) az containerapp logs show -n bilko-api-demo -g rg-bilko-demo Web (ACA) az containerapp logs show -n bilko-web-demo -g rg-bilko-demo DB (Azure PG) Azure Portal > bilko-demo-pg > Monitoring > Logs, or pg_stat_activity / pg_stat_statements CI/CD Azure DevOps > Bilko-CI-CD pipeline > run > stage logs Edge (CF Worker) wrangler tail against bilko-edge-proxy , or Cloudflare dashboard 10. Emergency Contacts and Escalation Role Contact Method On-call / CEO Alem Basic (alem@alai.no) Direct / Slack Azure support portal.azure.com support Web Azure DevOps dev.azure.com/alai-holding Web Cloudflare support dash.cloudflare.com support Web Escalation path: P1 (Production down): Fix immediately per §8 Troubleshooting; notify Alem directly if not resolved within 15 min. P2 (Degraded performance): Investigate per §8; notify Alem if unresolved within 1 hour. P3 (Non-urgent): Log as an MC task ( mc.js add ), handle in the next work cycle. Last updated: 2026-07-28 — B4 doc cleanup pass (MC #101088): rewritten for Azure Container Apps (all legacy platform and ORM references removed; content grounded against DEPLOY-MAP.md and apps/api/src/main/resources/db/migration/ ). Bilko main — scheduled validation backstop (MC #106809, 2026-08-05) Bilko main — scheduled validation backstop (MC #106809) Datum: 2026-08-05 · Builder: FlowForge · Merge: PR #317 (John, zeleni build #941) · Nezavisna verifikacija: 8/8 PASS (peer-verdict u evidence) Problem Push-triggered batchedCI buildovi na Bilko main otkazivali su jedni druge u lancu (org ima 1 self-hosted parallel slot — kontencija, ne bug u batch:true ): main danima bez završenog runa (zadnji zeleni 2026-08-03 16:48), tri Gate-0 merža nikad post-merge validirana. Rješenje (minimalno, 2 aditivne izmjene u azure-pipelines.yml ) schedules: cron 0 */3 * * * (samo main, always: false — vrti se samo ako ima novih commita). Jedna linija u Build stage uslovu: eq(variables['Build.Reason'], 'Schedule') — scheduled run izvršava PUN lanac. Scheduled trigger klasa nije podložna push-vs-push batch otkazivanju → u mirnijem prozoru run završi do kraja. batch:true i pr.autoCancel netaknuti (kelsey dissent iz forge 106804 D3 ispoštovan). Dokaz PR #317: merž SAMO nakon zelenog validation builda #941 (nova blokirajuća disciplina). Prvi scheduled run #944 (20260805.3) : completed/ succeeded , timeline potvrđuje da su CI Gates, Build, Flyway Migrate, Deploy→Stage i E2E UAT svi stvarno izvršeni (jedino Promote→Demo skipped — ručno odobrenje, očekivano). Nezavisna peer-verifikacija: 8/8 tvrdnji živim REST pozivima; merge diff = tačno 1 fajl. Evidence: ~/system/evidence/106809/ (+ peer-verify/independent-peer-verdict-106809.md ). Napomene Ponašanje kroz više tikova (svaka 3h + dedup) posmatrati narednih dana — dizajnirano po ADO semantici, prvi tik dokazan. Push izveden REST API-jem jer turbo binary u agent sandboxu biva SIGKILL-an (pre-push hook nemoguć) → MC #106854; sporedna posljedica: commit autorstvo = PAT identitet. Povezano: #106804 (QODY gate, ZATVOREN lanac), #106811 (mail digest — čeka par mirnih dana), #106802/#106841 (slot/Entra — pauzirano, CEO odluka). Bilko /login silent auth failure — dijagnoza i fix (MC #106849) Bilko /login silent auth failure — dijagnoza i fix (MC #106849) Datum: 2026-08-05 · PR: #318 ( fix/106849-login-dead-click , commit 3df83cd1 ) · Status: no active outage; hardening + observability fix Simptom Klik na "Prijavite se Microsoft računom" na app.bilko.cloud/login naizgled "ne radi ništa" — korisnik ostane na /login bez ikakve poruke, bez konzolnih grešaka, bez vidljivih network poziva. Root cause (potvrđeno live forenzikom) Frontend je bio ISPRAVAN: React onClick → MSAL loginRedirect() → top-level navigacija na ciamlogin.com/.../authorize se uredno izvršavala. Microsoft CIAM authorize je za sesiju s postojećim kolačićima vraćao HTTP 503 (čist curl bez kolačića: 200) — vrlo vjerovatno throttling zbog desetina automatizovanih pokušaja u kratkom periodu. Aplikacija je taj neuspjeh tiho gutala : povratak na /login bez poruke (503 prije ikakvog MSAL callbacka ⇒ MSAL nema event koji bi opalio). Dijagnostička zamka: top-level navigacije se ne vide kao XHR u network alatima i ne ostavljaju konzolne greške; uz to, sintetički klikovi browser-ekstenzije nisu ni stizali do stranice (automation artefakt) — vidi memory technical_extension_clicks_can_be_silent_noops_2026-08-05 . Fix (D3 — error surfacing) AUTH_ATTEMPT_PENDING_KEY sessionStorage marker postavljen neposredno prije loginRedirect() . Na sljedećem MSAL init-u: marker prisutan + 0 accounta ⇒ authError (pokriva fail-prije-callbacka); dodatno obrađen EventType.LOGIN_FAILURE . /login renderuje vidljiv alert ( data-testid="entra-auth-error" ) + retry dugme, svih 5 lokala (en/hr/bs/sr-Latn/sr-Cyrl). 7 novih testova u apps/web/test/login-silent-auth-fail-106849.test.tsx (oba puta + negativne kontrole); dokazano padaju na pre-fix kodu. Regression gate (D4) azure-pipelines.yml → Promote_Demo sada, odmah poslije curl-only ZAKON PI2 provjere, izvršava real-browser MSAL login smoke (postojeći login-dashboard.spec.ts ) protiv demo artefakta ( DEMO_WEB_URL=https://app.bilko.cloud ). Zatvoren strukturni gap: stage i demo su odvojeni buildovi, a login test je do sada postojao samo za stage. Verifikacija Nezavisni verifier: 35/35 testova samostalno izvršeno, scope čist, verdikt PASS — transcript: /Users/makinja/system/evidence/106849/verifier-transcript-2026-08-05.md CEO ljudski klik (telefon, čist browser): Microsoft login stranica se uredno otvara. Puna forenzika: /Users/makinja/system/evidence/bilko-login-dead-2026-08-05/repro-evidence.md + MC #106849 komentari. Lekcije "Mrtav klik" iz automatizacije ≠ mrtav klik: trijaža = instrumentacija stranice → programatski .click() → ljudski klik. curl health check je slijep za auth petlje; login smoke mora gađati artefakt koji korisnici stvarno dobijaju. Tihi auth fail je UX bug prvog reda: svaki neuspjeh prijave mora biti vidljiv korisniku. Povezani taskovi: MC #106849 (fix), #106834 (automation artefakt), #106850 (anvil incident), #106856 (subagent identity), #106858 (msal-provider minor). bilko.cloud landing — honest claims sweep (MC #106980, 2026-08-08) bilko.cloud landing — honest claims sweep (MC #106980) Datum: 2026-08-08 · Status: LIVE na produkciji · PR: 331 (azdo, fix/106980-landing-claims → main) · Commit: 149bb1a0 Šta je promijenjeno i zašto Izvor: ~/system/evidence/bilko-fiken-gap/LANDING-NALAZI.md (07.08) — landing je reklamirao zakonski obaveznu funkciju kao "beta". CEO nalog 08.08: ukloniti sve netačne/podcjenjujuće tvrdnje. Stavka Prije Poslije Hero eRačun "FISK 2.0/eRačun priprema dostupna u beta fazi " "eRačun (FISK 2.0) podrška je integrirana prema HR CIUS 2025 standardima — produkcijsko slanje je u fazi aktivacije " (CEO odabrao varijantu V1, 2026-08-08) Cjenovne kartice CTA /demo?country=HR ("Pogledaj demo") /register?plan=trial ("Isprobaj besplatno") — 2× eRačun tag na karticama "(beta — uskoro u produkciji)" "(u aktivaciji)" PSD2 auto-sync tag "(beta — uskoro u produkciji)" — funkcija ne postoji "(u pripremi)" — bez datuma i obećanja CSV bankovni uvoz istaknut kao feature (reklamiranje slabosti) "Uvoz bankovnih izvoda", neutralno JSON-LD / FAQ / stat badge "beta fazi" na 7 mjesta 0; product-level softwareVersion: beta namjerno zadržan (beta program stvarno traje) schema.org addressCountry: NO uz Zagreb HR Badge "FISK 2.0 u razvoju" "FISK 2.0 u aktivaciji" Model istine (granice tvrdnji) SVERACUN_HR_LIVE je OFF → ne smije se tvrditi živo produkcijsko slanje. "U fazi aktivacije" je tačno: produkcijski API ključ primljen 03.08 (MC #106981), aktivacija čeka FiskAplikacija potvrde (AMS + PostLink ovlaštenje) i komercijalne uslove. PSD2/Open Banking: provider nije izabran (MC #106365 paused) → samo "u pripremi", bez rokova. Deploy i verifikacija Deploy: wrangler pages deploy → CF Pages projekat bilko-cloud , --branch=main (production). Napomena: bez --branch=main deploy ide na preview alias, apex se ne mijenja. Verifikacija: Playwright 11/11 PASS na živom apexu ( browser-verification.json , 4 screenshota uklj. mobile 390×844) + curl content grep ( apex-verify-20260808.txt ). Nezavisni peer: native-peer-verify-transcript.md — Verdict: PASS, 9/9 tvrdnji. Evidence ~/system/evidence/106980/ — builder-evidence.md, HERO-VARIJANTE.md, apex-verify-20260808.txt, browser-verification.json, browser-screenshots/, native-peer-verify-transcript.md, pr-created-20260808.json. Napomene za budućnost apps/landing-hr/index.html JE izvor (nema više .tsx source-a u tom appu) — direktne HTML izmjene su trajne. PR 331 poravnava repo s live artefaktom; merge kad backend CI ozdravi (#106960) — live sajt ne zavisi od toga. Bilko Feature — Document Inbox / Ulaz dokumenata (MC #104515) Bilko — Document Inbox / Ulaz dokumenata (MC #104515) Status: BUILT AND LIVE on azdo/main (verified 2026-08-08 by reading apps/api and apps/web at the tip of azdo/main , commit 88769328 ). This page documents the actual shipped implementation — not a plan. The original MC #104515 gap audit (2026-06-29) flagged Document Inbox as "MISSING"; it shipped afterward across three merges: MC What shipped Merge #104515 (Phase 1) Capture-first upload → pending review queue → book as expense / reject PR 30, fee37dc8 #104519 Proveo route-layer test coverage for Phase 1 same PR #105687 Third terminal status archived — permanent document archive ( /dokumenti ) for documents that never become an expense e2ee2a4a #106195 UX redesign of the inbox/ ulazni-racuni screens 88769328 1. Concept Two distinct product surfaces share one table ( inbox_items ): /inbox ("Ulaz dokumenata") — the active review queue. A user uploads a receipt/invoice scan before any accounting record exists. Each upload becomes a pending row. From there it is either booked (creates an Expense and links back), rejected (discarded, reason optional), or archived (see below). /dokumenti ("Arhiva dokumenata", MC #105687) — permanent archive for documents that will never become an expense: contracts, bank statements, delivery notes ( otpremnica ), insurance policies. Modeled as a third terminal status on the same row rather than a new table (see design rationale in the V122 migration, §2). This is deliberately distinct from ExpenseDocuments (V40), which is attach-to-an- existing -expense, and from ReceivedEInvoices (V139), which is the Storecove e-invoice webhook capture (has OIB/UBL fields, no OCR). 2. Schema Table inbox_items , defined in V106__document_inbox.sql (Phase 1) and widened by V122__inbox_archive_documents.sql (archive feature). Kotlin Exposed object: InboxItems in apps/api/src/main/kotlin/no/alai/bilko/models/Tables.kt . V106 — base table: Identity: id (UUID PK), org_id (FK → organizations , ON DELETE CASCADE ) Storage: storage_url , storage_key , original_filename , content_type , file_size , checksum_sha256 , storage_backend ( r2 | local | unknown ) — mirrors the ExpenseDocuments / ReceiptService.uploadDocument storage pattern Lifecycle: status ( pending | booked | rejected , widened to add archived in V122), uploaded_by Phase 2 OCR fields (nullable, reserved, NOT populated by Phase 1 code): extracted_amount NUMERIC(19,4) , extracted_currency CHAR(3) , extracted_date DATE , extracted_vendor VARCHAR(500) , extracted_vat NUMERIC(19,4) , ocr_confidence NUMERIC(5,4) (checked 0.0000–1.0000 ) — the migration header states these are for Azure Document Intelligence , deferred. Booking linkage: booked_expense_id , booked_invoice_id , booked_at , booked_by Rejection: rejection_reason , rejected_at , rejected_by Audit: created_at , updated_at Constraints: status check, file_size > 0 , storage_backend allowlist, ocr_confidence range check Indexes: (org_id, status) , (org_id, created_at DESC) , (uploaded_by) RLS: ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY , policy org_isolation scopes every row to current_setting('app.current_org_id')::uuid for role bilko_app — same pattern as expense_documents (V40). V122 — archive extension (adds the archived outcome, MC #105687): Widens the status CHECK to include archived New columns: document_type (nullable, enum-checked only when populated: contract | statement | delivery_note | insurance_policy | other ), contact_id (nullable FK → contacts , ON DELETE SET NULL ), tags TEXT[] (default '{}' ), archived_at , archived_by New indexes: partial index on archived_at where status='archived' , partial index on document_type , index on contact_id , GIN index on tags Design decision (documented in the migration header): extend inbox_items rather than create a new archived_documents table, to avoid duplicating storage/RLS/index plumbing. booked_expense_id is reused (not status-changing) for the "naknadno vezanje" (late-link) flow — e.g. an otpremnica archived first, linked to an expense once the račun arrives later. Retention is explicitly NOT enforced in this migration — HR knjigovodstvene isprave retention (an 11-year candidate) needs validation with the bilko-racunovodstvo-hr domain expert before any auto-deletion ships. No expiry logic exists today. RLS needs no change — row-scoped policy from V106 automatically covers new columns. 3. Routes Defined in apps/api/src/main/kotlin/no/alai/bilko/routes/InboxRoutes.kt , wired in Routing.kt via inboxRoutes() and documentsRoutes() , service layer InboxService.kt (DI singleton in DI.kt ). inboxRoutes() — mounted under /inbox : Method Path Purpose GET /inbox/count Pending badge count (dashboard bell + sidebar) — registered before /{id} to avoid Ktor trie ambiguity GET /inbox Paginated list; query params status , page , perPage POST /inbox Multipart upload → creates a pending item GET /inbox/{id} Detail for the review screen POST /inbox/{id}/book Creates an Expense in the same transaction, transitions row to booked , best-effort attaches the original scan to the new expense via ExpenseService.attachDocument POST /inbox/{id}/reject Transitions to rejected , optional { "reason": string } POST /inbox/{id}/archive Transitions to archived (V122); body: documentType (required), contactId (optional), tags (optional) documentsRoutes() — mounted under /documents (backs the /dokumenti screen, MC #105687): Method Path Purpose GET /documents Paginated, filterable list of archived items ( documentType , contactId , tag , dateFrom , dateTo ) POST /documents/{id}/link-expense Late-link an already-archived document to an Expense created separately (body: { "expenseId": string } ) Upload security pipeline (POST /inbox , in order): permission check before multipart parse → UploadSecurityGate.authorizeActor (tenant-bound actor check) → MIME allowlist ( application/pdf , image/jpeg , image/jpg , image/png ) → 20 MB hard cap → empty-file guard → the client-declared Content-Type header check here is a cheap early rejection only (attacker-controlled) — the authoritative control is magic-byte content sniffing + ClamAV malware scan + persisted quarantine/scan-provenance state machine at the shared ReceiptService.uploadObject choke point ( UploadSecurityGate , MC #106852 G1-02), which also gates expense-attach, invoice-receipt, and support-ticket-attachment uploads. The route additionally hard-fails closed (compensates/deletes the stored object) if scan provenance ( scanAttemptId , scanState == "RELEASED" , scanEngine , scanEngineVersion , scannedAt ) is incomplete after upload — an inbox row is never created for an object without a verified clean-scan verdict. 4. RBAC Role hierarchy ( RbacHelper.kt ): viewer (0) < accountant (1) < admin (2) < owner (3). Permission catalog + role grants seeded in V67__rbac_permissions_catalog.sql . Route Permission Roles that hold it GET /inbox/count , GET /inbox , GET /inbox/{id} , GET /documents expense:read viewer, accountant, admin, owner POST /inbox (upload), POST /inbox/{id}/book expense:create accountant, admin, owner POST /inbox/{id}/reject , POST /inbox/{id}/archive , POST /documents/{id}/link-expense expense:categorize accountant, admin, owner No new permission keys were introduced for Document Inbox — it reuses the existing expense:* catalog, treating booking/rejecting/archiving as expense-adjacent classification actions. viewer role can browse the inbox and archive but cannot upload, book, reject, or archive. 5. Frontend apps/web/app/(dashboard)/inbox/page.tsx — list/queue screen: drag-and-drop or file-picker upload (PDF/JPEG/PNG, 20 MB cap), status tabs ( pending / booked / rejected — archived intentionally excluded, it lives on /dokumenti ), dashboard badge via GET /inbox/count . No client-side raw-byte preview; files are proxied through the expense-documents content endpoint. apps/web/app/(dashboard)/inbox/[id]/page.tsx — review/booking detail screen. apps/web/app/(dashboard)/dokumenti/page.tsx — permanent archive screen (MC #105687). Sidebar nav ( apps/web/components/sidebar.tsx ): two entries under the expensesGroup section — { key: 'inbox', href: '/inbox', icon: Inbox } and { key: 'dokumenti', href: '/dokumenti', icon: Archive } — both placed above expenses and purchases in the group. 6. i18n Sidebar labels are localized via apps/web/messages/{bs,en,hr,sr-Cyrl,sr-Latn}.json , navigation namespace: "inbox": "Ulaz dokumenata" "dokumenti": "Arhiva dokumenata" Gap found during this review: the inbox/dokumenti page bodies ( inbox/page.tsx , inbox/[id]/page.tsx , dokumenti/page.tsx ) do not call useTranslations / t(...) — grep for both found zero matches. All in-page copy (labels, buttons, empty states) is hardcoded Bosnian/Croatian JSX, not routed through next-intl. Only the sidebar navigation label is translated. This is a real gap, not a design choice documented anywhere in the code — flagging it here rather than in the "OCR hooks" section since it's a currently-live inconsistency, not deferred work. 7. Phase 2 — OCR hooks (deferred, not built) Both migration headers and the Tables.kt block comment explicitly scope OCR to Phase 2, deferred: "Phase 2 (OCR via Azure Document Intelligence): extracted_* columns are nullable and reserved for Phase 2 population. Phase 1 build leaves them NULL." What exists today as the OCR integration point: Six nullable columns on inbox_items : extracted_amount , extracted_currency , extracted_date , extracted_vendor , extracted_vat , ocr_confidence (0–1 range, checked). No service, route, or background job populates them — confirmed by reading InboxRoutes.kt and InboxService.kt end to end; no reference to Azure Document Intelligence, OCR, or any of the six extracted_* / ocr_confidence fields appears outside the schema/comments. No Phase 2 MC task exists yet for the OCR build itself (only the Phase 1 capture queue, MC #104515, and the archive extension, MC #105687, have shipped). Implication for a future Phase 2 build: the schema already has the landing spot for OCR output; the work is a new async job (upload → queue → Azure Document Intelligence call → populate extracted_* / ocr_confidence → surface a "confirm extracted values" step in the /inbox/{id} review screen before booking). No API contract for that job exists yet. 8. Verification method All facts on this page were read directly from azdo/main (Bilko repo, ~/business/ALAI-Holding-AS/products/Bilko ) at commit 88769328 (2026-08-08), not from prior planning docs or memory: git log --all --grep , git diff main...feat/document-inbox-104515 --stat , git merge-base --is-ancestor to confirm the feature branch's content reached main (Azure DevOps squash-merges, so individual feature-branch commits are not ancestors of main even though the content is — checked via git ls-tree -r azdo/main file presence, not commit ancestry alone). Full reads of V106__document_inbox.sql , V122__inbox_archive_documents.sql , InboxRoutes.kt (630 lines), relevant sections of Tables.kt , Routing.kt , DI.kt , sidebar.tsx , bs.json , V67__rbac_permissions_catalog.sql , RbacHelper.kt , and the first ~60 lines of inbox/page.tsx . The original MC #104515 gap audit (status: done, 2026-06-29) is the origin of this task but is now stale — it predates all three merges above and should not be treated as current state. 9. Cross-references MC #104515 — Fiken-gap audit that identified doc inbox as missing + Phase 1 build MC #104519 — Proveo route-layer test coverage for Phase 1 MC #105687 — Permanent document archive / archived status (V122) MC #106195 — Inbox/ ulazni-racuni UX redesign MC #106852 (G1-02) — UploadSecurityGate shared upload security choke point Related BookStack page: "Bilko Operational Runbook — Azure Container Apps" (same book) MC #900178 (2026-08-24): PROD BUG — upload u Ulaz dokumenata pada za sve korisnike: apiFetch šalje Content-Type: application/json s FormData body, server multipart ruta odbija. Fix: JSON header default samo za string/prazan body. RCA: evidence/incident-inbox-upload-20260823/RCA.md; forged prompt 900178.md. bilko.cloud landing truth and CTA remediation — MC #106174/#107324 — 2026-08-18 bilko.cloud landing truth and CTA remediation — MC #106174/#107324 Date: 2026-08-18 · Status: candidate branch validated; NOT merged, NOT deployed, NOT live Root cause The earlier honest-claims corrections existed on commits 149bb1a0 and d0f94baa but were never merged into canonical Azure DevOps main. MC #107298 later hardened and deployed the runtime-only landing tree from main, which still carried stale beta, PSD2, trial and pricing copy. Browser and byte-hash evidence proved live bilko.cloud was identical to green main 390bbb21 . Candidate Branch: fix/106174-landing-hr-truth Base: 390bbb2171027478dabb2d6c9f5679cc3e5d9054 Candidate HEAD: 06acc46db1376f5f9726263fd0262445ec4912af Runtime source: apps/landing-hr/public/index.html Changed scope: runtime HTML plus two focused tests only; MC #107298 headers, public-root controls, Functions and deployment files untouched. What changed Removed eRačun beta/coming-soon, PSD2 auto-sync, 7-day/no-card trial and retired registration claims. Primary path is the verified instant HR demo; high-intent CTAs go to access/contact. Public copy distinguishes offline HR-CIUS/UBL preparation from inactive, unverified production sending. Removed placeholder 30/100 quotas, PD-O/ZP filing claims, unconditional OCR/AI, public API/webhooks/Google SSO, broad accounting/export/currency promises, migration SLA and absolute GDPR/AZOP/security claims. Preserved confirmed prices €9, €19 and +€18 while separating unresolved provider/overage charges. Owner/accountant wording promises prepared reviews/exports, not unsafe shared accountant access. Verification Landing/public security check: PASS, 55 files, 0 errors. Node tests: 20/20 PASS. Builder browser proof: mobile/tablet/desktop PASS. Independent Finverge round 2: PASS, P0=0/P1=0. Independent cross-vendor Proveo: Playwright CLI 3/3 PASS at 390/768/1440, P0=0/P1=0, real demo CTA click, no request mocks. Worktree clean; exactly two landing-scoped commits. Evidence Durable evidence is stored under the local MC 106174 evidence root in the live-baseline, builder, finverge-review-round2 and proveo-round3 directories. The forged prompt is stored under the local forged-prompts registry for MC 106174. Release boundary This page documents a verified candidate. Production remains on stale main copy until branch push, Azure PR validation, CEO preview approval, merge and a separately verified Cloudflare Pages deploy. Do not describe this work as live before those steps.