Owners/admins can now see, on the Аккаунт settings tab, a
tamper-proof journal of who created, edited or deleted each order
and when — including employees who have orders.* permissions. It is
written from inside sun_save_app_state itself (which already
diffs orders server-side for permission checks), so it can't be
spoofed or wiped by the client, unlike the old per-browser
'История изменений' list which only covered the current device and
had a 'Clear history' button anyone could press.
- New table public.sun_order_audit_log (workspace, order id, action,
actor, summary, details), locked down to security-definer writes
only — no client insert/update/delete policy exists.
- New RPC sun_list_order_audit(workspace, limit), admin-only.
- New settings card 'Журнал заказов' reading it, admin-only,
classified into the existing Аккаунт settings tab.
- Verified end-to-end against a local PGlite instance: create/edit/
delete each produce one correctly-attributed row, and a non-admin
member is denied read access.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Order payment colors settings now live under the Оформление
(appearance) settings tab instead of Заказы, matching the request
to group interface/appearance-related settings together.
- Client sources card is now full-width (wide) like the event types
card, so both reference-list cards render with the same row width
instead of one looking noticeably shorter than the other.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clients: every page load re-sent all clients (72 requests for 18 clients)
because pushAll ran from several startup events at once, never skipped
unchanged clients, and overlapping saves of one client read the same stale
version, so ~40% ended in 409 conflicts. The payload also carried a fresh
updatedAt, so even identical re-sends bumped the server version and wrote a
change event, which made other devices' next save conflict too.
- Remember what the server holds per client (content fingerprint, scoped to
the workspace) and skip unchanged clients; seed it from the server
snapshot so a device that is already in sync sends nothing.
- Serialise saves per client and make pushAll single-flight.
- Load the server snapshot before the startup push instead of racing it.
- Drop the volatile updatedAt from the payload (server keeps updated_at).
Error log: a record the server refuses (Access denied for a workspace the
user is not in) stayed in the IndexedDB queue forever, was re-sent on every
flush and could block newer records behind it. Records from another
workspace are now dropped, others after 3 attempts.
Adds tests/client-sync-v1780.mjs (fake server enforcing the SQL conflict
rule; fails on the old module) to test:static, and bumps the cache-busting
versions of performance.js / app-runtime.js.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recover only unverified Caterium Developer TOTP factors from the full factor list. Preserve verified MFA, reuse the in-memory QR after closing the dialog, serialize enrollment and retain correct-code/AAL2 checks. Complete pull-request QA passed in run 35518429693. Standard main QA and publication gates remain unchanged.
Keep notification-read state personal to user/company, preserve server read-only sections during staff synchronization, and respect separate order-create/edit/delete rights. Add visible mobile header session actions and a sticky logout that survives profile RPC failure; scope asynchronous profile/branding to user and workspace. Targeted browser suites and isolated SQL recovery tests passed in run 35506145405, iPhone screenshots reviewed. Full main QA remains required before production promotion. Workspace branding RPC migration is included but has NOT been applied to production Supabase; older servers retain safe owner-only fallback. No live membership/business-data repair is claimed without identifying the reported employee.
Inline handlers built as onclick="fn('${esc(id)}')" were injectable:
esc() turns ' into ', which the browser decodes back to ' before
the JS runs, so an id like x');alert(1);// broke out of the string.
Ids can come from a restored backup file or a synced catalog. Add
SunSafe.jsArg (JSON.stringify + HTML escape) and use it in all 23
handlers in app-runtime.js and index.html. Verified in a browser: a
payload id is passed through as a plain string and nothing executes.
Also replace the Settings version label that still showed
v17.6.0 · 2026.09.07, and bump the cache-busting version of the two
changed scripts (sun-safe.js, app-runtime.js).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add Settings > Учебный каталог, off by default and available on ordinary writable profiles. Load ready example boxes, photos, TTKs and linked sample inventory additively; preserve own data, saved orders and edited examples when hiding or re-enabling. Respect company/profile scope, read-only permissions, failed downloads and tenant changes. Preserve the opened recipe guide across real catalog refreshes on iPhone. Integrated feature checks and full pull-request QA passed. Standard main QA and exact-asset production UI verification remain in place.
Add compact accessible client summaries and menu discounts in percent or rubles with explicit durations. Preserve ordinary catalog prices and order line snapshots; derive current prices and expire promotions automatically without background writes. Keep fractional prices, legacy sale compatibility, and regression coverage. Full pull-request QA passed. Publication remains gated by full main QA and byte-for-byte production asset and UI verification.
Add a new "Банкетное меню" catalog tab (category 6) alongside Боксы/
Премиум/Посуда for composing wedding/banquet/anniversary menus. Unlike
the other tabs, dishes here are priced and weighed per guest, grouped
by menu section (catalogSection), and clicking a checkbox doesn't add
to the order directly -- it toggles inclusion in a live summary panel
showing a running per-guest price table for the whole composed menu.
The item editor gets two new fields (menu section, weight per guest)
shown only for this category, reusing the existing generic item CRUD
(editBox/saveBox) rather than building a parallel admin UI.
Also re-bumped index.html's script cache-busting query string, which
the previous whitelist-fix commit changed the content of app-runtime.js
without updating -- the same stale-cache bug fixed earlier in the
session, now closed for directly-tagged scripts too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two separate Set-based whitelists (OFFER_TEMPLATE_IDS in app-runtime.js
and TEMPLATE_IDS in ux-fixes-v1764.js) validated any explicit template
selection before persisting/rendering it, but neither had been updated
when premium-dark/premium-emerald were added. The per-order "Оформление
PDF" picker's click handler calls persistOfferTemplate(), which silently
returned false when the id failed this check -- so clicking either
premium card in an order's offer modal did nothing, no error, no toast.
Verified end-to-end against a real order: both ids now persist and the
picker shows the selected state correctly.
Also bumped performance.js's internal RELEASE tag, since it gates the
cache-busting query string used to dynamically load ux-fixes-v1764.js
and eight other core modules -- otherwise this fix would hit the same
stale-cache issue just fixed in index.html.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two related fixes reported directly against live screenshots:
1. addonRow rendered each addon ("Напитки", "Чайная станция", ...) as
its own tall bordered pill sitting side by side -- disjointed, and
unlike the single-card "Полезные дополнения" treatment in the
referenced design. Now draws one card with a heading
("Рекомендуем добавить") containing all the icon+label items,
matching how checklistCard already works.
2. The addon list and the "Всё под контролем" checklist were hardcoded
constants in the canvas module, completely bypassing the app's
existing editable Client Offer Settings (Настройки → Предложение →
"Дополнительно к заказу", textarea-backed extraServices/controlLines
the user can already edit for the old templates). preparedSnapshot
now attaches the resolved controlLines/extraServices/titles onto
every snapshot; the 8 signature templates read them via a local
shadowing const (falls back to the previous defaults when nothing
is configured), so editing that Settings textarea now actually
changes what shows up in these templates too, on all 8 of them from
one change since addonRow/checklistCard are shared helpers.
Verified locally: default list (6 items) lays out cleanly in one card
with 2-line wrapping where needed; a snapshot with custom
controlLines/extraServices renders those exact custom strings instead
of the defaults, confirming the wiring actually works end to end and
isn't just falling back silently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User pointed at https://menu.furset.ru/view/23855347 as a design they
like and asked for a couple more templates in that direction. Added:
- premium-dark: near-black background, warm gold accents, real
line-drawn icon badges (calendar/people/box -- not emoji) for the
date/guests/boxes stats, bullet-point value props under the title,
minimal flat menu list -- closely matching that reference's visual
language instead of the card-heavy style of the existing templates.
- premium-emerald: the same layout in an emerald-green palette, as
the second "couple" of designs requested, sharing the cover code
with premium-dark and only differing by PALETTES entry.
Both plug into the existing shared renderContentPages engine, so they
get proper multi-page menu + pricing pages for free, consistent with
all 6 existing signature templates. New icon-drawing helpers
(iconBadge/iconCalendar/iconPeople/iconGauge/iconBox, statBadgeRow,
bulletList) live in the shared helper section for reuse.
Verified locally: both render in <70ms combined, produce 3 pages each
for a normal order, and hold up under the same adversarial-data pass
used for the other 6 (long names, 350 guests, seven-figure price,
item with no photo/price/category) -- no overlap, no crashes, generous
ellipsis truncation throughout. Fixed one real bug found while
testing: the date value in the stat badge ("8 августа 2026 г.") was
getting ellipsis-truncated at font-size 20px; reduced to 16px so it
fits cleanly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fix the price band on cream-elegance: it was a solid muddy olive
fill that didn't read as "this is your total" (reported directly by
the user). It's now a white card with a gold border, matching the
rest of that template's light card language; the dark templates'
solid-fill price band is unchanged since that already reads well
against a dark background.
- Give emerald-circles, midnight-checklist, gourmet-hero and
diamond-gold the same treatment as the first 2: real fonts
(Playfair Display/Montserrat or Unbounded/Manrope depending on
aesthetic) instead of Arial/Georgia, the same center-alignment fixes
applied everywhere text used align:'center', and their own
fully-styled inner pages (menu + pricing) via the shared
renderContentPages engine instead of the old generic base renderer.
- Fix two real regressions the font swap introduced and caught by
rendering each template locally: gourmet-hero's headline was
overflowing width and getting ellipsis-truncated ("КЕЙТЕРИНГ..."),
and diamond-gold's heading was wrapping to a second line that
collided with the subtitle below it. Both fixed with size/width
adjustments verified by direct canvas measurement.
- All 6 signature templates are selectable in Settings again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fix a systemic centering bug: several canvas text() calls used
align:'center' but passed the box's left edge as x instead of its
midpoint, so headings, badge numbers, checkmarks and chip labels
rendered pinned to the left instead of centered (most visible on
cream-elegance's "МЕНЮ" heading and neon-menu's guest badge).
- Fix cream-elegance's category icons: the emoji glyph was never
actually passed into iconCircle, so the icon circles rendered empty.
- Even out cream-elegance's bottom row (stat cards vs price band had
mismatched heights).
- Load distinctive Cyrillic-friendly type pairs instead of default
Arial/Georgia: Playfair Display + Montserrat for cream-elegance,
Unbounded + Manrope for neon-menu, awaited via the Font Loading API
before each render so text never draws in the fallback face.
- Narrow the Settings gallery to just these two templates per request
("остановимся на 2х пдф пока"); the other 4 signature designs and
the original 13 classic templates stay intact and renderable for any
order that already used one, just not offered for new ones.
Verified via a local static server (public/, http-server) with a real
uploaded box photo run through window.SunSignatureOfferPDFV18 and the
production sunClientOfferDebugPdfPages pipeline directly — 3 pages,
no duplicate cover, both fonts confirmed loaded via document.fonts.check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
renderOfferPdfPages routed the signature templates' inner pages back
through the classic module's own renderPages, which drew its own
extra cover page on top of the real content pages -- doubling the
render work and producing an extra page. Route straight to the plain
base renderer instead. Also await the gourmet-hero template's photo
cards instead of firing them off unawaited.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New "signature" template pack (cream-elegance, neon-menu,
emerald-circles, midnight-checklist, gourmet-hero, diamond-gold)
replaces the visible template gallery in Settings. The previous 13
classic/archive templates keep rendering correctly for any existing
order that already used one, but are no longer selectable for new
orders — nothing was deleted.
Also adds a Settings option to upload a custom logo and company name
for PDF documents, replacing the hardcoded "Солнце Кейтеринг" branding
wherever a logo fails to load.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RBAC permission checks (has()) always deferred to the cloud
workspace's role, which is null when using "work locally without
internet" — so every guarded action (save/delete order, edit catalog,
etc.) was blocked with "no permission" even though there is no
workspace to restrict against. Local-only mode now grants full access,
matching how nav permissions already behave with no cloud session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Settings "load from file" button only understood its own
sun-catering-backup format. Extend it to also read the
caterium-full-export format produced by the browser-console
emergency export (localStorage + IndexedDB), so an already-downloaded
export file can be restored directly through the UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets users download the local database as a JSON file and restore it
from a file, independent of cloud sync — needed while Supabase is
unreachable to move data between computers or recover after an outage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When Supabase is unreachable, employees were stuck on the login
screen with no way in, even though the app is fully usable offline
(orders, catalog, stock, etc. all live in localStorage already).
Adds a low-key link on the login screen that sets a local-only flag
and skips the auth gate entirely until a real cloud sign-in succeeds
(which clears the flag). Deliberately does not touch any cloud/session
state, so it can't trigger an automatic cloud pull that would overwrite
data created while working offline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
String(err) on a plain object (a PostgrestError-shaped object without a
.message, or any non-Error rejection) renders as the literal string
"[object Object]" with no useful information. Added errText() that
prefers err.message, falls back to JSON-stringifying the object, and
only then falls back to a generic message - and applied it to the three
catch blocks in the login/company-creation gate flow (auth(), the
pending-registration finisher, and the "retry workspace access" button),
which is what surfaced the raw "[object Object]" during login.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The api.caterium.ru PHP reverse proxy has been deployed and verified end-to-end
(curl tests plus live authenticated RPC calls through a real browser session).
This switches the client's Supabase fetch calls to go through the proxy so
requests from Russia work without a VPN. Realtime's WebSocket connection is
unaffected since it doesn't go through fetch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
showPasswordChangeGate() replaced the auth gate's content and returned,
but the same login also fires onAuthStateChange, which kicks off
loadMemberships() independently. Once that resolved (workspace found),
ensureAuthGate()'s periodic poll (every 5s, plus assorted event-driven
calls) saw signedIn+workspace and tore the gate down immediately,
regardless of whether a password-change form was currently blocking
it - the employee would see the "create a new password" screen flash
and vanish before they could use it, dropping them straight into the
app still on their temporary password.
Removes the forced gate entirely (mustChangePassword/
showPasswordChangeGate) and adds a "Пароль" box to the account
settings card instead, so changing your password is a deliberate,
always-available action rather than a one-shot dialog racing the rest
of the login flow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit accidentally carried over an uncommitted proxy
cutover (supabaseProxyFetch wired into createClient's global.fetch)
that was left sitting in the working tree from an earlier, paused
verification session - it was never meant to ship without first
completing the full checklist (auth, clients, orders, Storage,
Functions) against the live proxy. Reverting just that piece so
main goes back to calling cksuehzcimitsxmeloes.supabase.co directly,
keeping the "Загрузить из облака" button fix from the same commit.
The proxy cutover will come back once it's actually been verified
end-to-end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bindCloudUI() already wires a click handler on #sunCloudPullV2
(confirm dialog -> pullRemote({replace:true, quiet:false}), which
backs up the local copy and force-replaces it with the server's
orders/clients/etc.), but renderCloudUI()'s template never actually
emitted a button with that id - the handler was permanently a no-op.
Adds the missing button to the "Профиль и аккаунт" settings card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other cloud RPC call goes through sunCloudAwait (12s timeout),
but loadMemberships() (which resolves which workspace to open right
after login, showing "Загружаю рабочую базу...") called client.rpc/
.from directly with no timeout and no catch. A single slow or dropped
request left the auth gate stuck on that message forever instead of
surfacing a retryable error - there's already a "Проверить ещё раз"
button wired to reloadMemberships(), but nothing ever told the user
they needed to press it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>