Release 1.70.0
Important Release Documents
Release Notes - Seatmap Platform
Version 1.70.0 - 2026-08-02
Release Focus: A large editor and renderer release. The schema editor moves to layer-based canvas navigation, gains a global search across venues, schemas and events, and gets a dedicated events column in Pricing mode, alongside a security and data-integrity hardening pass that makes schema saves atomic and concurrency-safe. The admin renderer gains a flat section view, configurable hotkeys and block selection over grid coordinates. The booking renderer reports accurate numeric prices, keeps zoom, pinch and section-view animations within the pan bounds, renders very large venues faster, and loads venue backgrounds from bounded detail tiles instead of the full-size image. The booking service serves its public seatmap and prices endpoints from pre-compressed payloads.
MetaTitle: Release 1.70.0: Editor & Renderer Updates - Seatmap.pro
MetaDescription: Seatmap 1.70.0 adds editor layer navigation, global search and a Pricing events column, an admin flat section view, and faster booking endpoints.
Breaking
Webhook targets must be public HTTPS destinations
Webhook targetUrl values are now validated both when the configuration is saved and immediately before every delivery. A target is rejected unless it uses https and resolves entirely to public addresses. Loopback, private, link-local, unique-local, multicast, CGNAT and reserved ranges are blocked — including the cloud metadata endpoints 169.254.169.254 and 169.254.170.2.
Saving a rejected target returns HTTP 400 and stores nothing. A target that becomes invalid later is recorded as a TARGET_NOT_ALLOWED delivery attempt and is never retried.
Existing organisations whose webhook target uses http:// or points at an internal host will stop delivering after this upgrade and must be reconfigured.
Self-hosted deployments that legitimately deliver to an internal receiver can set seatmap.webhooks.allow-private-targets: true. This is an operator-only setting in application-seatmap.yaml; it is deliberately not reachable from the organisation or tenant webhook configuration API. The https requirement still applies when it is enabled.
See the editor-service webhook guide for the full list of blocked ranges.
Concurrent schema saves now return HTTP 409
Schemas carry an optimistic-lock version. When two saves of the same schema overlap, the first wins and the second is rejected with HTTP 409 instead of silently overwriting the other. Clients should reload the schema and retry on 409.
A schema save that references an unknown pricing zone now returns HTTP 404
Previously a seat referencing a pricing zone that no longer existed was silently saved with no zone, which produced incorrect pricing without any error. Such a save is now rejected. Zones deleted as part of the same save still unassign their seats as before.
Booking widget branding depends on the organisation’s plan
The booking service now resolves a branding level per render from the organisation that owns the public key, and the renderer applies it as an overlay above the canvas in both the WebGL and Canvas2D paths:
- No branding for an active organisation on a paid plan.
- A small clickable “Powered by Seatmap.pro” badge for an active organisation without a paid plan. The badge moves aside so it does not sit on the minimap.
- A blocking overlay that disables seat selection for a suspended organisation.
Branding resolution fails open: any error resolving the organisation renders with no branding, so it cannot break a booking flow.
Self-hosted deployments can force branding off with the SEATMAP_WATERMARK_DISABLED environment variable, or the booking.config.seatmap.watermark.disabled Helm value.
New in the schema editor
Layer navigation on the canvas (SEAT-1110)
The mode dropdown is replaced by an always-visible pill on the canvas that shows the active layer and switches between layers in one click. Canvas interaction is scoped to the active layer, and each layer keeps its own selection: switching away and back restores what was selected instead of clearing it.
The top menu is now a two-way switch between Edit and Prices. Edit returns to the active layer rather than forcing schema mode, and Prices no longer requires a published schema. The chosen mode is reflected in the URL as ?mode=prices and restored on reload.
The Objects and Image layers are gone. Objects only ever carried a shape lock that never took effect, and Image had no interactive surface, so neither changed what could be edited. Shapes are selectable in schema mode as before.
Global search across venues, schemas and events (SEAT-1084)
A search box in the navbar resolves event ids, schema ids, schema names, venue names and event names, groups the hits by entity type and navigates straight to the match. Exact-id matches rank first, then prefix, then contains, and the box is fully keyboard-driven — support and ops staff can paste an id from a ticket and land on the entity.
Results are scoped on the server: a global admin searches everything, a super admin searches their tenant, and everyone else searches their own organisation only.
Open an event in the schema editor pre-selected (SEAT-1088)
A new route opens the schema editor with a given event already active, so there is no flash of the default event. Two entry points use it: an event hit in global search, and an “Open in editor” link on the event detail modal for a same-organisation event that has a venue and a schema. A search hit in another organisation the user belongs to switches organisation first.
Events move into their own column in Pricing mode (SEAT-1096, SEAT-1102)
The event picker in Pricing mode is no longer a dropdown inside the right sidebar. Events now have a dedicated column between the canvas and the sidebar, showing every event in the schema at once with its status, date and pricing-coverage bar, so switching events no longer means opening and closing a menu.
- New: filter chips narrow the list to events with no price assignment at all, or to events with at least one, alongside the existing name and external-id search.
- New: the right sidebar always shows the active event on its own, with edit, preview and “open in playground” actions for it. The event’s status is a coloured dot beside its name.
- New: the column starts collapsed and is opened from the sidebar’s “All events” button; collapsing it from the column header hides it entirely. The choice is remembered per browser.
- Changed: the column appears only when the schema has more than one event. With a single event there is nothing to switch between, so the sidebar alone covers it.
- Changed: preview and “open in playground” moved out of the event rows into the sidebar, where they act on the active event.
- Changed for embedders: the existing
hidden=eventsURL parameter now hides this column. Embedders already passinghidden=eventswill see the events column disappear from Pricing mode; embedders who want it visible must remove the token.
Free rotation with Shift to snap (SEAT-1015)
Rotating a section, shape or table snapped to 15-degree increments at all times, so an arbitrary angle could not be chosen. Rotation is now free by default and snaps to the nearest 15 degrees only while Shift is held, matching the behaviour of common design tools. The live preview and the persisted angle agree.
Auto Login toggle in the organization edit form (SEAT-1087)
The Auto Login toggle was only available when creating an organisation. It now appears in the edit form as well, initialised from the organisation’s current value and persisted on update, and the admin API returns the current state.
Sales dashboard for billable organisations (SEAT-1111)
A global-admin dashboard summarises billable organisations with their plan, capacity and period-to-date usage, including a projection for the current billing period, served from a dedicated admin usage summary endpoint. It highlights organisations approaching their capacity and organisations that have gone dormant.
General-admission assignment and outline handling (SEAT-1112)
Assigning a price to a general-admission section is now a click, and the assigned quantity defaults to the section’s active count when it has one rather than always starting at 1. General-admission section titles and prices read correctly in the section panel, section titles are no longer stripped, and outlines can be detached and styled.
New in the booking renderer
Flat section view in the admin renderer (SEAT-1099)
The admin renderer can flatten a single section onto its seating grid, matching what the schema editor shows, and exit back to the venue view. The rest of the map is dimmed behind a backdrop while a section is flat, and row and seat labels are drawn over the flattened grid.
setFlatSectionView(sectionId: number | null): Promise<boolean>
onFlatSectionViewChange?: (sectionId: number | null) => void
setFlatSectionView resolves true when the view is flat for that section, or when null was passed to exit; it resolves false when the section cannot be flattened. Requesting the section that is already flat resolves true and does nothing. onFlatSectionViewChange fires on every entry and exit, including ones the renderer initiates itself.
Configurable admin hotkeys (SEAT-1047)
The admin renderer routes keyboard input through a hotkey controller with configurable bindings, covering selection, select-all and a temporary pan while a modifier is held. HotkeysSetting is exported from the package entry so integrators can supply their own bindings.
Block selection over grid coordinates (SEAT-1048)
The admin renderer exposes selectSeatBlock, which selects a rectangular block of seats by grid position rather than by hit-testing screen geometry. Untransformed grid coordinates come from dedicated per-section endpoints (event/grid/ and event/schema/grid/), fetched on demand and cached per section, which is why selectSeatBlock returns a promise.
Per-source section outline styling (SEAT-985)
Section outline styles applied uniformly to every outline, so a hover style meant for user-drawn zones also changed the auto-generated outlines around seat sections. svgSectionStyles.bySource now maps an outline source (svg, shape, auto, fallback) to the same state styles as the flat configuration and overrides them for that source only.
Flat configuration is unchanged and existing themes keep working. The WebGL overlay hover ring still reads the flat hovered stroke colour.
What’s fixed in the editor
Schema saves are atomic
The geometry write and the SVG regeneration now share a single transaction, so a failure between them can no longer leave persisted geometry with a stale or missing generated SVG. Thumbnail generation is still queued after the save commits and remains best-effort.
A failed bulk seat insert no longer reports success
An I/O failure during the bulk COPY of seats and rows was caught and logged while the surrounding transaction still committed, so the save returned HTTP 200 having silently dropped the seats; later saves then failed against the missing rows. The failure now propagates and the whole save rolls back.
Hardened XML and SVG processing
All XML parsing in the editor routes through a single hardened factory: DOCTYPE declarations are disallowed, external entity and DTD resolution are disabled, and secure processing is enabled. Attribute values recovered from a vector background are escaped before the background is rebuilt for measurement.
A schema background that declares a DOCTYPE or an external entity will not load. Backgrounds that rely on either need to be re-exported without them.
Organisation scoping on pricing, background and unselection endpoints
Pricing zones, prices, price deletion, pricing-zone deletion and background detection are scoped to the caller’s organisation: a request naming another organisation’s data returns 403. Integrations that relied on a shared client across organisations should send each request under the owning organisation. Global-admin access, API-role access, library sharing between organisations, and the public media route used to embed background images are unchanged.
The booking v2 unselection endpoint is scoped the same way: a request naming an event outside the caller’s organisation returns 403.
Webhook delivery logs redact credential headers
Response headers that carry credentials — authorization, proxy-authorization, set-cookie, www-authenticate, proxy-authenticate and x-amz-security-token — are stored redacted. Response body capture is unchanged by default and is now bounded by seatmap.webhooks.response-body-snippet-limit, or can be disabled with seatmap.webhooks.store-response-body: false.
Deprecated SSO token path removed
The deprecated Auth0/SSO token decode path has been removed along with its configuration. Tokens are verified against the configured RSA public key only. Deployments still setting SEATMAP_SECURITY_SSO_EXTERNAL_PUBLIC_KEY_URL can drop it; it is ignored.
A pricing zone shared across events resolved to another event’s price
Pricing-zone-to-price resolution ignored the event, so a zone used by more than one event could assign a price belonging to a different event. Resolution is now filtered by event, and a migration repairs the rows that were already written.
A schema save no longer deletes the schema’s pricing zones
A seatmap save whose payload omitted the zones key deleted every pricing zone on the schema and unassigned them from all seats and rows. The editor never sends that key, so any ordinary geometry save destroyed the schema’s zones.
An omitted zones key now means “leave the pricing zones untouched”. Sending "zones": [] explicitly still deletes every zone, and sending a partial list still deletes the zones missing from it, so the full-replace behaviour is unchanged for clients that send the field.
Cloned schemas no longer share generated image metadata
A cloned schema shared the source schema’s generated-PNG JSON node, so mutating one could corrupt the other. The node is now deep-copied.
Empty rows no longer accumulate when section deletion is prohibited
For organisations that prohibit section deletion, orphan cleanup was skipped entirely, so empty rows accumulated on every save and inflated exports. Empty rows without general-admission assignments are now removed; sections are still never deleted for these organisations.
Issue: SEAT-1059.
The Preview button no longer stays stuck loading (SEAT-1101)
Image-generation progress was published to an in-process message broker on whichever replica picked up the job, while the browser’s WebSocket session sat on an independently chosen replica. With more than one editor replica and no session affinity, the browser frequently never saw the completion event and the Preview button spun forever.
Progress is now published to Redis and relayed to every pod, and the client-side availability latch has a bounded timeout, so a missed event can no longer strand the button.
Selecting a shape no longer reorders the canvas (SEAT-1083)
Clicking a shape permanently hoisted it above labels and other objects, because selection mutated the canvas stacking order. Selection no longer changes stacking.
Outline path edits survive undo (SEAT-1113)
Editing an SVG outline’s path emitted an undo entry per point change, so a single edit could bury the previous state. Path edits are now batched to blur and land in undo history as one step.
Other editor fixes
- The venue artwork chip stays visible, and its badge appears only once there are bound artwork items to count.
- The active-layer chip returns from a non-layer mode.
- Layer and guide switching no longer enters undo history, and enabling a layer no longer undoes its locks.
- Initial grid visibility matches the guides state, and the crop guide is derived from mode and tool so the two cannot desync.
- Fit-to-screen accounts for the canvas HUD band, and the canvas bottom inset is applied when the renderer is created.
- Canvas drag state recovers when the pointer is released outside the window.
- The shortcut list documents the current digit bindings.
- A reducer import cycle that crashed schema-reducer-first loads is broken (SEAT-1114).
What’s fixed in the booking renderer
Accurate numeric prices in callbacks (SEAT-1056)
The booking renderer derives the numeric price it reports from a price’s display label. That derivation now:
- Preserves decimals. A price labelled
12.50is now reported as12.5instead of being truncated to12. This affectspriceon section-click and section-selection callbacks, andpriceon cart seats and general-admission entries (onSeatsSelect,getCart). - Continues to report
undefinedfor a non-numeric label (for exampleGold) and0for a0label, so a reportedpriceis always either a finite number orundefined.
Related correctness fixes in the same area:
- Cart price-id backfill now matches a cart price to its price entry numerically, so decimal and suffixed labels (for example
12.50,42 EUR) are matched correctly. - A general-admission entry whose stored price is not a finite number can once again be removed from the cart.
- Restoring a seat from a saved cart key now resolves a section by its name, so a section whose name happens to be numeric is no longer confused with a different section that has that numeric id.
- Loading a single section now trims the price list to that section, so seat and group price assignments from other sections are no longer carried into the isolated view.
Integrations that assumed integer-only prices, or that re-parsed the label themselves, should review these values.
Label styling is honoured again (SEAT-1069)
A schema’s labelStyle arrives on the wire as a JSON string and was never parsed, so every setting it carries — hiding the general-admission label, hiding the price dot, section label styling — was silently ignored. It is now parsed at schema ingest.
Zoom to a section no longer jerks or freezes (SEAT-1077)
During a zoom, two independent draw drivers painted the WebGL stage at different transforms, which showed as a two-step jerk, and a detail-crop upload running mid-animation blocked the main thread. The self-driven redraw loop is now gated while a transform animation runs, so the animator is the sole painter, and a pending viewport settle is cancelled when an animation starts — the detail crop runs once, after the final step settles. Concurrent zoom-in and zoom-out requests during an in-progress zoom are queued rather than interleaved.
Zoom and pinch settle within the pan limits (SEAT-1055)
Programmatic zoom-to-destination (including minimap clicks and 2D zoom-to-section) now commits the pan-limited position instead of the raw target, so a zoom into a point near the venue edge no longer rests past the bounds. A pinch-to-zoom that ends out of bounds now snaps back to the limit as well.
Section-view transitions no longer rely on a timing guess (SEAT-1055)
Entering and exiting a section view now waits for the zoom-to-fit animation to actually finish before rotating in, instead of guessing with a fixed timer. The rotate-in no longer starts early or late on slower devices. A section whose geometry produces an undefined rotation angle now logs a warning and resets cleanly instead of animating a NaN transform, and overlapping section rotations no longer corrupt each other’s state.
Zoom animations are robust and always resolve (SEAT-1055)
- A zero-duration zoom now commits in a single step with no blank or
NaNframe. - The pan-limit snapback runs even while a zoom animation is still in flight.
- Every transform animation now signals completion on every exit path – natural finish, cancel, or preemption – so an awaited zoom sequence interrupted by a gesture stops cleanly instead of leaving a pending promise or skipping its snapback.
Performance
Faster rendering and interaction for very large venues (SEAT-1078)
Data operations that previously re-scanned every seat or section now use internal indexes, so large maps (tens of thousands of seats, thousands of sections) load and respond faster. Seat filtering, locking, and state changes no longer rebuild the spatial hit-test index, minimap cart pins are drawn from the cart rather than by scanning all seats, and key-based seat lookups resolve in constant time per key.
The WebGL renderer also does less work per frame on large venues: row geometry is uploaded to the GPU only when it actually changes (not on every pan or animation frame), and the static background quad and per-seat selection/hover data are no longer re-sent each frame. The per-seat selection, hover, and loading overlay is now rebuilt only when the selection, hover, or a seat’s state actually changes (or a deselection animation reaches its end) instead of rescanning every seat on every pan and animation frame, and the “is any seat still loading” animation check runs in constant time. GPU shaders, textures, and buffers are also released more thoroughly on teardown – reducing memory use across repeated map loads. No renderer method signatures, configuration options, or callback values changed – this is an internal performance improvement only.
Bounded venue background loading (SEAT-1081)
Zooming into a venue background pulled detail from the full-size image, so a fit-to-screen view of a large stadium could fetch the whole tile grid — several megabytes for a single crop.
The converter now emits a chain of progressively smaller levels (2048px, 4096px, and so on while the level is still at most half the full width) alongside the tile grid, and encodes tiles as WebP instead of lossless PNG. The renderer picks the smallest level that serves the requested region, fetches its tiles in parallel and caches them, and re-checks each level’s decoded width so a level that under-delivers steps up to the next one. On a 9500px stadium background the worst case drops from about 2.5 MB to about 400 KB per crop.
The public seatmap is served from a pre-rendered payload
The public event endpoint cached a seatmap object graph and rebuilt the same response from it on every request. For a 34,500-seat venue that meant fetching 1.24 MB of serialized bytes, reconstructing around 200,000 objects, then serializing and compressing the same 891 KB of JSON again to produce byte-identical output.
The rendered payload is now cached as pre-compressed bytes and returned verbatim to clients that accept gzip, with an in-process layer in front of the shared cache so a hot event avoids the round trip entirely. Concurrent misses on the same key are served from a single render rather than racing.
Saving a schema evicts the shared cache for each of its events, so the next request re-renders. The in-process layer is per replica with a 30-second lifetime, so a change can take up to that long to appear on every replica. Callers that need a change reflected immediately should account for that window.
Faster and smaller prices endpoint
Building the prices response parsed each seat’s JSON twice per row, including for rows where the column was empty. On an event with 33,000 priced seats that dominated the request at roughly 1.9 seconds of CPU; it is now about 17 milliseconds. The response is also compressed, dropping it from 486 KB to 79 KB.
The wire format is unchanged, and the endpoint is deliberately not cached: it carries live availability driven by lock, unlock and sale, so every request reads current state.
Maintenance
- Booking-client dead code removed and the renderer audit’s remaining follow-ups closed: a WebGL hang and leak, the general-admission title restore, and a coverage gate (SEAT-1039, SEAT-1061).
- Type-checked ESLint enabled for booking-client, with the floating promises it surfaced fixed (SEAT-1057).
- Booking-service records per-endpoint timing and the calling actor across the v2 API and the public v1.0 read endpoints used by the renderer (SEAT-1108).
- Out-of-order Flyway migrations are configurable per environment in the editor-service chart (SEAT-1116).
Affected products
editor-service— atomic schema save, optimistic locking, bulk-insert rollback, pricing-zone preservation on save, hardened XML parsing, webhook target validation and response redaction, organisation scoping on pricing and background endpoints, removal of the deprecated SSO decode path, global search with server-side organisation scoping, admin usage summary for the Sales dashboard, organisation auto-login exposed on the admin API, image-generation progress published over Redis.editor-client— canvas layer pill navigation with per-layer selection and a two-way Edit/Prices menu, global search, opening an event in the editor pre-selected, Shift-constrained rotation snap, auto-login toggle in the organisation edit form, Sales dashboard, Preview availability latch with a bounded timeout, selection no longer reordering the canvas, batched outline path edits.editor-client— Pricing mode events moved from a sidebar dropdown into a dedicated collapsible column with assignment filters, shown only when the schema has more than one event; the active event and its edit, preview and playground actions live in the right sidebar, which also reopens the column; thehidden=eventsiframe token now hides that column.booking-service— per-section endpoints serving untransformed grid coordinates for the admin renderer, branding level resolved per render from the owning organisation, organisation scoping on the v2 unselection endpoint.booking-service— the public seatmap endpoint served from a pre-compressed payload cache keyed by event,disableOriginaland branding variant, with a per-replica in-process layer in front of it; the prices endpoint no longer reparses seat JSON per row and is returned compressed.booking-client— admin flat section view (setFlatSectionView,onFlatSectionViewChange), configurable admin hotkeys withHotkeysSettingexported,selectSeatBlockover grid coordinates, per-source section outline styling, “Powered by Seatmap.pro” and interruption overlays.booking-client— numeric price contract: decimals preserved and non-numeric labels reported asundefinedin section and cart price callbacks; numeric cart price-id backfill; removable non-finite-price general-admission entries; name-based section resolution when restoring cart seats; single-section loads trim the price list to the loaded section;labelStyleparsed at ingest.booking-client— pan-limited commits for zoom-to-destination and pinch-end; awaitable zoom-to-fit with correct padding restore; zero-duration and interruption-safe transform animations; section-rotation frame handling, serialization, and finite-angle guard; redraw gated during transform animations and queued concurrent zooms.booking-client— performance at scale: section-indexed seat lookups, spatial index no longer rebuilt on flag-only seat mutations, constant-time key-based seat resolution, and cart-scoped minimap pin drawing (behaviour-preserving).booking-client— WebGL memory and per-frame cost: row vertex buffer uploaded only on change, static background quad and always-zero seat flag buffers no longer re-sent per frame, price-color buffer updated in place, the per-seat selection/hover/loading overlay rebuilt only on change (or at a deselection-animation boundary) instead of a full per-frame seat scan, the “any seat loading” animation check reduced to constant time, and shaders/textures freed on teardown (no leak after destroy) – all behaviour-preserving.db-migrations—V109__schema_optimistic_lock_versionadds theschema.versioncolumn backing optimistic locking;V112__fix_pricing_zone_event_keysrepairs prices assigned across events by a shared pricing zone.converter-service— background level chain emitted alongside the tile grid, tiles encoded as WebP.deployment/helm— JWT signing keys stored in a Secret, with existing keys migrated on upgrade; removal of the deprecated external SSO public-key URL from the editor chart; out-of-order Flyway migrations configurable; watermark kill-switch for self-hosted deployments.
Upgrade
Database migrations run automatically at editor-service startup and need no manual step. Two of them are worth knowing about.
V109 adds the schema version column used for optimistic locking and backfills existing rows.
V112 repairs data: where a pricing zone was shared across events, seats could carry a price belonging to a different event. It creates the missing per-event price rows and rewrites the affected seat_on_event.price_id values. Take the usual database backup before upgrading. The full migration list is in the deployment guide.
JWT signing keys are stored in a Secret
The editor’s RSA signing keys are stored in a Secret named <release>-rsa.
The key generation job runs on upgrade as well as install, and migrates existing key material into the Secret verbatim, so issued tokens continue to verify and no one is signed out. A new keypair is generated only on a fresh install.
After upgrading, delete the now-unused ConfigMap:
kubectl -n <namespace> delete configmap <release>-rsa
Do this only once the editor pods are running and healthy on the new release. Deleting it before the upgrade completes removes the only copy of the keys.
Deployments that supply the keys by some other means, rather than letting the chart generate them, must provide a Secret named <release>-rsa with private-key and public-key entries.
Review any configured webhook targets before upgrading: targets that are not public HTTPS destinations will stop delivering. Self-hosted deployments delivering to an internal receiver must set seatmap.webhooks.allow-private-targets: true.
Clients that save schemas should handle HTTP 409 by reloading and retrying.
Self-hosted deployments that must not show “Powered by Seatmap.pro” should set SEATMAP_WATERMARK_DISABLED, or the booking.config.seatmap.watermark.disabled Helm value, before upgrading.
Existing venue backgrounds keep their current tiles and continue to render; the new level chain is produced the next time a background is processed.
Editor embedders already passing hidden=events in the iframe URL will now find the whole Pricing events column hidden; remove that token to keep the column visible.
Baked backgrounds from 1.67.0 onwards
Backgrounds baked between 1.67.0 and this release are missing their seated section titles, and the fix does not repair them retroactively. They are restored by regenerating the affected schemas, which a global administrator triggers once after upgrading; each background is also repaired the next time it is processed for any other reason.
Booking renderer
npm install @seatmap.pro/renderer@1.70.0
The booking renderer’s reported price values now preserve decimals (for example 12.5 instead of a truncated 12); a non-numeric label continues to report undefined. New admin-renderer surface in this release: setFlatSectionView, onFlatSectionViewChange, selectSeatBlock, clearSelection, selectAllSeats, and the exported HotkeysSetting type. No existing renderer method signatures or configuration options changed.