diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
index fc49eb2..ddd9dc6 100644
--- a/.beads/issues.jsonl
+++ b/.beads/issues.jsonl
@@ -3,6 +3,8 @@
{"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-2yd","title":"Security review: OpenCDE CDE routes (OAuth2 proxy + document picker)","description":"Perform a security review of the CDE-related code added in foundation_api.py and the select-documents additions to documents_api.py. Areas to examine:\\n- OAuth2 proxy: does proxying client_secret server-side introduce SSRF or credential leak risks? Are Forgejo error responses safely forwarded without leaking internal detail?\\n- Picker HTML: is callback_url validated before embedding? Could a crafted callback_url cause open redirect to an attacker-controlled host? Is the JSON embedding in \u003cscript\u003e safe against XSS (\u003c/script\u003e injection test exists but review the escaping logic)?\\n- document_id encoding: can a malformed document_id returned from the picker cause decode_document_id to crash or expose unexpected data?\\n- Rate limiting: do the new /foundation/ and /select-documents endpoints inherit the rate-limit middleware?\\n- Auth forwarding: the document picker UI calls Forgejo API with credentials:include (session cookie). Could a malicious callback_url cause the picker to exfiltrate the session cookie?\\n- Input validation: are all Pydantic models tight enough to reject unexpected fields?\\nProduce a prioritised list of findings with recommended fixes.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-07T11:21:23Z","created_by":"Bruno Postle","updated_at":"2026-06-07T11:28:58Z","started_at":"2026-06-07T11:28:58Z","dependency_count":0,"dependent_count":0,"comment_count":0}
+{"id":"ifcurl-dy9","title":"Manual test plan: OpenCDE Foundation API OAuth2 and document picker","description":"Step-by-step manual test procedure for the OpenCDE CDE functionality added in commits 48169ff and e2d3d12. Tests to cover:\\n1. Forgejo OAuth2 app registration: go to http://localhost:3000/-/admin/applications, create an app named 'ifcurl', set redirect URI, note client_id/secret, set IFCURL_OAUTH2_CLIENT_ID and IFCURL_OAUTH2_CLIENT_SECRET in ifcurl-api.service environment and restart.\\n2. GET /foundation/versions: confirm foundation 1.1, bcf 3.0, documents 1.0 all appear.\\n3. GET /foundation/1.1/oauth2/auth_url: returns {url, expires_in}; URL contains client_id and response_type=code.\\n4. GET /foundation/1.1/oauth2/auth_url?redirect_uri=...: redirect_uri is appended.\\n5. GET /foundation/1.1/oauth2/auth_url with no env vars set: returns 503.\\n6. POST /foundation/1.1/oauth2/token with a valid code (obtain via browser auth flow): returns access_token and refresh_token.\\n7. POST /foundation/1.1/oauth2/token_refresh with valid refresh_token: returns new access_token.\\n8. POST /documents/1.0/select-documents: returns select_documents_url pointing to /ui endpoint.\\n9. GET the select_documents_url in a browser: page loads, repo dropdown populates from Forgejo API.\\n10. Browse into a repo containing an IFC file, click it: browser redirects to callback_url with document_ids[] param containing a base64url-encoded owner/repo/path string that round-trips through decode_document_id.\\n11. POST /documents/1.0/document-versions with the returned document_id: returns version list with download URLs.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-07T11:21:19Z","created_by":"Bruno Postle","updated_at":"2026-06-07T11:21:19Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-m0y","title":"Phase 4: Bonsai integration — open ifc:// URLs natively","description":"Bonsai (the Blender-based IFC authoring tool) should be able to receive and handle ifc:// URLs natively, delegating all git fetch/cache/reconstruction work to the ifcurl Python library.\n\n## What Bonsai needs to implement\n\n**1. ifcurl library integration**\nBonsai calls ifcurl.resolve(url) → local IFC file path, and ifcurl.parse(url) → view state (camera position/direction/up, selector expression, visibility mode, clipping planes). The ifcurl library handles all transport, caching, and file reconstruction — Bonsai sees only a local path.\n\nIf the ifcurl library is not installed, all ifc:// features degrade gracefully: menu entries are hidden or disabled, CLI URL arguments are ignored with a logged warning, and BCF export omits ifc:// fields silently. No errors or broken UI.\n\n**2. 'Open ifc:// URL…' operator and menu entry**\nA Blender operator that accepts an ifc:// URL string, resolves it via ifcurl, opens the resulting IFC file, and applies the view state (camera, selector, visibility). Exposed as a menu entry (File \u003e Open ifc:// URL…) so users can paste a URL directly. This is also the entry point used when launched from ifcurl-open. Hidden when ifcurl is not installed.\n\n**3. CLI / launch argument handling**\nWhen Bonsai is launched with an ifc:// URL as a CLI argument (forwarded by ifcurl-open), the add-on detects it and invokes the same operator. ifcurl-open always launches a new Bonsai instance — there is no mechanism to send a URL to an already-running instance. Silently ignored if ifcurl is not installed.\n\n**4. 'Copy view URL' action**\nSerialise the current Bonsai camera position, active selector/filter, and visibility state into an ifc:// URL pointing to the current file's git repo and ref. Requires the file to have been opened from a git-tracked location. Hidden when ifcurl is not installed.\n\n**5. BCF viewpoint embedding**\nWhen Bonsai exports a BCF file, viewpoints that reference a git-tracked IFC file should include an ifc:// URL in the viewpoint metadata alongside the standard BCF camera fields. Omitted silently when ifcurl is not installed.\n\n**6. Federation: IFCDOCUMENTREFERENCE resolution**\nWhen Bonsai encounters an IFCDOCUMENTREFERENCE whose Location is an ifc:// URL, call ifcurl.resolve() to fetch and open the referenced model. This is the long-term federation goal. Falls back to ignoring the reference when ifcurl is not installed.\n\n## Out of scope here\n- The ifcurl Python library API itself (ifcurl.resolve, ifcurl.parse) — that is Phase 1 work\n- OS handler registration (ifcurl-coh) and dispatcher (ifcurl-7db)","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-07T07:20:36Z","created_by":"Bruno Postle","updated_at":"2026-06-07T07:39:58Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-8x4","title":"Deploy and test BCF export","description":"The BCF export was rewritten as pure client-side (commit 7c25e06). The query panel row-click highlight was also added (ifcurl-yhp). Deploy the updated JS assets to Forgejo and run the browser test checklist items 13 and 14 to verify BCF export and round-trip import still work correctly. Also test that clicking a row in the query panel highlights the corresponding element in the 3D view.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-07T06:32:36Z","created_by":"Bruno Postle","updated_at":"2026-06-07T07:47:27Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-coh","title":"ifcurl-register: register ifc:// OS protocol handler on Linux, macOS, Windows","description":"A ifcurl-register command that installs ifcurl-open as the OS-level ifc:// protocol handler. Linux: write ~/.local/share/applications/ifc-url-handler.desktop and run xdg-mime default. macOS: install a minimal app bundle with CFBundleURLTypes in Info.plist. Windows: write registry key under HKEY_CLASSES_ROOT\\ifc\\shell\\open\\command. Ship desktop/plist/reg templates in data/. Add ifcurl-register as a pyproject.toml entry point.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-05T14:43:04Z","created_by":"Bruno Postle","updated_at":"2026-06-07T08:33:09Z","started_at":"2026-06-07T07:49:58Z","closed_at":"2026-06-07T08:33:09Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0}
@@ -64,6 +66,7 @@
{"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-pq6","title":"Document OpenCDE Foundation API and document picker in SPECIFICATION.md","description":"The OpenCDE Foundation API 1.1 OAuth2 proxy routes (foundation_api.py) and document picker select-documents flow (documents_api.py) are implemented but not described in SPECIFICATION.md or the CLAUDE.md browser test checklist. Update SPECIFICATION.md §9 or add a new section covering: the OAuth2 setup (registering Forgejo app, env vars IFCURL_OAUTH2_CLIENT_ID/SECRET), the three /foundation/1.1/oauth2/ routes and their request/response shapes, the POST /documents/1.0/select-documents flow and the picker UI. Add a browser test checklist item to CLAUDE.md covering the end-to-end select-documents flow.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-07T11:21:26Z","created_by":"Bruno Postle","updated_at":"2026-06-07T11:21:26Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-4zf","title":"Document recently-closed features in README or SPECIFICATION","description":"Several features have been implemented and closed in recent beads but are not reflected in user-facing documentation. These include: OpenCDE Foundation API 1.1 OAuth2 proxy routes (ifcurl-bil), OpenCDE document picker select-documents flow (ifcurl-xfi, in progress), BCF export rewrite as pure client-side (7c25e06), ifc:// OS protocol handler ifcurl-open/ifcurl-register (d975658), PR/commit diff view, BCF round-trip import, query panel, click-to-inspect, metadata panel, and storey isolation. README and/or SPECIFICATION should be updated to reflect what is now implemented vs planned.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-07T09:11:03Z","created_by":"Bruno Postle","updated_at":"2026-06-07T09:11:03Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-bsw","title":"Socket IPC: dispatch ifc:// URLs to already-running viewer instance","description":"When a viewer (bonsai or ifcviewer) is running, ifcurl-open should send the URL to the existing instance rather than launching a new process. Protocol: on startup the viewer opens a Unix socket at ~/.cache/ifcurl/\u003cviewer-name\u003e.sock. ifcurl-open tries the socket first; if it connects it sends the URL as a newline-terminated string and exits. If no socket exists or connection fails, falls back to launching fresh. Bonsai and ifcviewer each need to implement the socket listener.","status":"closed","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-05T14:43:06Z","created_by":"Bruno Postle","updated_at":"2026-06-07T07:34:25Z","closed_at":"2026-06-07T07:34:25Z","close_reason":"Descoped: ifcurl-open will always launch a new viewer instance. Socket IPC is complex, not idiomatic for Blender, and can be added later if there is demand. Instance reuse policy is left to the viewer application.","dependencies":[{"issue_id":"ifcurl-bsw","depends_on_id":"ifcurl-7db","type":"blocks","created_at":"2026-06-05T15:43:26Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-77u","title":"Ambiguity resolution: ask user when multiple local repos match same genesis commit","description":"When local repo discovery finds more than one local repo with the same genesis commit (user has two side-by-side clones of the same project, by mistake or intent), there is no reliable heuristic for choosing. The system should ask the user once, then cache the choice keyed by genesis commit in ~/.config/ifcurl/config.toml under [project_origins]. The user can also set false to mark a project as read-only (no local push target).","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-05T14:42:56Z","created_by":"Bruno Postle","updated_at":"2026-06-05T14:42:56Z","dependencies":[{"issue_id":"ifcurl-77u","depends_on_id":"ifcurl-egi","type":"blocks","created_at":"2026-06-05T15:43:16Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
@@ -72,7 +75,7 @@
{"id":"ifcurl-yhp","title":"Query panel: clicking an element in the results list should highlight it in the 3D view","description":"When a selector query is active, the query panel lists matched elements. Clicking a row in this list should highlight (or isolate) that specific element in the 3D canvas, so the user can locate it spatially. This mirrors the click-to-inspect flow but driven from the panel rather than the canvas.","status":"closed","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-05-17T08:45:31Z","created_by":"Bruno Postle","updated_at":"2026-06-07T07:46:10Z","closed_at":"2026-06-07T07:46:10Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-6xq","title":"Query panel does not appear after selector resolves","description":"When a selector is set in the viewer and a query= parameter is also present, the query results table (showing GUID → value pairs) should appear in a side panel. This panel never appears. The applyQuery() function in viewer.js fetches /query from the ifcurl service and populates queryTbody, but the panel remains hidden. Needs investigation: check whether the query param is being passed correctly, whether the /query endpoint returns data, and whether queryPanel.style.display is being set.","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-05-16T13:24:59Z","created_by":"Bruno Postle","updated_at":"2026-05-16T20:02:21Z","started_at":"2026-05-16T14:14:52Z","closed_at":"2026-05-16T20:02:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-hwk","title":"Meta panel: missing type counts, icon is hamburger not (i)","description":"The metadata panel works for storey isolation but has two issues: (1) The toolbar button is a hamburger icon, not an (i) icon as documented in CLAUDE.md check #17. Update the checklist text to match reality. (2) The type counts breakdown (IfcWall: N, IfcSlab: N, etc.) does not appear in the panel — only the storey list is shown. Investigate whether populateMetaPanel() is failing silently or the type classifier isn't running correctly.","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-05-16T13:24:45Z","created_by":"Bruno Postle","updated_at":"2026-05-16T20:02:21Z","started_at":"2026-05-16T14:15:06Z","closed_at":"2026-05-16T20:02:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
-{"id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-06-07T09:13:38Z","started_at":"2026-06-07T09:13:38Z","dependency_count":0,"dependent_count":0,"comment_count":0}
+{"id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-06-07T11:19:12Z","started_at":"2026-06-07T09:13:38Z","closed_at":"2026-06-07T11:19:12Z","close_reason":"POST /documents/1.0/select-documents returns picker URL; GET /select-documents/ui serves self-contained HTML page that browses Forgejo repos/files and redirects to callback with document_ids[]. 18 tests passing.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-bil","title":"Implement OpenCDE OAuth2 proxy routes","description":"The OpenCDE Foundation API requires OAuth2 endpoints. Forgejo already has OAuth2 support — proxy through to it. Routes needed: GET /foundation/1.0/oauth2/auth_url (return Forgejo OAuth2 auth URL), POST /foundation/1.0/oauth2/token (proxy code exchange to Forgejo token endpoint), POST /foundation/1.0/oauth2/token_refresh (proxy refresh). Requires a one-time step of registering an OAuth2 app in Forgejo admin and storing client_id/client_secret as env vars.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:15Z","created_by":"Bruno Postle","updated_at":"2026-06-07T09:08:25Z","started_at":"2026-06-07T08:42:43Z","closed_at":"2026-06-07T09:08:25Z","close_reason":"Implemented in foundation_api.py — three routes at /foundation/1.1/oauth2/{auth_url,token,token_refresh}, proxying to Forgejo, guarded by IFCURL_OAUTH2_CLIENT_ID/SECRET env vars. 22 tests, all passing.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-k3v","title":"Add GET /documents/1.0/document-metadata/{document_id}","description":"The OpenCDE Documents API spec includes a GET endpoint for individual file metadata. Implement it: decode the document_id, fetch the file's latest info from Forgejo (name, size, last-modified), return as a metadata response.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:13Z","created_by":"Bruno Postle","updated_at":"2026-04-29T21:25:07Z","started_at":"2026-04-29T21:22:34Z","closed_at":"2026-04-29T21:25:07Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ye6","title":"Add BCF topic filtering query params","description":"The BCF 3.0 REST spec defines query parameters for GET /topics (topic_status, topic_type, priority, assignee, etc.). Currently we return all topics unfiltered. Translate BCF filter params to Forgejo issues API params (state, labels, assignee) and pass them through.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:00Z","created_by":"Bruno Postle","updated_at":"2026-04-29T21:25:06Z","started_at":"2026-04-29T21:22:21Z","closed_at":"2026-04-29T21:25:06Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
diff --git a/ifcurl/documents_api.py b/ifcurl/documents_api.py
index b32f58a..9f565f6 100644
--- a/ifcurl/documents_api.py
+++ b/ifcurl/documents_api.py
@@ -350,19 +350,25 @@ function addListItem(list, text, cls, onclick) {
function renderBreadcrumb(path) {
const bc = document.getElementById('breadcrumb');
+ bc.textContent = '';
const parts = path ? path.split('/') : [];
- const spans = ['' + escHtml(currentRepo) + ''];
+ bc.appendChild(makeCrumb(currentRepo, ''));
let built = '';
for (const p of parts) {
built = built ? built + '/' + p : p;
+ const sep = document.createTextNode(' / ');
+ bc.appendChild(sep);
const captured = built;
- spans.push('' + escHtml(p) + '');
+ bc.appendChild(makeCrumb(p, captured));
}
- bc.innerHTML = spans.join(' / ');
}
-function escHtml(s) {
- return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
+function makeCrumb(label, target) {
+ const s = document.createElement('span');
+ s.textContent = label;
+ s.style.cursor = 'pointer';
+ s.onclick = () => browseDir(target);
+ return s;
}
function selectFile(path) {