ifcurl/.beads/issues.jsonl
2026-04-25 10:06:31 +01:00

80 lines
83 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{"id":"ifcurl-vm5","title":"ifcopenshell segfault on malformed IFC: service DoS and potential RCE risk","description":"ifcopenshell uses C++ bindings (via web-ifc) that can segfault when given malformed or adversarially crafted IFC data. In the current service architecture this has two serious implications:\n\n1. **DoS**: a segfault kills the uvicorn worker process. Under repeated attack a service with --workers=1 (the common deployment) goes down entirely. Even with multiple workers, each is vulnerable independently.\n\n2. **Potential RCE**: memory-corruption bugs in C++ parsers are a well-known exploit vector. A crafted IFC file that triggers a heap overflow in ifcopenshell could yield arbitrary code execution as the service user. The service runs on the same host as Forgejo, making this a high-value target.\n\nThe two call sites at risk are:\n- `_load_model()` → `ifcopenshell.file.from_string()` in service.py (runs in the uvicorn worker thread)\n- `render_mod.render()` → `ifcopenshell.geom.iterator()` in render.py (already spawns worker subprocesses, so a segfault there kills a worker process but not the service)\n\nThe primary mitigation to investigate is **subprocess isolation for the parse step**: run `from_string()` in a short-lived child process with a timeout and resource limits. If the child exits with a signal (SIGSEGV, SIGABRT) or times out, return HTTP 422 to the caller without killing the service. This is the standard pattern used by document-conversion services (e.g. LibreOffice sandboxing).\n\nSecondary mitigations:\n- Apply `ulimit` / `resource.setrlimit` to cap address-space and CPU time in the child\n- Consider `seccomp` syscall filtering for the parse subprocess (restrict to read-only FS access, no network, no exec)\n- File-size cap on uploaded/fetched IFC bytes (tracked separately in ifcurl-fu5)\n- Fuzz testing with `atheris` or AFL++ against the parse path to surface crashes before deployment\n\nAcceptance criteria: a crafted request that would segfault the current service returns 422 and the service continues to handle subsequent requests.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T16:22:41Z","created_by":"Bruno Postle","updated_at":"2026-04-24T16:38:18Z","started_at":"2026-04-24T16:25:41Z","closed_at":"2026-04-24T16:38:18Z","close_reason":"Implemented subprocess isolation in ifcurl/sandbox.py. All ifcopenshell calls now run in a child process; segfault → SandboxCrashError → HTTP 422 without killing the service.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-p60","title":"Viewer: no element click-to-identify — can't get GlobalId or properties","description":"There is no way to click on a mesh in the viewer and see which IFC element it represents. Reviewers can see geometry but cannot reference specific elements — they can't discover a GlobalId to put in a selector URL, can't see the element name/type/Psets, and can't say 'this specific wall' in a BCF or Forgejo comment. This breaks the collaboration workflow at its foundation: every useful feedback comment needs to identify specific elements. Need: click or hover on a surface to highlight it and show a properties panel (name, type, GlobalId, key Psets). The GlobalId should be copyable into the selector field or URL. The ThatOpen components library provides the tools for this (OBC.IfcRelationsIndexer, element property queries).","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:02:33Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:23Z","started_at":"2026-04-24T11:14:57Z","closed_at":"2026-04-24T11:47:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-4l7","title":"viewer.html: scale= URLs don't load in orthographic mode","description":"applyCameraParam() reads the camera= and fov= parameters from the URL and applies them, but never reads scale= or switches the camera to orthographic (parallel) projection. URLs like ?camera=...\u0026scale=50 will load in perspective mode instead of orthographic. The OBC OrthoPerspectiveCamera can switch modes, but the code path to set parallel projection and apply the scale value is missing from applyCameraParam.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:51Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:19Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-362","title":"Add missing server-config files: gitconfig-ifcmerge and gitattributes","description":"forgejo/README.md documents deploying two files that don't exist in the repo: forgejo/server-config/gitconfig-ifcmerge and forgejo/server-config/gitattributes. Both are required for the ifcmerge git merge driver to work in bare Forgejo repos. The README even includes 'sudo cp forgejo/server-config/gitattributes /etc/gitattributes' and 'sudo git config --system include.path .../gitconfig-ifcmerge', but the files themselves are absent. Anyone following the deployment instructions will hit a 'file not found' error. Need to create both files with the content described in the README.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:31Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:51:47Z","started_at":"2026-04-24T10:50:15Z","closed_at":"2026-04-24T10:51:47Z","close_reason":"Files already exist: forgejo/server-config/gitconfig-ifcmerge and gitattributes are both present and correct. Missed in initial review.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-bue","title":"Split service into ifcurl-api and ifcurl-render for RCE isolation","description":"The ifcopenshell C++ bindings are the main RCE attack surface (memory corruption on malformed IFC). The git/token code is not at risk but currently shares the same process and systemd security profile.\n\nSplit the service into two systemd units with different security profiles:\n\nifcurl-api (renamed from ifcurl-preview): handles HTTP, git fetch, caching, auth token lookup. Needs execve (git CLI via GitPython) and AF_INET/AF_INET6. No ifcopenshell.\n\nifcurl-render: handles IFC parsing and PNG rendering only. Listens on Unix socket /run/ifcurl-render/render.sock. Has SystemCallFilter ~execve ~execveat (confirmed safe: strace shows pyvista and ifcopenshell geom iterator use fork/clone only, never exec). Has RestrictAddressFamilies=AF_UNIX only. No access to /etc/ifcurl tokens. Runs as separate user ifcurl-render.\n\nCommunication: API service calls render service via httpx over the Unix socket. Both service users belong to a shared ifcurl group for socket access.\n\nInterface (three endpoints on render service):\n POST /render multipart(ifc, params JSON) → JSON {png: base64, guids: list|null}\n POST /select multipart(ifc, params JSON) → JSON {guids: list}\n POST /render_diff multipart(head_ifc, base_ifc, params JSON) → image/png binary\n\nsandbox.py crash isolation is kept inside the render service for per-request segfault containment.\n\nBackward compat: if IFCURL_RENDER_SOCKET is unset, API service falls back to direct in-process rendering (current sandbox.py path).\n\nImplementation steps:\n1. ifcurl/render_service.py — FastAPI app with the three endpoints wrapping sandboxed functions\n2. ifcurl/service.py — delegate to render service when IFCURL_RENDER_SOCKET is set\n3. ifcurl/__main__.py — add 'render-service' CLI subcommand\n4. pyproject.toml — add httpx to service optional deps\n5. forgejo/server-config/ifcurl-api.service (rename + update)\n6. forgejo/server-config/ifcurl-render.service (new, hardened unit)\n7. forgejo/README.md — update deployment docs for two-service setup\n8. tests/test_render_service.py — integration tests for the render service endpoints","acceptance_criteria":"render service cannot exec /bin/sh even with RCE (verified by strace showing no execve during normal render); normal renders still produce correct PNG; API service correctly delegates to render service when socket is configured; fallback to in-process rendering works when socket is unset","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-25T08:32:27Z","created_by":"Bruno Postle","updated_at":"2026-04-25T08:57:22Z","started_at":"2026-04-25T08:32:57Z","closed_at":"2026-04-25T08:57:22Z","close_reason":"Two-service split implemented: render_service.py with /render /select /render_diff endpoints, service.py delegates via httpx when IFCURL_RENDER_SOCKET is set, render-service CLI subcommand added, two systemd units created","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ndf","title":"gitconfig-ifcmerge: update to use ifcmerge --prioritise-local instead of swapping LOCAL/REMOTE","description":"The current gitconfig-ifcmerge uses the old technique of swapping the order of %A and %B arguments to preserve the base branch's STEP ID space:\n\n driver = ifcmerge %O %A %B %L (standard)\n driver = ifcmerge %O %B %A %L (ours variant)\n\nBonsai's current ifcgit code (tool/ifcgit.py, config_ifcmerge) uses the newer --prioritise-local flag instead:\n\n ifcmerge --prioritise-local $BASE $LOCAL $REMOTE $MERGED\n\nThe --prioritise-local flag tells ifcmerge to preserve LOCAL's STEP ID space without needing to swap argument order. This is cleaner, more readable, and follows the canonical ifcmerge interface.\n\nThe gitconfig-ifcmerge needs updating to:\n driver = ifcmerge --prioritise-local %O %A %B %A\n\n(where %A is both LOCAL and the output file — git merge drivers write their result to %A)\n\nThe ifcmerge_ours variant (for rebase direction where base branch arrives as %B) would become:\n driver = ifcmerge --prioritise-local %O %B %A %B\n\nVerify that the --prioritise-local flag exists in the version of ifcmerge being used before merging.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-25T06:06:59Z","created_by":"Bruno Postle","updated_at":"2026-04-25T06:41:59Z","closed_at":"2026-04-25T06:41:59Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-qjd","title":"render_diff: auto-fit camera to changed elements, not whole model","description":"When rendering a diff of a large building model, the auto-fit camera fits to the entire head model. A wall added to one corner of a multi-storey building would appear as a tiny green speck. The camera should instead zoom to the bounding box of just the added + modified + removed elements so the changed area fills the viewport.\n\nImplementation sketch:\n- In render_diff(), after adding all shapes to plotter1, call plotter1.reset_camera() as now, but also compute the bounds of only the diff-coloured meshes (added + modified) and re-fit to those bounds.\n- For pass 2 (removed elements), the elements are already isolated in the iterator, so reset_camera() naturally fits to them. The pass 1 camera capture then needs to consider that pass 2 might be out of that frustum.\n- A combined approach: compute the bounding box union of added + modified + removed element meshes, then set the camera to fit that box with a small margin. This is the camera used for both passes.\n- pyvista Plotter has plotter.reset_camera(bounds=(xmin,xmax,ymin,ymax,zmin,zmax)) which can fit to an explicit box.\n- The bounds can be accumulated during the _add_shape loop for diff-coloured elements.\n\nAcceptance: a single modified wall in a large building renders with the wall clearly visible and filling most of the image.","status":"closed","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-25T06:06:39Z","created_by":"Bruno Postle","updated_at":"2026-04-25T06:57:06Z","closed_at":"2026-04-25T06:57:06Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-zxs","title":"sandbox.py: add seccomp filter to block execve and socket in render subprocess","description":"The subprocess isolation in sandbox.py (ifcurl-vm5) prevents DoS but not RCE: if a crafted IFC file achieves code execution inside the render child, that child still has full access to execve and the network. A seccomp BPF filter applied at the start of _worker() would block the two most useful post-exploitation steps even under active code execution.\n\nThe filter should:\n- Block execve/execveat (cannot exec a shell or any binary)\n- Block socket() for AF_INET/AF_INET6/AF_UNIX (cannot open a reverse shell or connect to the Forgejo Unix socket)\n- Allow clone/fork (ifcopenshell's geom.iterator spawns its own worker processes)\n- Allow everything else needed by Python + ifcopenshell + pyvista\n\nThe python-seccomp library (pip install seccomp, wraps libseccomp) is the cleanest way to do this. The filter would be installed via resource.setrlimit companion code already present in sandbox._worker().\n\nThe main risk is over-filtering: if the seccomp policy blocks a syscall that ifcopenshell or pyvista needs, renders fail with EPERM. The policy needs to be validated against the full render path (including the geometry iterator subprocesses, which inherit the filter across fork).\n\nAcceptance criteria:\n- A render subprocess cannot exec /bin/sh even with arbitrary code execution (verified by replacing fn with a ctypes execve call — should get EPERM/killed, not a shell)\n- Normal renders continue to produce correct PNG output\n- The seccomp library is optional (graceful fallback if libseccomp is absent, e.g. on macOS or minimal containers)","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T20:47:00Z","created_by":"Bruno Postle","updated_at":"2026-04-25T07:30:44Z","closed_at":"2026-04-25T07:30:44Z","close_reason":"systemd hardening accepted as sufficient; execve cannot be blocked per-subprocess without non-standard seccomp library; residual risk acceptable for threat model","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-9cr","title":"ifc:// URLs in Forgejo issues and PR comments not documented as a collaboration tool","description":"Forgejo's markdown extension renders ifc:// URLs as clickable 3D preview images in file README views, but it works equally in issue bodies, PR descriptions, and comment threads — which is where the actual collaboration happens. This is potentially the most useful feature: a reviewer can paste an ifc:// URL in a PR comment and everyone in the thread sees the 3D view inline. This is not mentioned anywhere in README.md, forgejo/README.md, or SPECIFICATION.md. Add a 'Collaboration workflow' section to the README showing: (1) navigate to view in viewer, (2) copy URL from address bar, (3) paste as [label](ifc://...) in a Forgejo comment/issue, (4) collaborators click the preview image to open the view.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:44Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:24Z","started_at":"2026-04-24T11:14:59Z","closed_at":"2026-04-24T11:47:24Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-n00","title":"Forgejo markdown previews fail silently for private repos — no auth path from preview service","description":"The Forgejo markdown extension generates \u003cimg src='http://localhost:8000/preview?url=ifc://...'\u003e tags. The preview service fetches the IFC file server-side using GET /preview with no authentication. For private repos, this returns HTTP 404 or 403 and the preview image shows as broken. There is no mechanism for the Forgejo markdown renderer to pass the user's session token to the preview service. Options to investigate: (1) the Forgejo integration could pass a signed request token; (2) the preview service could run as the Forgejo git user (which has read access to all repos); (3) SSH-based fetch from the service using the Forgejo server's own key. The current state is a silent failure that will confuse users.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:39Z","created_by":"Bruno Postle","updated_at":"2026-04-24T12:39:46Z","started_at":"2026-04-24T11:56:30Z","closed_at":"2026-04-24T12:39:46Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-2y6","title":"Private Forgejo repos work in viewer via session cookie (same-origin) but this is undocumented and untested","description":"When the viewer is served at https://forgejo.example.com/assets/viewer.html, toRawUrl() constructs a same-origin URL like https://forgejo.example.com/org/repo/raw/branch/main/model.ifc. The browser automatically attaches the Forgejo session cookie to this fetch, so private repos on the same Forgejo instance should be accessible to logged-in users without any extra configuration. This is a significant capability that is not documented or tested. Needs: (1) a test or note confirming this works, (2) documentation in forgejo/README.md explaining that private repos work automatically for logged-in users when the viewer is served from the same Forgejo instance, (3) clear note that external repos (other Forgejo, GitHub, GitLab) have no auth path from the browser viewer.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:26Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:25Z","started_at":"2026-04-24T11:15:00Z","closed_at":"2026-04-24T11:47:25Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-u8a","title":"PR review: no before/after 3D render when an IFC file is modified","description":"When a Forgejo PR modifies a .ifc file, the Files Changed tab shows raw STEP text diff — completely unreadable. Reviewers cannot see what geometry changed. The system should automatically add before/after rendered PNG images to the PR description or as a PR check comment when an .ifc file is modified. The service can render both the base-branch version and the PR-branch version using the commit hexshas already available in the PR context. This is the single most important workflow gap for IFC code review — currently 100% of the visual review burden falls on the reviewer manually opening the viewer and navigating. This supersedes and expands the existing ifcurl-prc issue.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:21Z","created_by":"Bruno Postle","updated_at":"2026-04-25T06:18:01Z","started_at":"2026-04-25T05:52:14Z","closed_at":"2026-04-25T06:18:01Z","close_reason":"footer.tmpl Case 3 + Nginx proxy docs implemented","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-sgv","title":"BCF import in viewer: reconstruct ifc:// URL from a BCF viewpoint file","description":"BCF is a standard exchange format for BIM issue tracking. Reviewers using other tools (Solibri, Navisworks, BIMcollab) will generate BCF files. There is currently no way to open a BCF file in the viewer to see the referenced view. Implement BCF import: accept a .bcf zip via drag-and-drop or a file picker, parse the first viewpoint's camera/clips, construct an ifc:// URL from the Components/Selection GUIDs (as a GUID-based selector), and load the view. This also completes SPECIFICATION.md §6 direction 2. The ifc:// URL base (repo, ref, path) cannot be recovered from BCF alone — the viewer should keep the current model context and only apply the viewpoint.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:07Z","created_by":"Bruno Postle","updated_at":"2026-04-24T12:39:47Z","started_at":"2026-04-24T11:56:31Z","closed_at":"2026-04-24T12:39:47Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-qcw","title":"Viewer: no 'Create Forgejo issue' button — BCF export and Forgejo issues are disconnected","description":"The BCF export downloads a file that the reviewer must then manually attach to a Forgejo issue. There is no direct path from 'I found a problem in the viewer' to 'this is now a Forgejo issue'. The feedback loop — reviewer flags, designer fixes, reviewer verifies — requires the issue to live in Forgejo where the code/model review happens. A 'Report issue' button in the viewer should call the Forgejo API (POST /api/v1/repos/{owner}/{repo}/issues) with the current ifc:// URL pre-filled in the issue description, so the issue body automatically renders a clickable 3D preview. The viewer already knows the repo path (from the URL) and when served by Forgejo the user is already authenticated.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:03Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:24Z","started_at":"2026-04-24T11:14:58Z","closed_at":"2026-04-24T11:47:24Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-unu","title":"Viewer: no 'Copy URL' / share button — relies on users knowing to use the address bar","description":"The viewer continuously updates the browser address bar via history.replaceState so the current view is always shareable. But there is no visible 'Copy link to this view' button. Non-technical users (clients, project managers) will not know to copy from the address bar, especially when the URL is long and truncated. A clipboard button alongside the BCF button — or a dedicated Share toolbar item — would make the core sharing workflow discoverable. Should copy the ifc:// URL (from urlInput, which reflects current camera state) to the clipboard and briefly confirm with a visual indicator.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:02:49Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:23Z","started_at":"2026-04-24T11:14:38Z","closed_at":"2026-04-24T11:47:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-9rk","title":"pyproject.toml: bcf package not listed as optional service dependency","description":"service.py and bcf.py work correctly because the 'bcf' Python library is installed, but it is not listed in pyproject.toml's [service] or [render] optional dependencies. A fresh install with 'pip install ifcurl[service]' may not install it. Add bcf to the [service] extras to make the dependency explicit.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:19Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:27Z","started_at":"2026-04-24T10:50:11Z","closed_at":"2026-04-24T10:56:27Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-czd","title":"GET /preview has no token parameter — private repos inaccessible","description":"POST /preview accepts a token field in the JSON body for private repository authentication. GET /preview (used by Forgejo \u003cimg src=\u003e tags) only takes a url query parameter with no way to pass a token. The Go markdown extension generates img tags using the GET form. This means inline previews in Forgejo markdown can never authenticate to private repos — they will silently fail or return 404. Either the Go extension should use POST, or a separate signed/session-scoped token mechanism is needed for the GET path.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:17Z","created_by":"Bruno Postle","updated_at":"2026-04-24T12:39:46Z","started_at":"2026-04-24T11:56:09Z","closed_at":"2026-04-24T12:39:46Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-3n4","title":"viewer-url.js: toRawUrl GitLab host detection too broad","description":"toRawUrl() uses host.includes('gitlab') to detect GitLab instances, which matches any hostname containing the string 'gitlab' — e.g. 'not-actually-gitlab.example.com' would be routed to the GitLab raw URL pattern. Should use a more specific check: host === 'gitlab.com' or host.startsWith('gitlab.') to avoid false positives on unrelated hostnames.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:01Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:28Z","started_at":"2026-04-24T10:50:14Z","closed_at":"2026-04-24T10:56:28Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-7wg","title":"service.py: /bcf endpoint doesn't include source URL in BCF description","description":"The viewer's client-side BCF generation records the source ifc:// URL as a \u003cDescription\u003e element in the markup, making the BCF traceable back to its origin. The service POST /bcf endpoint calls build_bcf() without passing any description, so service-generated BCF files contain no reference to the original URL. The URL should be included (in Description or a comment) so the BCF is self-documenting. build_bcf() needs a description parameter, or the url should be passed as comment/description automatically.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:17Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:17Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-sto","title":"viewer.html: BCF export ignores visibility mode from URL","description":"The viewer's client-side generateBcf() always writes '\u003cVisibility DefaultVisibility=\"true\"/\u003e' and never adds Selection or Exceptions elements based on the visibility= URL parameter. A URL with visibility=isolate or visibility=ghost generates BCF with no component visibility data, so a BCF tool importing the file will show all elements with no selection. The service /bcf endpoint handles this correctly. The viewer-side BCF should at minimum write the selector string as a Description so the visibility intent is preserved.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:14Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:18Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-t1k","title":"git.py: _evict_if_needed not called after fetch, only after clone","description":"_evict_if_needed() is called in _open_remote() only after a fresh clone (the 'if not git_dir.exists()' branch). When a mutable ref triggers a git fetch on an existing clone, the repo grows with new objects but eviction is never checked. Over time a long-running service can exceed IFCURL_CACHE_MAX_GB without triggering cleanup. Fix: call _evict_if_needed() after fetch as well as after clone.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:03Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:28Z","started_at":"2026-04-24T10:50:16Z","closed_at":"2026-04-24T10:56:28Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ttb","title":"README.md: documents GET /viewer endpoint that doesn't exist","description":"The preview service endpoints table in README.md lists 'GET /viewer?url=ifc://… — Redirect to the Forgejo browser viewer' but no such route exists in ifcurl/service.py. The viewer is served directly by Forgejo at /assets/viewer.html. The service only has POST /preview, GET /preview, and POST /bcf. Remove the bogus row or implement the redirect.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:05:00Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:27Z","started_at":"2026-04-24T10:50:13Z","closed_at":"2026-04-24T10:56:27Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-2v9","title":"Security audit: preview endpoint SSRF risk","description":"The /preview endpoint accepts an ifc:// URL and calls fetch_ifc(), which clones or fetches from whatever git host is named in the URL. This is a potential SSRF vector: a caller could supply ifc://internal-host/... to trigger outbound connections to internal infrastructure. Assess: (1) should the service validate that the host is in an allowlist or is a public forge? (2) are there other parameters that could be abused? (3) is the intended deployment model (co-located with Forgejo, not exposed publicly) a sufficient mitigation, or should we add explicit controls regardless? Document the threat model and add any necessary validation.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T07:09:33Z","created_by":"Bruno Postle","updated_at":"2026-04-24T07:21:07Z","started_at":"2026-04-24T07:12:29Z","closed_at":"2026-04-24T07:21:07Z","close_reason":"Implemented: reject local transport (403), --allowed-hosts allowlist for serve command, and private-IP literal blocking as defense-in-depth","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-bab","title":"Implement clip= parameter in viewer.html","description":"The ifc:// spec defines a repeatable clip= parameter (6 floats: point x,y,z + normal x,y,z in IFC world coords). The Python ifcurl renderer handles it, but viewer.html ignores it entirely — no parsing, no Three.js clipping planes, no round-trip into the URL on camera sync. Implement: parse clip= from the ifc:// URL, apply as THREE.Plane clipping planes to the renderer (with IFC→Three.js coordinate transform), and include current clip planes when rebuilding the URL.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:09:31Z","started_at":"2026-04-24T06:08:44Z","closed_at":"2026-04-24T06:09:31Z","close_reason":"applyClipPlanes() parses all clip= params, transforms IFC→Three.js coords via toThree(), constructs THREE.Plane objects and sets on world.renderer.three.clippingPlanes. syncCameraUrl already preserves clip= params unchanged.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-yju","title":"viewer.html: structured form UI for ifc:// URL components","description":"Replace the single ifc:// URL text input with individual labelled fields for each component: host/repository, ref (branch/tag/commit), path (file within repo), selector (IfcOpenShell selector), camera, fov/scale. Editing any field should update the ifc:// URL and reload the model. The raw ifc:// URL should still be visible/copyable. This makes the viewer usable without needing to hand-edit a URL string.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T21:07:11Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:32:17Z","started_at":"2026-04-23T21:12:42Z","closed_at":"2026-04-23T21:32:17Z","close_reason":"Structured form UI implemented with repo/ref/path/selector fields; crash recovered, changes committed.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-2zf","title":"viewer.html: FOV control and restore fov=/scale= from URL","description":"Two gaps: (1) No UI to change the camera FOV — add a control (e.g. a small number input or slider in the toolbar) so the user can adjust perspective FOV or orthographic scale. (2) applyCameraParam reads position/direction but ignores the fov= and scale= params written by syncCameraUrl — restore these when placing camera from URL so a shared link opens at exactly the recorded viewpoint.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:24Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:07:42Z","started_at":"2026-04-23T20:52:48Z","closed_at":"2026-04-23T21:07:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-yga","title":"viewer.html: implement ifc:// selector= parameter","description":"The ifc:// URL spec supports a selector= param using IfcOpenShell selector syntax (e.g. selector=IfcWall, selector=IfcSlab.Name='Floor'). The viewer currently ignores it. Implement selector filtering in the browser: parse the selector= value, query matching elements from the loaded FragmentsModel, and apply highlight/isolate/ghost visibility to them. @thatopen/components has Highlighter and Hider components that may be usable. The selector syntax used is the IfcOpenShell Python selector syntax — a JS port or subset may be needed.","notes":"Selector syntax docs: https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html — Python implementation in ifcopenshell.util.selector module. Need a JS port or subset. Syntax includes: IfcType, .Attribute='value', #GlobalId, [pset.prop], boolean operators (+ for union, , for intersection within group).","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:58:30Z","started_at":"2026-04-24T05:51:01Z","closed_at":"2026-04-24T05:58:30Z","close_reason":"applySelector() implemented: byCategory classification + prefix subtype matching + hider.isolate(). Supports simple type names and + union. Attribute filters noted as future work.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-set","title":"viewer.html: generalise toRawUrl() to support GitHub, GitLab etc.","description":"toRawUrl() currently only handles Forgejo/Gitea HTTPS URLs, mapping ifc://host/org/repo@hash?path=file to http(s)://host/org/repo/raw/commit/hash/file. This needs to: (1) detect the git host type and construct the correct raw file URL — GitHub → raw.githubusercontent.com/org/repo/hash/file; GitLab → host/org/repo/-/raw/hash/file; Forgejo/Gitea → host/org/repo/raw/commit/hash/file; (2) handle SSH ifc:// URLs (user@host form per the spec) by stripping the user@ prefix and rewriting to HTTPS for browser fetching — ifc://git@github.com/org/repo@hash becomes https://raw.githubusercontent.com/org/repo/hash/file.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:09Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:07:42Z","started_at":"2026-04-23T20:52:47Z","closed_at":"2026-04-23T21:07:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-1sh","title":"viewer.html: place camera from ifc:// URL camera= param on load","description":"When viewer.html loads with a camera= param in the ifc:// URL (9 comma-separated floats: position, direction, up in IFC world coordinates), apply the IFC→Three.js coordinate transform (Y/Z swap, one negated) and set the Three.js camera position, direction and up vector accordingly. Depends on the syncCameraUrl fix (ifcurl-j2s) so the forward and inverse transforms are consistent.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:05:57Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:45:10Z","started_at":"2026-04-23T20:09:26Z","closed_at":"2026-04-23T20:45:10Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-1sh","depends_on_id":"ifcurl-j2s","type":"blocks","created_at":"2026-04-23T21:06:08Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-940","title":"viewer.html: URL input box and drag-and-drop target","description":"Add a text entry box and a drag-and-drop target to viewer.html so any ifc:// URL can be loaded without navigating from a Forgejo page. The input should accept a pasted ifc:// URL and immediately load it into the viewer. Drag-and-drop should accept a dropped ifc:// URL or a .ifc file URL. This makes the viewer usable as a standalone tool.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:05:43Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:45:08Z","started_at":"2026-04-23T20:09:23Z","closed_at":"2026-04-23T20:45:08Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-cmd","title":"Apply forward IFC→Three.js transform when setting camera from URL params","description":"When the viewer page loads with a camera= param in the ifc:// URL, the 9 floats are in IFC world coordinates. Apply the IFC→Three.js transform to convert them before passing them to the Three.js camera / camera-controls. This is the forward direction of the same transform used in the inverse task.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:38Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:45:09Z","started_at":"2026-04-23T20:09:25Z","closed_at":"2026-04-23T20:45:09Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-cmd","depends_on_id":"ifcurl-ldk","type":"blocks","created_at":"2026-04-23T07:14:10Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-j2s","title":"Apply inverse IFC↔Three.js transform in syncCameraUrl","description":"Using the transform found in the research task, apply its inverse to camera.position, the direction vector, and camera.up before serialising them as the ifc:// camera param in syncCameraUrl. The result should be in IFC world coordinates (Z-up, matching BCF viewpoint conventions) not Three.js scene space.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:37Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:45:09Z","started_at":"2026-04-23T20:09:25Z","closed_at":"2026-04-23T20:45:09Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-j2s","depends_on_id":"ifcurl-ldk","type":"blocks","created_at":"2026-04-23T07:14:09Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-ldk","title":"Find IFC↔Three.js coordinate transform in @thatopen/components source","description":"Grep ~/src/engine_components and ~/src/engine_fragment for the matrix or transform applied when an IFC model is loaded into Three.js scene space. IFC uses Z-up; Three.js uses Y-up. @thatopen/components almost certainly applies a 90° X-axis rotation (and possibly a scale) when constructing the Three.Group from IFC geometry. Record the exact transform so the inverse can be applied in syncCameraUrl.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:36Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:05:29Z","started_at":"2026-04-23T06:19:35Z","closed_at":"2026-04-23T20:05:29Z","close_reason":"Transform identified: IFC→Three.js is a Y/Z swap with one axis negated (Z-up to Y-up). Unblocks j2s and cmd.","dependency_count":0,"dependent_count":2,"comment_count":0}
{"id":"ifcurl-49o","title":"Deploy footer.tmpl to Forgejo custom templates directory","description":"Copy forgejo/templates/custom/footer.tmpl from the repo to /etc/forgejo/templates/custom/footer.tmpl on the Forgejo server. No Forgejo restart needed — custom templates are loaded dynamically. Verify the View in 3D button appears on an .ifc file page.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:22Z","created_by":"Bruno Postle","updated_at":"2026-04-23T19:47:16Z","closed_at":"2026-04-23T19:47:16Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-49o","depends_on_id":"ifcurl-ohq","type":"blocks","created_at":"2026-04-23T07:14:08Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-ohq","title":"Update footer.tmpl: point viewer button to Forgejo static file URL","description":"Change the VIEWER_BASE URL in forgejo/templates/custom/footer.tmpl from http://localhost:8000/viewer to the Forgejo static file URL determined by the research task (ifcurl-5t3). The button href should become something like /assets/viewer.html?url=ifc://... rather than the ifcurl service URL.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:21Z","created_by":"Bruno Postle","updated_at":"2026-04-23T19:47:15Z","started_at":"2026-04-23T18:48:37Z","closed_at":"2026-04-23T19:47:15Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-ohq","depends_on_id":"ifcurl-5t3","type":"blocks","created_at":"2026-04-23T07:14:07Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-4yu","title":"Deploy viewer.html to Forgejo server","description":"Copy the viewer static file from the repo to the correct location on the Forgejo server so it is served at its expected URL. Document the deployment step in the repo (e.g. Makefile target or install notes).","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:11Z","created_by":"Bruno Postle","updated_at":"2026-04-23T19:47:16Z","closed_at":"2026-04-23T19:47:16Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-4yu","depends_on_id":"ifcurl-55f","type":"blocks","created_at":"2026-04-23T07:14:05Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-cuh","title":"Remove /viewer and /proxy endpoints from service.py","description":"Once the viewer HTML is served as a Forgejo static file and fetches IFC bytes directly from Forgejo (using the browser session for auth), the /viewer and /proxy endpoints in ifcurl/service.py are defunct. Remove them and clean up the associated VIEWER_HTML constant in viewer.py.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:10Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:05:04Z","started_at":"2026-04-23T19:54:44Z","closed_at":"2026-04-23T20:05:04Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-cuh","depends_on_id":"ifcurl-55f","type":"blocks","created_at":"2026-04-23T07:14:06Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-55f","title":"Move viewer HTML to Forgejo custom static file","description":"Extract the VIEWER_HTML constant from ifcurl/viewer.py and write it as a static HTML file in the repo under the Forgejo custom static directory (path determined by the research task). The file should be a standalone page — no dependency on the ifcurl service for serving.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:12:09Z","created_by":"Bruno Postle","updated_at":"2026-04-23T19:47:15Z","started_at":"2026-04-23T18:48:26Z","closed_at":"2026-04-23T19:47:15Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-55f","depends_on_id":"ifcurl-5t3","type":"blocks","created_at":"2026-04-23T07:14:03Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
{"id":"ifcurl-5t3","title":"Research Forgejo custom static file serving","description":"Determine where Forgejo looks for custom static files and what URL they get served at. Check the Forgejo docs and source for the custom/ static directory convention. The result feeds into the viewer HTML deployment and the footer.tmpl URL update.","notes":"Forgejo serves custom static files from \u003cCustomPath\u003e/public/assets/ at URL /assets/*. CustomPath is /var/lib/forgejo/custom on this system. No auth required. Any file type. Place viewer.html at /var/lib/forgejo/custom/public/assets/viewer.html → served at /assets/viewer.html.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T06:11:58Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:50:05Z","started_at":"2026-04-23T06:19:46Z","closed_at":"2026-04-23T06:50:05Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0}
{"id":"ifcurl-7x1","title":"Forgejo goldmark extension: handle bare ifc:// URLs in markdown","description":"The goldmark extension in the patched Forgejo binary (modules/markup/markdown/ifc_url.go) currently only detects ifc:// URLs when written in explicit markdown link form [Title](ifc://...). It should also auto-link bare ifc:// URLs pasted inline in markdown text, the same way goldmark auto-links http:// URLs. Extend the extension (or add a separate autolink pass) to detect raw ifc:// URL tokens and wrap them in the preview embed.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:58:02Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:50:06Z","started_at":"2026-04-23T06:19:47Z","closed_at":"2026-04-23T06:50:06Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ahd","title":"Serve viewer HTML as Forgejo static files instead of ifcurl /viewer endpoint","description":"Currently the viewer is served from the ifcurl preview service at GET /viewer, and IFC bytes are fetched via GET /proxy to work around CORS. This has two problems: it pipes all IFC data through the ifcurl service (bandwidth/latency), and it bypasses the browser's existing Forgejo authentication (cookies/session) so private repos won't work. Better architecture: deploy the viewer HTML as a Forgejo custom static file (e.g. /assets/viewer.html or via a custom template). The viewer JS can then fetch IFC bytes directly from Forgejo's raw file URL using the browser's existing session, so auth is inherited automatically and no proxy is needed. The /viewer and /proxy endpoints in ifcurl/service.py can be removed once this is working.","status":"closed","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:56:15Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:34Z","closed_at":"2026-04-23T06:14:34Z","close_reason":"Split into atomic tasks: ifcurl-5t3, ifcurl-55f, ifcurl-cuh, ifcurl-4yu","dependency_count":0,"dependent_count":2,"comment_count":0}
{"id":"ifcurl-ei8","title":"Fix syncCameraUrl: convert WebGL coords to IFC world space","description":"syncCameraUrl in viewer.py currently records camera.position/direction/up directly from Three.js, which are in WebGL/Three.js coordinate space (Y-up, different scale/orientation than IFC). The ifc:// URL spec expects IFC world coordinates (Z-up, matching BCF viewpoint conventions). Need to find the IFC→Three.js transform that @thatopen/components applies when loading a model, then apply its inverse in syncCameraUrl before serialising the camera params. Repos cloned for reference: ~/src/engine_components and ~/src/engine_fragment.","status":"closed","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-23T05:53:06Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:36Z","closed_at":"2026-04-23T06:14:36Z","close_reason":"Split into atomic tasks: ifcurl-ldk, ifcurl-j2s, ifcurl-cmd","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-i1s","title":"Test Phase 3b web viewer end-to-end","description":"After deploying the footer template (ifcurl-ach), test the full flow: open an .ifc file in Forgejo at localhost:3000, click 'View in 3D', verify the viewer loads the model. CDN versions (@thatopen/components 2.4.0, three 0.160.0, web-ifc 0.0.66) may need adjustment if the API has changed.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:01Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:05:04Z","started_at":"2026-04-23T19:54:56Z","closed_at":"2026-04-23T20:05:04Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-49o","type":"blocks","created_at":"2026-04-23T07:14:12Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-4yu","type":"blocks","created_at":"2026-04-23T07:14:11Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ach","type":"blocks","created_at":"2026-04-23T06:48:10Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:54Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-gy8","title":"Viewer: 'View in 3D' uses commit hash but no UI affordance to switch to branch tip","description":"The 'View in 3D' button in footer.tmpl always generates an ifc:// URL with a commit hash (@abc123...). This is correct for reproducibility, but users who want to track 'the latest version of main' must manually change the URL from @\u003chash\u003e to @heads/main. There is no indication in the viewer that the model is pinned to a specific commit. Add a visual indicator (e.g. 'commit abc1234 · switch to heads/main') in the viewer toolbar when the ref is a commit hash, offering a one-click switch to the branch tip. The branch name could be inferred from Forgejo's API given the commit hash.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:58Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-dag","title":"Viewer: no model metadata panel — disciplines, spatial structure, element counts","description":"Beyond individual element selection, reviewers and designers need a model overview: what disciplines/types are present, how many elements, what building storeys exist, what's the spatial structure (site/building/storey/space hierarchy). A collapsible side panel showing IFC spatial tree and type breakdown would give immediate context and allow navigating by storey or discipline. Without this, reviewers must already know the model structure before opening the viewer.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:55Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:55Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ov6","title":"Viewer selector only supports type names — property filters require server round-trip","description":"The viewer's applySelector() only supports IFC type names and their unions ('IfcWall+IfcSlab'). Full IfcOpenShell selector syntax — filter by property value (Pset_WallCommon.FireRating=2HR), by GlobalId, by name (.Name='Core Wall'), by spatial containment — is not supported. For collaboration, reviewers need to select specific elements ('all walls with fire rating 2HR that aren't compliant') not just all walls. The service's /preview endpoint already handles full selector syntax server-side. The viewer should forward complex selectors to the service and use the rendered image, or fetch the matching GlobalIds from a new /resolve-selector service endpoint and apply them in the viewer.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:38Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:38Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-f3p","title":"footer.tmpl: 'View in 3D' button absent from file history (git log) page","description":"The 'View in 3D' button is injected on the file view page (showing one commit of a file). It is not shown on the file history page (the list of commits that touched a file), which is where users naturally go to see how a model evolved over time. Each entry in the history list already has a commit hash in its href. Injecting a 'View in 3D' link or button alongside each history entry would let reviewers quickly compare model states across commits — which is a primary collaboration use case.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:23Z","created_by":"Bruno Postle","updated_at":"2026-04-24T14:04:56Z","started_at":"2026-04-24T13:21:51Z","closed_at":"2026-04-24T14:04:56Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-4rj","title":"Viewer: no branch/ref switcher — can't navigate model history or compare versions","description":"The viewer loads a single ifc:// URL and has no UI to switch to a different ref. A reviewer wanting to see 'what did this look like on main before the PR' or 'show me the v1.2 tag' must manually edit the URL. For a collaboration tool, being able to flip between 'current PR branch' and 'base branch' at the same camera view is essential for review. Consider a ref dropdown or input in the toolbar that reconstructs the URL with a different @ref, preserving camera/clip/selector state. The structured fields already expose repo/ref/path — the ref field is editable but not prominently labelled as a version picker.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:20Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:20Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-np8","title":"Viewer depends on CDN (unpkg, esm.sh) — broken in air-gapped or restricted networks","description":"viewer.html loads all JavaScript from public CDNs: three.js from esm.sh, @thatopen/components and fragments from esm.sh/unpkg, web-ifc WASM from unpkg, the fragment worker from unpkg, JSZip from esm.sh. In corporate BIM environments with restricted internet (common in construction/engineering), the viewer will fail to load entirely. There is no documented self-hosted path. Consider: document how to serve the dependencies from the Forgejo instance's own /assets/ directory, or provide a bundled viewer.html as a build artifact. The viewer worker fetch ('https://unpkg.com/@thatopen/fragments@3.3.1/dist/Worker/worker.mjs') is already fetched at runtime with a special workaround for Firefox — this is the most fragile CDN dependency.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:04Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:04Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-sa0","title":"Viewer: no load progress — large IFC files appear to hang","description":"The viewer status shows 'Fetching IFC file…' and 'Parsing IFC…' as plain text but gives no indication of how far along the download or parse is. IFC files in real projects are often 20200MB. The fetch() call and the web-ifc WASM parser both block for seconds with no visual feedback. A progress bar or percentage indicator during fetch (using response.body ReadableStream + Content-Length header) and a spinner during parse would prevent users abandoning the page thinking it crashed. The status element exists and is already used — it just needs richer updates.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:23Z","started_at":"2026-04-24T11:14:56Z","closed_at":"2026-04-24T11:47:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-4xl","title":"viewer.html: selector support is a subset of IfcOpenShell syntax — not documented","description":"applySelector() in the viewer only handles type-name union syntax ('IfcWall+IfcSlab'). The comment says 'Attribute/property filters not yet supported', but this limitation is not visible to users and not documented in README.md or SPECIFICATION.md. A user pasting a URL with 'selector=IfcWall,+Pset_WallCommon.FireRating=2HR' will see all elements rather than the filtered set, with no error. Should add a user-visible note or at minimum document the viewer's selector limitations.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:49Z","created_by":"Bruno Postle","updated_at":"2026-04-24T14:04:57Z","started_at":"2026-04-24T13:22:10Z","closed_at":"2026-04-24T14:04:57Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-fu5","title":"service.py: no rate limiting or request size cap on /preview and /bcf","description":"The preview and BCF endpoints have no rate limiting, no cap on IFC file size fetched, and no timeout on the render operation. A single large IFC file or a flood of requests can saturate memory and CPU. Consider: max IFC blob size (reject fetch if commit object too large), render timeout via threading.Timer, and a simple in-process rate limiter or documentation that a reverse proxy (nginx) should front the service with rate limits.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:45Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:41:45Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-d58","title":"ifc_url_test.go: no test for bare URL with path containing @ character","description":"The Go transformer reassembles multi-sibling text nodes for bare ifc:// URLs. There is a test for underscores in the path (which goldmark splits on), but no test for paths or query values containing '@', which is a valid character in query strings and could appear in selector expressions. Also missing: test for a bare URL followed immediately by punctuation like a period or closing parenthesis to verify the terminator logic.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:32Z","created_by":"Bruno Postle","updated_at":"2026-04-24T14:04:56Z","started_at":"2026-04-24T13:22:09Z","closed_at":"2026-04-24T14:04:56Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-7mg","title":"auth.py: token appears in git process arguments, visible in process list","description":"inject_token() embeds the authentication token into the HTTPS remote URL as 'https://\u003ctoken\u003e@host/path'. This URL is passed directly to git clone/fetch as a command-line argument, which means the token is visible in the process list (ps aux) while the git command runs. On a shared server this is a security leak. The standard mitigation is to use GIT_ASKPASS or a credential helper that supplies the token via stdin/environment rather than via the URL. Should be documented as a known limitation at minimum.","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:29Z","created_by":"Bruno Postle","updated_at":"2026-04-24T15:52:13Z","started_at":"2026-04-24T14:47:34Z","closed_at":"2026-04-24T15:52:13Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-z4f","title":"render.py: concurrent requests each spawn full CPU-count process pools","description":"render() calls ifcopenshell.geom.iterator(settings, model, multiprocessing.cpu_count(), ...). In a multi-request service scenario, N concurrent renders each spawn cpu_count worker processes, so a 16-core machine handling 4 simultaneous requests spawns 64 processes. This can saturate the machine and is effectively a DoS amplifier. Consider capping the worker count (e.g. max 2 or 4 per request), or using a shared process pool at the service level.","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:16Z","created_by":"Bruno Postle","updated_at":"2026-04-24T15:52:14Z","started_at":"2026-04-24T14:47:54Z","closed_at":"2026-04-24T15:52:14Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-5or","title":"README.md: library usage example doesn't show temp file pattern for ifcopenshell.open()","description":"The 'Use as a library' section shows 'model = ifcopenshell.open(...) # write ifc_bytes to tmp file first' with a comment but no actual code. ifcopenshell.open() requires a file path, not bytes. A new user following the README will be stuck. The example should show the tempfile.mkstemp pattern used in _load_model() in service.py, or at least reference that function.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:04Z","created_by":"Bruno Postle","updated_at":"2026-04-24T15:52:14Z","started_at":"2026-04-24T14:47:55Z","closed_at":"2026-04-24T15:52:14Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-dyy","title":"viewer.html: BCF export from viewer omits component GUIDs — undocumented","description":"The viewer's client-side BCF export cannot include component GUIDs because it has no access to the IFC model's resolved GlobalIds. The service POST /bcf endpoint resolves the selector server-side and produces proper BCF with \u003cSelection\u003e or \u003cVisibility/Exceptions\u003e elements. This discrepancy is not documented anywhere. The viewer's BCF button should either show a note ('GUIDs not resolved — use /bcf API for full component selection') or redirect to the service endpoint when available.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:49Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:40:49Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-tq8","title":"No tests for ifcurl cache CLI subcommands","description":"The 'ifcurl cache list', 'ifcurl cache prune', and 'ifcurl cache clear' subcommands in __main__.py have no automated test coverage. _repo_cache_entries(), _evict_if_needed(), and _get_max_cache_bytes() have unit tests in test_git.py, but the CLI layer (_cmd_cache) is untested. Should add tests that exercise the argparse wiring, the output formatting, and the prune logic via subprocess or by calling _cmd_cache() directly with a fake args namespace.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:45Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:40:45Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ovi","title":"dev/compose.yml uses Forgejo v10, but go.patch targets v13","description":"dev/compose.yml pulls codeberg.org/forgejo/forgejo:10 for local development. The go.patch and ifc_url.go were written and tested against the v13 branch. Running the dev environment with v10 won't have the patched binary or the correct template slots. The compose file should either use the patched v13 build, or document that the stock container is for testing the service/viewer in isolation only, not for testing the Go patch.","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:35Z","created_by":"Bruno Postle","updated_at":"2026-04-25T09:05:41Z","started_at":"2026-04-25T09:05:21Z","closed_at":"2026-04-25T09:05:41Z","close_reason":"Updated image to v13 and added comment clarifying stock container doesn't include the IFC patch","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-j35","title":"git.py: silent failure on mutable ref fetch serves stale data without warning","description":"In _open_remote(), when fetching for a mutable ref (branch/HEAD) fails — network error, auth failure, server down — the exception is caught and ignored ('pass # offline — use cached data'). The caller receives stale data with no indication anything went wrong. The render then proceeds, potentially showing outdated geometry. At minimum a log.warning should be emitted. Ideally the service response should include a header (e.g. X-Ifcurl-Cache: stale) so Forgejo or the browser viewer can surface a warning to the user.","status":"closed","priority":3,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-24T10:40:31Z","created_by":"Bruno Postle","updated_at":"2026-04-25T08:05:18Z","closed_at":"2026-04-25T08:05:18Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-69l","title":"License headers for Forgejo integration files","description":"The Go patch files (ifc_url.go, ifc_url_test.go) already carry MIT headers matching Forgejo's per-file convention. The JS/HTML assets have no license header: viewer.html, viewer-url.js, footer.tmpl. Review: (1) confirm MIT is correct for files intended for Forgejo contribution (Forgejo project is GPL-3.0 but individual contributed files use MIT); (2) check compatibility with ThatOpen components licence (viewer.html loads @thatopen/components from CDN); (3) add SPDX licence headers to the three unlicensed asset files. The Python/CLI core remains LGPL-3.0-or-later as it targets IfcOpenShell contribution.","status":"closed","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T07:09:46Z","created_by":"Bruno Postle","updated_at":"2026-04-24T07:29:22Z","closed_at":"2026-04-24T07:29:22Z","close_reason":"Added MIT SPDX headers to viewer.html, viewer-url.js, footer.tmpl — ThatOpen components also MIT-licensed so no compatibility issue","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-8s8","title":"viewer.html: interactive tool to set clipping planes","description":"Currently clip= planes can only be set by hand-editing the URL. Add an interactive clipping tool to the viewer: a UI control (e.g. toolbar button) that lets the user drag/place a clipping plane against the loaded model. The plane position and normal should be written back into the ifc:// URL as clip= parameters so the view is shareable. Consider using ThatOpen's clipper component (OBC.Clipper) if available, or a Three.js-based approach.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:49:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:23:14Z","started_at":"2026-04-24T06:11:49Z","closed_at":"2026-04-24T06:23:14Z","close_reason":"setupClipper() uses OBC.Clipper: seeds URL planes as draggable SimplePlanes, toolbar '✂ clip' toggle for double-click placement, '⊗ clips' clear button. syncCameraUrl extended to serialise current planes back to URL.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-07a","title":"Cache size control for bare repo clones","description":"git.py clones remote repos as bare repos under ~/.cache/ifcurl/\u003chash\u003e/repo.git with no size limit or eviction policy. Large repos accumulate indefinitely. Add a configurable max cache size (e.g. via env var IFCURL_CACHE_MAX_GB or a config file) with an LRU eviction policy based on last-access time of each repo directory. Should also expose a CLI command (ifcurl cache --list, --prune, --clear) to inspect and manage the cache manually.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:59Z","created_by":"Bruno Postle","updated_at":"2026-04-24T08:06:23Z","started_at":"2026-04-24T07:48:28Z","closed_at":"2026-04-24T08:06:23Z","close_reason":"IFCURL_CACHE_MAX_GB env var for LRU disk eviction; IFCURL_T2_MAX/T3_MAX for in-memory limits; ifcurl cache list/prune/clear CLI; remote_url stored per clone","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ts5","title":"Assess Go test strategy for Forgejo plugin patch","description":"The Forgejo integration is a low-maintenance source patch (footer.tmpl, viewer.html, /viewer and /proxy HTTP handlers). Evaluate whether adding Go tests is feasible without coupling to Forgejo's full test suite: can the handler logic be extracted into a small testable package, or do tests have to live inside the Forgejo tree? If the cost of keeping tests green across Forgejo upstream upgrades is too high, document the decision and close. Outcome: either a small test file or a documented rationale for no Go tests.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:57Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:51:21Z","started_at":"2026-04-24T06:36:35Z","closed_at":"2026-04-24T06:51:21Z","close_reason":"Go tests are feasible and low-maintenance. 7 tests in ifc_url_test.go live in the same package (markdown_test), use goldmark.New() directly (no Forgejo test infrastructure needed beyond TestMain which is already there). Only deps: setting.IfcURL.PreviewServiceURL (ours), goldmark (stable), testify. Test file copied to forgejo/modules/markup/markdown/ in ifcurl repo as patch artifact.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-hvw","title":"Add JavaScript tests for viewer.html URL logic","description":"viewer.html contains pure-JS functions (parseIfcUrl, buildIfcUrl, toRawUrl, syncCameraUrl) with no test coverage. Add a lightweight JS test harness (e.g. a standalone test.html or Node-runnable test.js using the native test runner) that unit-tests these functions: round-trip URL parse/build, toRawUrl for each host type (GitHub, GitLab, Forgejo), and camera parameter serialisation. No browser automation needed — pure function tests.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:56Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:30:47Z","started_at":"2026-04-24T06:26:01Z","closed_at":"2026-04-24T06:30:47Z","close_reason":"32 Node tests in tests/test_viewer_url.mjs covering parseIfcUrl, buildIfcUrl, toRawUrl. Pure functions extracted to viewer-url.js; viewer.html imports from it. Also caught + not being decoded in URL building.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-b9e","title":"Audit and extend Python test coverage","description":"103 tests exist across test_url.py, test_git.py, test_auth.py, test_render.py, and test_service.py. Audit what is missing: check branch/edge coverage in git.py (offline fallback, mutable ref fetch, auth injection), service.py (tier 2 cache eviction, mutable ref bypass), and the CLI entry points in __main__.py. Add tests for any significant gaps found.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:40Z","created_by":"Bruno Postle","updated_at":"2026-04-24T07:37:10Z","started_at":"2026-04-24T07:33:43Z","closed_at":"2026-04-24T07:37:10Z","close_reason":"Added 15 tests: SSRF protection (8), GET /preview endpoint (3), tier-2 LRU eviction (3), heads/\u003cbranch\u003e ref form (1)","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-bqt","title":"Phase 4: Bonsai IFCDOCUMENTREFERENCE federation via ifc://","description":"When opening a model via ifc://, resolve IFCDOCUMENTREFERENCE locations using the repo and ref from the root file's URL as context. Resolve ifc:// locations in linked model references recursively. Requires the Phase 4 protocol handler (ifcurl-0oj).","status":"deferred","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:56Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:11Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-bqt","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:02Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-lcr","title":"Phase 4: Bonsai 'Copy view URL' button","description":"Add a button to Bonsai that generates an ifc:// URL from the current state: git remote/ref/file path, active selection as IfcOpenShell selector, current camera as camera+fov or scale params in IFC world coordinates, active clipping planes as clip params, current visibility mode. Default to commit hash ref; secondary option for @heads/ ref.","status":"deferred","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:55Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:10Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-lcr","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:01Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-0oj","title":"Phase 4: Bonsai ifc:// OS protocol handler","description":"Register ifc:// as an OS protocol handler in Bonsai on installation (Windows, macOS, Linux). On receiving a URL, resolve the git source, load the model, and apply the view state from the URL parameters (camera, selector, clipping planes, visibility mode). This is the foundation for all other Phase 4 work.","status":"deferred","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:45Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:09Z","defer_until":"2026-12-01T00:00:00Z","dependency_count":0,"dependent_count":3,"comment_count":0}
{"id":"ifcurl-cyw","title":"Phase 3c: Verify Forgejo merge-ability check works with ifcmerge","description":"Investigate whether Forgejo's merge-ability check (the 'branches can be merged automatically' indicator) correctly reflects ifcmerge's ability to handle .ifc files. Determine if Forgejo runs a trial git merge or uses a heuristic. If needed, patch Forgejo to run a trial git merge --no-commit to get an accurate result.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T08:27:25Z","started_at":"2026-04-24T08:19:58Z","closed_at":"2026-04-24T08:27:25Z","close_reason":"Verified: git merge-tree --write-tree (Forgejo's code path for git ≥2.38) does invoke merge drivers. Bare repos require core.attributesFile in /etc/gitconfig — committed .gitattributes not read. Added gitattributes server-config file and updated deployment docs.","dependencies":[{"issue_id":"ifcurl-cyw","depends_on_id":"ifcurl-c69","type":"blocks","created_at":"2026-04-23T06:48:29Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-c69","title":"Phase 3c: Register ifcmerge as git merge driver on Forgejo server","description":"Configure ifcmerge as the git merge driver for .ifc files on the Forgejo server. Add to /etc/gitconfig or ~forgejo/.gitconfig: [merge \"ifcmerge\"] name = IFC merge driver / driver = ifcmerge %O %A %B %L. Add *.ifc merge=ifcmerge to server-side gitattributes. This allows Forgejo's 'merge automatically' path to invoke ifcmerge via git when merging .ifc files.","status":"closed","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:21Z","created_by":"Bruno Postle","updated_at":"2026-04-24T08:15:20Z","closed_at":"2026-04-24T08:15:20Z","close_reason":"Documented ifcmerge driver registration (gitconfig-ifcmerge), gitattributes setup, merge direction constraint, and merge-commit strategy requirement","comments":[{"id":"019dbc54-bf0a-76e2-b64d-7d1681f0c730","issue_id":"ifcurl-c69","author":"Bruno Postle","text":"ifcmerge needs to be installed as a pair of merge tools because the merge process is asymmetrical and we need to be able to prefer one branch over another (because step-ids from one branch get rewritten in the merged file). In practice we prefer origin and/or main/master branches. there is code in the ifcopenshell bonsai ifcgit module for reference.","created_at":"2026-04-23T21:52:44Z"}],"dependency_count":0,"dependent_count":1,"comment_count":1}
{"id":"ifcurl-u6r","title":"Forgejo commit page: no IFC thumbnail — git log gives no visual indication of model state","description":"In the Forgejo repository commit list, each commit shows only the commit message. When a commit modifies an IFC file, there is no thumbnail showing what the model looks like. Reviewers browsing commits have no visual cues about which commits changed significant geometry versus metadata-only changes. A Forgejo webhook or CI integration could post a rendered PNG thumbnail as a commit status/check comment when .ifc files change, using the preview service. This would make the git log visually meaningful for BIM projects.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:41Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:41Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-l50","title":"SPECIFICATION.md section 6: BCF-to-IFC-URL direction not implemented","description":"SPECIFICATION.md §6 defines both directions of BCF ↔ IFC URL conversion. The IFC URL → BCF direction is fully implemented (build_bcf(), /bcf endpoint, viewer BCF button). The BCF → IFC URL direction (extract GUIDs from Components/Selection, construct GUID selector, convert camera and clips to URL params) has no implementation. This would be useful for round-tripping: import a BCF viewpoint and get back a shareable ifc:// URL.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:01Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:41:01Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-8ns","title":"Future: Contribute ifc:// URL parsing to IfcOpenShell","description":"Once the Python core URL parsing and resolution logic is stable, contribute it to IfcOpenShell as a shared utility. Not a prerequisite for any phase — pursue after Phase 1 has been stable in production use for a while.","status":"open","priority":4,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:33Z","created_by":"Bruno Postle","updated_at":"2026-04-23T05:49:33Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-9yr","title":"Future: BCF export endpoint /bcf","description":"Add a /bcf endpoint to the preview service: POST /bcf with body {url: 'ifc://...'} returns a BCF zip file. Resolve the selector to a GUID set and construct a BCF viewpoint from the camera, clipping planes, and visibility state. Not planned before Phase 2 is complete.","status":"closed","priority":4,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:25Z","created_by":"Bruno Postle","updated_at":"2026-04-24T08:51:18Z","started_at":"2026-04-24T08:34:51Z","closed_at":"2026-04-24T08:51:18Z","close_reason":"bcf.py build_bcf() + /bcf POST endpoint + viewer BCF panel with JSZip client-side generation + 21 tests","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-vam","title":"Phase 6: Bonsai offer ifc:// URL when saving linked model reference","description":"When saving an IFCDOCUMENTREFERENCE in Bonsai, offer the option to write the location as an ifc:// URL rather than a relative path. When a relative path would traverse above the repository root, prompt the user to convert it to an ifc:// URL. Requires Phase 4 federation (ifcurl-bqt).","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:16Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:13Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-vam","depends_on_id":"ifcurl-bqt","type":"blocks","created_at":"2026-04-23T06:49:23Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-9k2","title":"Phase 5: IFC Viewer integration (equivalent of Phase 4)","description":"Add ifc:// OS protocol handler and 'Copy view URL' button to the IFC Viewer desktop application, equivalent to the Bonsai Phase 4 integration. Depends on Phase 4 being complete as a reference implementation.","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:08Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:12Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-9k2","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:14Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-prc","title":"Phase 3c: 3D diff view for IFC PR merges","description":"After a PR merge completes, surface a 'View merge in 3D' button using the Phase 3b viewer. Use @thatopen/components highlighter to colour added, removed, and modified elements differently, driven by comparing the merged IFC against either parent. Requires Phase 3b viewer (ifcurl-i1s) to be working.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:30Z","created_by":"Bruno Postle","updated_at":"2026-04-23T05:48:30Z","dependencies":[{"issue_id":"ifcurl-prc","depends_on_id":"ifcurl-i1s","type":"blocks","created_at":"2026-04-23T06:48:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"memory","key":"forgejo-build-and-deploy-procedure-go-build-c","value":"Forgejo build and deploy procedure: go build -C /home/bruno/src/forgejo -tags 'sqlite sqlite_unlock_notify' -ldflags \"-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'\" -o /home/bruno/src/forgejo/forgejo . — then sudo cp forgejo /usr/bin/forgejo. StaticRootPath=/usr/share/forgejo. CustomPath=/var/lib/forgejo/custom. Custom templates go to /var/lib/forgejo/custom/templates/custom/ (e.g. footer.tmpl → /var/lib/forgejo/custom/templates/custom/footer.tmpl). Custom static files go to /etc/forgejo/public/assets/ and are served at /assets/*. /var/lib/forgejo is root/forgejo-only. /etc/forgejo has conf/ public/ templates/ but only public/assets/ is used for static files."}