feat(apps): version on every build, a «what changed» modal, and a clean update
- The build number is the commit count, so every build raises it with no file to bump. scripts/app-version.mjs bakes it into each front-end (__APP_BUILD__) and writes dist/version.json with that app's notes. - Notes come from commit messages: only Persian lines that start with one of the four app tags are shown, each to its own app (docs/RELEASE-NOTES.md). - Both customer apps and the admin panel poll version.json (every 5 minutes and on return to the tab, bypassing caches). A newer build opens a modal that cannot be dismissed. «بهروزرسانی» unregisters the service worker, empties Cache Storage and reloads; localStorage (sign-in, cart, settings) is kept. - Replaces legacy's version dialog, which compared against a package.json version that was never bumped and so never opened, and useRefresh, which swapped in a new service worker silently. - nginx (all three apps and nginx-spa.conf): index.html, sw.js and version.json are never cached. sw.js used to fall under the 6-month rule, which could pin a browser to an old release. Checked: served with the real nginx config, a newer version.json opens the modal with its notes; the same build does not. اپ: اجرت هر کالا حالا همان است که در فاکتور حساب میشود؛ درصد خود شما و تخفیفها هم در آن آمده است. اپ: علاقهمندیهای شما ذخیره میشوند و در هر دستگاهی که وارد شوید دیده میشوند. اپ: از این به بعد هر نسخهی تازه با فهرست تغییراتش اعلام میشود. پنل: بخش تازهی «اجرت»: فرمول اجرت را با یک نمونه امتحان و بعد تأیید کنید. پنل: شهرها مثل قبل بهصورت درختاند (کشور، استان، شهر، محله)؛ شهری که درصد ندارد درصد شهر بالاتر را میگیرد. پنل: «تاریخ تولد» به فرم مشتری عمده و تکی برگشت. پنل: در گزارش تجمیعی سفارشها، دکمهی «دریافت فایل اکسل» اضافه شد. پنل: رد احراز هویت مشتری حالا دلیل هم میگیرد. پنل: با تأیید یا رد سند مالی، به مشتری پیامک میرود؛ در پیامک گروهی، %name با نام مشتری عوض میشود. پنل: از این به بعد هر نسخهی تازهی پنل با فهرست تغییراتش اعلام میشود. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2bbd9408dd
commit
dbead8a6ba
@@ -0,0 +1,89 @@
|
||||
// App version + «what changed» notes, baked into each front-end at build time.
|
||||
//
|
||||
// Version: the build number is the commit count of HEAD, so it only ever goes
|
||||
// up and needs no file to bump. It is compiled into the bundle (__APP_BUILD__)
|
||||
// and written to dist/version.json; the running app polls that file and, when
|
||||
// the server's build is newer than its own, shows the update modal.
|
||||
//
|
||||
// Notes come from commit messages. Technical text is never shown: only lines
|
||||
// that start with a Persian tag, written for the person using that part:
|
||||
//
|
||||
// اپ: … both customer apps
|
||||
// اپ۱: … user-app only
|
||||
// اپ۲: … user-app-v2 only
|
||||
// پنل: … the admin panel
|
||||
//
|
||||
// A commit may carry several such lines. Each app shows only its own.
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const TAGS = {
|
||||
'user-app': ['اپ', 'اپ۱', 'اپ1'],
|
||||
'user-app-v2': ['اپ', 'اپ۲', 'اپ2'],
|
||||
'admin-panel': ['پنل'],
|
||||
};
|
||||
|
||||
/** How many released builds with notes the file keeps. */
|
||||
const HISTORY = 30;
|
||||
|
||||
function git(args, cwd) {
|
||||
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
}
|
||||
|
||||
/** Pure, for testing: the note lines in one commit message that belong to `app`. */
|
||||
export function notesFor(app, message) {
|
||||
const tags = TAGS[app] ?? [];
|
||||
const out = [];
|
||||
for (const raw of message.split(/\r?\n/)) {
|
||||
const m = /^\s*([^\s::]+)\s*[::]\s*(.+?)\s*$/.exec(raw);
|
||||
// The note must be Persian: a wrapped English line that happens to begin
|
||||
// with a tag (e.g. in a commit body explaining the tags) is not a note.
|
||||
if (m && tags.includes(m[1]) && /[-ۿ]/.test(m[2])) out.push(m[2]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function computeAppVersion(app, cwd = process.cwd()) {
|
||||
const builtAt = new Date().toISOString();
|
||||
try {
|
||||
const build = Number(git(['rev-list', '--count', 'HEAD'], cwd).trim());
|
||||
const commit = git(['rev-parse', '--short', 'HEAD'], cwd).trim();
|
||||
// Newest first; a record separator between commits, unit separator between fields.
|
||||
const log = git(['log', '-n', '400', '--format=%ct%x1f%B%x1e'], cwd);
|
||||
const history = [];
|
||||
log.split('\x1e').forEach((rec, i) => {
|
||||
const [ts, body] = rec.replace(/^\s+/, '').split('\x1f');
|
||||
if (!body) return;
|
||||
const notes = notesFor(app, body);
|
||||
if (!notes.length) return;
|
||||
history.push({ build: build - i, date: new Date(Number(ts) * 1000).toISOString(), notes });
|
||||
});
|
||||
return { app, build, version: `1.${build}`, commit, builtAt, history: history.slice(0, HISTORY) };
|
||||
} catch {
|
||||
// Not a git checkout (e.g. a copied tree): still a valid, if note-less, build.
|
||||
const build = Math.floor(Date.now() / 1000);
|
||||
return { app, build, version: `1.${build}`, commit: null, builtAt, history: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin: defines __APP_BUILD__ / __APP_VERSION__ and emits version.json.
|
||||
* The dev server has no version.json, so the update check stays quiet there.
|
||||
*/
|
||||
export function appVersion(app) {
|
||||
const info = computeAppVersion(app);
|
||||
return {
|
||||
name: 'app-version',
|
||||
config() {
|
||||
return {
|
||||
define: {
|
||||
__APP_BUILD__: JSON.stringify(info.build),
|
||||
__APP_VERSION__: JSON.stringify(info.version),
|
||||
},
|
||||
};
|
||||
},
|
||||
generateBundle() {
|
||||
this.emitFile({ type: 'asset', fileName: 'version.json', source: JSON.stringify(info, null, 2) });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user