Skip to content

Commit 719ffe5

Browse files
v1.4.5 — Touch ID Google sign-in + UID migration UX + TigerScale v2 schema
═══ Migration consent + progress modals ═══ Two modals coordinate the on-the-fly decimal → hex spool id migration: 1. Consent modal (phase 1) — non-destructive, asks before any write. Shows the spool count and an estimated duration ("about N seconds" or "about N minutes" with proper pluralisation). Two buttons: - "Update now" → starts the migration - "Remind me later" → defers until next app launch (no persistent snooze; resets on every sign-in / app start) 2. Lock-screen progress modal (phase 2) — only opens AFTER consent and only when the backlog is non-trivial (≥ 3 docs). Shows a progress bar `done / total`, the count is updated live as the queue drains. Explicitly tells the user "do not close the app" in bold. Cmd+Q during this phase is intercepted by main.js — a native macOS dialog asks the user to confirm before letting the quit through. Adaptive politeness: 250 ms gap between migrations by default, 500 ms when the queue exceeds 50 spools, so we stay well within Firestore's per-document write throughput. ~1 s per spool end-to-end. ═══ TigerScale v2 schema cutover ═══ Studio Manager now reads the heartbeat doc using v2 field names only. No v1 fallback — old firmware writing `last_seen` / `name` / `last_spool` / `rssi` / `battery_pct` will appear OFFLINE in the UI until updated. New v2 reads (via dedicated accessor helpers in inventory.js): - last_heartbeat_at (Firestore serverTimestamp, NOT Unix ms) - display_name - current_spool_uid_1 / current_spool_uid_2 (twin-tag pair) - wifi_signal_dbm, battery_percent, is_charging - power_source enum ("ac" | "battery" | "usb" | "poe") - hardware_revision Twin-tag detection: when both _1 and _2 are filled, Studio cross- references them against state.rows. If one row's twin_tag_uid points at the other, render one card (= one physical spool). Otherwise, render two cards (= two unrelated spools on the platform). ═══ i18n ═══ 9 new keys × 9 locales (en / fr / de / es / it / pl / pt / pt-pt / zh): - uidMigrConfirmTitle / Msg / Remind / Accept - uidMigrProgressTitle / Msg / Warn - uidMigrDurationSeconds / Minutes (pluralised via {one,other}) The confirm modal message uses `{{count}}` and `{{duration}}` interpolation; the duration is computed by a JS helper that maps spool count → human-friendly time string. ═══ All previous v1.4.5-prep changes are now released ═══ This release also rolls in the changes that were already merged to main but never tagged: Google Auth via system browser (Touch ID + passkey work), friend banner repositioned to the top header with own-user variant, sidebar avatar swap-back ⇄ affordance, double chevron on the "Not Stored" tile, scale icon red fixed when no scale paired, friend-view bleed-through bug fix (3-layer defence), WCAG-driven readable initials on light avatar backgrounds, and the lazy decimal → hex migration core itself. Files: package.json + package-lock.json (1.4.4 → 1.4.5), README.md (changelog), main.js, preload.js, renderer/inventory.html + inventory.css + inventory.js, all 9 renderer/locales/*.json. Triggers GitHub Actions: tag v1.4.5 will run the macOS / Linux / Windows matrix. Windows .exe will be UNSIGNED (Microsoft Trusted Signing identity validation for STARGATE GROUP is still in progress, no TRUSTED_SIGNING_CERT_PROFILE variable on the repo). The signing step is skipped via the existing if-guard in build.yml, so the rest of the pipeline runs cleanly.
1 parent 810e4f5 commit 719ffe5

17 files changed

Lines changed: 724 additions & 50 deletions

‎README.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@
4141

4242
## Changelog
4343

44+
### v1.4.5 — 2026-05-03
45+
46+
- **Google sign-in via Touch ID / passkey.** Replaced `firebase.auth().signInWithPopup()` (whose Chromium popup couldn't reach macOS authd, leaving "Use your passkey" inert) with the loopback OAuth flow (RFC 8252 + PKCE). The system browser opens for the auth step, so Touch ID, passkeys, and hardware keys work natively. After the auth handshake completes, the Electron window is brought back to the foreground automatically. Falls back to popup gracefully if the loopback step fails for any reason.
47+
- **Lazy on-the-fly migration of legacy decimal spool ids → hex uppercase.** Whenever Studio sees an inventory doc whose id is in the legacy decimal big-endian form (e.g. `8307741719072896`), it migrates it to the canonical hex form (`1D895E7C004A80`) in the background. Migration is idempotent, atomic per spool (single Firestore batch per migration: SET hex doc + SET map entry + UPDATE twin partner's `twin_tag_uid` + DELETE decimal doc), polite (250-500 ms gap between writes to stay well within Firestore quota), and safe vs concurrent mobile-app writes. A new `users/{uid}/uidMigrationMap/{decimal_uid}` collection serves as a bridge — any client still holding a legacy decimal UID can resolve it to the canonical hex doc in one read.
48+
- **Migration consent + progress UI.** Two modals coordinate the migration UX:
49+
- A **consent modal** asks for confirmation before any write, showing the spool count and an estimated duration ("about N seconds" / "about N minutes"). Two buttons: "Update now" / "Remind me later" (re-prompts at next app launch). Fully translated across all 9 locales.
50+
- A **lock-screen progress modal** shows during the initial sweep (≥ 3 docs) with a progress bar and an explicit "do not close the app" warning. Cmd+Q during this phase is intercepted by the main process — a native macOS dialog asks the user to confirm before quitting mid-migration.
51+
- **TigerScale v2 schema cutover.** The scale heartbeat document at `users/{uid}/scales/{mac}` is now read with the v2 field names: `last_heartbeat_at` (Firestore serverTimestamp), `display_name`, `current_spool_uid_1` / `_2` (twin-pair detection), `wifi_signal_dbm`, `power_source` (enum `ac` | `battery` | `usb` | `poe`), `battery_percent`, `is_charging`, `hardware_revision`. No backward compatibility with v1 names — firmwares writing the legacy `name` / `last_seen` / `last_spool` / `rssi` / `battery_pct` fields will appear offline / unnamed until they ship a v2 update. Full contract documented at https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/clients/tigerscale-doc-schema.md.
52+
- **Twin-pair display on the TigerScale side-card.** When two RFID tags are simultaneously present and reference each other via `twin_tag_uid`, Studio renders them as a single physical spool (one card). Two unrelated tags render as two cards.
53+
- **Friend banner repositioned + own-user variant.** The "READ-ONLY" pill that used to sit inside `#card-inv` now lives in the top header, to the left of the KPI stats. Same chip frame in own-user mode shows a random welcome greeting from `t("welcomeBack")` instead of the READ-ONLY badge. Avatar size aligned to the stat tiles' height (50×50). The decorative card frame around the chip was removed for a cleaner top-row look.
54+
- **Sidebar avatar — swap-back affordance.** A small ⇄ badge appears at the bottom-right of the user's own avatar in the sidebar while a friend's inventory is being previewed. The whole avatar acts as a one-click "return to my own inventory" button in that mode. Pointer-events on the badge funnel clicks to the avatar itself.
55+
- **Storage view "Not Stored" tile — double chevron.** Single `›` replaced with a double `»` to better telegraph that clicking opens a side-card. Color inherits the orange of the `.rv-stat-chev` rule.
56+
- **Scale health icon — red when no scale paired.** When `state.scales.length === 0`, the ⚖ icon in the header turns red as a discoverability cue. Stays at default 18px size with no animation per user feedback.
57+
- **Friend-view bug fixed — previous user's data no longer bleeds through.** Three layers of protection: `unsubscribeInventory()` + `unsubscribeRacks()` are called BEFORE any state mutation in `switchToFriendView`, both `subscribeInventory` and `subscribeRacks` snapshot callbacks now have a `state.friendView` early-return guard (defence-in-depth against in-flight buffered events), and `switchBackToOwnView` calls `renderStats()` + `renderInventory()` immediately so the friend's data is wiped from the DOM the same frame the user clicks "swap back". `renderInventory()` also now hands off to `renderRackView()` even on empty/loading state when `viewMode === "rack"` so stale rack DOM is cleared.
58+
- **Initials contrast on coloured avatars.** A new `readableTextOn(bg)` helper computes WCAG luminance via a 1×1 canvas and switches initials text colour to `#1a1a1a` for light avatar backgrounds. Applied to all 10 avatar render sites.
59+
- **i18n.** 9 new keys for migration consent / progress modals, with proper `{{count}}` and `{{duration}}` interpolation, plus pluralised duration formatters (`one` / `other`). All 9 locales (en / fr / de / es / it / pl / pt / pt-pt / zh).
60+
4461
### v1.4.4 — 2026-05-02
4562

4663
- **Auto-update toggle.** New "Updates" section in **Settings** lets the user enable or disable automatic update downloads, and exposes a **Check for updates now** button. The preference persists to `<userData>/auto-update.json`. When enabled, the existing `electron-updater` flow runs on launch and prompts for restart when an update is ready; when disabled, the app stays on the installed version until the user manually checks. (`main.js`, `preload.js`, `renderer/inventory.js`)

‎main.js‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,48 @@ function initUpdater() {
495495
}
496496

497497
// IPC: renderer asks to install downloaded update
498+
// ─────────────────────────────────────────────────────────────────────────
499+
// UID migration — block accidental app quit during the initial sweep
500+
// ─────────────────────────────────────────────────────────────────────────
501+
//
502+
// Tiger Studio Manager migrates legacy decimal-format inventory ids to hex
503+
// uppercase in the background (see renderer/inventory.js). On the first
504+
// launch after the new mobile-app-version cutover, a user with a large
505+
// pre-existing inventory may have several hundred docs to migrate, taking
506+
// 30–120 seconds. The renderer puts up a lock-screen modal saying "do not
507+
// close the app", but a determined user can still hit Cmd+Q.
508+
//
509+
// The renderer signals via the `migration:set-in-flight` IPC when the
510+
// sweep starts/ends. While in flight, we intercept `before-quit` and
511+
// `mainWindow.close` events and pop a confirm dialog: leaving mid-sweep
512+
// is safe (next launch resumes), but we want the user to KNOW that.
513+
let _migrationInFlight = false;
514+
ipcMain.on('migration:set-in-flight', (_evt, inFlight) => {
515+
_migrationInFlight = !!inFlight;
516+
});
517+
518+
const { dialog } = require('electron');
519+
let _quitConfirmedDuringMigration = false;
520+
app.on('before-quit', (event) => {
521+
if (!_migrationInFlight || _quitConfirmedDuringMigration) return;
522+
// Block this quit attempt and ask the user to confirm
523+
event.preventDefault();
524+
if (mainWindow) mainWindow.show();
525+
const choice = dialog.showMessageBoxSync(mainWindow, {
526+
type: 'warning',
527+
title: 'Migration in progress',
528+
message: 'Inventory upgrade is still running.',
529+
detail: 'Closing now is safe — the migration will resume the next time you open Tiger Studio Manager — but for the cleanest experience, please let it finish (it usually takes less than a minute).\n\nQuit anyway?',
530+
buttons: ['Wait for it to finish', 'Quit anyway'],
531+
defaultId: 0,
532+
cancelId: 0,
533+
});
534+
if (choice === 1) {
535+
_quitConfirmedDuringMigration = true;
536+
app.quit(); // re-trigger the quit, this time we let it through
537+
}
538+
});
539+
498540
ipcMain.on('install-update', () => {
499541
autoUpdater.quitAndInstall();
500542
});

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tigertag-inventory",
3-
"version": "1.4.4",
3+
"version": "1.4.5",
44
"description": "Tiger Studio Manager — desktop app",
55
"main": "main.js",
66
"engines": {

‎preload.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
4747
// inert. Resolves with `{ ok, idToken, accessToken }` or `{ ok:false, error }`.
4848
signInWithGoogleLoopback: () => ipcRenderer.invoke('auth:google-loopback'),
4949

50+
// ── UID migration in-flight signal ─────────────────────────────────────
51+
// Renderer announces when the inventory migration sweep starts and
52+
// finishes. Main uses this to intercept Cmd+Q / window close and prompt
53+
// the user before letting them quit mid-migration (which would leave
54+
// a partial state, even though the next launch would resume cleanly).
55+
setMigrationInFlight: (inFlight) => ipcRenderer.send('migration:set-in-flight', !!inFlight),
56+
5057
// ── App info (version / platform — used by the diagnostic report) ──────
5158
getAppInfo: () => ipcRenderer.invoke('app:info'),
5259

‎renderer/inventory.css‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2836,6 +2836,162 @@
28362836
background: var(--danger) !important; color: #fff !important;
28372837
}
28382838

2839+
/* ── UID format migration overlay ─────────────────────────────────────
2840+
Lock-screen modal shown while Tiger Studio is migrating legacy
2841+
decimal-format inventory ids to the new hex canonical form. Z-index
2842+
above everything so the user CAN'T interact with the rest of the UI
2843+
and CAN'T dismiss the modal until the queue drains. The message
2844+
explicitly warns the user not to close the app. */
2845+
.uid-migr-overlay {
2846+
z-index: 10000;
2847+
}
2848+
.uid-migr-card {
2849+
max-width: 480px;
2850+
text-align: center;
2851+
padding: 32px 28px 28px;
2852+
}
2853+
.uid-migr-icon {
2854+
width: 64px; height: 64px;
2855+
margin: 0 auto 18px;
2856+
border-radius: 50%;
2857+
background: var(--primary-soft, rgba(255,122,24,.12));
2858+
display: flex; align-items: center; justify-content: center;
2859+
color: var(--primary);
2860+
animation: uidMigrSpin 1.6s linear infinite;
2861+
}
2862+
.uid-migr-icon .icon {
2863+
background: var(--primary);
2864+
width: 28px; height: 28px;
2865+
}
2866+
.uid-migr-card.uid-migr--done .uid-migr-icon {
2867+
background: rgba(16,185,129,.14);
2868+
color: #10b981;
2869+
animation: none;
2870+
}
2871+
.uid-migr-card.uid-migr--done .uid-migr-icon .icon {
2872+
background: #10b981;
2873+
/* Switch from refresh-arrow to check via mask swap */
2874+
-webkit-mask-image: url('../assets/svg/icons/icon_check.svg');
2875+
mask-image: url('../assets/svg/icons/icon_check.svg');
2876+
}
2877+
@keyframes uidMigrSpin {
2878+
from { transform: rotate(0deg); }
2879+
to { transform: rotate(360deg); }
2880+
}
2881+
.uid-migr-title {
2882+
font-size: 20px;
2883+
font-weight: 700;
2884+
color: var(--text);
2885+
margin: 0 0 12px;
2886+
letter-spacing: -.01em;
2887+
}
2888+
.uid-migr-msg {
2889+
font-size: 14px;
2890+
color: var(--muted);
2891+
line-height: 1.5;
2892+
margin: 0 0 22px;
2893+
}
2894+
.uid-migr-msg strong {
2895+
color: var(--text);
2896+
font-weight: 700;
2897+
}
2898+
.uid-migr-progress {
2899+
margin-bottom: 16px;
2900+
}
2901+
.uid-migr-progress-track {
2902+
height: 8px;
2903+
background: var(--border);
2904+
border-radius: 8px;
2905+
overflow: hidden;
2906+
margin-bottom: 8px;
2907+
}
2908+
.uid-migr-progress-fill {
2909+
height: 100%;
2910+
background: linear-gradient(90deg, var(--primary), #ffb056);
2911+
width: 0%;
2912+
transition: width .25s ease;
2913+
border-radius: 8px;
2914+
}
2915+
.uid-migr-count {
2916+
font-size: 13px;
2917+
font-weight: 600;
2918+
color: var(--text);
2919+
font-variant-numeric: tabular-nums;
2920+
}
2921+
.uid-migr-warn {
2922+
font-size: 12px;
2923+
color: var(--muted);
2924+
background: var(--surface-2);
2925+
border: 1px solid var(--border);
2926+
border-radius: 8px;
2927+
padding: 10px 12px;
2928+
line-height: 1.4;
2929+
}
2930+
.uid-migr-card.uid-migr--done .uid-migr-warn {
2931+
display: none;
2932+
}
2933+
.uid-migr-card.uid-migr--done .uid-migr-msg {
2934+
/* In success state, simplify to a one-liner */
2935+
display: none;
2936+
}
2937+
.uid-migr-card.uid-migr--done .uid-migr-title::after {
2938+
content: " — done";
2939+
color: #10b981;
2940+
}
2941+
2942+
/* ── Confirm modal (phase 1) — user consent before migration starts ──
2943+
Same visual base as the progress modal but DISMISSIBLE, with three
2944+
action buttons. The icon doesn't spin (no operation in progress yet). */
2945+
.uid-migr-confirm-card {
2946+
max-width: 480px;
2947+
text-align: center;
2948+
padding: 32px 28px 24px;
2949+
}
2950+
.uid-migr-confirm-icon {
2951+
animation: none; /* override the spinning animation from .uid-migr-icon */
2952+
}
2953+
#uidMigrationConfirmCount {
2954+
font-weight: 700;
2955+
color: var(--text);
2956+
}
2957+
.uid-migr-confirm-actions {
2958+
display: flex; flex-wrap: wrap;
2959+
justify-content: center;
2960+
gap: 8px;
2961+
margin-top: 20px;
2962+
}
2963+
.uid-migr-confirm-btn {
2964+
flex: 1 1 auto;
2965+
min-width: 110px;
2966+
height: 38px;
2967+
padding: 0 18px;
2968+
border-radius: 10px;
2969+
font-size: 13px;
2970+
font-weight: 600;
2971+
cursor: pointer;
2972+
font-family: inherit;
2973+
transition: background .15s, border-color .15s, color .15s;
2974+
}
2975+
.uid-migr-confirm-btn.ghost {
2976+
background: transparent;
2977+
border: 1.5px solid var(--border);
2978+
color: var(--text);
2979+
}
2980+
.uid-migr-confirm-btn.ghost:hover {
2981+
border-color: var(--primary);
2982+
color: var(--primary);
2983+
background: var(--primary-soft, rgba(255,122,24,.06));
2984+
}
2985+
.uid-migr-confirm-btn.primary {
2986+
background: var(--primary);
2987+
border: 1.5px solid var(--primary);
2988+
color: #fff;
2989+
box-shadow: 0 2px 10px rgba(255,122,24,.25);
2990+
}
2991+
.uid-migr-confirm-btn.primary:hover {
2992+
background: var(--primary-hover, #ff8a30);
2993+
}
2994+
28392995
/* Friend view — full error state inside card-inv */
28402996
.friend-inv-error {
28412997
text-align: center;

‎renderer/inventory.html‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,51 @@
10201020
</div>
10211021
</div>
10221022

1023+
<!-- ─── UID format migration — phase 1: user consent ───────────────────────
1024+
Asked once per session when Studio detects legacy decimal-format spool
1025+
ids. Three actions:
1026+
• Update now → starts the migration (phase-2 lock-screen modal opens)
1027+
• Remind me later → snoozes for 24 h (persisted in localStorage)
1028+
• Later → defers for the rest of this session only
1029+
If the user dismisses with the close X (top-right) we treat it as "Later". -->
1030+
<div class="modal-overlay uid-migr-confirm-overlay" id="uidMigrationConfirmOverlay">
1031+
<div class="modal-card uid-migr-confirm-card" role="dialog" aria-labelledby="uidMigrationConfirmTitle">
1032+
<div class="uid-migr-icon uid-migr-confirm-icon"><span class="icon icon-refresh icon-22"></span></div>
1033+
<h2 class="uid-migr-title" id="uidMigrationConfirmTitle" data-i18n="uidMigrConfirmTitle">Inventory format upgrade available</h2>
1034+
<p class="uid-migr-msg" id="uidMigrationConfirmMsg">
1035+
<!-- Dynamic content with {{count}} — populated by JS t() at show time -->
1036+
</p>
1037+
<div class="uid-migr-confirm-actions">
1038+
<button class="ghost uid-migr-confirm-btn" id="uidMigrationConfirmRemind" data-i18n="uidMigrConfirmRemind">Remind me later</button>
1039+
<button class="primary uid-migr-confirm-btn" id="uidMigrationConfirmAccept" data-i18n="uidMigrConfirmAccept">Update now</button>
1040+
</div>
1041+
</div>
1042+
</div>
1043+
1044+
<!-- ─── UID format migration — phase 2: lock-screen progress ──────────────
1045+
Opens AFTER the user clicked "Update now" on phase 1. NON-DISMISSIBLE
1046+
while the queue drains. Cmd+Q during this phase pops a confirm dialog
1047+
in main.js (see migration:set-in-flight IPC). After the last commit it
1048+
transitions to a success state and auto-closes 1.8 s later. -->
1049+
<div class="modal-overlay uid-migr-overlay" id="uidMigrationOverlay">
1050+
<div class="modal-card uid-migr-card" role="alertdialog" aria-live="polite" aria-busy="true">
1051+
<div class="uid-migr-icon"><span class="icon icon-refresh icon-22"></span></div>
1052+
<h2 class="uid-migr-title" id="uidMigrationTitle" data-i18n="uidMigrProgressTitle">Inventory format upgrade in progress</h2>
1053+
<p class="uid-migr-msg" id="uidMigrationMsg" data-i18n="uidMigrProgressMsg">
1054+
Your inventory is being upgraded to a new, more efficient storage format. Please do not close the app until this finishes. Each spool only takes a fraction of a second.
1055+
</p>
1056+
<div class="uid-migr-progress">
1057+
<div class="uid-migr-progress-track">
1058+
<div class="uid-migr-progress-fill" id="uidMigrationBar"></div>
1059+
</div>
1060+
<div class="uid-migr-count" id="uidMigrationCount">0 / 0</div>
1061+
</div>
1062+
<div class="uid-migr-warn" id="uidMigrationWarn" data-i18n="uidMigrProgressWarn">
1063+
⚠ Closing the app now would just resume on next launch — but for the cleanest experience, let it finish.
1064+
</div>
1065+
</div>
1066+
</div>
1067+
10231068
<script src="lib/firebase/firebase-app-compat.js"></script>
10241069
<script src="lib/firebase/firebase-auth-compat.js"></script>
10251070
<script src="lib/firebase/firebase-firestore-compat.js"></script>

0 commit comments

Comments
 (0)