Commit Graph

6 Commits

Author SHA1 Message Date
julian 663bf0e925 fix(print): out-of-window slip Code128 was too wide to print — width 2 + left-align
The slip printed the QR but NOT the Code128 barcode on the Rongta. Root cause:
the ~20-char occurrence id (SUBSESS-…) at module width 3 is ~765 dots wide —
over the 80mm head's ~576 printable dots — so the firmware silently aborts the
barcode (prints nothing). It was also emitted while ALIGN_CENTER (set for the
title) was active, which shifts the start point right and makes it overflow
even sooner. The QR, being compact, rendered fine — hence QR-only output.

code128() now takes a moduleWidth (default 3, so the shorter entry-ticket id is
unchanged) and forces ALIGN_LEFT (a wide barcode must hug the margin). The slip
passes width 2 (~510 dots — fits with quiet zones) and re-centers the QR/text
after. renderReceipt's voucher barcode re-asserts ALIGN_CENTER for the lines
that follow it.

Verified: width n=2 in the byte stream, est 510 dots; Code128 + QR both present;
build+lint 14/14.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-21 13:44:01 +02:00
julian df5caf8d87 feat(subs): scannable out-of-window slip + two-step booth flow
The advisory out-of-window slip for a subscriber had two problems:

1. Faulty character codes. It rendered via the generic text printReport,
   which has no CP852 mapping for the em dash, ellipsis, or warning sign in
   the composed strings — so they printed as "?" ("PARKIM ? JASHTE ORARIT").
   Added ASCII transliterations for that typographic punctuation in the
   ESC/POS encoder (— → -, ⚠ → !, … → ..., curly quotes/bullet), so they
   degrade to a readable glyph instead of "?".

2. Not scannable. The slip printed only "Nr: SUBSESS-…" as plain text, so
   the operator had to hand-key it. Gave the notice its own render function
   (renderWindowChargeNotice) + a printWindowChargeNotice device method that
   prints the occurrence id as a Code128 AND a QR — the same scan path as a
   transient ticket, so the operator scans it straight into the booth pay
   modal, which then quotes the combined window charge. Implemented on both
   the rongta and cashino drivers.

Also fixed the booth pay modal: "Open barrier" no longer shows by default
for a subscriber. A prepaid subscriber with nothing owed sees only a small
"assist open" reveal (the audited manual open for a faulty reader / lost
card stays available, just not the default). A subscriber owing an
out-of-window charge is now two steps — take payment first, then "Open
barrier" appears — instead of an always-on open button.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-21 13:12:05 +02:00
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