Mod:
- %config slots/pull/publish/unpublish for the four cloud slots
- %showcase [new|popular] [page] | load | copy, with number references
- %cloud opens a CloudScreen (link/unlink, slots, showcase) on vanilla widgets
- %config load IDDQD works offline (everything off except ChinaHat)
- default backend URL is now the production gateway
- shared ConfigRemoteApplier and ClientThread replace per-class copies
- %link unlink revokes the link on the server, then clears the local token
Backend:
- POST /device/revoke (RFC 7009 style self-revoke, always 204), rate limited
to 10/min per IP at the gateway
CloudScreen is compiled but has not been opened in a running client yet.
Lighthouse (production, default mobile 4x-CPU profile) scored
Performance 33/100, dominated by one ~536-538 KB entry chunk that
bundled every route's code regardless of which page a visitor landed
on -- the "chunks are larger than 500 kB" Vite warning on every build
this session.
Converted every route except / and the 404 catch-all to React Router
8's native route.lazy (blocks navigation until the chunk is ready --
no separate Suspense fallback needed). AppShell/HomePage/NotFoundPage/
RequireAuth stay eager since they're needed on every load regardless.
Result: main chunk 490.84 KB, no >500kB warning, one small chunk per
route (LoginPage 1.4 KB, ThemesPage 15.5 KB, AccountPage 11 KB, etc.)
loaded only when actually visited.
Fixed a real race the lazy boundary exposed: /login and /register
share an "Почта" field label, and during the chunk swap between them
the old page can stay mounted a beat longer than before -- a test
grabbing the field by label could land on the about-to-unmount one.
Also bumped RTL's asyncUtilTimeout and vitest's testTimeout, since the
route-transition + findBy chain didn't always land inside the old
1000/5000ms defaults under full parallel test-suite load.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
- robots.txt now also points at a real sitemap.xml (public pages
only: /, /download, /themes, /showcase) and disallows the
auth-gated routes (/account, /configs, /link) that have nothing
useful to a crawler
- canonical <link> on the root page
- SoftwareApplication JSON-LD so Google can render a rich result
(name, free price, icon) instead of a plain blue link
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Lighthouse audit findings:
- No robots.txt existed, so nginx's SPA fallback (try_files -> index.html)
served the React app's HTML at /robots.txt -- crawlers got 44 "syntax
not understood" lines instead of directives.
- No Cache-Control on /assets/*, so every visit re-fetched a hashed,
content-addressed bundle that can never actually change under that
URL. Lighthouse estimated 936 KiB/visit wasted on this alone.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Settings values and visibility predicates move to ButtonStyleSettings,
icon drawing to ButtonIconRenderer; facade keeps the public static API
and getters. Drops three dead one-liners (getLastContext,
renderCenteredText, renderLeftAlignedText) and the unused
renderButtonInternal wrapper.
Rendering moves to ScriptedTriangulatorRenderer, prop maps and cache
signatures to scripted/panel/; records and row helpers stay nested so
caller references (Panel, Palette, row, rows, idString) keep working.
React.memo alone does nothing here: PaletteTabs built a fresh
`(hex) => setColor(slot, hex)` closure per field on every render, so
memo's prop comparison always saw a "changed" onChange and re-rendered
anyway. Stabilized it properly:
- setColor/setGradient read draft/onChange from a ref (updated in a
useLayoutEffect, not during render -- oxlint's react(refs) rule
correctly flags a ref write in the render body) so they keep a
stable identity (useCallback, empty deps) across edits
- per-slot wrapper closures are built once in a useMemo keyed off
those now-stable functions, not off `draft`
This doesn't change the preset-switch case measured earlier (every
field's value genuinely changes then, so memo correctly re-renders
all of them -- that cost is what startTransition already covers). It
does stop sibling fields re-rendering on interactions that don't
touch them: undo/redo, tab switching, hovering unrelated panel
elements.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Measured with Playwright (rAF frame deltas around a preset click, real
Chromium, production): even with every CSS transition disabled, a
single frame still spiked to ~133ms on preset switch -- proving the
worst offender wasn't CSS at all, but the synchronous React re-render
(reset() swaps the whole draft, re-rendering every ColorField/
GradientField and the ClickGui panel at once).
Wrapped the three call sites that replace the whole draft (preset
pick, reset-to-default, file import) in startTransition so React can
deprioritize that render instead of blocking the frame the click
lands on.
Result (same measurement, after fix): peak frame 133ms -> 83ms,
frames over 100ms: 1 -> 0. Not a full fix -- frames over 16ms actually
rose slightly (18 -> 21), so total render cost is roughly the same,
just spread out instead of landing in one blocking spike. A real fix
for the remaining cost needs memoizing ColorField/GradientField so a
preset switch doesn't re-render every field instance; noted as a
follow-up, not done here.
panel.test.tsx: import's name-box assertion now waits instead of
checking synchronously, since the state update is deferred.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Two inherited-custom-property transitions were forcing a full style
recalculation on every animation frame during a theme switch:
- html transitioned --color-ice/--page-accent-glow (inherits: true),
both read by dozens of selectors site-wide (buttons, links, glows,
color-mix() gradients) -- animating them recomputed style for every
element that references them, across the whole page, not just the
swatch that was clicked.
- .lv-gui (the ClickGui replica used in ThemeStrip, HudPlayground, the
login backdrop and the theme editor panel) transitioned 21 inherited
custom properties at once, including per-row/per-tab ones
(--gui-row-*, --gui-tab-*, --gui-menu-*) -- with a long module list
that's the same cost multiplied by every row/tab in the tree.
Both now snap instantly instead of transitioning; .lv-gui keeps a
300ms cross-fade but only on the ~8 properties that read as "the
theme" (window/header/stroke/text/accent/panel), not per-row state.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
Owner's complaint: the two-pane layout's decorative right-side panel
("Твои настройки LoVisual на любом компьютере...") made no sense next
to a login form. Dropped the whole two-pane concept, not just its
visuals.
Login/register now render as the mod's own in-game account screen:
night-lake backdrop (the same Backdrop the landing hero uses), a
clock + Вход/Регистрация tab switcher styled like the mod's own
ClickGui tabs, a single glass panel on the mod's own category
colours, and a real mod-style hint row ("X - Y") instead of marketing
copy. What an account actually gets you is stated as plain hints
(4 cloud slots, share a slot's code, the mod signs itself in by code,
no account required to just use the mod) instead of a sales pitch.
- AuthSplit.tsx removed; AuthScreen.tsx + auth.css replace it
- LoginForm/RegisterForm: dropped the redundant login<->register text
link now that the tab switcher does that job; RegisterForm now
honours the original `from` redirect (previously always went to
/configs even when registration started from /link)
- Rewrote auth i18n copy to match the site's established voice
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
The editor's control panel is now a second ClickGui window skinned
live with the theme being edited, so every change shows in the tool
itself, not just the preview -- same "the section is a ClickGui
window" move the landing's ThemeStrip already uses, applied to a
working tool this time.
- Removed the three uppercase eyebrow-style section labels (the same
generic-tell pattern already fixed on the landing page); colour
slots are grouped into named fieldsets (Window/Modules/Text) instead
- Panel split into panel/{PanelHeader,PresetPicker,PaletteTabs,
ExportActions}.tsx to stay under the file-size limit
- Preset chips got a real swatch (accent dot over a mini card
gradient) -- previously same-hued presets like Classic/Blue/Azure
all read as identical blue dots
- Copy/share buttons confirm inline ("Скопировано"/"Ссылка
скопирована"); import/clipboard failures now surface a message
instead of failing silently
- Rewrote themes copy to match the landing's voice
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
dev-stack.sh boots accounts/configs/gateway on localhost; e2e.sh then
replays the exact request sequences the Java client sends (device link,
%config save/load, /me) plus the site-side register/login/confirm and
asserts every field the mod reads back. 31 checks, all green.
Landing:
- Rewrote all landing copy (ru/en): removed the tracked-caps eyebrow
chrome pattern everywhere, active-voice player-facing text
- ThemeStrip: full-bleed live-recolored ClickGui window instead of a
boxed swatch panel; HudPlayground: full-size real HUD screen
- ShowcaseTeaser: dropped the fake "example" fallback cards, shows an
honest empty state when there aren't 3 real configs yet
- Fixed VoxelScene parallax leaking outside the hero into the HUD
section
Data accuracy: the module list/counts advertised InvisESP, BlockESP
and FakeLag — all three were removed from the mod earlier this
session as cheat modules. Corrected the catalogue and the 88/52
module-count claims to the real 86/50.
Design QOL pass: removed the same eyebrow-label chrome from the site
footer; replaced plausible-looking fake share codes (howItWorks demo
slots, the chat command typing demo) with an explicit placeholder
string so they read as illustration, not fabricated real data.
Perf: promoted .lv-lower-bg onto its own compositor layer
(will-change/translateZ) — a full-height, multi-gradient, masked,
continuously-animated background layer was repainting on every
scroll tick in Safari.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
New visitors (no saved lv_theme) now get the Amber accent; reconciled the
static CSS accent, demo scene colors and console greeting to match so there
is no blue/emerald flash before the theme provider applies. Updated the
default-theme assertions in the theme tests.
Avatar objects live at a deterministic key (avatars/<id>.png) served with a
300s cache, so after a re-upload the <img> kept pointing at the same URL and
the browser showed the stale crop until reload. Append ?v=<uploaded_at epoch>
to every avatar_url (me, public profile, and showcase/grpc batch) so the URL
changes whenever the avatar changes.
Recolor the site-level --color-ice default and glow from #5cc8e7 to #34d399
and align the decorative VoxelScene/ParticleField accent and the DevTools
greeting. ClickGui/mod theme accents (classic preset) are left untouched; the
avatar success notice now reads as positive green via tone=success.
addAvatarHistory was fired off without awaiting, then the history tick was
bumped immediately, so the strip re-read IndexedDB before the write landed and
only populated after a remount. Await the write before bumping the tick.
accounts-service returned avatar_url built from the internal S3_ENDPOINT
(http://minio:9000/<bucket>), which browsers cannot resolve, so avatars never
rendered and the re-crop fetch failed. MinIO is intentionally not exposed.
Add a public GET /media/{key} read route in accounts-service (behind the
gateway internal-key guard, no identity required so anonymous profile pages
work) that streams the object out of private MinIO. Introduce
AVATAR_PUBLIC_BASE_URL so the browser-facing prefix is decoupled from the
internal endpoint; prod sets it to the same-origin /api/media, gateway routes
the media segment to accounts.
Task 12 batch around the existing crop/preview flow:
- Success Notice (auto-dismiss ~2.5s) after an avatar upload.
- "Reposition" re-crops the current avatar without re-picking a file.
- Drag & drop an image straight onto the profile avatar circle.
- Real upload progress via a new XHR-backed api.upload (fetch has none);
refreshOnce now also stores the renewed token (reload-restore fix).
- Local avatar history (last 5, IndexedDB, this-device-only) strip that
re-uploads a stored crop. Stored as ArrayBuffer (Blob is not cloneable
under structuredClone); label makes the per-device scope explicit.
- Added a "success" tone to Notice.
Infra (same session): registered @shared/@features/@pages/@app/@test path
aliases in vite.config + tsconfigs and rewrote deep ../../../ imports;
moved avatar components into components/avatar/ to keep folders at <=4 files.
Tests: avatarHistory cap/order, recrop visibility, drop-opens-cropper,
client upload/progress/401-retry. bun test 103 pass, lint 0 errors, build ok.
The download button pointed at a github.com releases redirect and the
changelog at the GitHub API — both unreachable if GitHub itself is
blocked for a visitor. The button now links to /downloads/lovisual.jar,
served directly from the site's own nginx. Release notes still read
the GitHub API (already degrades gracefully to an empty-state message
if that fails) since that's changelog content, not the actual download.
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
- nginx: rewrite the refresh cookie's Path=/auth to /api/auth on the
production proxy — without this the browser never sent the cookie
back on POST /api/auth/refresh and the session was lost on reload
- i18n: fallbackLng ru -> en, so only an actual ru browser locale
defaults to Russian, everything else defaults to English
- index.html: explicit English og:*/twitter:* tags so link previews
in Discord/etc. don't fall back to the (Russian) meta description
- Avatar upload: pick -> circular crop/reposition/zoom preview ->
confirm -> upload, instead of uploading the raw file blind
- Topbar: logo pill background removed, pixel-heart mark added next
to the LoVisual wordmark
- Fixed two tests left stale by the earlier tagline copy change
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
- Site favicon/mod icon: aligned voxel heart mark with lightning-bolt
side notches, crisp pixel edges, centered bounding box
- Landing hero + meta description updated to "visuals you'll fall in
love with" (RU/EN), no em dashes
- fabric.mod.json now points at the new mod icon asset
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ