The select-documents picker queried the Forgejo API with only the
browser's session cookie (credentials: 'include'), so an OpenCDE client
that authenticated via OAuth2 saw only public repositories.
Thread the access token presented on the select-documents request
through to the picker UI (query param) and have its JS send it as
Authorization: Bearer on every Forgejo API call, falling back to cookie
auth when no token is supplied. Add a no-referrer policy so the token in
the page URL does not leak via Referer on API fetches or the callback
redirect.
Closes ifcurl-i5c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
select_local_repo() replaces direct find_local_repos() calls in
_remap_origin. It reads/writes [project_origins] in config.toml:
a cached path is returned immediately; false marks a project read-only;
a single match is auto-cached without prompting; multiple matches call
a prompt_fn (default: interactive stdin) and cache the choice. Stale
config paths bust the discovery cache so the next call rescans.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After creating a working checkout, if find_local_repos() finds a local
clone or fork of the same project, rename 'origin' (bare cache) to
'upstream' and add a new 'origin' pointing at the local repo. Mirrors
the standard fork workflow: fetch from upstream, push to origin (local
repo), then push to the shared remote and open a PR. If no local repo
is found, origin stays as the bare cache (read-only). Idempotent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Stored XSS in populateMetaPanel (viewer.js): IFC type and storey
names from model data were interpolated unescaped into innerHTML.
Added hesc() helper and applied it to all IFC-derived values.
2. sessionStorage CSS injection (viewer.html): viewerSnapshot value was
used unsanitised in style.backgroundImage. Now validated to start
with 'data:image/jpeg;base64,' before use.
3. Hostname-based SSRF bypass (service.py): _is_private_ip() only
blocked literal IP addresses; hostnames resolving to private
addresses bypassed the check. Now resolves all addresses via
socket.getaddrinfo() and rejects if any resolve to a private range.
Unresolvable hostnames are treated as private (fail-safe).
4. SSRF + bearer token exfiltration via BCF snapshot (bcf_api.py):
get_snapshot() forwarded the caller's Authorization token to the
preview service for an ifc:// URL stored in a Forgejo comment body
(attacker-controlled). Now parses and validates the stored URL
through _ssrf_check(), and uses the server-side token for the
ifc:// host instead of the caller's credential.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
renderBreadcrumb was building HTML via string concatenation and writing
it to innerHTML. The path segments from Forgejo's API were inserted raw
into single-quote-delimited onclick attributes, and escHtml did not
escape single quotes, so a directory name containing ' could inject
arbitrary JS executed on click.
Replace innerHTML construction with DOM API calls (textContent +
element.onclick) matching the safe pattern already used in addListItem,
and remove the now-dead escHtml helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /documents/1.0/select-documents accepts a callback URL and returns a
select_documents_url pointing to a self-contained HTML picker that browses
Forgejo repos and IFC files, encodes the selection as a document_id, then
redirects back to the callback with document_ids[] appended.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements GET /foundation/1.1/oauth2/auth_url, POST /oauth2/token, and
POST /oauth2/token_refresh, proxying to Forgejo's OAuth2 endpoints using
IFCURL_OAUTH2_CLIENT_ID and IFCURL_OAUTH2_CLIENT_SECRET env vars.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ifcurl-open dispatches ifc:// URLs to a configured viewer (bonsai or
ifcviewer preset, or a custom command list). ifcurl-register installs
it as the OS-level handler: .desktop + xdg-mime on Linux, app bundle
+ lsregister on macOS, HKCU registry keys on Windows (no admin needed).
Data templates in ifcurl/data/. Both support --unregister.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
applySelector() now returns the resolved GUIDs array in both the simple
(ThatOpen classifier → getGuid()) and complex (/select → GUIDs already
fetched) paths. generateBcf() reads currentGuids directly and builds the
full BCF 2.1 zip in the browser, including <Selection>/<Exceptions> in
the viewpoint component list.
The Python POST /bcf endpoint, BcfRequest model, and build_bcf()/
_viewpoint_xml()/_markup_xml() functions are removed — the browser had
all the data needed to do this itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capture the WebGL canvas client-side before POSTing to /bcf, send the
base64 PNG as a new `snapshot` field, and embed it as snapshot.png in
the zip alongside viewpoint.bcfv. Also wires up the <Snapshot> element
in markup.bcf when a snapshot is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ifcurl-5oo. get_checkout() ensures the bare cache is current via
fetch_ifc(), then clones it locally to <cache_dir>/checkout/ (no network),
and places it at the resolved commit as a detached HEAD — forcing branch
creation before any commit. Viewer artefact patterns (*.ifcview, *.rdbview,
etc.) are appended to .git/info/exclude so they are locally ignored without
touching the repo's own .gitignore. When a newer commit is requested, the
checkout fetches from origin (the local bare cache) using the default refspec
to avoid git's "refusing to fetch into checked-out branch" error. Unblocks
the JSON-RPC connector (ifcurl-242).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ifcurl-egi. find_local_repos(genesis) scans directories
from [repo_search] paths in ~/.config/ifcurl/config.toml, computes the
genesis commit of each git repo found, and returns paths whose genesis
matches. Results are cached in ~/.cache/ifcurl/genesis_repos/<genesis>.json
for IFCURL_REPO_CACHE_TTL seconds (default 3600). Recursion stops at repo
boundaries and hidden directories; depth capped at 4 levels. Adds tomli
as a conditional dep for Python <3.11. Unblocks origin remapping (ifcurl-oef)
and ambiguity resolution (ifcurl-77u).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ifcurl-4d4. _compute_genesis() runs git rev-list --max-parents=0
HEAD to find the root commit(s); get_genesis_commit() wraps it with a
per-remote disk cache (~/.cache/ifcurl/<hash>/genesis_commit) so the git
command only runs once per repo. For multi-root repos the lexicographically
smallest hexsha is chosen for a stable result. Local repos compute fresh
on each call. Exported from __init__. Unblocks local repo discovery (ifcurl-egi).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ifcurl-i07. fetch_ifc_path() fetches the IFC file and writes
it to ~/.cache/ifcurl/<url-hash>/<hexsha>/<basename>, returning the path
alongside the is_stale flag. Content-addressed by commit hexsha, so the
file is never rewritten once present. Works for both local and remote
URLs. Underpins the connector and checkout layers (ifcurl-5oo).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These element types produced large grey boxes obscuring the building in
renders of models that contain space volumes or annotation geometry
(e.g. 2D floor-plan drawings stored as Body CSG). Other IFC viewers
(Bonsai, web viewer) suppress these types in 3D views by default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitPython raises a plain ValueError from _parse_object_header when a commit
hash is missing, not BadObject, so the previous message-based retry guard
("not found in repository") never matched. Broaden the except clause to catch
(ValueError, BadObject, BadName) and drop the fragile string check. Extract
the fetch logic into _fetch_all_refs helper and apply the same retry to
diff_text. Add tests covering _fetch_all_refs, the retry-on-BadObject path,
and the no-retry-for-local-transport invariant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ifcurl.js: URL-pattern cases (PR diff, commit page) now run before
the DOM-based file-view case, which also matches on Forgejo diff pages
and was causing an early return that skipped PR diff rendering.
git.py: fetch_ifc now retries with a full ref fetch when an immutable
ref (commit hash) is not found in the cached clone — PR branch commits
are absent from the initial default-branch clone and need a fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
service.py: return 400 with a clear message when query= looks like a
bare property set name (starts with Pset_ or Qto_ but has no dot).
Catches the common mistake of writing Pset_WallCommon instead of
Pset_WallCommon.FireRating, giving feedback instead of silent empty results.
viewer.html: update query input placeholder and title to show the
expected formats (Name, Description for attributes; Pset_X.Prop for
property sets).
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
Drop selector_b — the endpoint now takes the URL's selector= and tests all
matched elements against each other, consistent with the spec's model.
Self-pairs and duplicates are filtered; requires at least 2 matched elements.
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
render_service.py: _sandboxed_clash(ifc_bytes, selector_a, selector_b) uses
ifcopenshell.geom.tree.clash_collision_many for hard-clash detection (mesh-
level interpenetration, touching excluded). When selector_b is None, tests
set A against all other IfcProduct elements. Deduplicates pairs via frozenset.
render_service.py: /clash endpoint wraps _sandboxed_clash in the sandbox.
service.py: ClashRequest model, _clash_via_socket() for socket delegation,
and GET/POST /clash endpoints. URL selector= is set A; optional selector_b
body/query param is set B. Returns {"pairs": [["guidA", "guidB"], ...]}.
Reuses Tier 2 byte cache.
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
render.py: add _CLASH_COLOR and clash branch in _add_shape — selected
elements rendered in red, non-selected in normal material colours.
render_service.py: add _sandboxed_query(ifc_bytes, selector, query_path)
and /query endpoint. Dot-notation query_path splits into pset/property
(Pset_WallCommon.FireRating) or falls back to direct IFC attribute (Name).
Returns {GlobalId: str(value)}, excluding None and entity references.
service.py: add QueryRequest model, _query_via_socket() for socket
delegation, and GET/POST /query endpoints reusing Tier 2 byte cache.
https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
- IfcUrl gains a query field (str | None) for dot-notation
attribute/property paths (e.g. Pset_WallCommon.FireRating)
- visibility now accepts 'clash' in addition to the existing three modes
- to_string() serialises both new fields
- 10 new tests covering query parsing, pset paths, combined query+view,
clash visibility, roundtrip serialisation, and the new spec examples
https://claude.ai/code/session_01CKpkhWhzkzR5K5PDSR8N5F
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>
Stateless translation layer over Forgejo issues and comments. Auth is
forwarded verbatim (Authorization: Bearer), so Forgejo enforces per-user
permissions with no service token. Viewpoints are ifc:// URLs stored in
comment bodies; GUIDs are deterministically derived from Forgejo IDs and
are reversible without a lookup table.
Endpoints: GET/POST projects, topics, comments, viewpoints; PUT topic;
GET viewpoint snapshot (proxied through preview service).
Also adds ifc_url_to_bcf_viewpoint() to bcf.py (symmetric counterpart of
bcf_viewpoint_to_ifc_url), and fixes rate-limit state leaking between tests.
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>
- 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>
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>
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>
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>
When a branch/HEAD fetch fails (network error, auth failure, server down),
_open_remote now logs a warning rather than silently passing. A boolean
is_stale propagates through _get_repo → fetch_ifc so callers know the data
comes from a stale local cache.
The service adds X-Ifcurl-Cache: stale to /preview and /render_diff responses
when stale data was served, so Forgejo or a browser viewer can surface a
warning to the user.
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>
Implements the ifcgit/Bonsai diff algorithm for the preview service:
diff.py: step_ids_from_diff() parses git diff text (regex on +#NNN= / -#NNN=
lines) to classify step IDs as added/modified/removed. expand_step_ids()
walks IfcShapeRepresentation, IfcObjectPlacement, IfcPropertySet, and
IfcTypeProduct relationships to promote changed sub-entities to the parent
IfcProduct that is visually affected.
render.py: render_diff() does two passes over the same camera. Pass 1 renders
the head model with green=added, blue=modified, ghost=unchanged. Pass 2
renders only the removed elements from the base model in red, using the camera
auto-fitted to pass 1. PIL composites the passes: wherever pass 2 is not
white background it stamps onto pass 1. Moved elements (modified) appear once
in blue with no doubling artefact.
git.py: diff_text() runs git diff against the cached bare repo clone and
returns the raw text for diff.py to parse.
service.py: GET+POST /render_diff endpoint. Fetches both commits, gets
diff text, runs the full pipeline sandboxed. Caches on disk when both refs
are immutable. Pillow added to render/service extras.
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>
ifcopenshell can parse IFC bytes in-memory via from_string(); no need to
write a temp file, read it back, and unlink it. Simpler and faster.
Update README library example to match.
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>
- GET /preview now accepts a token= query parameter, matching POST /preview
- Forgejo Go: add SERVICE_TOKEN config field; renderer appends it to <img src>
preview URLs so a machine-user token can authenticate private repos
- Go tests: verify token appears in preview URL when set, absent when unset
- viewer.html: BCF import via drag-and-drop or Import .bcf button in BCF row;
parses first viewpoint camera/clips/GUIDs and reloads with the viewpoint
applied to the current model context
- viewer-url.js, viewer.html: unescape %24 → $ in URL building so GlobalIds
with $ characters are human-readable in ifc:// URLs
- README: document SERVICE_TOKEN and private-repo preview configuration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pyproject.toml: add bcf to [service] dependencies (ifcurl-9rk)
- README.md: replace bogus GET /viewer row with POST /bcf (ifcurl-ttb)
- viewer-url.js: fix GitLab host detection (includes → startsWith) (ifcurl-3n4)
- git.py: call _evict_if_needed() after fetch, not only after clone (ifcurl-t1k)
- bcf.py: add description param to build_bcf(); markup.bcf now includes
<Description> when provided (ifcurl-7wg)
- service.py: pass request URL as description to build_bcf() (ifcurl-7wg)
- viewer.html: BCF export reads visibility= from URL and sets
DefaultVisibility accordingly (ifcurl-sto)
- viewer.html: applyCameraParam now switches to orthographic projection
and applies scale when scale= is present in URL (ifcurl-4l7)
- tests: 3 new tests for description field; update JS tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ifcurl/bcf.py: build_bcf() builds BCF 2.1 zips with perspective/
orthographic camera, clipping planes, component selection, and
isolate visibility mode
- ifcurl/service.py: /bcf POST endpoint parses ifc:// URL, resolves
selector → GUIDs when present, delegates to build_bcf()
- viewer.html: BCF toolbar button + collapsible title/comment form;
client-side zip generation via JSZip (esm.sh CDN) so the browser
does not need service access
- tests/test_bcf.py: 21 tests covering build_bcf() unit behaviour and
the /bcf endpoint including SSRF rejection and selector resolution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Disk cache (bare repo clones under ~/.cache/ifcurl/):
- IFCURL_CACHE_MAX_GB env var sets the max total size; when exceeded after a
new clone the oldest-accessed repos are removed until under the limit
- remote_url stored per clone entry so ifcurl cache list can show the URL
- _touch_cache() updates mtime on every repo open for LRU tracking
In-memory caches (service.py):
- _T2_MAX and _T3_MAX now read from IFCURL_T2_MAX / IFCURL_T3_MAX env vars
(defaults: 8 IFC blobs, 64 selector results) — configurable without rebuild
New CLI: ifcurl cache list | prune [--max-gb N] | clear
Tests: 6 new unit tests for _dir_size, _touch_cache, _repo_cache_entries,
_evict_if_needed. 118 → 124 passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The preview endpoint is co-hosted with a specific Forgejo instance, so it
should only be allowed to fetch from that host.
- Local file transport (ifc:///path) is always rejected with 403 — the service
must not read from the server's own filesystem
- New --allowed-hosts CLI option for `ifcurl serve` takes a comma-separated
list of permitted git hostnames (e.g. git.example.com or localhost:3000);
any unlisted host is rejected with 403
- Defense-in-depth: literal private/loopback/link-local IP addresses are
blocked even without an allowlist (169.254.x.x, 127.x, RFC1918, etc.)
- README and forgejo/README.md updated to include --allowed-hosts in the
standard deployment invocation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace url-display span with editable input; Enter or drag-and-drop loads
a new ifc:// URL by navigating to the same page with updated ?url= param
- Fix syncCameraUrl: convert Three.js → IFC world coords (x,-z,y) before
serialising camera= param so URLs are in Z-up IFC space
- Apply IFC → Three.js transform (x,z,-y) when placing camera from URL param
- Drop viewer.py: viewer is now a Forgejo static file; service.py has no
/viewer or /proxy endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Viewer is now a Forgejo static file; IFC bytes are fetched directly from
Forgejo by the browser. The proxy is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /viewer?url=ifc://... serves a self-contained HTML page that loads
@thatopen/components v3 from CDN and renders the IFC model in WebGL
- GET /proxy?url=<raw> proxies IFC bytes from Forgejo to the browser,
avoiding CORS restrictions
- forgejo/templates/custom/footer.tmpl injects a "View in 3D" button on
.ifc file view pages in Forgejo; deploy to
/var/lib/forgejo/custom/templates/custom/footer.tmpl
CDN loading required several non-obvious fixes documented in viewer.py:
web-ifc loaded from unpkg (not esm.sh) to avoid the Node.js process polyfill
that breaks the browser ST build's environment check; fragments worker wrapped
in a blob URL for Firefox cross-origin worker compatibility; model.box used for
camera fit before tiles exist; fragments.core.update(true) to flush tile queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>