Skip to content

Main - #754

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

Main#754
ovurrsl wants to merge 297 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

High Risk
Changes production deploy topology, required MySQL at boot, GitHub secrets/tokens, and force-pushed live artifacts—mistakes can block releases or ship broken bundles while CI branch assumptions shift from main to integration.

Overview
This PR turns ovurrsl/editor into an operable fork: integration becomes the working and CI default (replacing main for pushes/PRs), main stays a pure upstream mirror, and a large set of docs (OTOMASYON.md, UPSTREAM.md, YAYINLAMA.md, fork block in AGENTS.md, handover notes) encodes branch rules and merge playbooks.

Release path changes materially: the root Dockerfile and .dockerignore are removed; production is documented as Hostinger via a new deploy-bundle workflow that builds Next standalone, runs MySQL boot smoke tests (refuse start without DB), assembles .github/deploy/ artifacts (package.json, setup-native.mjs for argon2 aliases), and force-pushes to ovurrsl/digitaltwin. Companion automation adds mirror-upstream, bump-plugin, pull-panel / manual sync-panel, relock, and upstream-check; CI drops the macOS cli-smoke job with an explained rationale.

Product wiring: the DigitalTwin console is mounted under apps/editor/app/(panel)/ (auth, MFA, /console/[tab] with admin/permission guards) using @panel imports and panel styling isolated via data-dt-theme. SETUP.md states Docker was removed on purpose (MySQL-only prod story).

Quality gates: new editor tests assert lazy plugin catalog behavior and, when .next exists, no plugin code in initial chunks and dynamic split for seven plugins.

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

claude and others added 30 commits July 31, 2026 06:31
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.
github-actions Bot and others added 17 commits August 24, 2026 14:32
`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ı
Comment thread apps/editor/app/(panel)/console/[tab]/page.tsx
Comment thread .github/workflows/bump-plugin.yml
Comment thread .github/deploy/setup-native.mjs
Comment thread .github/workflows/sync-panel.yml
Comment thread .github/workflows/deploy-bundle.yml
Comment thread apps/editor/__tests__/verify-bundle-isolation.test.ts
Comment thread apps/editor/app/(panel)/console/[tab]/page.tsx

- name: Install dependencies
if: steps.token.outputs.available == 'true' && steps.pull.outputs.changed == 'true'
run: bun install --frozen-lockfile

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Panel pull uses unpinned Bun

Medium Severity

pull-panel runs setup-bun with no bun-version, then bun install --frozen-lockfile. CI comments elsewhere state an unpinned Bun floats to latest and can fail the frozen lock without any repo change. A console pull that cannot install never type-checks or lands on integration.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 46948bb. Configure here.

} from '@pascal-app/core'
import { editorHostPanelRegistry } from '@pascal-app/editor'
import { PLUGIN_CATALOG, getPluginDescriptor } from '../lib/plugins/catalog'
import { usePluginManager } from '../lib/plugins/use-plugin-manager'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tests import missing plugin catalog

Medium Severity

Four new suites import lib/plugins/catalog and lib/plugins/use-plugin-manager, which are not in the tree, and they assert that bootstrap.ts has no static plugin imports. bootstrap.ts still statically loads warehouse, trees, bones, mint, and streetscape. The editor test script only runs lib/, so these suites never execute and describe an architecture that this change did not ship.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 46948bb. 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 3 total unresolved issues (including 2 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 f1e43e7. Configure here.

Comment thread apps/editor/package.json
"lint": "biome lint",
"check-types": "next typegen && tsc --noEmit",
"test": "bun test lib"
"test": "bun test lib --timeout 30000"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New editor tests never run

Medium Severity

Four new suites under apps/editor/__tests__/ are presented as bundle-isolation and dynamic-activation guards, but the editor test script is bun test lib. Those files sit outside lib/, so turbo run test and CI never execute them. The adversarial coverage they claim is not actually enforced.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f1e43e7. 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