From b5efd9eb128f8dcb39a743447f80c63e42bc9342 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Sun, 17 May 2026 14:59:36 +0000 Subject: [PATCH] Add system updates page to admin section Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true --- artifacts/api-server/package.json | 2 + artifacts/api-server/src/routes/index.ts | 2 + artifacts/api-server/src/routes/system.ts | 113 ++++++++++ artifacts/tx-os/src/locales/ar.json | 19 ++ artifacts/tx-os/src/locales/en.json | 19 ++ artifacts/tx-os/src/pages/admin.tsx | 198 +++++++++++++++++- .../src/generated/api.schemas.ts | 26 +++ lib/api-client-react/src/generated/api.ts | 81 +++++++ lib/api-spec/openapi.yaml | 58 +++++ lib/api-zod/src/generated/api.ts | 31 +++ pnpm-lock.yaml | 18 ++ version.json | 4 + 12 files changed, 568 insertions(+), 3 deletions(-) create mode 100644 artifacts/api-server/src/routes/system.ts create mode 100644 version.json diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json index f3d0b54a..ceb3ea15 100644 --- a/artifacts/api-server/package.json +++ b/artifacts/api-server/package.json @@ -35,6 +35,7 @@ "pino-http": "^10", "playwright-core": "^1.59.1", "sanitize-html": "^2.17.3", + "semver": "^7.6.3", "socket.io": "^4.8.3", "web-push": "^3.6.7", "zod": "catalog:" @@ -50,6 +51,7 @@ "@types/nodemailer": "^8.0.0", "@types/pdfkit": "^0.17.6", "@types/sanitize-html": "^2.16.1", + "@types/semver": "^7.5.8", "@types/web-push": "^3.6.4", "esbuild": "^0.27.3", "esbuild-plugin-pino": "^2.3.3", diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 10c62dd3..da09bce4 100644 --- a/artifacts/api-server/src/routes/index.ts +++ b/artifacts/api-server/src/routes/index.ts @@ -17,6 +17,7 @@ import rolesRouter from "./roles"; import auditRouter from "./audit"; import executiveMeetingsRouter from "./executive-meetings"; import pushRouter from "./push"; +import systemRouter from "./system"; const router: IRouter = Router(); @@ -38,5 +39,6 @@ router.use(rolesRouter); router.use(auditRouter); router.use(executiveMeetingsRouter); router.use(pushRouter); +router.use(systemRouter); export default router; diff --git a/artifacts/api-server/src/routes/system.ts b/artifacts/api-server/src/routes/system.ts new file mode 100644 index 00000000..6662f963 --- /dev/null +++ b/artifacts/api-server/src/routes/system.ts @@ -0,0 +1,113 @@ +import { Router, type IRouter } from "express"; +import semver from "semver"; +import { requireAdmin } from "../middlewares/auth"; +import versionFile from "../../../../version.json" with { type: "json" }; +import { GetSystemVersionResponse } from "@workspace/api-zod"; + +const router: IRouter = Router(); + +const CURRENT_VERSION: string = versionFile.version; +const CURRENT_BUILD: string = versionFile.build; + +const LATEST_URL = process.env.LATEST_VERSION_URL ?? ""; +const FETCH_TIMEOUT_MS = 5000; + +type LatestPayload = { + version?: unknown; + build?: unknown; +}; + +router.get("/system/version", requireAdmin, async (_req, res): Promise => { + const checkedAt = new Date().toISOString(); + + if (!LATEST_URL) { + res.json( + GetSystemVersionResponse.parse({ + current: { version: CURRENT_VERSION, build: CURRENT_BUILD }, + latest: null, + status: "not-configured", + errorMessage: null, + checkedAt, + configured: false, + }), + ); + return; + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetch(LATEST_URL, { + signal: controller.signal, + headers: { Accept: "application/json" }, + }); + if (!response.ok) { + res.json( + GetSystemVersionResponse.parse({ + current: { version: CURRENT_VERSION, build: CURRENT_BUILD }, + latest: null, + status: "error", + errorMessage: `HTTP ${response.status}`, + checkedAt, + configured: true, + }), + ); + return; + } + const body = (await response.json()) as LatestPayload; + const rawVersion = typeof body.version === "string" ? body.version : ""; + const rawBuild = typeof body.build === "string" ? body.build : ""; + const cleaned = semver.valid(semver.coerce(rawVersion)); + const localCleaned = semver.valid(semver.coerce(CURRENT_VERSION)); + if (!cleaned || !localCleaned) { + res.json( + GetSystemVersionResponse.parse({ + current: { version: CURRENT_VERSION, build: CURRENT_BUILD }, + latest: { version: rawVersion, build: rawBuild }, + status: "error", + errorMessage: "Invalid version format", + checkedAt, + configured: true, + }), + ); + return; + } + // semver.gt: strictly greater → update available. Equal or older → + // up to date (we never tell the user to "downgrade" — the desktop + // build is the source of truth for that scenario). + const status = semver.gt(cleaned, localCleaned) + ? "update-available" + : "up-to-date"; + res.json( + GetSystemVersionResponse.parse({ + current: { version: CURRENT_VERSION, build: CURRENT_BUILD }, + latest: { version: rawVersion, build: rawBuild }, + status, + errorMessage: null, + checkedAt, + configured: true, + }), + ); + } catch (err) { + const message = + err instanceof Error + ? err.name === "AbortError" + ? "Request timed out" + : err.message + : "Unknown error"; + res.json( + GetSystemVersionResponse.parse({ + current: { version: CURRENT_VERSION, build: CURRENT_BUILD }, + latest: null, + status: "error", + errorMessage: message, + checkedAt, + configured: true, + }), + ); + } finally { + clearTimeout(timer); + } +}); + +export default router; diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index aae157ea..f35cad83 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -528,8 +528,27 @@ "auditLog": "سجل التدقيق", "userManagement": "إدارة المستخدمين", "settings": "الإعدادات", + "systemUpdates": "تحديثات النظام", "menu": "القائمة" }, + "systemUpdates": { + "title": "تحديثات النظام", + "subtitle": "تحقّق فقط من توفر إصدار أحدث — لا يقوم النظام بتثبيت أي تحديث.", + "currentVersion": "الإصدار الحالي", + "buildNumber": "رقم البناء", + "latestVersion": "أحدث إصدار", + "updateStatus": "حالة التحديث", + "statusUpToDate": "النظام محدّث", + "statusUpdateAvailable": "يتوفّر تحديث جديد", + "statusNotConfigured": "لم يتم تهيئة مصدر التحديثات", + "statusError": "تعذّر التحقق من التحديثات", + "notConfiguredHint": "اطلب من المسؤول ضبط متغيّر البيئة LATEST_VERSION_URL على رابط ملف latest.json الخارجي لتفعيل التحقق.", + "checkNow": "تحقّق الآن", + "loading": "جاري التحقق من الإصدار…", + "lastChecked": "آخر تحقق", + "toastErrorTitle": "تعذّر التحقق", + "toastErrorDesc": "لم نتمكن من الوصول إلى مصدر التحديثات. حاول مرة أخرى لاحقاً." + }, "apps": { "counts": { "groups": "{{count}} مجموعة", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 695d5d79..c0a8ec2d 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -504,8 +504,27 @@ "auditLog": "Audit log", "userManagement": "User Management", "settings": "Settings", + "systemUpdates": "System Updates", "menu": "Menu" }, + "systemUpdates": { + "title": "System Updates", + "subtitle": "Check whether a newer version is available — this page never installs an update.", + "currentVersion": "Current Version", + "buildNumber": "Build Number", + "latestVersion": "Latest Version", + "updateStatus": "Update Status", + "statusUpToDate": "System is up to date", + "statusUpdateAvailable": "New update available", + "statusNotConfigured": "Update source not configured", + "statusError": "Unable to check for updates", + "notConfiguredHint": "Ask the administrator to set the LATEST_VERSION_URL environment variable to the external latest.json URL to enable checking.", + "checkNow": "Check Now", + "loading": "Checking version…", + "lastChecked": "Last checked", + "toastErrorTitle": "Check failed", + "toastErrorDesc": "Could not reach the update source. Please try again later." + }, "apps": { "counts": { "groups": "{{count}} groups", diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index fd18f5cc..4cdbbf0c 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -27,6 +27,8 @@ import { useUpdateAppSettings, useGetAdminStats, getGetAdminStatsQueryKey, + useGetSystemVersion, + getGetSystemVersionQueryKey, useGetAdminAppOpensByApp, getGetAdminAppOpensByAppQueryKey, getAdminAppOpensByApp, @@ -124,6 +126,7 @@ import type { ServiceDependentOrdersPage, UserDependentNotesPage, UserDependentOrdersPage, + SystemVersionResponse, } from "@workspace/api-client-react"; import { ListUsersStatus } from "@workspace/api-client-react"; import { useQueryClient, useQuery } from "@tanstack/react-query"; @@ -131,7 +134,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { useUpload, type UploadResponse } from "@workspace/object-storage-web"; import { isBuiltinAppSlug } from "@workspace/db/built-in-apps"; import { resolveServiceImageUrl } from "@/lib/image-url"; -import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon, TrendingUp, TrendingDown, UserPlus, Activity, KeyRound, Copy, ChevronDown, ScrollText, Download, X, ImageIcon, Power, HelpCircle, Palette, Link2, Hash, Lock, ExternalLink, AlertTriangle, type LucideIcon } from "lucide-react"; +import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon, TrendingUp, TrendingDown, UserPlus, Activity, KeyRound, Copy, ChevronDown, ScrollText, Download, X, ImageIcon, Power, HelpCircle, Palette, Link2, Hash, Lock, ExternalLink, AlertTriangle, Package, RefreshCw, CheckCircle2, AlertCircle, type LucideIcon } from "lucide-react"; import * as LucideIcons from "lucide-react"; function resolveAppIcon(name: string): LucideIcon | null { @@ -184,7 +187,7 @@ async function fetchAdminApps(): Promise { return res.json(); } -type AdminSection = "dashboard" | "apps" | "services" | "users" | "groups" | "roles" | "audit-log" | "settings"; +type AdminSection = "dashboard" | "apps" | "services" | "users" | "groups" | "roles" | "audit-log" | "settings" | "system-updates"; function LeaderboardAvatar({ src, @@ -1081,6 +1084,11 @@ export default function AdminPage() { ], }, { key: "settings", label: t("admin.nav.settings"), Icon: Settings }, + // #575: Read-only "System Updates" page — checks the running build + // against a remote latest.json (configured via LATEST_VERSION_URL on + // the server) and shows whether a newer version is available. It + // never performs an update — pure status surface. + { key: "system-updates", label: t("admin.nav.systemUpdates"), Icon: Package }, ]; // Sync section to URL hash so deep links work @@ -1090,7 +1098,7 @@ export default function AdminPage() { const sectionMatch = hash.match(/section=([a-z-]+)/); if (sectionMatch) { const s = sectionMatch[1] as AdminSection; - if (["dashboard", "apps", "services", "users", "groups", "roles", "audit-log", "settings"].includes(s)) { + if (["dashboard", "apps", "services", "users", "groups", "roles", "audit-log", "settings", "system-updates"].includes(s)) { setSection(s); } } @@ -2211,6 +2219,8 @@ export default function AdminPage() { )} + + {section === "system-updates" && } @@ -2331,6 +2341,188 @@ export default function AdminPage() { ); } +// #575: System Updates panel — read-only version checker. Reads the +// local build (server returns version.json embedded at build time) +// and, if the server has LATEST_VERSION_URL configured, compares it +// against the remote latest.json via semver on the server side. We +// never perform an update from this screen — pure status surface. +// The bell icon and other admin features stay untouched. +function SystemUpdatesPanel() { + const { t, i18n } = useTranslation(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const queryKey = getGetSystemVersionQueryKey(); + const { data, isLoading, isFetching, refetch } = useGetSystemVersion({ + query: { queryKey, refetchOnWindowFocus: false }, + }); + + const handleCheckNow = async () => { + // Invalidate first so React Query treats the next fetch as fresh + // (no stale cache reuse) — this is the user explicitly asking for + // a re-check, so we want a real network hit. + await queryClient.invalidateQueries({ queryKey }); + const result = await refetch(); + if (result.error) { + toast({ + title: t("admin.systemUpdates.toastErrorTitle"), + description: t("admin.systemUpdates.toastErrorDesc"), + variant: "destructive", + }); + } + }; + + const formatChecked = (iso: string | null | undefined) => { + if (!iso) return null; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return null; + return new Intl.DateTimeFormat(i18n.language === "ar" ? "ar" : "en", { + dateStyle: "medium", + timeStyle: "short", + }).format(d); + }; + + const renderStatusBadge = (resp: SystemVersionResponse | undefined) => { + if (!resp) return null; + const map: Record< + SystemVersionResponse["status"], + { label: string; cls: string; Icon: typeof CheckCircle2 } + > = { + "up-to-date": { + label: t("admin.systemUpdates.statusUpToDate"), + cls: "bg-emerald-50 text-emerald-700 border-emerald-200", + Icon: CheckCircle2, + }, + "update-available": { + label: t("admin.systemUpdates.statusUpdateAvailable"), + cls: "bg-amber-50 text-amber-800 border-amber-200", + Icon: AlertCircle, + }, + "not-configured": { + label: t("admin.systemUpdates.statusNotConfigured"), + cls: "bg-slate-50 text-slate-700 border-slate-200", + Icon: HelpCircle, + }, + error: { + label: t("admin.systemUpdates.statusError"), + cls: "bg-rose-50 text-rose-700 border-rose-200", + Icon: AlertTriangle, + }, + }; + const cfg = map[resp.status]; + const Icon = cfg.Icon; + return ( + + + {cfg.label} + + ); + }; + + return ( +
+
+
+
+

+ {t("admin.systemUpdates.title")} +

+

+ {t("admin.systemUpdates.subtitle")} +

+
+ +
+ + {isLoading ? ( +
+ + {t("admin.systemUpdates.loading")} +
+ ) : data ? ( +
+
+
+ {t("admin.systemUpdates.currentVersion")} +
+
+ {data.current.version} +
+
+ {t("admin.systemUpdates.buildNumber")}:{" "} + + {data.current.build} + +
+
+ +
+
+ {t("admin.systemUpdates.latestVersion")} +
+
+ {data.latest?.version ?? "—"} +
+
+ {t("admin.systemUpdates.buildNumber")}:{" "} + + {data.latest?.build ?? "—"} + +
+
+ +
+
+ {t("admin.systemUpdates.updateStatus")} +
+ {renderStatusBadge(data)} + {data.status === "error" && data.errorMessage && ( +
+ {data.errorMessage} +
+ )} + {data.status === "not-configured" && ( +
+ {t("admin.systemUpdates.notConfiguredHint")} +
+ )} + {data.checkedAt && ( +
+ {t("admin.systemUpdates.lastChecked")}: {formatChecked(data.checkedAt)} +
+ )} +
+
+ ) : null} +
+
+ ); +} + function SiteSettingsPanel() { const { t } = useTranslation(); const { toast } = useToast(); diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 46dd7c5e..40f5eaa6 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -9,6 +9,32 @@ export interface HealthStatus { status: string; } +export interface SystemVersionInfo { + version: string; + build: string; +} + +export type SystemVersionResponseStatus = + (typeof SystemVersionResponseStatus)[keyof typeof SystemVersionResponseStatus]; + +export const SystemVersionResponseStatus = { + "up-to-date": "up-to-date", + "update-available": "update-available", + "not-configured": "not-configured", + error: "error", +} as const; + +export interface SystemVersionResponse { + current: SystemVersionInfo; + latest: SystemVersionInfo | null; + status: SystemVersionResponseStatus; + /** @nullable */ + errorMessage: string | null; + /** @nullable */ + checkedAt: string | null; + configured: boolean; +} + export interface ErrorResponse { error: string; } diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index 33a704fc..f9a4843b 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -110,6 +110,7 @@ import type { ServiceOrder, SubscribePushBody, SuccessResponse, + SystemVersionResponse, UnsubscribePushBody, UpdateAppBody, UpdateAppSettingsBody, @@ -217,6 +218,86 @@ export function useHealthCheck< return { ...query, queryKey: queryOptions.queryKey }; } +/** + * Returns the local build version (read from version.json embedded at +build time) and, if `LATEST_VERSION_URL` is configured on the +server, the latest published version fetched from that URL. Admin +only. Never performs an update — pure read. + + * @summary Get current and latest system version + */ +export const getGetSystemVersionUrl = () => { + return `/api/system/version`; +}; + +export const getSystemVersion = async ( + options?: RequestInit, +): Promise => { + return customFetch(getGetSystemVersionUrl(), { + ...options, + method: "GET", + }); +}; + +export const getGetSystemVersionQueryKey = () => { + return [`/api/system/version`] as const; +}; + +export const getGetSystemVersionQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSystemVersionQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getSystemVersion({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetSystemVersionQueryResult = NonNullable< + Awaited> +>; +export type GetSystemVersionQueryError = ErrorType; + +/** + * @summary Get current and latest system version + */ + +export function useGetSystemVersion< + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetSystemVersionQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + /** * @summary Register a new user */ diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 3b576f49..05d48a7a 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -30,6 +30,8 @@ tags: description: Object storage upload and serving endpoints - name: audit description: Admin audit log + - name: system + description: System metadata (version, update availability) paths: /healthz: @@ -45,6 +47,24 @@ paths: schema: $ref: "#/components/schemas/HealthStatus" + /system/version: + get: + operationId: getSystemVersion + tags: [system] + summary: Get current and latest system version + description: | + Returns the local build version (read from version.json embedded at + build time) and, if `LATEST_VERSION_URL` is configured on the + server, the latest published version fetched from that URL. Admin + only. Never performs an update — pure read. + responses: + "200": + description: System version info + content: + application/json: + schema: + $ref: "#/components/schemas/SystemVersionResponse" + # Auth /auth/register: post: @@ -2927,6 +2947,44 @@ components: required: - status + SystemVersionInfo: + type: object + properties: + version: + type: string + build: + type: string + required: + - version + - build + + SystemVersionResponse: + type: object + properties: + current: + $ref: "#/components/schemas/SystemVersionInfo" + latest: + oneOf: + - $ref: "#/components/schemas/SystemVersionInfo" + - type: "null" + status: + type: string + enum: [up-to-date, update-available, not-configured, error] + errorMessage: + type: ["string", "null"] + checkedAt: + type: ["string", "null"] + format: date-time + configured: + type: boolean + required: + - current + - latest + - status + - errorMessage + - checkedAt + - configured + ErrorResponse: type: object properties: diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 1f471029..99439aed 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -14,6 +14,37 @@ export const HealthCheckResponse = zod.object({ status: zod.string(), }); +/** + * Returns the local build version (read from version.json embedded at +build time) and, if `LATEST_VERSION_URL` is configured on the +server, the latest published version fetched from that URL. Admin +only. Never performs an update — pure read. + + * @summary Get current and latest system version + */ +export const GetSystemVersionResponse = zod.object({ + current: zod.object({ + version: zod.string(), + build: zod.string(), + }), + latest: zod.union([ + zod.object({ + version: zod.string(), + build: zod.string(), + }), + zod.null(), + ]), + status: zod.enum([ + "up-to-date", + "update-available", + "not-configured", + "error", + ]), + errorMessage: zod.string().nullable(), + checkedAt: zod.coerce.date().nullable(), + configured: zod.boolean(), +}); + /** * @summary Register a new user */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd7d6728..5c6fcec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: sanitize-html: specifier: ^2.17.3 version: 2.17.3 + semver: + specifier: ^7.6.3 + version: 7.8.0 socket.io: specifier: ^4.8.3 version: 4.8.3 @@ -175,6 +178,9 @@ importers: '@types/sanitize-html': specifier: ^2.16.1 version: 2.16.1 + '@types/semver': + specifier: ^7.5.8 + version: 7.7.1 '@types/web-push': specifier: ^3.6.4 version: 3.6.4 @@ -2996,6 +3002,9 @@ packages: '@types/sanitize-html@2.16.1': resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -4668,6 +4677,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -7540,6 +7554,8 @@ snapshots: dependencies: htmlparser2: 10.1.0 + '@types/semver@7.7.1': {} + '@types/send@1.2.1': dependencies: '@types/node': 25.3.5 @@ -9221,6 +9237,8 @@ snapshots: semver@6.3.1: {} + semver@7.8.0: {} + send@1.2.1: dependencies: debug: 4.4.3 diff --git a/version.json b/version.json new file mode 100644 index 00000000..f0393445 --- /dev/null +++ b/version.json @@ -0,0 +1,4 @@ +{ + "version": "0.1.0-dev", + "build": "20260517" +}