Skip to content

Commit 2d5ccb8

Browse files
committed
feat: Tiger Studio Web hotfixes — npm run web:hotfix tags web-vX.Y.Z.N (web-only, no desktop build, X.Y.Z stays the release's), web-sync on those tags, web build number in version.json / About / reload check; web build drops the Agora script (not in git, desktop-only)
1 parent 5a97627 commit 2d5ccb8

7 files changed

Lines changed: 189 additions & 51 deletions

File tree

‎.github/workflows/web-sync.yml‎

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,81 @@
11
name: Web sync
22

3-
# Tiger Studio Web (tigersystem.io/studio) and the desktop app must ALWAYS be on
3+
# Tiger Studio Web (studio.tigersystem.io) and the desktop app must ALWAYS be on
44
# the same version. Both follow the published "latest" GitHub Release: the
55
# desktop auto-updater installs it, and the Hub builds it into /studio on every
6-
# deploy (TigerTag_Hub → scripts/fetch-studio-web.mjs). The moment a release is
7-
# PUBLISHED (drafts don't count — that is when desktops start updating), this
8-
# triggers a Hub production deploy, then waits until the live site serves that
9-
# exact version, and fails loudly if it doesn't — a red run here means desktop
10-
# and web are out of sync.
6+
# deploy (TigerTag_Hub → scripts/fetch-studio-web.mjs).
7+
#
8+
# Two triggers, one job — deploy the Hub, then wait until the live site serves
9+
# exactly the expected build, and fail loudly if it doesn't (a red run here means
10+
# desktop and web are out of sync):
11+
# • a release is PUBLISHED (drafts don't count — that is when desktops start
12+
# updating) → expect "X.Y.Z", web build 0;
13+
# • a web hotfix tag `web-vX.Y.Z.N` is pushed (docs/WEB_HOTFIX.md) → expect
14+
# "X.Y.Z · web N", with no desktop build at all. X.Y.Z must be the latest
15+
# release, or the tag is refused.
1116
#
1217
# Secret: HUB_DEPLOY_HOOK_URL — the Vercel deploy hook "studio-release" of the
1318
# tiger-tag-hub project (Settings → Git → Deploy Hooks, branch main).
1419

1520
on:
1621
release:
1722
types: [published]
23+
push:
24+
tags:
25+
- 'web-v*'
1826
workflow_dispatch:
1927

2028
jobs:
2129
deploy-web:
2230
runs-on: ubuntu-latest
2331
timeout-minutes: 30
2432
steps:
25-
- name: Trigger the Hub deploy (builds this release into /studio)
33+
- name: Work out the expected web version
34+
id: want
35+
env:
36+
RELEASE_TAG: ${{ github.event.release.tag_name }}
37+
REF_NAME: ${{ github.ref_name }}
38+
EVENT: ${{ github.event_name }}
39+
run: |
40+
latest=$(curl -fsSI "https://github.com/${{ github.repository }}/releases/latest" \
41+
| tr -d '\r' | sed -n 's#^[Ll]ocation: .*/releases/tag/v##p')
42+
if [ "$EVENT" = "push" ]; then
43+
base="${REF_NAME#web-v}"; build="${base##*.}"; base="${base%.*}"
44+
if [ "$base" != "$latest" ]; then
45+
echo "::error::$REF_NAME is a hotfix of $base, but the latest release is $latest — the Hub would ignore it"
46+
exit 1
47+
fi
48+
elif [ "$EVENT" = "release" ]; then
49+
base="${RELEASE_TAG#v}"; build=0
50+
else # manual run: whatever the Hub should currently serve
51+
base="$latest"; build=""
52+
fi
53+
echo "version=$base" >> "$GITHUB_OUTPUT"
54+
echo "build=$build" >> "$GITHUB_OUTPUT"
55+
echo "Expecting $base${build:+ · web $build}"
56+
57+
- name: Trigger the Hub deploy
2658
env:
2759
HOOK: ${{ secrets.HUB_DEPLOY_HOOK_URL }}
2860
run: |
2961
test -n "$HOOK" || { echo "::error::HUB_DEPLOY_HOOK_URL secret is missing"; exit 1; }
3062
curl -fsS -X POST "$HOOK" > /dev/null
3163
echo "Hub deploy triggered"
3264
33-
- name: Wait until tigersystem.io/studio serves this version
65+
- name: Wait until studio.tigersystem.io serves it
3466
env:
35-
TAG: ${{ github.event.release.tag_name }}
67+
WANT_VERSION: ${{ steps.want.outputs.version }}
68+
WANT_BUILD: ${{ steps.want.outputs.build }}
3669
run: |
37-
if [ -z "$TAG" ]; then # manual run: the current latest release
38-
TAG=$(curl -fsSI "https://github.com/${{ github.repository }}/releases/latest" \
39-
| tr -d '\r' | sed -n 's#^[Ll]ocation: .*/releases/tag/##p')
40-
fi
41-
want="${TAG#v}"
4270
for i in $(seq 1 60); do
43-
live=$(curl -fsS "https://tigersystem.io/studio/version.json?t=$(date +%s)" 2>/dev/null \
44-
| sed -n 's/.*"version": *"\([^"]*\)".*/\1/p')
45-
echo "live=${live:-none} release=$want"
46-
[ "$live" = "$want" ] && { echo "Web and desktop are both on $want"; exit 0; }
71+
json=$(curl -fsS "https://studio.tigersystem.io/studio/version.json?t=$(date +%s)" 2>/dev/null || true)
72+
live=$(echo "$json" | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p')
73+
lb=$(echo "$json" | sed -n 's/.*"webBuild": *\([0-9]*\).*/\1/p'); lb=${lb:-0}
74+
echo "live=${live:-none} · web ${lb} want=$WANT_VERSION${WANT_BUILD:+ · web $WANT_BUILD}"
75+
if [ "$live" = "$WANT_VERSION" ] && { [ -z "$WANT_BUILD" ] || [ "$lb" = "$WANT_BUILD" ]; }; then
76+
echo "Web is on $live${WANT_BUILD:+ · web $WANT_BUILD}, desktop on $WANT_VERSION"; exit 0
77+
fi
4778
sleep 20
4879
done
49-
echo "::error::tigersystem.io/studio serves ${live:-nothing} but the release is $want — web and desktop are out of sync"
80+
echo "::error::studio.tigersystem.io serves ${live:-nothing} · web ${lb}, expected $WANT_VERSION${WANT_BUILD:+ · web $WANT_BUILD} — web and desktop are out of sync"
5081
exit 1

‎WORKLOG.md‎

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
1-
# Worklog — v2.39.1 (in progress)
1+
# Worklog — v2.40.1 (in progress)
22

33
## Added
4-
- Tiger Studio Web preparation plan (audit of the brief against the code, decisions, step plan) — `docs/STUDIO-WEB-PLAN.md`
5-
- Tiger Studio Web — browser bridge: a web `electronAPI` (links, app info, product lookup / refresh, full catalogue, image download) loaded only when the Electron preload is absent; `HAS_NATIVE` replaces the "electronAPI exists" desktop checks around RFID — `renderer/web-bridge.js`, `renderer/inventory.html`, `renderer/inventory.js`, `renderer/rfid_protocol/tigertag/index.js`
6-
- Tiger Studio Web — desktop-only UI hidden in a browser through one `data-desktop-only` attribute + `html.is-web` rule (NFC readers bar and tester, TD1S, updater card / icon / banner, printers views, camera detach); printer view modes fall back to the inventory grid — `renderer/inventory.html`, `renderer/css/00-base.css`, `renderer/inventory.js`
7-
- Tiger Studio Web — friend link `?friend=XXX-YYY` pre-fills Add-friend (web equivalent of `tigertag://friend/…`) — `renderer/inventory.js`
8-
- Tiger Studio Web — one sign-in with the Hub: on a first web visit Studio adopts the tigersystem.io Firebase session (verified addresses), signing in to Studio no longer signs the shared DEFAULT session out on the web, and signing out ends it only for the same account — `renderer/inventory.js` (`_adoptSharedWebSession`, sign-in flows, `fbSignOut`)
9-
- Tiger Studio Web — deployment guide (architecture, what works on the web, Hub/Vercel/DNS/Firebase setup) — `docs/WEB_DEPLOYMENT.md`
10-
- Tiger Studio Web — static build `npm run web:build` (`--out`, `--base /studio/`) → `dist-web/` with the app page at the root via `<base>`; `npm run web:serve` to try it locally — `scripts/build-web.mjs`, `package.json`, `.gitignore`
11-
- Tiger Studio Web — installable as an app (PWA): the web build ships a manifest (scope = the served folder, standalone, the app icons) and the home-screen tags iOS reads; an "Install the app" sidebar button appears only when the browser can install it (replays Chrome/Edge/Android's install prompt, unfolds the Share → Add to Home Screen hint on iPhone/iPad), hidden once installed; no service worker on purpose (no stale offline copy after a release) — `scripts/build-web.mjs`, `renderer/web-bridge.js`, `renderer/inventory.js`, `renderer/inventory.html`, `renderer/css/00-base.css` (`data-web-only`), `renderer/css/10-settings.css`
12-
- Tiger Studio Web — never drifts from the desktop app: `web-sync.yml` (release published → Hub deploy hook → waits for `tigersystem.io/studio/version.json` to match, red run otherwise); an open page or installed app reloads onto a new version when it comes back into view — `.github/workflows/web-sync.yml`, `renderer/web-bridge.js`, `docs/WEB_DEPLOYMENT.md`
4+
- Tiger Studio Web hotfixes — ship web-only fixes without a desktop build: `npm run web:hotfix` tags the commit `web-vX.Y.Z.N` (X.Y.Z = the latest release, checked against package.json); `web-sync.yml` also runs on those tags (refuses a non-latest base, deploys the Hub, waits for `{version, webBuild}`); the build stamps the web build number (`--web-build`, shown as "X.Y.Z · web N", part of the reload check); the Hub builds the highest hotfix of the latest release — `scripts/web-hotfix.mjs`, `scripts/build-web.mjs`, `renderer/web-bridge.js`, `.github/workflows/web-sync.yml`, `package.json`, `docs/WEB_HOTFIX.md`
135

146
## Changed
7+
- Studio Web served at the root of `studio.tigersystem.io` (no redirect); `tigersystem.io/studio` redirects there; separate sign-in from the Hub — `docs/WEB_DEPLOYMENT.md`, `docs/STUDIO-WEB-PLAN.md`, `README.md`, `FEATURES.md`, `llms.txt`, `scripts/build-web.mjs` (comments)
158

169
## Fixed
17-
- FlashForge / Snapmaker / Creality background ping threw every 30 s while signed out (no printer list yet) — `renderer/printers/{flashforge,snapmaker,creality}/index.js`
18-
- Low-stock notifications stored the desktop's local image-cache path (`/img-cache/…`) in Firestore, so the photo was missing on the account's other devices and on the web; they now store the public image URL, and the web skips the old local paths — `renderer/inventory.js`
10+
- Tiger Studio Web: the page no longer requests the Agora SDK (Anycubic cloud camera, desktop-only, not in git), which answered 404 on the web (shipped to the web as web-v2.40.0.1) — `scripts/build-web.mjs`
1911

2012
## Removed
2113

2214
## i18n
23-
- Added: `sbInstallBtn`, `installIosHint` — 11 locales

‎docs/WEB_HOTFIX.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Tiger Studio Web — web hotfixes
2+
3+
How to ship a fix to **Tiger Studio Web** (`studio.tigersystem.io`) **without a
4+
desktop build** — and without the web and desktop version numbers drifting apart.
5+
6+
## The rule
7+
8+
The version number `X.Y.Z` belongs to the **desktop release**. The web always runs
9+
the latest published release's `X.Y.Z`. A web hotfix adds a **web build number**
10+
on top: the web shows `X.Y.Z · web N`, the desktop stays on `X.Y.Z`. `X.Y.Z` itself
11+
only ever moves with a full desktop release.
12+
13+
| Change | How it ships |
14+
|---|---|
15+
| Web-only fix (browser bridge, web build, a bug only the web hits) | **Web hotfix** — `npm run web:hotfix`, live in ~2 min, no installers |
16+
| Anything a desktop user would also get (shared UI, data, behaviour) | **Normal release** (`vX.Y.Z` tag, CI builds the installers), web follows automatically |
17+
18+
**The limit to keep in mind:** a hotfix tag builds whatever is on `main` at that
19+
commit, so the web gets every commit since the release — shared code included. Use
20+
a hotfix when `main` holds only web-side changes since the release (or changes that
21+
are harmless on the web); when it holds desktop work too, cut a normal release.
22+
23+
## Shipping one
24+
25+
1. Commit and push the fix to `main` (as usual, on the founder's order).
26+
2. `npm run web:hotfix` (`-- --dry-run` to see what it would do). It checks that the
27+
commit is on `origin/main` and that `package.json` at that commit is the latest
28+
release's version, then tags it `web-vX.Y.Z.N` (next free `N`) and pushes the tag.
29+
3. The tag triggers **Web sync** (`.github/workflows/web-sync.yml`): it refuses a tag
30+
whose `X.Y.Z` is not the latest release, deploys the Hub, and waits until
31+
`studio.tigersystem.io/studio/version.json` shows `{ "version": "X.Y.Z",
32+
"webBuild": N }`. A red run means the web is not on that build.
33+
4. Open pages and installed apps reload onto it the next time they come into view.
34+
5. Note it in `WORKLOG.md` under the next version, *"(shipped to the web as
35+
web-vX.Y.Z.N)"* — it reaches the desktop with that release.
36+
37+
The desktop build (`build.yml`) only runs on `v*` tags, never on `web-v*`.
38+
39+
## How the pieces read it
40+
41+
- `scripts/build-web.mjs --web-build N` stamps `N` into `version.json` and into
42+
`renderer/web-bridge.js` (Settings → About shows `X.Y.Z · web N`; the reload check
43+
compares version **and** web build).
44+
- The Hub (`TigerTag_Hub` → `scripts/fetch-studio-web.mjs`) takes the latest release,
45+
lists `web-v<release>.*` tags with `git ls-remote`, builds the highest one, and
46+
fails the build if the result is not exactly that release and that `N`. Hotfix tags
47+
of an older release are ignored as soon as a new release is published.

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"theme:check": "node scripts/check-theme-tokens.mjs",
3434
"ffmpeg:prepare": "node scripts/prepare-ffmpeg.mjs",
3535
"web:build": "node scripts/build-web.mjs",
36-
"web:serve": "node scripts/build-web.mjs && python3 -m http.server 5181 --directory dist-web"
36+
"web:serve": "node scripts/build-web.mjs && python3 -m http.server 5181 --directory dist-web",
37+
"web:hotfix": "node scripts/web-hotfix.mjs"
3738
},
3839
"dependencies": {
3940
"agora-rtc-sdk-ng": "^4.24.4",

‎renderer/web-bridge.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
// Replaced by scripts/build-web.mjs with package.json's version.
3434
const APP_VERSION = "__STUDIO_WEB_VERSION__";
35+
// Web hotfix number on top of that release (0 = the release itself).
36+
const WEB_BUILD = Number("__STUDIO_WEB_BUILD__") || 0;
3537

3638
// No version drift with the desktop app: a tab (or the installed app) left
3739
// open across a release must not keep running the old code. When the page
@@ -49,9 +51,10 @@
4951
lastCheck = Date.now();
5052
try {
5153
const r = await fetch(new URL("../version.json", document.baseURI), { cache: "no-store" });
52-
const live = r.ok ? (await r.json()).version : null;
53-
if (live && live !== APP_VERSION && !busy()) location.reload();
54-
else if (live && live !== APP_VERSION) lastCheck = 0; // retry next time
54+
const v = r.ok ? await r.json() : null;
55+
const changed = v && (v.version !== APP_VERSION || (v.webBuild || 0) !== WEB_BUILD);
56+
if (changed && !busy()) location.reload();
57+
else if (changed) lastCheck = 0; // retry next time
5558
} catch { /* offline: try again later */ }
5659
};
5760
document.addEventListener("visibilitychange", check);
@@ -84,7 +87,8 @@
8487

8588
async getAppInfo() {
8689
return {
87-
appVersion: APP_VERSION.startsWith("__") ? "dev" : APP_VERSION,
90+
appVersion: APP_VERSION.startsWith("__") ? "dev"
91+
: WEB_BUILD ? `${APP_VERSION} · web ${WEB_BUILD}` : APP_VERSION,
8892
platform: "web",
8993
electron: "n/a",
9094
chrome: navigator.userAgent,

‎scripts/build-web.mjs‎

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
// manifest.webmanifest → makes it installable as an app (PWA)
1313
// version.json → { version } for the host / support
1414
//
15-
// The Hub (TigerTag_Hub) serves this folder at tigersystem.io/studio/.
16-
// Usage: npm run web:build [-- --out <dir>] [--base /studio/]
15+
// The Hub (TigerTag_Hub) builds this folder into its public/studio/ with
16+
// --base /studio/ and serves the page at the ROOT of studio.tigersystem.io (the
17+
// subdomain rewrites "/" to /studio/index.html; tigersystem.io/studio redirects
18+
// there).
19+
// Usage: npm run web:build [-- --out <dir>] [--base /studio/] [--web-build N]
1720

1821
import fs from "node:fs";
1922
import path from "node:path";
@@ -26,6 +29,11 @@ const OUT = path.resolve(ROOT, outArg > -1 ? process.argv[outArg + 1] : "dist-we
2629
const baseArg = process.argv.indexOf("--base");
2730
const BASE = (baseArg > -1 ? process.argv[baseArg + 1] : "/").replace(/\/?$/, "/");
2831
const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
32+
// Web hotfix number on top of the desktop release (tag web-vX.Y.Z.N → N; 0 =
33+
// the release itself). The X.Y.Z never moves without a desktop release — see
34+
// docs/WEB_HOTFIX.md.
35+
const wbArg = process.argv.indexOf("--web-build");
36+
const WEB_BUILD = wbArg > -1 ? Math.max(0, parseInt(process.argv[wbArg + 1], 10) || 0) : 0;
2937

3038
// Top-level folders shipped to the browser.
3139
const INCLUDE = ["renderer", "data", "assets"];
@@ -63,18 +71,23 @@ for (const dir of INCLUDE) copyTree(dir);
6371

6472
// Stamp the version into the web bridge (shown in Settings → About).
6573
const bridge = path.join(OUT, "renderer/web-bridge.js");
66-
fs.writeFileSync(bridge, fs.readFileSync(bridge, "utf8").replace("__STUDIO_WEB_VERSION__", VERSION));
74+
fs.writeFileSync(bridge, fs.readFileSync(bridge, "utf8")
75+
.replace("__STUDIO_WEB_VERSION__", VERSION)
76+
.replace("__STUDIO_WEB_BUILD__", String(WEB_BUILD)));
6777

68-
// Entry point: the app page itself, served at the folder's root (e.g.
69-
// tigersystem.io/studio) so the address stays short. A <base> pointing at
70-
// renderer/ makes every relative URL of the page — stylesheets, scripts,
71-
// fetch("../data/…") — resolve exactly as it does from renderer/inventory.html.
72-
// It must be absolute: "/studio" (no trailing slash) would otherwise resolve
73-
// "renderer/" against "/". The page has no #anchors, which a <base> would break.
78+
// Entry point: the app page itself, at the folder's root — on the Hub it is
79+
// served at "/" of studio.tigersystem.io, an address that is NOT the folder the
80+
// files live in (/studio/). A <base> pointing at renderer/ makes every relative
81+
// URL of the page — stylesheets, scripts, fetch("../data/…") — resolve exactly as
82+
// it does from renderer/inventory.html. It must be absolute for that reason: a
83+
// relative "renderer/" would resolve against wherever the page is served. The
84+
// page has no #anchors, which a <base> would break.
7485
//
7586
// Installable app (PWA): a web manifest + the home-screen tags iOS reads. Its
76-
// scope is the folder WITHOUT the trailing slash, because the page itself is
77-
// served at "/studio" — a "/studio/" scope would leave the page outside it.
87+
// scope is the folder WITHOUT the trailing slash, so a page served at the bare
88+
// folder address stays inside it. The Hub then sets id / start_url / scope to
89+
// "/" after the build (scripts/fetch-studio-web.mjs), because there the
90+
// installed app lives at the root of studio.tigersystem.io.
7891
// No service worker: Chrome no longer requires one to install, and an offline
7992
// cache of a 38k-line app would serve a stale version after every release.
8093
const SCOPE = BASE === "/" ? "/" : BASE.replace(/\/$/, "");
@@ -104,10 +117,15 @@ const PWA_HEAD = [
104117
`<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">`,
105118
].map(l => " " + l).join("\n");
106119

107-
const page = fs.readFileSync(path.join(ROOT, "renderer/inventory.html"), "utf8");
120+
// The Agora SDK (Anycubic cloud camera — desktop-only) is copied from
121+
// node_modules at install time and is not in git, so a clean checkout has no
122+
// file to serve; its <script> would 404. agora-cam.js already copes with a
123+
// missing `window.AgoraRTC`.
124+
const page = fs.readFileSync(path.join(ROOT, "renderer/inventory.html"), "utf8")
125+
.replace(/^\s*<script src="lib\/agora\/[^"]+"><\/script>\s*\n/m, "");
108126
if (!/<head>/i.test(page)) throw new Error("inventory.html: no <head> to inject <base> into");
109127
fs.writeFileSync(path.join(OUT, "index.html"),
110128
page.replace(/<head>/i, `<head>\n <base href="${BASE}renderer/">\n${PWA_HEAD}`));
111-
fs.writeFileSync(path.join(OUT, "version.json"), JSON.stringify({ version: VERSION }) + "\n");
129+
fs.writeFileSync(path.join(OUT, "version.json"), JSON.stringify({ version: VERSION, webBuild: WEB_BUILD }) + "\n");
112130

113-
console.log(`web build v${VERSION} (base ${BASE}) → ${path.relative(ROOT, OUT) || OUT}/ (${files} files, ${(bytes / 1048576).toFixed(1)} MB)`);
131+
console.log(`web build v${VERSION}${WEB_BUILD ? ` · web ${WEB_BUILD}` : ""} (base ${BASE}) → ${path.relative(ROOT, OUT) || OUT}/ (${files} files, ${(bytes / 1048576).toFixed(1)} MB)`);

0 commit comments

Comments
 (0)