Claude/x86 graphics performance issue 9rac6j - #601
Conversation
…dows
Registers `@ovurrsl/plugin-warehouse` through `extendPluginDiscovery` rather
than `setPluginDiscovery`, which would replace the whole chain and silently
drop the trees pack registered above it. The panel goes in separately via
`registerEditorHostPanel` — it is host UI, not part of the node manifest.
`allowedDevOrigins` gains the LAN and WAN addresses so the dev server can be
reached from another machine. Note this exposes an unauthenticated editor
with readable source maps to whatever can route to it.
The `dev` script did not run on Windows at all:
next dev --port ${PORT:-3002}
error: '${PORT:-3002}' is not a non-negative number
`${PORT:-3002}` is POSIX parameter expansion. Bun's script runner does not
expand it on Windows, so the literal string reached Next's argument parser.
The `dotenv -e ../../.env.local` prefix was also loading a file that does not
exist, and every entry in `.env.example` is optional — so both go, leaving a
script that runs on either platform.
Also carries the scene-clipboard fallback that lets plugin-contributed kinds
be duplicated (submitted upstream as pascalorg#547). `AnyNode` is a
hand-maintained union of built-in kinds, so a plugin kind can never be a
member and `capabilities.duplicable` cannot be honoured for any plugin —
including the first-party trees pack, which declares it and fails the same
way. The fallback tries `AnyNode` first, so built-in behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…duplicate
Reported as a paste failure:
Failed to paste copied node "pallet_rack_7vp5f3t58sm5iuch"
pattern: "^pallet_rack_[\s\S]{0,}$"
`extractIdPrefix` recovered the prefix by splitting at the FIRST underscore.
`generateId` builds ids as `${prefix}_${customId()}` where the suffix is drawn
from `0123456789abcdefghijklmnopqrstuvwxyz` — it never contains an underscore —
so the prefix is everything before the LAST one.
Built-in kinds never noticed, because none of their prefixes contains an
underscore: `wall_abc` splits the same either way. A plugin kind whose prefix
does — `pallet_rack` — cloned as `pallet_abc`, and the kind's own schema then
rejected it. Duplicate and copy/paste were unusable for that kind, and the
error named the plugin rather than the id helper.
The same three-line helper had been copied into three files, so all three had
the bug: `editor/lib/scene-clipboard.ts`, `core/registry/subtree.ts`, and
`core/utils/clone-scene-graph.ts`. Each now uses `lastIndexOf`. Behaviour for
every built-in kind is byte-identical, which is why the existing clipboard
tests pass unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g GPU disposal
Three defects one debugging session paid for in full:
- saveAsset called crypto.randomUUID unconditionally; it exists only in
secure contexts, so an editor served over plain http on a LAN threw on
the first line of every upload. getRandomValues is not gated the same
way and provides the same uniqueness.
- Two guide-image catch{} blocks swallowed the reason. The message the
user saw ("Could not add that guide image.") was true and useless; the
cause now reaches both the console and the toast.
- DragBoundingBox minted geometry per dimension change and disposed the
previous one in effect cleanup — once per frame during a resize drag,
while WebGPU could still be executing the command buffer referencing
the destroyed buffers, which drops the ENTIRE frame ("Vertex buffer
slot … was not set"). Unit geometry scaled through the mesh transform
and colour-cached materials: nothing is built or disposed mid-drag.
Also carries the clipboard regression test for multi-underscore id
prefixes, and pins plugin-warehouse at the current catalogue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the boom-tip anti-collision sensor and operator step platform, researched against real telescopic conveyor manufacturers (Feifer, Dahan) per the user's reference photos.
…s (0927769) hasSensor / hasPlatform now controllable from the parametrics panel instead of hardcoded always-on.
Adds warehouse:mezzanine — Mecalux multi-tier structural steel platform, structural skeleton only (grid/column/beam/floor). No staircases, gates, railings, or rack-on-mezzanine integration yet.
apps/ifc-converter/next-env.d.ts and bun.lock picked up churn from rebuilding every @pascal-app/* package's stale dist/ against the 1.0.0-beta.1 release commit — needed to unblock the mezzanine deploy (next build failed on missing exports from unbuilt package dists).
Staircases (EN ISO 14122-3), railings derived from openings, gates, stair voids as panel exclusion, plan symbols sharing the 3D calculators, and rack-on-mezzanine load-class checking + 3D clash.
Adds warehouse:live-racking — Mecalux gravity-flow channel. One node is one channel column; bay width and roller length derive from the pallet by catalogue formula. Joins the capacity readout as storage-only depth.
Mezzanine now publishes a real host `slab` per tier, which is the only way `spatialGridManager` will elect it as a support surface — so racks, pallets and conveyors can finally be placed on a mezzanine deck. Host level filtering and pointer-based tier selection come free from the existing support machinery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flow hardware now exists as geometry (brake rollers + drums, centralising strips, retainers, exit beam, end stop) and `variant` finally changes the mesh — FIFO and LIFO were byte-identical before. Adds the floor-set pallet truck level and clad-rack configurations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds per-level SKU on live racking channels and an unconditional plan label, so a layout reader can see which lane holds which reference without selecting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three deploy-only changes; packages/* and dependency pins untouched: - next.config.ts: output 'standalone' so the build emits a bundle that runs without the monorepo's root node_modules (Hostinger only ships the output directory). - package.json: drop the dotenv -e ../../.env.local build wrapper — .env.local is gitignored so the file never exists on the build host, and the app builds with no env vars. - hostinger-server.js: entry file copied to .next/standalone/server.js at build time; chdirs into apps/editor so the standalone output finds .next/ and public/, and passes PORT through untouched so a Unix socket path works as well as a number. Verified locally: clean install, turbo build --filter=editor, and the standalone server serving /, /api/health, /_next/static/* and public/ assets over both a TCP port and a Unix socket. Note: bun resolves the two github git dependencies through the GitHub API tarball endpoint, so the git+ssh pin installs anonymously as long as the repo is public; for a private repo set GITHUB_TOKEN in the host environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Four mezzanine controls offered a choice and changed nothing: the two gate types drew identical boxes, turn90 and turn180 cut identical floor voids, SIGMA silently discarded profile overrides, and hatch2D was populated on all seven floor types but never read. Each now has a real effect or says plainly that it is ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hostinger's Node.js app builder only offers fixed build commands (npm run build), so the root build script now runs the editor-only turbo build plus the standalone completion steps (public/, .next/static, and the hostinger-server.js entry copy) that were previously meant for a custom build command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Hostinger's fixed install step runs npm install, and npm 7+ auto-installs peer dependencies: the plugin packages pin @pascal-app/* peers to '>=0.9.2 <1', which the workspace's 1.0.0-beta.1 prerelease does not satisfy, so npm shadowed the workspace packages with stale published copies under apps/editor/node_modules and the build failed on missing exports. The build host ships bun, so the build script now clears npm's tree and reinstalls from bun.lock, reproducing the locally verified resolution before running turbo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Staircases now carry railings on their open edges, a real landing platform between flights, and a multi-flight layout driven by the landing type. Adds the 4000 mm single-straight-flight exception and the 15-step auto-split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified this time against a copy of the standalone directory moved out of the repo, which is what the host actually deploys — the earlier in-place verification silently resolved modules from the repo's root node_modules, masking two failures: - bun 1.3 defaults workspaces to the isolated linker, and the standalone bundle it produces carries node_modules/.bun symlinks that break once the output directory is moved (Cannot find module 'next'). Install with --linker=hoisted so the bundle contains plain directories. - the custom entry used the programmatic next() API, whose dependency surface (e.g. next/dist/compiled/webpack/webpack-lib) is not part of the file-traced bundle. The entry is now a thin CJS shim that dynamic- imports the server Next itself generates in the bundle, which handles cwd and PORT wiring. The moved-copy smoke test now passes: /api/health, /, page CSS and public/ assets all 200. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Catalogue tiles now ship staircases and pallet gates, so a mezzanine placed from the catalogue can actually be reached. Adds the missing MIXED tile, a step-count picker and safety-zone creation in the accessory editor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bun's hoisted linker drops the execute bit when materializing packages from a warm cache on the build host, so spawning the turbo native binary failed with EACCES. Re-apply execute permissions to node_modules/.bin and the turbo platform package before running the build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Stacked mezzanine decks could not be targeted: the host's pointed-surface election takes the nearest ray crossing, which from a camera above is always the topmost deck. Placement now honours an explicit active-deck selection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five provenance columns — sites.created_by, assignments.granted_by, invitations.invited_by, api_keys.created_by, jobs.queued_by — were NOT NULL with bare foreign keys, whose default rule is RESTRICT. The moment an account had created a site or queued a job, DELETE FROM users failed with a constraint error nothing caught, so the console answered "something went wrong on our side" to a perfectly reasonable request. Provenance is history, not ownership: migration 006 lets all five go NULL on deletion, the way audit_log.actor_user_id always has. The three readers that inner-joined users on those columns become LEFT JOINs — without that, deleting a user would have silently removed every site they created from Sites & Projects, which is worse than the bug being fixed. If the migration has somehow not run, the delete route now maps MySQL's 1451 to a message that says what is pointing at the account and what to do, instead of the generic server error. Verified against the standalone bundle on MariaDB: an admin deleting the account that created the only site and queued three jobs got 500 before, gets 200 after; the site and jobs remain listed with their actor shown as "—".
An administrator approved an access request, got a green toast, and waited for a reply that could not come: the account was created and the invitation issued, but the email never left the building and nothing anywhere said so. send() swallows delivery failures by design — a mail server having a bad afternoon must not roll back the account — but "swallow" was being read as "succeed" all the way up to the UI. deliverInvite now returns whether the message reached the mail server. POST /api/requests/:id/approve carries that back as `mailDelivered` (the account is still created either way) and writes an error-level audit line when it is false; the approve dialog shows a warning naming the Resend button instead of "invited". Resend itself answered 200 after sending nothing, which is the least useful possible behaviour for the one button you press precisely because the first message did not arrive — it now fails with the reason. The likely cause of the original silence is also removed: the SMTP transport was pooled, and an idle pooled socket on shared hosting is dropped by the provider without the pool noticing, so the first message of a session succeeds and a later one fails on a socket that looks alive. At a handful of messages a day a pool buys nothing. Connection, greeting and socket timeouts are set too, so a hung mail server can no longer hold an HTTP request open indefinitely. Verified against the standalone bundle both ways: with SMTP pointed at a dead port, approve returns 201 with mailDelivered:false, the account exists as Invited, resend answers 500 err.mailFailed and the audit trail carries an error line; with SMTP reachable, approve returns mailDelivered:true and both the receipt and the invitation arrive, the latter carrying its /welcome?token link.
…the system audit A five-dimension audit of the console/server system (83 raw findings, top ten adversarially verified, none refuted) turned up one real hole and several things that were quietly wrong. GET /api/scenes/:id and its SSE stream had no identity check at all. The surrounding guard proves a request came from this origin, not that it came from anybody in particular, so a scene id was enough to read the drawing — while /scene/:id, the page rendering the same data, had redirected strangers to sign in since this morning. Both now apply the rule the scene *list* already applied: your own scenes, an admin's anything, and published projects to any signed-in account, since publishing is an administrator approving a project for the whole organisation. The idle timeout could never fire. The console polls /api/auth/session every 30 s while its tab is visible, and that read slid the very window it was reporting — so a console left open on an unattended screen kept itself signed in indefinitely. The poll no longer touches; real work still slides the window through requirePermission, and the idle dialog keeps its own touch endpoint. Two regressions from this morning's migration 006, which let provenance columns go NULL: resolveInvitationToken still INNER JOINed the inviter, so deleting an administrator would have silently invalidated every invitation they ever sent, and the site-scene worker INNER JOINed the site creator, so a site whose creator was deleted would never have got its scene. Both are LEFT JOINs now — the same sweep I did for sites, jobs and API keys, which missed these two because they live outside the directory I grepped. Thirteen more error keys had no entry in resolveApiMessage and rendered as "Something went wrong on our side" — the same class of defect that hid the superuser and delete refusals twice today. All thirteen now say what happened, in both languages, and the enumeration is mechanical so the set is complete. Performance: listUsers computed a correlated COUNT(*) over `scenes` per account whose result the only caller discarded, and which could not use an index because the collation split forces CONVERT() on the join column; it is gone, and the listing is bounded. Migration 007 adds the three missing indexes on lookups that run on pages everyone opens — sites.scene_id (added by 004 with no index, scanned on every publish, unpublish and console load), users.created_at and audit_log.created_at. Verified against the standalone bundle on MariaDB: anonymous read of a scene and of its event stream both answer 401; a viewer reading another account's unpublished scene gets 403 on both, the owner still gets 200, an admin still reads anything, and a published scene stays readable by any signed-in account.
The old install represented racking as a generic item node pointing at asset://procedural/rack with a bounding box; the current build models it as a parametric warehouse:pallet-rack node. The converter rebuilds each procedural item as its plugin node (box folded into bay width, upright height and depth, everything else on schema defaults), repairs the legacy writer's null parentIds from the children arrays, and validates the result with the same apiGraphSchema the API enforces. An unmapped procedural kind aborts the whole migration with a report — a silently skipped rack is a rack missing from the customer's floor. The sqlite side discovers the legacy table instead of assuming it, and --list prints a per-scene inventory for planning the bulk migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The late legacy vintage stored equipment as library items whose models lived in the desktop app's own IndexedDB, so the converter gains their mappings: asset://rack becomes a pallet rack, the euro-pallet pair become pallets (loaded ones carry carton cargo), the flat wire mesh conveyor becomes a roller conveyor, and warehouse ids written under old prefixes (palletrack_) move to the prefixes the current schemas accept. Handles the owner chose to keep as items are reported as invisible instead of silently passing. The hard lesson is the parse step: setScene stores nodes verbatim, so a synthesised node missing a defaulted field reaches kind systems in a shape editor-created nodes never have. A pallet without supportSlabId crashed the load the moment a slab stood under it — and the editor then autosaved the empty fallback over the scene. Every converted node now runs through its owning schema via parseNodeWithDefaults (exported from the API validation module so there is one source of truth), which materialises every default and keeps that class of failure impossible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The dependency was pinned to 9e1b525 (31 July). Every plugin commit since then — drive-in racking, M7 longspan, M3 shelving, the mezzanine tier system, the conveyor family joining collective instancing, the panel refactor, the solo-mode layer-mask fix and the placement-click fix — was invisible to the app, because `bun install` resolves the pinned commit no matter what the plugin repo does. Moved to b16d1c8, the current head. Peer dependency ranges are unchanged between the two commits, so the lockfile edit is the resolved commit and nothing else. Not verified by a local install: this session's GitHub proxy scope excludes pascalorg/*, so `bun install` cannot resolve @pascal-app/plugin-trees and aborts before writing. The two lockfile lines were edited by hand to the form bun itself produced when it cached the new commit (ovurrsl-plugin-warehouse-b16d1c8). The build must be run before this is deployed. Carries a breaking change from 7c25751: warehouse:live-racking, warehouse:longspan and warehouse:m3-shelving are renamed to live-rack, longspan-rack and m3-rack. Saved scenes holding the old kinds need checking before this reaches production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The deploy template still read 2.10.0 while the live bundle reports 2.12.0 — the last two releases were versioned in the deployment repository by hand, so a workflow-built bundle would have shipped a lower version than the one it replaced. 2.13.0 is this bundle: the warehouse plugin moved from 9e1b525 to b16d1c8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Today's release took four attempts, and every one of them failed on something that is invisible from the code: workflow_dispatch returning 404 because the workflow file was not on the default branch, an empty DEPLOY_TOKEN, and then a token that could read the deployment repository but not write to it. None of those are findable by reading the diff, and all three will happen again to whoever runs the next release. YAYINLAMA.md records the chain (site ← deployment repo ← this repo ← plugin), the one-time prerequisites, the update and publish steps, what to verify, and a table mapping each of those failures to its fix. It also writes down the two things that lose data: renaming a plugin kind while scenes hold the old name, and making the deployment repository public while a .env sits in its root. The lockfile step points at the Relock workflow rather than `bun install`, because a sandbox cannot compute the sha512 of a GitHub tarball it is not allowed to fetch — which is the reason that workflow exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
b16d1c8 is two days old. Since then the plugin fixed the two things reported as "warehouse equipment behaves on its own whim": its nodes never entered the host's shadow-frustum fit, so shadows cut off at an invisible line and the whole scene's shadow quality shifted whenever a rack was selected; and its materials ignored the Display menu, so Render and Textures changed host geometry while racks and pallets stayed as they were. Cargo also moved into the collective pool, which takes a 3,000-pallet warehouse from 6,000 draw submissions a frame to 80. 1c73ce2 is the head of the plugin's main at its v0.1.1 tag. The bundle goes to 2.14.0 rather than a patch: the appearance work is a fix from the plugin's side, but from the site's side the same scene renders differently after this, which is a release worth naming. bun.lock is left to the Relock workflow — this environment cannot compute the sha512 of a tarball it is not allowed to fetch. The trailing comment in relock.yml is what fires it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Warehouse equipment follows the editor's Display → Units setting now. It previously printed every dimension in metres regardless: an Imperial user read a host wall in feet and the rack beside it in metres, and in the stats tab an area in ft² sat next to an elevation in m on the same row. Figures quoted from EN ISO 14122-3, EN 15620 and the Mecalux catalogues stay in the unit their source publishes — converting a cited 220 mm riser to 0'9" would invent a precision the document does not have. Also fixes an inspector crash: the drive-in panel called a hook below its early return, so selecting a lane after the panel had rendered empty threw "Rendered more hooks than during the previous render". Nothing changes for a metric user; every conversion is the identity on that path.
The plugin pin moved to v0.1.2 (49b2f16) in the previous commit, but the deploy template was left at 2.14.0 — the bundle would have been published claiming to be the release it replaces. v0.1.2 is the Units work: every readout in the plugin's panels now follows the host's Units setting instead of printing metres regardless, including the case where one row's area was converted while the elevation beside it was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
`49b2f16` → `fd22b04`. Editör kaynağında değişiklik yok. Bu dal, üretim bundle'ının gerçekte üretildiği dal: `deploy-bundle.yml` `push: branches: [main]` diyor ama `main`'in `apps/editor/next.config.ts`'inde `output: 'standalone'` yok, o yüzden main'den build alındığında `.next/standalone/apps/editor/server.js` hiç oluşmuyor ve "Assemble the bundle" adımı düşüyor. Şimdiye kadarki başarılı deploy'ların hepsi bu daldan elle `workflow_dispatch` ile koşmuş. Pin bu dala gelmediği sürece eklenti düzeltmeleri canlıya çıkamıyor: eklenti uygulamanın içine derleniyor, yani bir eklenti sürümü siteye ancak yeniden derlemeyle ulaşıyor. Atlanan iki sürüm: **v0.1.3 — kirli bayrağı tüketimi.** Host'un `FloorElevationSystem`'i kirli düğüm bayrağını yalnız `def.geometry`/`def.system` bildirMEyen kind'lar için düşürüyor; bildirenler kendi bayraklarını kendileri tüketmek zorunda. Eklenti `def.system` bildirip hiçbir yerde `clearDirty` çağırmıyordu. Sahne yüklenirken her slab ayak izine değen her `floorPlaced` düğümü kirletiyor — depodaki rafların tamamı — ve bayrak bir daha düşmüyordu. `dirtyNodes.size === 0` on iki host sisteminin ortak erken çıkışı olduğu için sahne açık kaldığı sürece hepsi kümenin tamamını her karede geziyordu. **v0.1.4 — raf başına tekrarlanan hesaplar.** Şekil anahtarı artık düğüm nesnesine memoize (çağrı başına 12,7 µs → 0,31 µs) ve kademeli mount kapısı kolektif çizen on üç kind'ın hepsinde, yalnız `warehouse:pallet-rack`'te değil. Kullanıcının makinesinden gelen taban ölçümü (pin `49b2f16`, yani düzeltmesiz): 41,3 saniyede 22 kare, p50 kare 2098 ms, ana iş parçacığı %98,9 bloke. peerDependencies bloğunun v0.1.4'te birebir aynı olduğu doğrulandı. bun.lock elle güncellendi: `bun install` bu ortamda `pascalorg/plugin-trees` tarball'ında 403 alıyor. Aynı SHA üç yerde geçiyor, üçü de değişti. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
49b2f16 -> fd22b04. Editör kaynağında değişiklik yok. Pin main'e de kondu (#2) ama main'den bundle üretilemiyor: next.config.ts'inde output: 'standalone' yok, o yüzden .next/standalone/apps/editor/server.js hiç oluşmuyor ve "Assemble the bundle" adımı düşüyor. Üretim bundle'ı bu daldan elle workflow_dispatch ile üretiliyor, o yüzden pinin canlıya ulaşması için burada olması gerekiyor.
Both MFA routes consulted the lock only on the FAILURE path: a wrong code called `registerFailure` and the response then said `account_locked`. A correct code never met the lock at all — it was verified, `clearFailures` ran, and `mfa_pending` was cleared. So a locked account still signed in, and the lock counted misses and announced a state it never enforced. The password step does check first (`auth/signin/route.ts:47`), so the gap was the second factor alone — precisely the step whose reason to exist is that a stolen password should not be enough. An attacker holding the password gets a session in `mfa_pending` and can then guess the code with no effective budget, which is what the comment above `POST` already promised would not happen. `readLockState()` existed in `panel/lib/auth/lockout.ts` for exactly this and was not called anywhere; both routes now call it immediately after the session check and refuse before spending a code. `ovurrsl/panel`'s copies of both routes already do this — the guard was dropped while vendoring, not designed away, so this restores the upstream behaviour rather than inventing a policy. Recovery codes matter more here than TOTP: they are the credential that survives losing the authenticator, so an unbounded guessing budget against them is the weakest point in the whole second factor. Not covered by a test: this repository has no harness for API routes (every existing test is a pure-function test — `scene-api-security`, `env-file`, `graph-schema`, `mail-template`), and these routes need a database and a live session. A real guard would assert that a locked account is refused even when the submitted code is correct. Verified here by reading the order of operations and by `tsc --noEmit` over `apps/editor` (no errors in either file). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The sync would have created `scenes-tab.tsx` and `guides-tab.tsx` upstream and
overwritten `console-tabs.ts` to register them, while `ROUTES` (create-new is
`false`) declined to push the endpoints behind them. The standalone console
would have gained two rail entries whose fetches 404.
The endpoints cannot exist upstream, which is why declining them is right and
the tabs are what is wrong:
/api/guides imports the editor's own `@/lib/guides-content` and
`@/lib/scene-api-security` — absent in the console repo,
so the route would not even build there.
/api/admin/scenes reads a `scenes` table. No migration in the synced set
creates one; `004_site_scene.sql` only adds
`sites.scene_id`. The console's schema has no scenes.
So all three files join `EDITOR_OWNED`. Verified with `--check` against a
checkout of ovurrsl/panel: 133 planned actions before, 130 after, and the
three named files gone from the list.
The cost, stated plainly: changes to the SHARED tabs' metadata in
`console-tabs.ts` no longer reach upstream either. That is what a file which
must legitimately differ between two deployments costs. Splitting the list
into a base the console owns and an extension the editor adds would remove the
conflict properly; the comment says so, so the next person does not read this
as the intended end state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
Verified by applying the sync to a real checkout of ovurrsl/panel and running
that repository's own checks. Its test suite passed with all of these broken —
`tsc --noEmit` is what caught them, so it is the check that matters when this
list changes.
`tab-content.tsx` imports both `guides-tab` and `scenes-tab` and switches on
their names. Holding the tabs back while pushing their only caller turned a
missing feature into a repository that does not compile:
tab-content.tsx(4,27): Cannot find module '@/components/console/guides-tab'
tab-content.tsx(32,10): Type '"scenes"' is not comparable to type
'"audit" | "integrations" | ... | "users"'
`api/health/route.ts` is the same shape one layer down — the editor's copy
imports `@/lib/auth/db` and `@/lib/scene-store-server`, neither of which exists
upstream. It is an existing upstream path, so `ROUTES` overwrote it.
Test files now never cross. The two repositories run different runners — this
one is on `bun:test`, the console on vitest — so `mail-template.test.ts` landed
upstream as a file vitest does not collect (its config globs `tests/**`) and
`tsc` cannot resolve (`Cannot find module 'bun:test'`). The console's own suite
lives in `tests/`, outside every mapping here, so nothing in this direction can
reach it either way.
After: 127 planned actions. In the panel checkout, `npm run typecheck` clean and
`npm test` 59/59.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The console repository is the home of the console now, so the direction that matters day to day is inward. `--pull` adds it, inverting the existing tables rather than declaring its own: a mapping added for one direction is a mapping the other gets for free, which is the only way two directions stay honest. Inverting is not a swap. The console's tree nests where the editor's does not — `src/app/api` and `src/app/globals.css` both live INSIDE `src/app` — so walking `src/app` naively drags the console's endpoints into `app/(panel)/api/` and its stylesheet into `app/(panel)/globals.css`, both wrong and both silent. Longest source path first, and the first pair to claim a file keeps it. `EDITOR_OWNED` needed no second list: it holds console-side paths, so it is the origin on pull and the target on push, and it means the same thing either way — the editor's copy is the authority. `create` is dropped on pull. Its `false` on `ROUTES` protects the CONSOLE from receiving editor-only routes; the mirror risk, the editor receiving a console route it lacks, is not a risk but the point. ## The asymmetry this surfaced A dry run against the real console reported one file, `console-shell.tsx`, while the push direction called the same pair in sync. That contradiction is the signature of a real defect: the forward rewrite is LOSSY. `@panel/x` and a literal `@/x` both leave as `@/x`, so the inverse cannot tell them apart. The file held a comment naming `@/lib/escape-layers`; push left it, pull would rewrite it, and the two directions would have "fixed" each other forever. Fixed at the source — in the editor that module IS `@panel/lib/escape-layers`, so the comment is now true as well as stable, and both directions report clean. The constraint it implies is written down where the rewrite lives: a synced file under `apps/editor/panel/` must never contain a bare `@/`, in an import or in prose, and the way that mistake shows up is exactly the contradiction above. ## Verified against the real repository Not a dry run only. With ovurrsl/panel checked out at its merged `main`: - both directions report in sync, exit 0 — the round trip is stable - a change to `src/lib/cn.ts` plus a NEW `src/lib/__pull-probe.ts` importing `@/lib/cn`: pull placed both under `apps/editor/panel/lib/`, and the new file's import arrived as `@panel/lib/cn` - probes removed, both repositories clean, both directions in sync again `biome check .` clean. `bun test` unchanged from base (the one failure is the pre-existing `graph-schema` plugin-kind case, which fails on the base branch identically). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The console repository is the home of the console, so the automatic direction is inward. `pull-panel.yml` runs the sync with `--pull` hourly (and on demand, and on a `panel-updated` repository dispatch the console can fire once it has a token), commits to the integration branch, and stops there. Gated on `bun run check-types`, the same command CI runs. Pushing straight to the branch is what makes this automatic, and automatic without a gate means one console commit that does not compile here quietly breaks the branch the bundle is built from. Fails red, pushes nothing. A scheduled run starts on the default branch, so the branch to update is named rather than inherited: `vars.INTEGRATION_BRANCH`, falling back to the branch it is today. Renaming it is a repository-variable edit, not a workflow edit. ## Why the outbound sync is now manual only Both directions on `push` would give one file two masters: a change made in the console flows here, the outbound sync fires on that very commit and pushes it straight back, and the two spend the day answering each other — whichever ran last looking right. It keeps `workflow_dispatch` because it is still the tool for seeding the console after a change had to be made here, which the comment now says. ## And a bug that would have started with the first merge `gh pr view <branch>` resolves closed and merged pull requests too, so the plain existence check would have edited the body of the pull request just merged and never opened a new one — while the force-push to `sync/from-editor` had already landed. Files would move; nothing would ask anyone to look. It now asks for the state and only edits when it is OPEN. Both files parse; `biome check .` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The last manual link. The warehouse plugin is a git dependency pinned to an exact sha and it compiles into the app, so a plugin release reaches the site only when this repository moves the pin AND the bundle is rebuilt. Doing that by hand is what left a set of freeze fixes sitting unreleased while the plugin's main had moved on and production had not. `bump-plugin.yml` compares the pin with the plugin's head, moves it, relocks with a real `bun install` (the lockfile records the resolved tarball's sha512, which is why `relock.yml` exists at all), type checks, and commits. Needs no secret: the plugin repository is public, so both reading its head and resolving the dependency work with nothing configured. Gated on `bun run check-types` for the reason an exact pin exists — a plugin release that does not compile against this editor should stop here, visibly, rather than reach the branch the bundle is built from. ## The handoff, which is not optional Both this and `pull-panel.yml` end by dispatching `deploy-bundle.yml` explicitly. A push made with GITHUB_TOKEN starts no workflow runs — `relock.yml` already says so in its own header — so without the dispatch the pin moves, the console lands, and the site never rebuilds. The chain would look wired and stop one step short, which is the same shape as the failure this whole change set is about. Verified against the real files rather than by reading: the pin grep returns `49b2f16…` from `apps/editor/package.json`, `git ls-remote` returns the plugin's head, and the `sed` rewrites exactly the one occurrence. Both workflow files parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
… mirror upstream into `main` Four workflows disagreed about which branch was the real one — `ci`, `mcp-ci` and `deploy-bundle` watched `main`, `relock` watched the working branch, `sync-panel` watched both. That disagreement is not cosmetic: it is why a pin bump merged to `main` never deployed (the branch it landed on could not build, because `next.config.ts` there carries no `output: 'standalone'`), and why a pull request against the branch all the work happens on gets no CI at all. They now all name `integration`, and `main` becomes what a fork's `main` should be: a clean mirror of `pascalorg/editor`, no local commits, ever. ## Why that split, and why `integration` is the default branch A mirror with nothing of ours on it can only fast-forward, so taking upstream never conflicts and never needs a decision. Every conflict then has exactly one place to happen — the pull request from `main` into `integration` — instead of being spread across whichever branch someone last merged into. `integration` has to be the DEFAULT branch for this to work at all, and that is not a preference: GitHub runs scheduled workflows only from the default branch. Leaving `main` as the default while emptying it of our workflows would have silently killed every schedule here — the console pull, the plugin bump, the upstream check — while the files sat on a branch no scheduler reads. ## mirror-upstream.yml Fast-forwards `main` to `upstream/main` daily, then opens one long-lived pull request into `integration` that accumulates whatever upstream has added. It refuses to force. A `main` that cannot fast-forward is a `main` somebody has committed to, and erasing that quietly is worse than a red job. ## relock.yml Its self-triggering hack existed because dispatch needs the file on the default branch and the working branch was not it. It is now, so the hack is gone and it is dispatched like anything else. The trailing marker comments stay as a record of which relocks were run that way, not as a mechanism. Routine plugin bumps no longer come here at all — `bump-plugin.yml` moves the pin and relocks in one job. All ten workflow files parse; `biome check .` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
Depolar arası otomasyon + ikinci faktörde kilidi gerçekten uygula
Was fd22b04.
`pull-panel` decided whether the console had moved by reading a bare `git status --porcelain`. That is never empty in this job: `tee pull.log` writes an untracked file and the `actions/checkout` of ovurrsl/panel leaves an untracked `panel-upstream/` directory, and .gitignore covers neither. So `changed` was true on every run. A run where the console had not moved still installed and type-checked, then reached `git commit` with nothing staged — which exits non-zero. Every hourly run against an unchanged console would have gone red, and the real signal would have been buried in the noise. Scope the check to the three paths the commit actually stages, so the two agree by construction. Also `set -o pipefail`: the step's default shell is `bash -e`, not `-eo pipefail`, so `tee` was reporting its own exit status over a crashed sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The pull request step was gated on `moved` — did this run push the mirror. `main` can also be advanced by hand, and when it is, no run ever pushes, so no pull request is ever opened and the update sits on `main` unseen. That is not hypothetical: 42 upstream commits accumulated there today with nothing pointing at them. Gate on the question that actually matters instead — is the integration branch behind the mirror — and carry the count into the body. Fetch the integration branch explicitly rather than relying on checkout's refspec, so the count has both sides to compare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
UPSTREAM.md still described the plugin bump as a manual pin edit and the console as an outbound sync, both of which stopped being true when `bump-plugin` and `pull-panel` landed. A topology document that is wrong about which direction the console flows is worse than none. Corrects those rows, records the two-branch model and why the integration branch has to be the default one, and adds the two conflict rules learned from the beta.4 trial merge that were costing a rediscovery each time. OTOMASYON.md is the same picture in plain language for whoever operates this rather than edits it: what runs when, which secret each workflow needs, what the smoke tests guard, and where to look when a link goes quiet. Every trigger, secret and gate in it was read out of the workflow files and checked a second time rather than recalled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
`AGENTS.md` here is upstream's file, and `CLAUDE.md`, `GEMINI.md` and the copilot instructions are all symlinks to it. It is accurate about the code and completely silent about this fork: it does not say that `integration` is the default branch, that `main` is a pure mirror nothing may be committed to, or that the plugin pin and the console arrive here on their own every hour. An agent opening this repository therefore reads instructions written for `pascalorg/editor` and acts on them. The expensive version of that mistake is a commit on `main`: the mirror stops being a mirror, `mirror-upstream` refuses to force over it — correctly — and every later upstream take needs a hand-unwind first. A short block at the top, ahead of upstream's title, with the four facts that cost something when unknown and pointers to `OTOMASYON.md` and `UPSTREAM.md` for the rest. It conflicts on every upstream merge because upstream has no equivalent, so the resolution is recorded in UPSTREAM.md alongside the other per-file rules: keep the block, take their body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ec6f460. Configure here.
| const session = await getSession() | ||
| if (!session || session.mfaPending) { | ||
| return new Response('unauthorized', { status: 401 }) | ||
| } |
There was a problem hiding this comment.
Jobs stream skips admin gate
High Severity
GET /api/jobs/stream only checks for a signed-in session, while GET /api/jobs and the cancel/retry routes require admin_access. Any authenticated non-admin can open the SSE stream and receive the full job queue, bypassing the admin-only gate the polling API enforces.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ec6f460. Configure here.
| /** GET /api/requests — the pending strip above the user table. */ | ||
| export const GET = handler(async () => { | ||
| const guard = await requireSession() | ||
| if (!guard.ok) return fail('unauthenticated', 'err.sessionExpired') |
There was a problem hiding this comment.
Requests list lacks permission check
High Severity
GET /api/requests only requires a session, so any signed-in account can list pending access requests with full name, email, username, department, and notes. Approve/reject and the users list require stronger permissions (edit_users / admin_access), so this endpoint leaks colleague PII to editors and viewers who cannot reach the Users tab in the UI.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ec6f460. Configure here.


What does this PR do?
How to test
Screenshots / screen recording
Checklist
bun devbun checkto verify)mainbranchNote
High Risk
Touches authentication, sessions, MFA, admin APIs, and production deploy paths; misconfiguration of secrets or scene authorization could block deploys or expose data.
Overview
This PR turns ovurrsl/editor into a production fork: work moves on
integration,mainstays an upstream mirror, and automation keeps the warehouse plugin, console panel, and deploy bundle in sync.CI/CD and release adds
deploy-bundle(standalone build, MySQL smoke tests, force-push toovurrsl/digitaltwin),mirror-upstream/upstream-check, hourlybump-pluginandpull-panel, manualsync-panelandrelock, plus.github/deploy/(package.json,setup-native.mjsfor argon2 symlinks, Hostinger README). Existing workflows (ci,mcp-ci) now targetintegrationinstead ofmain.Documentation adds
OTOMASYON.md,UPSTREAM.md,YAYINLAMA.md, and a fork warning block at the top ofAGENTS.md.Application wires the vendored
@panelconsole: routes underapp/(panel)/(sign-in, MFA, console tabs), public(public)/guides/changelog, and a largeapp/api/surface (auth, users, roles, sites, jobs, audit/logs, admin scene publish/manage). Scene APIs gainauthorizeSceneRead/authorizeSceneMutationand owner-scoped listing;/api/healthreports MySQL backend and auth status.Reviewed by Cursor Bugbot for commit ec6f460. Bugbot is set up for automated code reviews on this repo. Configure here.