- mail/ module (lettre, ru/en templates), links built only from PUBLIC_BASE_URL
- migration 0006: accounts.email_verified_at, shared email_tokens table (verify + reset), existing accounts marked verified
- reset mailer no longer logs tokens; RESET_MAIL_MODE=log is dev-only and refused with COOKIE_SECURE=true; Disabled by default answers 503
- forgot-password and resend-verification do their work in a background task (no timing oracle)
- device linking requires a verified email; email_verified exposed via /me and gRPC
- gateway rate limits, SMTP_* in compose and .env.example
- frontend: verify, forgot-password, reset-password pages, verify banner, ru/en strings
- Cosmetics: new Settings category with kind pills (Wings, Hats, Models, Masks, Weapons) and a card grid; Wings, ChinaHat, TazikHat,
CustomModels, TrollfaceMask and Knife moved to ModuleCategory.ACCESSORIES and hidden from the Modules panels
- Card previews: pick a picture (native dialog or drag and drop) and crop it in an editor; PreviewCrop, PreviewStore, PreviewEditor
stored under config/lovisual/cosmetics/previews
- HUD: right click a widget while the chat is open to get a popup with its toggles, sliders and modes (quick settings)
- Themes tab: HUD and ClickGUI pages; new ClickGuiLookConfig replaces hard-coded panel blur constants, HudGlobalConfig look defs
- TotemSound: own sound on totem pop (everyone/self/others, volume, pitch, mute vanilla through SoundEngineMixin)
- tests: CosmeticKind, PreviewCrop, QuickSliderMath, TotemSound; ./gradlew test green
- GodRays: PostProcessPass PRE_HAND, godrays.frag (radial sky blur to the sun over the scene, depth from mainDepth),
GodRaysUniforms, GodRaysSkyMixin takes sunAngle/sunset colour from SkyRenderState; off under shaderpacks (Iris)
- Knife (Melee Skins): 33 skins from Bedrock geo/anim in assets/lovisual/knife, animator (static_idle, draw/inspect/melee/run,
0.2 s crossfade, Catmull-Rom) with background baking, first/third-person render, sounds, mixins ItemInHand/PlayerItemInHand/Minecraft
- Wings: wingSource Procedural/Model with 14 textured Bedrock models (WingModelDef/WingModels/WingModelRenderer, BedrockClip +
procedural angel/demon/dragon); shared Bedrock parser util/bedrock (GeoParser, GeoFaces, Channel, Clip)
- EnchantGlint: Pulse/Fire/Galaxy/Hologram/Glitch/Sparkle/Aurora styles in glint/style, coverage setting, custom animated
texture matrix via GlintTextureMixin.styleMotion; lang en/ru for all new settings
- Themes: shader backdrop for ClickGUI (Backdrop record, 6 modes, intensity/speed/scale, ui_theme_backdrop_batch.frag with
700 ms REVEAL/ERASE wipe, ThemeBackdrop2D in BackgroundComponent), ThemeEntry.backdrop persisted in JSON and profile codec,
6 presets with backdrops, backdrop settings in the theme editor
- tests: GodRaysSun, GlintStyles, Knife assets/animator, WingModel, Backdrop, bedrock Channel; ./gradlew build green
- AutoSprint, TotemCounter, EnchantGlint (styles, texture repaint, scroll speed/scale mixin), ArmorColor (durability colouring, targets, pulse),
PlaceAnimation (block slides in from the hand; chunk-mesh hiding incl. Sodium, moving-block render), aligned with evo behaviour
- CustomModels: Chicken, Rabbit and Verity (Blockbench) models for chosen players, bundled assets, render-state carrier + submit redirect
- KineticLyrics: fix track key wiped by reset() (lyrics never applied, lrclib hammered every tick); new LyricsFinder with ranked lrclib search,
unsynced timing estimate, NetEase fallback and retry on busy services; tests for the pure parts
- fix mode()/modeSetting() misuse for new modules; TODO updated
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