Commit graph

110 commits

Author SHA1 Message Date
CRT
fd6ae707ca Report a problem: the webview's full version (the user agent carries a reduced one)
Seen live: the report said "WebView2 154.0.0.0", Chromium's reduced user agent version. The engine's brand list (`fullVersionList`) gives the full one, which the report now uses when it is a plain version (tested).
2026-10-01 03:56:18 +07:00
CRT
25f84a932f App menu and mode words in every catalog: find in page, update check, safe mode, report a problem, Custom/Re-apply
menuMessages.en.json stays the English source of the menu's words; en.json now carries them too
(the i18n test takes them into the source union) and 48 catalogs translate all 51 menu words and
the seven new mode words (plural forms per language for the match count).
2026-10-01 03:50:45 +07:00
CRT
9b3b7dcd6a Remote builds: list the output with numeric owners, so a build log never shows the machine's account name 2026-10-01 03:48:16 +07:00
CRT
3f7f0e0251 Find in page: WebView2 counts its active match from 1; the shell's index is 0-based
Seen live on the shared app: "domain" on example.com showed "2/2" for the first of two matches, and "2 matches" after Next. `ICoreWebView2Find::ActiveMatchIndex` starts at 1 (-1: none); `FindState::from_webview2` converts it (tested), so the bar reads 1/2, then 2/2.
2026-10-01 03:47:23 +07:00
CRT
005a953162 Host: the store's commands (install links, dropped files, the signed Store, the install screen) are served for every app that sets AppConfig::with_store; commands::STORE, testkit::assert_shell_only_group and assert_store_guards 2026-10-01 03:40:37 +07:00
CRT
d67d55886d Startup cover never outlasts a page that does not say it painted; menu clicks ask nothing native; mode cards show their picture
Release blocker (macOS 1.0.0: both panes stayed on the shell's loading shape). After `shell_ready` the startup cover waited, with no limit, for every visible site tab to report that it painted. A page whose report never comes (its paint check is frame-driven and gives up after 10 s, and an engine may hold a hidden window's frames back) kept the shell raised over the panes, showing the skeleton, for good. The wait is now bounded (8 s after the window is shown); the page then shows as it is.

The app menu asked for the keyboard natively (`shell_focus`) on every open. It now does so only when the shell does not have it (keys pressed in a tab); a click on the shell already gave it the keyboard.

The mode cards' default picture was shrunk to 16px by the shell's base glyph rule ("a few dots"); it keeps its size.
2026-10-01 03:40:02 +07:00
CRT
95613c9f03 Plugin API: a style injected at document start waits for the document root
WebView2 runs document-start scripts before the document has an element, so
injectCSS threw ("reading 'appendChild'") for every CSS feature that was on at
start, e.g. hiding promoted posts, until the next flags delivery restarted it.
Such styles now wait (one MutationObserver for all of them), are attached as
soon as the document has a root, and get their CSP check then; a style cleared
while waiting is never attached.
2026-10-01 03:39:16 +07:00
CRT
ba684b477b Modes: a mode that no longer matches the features reads "Custom (based on <mode>)" and offers "Re-apply <mode>"
A mode is a starting point, never a lock and never applied again on its own. modeStatus() (runtime)
compares the flags with the mode's preset: custom, what re-applying turns on and off, and which of
those the preset gained in an update since it was applied. mode_set keeps the preset of that moment
(modes.json "preset", flag names only, bounded; forgotten when the mode changes). Settings, the
first-run screen and the app menu's Mode row show Custom; Re-apply lists what it turns on and off
before it writes anything; a preset that grew is mentioned once on the mode row (Review), never applied.
Tests: exact match, changed by the owner, preset grew, choice made before the preset was kept.
2026-10-01 03:33:41 +07:00
CRT
c18f4cf283 String packs: a page gets only the slice of the inventory its packs' keys need (sliceInventory, isPageInventory); a slice does not call other keys new 2026-10-01 03:30:21 +07:00
CRT
c94cede85a Find in a deck column; the app menu no longer closes on a passing blur
Ctrl+F pressed in a deck column (or in the shell while a deck is in front, for its focused column) opens that column's own find bar, laid over the column's header; F3 and Shift+F3 step through it. Same bar, same `tab_find`.

The app menu closed on any window blur. Taking the keyboard from a focused tab (Alt+F / F10 forwarded by the tab, then `shell_focus`) can pass through a blur on its way to the shell, which closed the menu a few milliseconds after it opened. It now closes on blur only if the focus is still away a moment later.
2026-09-30 23:35:58 +07:00
CRT
581e77b22d release macos|linux: --commit <ref> builds an exact commit (its submodule pointers included), the artifact version comes from that commit 2026-09-30 23:33:11 +07:00
CRT
cffbe8946f Remote builds: the updater tarball carries no account name in its entry headers (owner 0/root); the scan caught it 2026-09-30 23:31:26 +07:00
CRT
124ebb3a73 Theme runtime: a static sheet for what does not depend on the site's stylesheet (components, variants, decoration, icons); refresh()
The site adapter's staticCss(choice, mode) is written to its own <style> (wb-theme-static), rebuilt
only when the theme or the site's mode changes or on refresh(); the palette pass never reads it back
as the site's rules, and the first-frame snapshot carries it.
2026-09-30 23:30:34 +07:00
CRT
3d9189077a waybetter manifests: winget, Chocolatey, Homebrew cask and AUR files from one JSON description
Generic (publisher, id, name, version, installer URLs and sha256 or local files, silent switches, licence, homepage). winget: version, installer and locale manifests (validated with the installed winget), Chocolatey: nuspec + install script with checksum (choco pack), Homebrew: a cask for our own tap, AUR: PKGBUILD and .SRCINFO from the .deb or AppImage. waybetter release windows|macos|linux writes them to <outDir>/distribution/<version> once the public download URL is given (--download-base); nothing is submitted.
2026-09-30 23:28:44 +07:00
CRT
c92e99b4bd Remote builds: unpack with the current time (cargo must not reuse stale objects from a cached target), export blobs as committed, build at low priority
git archive stamps files with the commit time, older than the cached build in the shared target dir: a changed source looked unchanged and the build reused stale objects (seen as a missing module after a pointer bump).
2026-09-30 23:26:19 +07:00
CRT
d6300aee3a Report a problem: a diagnostic summary with no personal data, shown as it would be shared, with Copy and the issue page
The app menu's "Report a problem…" opens a small dialog with the summary (reportModel.ts, English on purpose: the people who read reports do): the app and framework versions, the system and webview (coarse, from the user agent), the language and how it was chosen, the mode, the theme and icon pack ids, the file names of the plugins that are on, what paused itself (the panel's `reportDetails`), and safe mode. The model has no field for an account, a handle, an address, a tab or a path, and a value that is not made of id characters is left out rather than trimmed into shape, so nothing of that kind can ride along (tested with hostile input). The dialog sends nothing: Copy puts the text on the clipboard, and "Open the issue page" opens `ShellSite.menu.report.issueUrl` bare in the system browser (the summary is never put in a URL). Without an issue page the button is off and the dialog says so.
2026-09-30 23:25:13 +07:00
CRT
22a4665f18 waybetter release: builds of the committed state on a Mac over SSH and in WSL
release macos --remote and release linux --wsl export HEAD plus each submodule at its recorded commit (git archive, never the working tree, nothing fetched from origin), build it path-free through build-env with unsigned bundles, bring the artifacts back and scan them here. release linux --appimage/--deb signs the AppImage (the Linux update payload) on the release machine and adds linux-x86_64 to latest.json. The remote script is low priority, never installs, quits or replaces an app on the Mac. Config: prepareCommand, exportExclude.
2026-09-30 23:24:04 +07:00
CRT
5f910e95c1 Windows installer: an install made under the app's old name is older than any setup of the renamed app, whatever its version number (upgrade in place, data kept)
The renamed app may start its numbering again. Told apart by DisplayName in the uninstall key
(the old app wrote its old name there; this template writes the new one). Also exempt from the
silent-downgrade abort. Compiled with and without a legacy name and run on the test variant.
2026-09-30 23:23:43 +07:00
CRT
582c3d9460 Theme engine v2, the document side: component tokens, layout variants, decoration, icon packs
A theme may now say how each piece of a site is built, not only one radius and one density:
- components (themeComponents.ts): 36 generic parts with clamped tokens (radius, border incl.
  98.css-style bevels, fill by color role / glass / gradient, padding, sizes, type, shadow, shape,
  hover/active/focus), presets and parents; compiled over a site's part map as soft values
  (--wb-part-*) and rigid rules; text on a chosen fill keeps its contrast.
- variants (themeVariants.ts): fixed words on six axes; the owner holds an axis or switches layout
  changes off; the look of a variant is the site's skin.
- decoration (themeDecor.ts): dividers, chrome (title bar, masthead, prompt), a focus ring that is
  always visible, selection, scrollbars, links, motion under reduced-motion.
- icon packs (themeIcons.ts): a standalone document of glyphs by semantic name (path data, colored
  layers or a small PNG, never an SVG document), with license, source and credits; a theme only
  names a pack; the owner's icon choice is separate; drawn as CSS masks of the current color over
  the site's own svg boxes, behind forced-colors and mask-support guards, per group.
- a theme document gains the optional groups components, variants, decor, icons and
  colors.actions; a theme without them is sanitized to exactly what it was.
- theme.schema.json and the new icons.schema.json are generated from the same tables
  (scripts/theme-schema.mjs; a test fails when they are stale).
2026-09-30 23:22:36 +07:00
CRT
507cd95d8e READMEs for strangers: overview, three layers, a real minimal adapter, security model; the long reference moves to docs/ARCHITECTURE.md 2026-09-30 23:20:58 +07:00
CRT
8dd570147e Safe mode: one run with only the shell working (the app menu, --safe-mode, Shift at launch on Windows)
Host: `safe.rs` is a flag in this process's memory, never written to disk, so the next normal start is back to the owner's setup. While it is on a site tab gets the tab bridge and nothing of the app's page layers: its document-start scripts are `SiteHooks::safe_init_scripts` (default: the bridge alone) instead of `init_scripts`; `site_page_loaded`, `tab_placed` and `tab_background` are not called; `page_scripts` is empty; `eval_in_site_tabs` does nothing. So no page runtime, plugins, theme, custom CSS, feature flags, fixes, hotfix data or words reach a page: the site runs as in a browser, and only the shell is the app's. It starts from the launch argument `--safe-mode` (for when the interface itself is broken), from Shift held while the app starts (Windows), or from the shell: `safe_mode_get` / `safe_mode_set {on}` (shell-only), where a switch rebuilds every tab webview (profiles marked stale, `tabs-rebuild` asks the shell for its layout) so document-start scripts change too. Tests: the argument, the flag, and that app.rs and host.rs gate every layer.

Frame: the app menu's "Restart in safe mode…" says what it does before anything reloads; while on, the titlebar shows an amber "Safe mode | Leave" chip (one click leaves) and the row reads "Leave safe mode"; the chrome drops its theme (and pane themes) and takes it back on leaving, without touching the first-frame cache. `features.safeMode` (on by default).
2026-09-30 23:13:03 +07:00
CRT
b55c802097 Words: the deck is "Deck" (it was "Power User Mode", which collided with the Power User mode); the three feature-health words in all 48 translated languages (machine)
Keys and saved data unchanged. Each language reuses its own word for a deck; only "Deck settings"
is newly translated.
2026-09-30 23:05:23 +07:00
CRT
33c693ed93 Theme color model: a foreground the site paints dimmer than its secondary text becomes the theme's secondary text
A site may color an icon or text with one of its border grays (X's composer toolbar: 1.7:1 on its
own black). Placed by axis position it landed between a theme's background and its secondary
text, under every contrast a theme guarantees. In text usage that stretch of the axis (above the
most raised surface, below secondary text) now maps to the theme's secondary text; surface-dark
foregrounds (labels on a filled surface), borders and backgrounds are unchanged.
2026-09-30 22:58:48 +07:00
CRT
615981c7c9 App menu: "Check for updates" is a real check, once, now, and says what came of it calmly
Host: `updater::check_now` runs the same check the timer runs (the same updater endpoint, nothing else) for the app's shell-only `update_check`. It never queues or retries: while a check is in flight (the timer's or an earlier request's), or within 10 seconds of the last request, it only answers with the current status (`update::may_check_now`, tested). The timer's loop uses the same single-flight guard.

Frame: the menu's Updates row is that check (`updateRow`, tested): "Checking…", "Up to date · <version>", "Downloading <version>…", "Restart to update" when an update is ready (the row then installs it), and "Could not check" with a plain sentence as its tooltip when the server does not answer. The updater's error text is never shown there, and a failure is only mentioned when the owner asked for the check in this menu; the timer's own failures stay quiet.
2026-09-30 22:54:51 +07:00
CRT
9209280959 String packs, phase 1: data-only packs that replace the wrapped site's own UI strings. Pack and inventory formats, validator (only inventory keys, parameter names, plural and select kinds, no markup), ordered idempotent engine that keeps what a restore needs, coverage and drift report; the per-site catalog hook adapter with a self-check (ok, hook not matched, no catalog seen), a fail-open webpack chunk array hook, a small DOM text net; waybetter strings check 2026-09-30 22:38:07 +07:00
CRT
8324bc94af Find in page: the shell's find bar per pane (Ctrl+F, the app menu, the palette) over the engine's own find
Host: `tab_find {id, action, query}` (shell-only; start, next, previous, stop). On Windows it drives WebView2's own find (`ICoreWebView2Find` from `ICoreWebView2_28`, find options from `ICoreWebView2Environment15`; its built-in find bar suppressed, all matches highlighted) and reports the engine's match count and active match to the shell as `tab-find` events. On macOS, Linux and an older WebView2 Runtime native code evaluates the engine's `window.find` in the tab instead (next and previous work, no count); WKWebView's and WebKitGTK's native find are not wired here because this tree cannot be compiled or run for them on this machine. The query is one typed line (1..=256 characters, no control characters), goes to the engine and is kept nowhere. Tabs gain no command: a tab forwards the existing key name `find` (Ctrl+F) through tab_shortcut.

Frame: FindBar.tsx takes the address field's place in its pane's toolbar while open, so the page does not move and in split view the bar is the searched pane's. "3/17" where the engine counts, "No matches" for none; Enter / F3 next, Shift+Enter / Shift+F3 previous, Esc closes, clears the highlights and gives the page the keyboard. `features.find` (on by default) gates the menu row, the palette entry and the keys. Only what the page has rendered can be found, as in a browser.

Also: this makes `pub mod find` in lib.rs (already in HEAD) resolve again.
2026-09-30 22:34:48 +07:00
CRT
24c8620d4e README: Dev loop section 2026-09-30 22:32:38 +07:00
CRT
9af40411dc devloop guard test: skip its own module 2026-09-30 22:21:41 +07:00
CRT
04a405c1ba Dev loop: debug-only page scripts from disk + tab rebuild on change (waybetter-host devloop, page_script!), generic devmode watcher, waybetter dev, buildRuntime watch, template scripts 2026-09-30 22:20:22 +07:00
CRT
2fd866d8fb App menu: the titlebar logo opens it too, hung under the logo on its side of the window
The logo at the start of the titlebar is now a button (aria-haspopup, keyboard reachable, tooltip) and a second trigger of the same menu: same component and model, only the anchor differs (`openAppMenu({ anchor: 'logo' })`). A submenu opens toward the middle of the window, else on the other side, else over the menu (`submenuLeft`, tested). A click on the logo never starts a window drag; a press that moves more than 5px does (the window's `startDragging`), so the logo still moves the window like the titlebar around it. A right-click shows the window's system menu. Clear of the macOS traffic lights: the logo sits after the titlebar's 78px inset and the menu hangs below it. The button at the end and the account menu's row stay.
2026-09-30 22:15:09 +07:00
CRT
8dd5eee99b Feature health on the area cards: a feature that paused itself is named by its area's "health" palette entry, and the panel reads it for the row and the card (no new report to the host)
The entry opens the notes in the page; its keywords carry the paused flags of that area. English
for the three new words; the other catalogs follow with the words step.
2026-09-30 22:00:04 +07:00
CRT
715f6f2fec App menu: the titlebar's "Settings and more" (Alt+F, F10, the palette, the account menu's last row) with rows from the site config; page zoom per tab in the host
Frame: AppMenu.tsx is a menu at the end of the titlebar cluster (before the caption buttons; macOS has none there). Its rows are `ShellSite.menu`: ordered groups of built-in rows by name and the app's own ({id, label, icon, shortcut, hint, when, checked, run | submenu}); `DEFAULT_MENU`, `menuWith` and `menuWithout` order, add and remove. A built-in row shows only when the app serves it (decks, `menu.languages`, themes, `menu.modes`, and `features`: zoom, store, workspaces, developer, updates), so no app gets a dead row. Language, Theme, Mode, Workspaces and Developer are submenus; Language and Theme apply live and keep the menu open, Mode asks the same keep-my-changes question as Settings (panel `menuModes()`). Keyboard: arrows, Home/End, type-ahead, the arrow along the reading direction opens a submenu, Esc closes one level; ARIA menu roles; submenus stay inside the window; one highlight for pointer and keys. It is an overlay like the other shell menus (the shell is raised, panes stay visible, nothing is dimmed).

Words: menuMessages.en.json (`wb.menu.*`), read through the framework i18n with that English as the fallback until the catalogs have them; words the panel already has are reused.

Host: `tab_zoom {id, factor}` (shell-only) sets a tab's page zoom with the engine's own zoom, clamped to 25%..500% and remembered per tab id in app data `zoom.json` (a number per tab id, nothing else; a closed tab's entry goes); without a factor it reads. A tab may forward four more shortcut names: `app-menu`, `zoom-in`, `zoom-out`, `zoom-reset`. The titlebar shows a zoom chip only while the pane in front is not at 100%.

create-waybetter: `tab_zoom` in the shell capability list; the template says which panel sections it lacks (store, updates) and how to add menu rows.
2026-09-30 21:52:51 +07:00
CRT
061f9c4725 Route listeners: one removed by an earlier listener in the same round is not called (a feature stopped by the route re-added its stylesheet); the route poll never keeps a test process alive 2026-09-30 21:19:02 +07:00
CRT
041ee3b4f6 Panel: a long section name wraps left-aligned in the side rail instead of centring 2026-09-30 21:19:00 +07:00
CRT
e66875515d Theme engine: a rule is read as it takes effect (a Tailwind opacity utility's nested @supports value, not its solid fallback); unsupported @supports groups are left out; reach() is exported for site adapters 2026-09-30 21:04:12 +07:00
CRT
b7575681b2 Feature modules: a feature ported from a plugin keeps that plugin's words (words), so nothing is retranslated; the keep-my-changes question takes its own line 2026-09-30 19:53:11 +07:00
CRT
d633de09d2 Locales: the area cards, the Fixes list, modes and the feature settings words in all 48 translated languages (machine) 2026-09-30 19:37:56 +07:00
CRT
513200b707 Feature modules in a new app, i18n rename, and what the first area port needed
- create-waybetter: the generated app has a feature table (one area, one feature, three modes), a feature module, the registry in its runtime, the cards and the first-run screen in its panel
- waybetter i18n rename --map <file> [--copy]: keys move or are copied in every catalog with each language's own value
- feature context: win (no globals in a module), text() for a feature's words (t('...') is read as a whole catalog key by the i18n tools)
- probes: `when` (only counts while the part of the page it lives in is there)
- cards: a caret, an empty area says its features come later; the first-run screen takes the whole panel
2026-09-30 19:25:15 +07:00
CRT
c7c25fa8bc Panel: area plugin cards with per-feature toggles, the Fixes list, modes (first-run screen and Settings)
- PanelSite.features: the app's feature table; AreaPlugins draws one card per area (switch, count, grouped features, Settings button that runs the area's command in the site tab) and the core flags as Fixes
- a feature still backed by an old plugin file is switched through plugin_set_enabled from its card and left out of the JavaScript plugins list
- Modes: Onboarding (three cards, recommended preselected; an existing setup may be kept) and ModeSettings with the keep-my-changes question; openOnboardingWhenDue for ShellSite.start
- flags are saved sparsely (only what differs from the table's defaults)
- PanelSite.appearance.extra; palette commands of an area are listed under its name
- the kit's words for feature settings and health notes; English in the framework catalog
2026-09-30 18:34:14 +07:00
CRT
e38314f65c Host: 256 feature flags, the onboarding mode (mode_get/mode_set, shell-only), and the one-time move of replaced plugin files to feature flags
- settings: MAX_FEATURES 64 -> 256 (an older app's whole-map backup still validates)
- modes: ModeState {mode, onboarded, existing} in modes.json; existing is decided natively on the first start
- plugins::migrate: PluginHost.superseded (file -> flag); enabled and as shipped -> flag on and the file goes; edited -> kept, flag left off; marker written last, every step repeatable; map_backup for imported enabled lists
- a superseded file is never written again by install_bundled
- create-waybetter: the host command list and PluginHost.superseded
2026-09-30 18:34:13 +07:00
CRT
43fa215f3b Feature modules: a feature table (areas, flags, modes), a registry that runs a module only on its surfaces, one shared observer for every feature and plugin, settings from a schema, selector health with self-pause
- featureTable.ts: areas, per-feature flags, core flags, modes as presets; sparse storage, ownership (an area that is off turns off what it owns), mode switching that keeps or drops manual changes, table validation
- featureModules.ts: createFeatureRegistry (flag + surface gating, scoped context, palette commands under the area with a budget of 5, settings dialog, probes, hotfix selector overrides and kill flags)
- settingsForm.ts: a settings schema normalizes stored values and draws the controls from the UI kit
- domHub observeSelector; api.observe goes through the shared hub (a root of the caller's own keeps its observer)
- createFeatureFlags: a delivery is the whole state (a flag it does not name is at its default), resolve() for ownership, current()
2026-09-30 18:11:42 +07:00
CRT
7cc82a9f2a build-env: MSVC C and C++ lose the build paths too (/d1trimfile in CL, /PDBALTPATH in LINK)
A library a build script compiles with MSVC (CMake, the cc crate) kept its
absolute source paths through __FILE__: -ffile-prefix-map exists only on gcc
and clang. cl.exe and link.exe read CL and LINK themselves, so the roots are
trimmed whatever runs the compiler. Longest root first: the first match wins.
2026-09-30 17:48:27 +07:00
CRT
308d78920a Comments: X's own panel sections are Tweaks and Drafts (My Space removed for now)
wayBetter for X took My Space (profile customization) out; the framework only named it in
comments as an example of an app's own section and runtime member.
2026-09-30 16:52:50 +07:00
CRT
6acc84a8e3 waybetter 0.6.0: path-free native builds (build-env) and the privacy-scan release gate
A Rust binary embeds the absolute paths of its sources, and on a developer's machine those hold
the account name. waybetter build-env runs a build with the home folder, the workspace, CARGO_HOME,
RUSTUP_HOME and CARGO_TARGET_DIR remapped to /home, /src, /cargo, /rustup and /target (flags already
set are kept), a path-free PDB reference on MSVC and the same mapping for C sources on gcc and clang.

waybetter privacy-scan searches files, binaries (UTF-8, UTF-16), archives (zip, tar, gzip, zstd,
.deb, DMG; NSIS, AppImage and xz through 7-Zip or the matching tool), git-tracked files and a whole
history for the terms of a file kept outside every repository plus the machine's account name. Hits
are printed masked; a container that cannot be opened is a failure.

waybetter release windows builds with build-env; both release commands refuse an artifact with a hit.
The scaffolded app gets desktop:build and privacy:scan scripts.
2026-09-30 15:58:25 +07:00
CRT
a8dd727612 i18n: supported and community language tiers
Automatic language choices resolve only to supported languages; community
translations are listed apart in Settings and offered as a one-click switch.
waybetter i18n check reads `supported` and generates `tier` into languages.json.
2026-09-30 15:48:11 +07:00
CRT
eb496a795b locales: light review of the 11 most-used languages (terminology, wording; 272 strings) 2026-09-30 14:59:59 +07:00
CRT
f99b8e6755 waybetter 0.5.0: docs for the host's page commands, the runtime composition, the i18n command and dev-tools 2026-09-30 14:48:06 +07:00
CRT
e1aa2631d9 dev-tools: the generic development harnesses (perf probes, close check, tab and webview stress)
Moved from wayBetter for X; an app passes its own origin, process name, scripts and fixtures.
2026-09-30 14:48:06 +07:00
CRT
d8a94a2285 waybetter i18n check|extract: an app's catalogs against the framework's rules
Configured by waybetter.i18n.json in the app's root; --write regenerates languages.json and the native localization files (macOS, Linux).
2026-09-30 14:48:05 +07:00
CRT
c570f308e9 locales: the Windows installer's words (wb.installer.*) and its language files, generated from the catalogs
Moved from wayBetter for X: 27 messages in 51 languages, and windows/languages/<Language>.nsh for the 17 NSIS languages Tauri does not ship (scripts/installer-languages.mjs writes and checks them).
2026-09-30 14:48:04 +07:00