Bruadam/web enhancements#31
Conversation
bruadam
commented
Mar 28, 2026
- Implemented Linear GraphQL API client for issue management.
- Added linearIssueId to clashes table and updated related functions.
- Created scripts for importing BCF files and parsing IFC files.
- Enhanced IfcViewer component with color-by picker and right-click context menu for bubbles.
- Updated Topbar to include a link to Linear integration settings.
- Added linear settings management in the database.
- Improved clash handling with new priority and status metadata.
- Added a new script `generate-snapshots.mjs` for generating headless screenshots of clashes. - Updated `package.json` to include new dependencies: `canvas`, `gl`, and `puppeteer`. - Added new npm scripts for generating snapshots and snapshots with specific GUID. - Created a `.gitkeep` file in the `public/og` directory to track generated images.
- Implemented Linear GraphQL API client for issue management. - Added linearIssueId to clashes table and updated related functions. - Created scripts for importing BCF files and parsing IFC files. - Enhanced IfcViewer component with color-by picker and right-click context menu for bubbles. - Updated Topbar to include a link to Linear integration settings. - Added linear settings management in the database. - Improved clash handling with new priority and status metadata.
There was a problem hiding this comment.
Pull request overview
This PR enhances the ClasHero web app and the Rust clash CLI with end-to-end IFC/BCF workflows, snapshot OG image generation, and Linear issue integration, while migrating the web app’s persistence layer from JSON to SQLite.
Changes:
- Added SQLite-backed storage for clashes, activity/comments, snapshots, IFC model registry/elements, and Linear settings.
- Implemented BCF import/export + IFC parsing (scripts and server API endpoints) and introduced an OG snapshot pipeline (snapshot page + Puppeteer script + OG API).
- Added Linear OAuth + settings UI and sync endpoints to push/pull clash metadata to/from Linear.
Reviewed changes
Copilot reviewed 46 out of 50 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| web/scripts/parse-ifc.mjs | Standalone IFC→SQLite parser using web-ifc + better-sqlite3. |
| web/scripts/import-bcf.mjs | Imports BCFZip into SQLite clashes table with status/priority mapping. |
| web/scripts/generate-snapshots.mjs | Puppeteer-based OG snapshot generator + persistence to DB. |
| web/public/og/.gitkeep | Keeps public/og directory in git for generated assets. |
| web/package.json | Adds scripts + dependencies for SQLite, IFC parsing, and headless snapshots. |
| web/next.config.ts | Enables output: "standalone" for deployment packaging. |
| web/lib/types.ts | Extends Clash type with optional linearIssueId. |
| web/lib/linear.ts | Adds Linear GraphQL client + OAuth token exchange helpers. |
| web/lib/dummy-clashes.ts | Updates dummy clash metadata to new Building IFC naming. |
| web/lib/db.ts | Replaces JSON “db” with SQLite schema + CRUD + legacy JSON migration. |
| web/lib/bcf-parser.ts | Adds BCF 2.1 zip parser to Clash[]. |
| web/lib/bcf-exporter.ts | Adds BCF 2.1 exporter + browser download helper. |
| web/components/ui/popover.tsx | Adds Radix Popover UI wrapper component. |
| web/components/topbar.tsx | Adds Linear settings link and refines topbar styling. |
| web/components/model-manager.tsx | UI for uploading/parsing/deleting IFC models via API. |
| web/components/display-options-panel.tsx | Updates panel UI and outside-click handling via anchorRef. |
| web/components/clash-detail.tsx | Adds Linear sync actions, BCF export, link copy, UI refinements. |
| web/components/bubble-context-menu.tsx | Adds right-click context menu for quick clash edits. |
| web/components/bcf-create-dialog.tsx | Dialog to create a clash issue from selected IFC elements. |
| web/app/snapshot/[guid]/page.tsx | Client snapshot renderer for Puppeteer to screenshot a clash. |
| web/app/settings/linear/page.tsx | Linear integration settings UI (connect/team/project/sync). |
| web/app/clash/[id]/page.tsx | Makes per-clash page load/sync via API (GUID-based routing). |
| web/app/clash/[id]/layout.tsx | Adds per-clash OG metadata generation + snapshot lookup. |
| web/app/api/snapshots/route.ts | API to persist and list snapshot paths in SQLite. |
| web/app/api/og/[guid]/route.ts | Serves stored OG PNG or SVG placeholder fallback. |
| web/app/api/models/upload/route.ts | Upload endpoint for IFC models + DB registration. |
| web/app/api/models/route.ts | List/delete models + auto-seed DB registry from disk. |
| web/app/api/models/[filename]/route.ts | Serves IFC files from models/Building. |
| web/app/api/models/[filename]/parse/route.ts | Server-side IFC parsing endpoint storing elements in SQLite. |
| web/app/api/linear/teams/route.ts | Lists teams from Linear using stored access token. |
| web/app/api/linear/sync/[guid]/route.ts | Pushes a clash to Linear as an issue and stores linkage. |
| web/app/api/linear/sync-project/route.ts | Bulk-pushes all unsynced clashes to Linear. |
| web/app/api/linear/settings/route.ts | Gets/updates/clears Linear settings stored in SQLite. |
| web/app/api/linear/pull/[guid]/route.ts | Pulls status/assignee from Linear and applies to clash. |
| web/app/api/linear/projects/route.ts | Lists projects for a Linear team. |
| web/app/api/linear/callback/route.ts | OAuth callback: exchanges code for token and stores settings. |
| web/app/api/linear/auth/route.ts | Starts OAuth flow and sets CSRF state cookie. |
| web/app/api/clashes/route.ts | Lists clashes from DB/BCF/dummy and supports inserting new clashes. |
| web/app/api/clashes/[guid]/route.ts | Fetches and patches a single clash in SQLite. |
| models/Building/Building-Structural.ifc | Adds Building model IFC (LFS pointer). |
| models/Building/Building-Hvac.ifc | Adds Building model IFC (LFS pointer). |
| models/Building/Building-Architecture.ifc | Adds Building model IFC (LFS pointer). |
| models/AC20-FZK-Haus.ifc | Adds large IFC sample (LFS pointer). |
| db/db.json | Adds legacy JSON DB content (activity/comments). |
| db/clashero.db | Adds a generated SQLite DB file. |
| clash/tests/viewpoint_tests.rs | Adds tests validating BCF viewpoint camera calculation. |
| clash/tests/models/Building-Structural.ifc | Adds test IFC (LFS pointer). |
| clash/tests/models/Building-Hvac.ifc | Adds test IFC (LFS pointer). |
| clash/tests/models/Building-Architecture.ifc | Adds test IFC (LFS pointer). |
| clash/tests/cli_tests.rs | Adds CLI integration tests (help, detect, output, filtering). |
| clash/src/main.rs | Implements clap-based CLI with clash detection + optional BCF output. |
| clash/src/lib.rs | Exposes new bcf_reporter module. |
| clash/src/bcf_reporter.rs | Implements BCF ZIP/XML generation for detected clashes. |
| clash/ralph.sh | Adds a docker-driven iteration runner script. |
| clash/Dockerfile | Adds a Rust+Node container setup for Junie tooling. |
| clash/Cargo.toml | Adds deps for clap, BCF XML/ZIP output, and CLI tests. |
| clash/.output.txt | Adds generated tooling output file. |
| clash/.junie/memory/language.json | Adds generated Junie state file. |
| clash/.gitignore | Expands ignore rules to include env/tooling artifacts. |
| clash/.env | Adds an env file containing a key. |
| clash/.dockerignore | Adds docker ignore rules for build context. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Look up pre-generated OG screenshot | ||
| const snapshotPath = clash ? getSnapshot(clash.guid) : null; | ||
| // snapshotPath is like "/og/clash-001.png" — for metadata we use the API route | ||
| const ogImageUrl = clash | ||
| ? `/api/og/${clash.guid}` | ||
| : "/og/default.png"; |
There was a problem hiding this comment.
snapshotPath is computed but never used. Remove it (or use it to decide ogImageUrl/fallback) to avoid unused-variable lint errors.
| const ifcType = (line.type as string) ?? ""; | ||
| // Skip non-product entities (relations, property sets managed separately) | ||
| if (!ifcType || ifcType.startsWith("IFC_REL") || ifcType === "IFCPROPERTYSINGLEVALUE") continue; |
There was a problem hiding this comment.
line.type from web-ifc is a numeric type code (as used in scripts/parse-ifc.mjs via GetNameFromTypeCode). Here it’s treated as a string and .startsWith(...) is called, which will throw at runtime when line.type is a number. Use ifcApi.GetNameFromTypeCode(line.type) (or String(line.type) after converting) and align the skip checks with the returned casing (e.g. IfcRel*, IfcPropertySingleValue).
| const ifcType = (line.type as string) ?? ""; | |
| // Skip non-product entities (relations, property sets managed separately) | |
| if (!ifcType || ifcType.startsWith("IFC_REL") || ifcType === "IFCPROPERTYSINGLEVALUE") continue; | |
| const ifcType = | |
| line.type != null | |
| ? (ifcApi.GetNameFromTypeCode(line.type as number) as string) ?? "" | |
| : ""; | |
| // Skip non-product entities (relations, property sets managed separately) | |
| if (!ifcType || ifcType.startsWith("IfcRel") || ifcType === "IfcPropertySingleValue") continue; |
| const copyLink = useCallback(() => { | ||
| const url = `${window.location.origin}/clash/${clash.guid}`; | ||
| navigator.clipboard.writeText(url).then(() => { | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| }); | ||
| }, [clash.id]); |
There was a problem hiding this comment.
copyLink closes over clash.guid but the dependency array is [clash.id]. Update dependencies to include clash.guid (and/or clash.id) so the copied URL stays correct when navigating between clashes without remounting.
| const snapshotPath = getSnapshot(guid); | ||
| if (snapshotPath) { | ||
| const abs = path.join(PUBLIC_DIR, snapshotPath); | ||
| try { | ||
| const buf = await readFile(abs); |
There was a problem hiding this comment.
snapshotPath values like "/og/....png" are treated as absolute paths by path.join(PUBLIC_DIR, snapshotPath), which discards PUBLIC_DIR. Combined with /api/snapshots allowing arbitrary path, this can read files outside public/ (e.g. "/etc/passwd"). Ensure stored snapshot paths are relative (no leading "/"), restrict them to a safe subdir (e.g. only "og/.png"), and resolve+verify the final path stays under PUBLIC_DIR before reading.
| @@ -0,0 +1,17 @@ | |||
| import { NextRequest, NextResponse } from "next/server"; | |||
| import { getSnapshot, setSnapshot, getAllSnapshots } from "@/lib/db"; | |||
There was a problem hiding this comment.
Unused import: getSnapshot is imported but never used in this route. Remove it to avoid lint failures under next lint/no-unused-vars.
| import { getSnapshot, setSnapshot, getAllSnapshots } from "@/lib/db"; | |
| import { setSnapshot, getAllSnapshots } from "@/lib/db"; |
| for (const p of BCF_PATHS) { | ||
| try { | ||
| const buf = await readFile(p); | ||
| return await parseBcf(buf.buffer as ArrayBuffer); |
There was a problem hiding this comment.
readFile returns a Node Buffer; passing buf.buffer can include unrelated bytes (offset/length mismatch) and break ZIP parsing. Pass buf directly (it is a Uint8Array) or slice the ArrayBuffer using buf.byteOffset/buf.byteLength before calling parseBcf.
| return await parseBcf(buf.buffer as ArrayBuffer); | |
| const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); | |
| return await parseBcf(arrayBuffer); |
| /** One-time migration of existing db.json data into SQLite. */ | ||
| function migrateLegacyJson() { | ||
| const jsonPath = path.join(DB_DIR, "db.json"); | ||
| if (!fs.existsSync(jsonPath)) return; | ||
|
|
There was a problem hiding this comment.
migrateLegacyJson() looks for the legacy file at path.join(DB_DIR, "db.json") (repo-root db/db.json), but the previous implementation stored it at web/data/db.json (path.join(process.cwd(), "data", "db.json") when Next runs in web/). This means existing users won’t be migrated. Consider checking both locations (old web/data/db.json and new db/db.json) or deriving the legacy path from the old DB_PATH logic.
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| const url = new URL("/settings/linear", req.nextUrl.origin); | ||
| url.searchParams.set("error", encodeURIComponent(message)); |
There was a problem hiding this comment.
The error query param is being URL-encoded twice: searchParams.set("error", encodeURIComponent(message)) and then URL encoding happens again when generating the redirect. This makes the UI display a still-encoded string. Set the raw message into the query param and let URLSearchParams handle encoding (or decode only once on the client).
| url.searchParams.set("error", encodeURIComponent(message)); | |
| url.searchParams.set("error", message); |
| for (const bcfPath of [BCF_PATH, BCF_PATH_ALT]) { | ||
| try { | ||
| const buf = await readFile(bcfPath); | ||
| const clashes = await parseBcf(buf.buffer as ArrayBuffer); |
There was a problem hiding this comment.
readFile returns a Node Buffer; passing buf.buffer can include extra bytes (because Buffer can be an offset view). JSZip.loadAsync may fail on such buffers. Pass buf directly (as Uint8Array) or slice the ArrayBuffer with buf.byteOffset/buf.byteLength.
| const clashes = await parseBcf(buf.buffer as ArrayBuffer); | |
| const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); | |
| const clashes = await parseBcf(arrayBuffer as ArrayBuffer); |
| if (!a) return b; | ||
| return `${a}x${b}`; | ||
| } |
There was a problem hiding this comment.
deriveRuleId returns ${a}x${b}, but other parts of the app (e.g. web/lib/bcf-parser.ts) use the "×" separator. This will create inconsistent rule IDs and affect grouping/filtering. Standardize on one separator.