Commit graph

14 commits

Author SHA1 Message Date
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
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
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
5d5b8c6516 tests: extend coverage — SSRF protection, GET endpoint, LRU eviction, branch ref
- SSRF: test local transport rejection, allowlist enforcement, private/loopback/
  link-local IP blocking, allowlist with port, public host permitted without list
- GET /preview: happy path, invalid scheme, local transport rejection
- Tier 2 LRU: eviction at capacity, LRU promotion, cache miss
- git: heads/<branch> ref form (was only tested via HEAD/tag/hash)
- clear_caches fixture now also resets _allowed_hosts between tests

103 → 118 passing tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 08:38:03 +01:00
d98d4c35d5 viewer: extract URL logic to viewer-url.js, add JS tests
parseIfcUrl, buildIfcUrl, toRawUrl extracted from viewer.html into
viewer-url.js so they can be imported and tested independently.
viewer.html imports from the module. 32 Node test-runner tests in
tests/test_viewer_url.mjs cover all three functions across GitHub,
GitLab, Forgejo/Gitea, SSH user@ prefix, and round-trip cases.

Caught a bug: + (selector union operator) was being URL-encoded as
%2B in buildIfcUrl; fixed by adding .replace(/%2B/g, "+") alongside
the existing comma fix, in both viewer-url.js and syncCameraUrl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 07:31:19 +01:00
cf935a2a49 FIxes 2026-04-22 08:31:24 +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
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