Main - #754
Conversation
Everything a visitor or a casual DevTools glance would see now reads digitaltwin instead of pascal. Internal identifiers stay untouched so upstream merges and the plugins keep working: @pascal-app/* package names, the x-pascal-scene-token header, PASCAL_* env vars, DOM data attributes, CSS class names, and persisted keys (localStorage, pascal:editor/floorplan, plugin ids, GLB userData) are all deliberately left as-is. - layout.tsx gains page metadata (there was none): title "DigitalTwin Editor". - terms/privacy: entity, product name and support email rebranded; the email is a TODO placeholder pending a real address. - plugins panel "Create a Pascal plugin", the WebGPU fallback message, and the material-picker source label now say DigitalTwin (the material source id stays 'pascal' — it is persisted). - console registry prefix [pascal:registry] -> [digitaltwin:registry]. Verified in the compiled bundle: the tab title is "DigitalTwin Editor", the old console prefix is gone, and /terms and /privacy contain no "pascal". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Trucks can now pin their duty-cycle source and target slots from the panel; a pin that goes stale falls back to the deterministic draw and the panel says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds authentication so scenes belong to the user who creates them, using the database and infrastructure already in place — no new runtime dependency. Password hashing is node:crypto scrypt; sessions are opaque tokens stored hashed, delivered as an httpOnly SameSite=Lax cookie whose Secure flag follows x-forwarded-proto (the Hostinger proxy terminates TLS). Auth uses its own small mysql2 pool from the same PASCAL_MYSQL_URL and creates its two tables (users, user_sessions) at boot via instrumentation.ts, mirroring the scene store. - lib/auth: db (pool + migrate + authAvailable), password (hash/verify), session (token + cookie + getSessionUser), service (register/login/ session/logout + DIGITALTWIN_ADMIN_EMAIL admin seed), guard (per-scene mutation authorization). - API: POST /api/auth/register|login|logout, GET /api/auth/session — origin-guarded (no scene token), login rate-limited, force-dynamic. - Scenes: POST stamps ownerId from the session (401 when signed out); GET and /scenes list filter to the caller; PUT/PATCH/DELETE authorize against the scene's owner (403 across users, admin may edit any). Pre-existing null-owner scenes become unowned — absent from user lists, still openable by direct URL; a later admin panel manages them. - Client: SessionProvider + useSession, a dependency-free auth dialog, an AuthMenu on the editor and /scenes; create/save/save-as and the autosave path open the dialog when signed out and on a 401. - MySQL-only: with no database (SQLite dev) auth is disabled — endpoints report it, creation stays open and unowned, nothing regresses. - health reports auth: ok|disabled. Verified against MariaDB end to end: register sets a Secure cookie under forwarded https; session round-trips; a signed-in POST stamps owner_id; the list shows only the caller's scenes; a second user gets 403 deleting the first's scene; logout returns 204 and clears the session. Unit tests cover hashing and the cookie-secure logic; an env-gated integration test covers register/login/session/admin-seed. 306 mcp tests still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
An admin-only /admin page (any other visitor gets a 404) that lists users and scenes and manages access, built on the role column and session already in place. - Users table: email, role, scene count, join date; promote/demote a user's role. An admin can't demote themselves out of the panel. - Scenes table: every scene with its owner; reassign a scene to any user or make it unowned; one-click adopt of all legacy null-owner scenes to the admin — the migration path promised when ownership landed. - API under /api/admin/* is guarded by role admin (403 otherwise); the page redirects non-admins to a 404. - AuthMenu shows an Admin link only to admins. Verified against MariaDB: a non-admin gets 404 on /admin and 403 on the APIs; DIGITALTWIN_ADMIN_EMAIL makes the first account admin; promote, self-demote guard, adopt-unowned, and scene reassignment (valid, invalid owner, make-unowned) all behave. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The variables typed into the hosting panel are the last place the old project name was still visible. Every configuration value is now read as DIGITALTWIN_<NAME>, falling back to PASCAL_<NAME> so a running deployment keeps working until its panel is updated. A shared readEnv() does the two-name lookup and treats a blank value as unset — control panels save an empty field rather than omitting it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The runner has no ssh key, so `bun install` fails to clone the two plugins pinned as git dependencies — every job dies before it reaches a test. Both plugin repositories are public, so rewriting the transport to https resolves the same commits with no credentials. This rewrites transport only: the lockfile still records the ssh URL, so --frozen-lockfile stays satisfied and the pinned commits do not move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
This reverts commit 20761bd.
The ssh form cannot be cloned on a CI runner, which has no key, so every job died in `bun install` before reaching a test. The sibling plugin in the same file uses the `github:` shorthand and resolves fine in the same run. Same repository, same commit — only the transport changes. The lockfile still needs regenerating: `bun install` resolves `github:` specs through api.github.com, which this environment's egress policy blocks, so it could not be updated here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
A refused connection arrives as an AggregateError whose own message is empty, so the boot log printed a bare "AggregateError:" over a stack through minified chunks — naming neither the host, the port, nor the reason. A wrong port cost a live deploy and a log read to find. Unwrap the aggregate and name the misconfiguration each driver code points at, so the log line is the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Some hosting panels drop their environment variables on redeploy, and because the database is required the app then refuses to start. Settings can now come from a file as well: `.env` beside the server, or `~/.digitaltwin.env`, which is the one that survives a release since the deployed directory is replaced wholesale. A real environment variable always wins, so a configured panel keeps precedence and nothing changes for a working deploy. The boot log names the files read and how many settings each supplied, never the values. The publish workflow now carries an existing `.env` across instead of force-pushing over it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Architects export from Revit or ArchiCAD as IFC, and until now the only way to get one into the editor was the separate converter app: convert there, download JSON, load it here. The conversion package already existed — this wires it into the editor as one button. Conversion runs in the browser. web-ifc is a WASM parser and a model is routinely tens of megabytes, so uploading the file first would buy nothing. Only the converted graph is posted, through the same endpoint as "Create new scene", so an import is owned by the signed-in user and stored like any other scene. The converter and its WASM load on first use, keeping ~1.5 MB out of the initial page for everyone who never imports a model. Only the two browser blobs are copied into public/; the node build would be dead weight in the deployed bundle. IFC support is upstream's own early alpha — exports vary wildly and elements can land wrong or not at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
An AI assistant can now edit scenes through the same operations the editor uses, over /api/mcp. The MCP tools have no notion of a user: left alone they write scenes with no owner, which appear in nobody's list yet can be opened and changed by anyone holding the link — a hole straight through the ownership rules. So access is per person. A bearer token, issued from the admin panel, resolves to a user, and a wrapper binds the scene store to them: writes are stamped with their id, lists are confined to what they own, and someone else's scene reads as missing rather than forbidden so an agent cannot probe for ids. Admins get no bypass here. They can already reach every scene through the panel; letting an agent inherit that would mean one leaked token edits the whole installation. Only the sha256 of a token is stored, as for sessions, and granting again replaces the previous token so access removed from a machine cannot be resurrected by an older copy. Sessions hold the agent's working scene between requests, dropped on idle and capped, since the host is one long-lived process. Not yet exercised against a live client — the endpoint compiles and the panel wiring is in place, but the round trip is untested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
This reverts 02da3fb. The endpoint cannot work as written, and the reason is structural rather than a bug to chase. MCP's tools edit through the live scene store, and that store is a 'use client' module. Bundled into Next's server graph it is replaced by a client-reference stub that throws on every call — the built output literally contains `throw Error("... is on the client")`, which is the `getState is not a function` seen at runtime. Marking the packages external is the documented escape, but Next rejects it: the same packages must be transpiled for the editor UI, and `serverExternalPackages` and `transpilePackages` cannot both claim them. Carrying it meanwhile costs something real: a dead /api/mcp route and a migration creating an mcp_tokens table on a live database for a feature that does not exist. The work is preserved in 02da3fb and restores with `git revert` of this commit once the packaging question is settled — either vendoring the packages so Node loads them outside the bundle, or running MCP as its own process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
plugin-warehouse was switched to private, so every job dies at `bun install` with a 404 on its tarball — GitHub's answer to an unauthenticated request for a private repository. Back to the git+ssh pin, and a key rather than a token. The `github:` shorthand fetches through api.github.com, and bun has no documented way to authenticate that request — the tracking issue is still open. bun does shell out to `git clone` for a git+ssh spec, so an ssh key works through git's own machinery. Its url-rewriting does not: a `url.insteadOf` rule has no effect on bun's clones, which I verified before abandoning that route. Needs a read-only deploy key on plugin-warehouse, with the private half stored as the PLUGIN_SSH_KEY secret. Without it the step says so and carries on, so the failure names its own cause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Saving any scene containing a plugin node — every warehouse object, for one — failed with 400. The API validates each node against core's AnyNode, and AnyNode is a hand-maintained union of the HOST's kinds: by construction it cannot know a plugin's. The warehouse plugin's own source says exactly this and points at the answer — "the registry validates against def.schema at runtime." Do the same at the boundary: kinds claimed by a plugin manifest are validated against that plugin's schema, and everything else still faces AnyNode, so unknown kinds and malformed plugin nodes are refused as before. The plugin barrels already run server-side during SSR, so importing them in a route adds no new constraint. Proven against the built bundle on MySQL: a pallet built by the plugin's schema saves with 200, loads back, renders in the editor, and the editor's own autosave of that graph succeeds — the exact path that returned 400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The console app's lib, components and migrations, copied under an isolated @panel alias so they cannot collide with the editor's @/*. Nothing imports them yet — routes, session bridge and the db-env shim land as separate steps.
The console app (ovurrsl/panel) now lives inside the editor: its screens mount under app/(panel) and its API routes under /api, all its code stays under the isolated @panel alias so a re-sync from its own repository cannot mix with editor code. `/` routes by session state — sign-in, 2FA, forced password change, console — and the editor moved to /editor with /scenes and /scene/[id] unchanged. Identity is the console's: argon2id passwords, TOTP, invitations, lockout, its own sessions table. The editor's account machinery (register/login routes, scrypt, the sign-in dialog) is gone; its getSessionUser() now validates the console session and folds the permission model down to the editor's two roles, so scene ownership, guards and the scenes admin keep working with console ULIDs as owner ids. A half-open session (2FA pending, password change due) counts as signed out. Portability shims, worth upstreaming to the panel repo: DIGITALTWIN_* database variables honoured before DATABASE_*; utf8mb4_0900_ai_ci and CAST(... AS JSON) and the functional index in 002 replaced with MariaDB-safe equivalents — shared hosting runs MariaDB, which has none of the three. Proven locally end to end against MariaDB: fresh migrate + seed, / → signin, Admin/Admin → forced password change (policy checks all pass) → console overview live, then /scenes under the same cookie, scene created with owner_id = the console admin's ULID. Not deployed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Rack levels are now one table — each row a level with its clearance and its type. Fixes three measured bugs: the top level's clearance was never editable (rows counted fitted levels, not levels+1), a level that did not fit was hidden so its clearance could not be reduced to make it fit, and levelClears was never trimmed so overrides reappeared when the level count went down and back up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
plugin-warehouse went public, which changes the transport story: bun resolves any GitHub https/github: spec through the api.github.com tarball endpoint, which now needs no credential — so the ssh deploy-key steps come out of all four workflows and the pin becomes git+https. The lockfile cannot be regenerated from the sandbox: it records each GitHub tarball's sha512, and a locally-built tarball's hash would never match GitHub's bytes — verified by diffing what a local regeneration wrote. A one-shot Relock workflow runs `bun install` on a real runner and pushes the corrected bun.lock back to the branch instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The dispatch endpoint only recognizes workflows present on the default branch; a path-filtered push trigger reaches the same one-shot effect.
A redeploy is now the entire upgrade. At boot, ensureConsoleSchema() recognizes where the database is in its history and brings it forward: the editor's old auth tables are renamed aside untouched, the console migrations run (tracked in schema_migrations), a settings row appears with 2FA optional, and every legacy account is carried over — Admin role preserved, scenes re-owned to the new ULID, and a temporary password printed once to the runtime log, since scrypt hashes cannot become argon2 ones and the log is where this deployment's operator reads. Re-runs are no-ops at every step. Turbopack requires an externalized native package under a build-specific hashed alias it never creates; setup-native.mjs, run as the bundle's build step right after npm install on the host, scans the chunks and symlinks each alias to the real @node-rs/argon2. Rehearsed against a replica of the live database: legacy tables set aside, three migrations applied, the account migrated, sign-in with the logged temporary password lands in firstSignIn with Admin permissions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
# Conflicts: # apps/editor/package.json # bun.lock
…ed console The console was written against its own looser Biome/tsconfig; this repo runs noUncheckedIndexedAccess and a stricter Biome profile, so the vendored sources needed a formatting pass plus guards on indexed access. Panel-only hook-dependency pinning is exempted via a biome.jsonc override instead of being rewritten.
Structure / Furnish / Zones → Structure / Furnish / Assets / Zones. Kurulu eklentilerin kattığı nesneler Furnish'ten çıkıp kendi listesine taşındı; raf dolu bir hol yüzlerce satır ve host mobilyasıyla aynı listede ikisi de kayboluyordu. Üyelik kind'ın adına değil kim kaydettiğine bakıyor: isPluginContributedKind registry'ye soruyor (getNodePluginId zaten her kind'ın arkasındaki eklentiyi tutuyor), dolayısıyla sonradan eklenen bir eklenti buraya dokunmadan Assets'e düşüyor ve host koduna warehouse: gibi bir önek yazılmıyor. Helper core'da, isNodeKindEnabled'ın yanında. Assets furnish kategorisinden önce test ediliyor çünkü bir eklenti kind'ı aynı zamanda categoryOf === 'furnish' ve Assets daha spesifik cevap. Üç sekme tamamlayıcı kalıyor. Furnish düğmesi artık layer'ı da temizliyor: iki sekme de phase 'furnish' altında ve structureLayer ile ayrılıyor, bu olmadan Assets'ten dönmek zaten geçerli bir phase'i set edip hiçbir şey yapmıyormuş gibi görünüyordu. Testler registerNode yerine loadPlugin üzerinden gidiyor — eskisi kind'ı host kind'ı yapıyordu, yani hiç çalışmayan bir sekmeye karşı da geçerlerdi.
Kat döşemelerinde şaft açıklığı açma ve bakımı iyi yazılmıştı ama eklentiye kapalıydı: SurfaceHoleMetadata.source kapalı bir enum'du (manual|stair|elevator) ve sync sahiplerini node.type === 'elevator' ile buluyordu. capabilities.verticalOpening bunu açıyor. Kind açıklık poligonunu veriyor ve belirli bir kattan geçip geçmediğini söylüyor; sync onu registry üzerinden buluyor. İkisi de node'un fonksiyonu, böylece kind kendi şaftını kendi parametrelerinden ölçüyor. Asıl mesele ownerId: sahibi işaretli bir delik, node taşınınca yerini değiştiriyor ve silinince kalkıyor. Kendi deliğini yazan bir eklenti yalnız manual yazabilir — kullanıcınınkinden ayırt edilemez — ve her taşımada bir öncekini ortada bırakır. Slab ve ceiling geçişleri tek predikat dışında aynı kırk satırdı; teke indi. Delikleri koruyan open-coded source !== 'elevator' ifadesi isAutoHoleSource'a çevrildi — eski hâliyle bayat bir verticalOpening deliği kullanıcının çizdiği sanılıp korunur ve hiç kaldırılmazdı. Testi var ve eski filtreye karşı düşüyor. Mevcut sahneler etkilenmiyor: enum bir üye kazanıyor, ownerId opsiyonel, stair ve elevator aynen çalışıyor.
Was 021b9ec.
Lean-to roof extensions with automatic drainage, Blender-style custom mesh editing, synchronized 2D viewer modes, shared-parameter editing across a homogeneous multi-selection, plugin inspector-card extensions, an empty-graph save guard, a batch of wall hover/pick correctness fixes, and the autosave fix that stopped scenes being wiped during the load window. Thirty-five files conflicted; five of them were not real conflicts. `integration` carries cherry-picks of upstream pascalorg#607, pascalorg#608 and pascalorg#638, so git saw two independent additions of the same path. Four were byte-identical to the commit they were picked from and the fifth differed by one defensive `?.`, so upstream's newer copies were taken outright — upstream has since fixed the same files. Fork positions kept, each already written down in UPSTREAM.md: the `resolveSelectionHighlight` thunk and `freezeObjectTransform` in the wall systems, the warehouse-scale room test with no upper area bound, the plugin-aware `graph-schema.ts`, the ownership and edit-lease checks on the scene API, `output: 'standalone'`, the warehouse pin, and the vendored articraft and trees workspaces. Four positions were not written down and now are: the room-envelope height caps, which upstream raised from 6 m to 20 m and this fork removed outright because a cap clamps typed input as well as drag; the trees pin, which stays `workspace:*` while the vendored copy exists; the scene-loader's floating navigation; and the site tree's `def.tree` gate, which now lives inside upstream's own `getTreeNodeComponent`. Upstream's empty-graph guard tests failed on arrival for the reason the last merge's log predicted. Their fixture builds an invented `qa:box` kind, which upstream's validator holds to the BaseNode envelope and this fork's refuses outright, so all three saves returned 400 before reaching the guard under test. The fixture now calls `WallNode.parse`, which is the rule that log already drew from `graph-schema.test.ts`. `bun.lock` is committed unchanged. Upstream bumped plugin-bones to 85238a8e, and the lockfile records the sha512 of each GitHub tarball — which only a machine that can reach the real api.github.com can compute. Relock is dispatched on this branch afterwards; until it runs, `--frozen-lockfile` is expected to fail on that hash and nothing else. Also folds in the one item from the audit that this file was already open for: `@pascal-app/plugin-articraft` joins `transpilePackages`. It ships raw TypeScript and `bootstrap.ts` imports it, and it built until now only because bun's symlink layout drops its real path outside `node_modules`. Gates: biome clean, `check-types` clean for every workspace that can resolve its dependencies here, and `bun run test` green across all 16 tasks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
A level's slab and its ceiling sit at opposite ends of the level, so a shaft crossing floors 1-3 cuts slabs 2 and 3 but ceilings 1 and 2. The predicate saw only the level id, so a kind had one answer for both surfaces and was necessarily wrong at one end: slab semantics leave the bottom floor's ceiling sealed across the shaft, ceiling semantics cut a hole in the ceiling above the top stop. The elevator has always had two predicates for exactly this reason; the capability handed plugins a single one. Pass the surface kind — the sync already holds the surface node, so it costs one argument — and the two sides can express the same rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
`floorPlaced.collides` is the spatial grid's plan rectangle: XZ only, no height at all. That is right for furniture on a floor and wrong for anything whose usable volume is mostly air — to the grid, a conveyor threading the walkway under a racking run is indistinguishable from one driven through its uprights. Kinds like that must leave `collides` off. Turning it off turned off their move validation entirely. `boxDimensions` is what drives `recomputeValidity`, and it was gated on `collides` alone, so such a kind could be placed through a gate that checks it in three dimensions and then dragged straight into solid steel with nothing consulted again. Placement refused what the drag then allowed. `movable.canMoveTo` is the kind's own answer, run on every pointer move of a drag and composed with the grid's rather than replacing it. Declaring it also turns the green/red box on, because refusing a drop with no visible reason reads as the object simply refusing to move; Alt forces it, exactly as it does for a colliding kind. A kind that declares neither keeps the plain arrow cursor it has always had, which is what the extracted `showsValidityBox` is tested on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
…push-turbopack-fix-v0z56s # Conflicts: # bun.lock
Opening a zone's settings took the whole editor down with React 185 —
maximum update depth exceeded.
The contents selector ran through `useShallow`, which compares array
elements with `Object.is`, and it built fresh `{ label, count }` objects
on every call. Two calls over an identical scene were therefore never
equal, so the snapshot read as changed on every render and the loop never
settled. Every other `useShallow` in this codebase maps ids onto nodes
that already exist; not one constructs an object, and this is why.
The selector now returns strings — one label per node, compared by value —
and the grouping moved out into a `useMemo`. `collectZoneObjectLabels`
sits beside `collectZoneObjectIds` so the property that matters can be
tested without a renderer: two calls over an unchanged scene are
shallow-equal, and a node moving out of the zone still makes them
unequal, so the panel keeps updating. Both fail against the old shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
Was f93b4f8.
Was 119716b.
…custom modifications
| // Without this a view-only account reached Overview, Users and Sessions — | ||
| // every colleague's name, address and login history — because those tabs | ||
| // carry no permission of their own. | ||
| if (!session.user.permissions.includes('admin_access')) redirect('/') |
There was a problem hiding this comment.
Console locked to admins only
High Severity
The new console route requires admin_access on every tab, which contradicts TAB_META and the Supervisor role. Supervisors carry edit_users and view_logs but no admin_access, so user management, logs, and audit are unreachable. Sign-in, MFA, and welcome still send every finished session to /console/overview, which then bounces non-admins to /.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit add8c82. Configure here.
…SM, and preserve warehouse plugins/local features
| symlinkSync('argon2', target) | ||
| console.log(`[setup-native] ${alias} -> @node-rs/argon2`) | ||
| } | ||
| if (found.size === 0) console.log('[setup-native] no hashed native aliases found') |
There was a problem hiding this comment.
Native alias scan skips nested chunks
High Severity
setup-native.mjs only reads .js files in the top level of .next/server/chunks and treats zero matches as success. Turbopack often emits server chunks under nested folders such as ssr/. If the hashed @node-rs/argon2-… alias lives only in those files, no symlink is created and Hostinger login fails at runtime. Smoke tests never exercise argon2, so this ships green.
Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.
| echo 'changed=false' >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo 'changed=true' >> "$GITHUB_OUTPUT" | ||
| fi |
There was a problem hiding this comment.
Sync workflow hides crashed panel sync
Medium Severity
sync-panel pipes scripts/sync-panel.mjs into tee without pipefail. The default shell is bash -e, so tee's success becomes the step status and a crashed sync is swallowed. The job can then commit a partial or empty tree to ovurrsl/panel. The inbound pull-panel workflow already documents and fixes this exact hazard.
Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.
| paths: | ||
| - 'apps/editor/**' | ||
| - 'packages/**' | ||
| - 'bun.lock' |
There was a problem hiding this comment.
Deploy path filters miss scaffold
Medium Severity
Automatic deploy-bundle runs only when apps/editor/**, packages/**, or bun.lock change. Edits to .github/deploy/package.json, setup-native.mjs, or the workflow itself do not trigger a rebuild. A runtime Next bump or native-alias fix can sit unreleased until an unrelated editor commit lands, which is how the last Next build/run skew stayed live.
Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.
| const buildManifestPath = path.join(nextDir, 'build-manifest.json') | ||
| const reactLoadableManifestPath = path.join(nextDir, 'react-loadable-manifest.json') | ||
| const staticChunksDir = path.join(nextDir, 'static', 'chunks') | ||
|
|
There was a problem hiding this comment.
Isolation tests never run in CI
Low Severity
The new suite under apps/editor/__tests__/ is never executed. The editor package test script is bun test lib, and CI runs tests before bun run build. Bundle-isolation cases also require a pre-existing .next tree, so they cannot pass in the current job order even if the glob were widened. The dynamic-plugin guarantees they claim to enforce are unchecked.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.
…it-lock in keyboard shortcuts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b2eb893. Configure here.
| // Without this a view-only account reached Overview, Users and Sessions — | ||
| // every colleague's name, address and login history — because those tabs | ||
| // carry no permission of their own. | ||
| if (!session.user.permissions.includes('admin_access')) redirect('/') |
There was a problem hiding this comment.
Console admin gate gets overwritten
High Severity
The new admin_access check on /console/[tab] is not in EDITOR_OWNED. Hourly pull-panel copies src/app/console/[tab]/page.tsx from ovurrsl/panel over this file and commits apps/editor/app/(panel), so the gate that stops view-only accounts from opening Users and Sessions is removed on the next pull.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b2eb893. Configure here.


What does this PR do?
How to test
Screenshots / screen recording
Checklist
bun devbun checkto verify)mainbranchNote
Medium Risk
Changes production deploy paths, CI branch defaults, and console auth routing; misconfigured secrets or workflow dispatch could block releases, while admin gating on
/consolealters who can reach administration APIs.Overview
This PR turns the ovurrsl/editor fork into an operable DigitalTwin product line: documentation for branches, upstream merges, and Hostinger publishing; removal of upstream Docker assets in favor of a standalone bundle published to
ovurrsl/digitaltwin.CI and automation move primary development to
integration(CI, MCP CI). New workflowsdeploy-bundle(build, MySQL smoke tests, force-push artifact),mirror-upstream,pull-panel/sync-panel,bump-plugin,relock, andupstream-check.cli-smokeis dropped with an explanation tied to the fork’s rootbuildscript..github/deploy/adds runtimepackage.json, Hostinger README, andsetup-native.mjsfor Turbopack’s hashed@node-rs/argon2alias.Editor app gains vendored console routes under
app/(panel)/— sign-in, MFA, reset, and/console/[tab]with server guards (session,admin_access, per-tab permissions).AGENTS.mdgets a fork warning block;OTOMASYON.md,UPSTREAM.md,YAYINLAMA.md, andPROJECT_HANDOVER.mddocument the multi-repo pipeline.Quality gates add large
__tests__suites that assert lazy plugin loading (no static plugin imports in bootstrap, dynamic chunks in.next) and runtime install/uninstall behavior for the seven catalog plugins..agents/rules/nextjs_turbopack.mdrecords why client bundles must not reachnode:*(post‑MCP relay outage)..gitignoreignores.agents/.Reviewed by Cursor Bugbot for commit b2eb893. Bugbot is set up for automated code reviews on this repo. Configure here.