Skip to content

Main - #754

Open
ovurrsl wants to merge 295 commits into
pascalorg:mainfrom
ovurrsl:main
Open

Main#754
ovurrsl wants to merge 295 commits into
pascalorg:mainfrom
ovurrsl:main

Conversation

@ovurrsl

@ovurrsl ovurrsl commented Sep 2, 2026

Copy link
Copy Markdown

What does this PR do?

How to test

Screenshots / screen recording

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note

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 /console alters 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 workflows deploy-bundle (build, MySQL smoke tests, force-push artifact), mirror-upstream, pull-panel / sync-panel, bump-plugin, relock, and upstream-check. cli-smoke is dropped with an explanation tied to the fork’s root build script. .github/deploy/ adds runtime package.json, Hostinger README, and setup-native.mjs for Turbopack’s hashed @node-rs/argon2 alias.

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.md gets a fork warning block; OTOMASYON.md, UPSTREAM.md, YAYINLAMA.md, and PROJECT_HANDOVER.md document 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.md records why client bundles must not reach node:* (post‑MCP relay outage). .gitignore ignores .agents/.

Reviewed by Cursor Bugbot for commit b2eb893. Bugbot is set up for automated code reviews on this repo. Configure here.

claude and others added 30 commits July 31, 2026 05:36
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
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.
ovurrsl and others added 22 commits August 24, 2026 16:05
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.
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
…ve zone paneli düzeltmesi (#41)

Upstream 7f629b8'yi al (55 commit) + dikey açıklık ve sürükleme kapısı
// 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('/')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit add8c82. Configure here.

Comment thread .github/workflows/bump-plugin.yml
symlinkSync('argon2', target)
console.log(`[setup-native] ${alias} -> @node-rs/argon2`)
}
if (found.size === 0) console.log('[setup-native] no hashed native aliases found')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.

echo 'changed=false' >> "$GITHUB_OUTPUT"
else
echo 'changed=true' >> "$GITHUB_OUTPUT"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.

paths:
- 'apps/editor/**'
- 'packages/**'
- 'bun.lock'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4e8ecbd. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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('/')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b2eb893. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants