Commit graph

37 commits

Author SHA1 Message Date
Claude
6ffc4a2b29 Simplify /clash: single selector, detect collisions within selected set
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
2026-05-09 08:57:54 +00:00
Claude
ccf692b168 Add /clash endpoint for geometry intersection detection (ifcurl-k0x)
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
2026-05-09 08:53:51 +00:00
Claude
b560551f3c Implement clash visibility mode and /query endpoint (ifcurl-19h, ifcurl-8rf)
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
2026-05-09 08:46:26 +00:00
Claude
8ab6603a44 url.py, test_url.py: add query parameter and visibility=clash
- 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
2026-05-09 07:27:35 +00:00
9afeae5d44 bcf_api: add topic filtering; documents_api: add GET /document-metadata
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>
2026-04-29 22:24:52 +01:00
63a6b2bfa6 documents_api: implement OpenCDE Documents API 1.0
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>
2026-04-29 20:27:25 +01:00
9e1a10e0b6 bcf_api: implement BCF 3.0 REST API as Python routes in the preview service
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>
2026-04-29 19:38:34 +01:00
ee57e812e5 service: add GET /foundation/versions OpenCDE discovery endpoint
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>
2026-04-29 19:20:54 +01:00
930b8b5e32 bcf: add bcf_viewpoint_to_ifc_url() and IfcUrl.to_string()
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>
2026-04-29 19:04:04 +01:00
837a9d10c8 render: fix render_diff — tuple unpack, IfcProduct filter, diff-area zoom
- 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>
2026-04-25 23:54:35 +01:00
bcf9d58470 service: fix preview pipeline for local Forgejo deployment
- 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>
2026-04-25 23:10:35 +01:00
e66460fb13 service+viewer: add /select endpoint for complex selector resolution
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>
2026-04-25 20:20:41 +01:00
fe793d2194 service: add per-IP rate limiting and IFC blob size cap
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>
2026-04-25 13:01:56 +01:00
6b6e47ef54 Split service into ifcurl-api + ifcurl-render for RCE isolation
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>
2026-04-25 10:00:33 +01:00
6e62f9e51c git.py: log warning and return is_stale flag when mutable-ref fetch fails
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>
2026-04-25 09:05:13 +01:00
a6206a459e style: ruff --fix + black across ifcurl/ and tests/
Import sorting, quoted type annotation removal, and black reformatting.
No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:34:56 +01:00
4b32726f87 render_diff: auto-fit camera to changed elements bounding box
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>
2026-04-25 07:56:53 +01:00
96aa1a2377 diff rendering: two-pass IFC diff with green/blue/red colouring
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>
2026-04-25 06:52:01 +01:00
65a62ffc8b sandbox.py: subprocess isolation for ifcopenshell parse + render
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>
2026-04-24 17:38:07 +01:00
02d93e68e1 service.py: use ifcopenshell.file.from_string() instead of temp file
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>
2026-04-24 17:19:27 +01:00
19ce25fdc0 render.py: cap worker count; auth.py: document token leak; README: fix example
- 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>
2026-04-24 16:56:39 +01:00
39a2786872 Private repo previews, BCF import, SERVICE_TOKEN support
- 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>
2026-04-24 14:20:17 +01:00
01de9fdcac Fix 7 issues from deep review
- 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>
2026-04-24 11:57:06 +01:00
dae1435519 Add BCF export: /bcf endpoint, build_bcf(), viewer panel
- 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>
2026-04-24 09:59:22 +01:00
335349db1a git.py: LRU disk cache eviction; service.py: configurable in-memory limits
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>
2026-04-24 09:06:56 +01:00
d076196bac service.py: SSRF protection — reject local transport, add --allowed-hosts
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>
2026-04-24 08:26:37 +01:00
81f022d4af viewer.html: URL input, drag-and-drop, IFC coordinate transform, camera placement
- 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>
2026-04-23 21:15:30 +01:00
9e0ef5b41f Remove /viewer and /proxy endpoints from service.py
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>
2026-04-23 21:06:36 +01:00
c9af45b354 Remove PLAN.md (tasks now in beads), add syncCameraUrl to viewer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 07:18:28 +01:00
038827fcf2 Add Phase 3b web IFC viewer: /viewer and /proxy endpoints
- 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>
2026-04-22 22:04:28 +01:00
cf935a2a49 FIxes 2026-04-22 08:31:24 +01:00
95adb177f6 Fixes for render bugs 2026-04-22 00:22:35 +01:00
502e497d92 Add GET /preview endpoint for HTML img tag use
POST /preview requires a JSON body so cannot be used in <img src>.
The new GET /preview?url=... delegates to the same handler and shares
all caching and authentication behaviour.
2026-04-16 00:43:40 +01:00
778f0004e7 Fix port preservation and HTTP fallback for local Gitea
- url.py: preserve non-standard ports in host field; ifc://host:3000/...
  now correctly generates https://host:3000/... rather than dropping :3000
- git.py: fall back from https to http on clone/fetch failure, enabling
  local Gitea instances running on plain HTTP; inject tokens into http://
  URLs as well as https://; extract _clone_bare() helper
- tests/test_url.py: three new tests for port preservation in host,
  git_remote_url with non-standard HTTPS port, and SSH with non-standard port
2026-04-15 23:50:54 +01:00
7f92cadc50 Authentication and test coverage
- ifcurl/auth.py: token config (~/.config/ifcurl/tokens.json),
  get_token_for_host(), inject_token() for HTTPS URL credential injection
- ifcurl/git.py: thread token= through fetch_ifc/fetch_ifc_bytes/_open_remote;
  authenticated fetch uses 'git fetch <auth_url> +refs/*:refs/*' so the
  token is never written to on-disk git config
- ifcurl/service.py: optional token field in PreviewRequest; request token
  takes precedence over config-file token; get_token_for_host imported at
  module level for clean monkeypatching in tests
- tests

Generated with the assistance of an AI coding tool.
2026-04-15 23:21:13 +01:00
018ee446b5 Preview service and improved CLI help
- ifcurl/service.py: FastAPI POST /preview → image/png with three
  caching tiers (bytes LRU, GUID-set LRU, PNG filesystem)
- ifcurl/git.py: add fetch_ifc() returning (hexsha, bytes); refactor
  around _get_repo() and _read_commit_blob() helpers
- ifcurl/__main__.py: add 'ifcurl serve' subcommand; no-args prints
  full help; all subcommands have descriptions and examples; URL format
  and parameter reference in every help page

Generated with the assistance of an AI coding tool.
2026-04-15 21:46:50 +01:00
7e83c00487 Python core library and ifcurl CLI
- URL parser (IfcUrl) handling all ifc:// forms: SSH, HTTPS, local;
  all ref types; camera, selector, clip, visibility parameters
- Git fetcher using GitPython with OS-appropriate bare-clone cache
  (platformdirs) and fetch-on-mutable-ref behaviour
- Renderer adapted from ifcquery: explicit camera, clipping planes,
  highlight/ghost/isolate visibility modes, type entity support
- ifcurl CLI: `ifcurl render "<url>" [-o output.png]`
- LICENSE file (LGPLv3+)

Generated with the assistance of an AI coding tool.
2026-04-15 21:24:35 +01:00