bilko
Bilko product documentation
- Test Sweep 2026-05-15
- Bilko HR Roadmap — Fiscal Compliance + Feature-Enable Architecture (2026-05-29)
- Bilko BUG-005 — revenueMTD / credit-note documentType + RLS migration landmine
- Bilko Backoffice — Backend MVP (Sentry + audit request_id + support_tickets)
- Bilko Backoffice — Ops Infra (Logging Views + support@ Forwarding + Preflight)
- MC #105153 — API write bucket stage E2E rate-limit override
- Bilko sveRačun Faza 1 — TEST-live aktivacija na demou (MC #103450, 2026-07-11)
- Bilko — Minimax Gap Program (2026-07-20)
- Smart Forge (HR) — državni poticaji i zapošljavanje — istraživanje 2026-07-21
- Bilko Operational Runbook — Azure Container Apps
- Bilko main — scheduled validation backstop (MC #106809, 2026-08-05)
- Bilko /login silent auth failure — dijagnoza i fix (MC #106849)
- bilko.cloud landing — honest claims sweep (MC #106980, 2026-08-08)
- Bilko Feature — Document Inbox / Ulaz dokumenata (MC #104515)
- bilko.cloud landing truth and CTA remediation — MC #106174/#107324 — 2026-08-18
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
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 |
|---|---|---|---|
| DEFERRED — 21 dana effort | |||
| 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.createCreditNoteinsert omitteddocumentType. TheInvoices.documentTypecolumn 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-%') recordedsuccess=tbut affected 0 rows. Theorg_isolationRLS policy returns FALSE whenapp.current_org_idis unset; Flyway runs asbilkowith no org GUC, so RLS hid every row. No SQL error → Flyway "succeeded".
Fix
- PR #255:
createCreditNotenow setsit[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 SECURITYrestored.
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 SECURITYwrapper) 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_DSNsecret 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()checksSENTRY_DSN; if absent or blank,Sentry.initis not called. The SDK stays in silent no-op mode. CI / Testcontainers / local dev never emit live Sentry events. - Cloud Run metadata:
K_REVISIONmaps tooptions.release;K_SERVICEmaps tooptions.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.captureExceptionis placed exclusively in theexception<Throwable>handler inStatusPages.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:
requestIdfromcall.callId(CallId plugin canonical source),orgIdfromBilkoPrincipal.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). PlainCREATE INDEX(not CONCURRENTLY) on partial indexWHERE 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-IDheader 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? = nullas 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):
orgIdanduserIdextracted fromBilkoPrincipalonly — never from request body.context_bundleserver-side validated against CONTEXT_BUNDLE_ALLOWLIST before insert.app.current_org_idset viaorgTransaction(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, optionalstatusandorgIdfilters. Returnsdataarray +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
resolutionNotefor RESOLVED/CLOSED, inserts audit_log row for every status change withrequestId = 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 = '<ticket-uuid>';
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.kt730f76a245fb0492f5f94c378e18973242e7e9a0f9c4de5353dc8be268a38b2f; OrgScopeSessionVariable.kt2c5c992c92c5f548c22092c171a98fb599760f3ce827d1e72db26d901c0c89f2
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 <localpart>@<same-domain>. 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.cloudmailbox: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-demoandbilko-web-demo. - STEP 3 — Print rollback commands (print only, never executes): Outputs the exact
gcloud run services update-trafficcommands needed to roll back to the captured revisions. These areecho-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 usesAPI_RATE_LIMIT_PER_MINUTEwith default100.
- Added
apps/api/src/test/kotlin/no/alai/bilko/plugins/RateLimitTest.kt- Added tests for default
100, stage override500, invalid fallback100.
- Added tests for default
azure-pipelines.yml- Stage ACA deploy sets
API_RATE_LIMIT_PER_MINUTE=500forbilko-api-stage.
- Stage ACA deploy sets
.github/workflows/azure-stage.yml- Stage ACA deploy sets
API_RATE_LIMIT_PER_MINUTE=500.
- Stage ACA deploy sets
.env.example- Documents
API_RATE_LIMIT_PER_MINUTE,AUTH_RATE_LIMIT_PER_MINUTE,DEMO_RATE_LIMIT_PER_MINUTE,DEMO_RATE_LIMIT_PER_HOURdefaults.
- Documents
infrastructure/gcp/ENV-MATRIX.md- Documents stage-only
API_RATE_LIMIT_PER_MINUTE=500; demo/prod default/absent.
- Documents stage-only
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
<testsuite name="no.alai.bilko.plugins.RateLimitTest" tests="22" skipped="0" failures="0" errors="0" ...>
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:115apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt:125azure-pipelines.yml:527.github/workflows/azure-stage.yml:181infrastructure/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-demorev 0000043):SVERACUN_HR_LIVE=true,SVERACUN_API_KEY=secretref:sveracun-api-key,SVERACUN_SENDER_VAT=HR91276104352,DEMO_MODE=true.SVERACUN_BASE_URLnije postavljen — kod defaultira na TEST (SveRacunHrEInvoiceAdapter.kt:70), a per-profileapi_base_urlu DB je također TEST. - DB flip (bilko-demo-pg,
hr_einvoice_issuer_config):enabled=trueza tačno 2 test org-a — HR demo org00000000-0000-0029-c000-000000000001+ E2E org1f9811d2-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_LIVEenv.adapter_configtabela gate-uje samo Storecove (mrtav put). Stale GCP secret-ref u DB redovima je inertan (EnvVarSecretManagerClientparsira 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=truedeploy 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, subscription5b0b4d9b). 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.mdLive 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 appbilko-web-demo(+bilko-web-stage) - API (
apps/api/) — Kotlin/Ktor + Exposed backend, ACA appbilko-api-demo(+bilko-api-stage). This is the sole canonical backend (ADR-020/ADR-021) — the old Expressapps/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_Gatesstage is green on the branch (Prettier, TSC, Vitest, Kotest, Trivy FS, Gitleaks, Semgrep) -
Buildstage produced digest-pinned images inbilkodemo.azurecr.io -
Flyway_Migratesucceeded againstbilko-demo-pg— no failed migrations inflyway_schema_history -
Deploy_Stage+E2E_UATpassed againstbilko-*-stage - If schema changed: new
V{N+1}__description.sqlexists inapps/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 groupbilko-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 <previous-revision-name>=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 <previous-revision-name>=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 —curlthe 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 connectionserrors (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 <name>
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-passwordsecret / 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 Vaultkv-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 ANALYZEon 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) — checkaz containerapp hostname list -n bilko-api-stage -g rg-bilko-demofor 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 configurationmonitoring/alert-rules.json— alert thresholds and routingmonitoring/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:cron0 */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
turbobinary 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 naciamlogin.com/.../authorizese 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_KEYsessionStorage marker postavljen neposredno prijeloginRedirect().- Na sljedećem MSAL init-u: marker prisutan + 0 accounta ⇒
authError(pokriva fail-prije-callbacka); dodatno obrađenEventType.LOGIN_FAILURE. /loginrenderuje 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_LIVEje 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 projekatbilko-cloud,--branch=main(production). Napomena: bez--branch=maindeploy 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.htmlJE 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 apendingrow. From there it is either booked (creates anExpenseand 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 theExpenseDocuments/ReceiptService.uploadDocumentstorage pattern - Lifecycle:
status(pending|booked|rejected, widened to addarchivedin 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)(checked0.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:
statuscheck,file_size > 0,storage_backendallowlist,ocr_confidencerange check - Indexes:
(org_id, status),(org_id, created_at DESC),(uploaded_by) - RLS:
ENABLE ROW LEVEL SECURITY+FORCE ROW LEVEL SECURITY, policyorg_isolationscopes every row tocurrent_setting('app.current_org_id')::uuidfor rolebilko_app— same pattern asexpense_documents(V40).
V122 — archive extension (adds the archived outcome, MC #105687):
- Widens the
statusCHECK to includearchived - 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_atwherestatus='archived', partial index ondocument_type, index oncontact_id, GIN index ontags - Design decision (documented in the migration header): extend
inbox_itemsrather than create a newarchived_documentstable, to avoid duplicating storage/RLS/index plumbing.booked_expense_idis 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-hrdomain 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—archivedintentionally excluded, it lives on/dokumenti), dashboard badge viaGET /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 theexpensesGroupsection —{ key: 'inbox', href: '/inbox', icon: Inbox }and{ key: 'dokumenti', href: '/dokumenti', icon: Archive }— both placed aboveexpensesandpurchasesin the group.
6. i18n
"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.ktandInboxService.ktend to end; no reference to Azure Document Intelligence, OCR, or any of the sixextracted_*/ocr_confidencefields 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-ancestorto confirm the feature branch's content reachedmain(Azure DevOps squash-merges, so individual feature-branch commits are not ancestors ofmaineven though the content is — checked viagit ls-tree -r azdo/mainfile presence, not commit ancestry alone).- Full reads of
V106__document_inbox.sql,V122__inbox_archive_documents.sql,InboxRoutes.kt(630 lines), relevant sections ofTables.kt,Routing.kt,DI.kt,sidebar.tsx,bs.json,V67__rbac_permissions_catalog.sql,RbacHelper.kt, and the first ~60 lines ofinbox/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 /
archivedstatus (V122) - MC #106195 — Inbox/
ulazni-racuniUX redesign - MC #106852 (G1-02) —
UploadSecurityGateshared 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.