Initial commit: Hedena Gold monorepo.

First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
This commit is contained in:
s.esmaeili
2026-10-15 17:19:10 +03:30
commit ea86636027
1121 changed files with 401065 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
import type { Plugin } from 'vite';
export interface AppVersionInfo {
app: string;
build: number;
version: string;
commit: string | null;
builtAt: string;
history: { build: number; date: string; notes: string[] }[];
}
export function notesFor(app: string, message: string): string[];
export function computeAppVersion(app: string, cwd?: string): AppVersionInfo;
export function appVersion(app: 'user-app' | 'admin-panel'): Plugin;
+87
View File
@@ -0,0 +1,87 @@
// 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:
//
// اپ: … the customer app
// اپ۱: … user-app (alias of اپ)
// پنل: … 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'],
'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) });
},
};
}
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env node
// For each npm package, prints its four newest major lines: first release,
// latest version and when that latest version was published. A line with no
// release for a long time has probably stopped receiving patches.
//
// Backs the version table in docs/GO-LIVE-PLAN.md §4. Re-run it before acting
// on that table — the numbers there are only true for the day they were taken.
//
// node scripts/check-majors.mjs # the packages the plan tracks
// node scripts/check-majors.mjs vite pinia # any packages
//
// Goes through the npm CLI rather than fetch() so the machine's registry or
// mirror settings (.npmrc) apply.
import { execSync } from 'node:child_process';
const TRACKED = [
'vue', 'vite', '@vitejs/plugin-vue', 'vite-plugin-pwa', 'vue-router', 'pinia',
'typescript', 'vue-tsc', 'tailwindcss', 'ant-design-vue', 'vant', '@vueuse/core',
'@nestjs/core', 'prisma',
];
// Package names only — they are interpolated into a shell command.
const PACKAGE_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i;
const STALE_AFTER_DAYS = 180;
const pkgs = process.argv.length > 2 ? process.argv.slice(2) : TRACKED;
const now = Date.now();
for (const pkg of pkgs) {
if (!PACKAGE_NAME.test(pkg)) {
console.log(`\n${pkg}: not a package name, skipped`);
continue;
}
let published;
try {
published = JSON.parse(
execSync(`npm view ${pkg} time --json`, {
encoding: 'utf8',
timeout: 60_000,
stdio: ['ignore', 'pipe', 'ignore'],
}),
);
} catch {
console.log(`\n${pkg}: npm view failed`);
continue;
}
const lines = new Map();
for (const [version, date] of Object.entries(published)) {
// Stable releases only; this also drops the `created` / `modified` keys.
if (!/^\d+\.\d+\.\d+$/.test(version)) continue;
const major = Number(version.split('.')[0]);
const line = lines.get(major) ?? { firstVersion: version, first: date, lastVersion: version, last: date };
if (date < line.first) Object.assign(line, { firstVersion: version, first: date });
if (date > line.last) Object.assign(line, { lastVersion: version, last: date });
lines.set(major, line);
}
console.log(`\n${pkg}`);
const newestFirst = [...lines].sort(([a], [b]) => b - a).slice(0, 4);
for (const [major, line] of newestFirst) {
const idleDays = Math.floor((now - Date.parse(line.last)) / 86_400_000);
const stale = idleDays > STALE_AFTER_DAYS ? ` <- no release for ${idleDays} days` : '';
console.log(
` v${String(major).padEnd(3)} first ${line.firstVersion.padEnd(9)} ${line.first.slice(0, 10)}` +
` latest ${line.lastVersion.padEnd(9)} ${line.last.slice(0, 10)}${stale}`,
);
}
}
+34
View File
@@ -0,0 +1,34 @@
from PIL import Image
from pathlib import Path
# Point `src` at your Hedena Gold master logo, then run from the repo root.
src = Path(r"path/to/hedena-gold-logo.webp")
img = Image.open(src).convert("RGBA")
print("source size", img.size)
root = Path(__file__).resolve().parents[1]
(root / "apps/user-app/public/img").mkdir(parents=True, exist_ok=True)
(root / "apps/user-app/src/assets/img").mkdir(parents=True, exist_ok=True)
(root / "apps/admin-panel/src/assets/images/logo").mkdir(parents=True, exist_ok=True)
img.save(root / "apps/user-app/public/img/brand-logo.webp", "WEBP")
img.save(root / "apps/user-app/public/img/brand-logo.png", "PNG")
img.save(root / "apps/user-app/src/assets/img/brand-logo.png", "PNG")
img.save(root / "apps/admin-panel/src/assets/images/logo/brand-icon.webp", "WEBP")
img.save(root / "apps/admin-panel/src/assets/images/logo/brand-icon.png", "PNG")
img.save(root / "apps/admin-panel/src/assets/images/logo/brand-horizontal.png", "PNG")
for size, path in [
(192, root / "apps/user-app/public/icon-192.png"),
(512, root / "apps/user-app/public/icon-512.png"),
(192, root / "apps/admin-panel/public/icon-192.png"),
(512, root / "apps/admin-panel/public/icon-512.png"),
]:
icon = Image.new("RGBA", (size, size), (0, 0, 0, 0))
fitted = img.copy()
fitted.thumbnail((size, size), Image.Resampling.LANCZOS)
x = (size - fitted.width) // 2
y = (size - fitted.height) // 2
icon.paste(fitted, (x, y), fitted)
icon.save(path, "PNG")
print("wrote", path)