Skip to content

Bruadam/web enhancements#31

Merged
bruadam merged 5 commits into
mainfrom
bruadam/web-enhancements
Mar 28, 2026
Merged

Bruadam/web enhancements#31
bruadam merged 5 commits into
mainfrom
bruadam/web-enhancements

Conversation

@bruadam

@bruadam bruadam commented Mar 28, 2026

Copy link
Copy Markdown
Owner
  • 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.

bruadam added 4 commits March 28, 2026 14:17
- 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.
Copilot AI review requested due to automatic review settings March 28, 2026 16:12
@bruadam bruadam merged commit 54cb027 into main Mar 28, 2026
@bruadam bruadam deleted the bruadam/web-enhancements branch March 28, 2026 16:14

Copilot AI 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.

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.

Comment on lines +40 to +45
// 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";

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

snapshotPath is computed but never used. Remove it (or use it to decide ogImageUrl/fallback) to avoid unused-variable lint errors.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +63
const ifcType = (line.type as string) ?? "";
// Skip non-product entities (relations, property sets managed separately)
if (!ifcType || ifcType.startsWith("IFC_REL") || ifcType === "IFCPROPERTYSINGLEVALUE") continue;

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +151
const copyLink = useCallback(() => {
const url = `${window.location.origin}/clash/${clash.guid}`;
navigator.clipboard.writeText(url).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, [clash.id]);

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +30
const snapshotPath = getSnapshot(guid);
if (snapshotPath) {
const abs = path.join(PUBLIC_DIR, snapshotPath);
try {
const buf = await readFile(abs);

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
import { getSnapshot, setSnapshot, getAllSnapshots } from "@/lib/db";

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

Unused import: getSnapshot is imported but never used in this route. Remove it to avoid lint failures under next lint/no-unused-vars.

Suggested change
import { getSnapshot, setSnapshot, getAllSnapshots } from "@/lib/db";
import { setSnapshot, getAllSnapshots } from "@/lib/db";

Copilot uses AI. Check for mistakes.
for (const p of BCF_PATHS) {
try {
const buf = await readFile(p);
return await parseBcf(buf.buffer as ArrayBuffer);

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
return await parseBcf(buf.buffer as ArrayBuffer);
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
return await parseBcf(arrayBuffer);

Copilot uses AI. Check for mistakes.
Comment thread web/lib/db.ts
Comment on lines +138 to +142
/** 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;

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
} 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));

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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

Suggested change
url.searchParams.set("error", encodeURIComponent(message));
url.searchParams.set("error", message);

Copilot uses AI. Check for mistakes.
for (const bcfPath of [BCF_PATH, BCF_PATH_ALT]) {
try {
const buf = await readFile(bcfPath);
const clashes = await parseBcf(buf.buffer as ArrayBuffer);

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +94
if (!a) return b;
return `${a}x${b}`;
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.

2 participants