# 2026-10-22

## Bilingual RFP proposal — logistics operations & costing platform

Built a self-contained bilingual (ID/EN) proposal document answering a 7-point freight-forwarding
RFP (foundation / operations / costing / management + optional items).

Files in project root:
- `Proposal-Logistics-Platform.html` — single-file proposal, language toggle (`data-lang` on `<html>`,
  `.only-id` / `.only-en` helpers, `?lang=id|en` URL param, localStorage persistence), print CSS.
- `Proposal-Logistics-Platform-ID.pdf`, `-EN.pdf` — 18 pages each, A4, page stamps + metadata
  added with PyMuPDF after Chrome headless `--print-to-pdf`.

Key conventions worth keeping:
- Design system: navy ink `#0F1B2B`, bronze accent `#A2701F`, paper `#FBFAF7`, Fraunces (display) +
  Inter (body) + IBM Plex Mono (numbers). Editorial layout, thin rules, numbered sections.
- Pricing model used: 334 person-days @ IDR 1,750,000 blended = IDR 585,000,000 total;
  optional items separate (accounting 96M, portal 119M, training 35M, cash advance 28M);
  maintenance tiers 4.5 / 8.8 / 15.5 M per month; hosting USD 255–345 ≈ IDR 4.1–5.6 M.
- Timeline: 22 weeks, 4 test gates (W6, W11, W15, W20), payment milestones tied to gate sign-off.
- Gantt implemented as a real `<table>` with 22 `<col>` + colspan bars — CSS gradient/percentage
  gridlines rendered unreliably in Chrome print, table is deterministic.

Gotchas learned:
- Print media on A4 (~794px CSS width) triggers `max-width:900px` queries → always scope mobile
  queries with `@media screen and (...)`.
- `<tfoot>` repeats on every printed page fragment in Chrome; use a trailing `<tbody class="tf">`
  row instead so totals appear once.
- Chrome `repeating-linear-gradient` with percentage stops rasterises as thick bands in print;
  prefer `background-size` tiling or real elements.

## Prototype app — ARUS (single-file demo)

`ARUS-Demo.html` — self-contained logistics operations & costing demo, no build step, no server.
Opened by double-click; also shareable as one file. Bilingual ID/EN via `L(id,en)` inline helper
(no dictionary, so a missing translation is impossible), re-rendered on language switch.

Screens: login (role picker) · dashboard (KPI + 12-week margin SVG + approval queue + at-risk jobs)
· jobs list (search, status chips) · job detail (stepper + 4 tabs: overview/costs/docs/activity)
· readiness board (3 computed lanes) · variance approvals (approve/reject mutates state)
· reports (4 real CSV exports via Blob).

Deep-link helpers kept for demos: `?as=manager|ops|costing` auto-login, `&lang=en`, `#/job/<encoded id>/<tab>`.

Data model: job.costs lines are arrays `[id, en, vendor, planned, actual, rebillable, evidence, status]`
— index 7 is status (bug fixed: code had c[8]). All totals/margins/variance derived from lines,
never stored twice. Readiness is computed from docs + booking + containers + costs, never typed in.

Lessons: classic `<script>` only (ES modules blocked on file://); CSS class name mismatch (.grid2 vs
.grid-2) silently collapsed a two-column modal; scope mobile media queries with `@media screen` or
print inherits them.

Also wrote `Email-Pengantar.md` — ID + EN cover email with pre-send checklist.

## Serving the demo for Caddy (logistik.vibeprojects.my.id)

Machine is 192.168.1.100. Caddy runs on ANOTHER host (no caddy process locally), so the upstream
must be reachable on the LAN interface, not loopback.

Working setup: bundled python `C:/Program Files\WorkBuddyAI\resources\vendor\python\python.exe`
bound to `192.168.1.100:13131`. Reason: Windows Firewall (Public profile, BlockInbound) only has
inbound allow rules for that bundled interpreter — the managed python under
`C:/Users/ponco/.workbuddy-ai/binaries` has none, so LAN traffic would be dropped.
Consequence: `localhost:13131` no longer answers; use `http://192.168.1.100:13131/`.

Caddy review notes for their Caddyfile (not yet applied by them):
- `rewrite * /laundry_pos{path}` double-prefixes when the request already carries the prefix;
  guard with `@noprefix not path /laundry_pos/*`.
- pob_template: `not path /pob_ms/*` lets a bare `/pob_ms` become `/pob_ms/pob_ms`; add `/pob_ms`.
  Also `rewrite @hasPrefix {path}` is a no-op and `header_up Cookie` is redundant.
- All `192.168.1.100:*` upstreams only work if Caddy sits inside the same LAN or a tunnel.

## i18n pass on ARUS demo

Two gaps found and closed:
1. Login screen had NO language switcher at all — the toolbar only exists inside the app shell, so a
   visitor could never reach EN before signing in. Added a fixed `.login-lang` segmented control
   (top-right) + `localStorage("arus-lang")` persistence + `applyLang()` at boot (sets `<html lang>`).
2. Job activity logs were stored as single-language strings, so the Activity tab stayed Indonesian in
   EN mode. Migrated all 24 entries to `[timestamp, id, en, user]` and render with `L(a[1], a[2])`.
   New entries created by approve/reject/post-invoice now push 4-element arrays too.

Also translated: "Job tidak ditemukan", container table header "Depo" -> "Depot".
Convention going forward: UI strings use inline `L(id,en)`; any *data* that reaches the screen must
carry both languages in its record, never a pre-translated string.

Verified with a node script that parses the DATA block and asserts cost lines = 8 fields,
activities = 4 fields, and every doc/status/mode key resolves. Temp script deleted after use.

## Naming locked: "Logistics Operations and Costing Web App"

Client confirmed the subject line of the whole engagement is *Logistics Operations and Costing Web App*,
not "platform". Applied everywhere:
- proposal `<title>`, cover eyebrow, cover sub, JS dynamic title (both langs) — word "platform" now 0 occurrences
- section 01 headline changed from "Anda tidak sedang membeli aplikasi" to "…membeli daftar fitur"
  (the old line rhetorically denied the very thing being sold)
- demo `<title>` + brand subtitle "Logistics OS" -> "Operations & Costing" (fits sidebar one line at 8.5px/.18em)
- email subject lines, both languages
- files renamed: Proposal-Logistics-Platform.* -> Proposal-Logistics-Web-App.{html,-ID.pdf,-EN.pdf}

Rule for this project: say "web app" (ID: "aplikasi web"), never "platform"/"sistem" as the product noun.
