Skip to content

Releases: CaspianTools/caspian-store

v12.0.0 — Register correctness pass, and the till is a PWA only

Choose a tag to compare

@CaspianTools CaspianTools released this 24 Aug 09:54

v12.0.0 — Register correctness pass, and the till is a PWA only

A full review of src/pos/ turned up sixteen defects, from a receipt printed with no
number on it to a real sale that could sit invisible in the outbox forever. All of them
are fixed here. Separately, the Tauri desktop shell is gone: the register ships as a PWA
and only as a PWA.

Consumer action required on upgrade

commitPosSale now returns the priced lines it already computes, and the register builds
the customer's receipt from those. Redeploy the caspian-pos codebase or receipts keep
falling back to the till's scanned prices:

npm install github:CaspianTools/script-caspian-store#v12.0.0
cd firebase/functions-pos && npm install && npm run build && cd ../..
firebase deploy --only functions:caspian-pos

Nothing else changes for a consumer site. If your code imported readStorageMode,
writeStorageMode, readPrinterTransport or writePrinterTransport, delete those calls —
see Removed.

Fixed

  • An offline sale with no leased numbers left printed a blank receipt number.
    PosSaleQueue.capture returns a device-local reference for exactly this case and
    PosQueuedCloudAdapter was discarding it, so the customer got a slip with no identifier
    at all. The reference is now used and labelled as provisional, on screen and on the
    printed receipt.
  • A sale interrupted mid-send was never retried and never cleared. send() marks a
    sale sending before the call; a reload or a closed tab left it there permanently —
    drain() only picks up held, pruneSent() only removes sent, and the held-sales
    page offers Retry on blocked alone. The queue now sweeps sending back to held once
    per session, before the online check so the count is right even offline.
  • Receipt lines could disagree with the receipt total. Lines and subtotal came from the
    open ticket while the total came back from the commit, so a catalogue edit mid-sale
    printed a slip whose own lines did not add up to its own total. commitSale now returns
    what was charged and the receipt is built from that.
  • An online sale burnt a leased receipt number the server never honoured.
    PosCloudAdapter was not forwarding the receipt claim (or capturedByUid /
    capturedByName) to the callable, so the ordinal was spent locally while the server
    allocated a different number from the counter. Leases drained at one number per sale and
    numbering developed permanent gaps.
  • Typing into the register's quick-add dialogs fired phantom barcode scans. The
    keyboard wedge was silenced only for the tender phase, which could not see the product
    and person dialogs the POS header opens outside PosRegister. It now detects any open
    dialog from the DOM, so consumer-added dialogs are covered too.
  • 1,234.50 was read as 1.234 at the tender screen. String.replace with a string
    argument replaces only the first match, so a grouping separator produced two decimal
    points and parseFloat stopped at the second. On the tendered field that is wrong change
    handed to a customer.
  • The register, the connection pill and the held-sales page each built their own
    outbox.
    They shared IndexedDB so no sale was lost, but capture() and markSent()
    notified an instance nobody was subscribed to, so the held-sales badge only moved on a
    30-second timer and paused — in-memory state — could never be observed by the pill.
    There is now one adapter, provided by PosShell.
  • Backups silently dropped custom roles. LOCAL_BACKUP_VERSION is 2 and carries
    roles; v1 files still restore. parseLocalBackup also validates sales is an array,
    which it never did while restoreLocalBackup iterated it.
  • "0 receipt numbers left" could never be cleared. The low-numbers warning fired on
    leasedRemaining === 0, which is also a healthy till before its first lease and a store
    with no functions-pos deployment. Lease failure is now tracked separately and reported
    as "Cannot reserve receipt numbers".
  • The Math.random fallback in randomId() had no length floor, and the server requires a
    sale id of at least eight characters.

Added

  • Line discounts have a control at last. usePosTicket.setLineDiscount was plumbed
    end to end — cloud payload, priceLocalSale, commitPosSale validation, receipt
    renderer — with nothing calling it, so the ticket's discount row and the receipt's
    per-line discount were unreachable. Each ticket line now has a markdown button.
  • PosAdapterProvider / usePosAdapter, the shared register storage seam.
  • PosSoldLine — a line as it was charged, distinct from the scanned PosSaleLine.
  • summariseSoldLines and parseAmount are exported, both pure and both checked by
    scripts/check-standalone.mjs.

Removed

  • desktop/ and its build workflow. The Tauri shell that bundled the register for
    Windows is gone; the till is installed from Chrome or Edge with the register's own
    Install button. Existing desktop/v* releases stay on GitHub but are no longer built or
    documented, and the POS manual's Windows-installer section has been deleted. Standalone
    mode itself (<CaspianStoreProvider standalone>, PosLocalAdapter, the local back
    office) is untouched and still exported — it simply has no first-party packaged delivery.
  • readStorageMode, writeStorageMode, readPrinterTransport, writePrinterTransport
    and idbCountByIndex. Nothing called any of them: the printer control is hard-coded to
    the browser transport until the ESC/POS paths ship, and the storage mode is derived by
    resolvePosStorageMode rather than stored.
  • promoCode from the standalone commit path. priceLocalSale never applied a promo, so
    the field recorded a claim nothing honoured.

Changed

  • PosQueuedCloudAdapter takes its queue as a constructor argument instead of building one.
  • PosCommittedSale gains optional lines and provisionalReceipt.

v11.0.2 — The Windows register becomes a till with no website

Choose a tag to compare

@CaspianTools CaspianTools released this 24 Aug 02:47

v11.0.2 — The Windows register becomes a till with no website

The Windows register app was a thin window onto a shop's hosted /pos page, and it asked for that
address on first run. An address typed once on setup day and never looked at again is an address that is
sometimes wrong: a till pointed at a company's marketing site showed that site's 404 and nothing else.
v0.2.0 added a check and a way back to the setup screen, but could not help a till that had already
stored a bad address, because it deliberately never re-probed on launch — so the failure survived the
upgrade.

The address is now gone. desktop/ bundles the register and runs it in the library's standalone
mode: the catalogue, the staff, the sales and the receipt numbers live in a local database inside the
app, and the till contacts nothing. There is no address to type, no account, no website behind it, and
no 404 that can occur. It is released separately as desktop/v1.0.0.

Standalone mode is what made this possible, and it removed both objections that had kept the register
from being bundled: signInWithPopup is irrelevant with no Firebase Auth in the tree, and
version-locking the UI to a signed binary is what an offline product is.

No consumer action required

Documentation only in the npm package — the register manual and the version stamp. No library source
changed
: the offline app is assembled entirely from the existing public API (standalone,
CaspianRoot, features.posOnly and the framework-adapter contract). The Windows shell is not part of
the published tarball and releases on its own desktop/v* tag, so upgrading the library does not change
a till, and updating a till does not require upgrading the library.

Shops running a hosted Caspian store are unaffected: the cloud register at /pos and its browser
install are unchanged. What is gone is the desktop app's cloud mode — that audience installs the
register from Chrome or Edge as before.

Changed

  • desktop/ — the register is bundled and offline. The app mounts
    <CaspianStoreProvider standalone scriptSettings={{ features: { posOnly: true } }}> over a Vite React
    bundle served from inside the binary. The setup screen, the address probe, the store.json file and
    the Till → Change shop address menu item are all removed; the Till menu keeps Reload and Quit.
    ureq and url leave the Rust crate with the network code that used them.
  • desktop/ — routing is an in-memory adapter. Tauri serves the bundle from a custom protocol with
    no SPA fallback, so window.location navigation to /pos/settings would ask for a file that does not
    exist. desktop/src/memory-navigation.tsx supplies pathname, a reactive searchParams, push,
    replace and back through the documented adapter contract — which is why no library change was
    needed. External links are inert on purpose: an offline till has no address bar and no back button, so
    following one is a one-way trip out of the register.
  • A store.json left by a pre-1.0 install is deleted on first launch, so an upgrade needs no
    hand-edit.

Fixed

  • A till upgraded from desktop/v0.1.0 no longer inherits an unchecked address. v0.1.0 stored
    whatever was typed with no validation, and v0.2.0 read it back without re-probing, so a till that had
    already been pointed at the wrong site kept opening that site's 404 after the upgrade. There is no
    address to inherit any more.

Documentation

  • The register manual (docs/pos-manual.html) rewrites "The Windows register app" for what it now
    is, including the thing that most needs saying out loud: the shop's catalogue, staff and entire
    trading history exist on that one computer and nowhere else, and Back office → Backup is the only
    copy there will ever be. The az, ru and tr overlays for that section are deleted rather than left
    stale, so it renders in English with the "not translated yet" notice until it is re-translated.
  • desktop/README.md corrects what it said about SmartScreen. The warning is not a signature check
    alone — it weighs the signature, the reputation of both the certificate and the exact file hash, and
    the Mark of the Web, which is why unsigned software installed from a USB stick raises nothing at all.
    The cost table gains the cheap options it was missing (Certum open-source signing at roughly €30/year,
    Azure Trusted Signing, Microsoft's file-submission review). No signing path has been chosen — the
    installer is still unsigned and SmartScreen still warns.

Caspian Register desktop/v1.0.1

Choose a tag to compare

@github-actions github-actions released this 24 Aug 03:00

Windows installer for the Caspian Register. The till runs entirely on the computer it is installed on - no internet connection, no account and no website are needed. Unsigned, so Windows SmartScreen warns on first run: choose More info, then Run anyway. See desktop/README.md.

v11.0.1 — The register app refuses an address with no register on it

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 21:18

A shop that typed its company website into the Windows register app got a bare 404 and no way out. The
address was saved before anything checked it, nothing on screen named the address the window was asking
for, and the command to change it had no button, menu item or shortcut anywhere in the shipped app — the
only recovery was deleting a file from %APPDATA%.

The shell now checks the address before it saves it, and always carries a way back to the setup screen.
It is released separately as
desktop/v0.2.0.

No consumer action required

Documentation only in the npm package — the register manual and the version stamp. The Windows shell is
not part of the published tarball and releases on its own desktop/v* tag, so upgrading the library
does not change a till, and updating a till does not require upgrading the library.

Changed

  • desktop/ — the address is checked before it is saved. The setup screen requests /pos on
    whatever was typed and refuses a definitive 404, naming the mistake it is almost always catching: a
    company's marketing site rather than the address the shop itself runs on. Only a definitive 404 or
    410 refuses.
    An
    unreachable host, a timeout, a TLS failure or a 500 are all accepted, because a till is routinely set
    up before the shop's site is live and on connections having a bad morning — refusing then would strand
    a shop that had typed exactly the right address. The check is a catch for the obvious mistake, not a
    promise that the address is right, and the manual says so.
  • The register window has a Till menu — Change shop address (Ctrl+Shift+A) and Reload. It is
    native, so it still works when the page below it is a 404 or a blank "can't reach this page", which is
    exactly when somebody needs it. reset_store_url existed in v0.1.0, but no shipped UI could call it
    and the remote origin has no IPC access, so it was unreachable code documented as a feature.
  • The window title names the address — Caspian Register — shop.example.com. A page that fails to
    load can now be traced back to what was typed.
  • Repointing a till is an edit, not a retype. The setup box opens prefilled with the current address,
    and the saved value is overwritten only once a new one passes the check, so closing that window leaves
    a working till working. reset_store_url is renamed change_store_url and no longer deletes the
    config file.

Fixed

  • http://localhost.evil.com counted as local and bypassed the https requirement, because the test
    was a starts_with("http://localhost") prefix match on the whole string rather than on the host. The
    host is now parsed out and compared. A bare localhost:3000 is also accepted now and defaults to
    http, rather than being rejected as "not a web address".
  • The desktop release build could ship the previous release's installer. desktop-build.yml caches
    src-tauri/target, which carries the last build's bundle/ with it, so the collect step globbed a
    directory holding two -setup.exe files and took whichever sorted first. The 0.2.0 build produced a
    correct Caspian Register_0.2.0_x64-setup.exe and was rejected because the cached 0.1.0 one won the
    glob. The bundle directory is now cleared before building, and finding more than one installer is a
    hard failure rather than a coin toss.
  • The register manual told owners to change a till's address by deleting store.json by hand. It now
    documents the menu, in all four languages, and states plainly that the new check cannot catch a wrong
    address on a site it cannot reach.

v11.0.0 — A till that needs no shop behind it

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 14:38

Many physical shops have no online presence, and until now the register assumed one: a Firebase project, a website, a cloud admin panel. This release adds standalone mode — mount the provider with standalone and the register runs entirely on one computer. Catalogue, staff, sales and receipt numbers live in that machine's IndexedDB, and the till contacts nothing.

It comes with its own back office at /pos/admin, because a till you cannot put products into is not a till.

Consumer action required on upgrade

For almost everyone this is a reinstall and nothing else:

npm install github:CaspianTools/script-caspian-store#v11.0.0

Rules, indexes and Cloud Functions are unchanged. The major bump is for one narrow type change: CaspianStoreContextValue.firebase and .collections are now | null, because in standalone mode there is no Firebase project. If you read them off the low-level escape hatch:

// before
const { firebase } = useCaspianStore();
firebase.db;

// after — the documented accessor, which still returns non-null
const { db } = useCaspianFirebase();

useCaspianFirebase() and useCaspianCollections() are unchanged and still return non-null, so the ninety-odd storefront and admin call sites need no edits.

Added

  • Standalone mode. <CaspianStoreProvider standalone> boots the whole tree with no Firebase project. Deliberately explicit rather than inferred from a missing config: falling back automatically would mean a real shop whose credentials broke came up as an empty local register and started taking sales into a database nobody knows about — a failure that looks exactly like a working till.
  • PosLocalAdapter, implementing the PosStorageAdapter seam that has been in the codebase since v10.0.0 waiting for it. No queue, no lease, no commit fuse and no online check: those exist to paper over the gap between a sale happening and a server hearing about it, and here there is no gap.
  • Local accounts and three tiers — superadmin (Support) → admin (Owner) → staff (Cashier), cumulative rather than exclusive so an owner can work the counter without signing out. Passwords are PBKDF2-SHA-256 with a per-user salt and per-user iteration count. Technical Support commissions a machine by creating the first account; the shop's own staff are added afterwards, from the back office.
  • A local back office at /pos/admin — items (add, edit, delete, search, CSV template / export / import), sales and takings with a CSV export, people and roles, shop details and receipt wording, and backup / restore.
  • Backups. A dated JSON file holding items, people, sales, the receipt counter and the shop record. Restore is additive and never overwrites a sale already on the till, and the receipt counter only ever moves forward — winding it back would reissue numbers already printed.
  • CSV headers match the cloud products export where the two models overlap, and unknown columns are ignored rather than rejected — so a spreadsheet exported from an online store imports straight into a standalone till.
  • scripts/check-standalone.mjs and a CI workflow: 32 assertions over the money arithmetic and the CSV round-trip. Plain node:assert, no test runner and no new dependency.

Changed

  • Where sales are stored is now shown at /pos/settings, not chosen. The radio was previously disabled and labelled "Available in a coming release". It now reflects the mode the till is actually in and stays read-only. Offering it as a per-device switch was a trap: a cloud shop that picked "this computer only" would have got a register backed by an empty local catalogue and no way to fill it.
  • reportServiceError accepts a null Firestore; PosSaleQueue, CartProvider, WishlistProvider and ScriptSettingsProvider accept a null database. ScriptSettingsProvider gains a seed — it was the single provider making an unconditional Firestore read at mount, and a standalone till would otherwise have waited forever on a subscription that never fires.
  • The register's queue tab and connection pill are hidden in standalone mode, where there is no outbox and nothing to be offline from.

Fixed

  • docs/pos-manual.html no longer describes local storage as unavailable in all four languages.
  • README's POS manual section count said 38; it is 41.

Notes

  • The Windows installer still asks for a shop address. The desktop shell is a window over a shop's own /pos page and has not moved to standalone yet — packaging a standalone till as an .exe that needs no website at all is the next desktop release, on its own desktop/v* tag.
  • Nothing syncs to a website. Attaching a shop link later and pushing local history up to it is a separate release.
  • docs/pos-manual.html gains no standalone chapter in this release: a shop cannot reach the mode until the desktop shell delivers it, and the manual documents what an owner or cashier can actually do.
  • create-caspian-store is unchanged — no scaffolder CLI surface change, so no republish.

v10.5.0 — The register keeps selling when the internet drops

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 10:55

v10.5.0 — The register keeps selling when the internet drops

A till no longer stops when the connection does. The cashier scans, takes payment and prints a receipt
exactly as always; the sale is written to the computer instead of the shop and sent by itself when the
connection returns. The customer gets an ordinary receipt with a real receipt number on it, because
the till reserves a block of numbers in advance while it is still online — not a slip saying it is not
a receipt.

Consumer action required on upgrade

Redeploy the register's Cloud Functions if you have not already (v10.4.0 added the lease callable):

npm install github:CaspianTools/script-caspian-store#v10.5.0
firebase deploy --only functions:caspian-pos

Rules and indexes are unchanged since v10.4.0.

Added

  • A hold-and-forward queue in IndexedDB. Sales, leased receipt blocks, a projected catalogue, and
    the open ticket. IndexedDB rather than localStorage because this holds money: localStorage has no
    transactions, so "write the sale" and "spend the receipt number" could not be made atomic, and a
    crash between them would either lose a sale or reuse a number.
  • PosQueuedCloudAdapter, which wraps the cloud adapter rather than replacing it, so the register
    never learns whether the network is up. It writes the sale to disk and awaits that commit before
    attempting the network
    — a request that timed out and a request that was refused are
    indistinguishable from the client, so the queue decides before acting rather than guessing after.
  • A local catalogue cache, synced through the getPosCatalogDelta endpoint that has existed with
    no callers since v10.0.0. Same lookup precedence offline as online: barcode, then SKU, then id.
  • /pos/queue — Held sales. A sale that cannot be sent is money in the drawer with no record a
    cashier can see, so it gets a page, with a reason and a Try again button.
  • A connection marker in the register's top bar, silent when everything is normal. It warns that
    receipt numbers are running low while the till is still online and can top up — discovering that
    mid-outage in front of a customer is the wrong time.
  • An error taxonomy the register has never had. pos-register.tsx only ever read error.message,
    which for any infrastructure failure is the literal string INTERNAL. The queue reads the callable
    code instead: transient failures back off, an expired token refreshes without consuming an attempt,
    a revoked role pauses the whole queue and says so, and a deleted product stops for a person.
    internal is retried but bounded, because firebase-functions collapses a transient Firestore
    abort and a permanent crash into the same opaque code.
  • 26 new strings in all four languages, and a register-manual section, Selling when the internet
    drops
    .

Changed

  • Five statements in the register manual were made false by this release and are rewritten in all
    four languages
    — every place that said the register does not work offline, has no queue, or that
    a lost connection means a lost sale.
  • PosStorageAdapter gains an optional queue; PosSaleDraft gains receipt, capturedTotal,
    capturedSubtotal, capturedByUid and capturedByName. Both additive — a consumer's own adapter
    still compiles.
  • PosCommittedSale.pending is finally set. It has been declared since v10.0.0 and read by
    nothing.

Notes

Three limits, in the manual rather than left to be discovered:

  • Clearing the browser's site data while sales are waiting destroys them. They exist nowhere else
    yet. The Windows app keeps its storage separately, which is a reason to prefer it on a busy counter.
  • Waiting sales are only sent while a register tab is open. They go as soon as somebody opens the
    register again; they will not go overnight on their own.
  • An item the till has never downloaded cannot be scanned offline. The catalogue refreshes
    whenever the till is online, so this only affects something added very recently.

The commit fuse is 3 seconds rather than the SDK's default 70. Seventy seconds of a frozen till with a
queue forming is not a thing anyone would choose, and leaving the call running costs nothing: the
leased receipt number is identical on both paths, so a late landing reconciles through the idempotency
gate with no visible consequence.

v10.4.0 — Receipt-number leases, and the till stops emailing you

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 08:39

v10.4.0 — Receipt-number leases, and the till stops emailing you

Groundwork for offline selling, plus a bug that has been filling shop owners' inboxes.

The hard problem with an offline till is the receipt: the server allocates receipt numbers inside the
commit transaction, so a sale captured offline has no number to print at the moment the customer is
standing there. A till can now reserve a block of real numbers in advance and spend from it while
offline, so the customer gets an ordinary receipt with a final, store-unique number on it — not a
slip that says NOT A RECEIPT.

Nothing user-visible changes yet. The register does not use leases until the offline queue ships; this
release lands the server half first, so an old client keeps working against the new server.

Consumer action required on upgrade

Redeploy two Cloud Functions codebases. Rules do not need redeploying — the new collections are
server-only, and unlisted paths already deny by default, so the added blocks change no behaviour.

npm install github:CaspianTools/script-caspian-store#v10.4.0
firebase deploy --only functions:caspian-pos,functions:caspian-email

Fixed

  • Every counter sale emailed the shop owner a "new order". runEmailOnOrderCreate fires on any
    orders/{id} create, and a POS sale is written with status: 'paid', which maps to the
    new_order_admin template. The customer half was already gated on there being an address to write
    to; the admin half was not. A shop doing two hundred sales on a Saturday got two hundred emails.
    POS orders now return early — the sale is already on the Point of sale page and in the Orders list.
  • The commit transaction could exceed Firestore's 500-write ceiling. It writes one update per
    distinct product, plus the order, plus (now) the receipt-number claim, while the line cap was 500.
    Over the ceiling it fails as an opaque internal, which a retrying client reads as transient and
    replays forever. The cap is now 400.
  • A backlog drained by a manager was recorded against the manager. cashierId came from the
    caller, so a held sale replayed the next morning lost the cashier who actually rang it. The client
    can now send capturedByUid, and the caller is kept separately as replayedByUid.

Added

  • leasePosReceiptBlock — reserves a block of receipt numbers for one register. Bumps
    posCounters/receipt once in a two-write transaction; the till then spends ordinals from the block.
  • commitPosSale accepts receipt: { leaseId, ordinal }. The client never sends a receipt
    string — the server derives it from the lease document it wrote itself, so a tampered client can
    only pick a number inside a block genuinely issued to its own device. Omit it and allocation works
    exactly as before, so old clients are unaffected.
  • posReceiptNumbers — a claim registry that makes a duplicate number impossible. This is not
    belt-and-braces. Imaging a Windows till, which is routine in multi-till rollouts, clones the device
    id and any stored lease, so two registers can hold the same block and spend the same ordinal. The
    claim catches it and the second sale falls back to a fresh number instead of handing two customers
    the same receipt.
  • posReceiptLeases and posReceiptNumbers collections, refs in collections.ts, and explicit
    server-only rules blocks matching the posCounters / posLicenses pattern.

Notes

The counter is only read when a sale has no lease. Reading it on every sale is what would serialise a
whole shop on one document, and removing that contention is half the reason leases exist. Gaps in the
numbering are expected — a till that leases 200 numbers and sells 3 has burnt 197, and receipt numbers
were never contiguous across tills anyway. What matters is that no number is ever issued twice.

v10.3.2 — Point the scaffolder at the org that actually exists

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 08:32

v10.3.2 — Point the scaffolder at the org that actually exists

Housekeeping ahead of the offline work. The scaffolder installed the library from
github:Caspian-Explorer/... while the repository lives at CaspianTools/.... GitHub's rename
redirect has been quietly covering for it, so nothing was broken — but a redirect is not a guarantee,
and it stops the day somebody else claims the old organisation name.

No consumer action required

Existing installs are unaffected; both spellings still resolve today. New scaffolds now install from
the canonical URL instead of relying on a redirect.

Fixed

  • 24 GitHub URLs repointed from Caspian-Explorer/ to CaspianTools/ across package.json,
    README.md, INSTALL.md, scaffold/create.mjs, scaffold/README.md and create-caspian-store/.
    The load-bearing one is scaffold/create.mjs, which is the spec every scaffolded site installs from.
    Caspian-Explorer is left untouched wherever it is the author or copyright holder — that is a
    name, not a typo.
  • Install examples were three majors stale. README.md and INSTALL.md told people to install
    #v8.0.0, and INSTALL.md asserted "v8.0.0 is the current release", while the package was on v10.3.1.
  • stripLocalePrefix had a third private copy in layout-shell.tsx; it now uses the shared helper.

Added

  • scripts/check-manuals.mjs gains check C7 — the install examples that claim to be current
    must pin the current version. This is the same rot as the manual's footer version stamp, which sat
    at v10.0.0 through two releases because nothing checked it. Historical migration notes are left
    alone: naming v9.0.0 in a v9 upgrade section is correct.

v10.3.1 — A Windows installer for the register

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 08:03

v10.3.1 — A Windows installer for the register

The register can now be handed to a shop as a file. desktop/ is a small Tauri shell — a native
window pointing at the shop's own /pos page, using the WebView2 runtime already on Windows rather
than shipping a browser, so the installer is about 1.5 MB. It releases on its own desktop/v* tag and
is not part of the npm package.

No consumer action required

Documentation and a new sibling project. Nothing in dist/, firebase/ or scaffold/ changed, and
the library API is untouched. The installer is an optional extra for shops that would rather run a
setup file than install a web app from a browser prompt.

Added

  • desktop/ — the Windows register shell, released as
    desktop/v0.1.0.
    One binary for every shop: the store address is asked once on first run rather than compiled in,
    because SmartScreen reputation accrues per signed binary and per-customer builds would leave every
    customer staring at "Windows protected your PC" forever. Only https:// addresses are accepted —
    Firebase Auth will not sign a cashier in over plain http, so an http till would look correct and
    then refuse every login.
  • .github/workflows/desktop-build.yml — builds the installer on windows-latest, derives the
    icon set from a committed source PNG, and attaches the .exe to a desktop/v* release. CI is the
    build path because the shell is Rust and most people maintaining this repo have no Rust toolchain.
  • The register manual gains The Windows register app in all four languages.

Notes

Two limits, stated plainly in the manual rather than left to be discovered:

  • The installer is unsigned, so Windows warns on first run until a code-signing certificate is
    bought. That is procurement, not a code change, so it is not wired up with placeholder secrets that
    would fail every build.
  • v0.1.0 does not print directly to a thermal printer. Receipts go through the same print dialogue
    as the browser. Direct ESC/POS printing over the Windows spooler is the main reason to want the
    shell and needs a physical printer to validate, so it is next rather than now.

The shell is not a bundled copy of the register — it points at the live origin, so a shop that
updates its site gets the new register on the till's next launch with nothing to reinstall.

v10.3.0 — The register installs as its own app

Choose a tag to compare

@CaspianTools CaspianTools released this 23 Aug 07:41

v10.3.0 — The register installs as its own app

A till can now install the register: its own icon, its own window, no address bar. It is still the same
web page — nothing to download, nothing to keep up to date by hand — but it starts like a program and a
cashier can no longer type a URL into it by accident. The storefront and the register install as two
separate apps
on one origin, so a counter machine gets an icon that opens straight into the register.

Scaffolded sites get the whole layer generated. Existing sites can adopt it by hand — see below.

Consumer action required on upgrade

Nothing breaks if you do nothing; the register keeps working exactly as before. To make it installable
on an existing site you need the generated files, which is easiest to get by scaffolding a throwaway
project and copying them across:

npm install github:CaspianTools/script-caspian-store#v10.3.0
npx create-caspian-store@latest /tmp/pwa-ref -- --with-pos
# copy from /tmp/pwa-ref into your project:
#   src/app/_pwa-brand.ts  src/app/manifest.webmanifest/  src/app/pos.webmanifest/
#   src/app/icon/          src/app/pos/                   public/sw.js  public/sw-pos.js
#   public/offline.html    public/pos-offline.html
# then merge the redirects() and headers() blocks from its next.config.mjs

If you already wired the storefront PWA by following INSTALL.md §9.5, your build has been failing —
see the third entry under Fixed. Change that one import to @caspian-explorer/script-caspian-store/pwa.

Added

  • A fourth entry point, ./pwa — buildWebManifest and the new buildPosWebManifest, importable
    from a server route handler. This is the fix for the broken manifest route described below.
  • buildPosWebManifest() — the register's manifest. scope/start_url /pos, short_name
    "Register" (deliberately not the brand: the OS shows it under the icon, and that word is what tells a
    cashier which of the two apps to tap), id for a stable install identity, and
    launch_handler: focus-existing so tapping the icon during a sale focuses the open window instead of
    re-navigating and destroying a half-rung ticket.
  • <PosInstallButton> in the register's top bar, and <PosServiceWorker>, which registers a
    /pos-scoped worker, asks for persistent storage, and offers updates on a strip that never applies
    itself mid-sale.
  • The scaffolder generates the PWA layer: both manifest routes, a generated icon route, the /pos
    route segment with its own layout, two service workers, two offline pages, plus redirects() and
    headers() in next.config.mjs. Verified by building a scaffolded site end to end.
  • <ServiceWorkerRegister> and <PosServiceWorker> gain enableInDev — offline behaviour was
    untestable under npm run dev, which is untenable for a feature about the network being down.
  • stripLocalePrefix is exported, and the register manual gains Installing the register as an app on
    a till
    in all four languages.

Fixed

  • /az/pos/settings rendered the register instead of the settings page. PosRoot matched
    ^/pos/ against a pathname that still carried its locale prefix, so nothing matched and it fell to
    the default branch. A cashier on any non-English till could not reach their own settings at all —
    language, register name, scanner speed, printer. The nav highlight was wrong for the same reason.
  • The storefront's install banner floated over the register's cash keypad. It is
    position: fixed; zIndex: 800; the tender dialog is zIndex: 60. <InstallAppPrompt> now takes
    hideOnPathPrefixes, defaulting to ['/pos','/admin'].
  • INSTALL.md §9.5's manifest route has never worked. It told consumers to import
    buildWebManifest from the main entry, but the build stamps that entry 'use client', so the
    Next.js build fails with "Attempted to call buildWebManifest() from the server." Broken from
    v9.10.0 until now. The example now imports from ./pwa, and it also stopped pointing at
    examples/, a directory that is not in the package's files list and so is absent from every
    installed copy.
  • Brand colours were silently dropped from the manifest. NEXT_PUBLIC_BRAND_THEME_COLOR=#1d4ed8
    reads as empty, because dotenv treats an unquoted # as the start of a comment. The generated
    .env.example now quotes them, and the generated code accepts a bare 1d4ed8 too rather than
    making every consumer find that out the hard way.
  • Two service workers on one origin shared a CacheStorage, and the reference worker deleted every
    cache key that was not its own on activate — the storefront worker would have wiped the register's
    shell out from under a live till. Both now clean only their own prefix.
  • stripLocalePrefix was defined privately in two components; it is now one shared helper.

Notes

Installing does not make the register work offline — it still needs the network to look up a
product and to complete a sale. The manual says so in the first note of the new section rather than
letting a shop discover it during an outage. The offline sale queue is a separate piece of work.

create-caspian-store sibling unaffected — no change to create-caspian-store/ and no scaffolder CLI
surface change.