Displays "Double-click model surface to place clip plane" in the status
bar when clip mode is toggled on, clears it when toggled off.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BCF list_topics now maps topic_status, assigned_to, label, modified_after,
modified_before query params to Forgejo issue API params.
documents_api adds GET /document-metadata/{document_id} which decodes the
document_id and proxies Forgejo's contents endpoint for file metadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /documents/1.0/document-versions resolves document_ids (base64url
encoding of owner/repo/path) to versioned download URLs. Exposes only
git tags + the current HEAD commit — mirrors CDE conventions where only
released versions are visible, while still surfacing the WIP state.
version_number is the tag name for releases and short SHA for HEAD.
Tagged SHAs are deduplicated so HEAD at a tag doesn't create a duplicate.
Download URLs point at Forgejo's existing raw/commit endpoint.
Auth forwarded verbatim. No Go code, no Forgejo recompilation.
Proxy at /documents/ on the Forgejo hostname.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns the Foundation API 1.1 version document listing BCF 3.0 and
Documents 1.0 API base URLs. Public, no auth required. Derives the
public base URL from X-Forwarded-Host/X-Forwarded-Proto headers so it
works correctly behind an nginx/Caddy reverse proxy with no extra config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements SPECIFICATION.md §6 BCF→ifc:// direction: parse a BCF 3.0
REST viewpoint dict (camera, clips, selection/visibility) and apply it
to a base IfcUrl to produce an ifc:// URL string.
IfcUrl.to_string() serialises any IfcUrl back to a valid ifc:// URL,
enabling round-trip parse→to_string for all URL forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bundle OBCF (components-front) alongside OBC so Highlighter is sourced from the
correct package. Update click handler to use item.getGuid() — GlobalId is no
longer in the attributes map in fragments 3.4.x. Fix populateMetaPanel crash
(ClassificationGroupData now wraps ids in a .map property, not a plain object).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On /src/commit/{hash}/file.ifc pages, breadcrumb anchors match the
a[href*="/src/commit/"] selector before the Permalink anchor, returning
a directory URL that parseCommitHref rejects (no file path). Fix by
trying window.location.pathname first — it parses correctly when already
on a commit file URL — and only falling back to a Permalink anchor search
for branch/tag file views. Add a regression test for directory-level
commit hrefs returning null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract parseCommitHref, isSimpleTypeSelector, subtractIdMap, and
isCommitHash into a new viewer-util.js ES module so they can be tested
without a browser. Convert ifcurl.js from an IIFE to an ES module
importing from viewer-util.js; update footer.tmpl to type="module".
Add node:test suites for viewer-url.js (32 tests) and viewer-util.js
(28 tests). Add npm test script and document it in the build section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ifcurl.js: scan a[href^="ifc://"] at page load and replace with preview
figures — works with [title](ifc://...) syntax without any Go extension
- viewer.js: Issue button now emits [title](ifc://...) markdown link syntax
instead of a bare URL, so shared views render as previews without the patch
- README: document no-rebuild quick setup path; Go patch section is now optional
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
footer.tmpl now just loads /assets/ifcurl.js; all injection logic lives in
the new ifcurl.js file alongside the other viewer assets. Update deploy
instructions in both READMEs; fix incorrect /etc/forgejo path in top-level README.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
viewer.html was ~1250 lines of almost entirely JavaScript. The script
block is now a single <script type="module" src="./viewer.js"> tag,
leaving viewer.html as pure HTML/CSS (~238 lines).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Types panel: fix count by iterating groupData directly (groupData.map
was undefined — FragmentIdMap is the groupData value, not a wrapper)
- Clip planes: register OBC.Raycasters for the world before setupClipper
so clipper.create(world) can raycast against the model surface
- Ref navigation: ▾ button now navigates when input already has a value;
add change listener alongside input for cross-browser datalist coverage
- Orthographic projection: replace missing controls.setOrthoCamera() with
world.camera.projection.set("Orthographic") + threeOrtho scale fix;
refresh threeCamera reference after projection switch
- BCF export: capture rendererCanvas snapshot and include snapshot.png in
the BCF zip with a <Snapshot> reference in markup.bcf
Closes: ifcurl-07d, ifcurl-1pa, ifcurl-66h, ifcurl-xy9, ifcurl-uo9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a permanent 'All storeys' row at the top of the Storeys list in the
model overview panel. It starts active (all visible) and clicking it
restores full model visibility. Clicking any storey row then back to
'All storeys' is now always possible, even after switching between storeys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move ⎘ copy-URL button immediately after the URL text box (ifcurl-2m9)
- Widen selector field from flex:1 to flex:2 (ifcurl-mts)
- Use window.location.origin for the new-issue link, removing hardcoded https:// (ifcurl-njq)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- visibility=highlight: apply orange colour overlay to selected elements
(all others visible normally) using FragmentsManager.highlight() directly
— OBC.Highlighter is in @thatopen/components-front which is not installed
- visibility=ghost: dim non-selected elements to 15% grey opacity
- visibility=isolate: unchanged (hider.isolate)
- Add 'show' dropdown (highlight/ghost/isolate) that appears when a
selector is set; changing it reloads with the new visibility= param
- Add 'fit' button: calls cameraControls.fitToBox(model.box, true)
- Smooth load transition: capture canvas snapshot (preserveDrawingBuffer)
before navigation, display it as an overlay on the new page, fade it
out once the model has rendered
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously applySelector always called hider.isolate() which hid all
non-selected elements regardless of visibility mode. For highlight mode,
set up the Highlighter before applying the selector and use
highlightByID("select") instead so all elements remain visible with the
selection highlighted in orange.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- git.py: unpack (repo, is_stale) tuple from _get_repo() in diff_text;
was AttributeError 'tuple has no attribute commit' on every render_diff
- render.py: filter removed_entities and _entity_bounds include list to
IfcProduct only; non-geometric entities (IfcTaskTime etc) crash the
ifcopenshell geom iterator
- render.py: replace reset_camera(bounds=diff_bounds) with explicit camera
positioning; pyvista's reset_camera re-expands clipping to all actors
so the full scene remained visible — now camera is repositioned at the
default viewing direction but at a distance sized to the diff bounds,
zooming tightly to just the changed elements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth.py: catch OSError (not just FileNotFoundError) so ProtectHome=yes
doesn't crash token lookup — returns None and falls back to anonymous
- git.py: wipe partial bare-clone dir before HTTP fallback so git can retry
after HTTPS fails (port 443 not available on local HTTP-only Forgejo)
- pyproject.toml: add python-multipart to service deps (FastAPI form data)
- ifcurl-api.service: fix ExecStart path (/opt/ifcurl), set allowed-hosts
to localhost, add MPLCONFIGDIR to silence matplotlib cache warning
- ifcurl-render.service: fix ExecStart path, add CacheDirectory + MPLCONFIGDIR
- nginx-ifcurl.conf: new file tracking the nginx reverse-proxy config;
listen on [::]:80 so IPv6 loopback requests reach the Forgejo proxy block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9 tests exercise _cmd_cache() directly with fake cache dirs and a
monkeypatched user_cache_dir, covering output formatting, empty states,
prune with --max-gb arg, prune via IFCURL_CACHE_MAX_GB env, and the
error exit when neither is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ref input now has a datalist populated from the Forgejo branches+tags
API on load. A ▾ button clears and focuses the field to expose all refs.
Selecting a datalist option auto-navigates; pressing Enter in any structured
field now preserves camera/clip/fov/scale from the current URL so switching
refs keeps the same viewpoint.
Closes ifcurl-4rj.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New GET+POST /select endpoint resolves an ifc:// URL selector to a JSON
list of GlobalIds, with T3 caching. The viewer now detects non-type-name
selectors (property filters, Name=, pset notation) and falls back to a
/select round-trip, applying the returned GUIDs via getLocalIdsByGuids().
Closes ifcurl-ov6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A new '☰' toolbar button opens a panel showing the IFC type breakdown
(sorted by element count) and storey list. Clicking a storey isolates
its elements via OBC.Hider; clicking again restores full visibility.
Closes ifcurl-dag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an ifc:// URL uses a bare commit hash as the ref the toolbar now
shows an amber badge with the abbreviated hash and (after a Forgejo API
lookup) a '→ heads/main' button that reloads with the branch-tip ref.
Closes ifcurl-gy8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
three 0.177→0.184, web-ifc 0.0.75→0.0.77, @thatopen/components
3.3.1→3.4.2, @thatopen/fragments 3.3.1→3.4.3, + new camera-controls
3.1.2 peer dep. Reduces fast-xml-parser findings from 2 critical to
1 high + 1 low; remaining issues are upstream in @thatopen >= 3.3.3
with no fix released yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace importmap pointing at esm.sh/unpkg with locally-bundled files
built from pinned npm packages (package.json + lockfile). Run
`cd forgejo && npm install && npm run build` to regenerate after a
version bump. Tracked in ifcurl-dkz: fast-xml-parser CVEs in
@thatopen/components 3.3.1 should be addressed in a follow-up upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IFCURL_RATE_LIMIT (default 60 req/min, 0=off) — sliding-window middleware
returns 429 before any expensive work is done. IFCURL_MAX_IFC_MB (default
256, 0=off) — rejects oversized fetches with 413 in preview, bcf, and
render_diff endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client-side BCF generation cannot resolve IFC GlobalIds from a selector
string. When a selector is present, POST to /bcf instead — the service
runs the selector through IfcOpenShell and writes resolved GUIDs into the
viewpoint. Fails visibly if unreachable; no silent fallback to a GUIDless
file. Also adds /bcf to the Nginx proxy block in forgejo/README.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Namespace clean, no prior art, compound schemes would break transport-inference
design. Created ifcurl-nd1 to file provisional IANA registration post-Phase 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
render_service.py: new FastAPI app with /render, /select, /render_diff
endpoints that wrap run_sandboxed — listens on a Unix domain socket.
service.py: imports the three pipeline functions from render_service.py;
delegates to render service via httpx when IFCURL_RENDER_SOCKET is set,
falls back to direct run_sandboxed when unset (single-service mode).
__main__.py: add 'render-service' subcommand (--socket PATH).
pyproject.toml: add httpx to service optional deps.
ifcurl-api.service: renamed from ifcurl-preview.service; sets
IFCURL_RENDER_SOCKET, adds AF_UNIX to RestrictAddressFamilies.
ifcurl-render.service: new hardened unit — AF_UNIX only, ~execve
~execveat blocked, no credentials, separate ifcurl-render user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
execve is actually permitted — it arrives via @default which is included in
@system-service. The main service needs it for GitPython's git CLI calls.
Blocking execve only in render subprocesses would require per-process seccomp,
which is not worth the complexity for this threat model.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no explicit camera is given, compute the combined bounding box of all
changed elements (added/modified from head model, removed from base model)
using quick subset iterators before pass 1 renders. Pass
plotter.reset_camera(bounds=...) so the view zooms to the area of change
rather than the whole scene. Falls back to reset_camera() (whole scene) only
if no changed elements have renderable geometry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Makes explicit which features need the Go patch (markdown preview only) vs
which are static assets that survive Forgejo upgrades without recompilation.
Documents that the Nginx proxy is required for PR diff images because the
browser fetches them and can't reach localhost directly, and notes that most
production deployments already have Nginx for TLS anyway.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Case 3 in footer.tmpl detects /{owner}/{repo}/pulls/{N}/files, fetches base/
head SHAs from the Forgejo API, then injects <img src="/render_diff?…"> below
the file header of each .ifc file in the diff. Returns early so Case 2 (history
page) does not also run on the PR diff page.
README documents the required Nginx proxy for /preview and /render_diff, and
clarifies that PREVIEW_SERVICE_URL is server-side-only (localhost:8000 is
correct for it); the browser-side diff images need the proxy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add sandboxing directives to limit blast radius from a compromised render
subprocess:
- NoNewPrivileges + RestrictSUIDSGID: no privilege escalation
- ProtectSystem=strict + ProtectHome: Forgejo data tree is unwritable
- PrivateTmp: isolated /tmp
- RestrictAddressFamilies=AF_INET AF_INET6: no Unix socket access to Forgejo
- SystemCallFilter=@system-service @process @files @io-event @network-io:
allows fork/clone (needed by ifcopenshell geom workers) but excludes
execve/execveat so an attacker cannot exec a shell
- RestrictNamespaces, LockPersonality, ProtectKernelTunables, etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All ifcopenshell calls (from_string, filter_elements, geom.iterator) now
run inside a short-lived child process via run_sandboxed(). A SIGSEGV or
SIGABRT in the child returns HTTP 422 instead of killing the service worker.
Configurable timeout (IFCURL_SANDBOX_TIMEOUT, default 120s) and optional
resource limits (IFCURL_SANDBOX_MEMORY_MB, IFCURL_SANDBOX_CPU_SECS).
The T3 GUID cache still works: the sandboxed pipeline returns resolved
GUIDs on selector miss so the parent can populate the cache; on hit the
parent passes cached GUIDs and the child converts them to step IDs.
Tests use a sync_sandbox fixture that runs run_sandboxed synchronously so
mock patches remain visible; four dedicated sandbox tests cover normal
return, exception propagation, segfault detection, and timeout.
Closes ifcurl-vm5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- render.py: replace bare cpu_count() with _WORKER_COUNT = min(cpu_count, 4),
configurable via IFCURL_RENDER_WORKERS env var; prevents N concurrent
requests from spawning N×cpu_count geometry worker processes
- auth.py: document that inject_token() embeds the token in git CLI arguments,
making it visible in the process list; note SSH as the workaround
- README: replace placeholder comment with actual tempfile.mkstemp pattern for
loading IFC bytes into ifcopenshell (which requires a file path, not bytes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- footer.tmpl: refactor to inject per-commit "View in 3D" links on the file
history page, alongside the existing file view page button
- ifc_url.go: add ')' to urlTerminators so bare ifc:// URLs followed by a
closing parenthesis are correctly terminated
- ifc_url_test.go: add tests for @ in query value, ) terminator, trailing
period, and SERVICE_TOKEN presence/absence in preview URL
- viewer.html: selector input placeholder and title attribute now document
that the browser viewer only supports type-name union selectors
- README: document viewer selector limitation vs full server-side behaviour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>