Commit Graph

4 Commits

Author SHA1 Message Date
julian bbf61c48df fix(ticket): 11-digit IDs — fix KP-300H barcode line-overflow
The Cashino KP-300H printed entry tickets as raster garbage (solid black
bars / banding) while the Rongta printed the same byte stream fine. Root
cause: the barcode overflowed the print line, not data corruption.

A 13-digit Code128 at module width 3 is ~534 dots. The KP-300H prints 72mm
(512 usable dots at 203 dpi), so the symbol overran the line and the firmware
rendered the overflow as raster noise. The Rongta runs 80mm (576 dots) and had
just enough room — which is why only the Cashino failed. Confirmed on hardware:
plain text printed clean, the barcode was the trigger, and an 11-digit code at
width 3 (~468 dots) both fits and scans the full value at the exit reader.

- Ticket IDs reduced 13 → 11 digits (10 random + Luhn). Length is driven by
  guess-resistance (10^10 space, ~1-in-10^7 to hit a live OPEN ticket even with
  thousands parked — the booth-operator threat model), not volume.
- validateTicketCode is now length-agnostic (\d{10,14} + Luhn) so legacy
  13-digit tickets still in circulation keep validating; the id stays opaque.

Also: sendRaw now closes the print socket GRACEFULLY (end()+FIN, wait for
close) instead of write-then-destroy, which could RST mid-stream and truncate a
job. A separate latent bug found while diagnosing, fixed here.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-19 11:35:13 +02:00
julian 00f3d141b6 feat: human + relative dates; fix language/theme toggle stale-context
Dates were raw ISO on printed slips and time-only in the UI (a session from
two days ago showed just "10:48"). Make them human and day-relative. Also fix
a latent toggle bug surfaced while testing.

Dates:
- Printed tickets/receipts/subscription cards now show "19 Qershor 2026
  10:48:25" (Albanian month, 24h with seconds) instead of YYYY-MM-DD HH:MM.
  stamp() exported as formatStampSq so the shift Z-report shares it.
- Shift Z-report is now Albanian (Operatori/Nga/Deri/Para në dorë/Arka…),
  was English-only with ISO dates.
- Web sessions/logs/history show relative days: "Sot 10:48" / "Dje 17:33" /
  "17 Qershor 10:48" via formatRelativeDateTime(). Month names come from the
  i18n catalog (common.months), NOT Intl — the appliance browser's ICU lacks
  Albanian locale data and Intl silently falls back to English month names.

Toggle fix:
- The language + theme toggles read the active value from the TanStack Router
  context `user`, which is captured at route-resolution time and does not
  re-render on setUser. After one switch the highlight froze and the equality
  guard blocked switching back until a page refresh. Drive them off live state
  instead: language from i18n.language (useTranslation subscribes to
  languageChanged), theme from local useState. (Bug dated to 040c0ff.)

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-19 11:14:06 +02:00
julian d71ba82999 feat(booth): payment receipt / exit voucher — transparency slip + CP852 fixes
After a completed payment the customer always gets a transparency record:
entry time, payment time, duration parked, amount + tender. One shared
ESC/POS renderer (renderReceipt + ReceiptData in @parking/devices), two
modes: VOUCHER = those figures PLUS the scannable Code128 barcode and an
emphasised walk-back-grace line, so the one slip both proves payment and
self-exits at a distant exit reader (replaced the old barcode-only voucher);
STANDALONE = detail-only, auto-printed at payment when no voucher is issued.
Figures fold from the SIGNED ledger (latest payment event); printed on the
booth printer (failover to dispenser). Best-effort: a printer fault never
blocks the exit that already happened — the modal shows a note and offers
"Reprint receipt".

Server: booth-print.ts printPaymentReceipt() + receiptFigures(); routes
POST /api/voucher (voucher) + new POST /api/receipt (standalone/reprint).
Both ESC/POS drivers gained printReceipt(). Web: BoothPayModal auto-prints
after a non-voucher payment + reprint button; api.ts printReceipt().

CP852 fixes found on a real printout: (1) uppercase Ë was mapped to 0xEB
(that's ű) — correct byte is 0xD3; (2) Intl.NumberFormat injects a NO-BREAK
SPACE (U+00A0/U+202F) that isn't in CP852 and printed as "?" — line() now
normalises it to a plain space ("1000 Lekë"); (3) grace line wrapped
mid-word — split into two short lines.

Full build green; both receipt modes render-verified; routes live.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-18 20:46:38 +02:00
julian 3e6773a6d5 fix(devices): Cashino printer — ping-only driver (no false status) + Albanian role wording
The Cashino 80mm printer reported wrong status: it ran on the `rongta`
driver, whose readStatus() scrapes the Rongta board's /prn_stat.htm status
page — which the Cashino does not serve — yielding a bogus degraded/page-
error verdict while the printer was online and printing fine. Root cause:
the Cashino is an ESC/POS PRINT clone with no trustworthy STATUS mechanism.

Fix: extract the shared ESC/POS rendering + transport (renderTicket/
renderReport/renderSubscriptionCard/sendRaw/probe + CP852 map + code128/
qrCode) from printer-rongta into drivers/printer-escpos.ts, and add a
dedicated `cashino` driver that reuses that print path but is deliberately
NOT MonitorableDevice (no readStatus). isMonitorable() is then false, so the
device monitor falls back to healthCheck() — a plain TCP reachability ping:
reachable -> ready, unreachable -> offline, never a guessed paper/cover
state it cannot sense. Rongta driver unchanged (still scrapes its page,
still monitorable). Register + re-export cashinoDriver.

Verified at runtime (cashino registered, isMonitorable=false, no readStatus,
healthCheck->offline on unreachable) and live: /api/devices/status shows both
printers ready (lane via ping, booth via page). The live entry-dispenser at
10.0.10.9 was switched rongta->cashino in the operator DB (backed up).

Also fix the Albanian device-role chip wording, which read wrong as a
"{category} {role}" label: access mixed "i përzier" -> "hyrje/dalje"
(it means a barrier spanning both directions); printer lane "korsia" ->
"në korsi"; booth "kabina" -> "në kabinë". English tidied to match
(mixed->entry/exit, lane->at lane, booth->at booth).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-18 20:00:42 +02:00