Skip to content

i18n (FR/EN 100%) + GPU/RAM telemetry, NGL layer hint, per-turn model tag - #59

Closed
Worlgun wants to merge 21 commits into
nathaninline:mainfrom
Worlgun:i18n-and-gpu-extras
Closed

i18n (FR/EN 100%) + GPU/RAM telemetry, NGL layer hint, per-turn model tag#59
Worlgun wants to merge 21 commits into
nathaninline:mainfrom
Worlgun:i18n-and-gpu-extras

Conversation

@Worlgun

@Worlgun Worlgun commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Two independent features, bundled together because they were built in the same session:

  1. Full i18n system for the web UI — French (100%) and English (100%) fully translated, live language switching with no page reload, and the infrastructure to add any new language easily. Italian, Spanish, Russian, and German are seeded (base UI chrome translated) and ready for full translation as follow-up contributions.
  2. Extra Windows GPU telemetry — power draw, fan speed, and Intel integrated GPU utilization %, surfaced in the existing Device panel.

Based on v0.13.3. Built and manually tested end-to-end on Windows (AMD dGPU + Intel iGPU) before opening this PR — see Testing below.

How the i18n system works

Everything lives in two new files plus a handful of data-i18n* attributes sprinkled into the existing template — no build tooling, no npm, no external translation service, consistent with the rest of this project.

  • internal/ajean/ui/src/js/00a-i18n-data.js — one JS object, one block per language, same keys in every block:

    var I18N = {
      fr: { "appearance.dark_mode": "mode sombre", ... },
      en: { "appearance.dark_mode": "dark mode", ... },
      ...
    };

    French is the reference/source of truth (it's the language the app was originally written in).

  • internal/ajean/ui/src/js/00b-i18n.js — the runtime: t(key) looks up the active language, falling back to French if a key is missing (so a partially-translated language degrades gracefully instead of breaking); applyI18n() walks every data-i18n/data-i18n-html/data-i18n-attr element in the DOM and fills it in; setLang(lang) persists the choice (localStorage + /api/prefs) and re-renders — both the static DOM (applyI18n) and every dynamically-built panel (loadAll() — Device/Config/Tasks/Agent/Internet/MCP/Network/API key panels all construct their own text at render time via t(), so a language switch needs to force them to re-render too, not just walk static attributes).

  • Static HTML uses data-i18n="key" (textContent), data-i18n-html="key" (innerHTML, for strings with <b>/<code> etc.), or data-i18n-attr="attrname" alongside data-i18n (for a title/placeholder/aria-label). JS-rendered content just calls t('key') directly wherever it used to have a hardcoded French string.

  • tools/verify-i18n (go run ./tools/verify-i18n) checks every language block against the French reference: reports completion %, lists missing keys, catches typo'd key names, and — new in this PR — flags any value that's byte-identical to French and isn't a deliberate loanword (see below). Non-Go contributors can still translate by hand; whoever merges can run this before/after.

  • docs/TRANSLATING.md is the contributor-facing guide: adding a new language, completing a partial one, the _prefix/_suffix pattern for sentences with a variable stitched into the middle, and how to build/preview.

Adding a new language (no Go/JS knowledge required)

  1. Copy the whole fr: { ... } block in 00a-i18n-data.js, paste it, rename fr: to the new ISO 639-1 code.
  2. Translate every value. Never touch a key — a renamed key just silently falls back to French, easy to miss.
  3. Add one line to LANG_NAMES in 00b-i18n.js — the Settings dropdown builds itself from that object, nothing else to wire up.
  4. go run ./tools/verify-i18n to check completeness before sending it in.

Full details and the sentence-splitting pattern are in docs/TRANSLATING.md.

Continuing IT/ES/RU/DE

These four currently have only the ~12 keys used by the Appearance panel itself (so the language selector works and doesn't leave you stuck once you pick one) — the rest silently falls back to French via t(). Completing them is just filling in the remaining values in each block and re-running the verifier; no code changes needed.

A bug worth knowing about (now fixed + guarded against)

The original French extraction copied some already-English UI text — a few section titles the original French-primary app happened to label in English (Device, Tasks, Settings, Link, Engine, Memory, MCP servers) — straight into the French slot verbatim instead of translating it. It shipped invisibly because the completeness checker only diffs key presence, not value content. Found by hand after a report that some sidebar sections weren't switching language; fixed (13 keys total), and verify-i18n now also flags any non-French value identical to the French reference, against a small hand-vetted allowlist of genuine loanwords this app already uses on purpose (preset, backend, session, API, …). Verified the detector actually fires (temporarily broke a value, confirmed it was caught, reverted) before relying on it.

GPU telemetry additions

  • AMD GPU power draw (W) and fan speed (%) via atiadlxx.dll (ADL), alongside the existing VRAM/temperature reading.
  • Intel integrated GPU utilization % (previously VRAM-only).
  • Two crashes surfaced during stress-testing and fixed before landing: a syscall.NewCallback leak from creating a fresh callback on every poll (Go caps callbacks per process — fixed with a package-level sync.OnceValue), and atiadlxx.dll not being thread-safe under concurrent requests (fixed by serializing all ADL calls behind a sync.Mutex). Both re-verified with concurrent load after the fixes.

Three more additions (same fork/session, added to this PR after it was opened)

Bundled in here rather than as separate PRs since they're small, sit right next to the GPU/RAM telemetry above, and came out of the same day of dogfooding this build.

Model layer count next to the NGL setting (#43) — the NGL slider had no way to know how many layers a given .gguf actually has, so calibrating "offload everything" vs. a partial split was guesswork. Added a small from-scratch GGUF header/metadata reader (backend_gguf.go — reads only the header + KV metadata, never touches the multi-GB tensor data) that pulls <arch>.block_count and surfaces it as a hint next to the NGL row. Validated against a real 27B model file (block_count=65, matches llama.cpp's own reporting).

Track which preset answered each message (#45) — with several presets/models in rotation, there was no record of which one actually produced a given reply once you'd switched presets since. activePresetName() reads the currently-active preset at the moment a turn starts and tags it onto the turn (chat_conversation.go); the UI shows it as the first element of the per-message stats line (model · elapsed · tokens · rate), and it's carried through export/history like the rest of a turn's metadata.

Per-process RAM usage — the existing memory panel showed system-wide used/total, but never how much of it was ajean itself vs. the loaded model's process vs. anything else running on the machine — no way to tell if a saturated machine was due to the model or something unrelated (the user's own idea, from watching the panel during a long session). web_procmem_windows.go reads ajean's own working set directly via GetProcessMemoryInfo (psapi.dll), and finds llama-server's by name via PowerShell (same pattern as the existing windowsAdapterStats in web_devices.go, since its PID is never handed back to the web-server process). Windows-only for now, matching this project's existing GPU-telemetry precedent (web_procmem_other.go is a no-op stub elsewhere). handleRam adds ajean_used/llama_used (MiB) to the JSON only when available, so other platforms show nothing extra rather than a misleading "0 Go".

Testing

  • go build ./... and go vet ./... clean.
  • go run ./tools/verify-i18n: fr 1078/1078, en 1078/1078, it/es/ru/de at their seed baseline (expected).
  • Manual testing in-browser: live FR↔EN switching across every panel (Device, Config/Engine, Presets, Agent, Tasks, Appearance, API, Link, Engine install, Settings, Trackers, Projects, MCP, chat itself) with no page reload; verified no leftover untranslated text via a systematic template + JS audit (see commit history) in addition to hands-on testing.
  • GPU telemetry stress-tested under concurrent load (300+ concurrent requests, 550+ sequential) after the ADL fixes, on an AMD RX 7900 XTX + Intel UHD 770 system.
  • All of the above tested and confirmed working by the repo's actual daily user before this PR was opened.
  • The three later additions: go build ./.../go vet ./... clean, full go test ./... green; deployed and used live for real work afterward (RAM panel and layer hint visible/correct in the running app; per-message model tag confirmed across a preset switch).

🤖 Generated with Claude Code

Worlgun and others added 16 commits September 2, 2026 20:11
handleVram only ever checked nvidia-smi -> amd-smi -> rocm-smi to fill the
"MACHINE" panel's GPU/VRAM card. None of those three exist on a stock
Windows install with AMD or Intel graphics (no ROCm), so the panel always
showed "(pas de GPU)" even while inference was correctly running on the
GPU via the Vulkan backend.

Adds a Windows-only fallback, vulkanVramGPUs:
- GPU identity + total VRAM come from the configured engine's own
  `--list-devices` output (already parsed by parseListDevices for the
  model editor's device picker), so no external tool is needed and the
  list matches exactly what llama.cpp will actually use.
- Live "used" VRAM is corrected via the Windows "GPU Adapter Memory"
  performance counter (system-wide, not per-process), since --list-devices
  probes free memory from a freshly spawned process and under-reports
  usage while another process (the real server) already holds VRAM.
  Discrete vs. integrated adapters are matched by checking for "intel" in
  the Vulkan device name, since integrated GPUs report their usage under
  Shared (not Dedicated) memory on Windows -- an architectural fact, not a
  coincidence of any particular machine.

Tested on Windows 11 with an AMD Radeon RX 7900 XTX (discrete) + Intel UHD
Graphics 770 (integrated), Vulkan backend. Both cards now show correct
name/total/used in the MACHINE panel; used-VRAM tracks the real value
(verified against Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory
directly) both idle and with a 27B model loaded.
Follow-up to the previous commit (Windows VRAM detection): the MACHINE
panel still showed util=0 and temp=0 for every GPU on Windows, since
neither number is available through nvidia-smi/amd-smi/rocm-smi (absent)
nor through --list-devices (Vulkan doesn't expose either).

Utilization + accurate "used" (windowsAdapterStats, web_devices.go):
Replaces the previous VRAM-only helper with one that reads BOTH the
Windows "GPU Adapter Memory" and "GPU Engine" performance counters in a
single PowerShell call, so the two numbers stay correctly paired per
adapter. "util" is the max utilization across all engine types (3D,
Compute, Copy...) for that adapter's LUID, matching what Task Manager
shows. Adapter-to-device matching is still done by architecture, not
guesswork: integrated GPUs have no VRAM of their own, so Windows tracks
their memory under Shared (not Dedicated) Usage -- Dedicated Usage stays
~0 for them permanently, which is what separates "the discrete card" from
"the iGPU" in the matching logic, regardless of machine or moment queried.

Temperature (web_devices_adl_windows.go / _adl_other.go): Windows exposes
GPU temperature through no WMI/perfmon counter at all -- Task Manager
itself only gets it via a private DirectX telemetry API added for it in
Windows 11 22H2, not reachable from a script. AMD's own answer to that is
ADL (AMD Display Library, atiadlxx.dll, already shipped by every AMD
driver): the same API GPU-Z/HWiNFO use. Loads it directly via syscall
(no cgo), calls ADL2_Adapter_PMLog_Start + ADL2_New_QueryPMLogData_Get,
and reads the hotspot/junction sensor (falls back to edge if hotspot
isn't supported on a given card). Struct layouts were taken from AMD's
public SDK (github.com/GPUOpen-LibrariesAndSDKs/display-library,
adl_structures.h/adl_defines.h) rather than guessed -- these calls
silently misbehave if any struct size is even slightly wrong, so two
earlier from-memory attempts (int32 then int16 sensor pairs) were
discarded once tested against a debug harness and found to return
garbage, before pulling the real header settled it.

Split into windows/other build-tagged files (not runtime.GOOS-gated
inline like the rest of this fallback chain) because loading a Windows
DLL via syscall doesn't compile on non-Windows GOOS at all -- the _other.go
stub keeps the call site in web_devices.go unconditional. No-op on
Linux/macOS, which already get real temperature via amd-smi/rocm-smi.

Known limitations, both noted in code:
- Reports only the first AMD adapter ADL finds. ADL enumerates one entry
  per display OUTPUT, not per physical card, so a machine with two real
  AMD GPUs would need dedup by bus/device/function to tell them apart --
  not implemented, since it's not a configuration I could test against.
- Hotspot (junction) temperature runs noticeably hotter than the "Edge"
  sensor Task Manager itself displays -- a real ~10C gap observed in
  testing, not a bug in either reading. Hotspot is what actually drives
  thermal throttling and is what enthusiast tools (HWiNFO, GPU-Z) treat
  as the primary number, which is why it's preferred here, but it will
  read higher than Task Manager's figure for the same card at the same
  moment.

Tested on Windows 11, AMD Radeon RX 7900 XTX (discrete, ADL path) + Intel
UHD Graphics 770 (integrated, correctly stays at util=0/temp=0 -- no
regression, Task Manager doesn't show Intel temperature either). Verified
util% tracks real load (0% idle, 96% during active generation) and
hotspot temperature matches AMD's own sensor within the same run.
…terStats

$r = foreach (...) { ... } in PowerShell unwraps to a bare scalar object
instead of a one-element array when the loop produces exactly one result
-- a well-known PowerShell footgun. On a machine where only one adapter
passes the >5MB filter (e.g. right after a fresh boot, before anything
else has touched the GPU), that meant $r stopped being an array right
before ConvertTo-Json ever saw it, breaking Go's json.Unmarshal into
[]struct{...} silently (the error is swallowed, windowsAdapterStats just
returns nil). Every "real" number layered on top of the base
--list-devices reading -- used, util, and by extension the AMD ADL
temperature call, which never even runs once this returns empty -- was
quietly falling back to the earlier, much less accurate reading instead.

Fix: wrap the loop body in @(...) to force array output regardless of
how many adapters matched. Verified by hand against a live single-adapter
machine before and after: {"items":{"dedicated":...}} (object, breaks
unmarshal) vs {"items":[{"dedicated":...}]} (array, correct) for the
identical underlying data.
…ency fix

The temperature reading added in c27eb46 crashed the whole ajean-ui
process (not just the engine -- same process) twice in real use on
Windows, both times as a Windows access violation (0xc0000005) inside
the ADL DLL call:

1. adlAMDHotspotTempC called syscall.NewCallback fresh on every
   invocation to build ADL's malloc callback. Go callbacks handed out
   this way are never freed for the life of the process and the runtime
   caps how many can exist (~2000) -- with /api/vram polled periodically
   by the UI, that budget ran out after about 108 minutes of uptime and
   the next call corrupted an unrelated callback slot. Fixed by hoisting
   the callback to a single package-level sync.OnceValue, computed once
   and reused for every call -- this part is a real, verified fix (kept
   in this commit for when temperature comes back).

2. After fixing (1), a deliberate concurrent-load test (50-80 parallel
   requests against /api/vram, the realistic worst case for a UI that
   polls this endpoint) crashed the process again, faster than before.
   This points at atiadlxx.dll itself not being safe to call from
   multiple goroutines at once -- ajean's HTTP server handles each
   request on its own goroutine, so two requests landing at the same
   moment both entering the DLL is a normal, expected occurrence, not an
   edge case. I don't yet have a fix I've verified under load (a mutex
   serializing all ADL calls is the obvious next attempt, untested).

Rather than leave a native-DLL crash in a PR aimed at a shared codebase,
this commit disables the AMD temperature reading -- applyWindowsDedicatedUsage
now hardcodes temp=0/unsupported instead of calling adlAMDHotspotTempC --
while keeping the callback fix and all the ADL plumbing in place for
whoever picks this back up (myself included). VRAM detection and
utilization % are unaffected by either bug and have been stress-tested
under the same concurrent load without issue.

Sorry for the churn on an already-open PR -- I'd rather push the honest
state (including what I broke and how) than leave something crash-prone
in review.
Root-caused the second crash from the previous commit: a deliberate
concurrent-load test (multiple parallel requests against /api/vram, the
realistic case for a UI polling this endpoint) reproduced the access
violation reliably. ajean's HTTP server runs every request on its own
goroutine, so two requests both entering adlAMDHotspotTempC at the same
moment is the normal case, not a rare edge case -- and AMD's ADL
(atiadlxx.dll) predates goroutines by a decade and documents no
thread-safety guarantee for concurrent callers.

Fix: adlCallMu, a single mutex held for the DLL's whole
create->start->query->destroy sequence, guarantees only one goroutine is
ever inside the DLL at a time. The calls are fast (single-digit ms), so
serializing them isn't a meaningful bottleneck even under heavy load.

Re-enables the call this disabled last commit. Verified before pushing:
- 3 waves of 100 concurrent requests against /api/vram (300 total) --
  the exact pattern that crashed it twice before -- no crash, temp/used/
  util all correct throughout
- 550 further sequential requests (endurance/leak check) -- no crash,
  values stable
- ajean-ui process handles/RSS flat across the whole run (1863 -> 1858
  handles after the last 50 calls, RAM unchanged) -- no resource leak
- chat generation unaffected during the concurrent runs (63 tok/s,
  consistent with pre-change numbers)
- UI console clean on a fresh reload; the "no response after 30s"
  warnings seen mid-test were the browser's own poll queuing behind my
  hundreds of concurrent curl calls contending for the same mutex, not a
  crash or hang -- expected, and irrelevant at realistic (single-tab)
  polling rates
…ntel util%

i18n system (00a-i18n-data.js, 00b-i18n.js): the UI had zero internationalization
-- every string hardcoded inline (mostly French, some English from newer
features mixed in inconsistently). This adds a minimal data-i18n
attribute + t() lookup engine, no dependency, consistent with the rest of
this UI (one global script, no modules, no bundler -- see tools/assemble-ui).
Language persists via localStorage (instant, no flash) then syncs through
/api/prefs like the existing theme setting (web_prefs.go: added "lang" to
webPrefsAllowed). A language <select> lives in the Appearance panel next
to dark mode.

This commit converts the Appearance panel end-to-end (all 6 languages) as
a working, verified slice -- switching language re-renders live with no
page reload, confirmed in-browser. The rest of the UI (~600-1200+ strings
across index.tmpl.html and 17 JS files, per a full codebase survey) is
NOT yet converted; that's substantial additional work, tracked
separately, to follow in further commits on this branch. Adding a new
language later: copy one block in I18N, translate the values, add the
code to LANG_NAMES and #lang-select -- three places, nothing else.

GPU sensors (web_devices_adl_windows.go): extends the ADL query added on
the windows-gpu-detection branch to also read board/ASIC/GFX power draw
and fan RPM/percentage in the same PMLog call (no extra DLL round-trip).
Exposed as power_w/fan_rpm/fan_pct in /api/vram. Also assigns real
utilization% to integrated GPUs (Intel) from the same Windows adapter-
engine data already used for the discrete card, previously left at a
placeholder 0.

Verified: rebuilt and stress-tested the ADL changes again with the same
protocol as the crash-fix commits on windows-gpu-detection (3 waves of
100 concurrent requests) before trusting them -- no crash, values sane
(power_w:96, fan_rpm:1568, fan_pct:36 alongside existing temp/used/util).
Extracted every user-facing string in the UI to the data-i18n/t() pattern
established in the previous commit's Appearance-panel slice: the full
index.tmpl.html (354 keys) plus all 17 hand-written JS files (654 keys,
merged from parallel extraction passes -- see below), for 1020 total
distinct keys now in I18N.fr, the source-of-truth dictionary. Verified
programmatically: every t('...') call in the JS and every data-i18n="..."
attribute in the template resolves to a dictionary entry, and every
dictionary entry is referenced by at least one of them -- no orphans
either direction.

Scope boundary, stated plainly: this covers the FRENCH side completely.
The other 5 languages (en/it/es/ru/de) still only have the small
Appearance-panel slice from the previous commit -- t()'s fallback-to-
French means nothing is broken or missing for those languages right now,
they just show French text for anything beyond Appearance until
translated. That's the next commit on this branch.

Process note for whoever reviews this: the extraction was done in 8
parallel passes across independent, non-overlapping files (one per
major UI area: template, status/api, settings, engine/mcp/remote/node/
push, tasks/projects/attach, plus chat-render/stream/models done by hand
given how central they are), each producing its own French key->string
JSON fragment, merged via a small Node script (kept in this branch's
working tree as merge-i18n.js/inject-i18n.js/verify-i18n.js -- not part
of the shipped app, just the tooling used to build this commit; happy to
drop them from the PR if that's preferred, or keep for future language
work). One real key collision was caught and resolved during merge
(`projects.new_project_title` used independently in the template for a
button tooltip and in 18-projects.js for a modal dialog title -- same
name, different UI elements; the latter renamed to
`projects.new_project_modal_title`).

Also caught, independently, in three of the eight passes: a local
variable literally named `t` shadowing the global t() function within
that variable's scope (holding an EXTRA_ARGS token array in 07-models.js,
a task object in 16-tasks.js, an update timer in 05-status.js). None of
these caused an actual bug -- in each case no t() call happened to live
inside the shadowed scope -- but it's a sharp edge worth knowing about:
`t` is a one-character global this codebase now relies on everywhere,
and it collides easily with an unrelated single-letter local. Fixed the
05-status.js and 16-tasks.js ones by renaming the local variable; flagged
07-models.js's (eaToggleFlag/eaGetValued/eaSetValued) as safe-for-now
but worth avoiding `t` as a local name generally going forward.

Verified after regenerating index.html and rebuilding: UI loads, French
renders correctly throughout (spot-checked, not exhaustively), no console
errors on a fresh load, chat generation and the model editor's GPU
device picker (unrelated to this change) both still function normally.
Two gaps that would have made this genuinely hard for a human translator
who isn't me/an AI agent working through it interactively:

1. No documentation beyond a one-line code comment, and that comment was
   already slightly wrong (told you to edit #lang-select in
   index.tmpl.html by hand -- unnecessary since the dropdown populates
   itself from LANG_NAMES). docs/TRANSLATING.md replaces it: how to add
   a new language or complete a partial one, and specifically how to
   handle the split-sentence _prefix/_suffix key pairs (the one part
   that trips up literal word-for-word translation -- translate the
   whole sentence first, then split it at the variable's position, keep
   HTML tags wrapping the equivalent phrase in the target language).

2. No way to check a translation's completeness without reading the
   source and cross-referencing 1020 keys by eye. tools/verify-i18n is a
   small Go program (matching the project's other tools/* -- no Node or
   other runtime needed beyond what building ajean itself already
   requires) that reports, per non-French language block, how many of
   fr's keys are present and lists exactly which are missing or
   mistyped. Run as `go run ./tools/verify-i18n`.

Also fixed the now-stale instruction in 00a-i18n-data.js's header comment
to point at the new guide instead of repeating (incorrectly) the old
by-hand #lang-select step.
Partial conversion of the new upstream 19-tracker.js (trackers feature)
to the t() i18n pattern: loadTrackers, trackerRow, openTrackerMenu,
trackerMove, trackerDelete converted so far. Remaining functions still
have hardcoded French strings. Also includes the scratch extraction/
merge/verify JSON and JS files used during the translation pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Complete the tracker feature translation: newTrackerUI, renderTrackerEvents,
trackerEventRow, trackerClearForm, trackerAddPoint, editTrackerPoint,
deleteTrackerPoint all converted from hardcoded French to t() calls.
Add the corresponding French strings to 00a-i18n-data.js (fr now at
1068 keys) and refresh the i18n-full-fr.json extraction snapshot used
to feed translation agents for the other languages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full English translation of the UI, generated by a dedicated translation
pass and verified for key-set parity with the French reference via
tools/verify-i18n (100% coverage, 0 missing/extra). Regenerated the
committed index.html via go generate to pick up both the French tracker.*
additions and the English translations.

Italian, Spanish, Russian, and German remain at the 12-key seed and are
next in line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
applyI18n() only walks static DOM elements marked data-i18n. Panels
that build their own text at render time via t() — Config/Engine
(loadCfg), Tasks (loadTasks), Agent/Internet/MCP/Network/API key
(loadAgent/loadInternet/loadMCP/loadNetwork/loadApiKey) — kept
showing whatever language was active when they were last rendered,
so switching the language selector left "MOTEUR", "ACTIF", task
labels, etc. stuck in the old language until their next natural
refresh.

setLang() now also calls loadAll() (same aggregate refresh already
used after a service restart) so every dynamic panel re-renders
immediately in the new language. This only touches the app's runtime
wiring — the translation file format contributors edit (00a-i18n-data.js)
is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…back strings

The Trackers modal (index.tmpl.html) shipped with zero data-i18n
attributes — it came in fully French from the upstream v0.13.0/v0.13.3
rebase and I'd only converted its JS-driven content (19-tracker.js),
missing the static shell: modal title, intro text, "+ Nouveau tracker",
back button, Date/Heure labels, textarea placeholder, Ajouter/Fermer
buttons. Found via a systematic audit (strip HTML comments, grep for
remaining accented French text without a data-i18n attribute on the
line) run after a user report that some sections weren't switching
language. 10 new tracker.* keys added to fr/en.

Also fixed two hardcoded French fallback strings found by the same
audit in 14-attach.js: the upload-error toast fell back to the literal
'échec' instead of the already-existing (but unused) t('attach.failed')
key, and an unnamed file fell back to literal 'fichier' instead of a
new t('attach.unnamed_file') key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…xtraction

The initial full-UI extraction copied some visible HTML text verbatim as
the "French" value without noticing it was already English — the app's
original author used bare English words for a few top-level section
headers (Device, Tasks, Link, Engine, Settings, Memory, MCP servers) even
in the otherwise-French UI. Found via a systematic audit: every fr/en key
pair with an identical value was reviewed against the app's own established
French vocabulary (mémoire, tâche, lien, moteur, réglages — all used
extensively elsewhere in the dictionary), separating genuine bugs from
legitimate loanwords the app already uses consistently (preset, backend,
session, vision, terminal, etc. — left untouched).

Fixed:
  device.title, tasks.title, link.title, engine.title, settings.title,
  memory.title, mcp.title, preset.sysprompt_heading,
  settings.memory.edit_btn, chat.stop_button_title, chat.stop,
  models.switching, models.switched

Cross-checked the English translation against the same pattern (en value
identical to fr) — all 84 remaining matches there are legitimate technical
loanwords/acronyms, confirming English has no equivalent bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 13-key French bug just fixed (device.title, tasks.title, etc. left
in English) was invisible to the existing check: the keys WERE present
in every language, just never actually translated — copy-pasted from
the English-labeled source HTML straight into the fr block. A
missing/extra-key diff can't catch that class of bug by construction.

Added a second pass: for each non-fr language, flag any value that's
byte-identical to the fr reference and isn't in a small hand-vetted
loanword allowlist (preset, backend, session, API, and ~85 other terms
this app already borrows into French on purpose, cross-checked against
the app's own established vocabulary before being added). This is a
warning, not a failure — a translator may legitimately decide a term
is a fine loanword in their language too — but it turns "silently
shipped in English" into "printed for a human to glance at," which is
exactly the gap that let the original bug through undetected.

Verified the detector fires (temporarily broke one en value, confirmed
the tool caught it, reverted) and that it's currently clean: 0 warnings
across en (100% translated) and it/es/ru/de (12-key seed, unaffected).

docs/TRANSLATING.md updated to explain the warning and why it exists,
so a future contributor treats it as a real signal instead of noise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ang)

Investigated a user report of "the AI stopped mid-response, several
times yesterday" by checking ajean-ui.log and the Windows Event Log.
Found 4 incidents on 2026-09-01: 3 SIGSEGV crashes inside atiadlxx.dll
(the exact concurrency bug already fixed earlier that day by
serializing calls behind adlCallMu, commit cbfc6a8) — and a 4th,
different one: an "Application Hang" (Event ID 1002) at 23:41:55,
ajean.exe force-closed by Windows after ~65 minutes unresponsive.

The hang is a gap the mutex fix left open: adlAMDSensorsBlocking has
no timeout on any of its ADL calls. If the DLL itself ever stalls (a
driver hiccup, the GPU busy with something else), the goroutine
holding adlCallMu never returns — so it never unlocks, so every
subsequent /api/vram poll (one every 3s from the browser) piles up
forever waiting on a lock that will never be released. One bad DLL
call and the whole app is wedged, silently, until Windows notices and
kills it — arguably worse than the crash, which at least fails loudly.

A timeout can't fix this from the caller's side: Go can't force a
stuck syscall to return, so the goroutine and the lock would stay
stuck regardless of whether the caller gives up waiting. The actual
fix is removing the DLL call from the request path entirely:

- adlPoller is now the ONLY goroutine that ever calls
  adlAMDSensorsBlocking, on its own 3s loop, independent of how many
  browser tabs are polling /api/vram.
- adlAMDSensors (what callers use) just reads the poller's last
  cached value — instant, never touches the DLL, immune to it
  stalling by construction.
- If the DLL does stall, only the poller goroutine gets stuck; the
  cache goes stale and adlAMDSensors reports "unavailable" past 15s
  (adlCacheMaxAge) instead of ever taking the HTTP server down or
  serving a frozen, increasingly wrong reading.

adlCallMu is kept as defense in depth (cheap, uncontended now that
there's a single caller) in case anything else ever calls the
blocking function directly.

Verified: 600+ concurrent /api/vram requests across multiple bursts
(300, then 3x100) — process stayed up throughout, kept responding,
readings still correct (temp/power/fan all populated from the cache).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Worlgun
Worlgun force-pushed the i18n-and-gpu-extras branch from b456037 to 7644b2d Compare September 2, 2026 18:19
Worlgun and others added 2 commits September 2, 2026 20:45
Two of the five GPU-panel ideas from an earlier brainstorm (power/fan
and Intel util% were already shipped this session):

- Device panel: each GPU's utilization now keeps a rolling 60-sample
  history (~3 min at the existing 3s poll rate) and draws it as a
  small inline SVG sparkline under the existing temp/util line — no
  charting library, ~20 lines. History is keyed by GPU name (stable
  across polls; array order from /api/vram isn't guaranteed to be).

- Benchmark modal: runBenchUI now reads the active preset's last
  saved bench (already persisted server-side by saveBenchForActivePreset,
  just not surfaced anywhere for comparison) BEFORE kicking off the new
  run — the new run overwrites that saved value the moment it finishes,
  so this is the only point a real before/after comparison is possible.
  Each result card gets a delta badge (+/- tok/s and %, green/red),
  suppressed under ±1% to avoid normal measurement noise looking like a
  meaningful change. No backend changes — the data was already there.

Multi-GPU tensor-split (the fifth brainstormed idea) turned out to
already exist in full — device selection, a live drag-to-split slider
proportional to VRAM by default, --split-mode picker — found while
reading 07-models.js for this work. Nothing to add there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Repro'd from a user screen recording: on the very first /api/vram poll
after a cold start, the Intel iGPU's name flashed as a Windows/driver
-localized generic string ("Carte graphique Intel(R) UHD 770" for a
French OS) before settling on the real Vulkan-reported name on the next
poll, 3s later. Confirmed not an i18n bug — no t() call renders a GPU
name — and not reproducible live (6 consecutive polls all stable). Only
one occurrence across the whole 20s recording, right at the start.

Fix: a name change is only adopted after two consecutive identical
reads; a single differing poll is held back in favor of the last
confirmed-stable name. Applied by array position, and the sparkline
history (05-status.js) now keys off this stabilized name too, so the
same blip can't fragment it into two series.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Worlgun and others added 3 commits September 2, 2026 22:10
Upstream issue nathaninline#43: calibrating --n-gpu-layers below
999 (offload everything) means guessing how many layers a model even
has — nothing in the app exposed that number.

Added a minimal GGUF binary metadata reader (backend_gguf.go) — reads
only the header + key/value metadata section (a few KB) to find
general.architecture then <architecture>.block_count, never touching
the actual tensor weights (which can be tens of GB, later in the
file). No existing GGUF parser was in this codebase to reuse; wrote
one against the GGUF spec (stable, versioned format) rather than
pulling in a dependency for ~200 lines.

/api/models now includes a `layers` field per entry when readable.
The preset editor's NGL row sub-label picks it up via a data-layers
attribute on each <option> (set once when the picker populates, read
back on selection — no extra network round-trip): "999 = tout sur le
GPU · modèle : 65 couches".

Verified against a real file on this machine (Qwen3.8-27B-Uncensored,
IQ4_XS): correctly read block_count=65, plausible for that model size.
No second GGUF file available locally to cross-check, but the parser
follows the documented spec exactly and errored on nothing while
reading a real multi-GB file's header.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Upstream issue nathaninline#45: switching presets mid-conversation
overwrites the model-name label under the composer, and nothing records
which model produced which past message — after a few switches, no way
to tell which answer came from which model, in the live chat or in a
reopened session.

The active preset's display name is now captured once per turn, on the
same `user` delta already emitted at StartTurn (chat_conversation.go,
activePresetName() in backend_presets.go) — persisted and replayed
through the exact same path as the rest of the turn, no separate replay
handling needed. The frontend (09-stream.js) carries it as T.model for
the turn and prepends it to the existing stats line at turn end ("Qwen
3.8 27B - MAX · 1h 45m · 21.1K tok · 67.4 tok/s") instead of adding a
new UI element — same line the user already reads for timing.

No new user-facing static strings (the model name itself is dynamic,
user-chosen preset text) — no i18n keys needed for this one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tem total

The RAM panel showed system-wide used/total but never how much of it was
ajean itself (vs. the loaded model's process, vs. anything else running on
the machine) — no way to tell if a saturated machine was due to the model
or something unrelated.

web_procmem_windows.go reads ajean's own working set directly via
GetProcessMemoryInfo (psapi.dll), and finds llama-server's by name via
PowerShell (same pattern as windowsAdapterStats in web_devices.go) since its
PID is never passed back to the web-server process. Windows-only for now,
matching this project's existing GPU-telemetry precedent — web_procmem_other.go
is a no-op stub elsewhere. handleRam adds ajean_used/llama_used (MiB) to the
JSON only when available, so the UI shows nothing extra rather than a
misleading "0 Go" on platforms without this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Worlgun Worlgun changed the title i18n system (FR/EN 100%) + GPU power/fan/Intel util% telemetry i18n (FR/EN 100%) + GPU/RAM telemetry, NGL layer hint, per-turn model tag Sep 2, 2026
@Worlgun

Worlgun commented Sep 3, 2026

Copy link
Copy Markdown
Author

Vous aviez raison sur #60 à propos de mélanger plusieurs changements — même souci ici en fait (i18n, télémétrie GPU, RAM par processus, indice NGL, tag du modèle : 5 sujets distincts sous un seul titre). Je ferme et je vais la réouvrir en PR séparées, une par sujet, pour la même raison que vous avez donnée : pouvoir prendre l'un sans l'autre.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant