Fix workspace store capability reporting to reflect backend support
This commit is contained in:
parent
5a140178a1
commit
d045614909
14
README.md
14
README.md
|
|
@ -324,8 +324,9 @@ curl --unix-socket amduatd.sock \
|
||||||
|
|
||||||
`/v1/space/workspace` returns a deterministic, read-only snapshot for the
|
`/v1/space/workspace` returns a deterministic, read-only snapshot for the
|
||||||
effective space. It aggregates the manifest, mount resolution, per-mount cursor
|
effective space. It aggregates the manifest, mount resolution, per-mount cursor
|
||||||
status, store backend metadata, federation flags, and store capabilities into
|
status, store backend metadata, federation flags, and store capabilities
|
||||||
one JSON response. It performs no network I/O and does not mutate storage.
|
(`capabilities.supported_ops`) into one JSON response. It performs no network
|
||||||
|
I/O and does not mutate storage.
|
||||||
|
|
||||||
This is a local snapshot that complements:
|
This is a local snapshot that complements:
|
||||||
- `/v1/space/manifest` (manifest root + canonical manifest)
|
- `/v1/space/manifest` (manifest root + canonical manifest)
|
||||||
|
|
@ -342,9 +343,12 @@ curl --unix-socket amduatd.sock \
|
||||||
## Workspace UI
|
## Workspace UI
|
||||||
|
|
||||||
`/workspace` serves a minimal, human-facing page that consumes
|
`/workspace` serves a minimal, human-facing page that consumes
|
||||||
`/v1/space/workspace` and `/v1/space/mounts/sync/until`. It is a convenience
|
`/v1/space/workspace` and `/v1/space/mounts/sync/until`, plus read-only
|
||||||
view for inspection and manual sync control, not a stable API. For
|
health panels for `/v1/space/doctor`, `/v1/space/roots`,
|
||||||
programmatic use, call the `/v1/*` endpoints directly.
|
`/v1/space/sync/status`, `/v1/space/mounts/resolve`, and
|
||||||
|
`/v1/space/manifest`. It is a convenience view for inspection and manual sync
|
||||||
|
control, not a stable API. For programmatic use, call the `/v1/*` endpoints
|
||||||
|
directly.
|
||||||
|
|
||||||
## Space mounts sync (track mounts)
|
## Space mounts sync (track mounts)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,59 @@
|
||||||
},
|
},
|
||||||
"peers": {"type": "array", "items": {"$ref": "#/schemas/space_sync_status_peer"}}
|
"peers": {"type": "array", "items": {"$ref": "#/schemas/space_sync_status_peer"}}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"space_workspace_response": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["effective_space", "store_backend", "federation", "capabilities", "manifest_ref", "manifest", "mounts"],
|
||||||
|
"properties": {
|
||||||
|
"effective_space": {"type": "object"},
|
||||||
|
"store_backend": {"type": "string"},
|
||||||
|
"federation": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["enabled", "transport"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {"type": "boolean"},
|
||||||
|
"transport": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["supported_ops"],
|
||||||
|
"properties": {
|
||||||
|
"supported_ops": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"put": {"type": "boolean"},
|
||||||
|
"get": {"type": "boolean"},
|
||||||
|
"put_indexed": {"type": "boolean"},
|
||||||
|
"get_indexed": {"type": "boolean"},
|
||||||
|
"tombstone": {"type": "boolean"},
|
||||||
|
"tombstone_lift": {"type": "boolean"},
|
||||||
|
"log_scan": {"type": "boolean"},
|
||||||
|
"current_state": {"type": "boolean"},
|
||||||
|
"validate_config": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"implemented_ops": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"put": {"type": "boolean"},
|
||||||
|
"get": {"type": "boolean"},
|
||||||
|
"put_indexed": {"type": "boolean"},
|
||||||
|
"get_indexed": {"type": "boolean"},
|
||||||
|
"tombstone": {"type": "boolean"},
|
||||||
|
"tombstone_lift": {"type": "boolean"},
|
||||||
|
"log_scan": {"type": "boolean"},
|
||||||
|
"current_state": {"type": "boolean"},
|
||||||
|
"validate_config": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manifest_ref": {"type": "string"},
|
||||||
|
"manifest": {"type": "object"},
|
||||||
|
"mounts": {"type": "array"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"88c7e93034dacc34318b0771f1bae232bb4a34913f7274d7e99b007fe4f697c3","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."}
|
{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"38cb6beb6bb525d892538dad7aa584b3f2aeaaff177757fd9432fce9602f877b","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."}
|
||||||
|
|
|
||||||
375
src/amduatd.c
375
src/amduatd.c
|
|
@ -233,7 +233,79 @@ static const char k_amduatd_workspace_html[] =
|
||||||
" <h2>Raw workspace JSON</h2>\n"
|
" <h2>Raw workspace JSON</h2>\n"
|
||||||
" <pre id=\"workspace_raw\"></pre>\n"
|
" <pre id=\"workspace_raw\"></pre>\n"
|
||||||
" </div>\n"
|
" </div>\n"
|
||||||
" </div>\n"
|
"\n"
|
||||||
|
" <div class=\"card\" style=\"margin-top:16px;\">\n"
|
||||||
|
" <h2>Health / Debug</h2>\n"
|
||||||
|
" <p class=\"muted\">Fetch read-only diagnostics for the current space.</p>\n"
|
||||||
|
"\n"
|
||||||
|
" <div class=\"card\" style=\"margin-top:12px;\">\n"
|
||||||
|
" <h2>Doctor</h2>\n"
|
||||||
|
" <div class=\"bar\">\n"
|
||||||
|
" <button id=\"doctor_fetch\">Fetch</button>\n"
|
||||||
|
" <span id=\"doctor_status\" class=\"muted\">idle</span>\n"
|
||||||
|
" <span id=\"doctor_summary\" class=\"mono\"></span>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" <details id=\"doctor_details\" hidden>\n"
|
||||||
|
" <summary>Raw JSON</summary>\n"
|
||||||
|
" <pre id=\"doctor_raw\"></pre>\n"
|
||||||
|
" </details>\n"
|
||||||
|
" </div>\n"
|
||||||
|
"\n"
|
||||||
|
" <div class=\"card\" style=\"margin-top:12px;\">\n"
|
||||||
|
" <h2>Roots</h2>\n"
|
||||||
|
" <div class=\"bar\">\n"
|
||||||
|
" <button id=\"roots_fetch\">Fetch</button>\n"
|
||||||
|
" <span id=\"roots_status\" class=\"muted\">idle</span>\n"
|
||||||
|
" <span id=\"roots_summary\" class=\"mono\"></span>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" <div id=\"roots_list\" class=\"mono\" style=\"margin-top:8px;\"></div>\n"
|
||||||
|
" <details id=\"roots_details\" hidden>\n"
|
||||||
|
" <summary>Raw JSON</summary>\n"
|
||||||
|
" <pre id=\"roots_raw\"></pre>\n"
|
||||||
|
" </details>\n"
|
||||||
|
" </div>\n"
|
||||||
|
"\n"
|
||||||
|
" <div class=\"card\" style=\"margin-top:12px;\">\n"
|
||||||
|
" <h2>Sync status</h2>\n"
|
||||||
|
" <div class=\"bar\">\n"
|
||||||
|
" <button id=\"sync_status_fetch\">Fetch</button>\n"
|
||||||
|
" <span id=\"sync_status\" class=\"muted\">idle</span>\n"
|
||||||
|
" <span id=\"sync_status_summary\" class=\"mono\"></span>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" <div id=\"sync_status_table\" style=\"margin-top:8px;\"></div>\n"
|
||||||
|
" <details id=\"sync_status_details\" hidden>\n"
|
||||||
|
" <summary>Raw JSON</summary>\n"
|
||||||
|
" <pre id=\"sync_status_raw\"></pre>\n"
|
||||||
|
" </details>\n"
|
||||||
|
" </div>\n"
|
||||||
|
"\n"
|
||||||
|
" <div class=\"card\" style=\"margin-top:12px;\">\n"
|
||||||
|
" <h2>Mounts resolve</h2>\n"
|
||||||
|
" <div class=\"bar\">\n"
|
||||||
|
" <button id=\"mounts_resolve_fetch\">Fetch</button>\n"
|
||||||
|
" <span id=\"mounts_resolve_status\" class=\"muted\">idle</span>\n"
|
||||||
|
" <span id=\"mounts_resolve_summary\" class=\"mono\"></span>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" <details id=\"mounts_resolve_details\" hidden>\n"
|
||||||
|
" <summary>Raw JSON</summary>\n"
|
||||||
|
" <pre id=\"mounts_resolve_raw\"></pre>\n"
|
||||||
|
" </details>\n"
|
||||||
|
" </div>\n"
|
||||||
|
"\n"
|
||||||
|
" <div class=\"card\" style=\"margin-top:12px;\">\n"
|
||||||
|
" <h2>Manifest</h2>\n"
|
||||||
|
" <div class=\"bar\">\n"
|
||||||
|
" <button id=\"manifest_fetch\">Fetch</button>\n"
|
||||||
|
" <span id=\"manifest_status\" class=\"muted\">idle</span>\n"
|
||||||
|
" <span id=\"manifest_summary\" class=\"mono\"></span>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" <details id=\"manifest_details\" hidden>\n"
|
||||||
|
" <summary>Raw JSON</summary>\n"
|
||||||
|
" <pre id=\"manifest_raw\"></pre>\n"
|
||||||
|
" </details>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" </div>\n"
|
||||||
|
" </div>\n"
|
||||||
"\n"
|
"\n"
|
||||||
" <script>\n"
|
" <script>\n"
|
||||||
" const spaceInput = document.getElementById('space');\n"
|
" const spaceInput = document.getElementById('space');\n"
|
||||||
|
|
@ -252,6 +324,33 @@ static const char k_amduatd_workspace_html[] =
|
||||||
" const syncSummary = document.getElementById('sync_summary');\n"
|
" const syncSummary = document.getElementById('sync_summary');\n"
|
||||||
" const syncDetails = document.getElementById('sync_details');\n"
|
" const syncDetails = document.getElementById('sync_details');\n"
|
||||||
" const syncRaw = document.getElementById('sync_raw');\n"
|
" const syncRaw = document.getElementById('sync_raw');\n"
|
||||||
|
" const doctorFetch = document.getElementById('doctor_fetch');\n"
|
||||||
|
" const doctorStatus = document.getElementById('doctor_status');\n"
|
||||||
|
" const doctorSummary = document.getElementById('doctor_summary');\n"
|
||||||
|
" const doctorDetails = document.getElementById('doctor_details');\n"
|
||||||
|
" const doctorRaw = document.getElementById('doctor_raw');\n"
|
||||||
|
" const rootsFetch = document.getElementById('roots_fetch');\n"
|
||||||
|
" const rootsStatus = document.getElementById('roots_status');\n"
|
||||||
|
" const rootsSummary = document.getElementById('roots_summary');\n"
|
||||||
|
" const rootsList = document.getElementById('roots_list');\n"
|
||||||
|
" const rootsDetails = document.getElementById('roots_details');\n"
|
||||||
|
" const rootsRaw = document.getElementById('roots_raw');\n"
|
||||||
|
" const syncStatusFetch = document.getElementById('sync_status_fetch');\n"
|
||||||
|
" const syncStatus = document.getElementById('sync_status');\n"
|
||||||
|
" const syncStatusSummary = document.getElementById('sync_status_summary');\n"
|
||||||
|
" const syncStatusTable = document.getElementById('sync_status_table');\n"
|
||||||
|
" const syncStatusDetails = document.getElementById('sync_status_details');\n"
|
||||||
|
" const syncStatusRaw = document.getElementById('sync_status_raw');\n"
|
||||||
|
" const mountsResolveFetch = document.getElementById('mounts_resolve_fetch');\n"
|
||||||
|
" const mountsResolveStatus = document.getElementById('mounts_resolve_status');\n"
|
||||||
|
" const mountsResolveSummary = document.getElementById('mounts_resolve_summary');\n"
|
||||||
|
" const mountsResolveDetails = document.getElementById('mounts_resolve_details');\n"
|
||||||
|
" const mountsResolveRaw = document.getElementById('mounts_resolve_raw');\n"
|
||||||
|
" const manifestFetch = document.getElementById('manifest_fetch');\n"
|
||||||
|
" const manifestStatus = document.getElementById('manifest_status');\n"
|
||||||
|
" const manifestSummary = document.getElementById('manifest_summary');\n"
|
||||||
|
" const manifestDetails = document.getElementById('manifest_details');\n"
|
||||||
|
" const manifestRaw = document.getElementById('manifest_raw');\n"
|
||||||
"\n"
|
"\n"
|
||||||
" function getSpaceHeader() {\n"
|
" function getSpaceHeader() {\n"
|
||||||
" const raw = spaceInput.value.trim();\n"
|
" const raw = spaceInput.value.trim();\n"
|
||||||
|
|
@ -282,13 +381,25 @@ static const char k_amduatd_workspace_html[] =
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
" function renderCapabilities(caps) {\n"
|
" function renderCapabilities(caps) {\n"
|
||||||
" if (!caps || !caps.store_ops) {\n"
|
" if (!caps || !caps.supported_ops) {\n"
|
||||||
" setMonospaceText(capabilitiesEl, '-');\n"
|
" setMonospaceText(capabilitiesEl, '-');\n"
|
||||||
" return;\n"
|
" return;\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
" const keys = ['put','get','put_indexed','get_indexed','tombstone','tombstone_lift','log_scan','current_state','validate_config'];\n"
|
" const keys = ['put','get','put_indexed','get_indexed','tombstone','tombstone_lift','log_scan','current_state','validate_config'];\n"
|
||||||
" const lines = keys.map((key) => `${key}: ${caps.store_ops[key] === true ? 'true' : 'false'}`);\n"
|
" const lines = keys.map((key) => `${key}: ${caps.supported_ops[key] === true ? 'true' : 'false'}`);\n"
|
||||||
" capabilitiesEl.textContent = lines.join('\\n');\n"
|
" capabilitiesEl.textContent = lines.join('\\n');\n"
|
||||||
|
" if (caps.implemented_ops) {\n"
|
||||||
|
" const impl = keys.map((key) => `${key}: ${caps.implemented_ops[key] === true ? 'true' : 'false'}`);\n"
|
||||||
|
" const details = document.createElement('details');\n"
|
||||||
|
" const summary = document.createElement('summary');\n"
|
||||||
|
" const pre = document.createElement('pre');\n"
|
||||||
|
" summary.textContent = 'Implemented ops (debug)';\n"
|
||||||
|
" pre.textContent = impl.join('\\n');\n"
|
||||||
|
" details.appendChild(summary);\n"
|
||||||
|
" details.appendChild(pre);\n"
|
||||||
|
" capabilitiesEl.appendChild(document.createElement('div'));\n"
|
||||||
|
" capabilitiesEl.appendChild(details);\n"
|
||||||
|
" }\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
" function renderMounts(mounts) {\n"
|
" function renderMounts(mounts) {\n"
|
||||||
|
|
@ -319,6 +430,75 @@ static const char k_amduatd_workspace_html[] =
|
||||||
" });\n"
|
" });\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" function truncateBody(text, limit) {\n"
|
||||||
|
" if (!text) {\n"
|
||||||
|
" return '';\n"
|
||||||
|
" }\n"
|
||||||
|
" if (text.length <= limit) {\n"
|
||||||
|
" return text;\n"
|
||||||
|
" }\n"
|
||||||
|
" return text.slice(0, limit) + '...';\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
|
" async function fetchPanel(endpoint, statusEl, summaryEl, rawEl, detailsEl, onData) {\n"
|
||||||
|
" clearMessage();\n"
|
||||||
|
" statusEl.textContent = 'loading';\n"
|
||||||
|
" summaryEl.textContent = '';\n"
|
||||||
|
" if (detailsEl) {\n"
|
||||||
|
" detailsEl.hidden = true;\n"
|
||||||
|
" }\n"
|
||||||
|
" if (rawEl) {\n"
|
||||||
|
" rawEl.textContent = '';\n"
|
||||||
|
" }\n"
|
||||||
|
" const space = getSpaceHeader();\n"
|
||||||
|
" if (spaceError.hidden === false) {\n"
|
||||||
|
" statusEl.textContent = 'error';\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" const headers = {};\n"
|
||||||
|
" if (space) {\n"
|
||||||
|
" headers['X-Amduat-Space'] = space;\n"
|
||||||
|
" }\n"
|
||||||
|
" let res;\n"
|
||||||
|
" try {\n"
|
||||||
|
" res = await fetch(endpoint, { headers });\n"
|
||||||
|
" } catch (err) {\n"
|
||||||
|
" statusEl.textContent = 'error';\n"
|
||||||
|
" summaryEl.textContent = 'network error';\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" const body = await res.text();\n"
|
||||||
|
" if (rawEl) {\n"
|
||||||
|
" rawEl.textContent = body;\n"
|
||||||
|
" }\n"
|
||||||
|
" if (detailsEl) {\n"
|
||||||
|
" detailsEl.hidden = false;\n"
|
||||||
|
" }\n"
|
||||||
|
" if (!res.ok) {\n"
|
||||||
|
" statusEl.textContent = 'error';\n"
|
||||||
|
" summaryEl.textContent = `http ${res.status}: ${truncateBody(body, 240)}`;\n"
|
||||||
|
" if (onData) {\n"
|
||||||
|
" onData(null, res.status);\n"
|
||||||
|
" }\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" let data;\n"
|
||||||
|
" try {\n"
|
||||||
|
" data = JSON.parse(body);\n"
|
||||||
|
" } catch (err) {\n"
|
||||||
|
" statusEl.textContent = 'error';\n"
|
||||||
|
" summaryEl.textContent = 'invalid json';\n"
|
||||||
|
" if (onData) {\n"
|
||||||
|
" onData(null, res.status);\n"
|
||||||
|
" }\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" statusEl.textContent = 'ok';\n"
|
||||||
|
" if (onData) {\n"
|
||||||
|
" onData(data, res.status);\n"
|
||||||
|
" }\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
" async function loadWorkspace() {\n"
|
" async function loadWorkspace() {\n"
|
||||||
" clearMessage();\n"
|
" clearMessage();\n"
|
||||||
" const space = getSpaceHeader();\n"
|
" const space = getSpaceHeader();\n"
|
||||||
|
|
@ -425,12 +605,140 @@ static const char k_amduatd_workspace_html[] =
|
||||||
" syncing.hidden = true;\n"
|
" syncing.hidden = true;\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
" refreshBtn.addEventListener('click', loadWorkspace);\n"
|
" function renderDoctor(data, status) {\n"
|
||||||
|
" if (!data) {\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" if (data.summary && (data.summary.ok_count !== undefined || data.summary.warn_count !== undefined)) {\n"
|
||||||
|
" const okCount = data.summary.ok_count !== undefined ? data.summary.ok_count : 0;\n"
|
||||||
|
" const warnCount = data.summary.warn_count !== undefined ? data.summary.warn_count : 0;\n"
|
||||||
|
" const failCount = data.summary.fail_count !== undefined ? data.summary.fail_count : 0;\n"
|
||||||
|
" const skippedCount = data.summary.skipped_count !== undefined ? data.summary.skipped_count : 0;\n"
|
||||||
|
" doctorSummary.textContent = `ok=${okCount} warn=${warnCount} fail=${failCount} skipped=${skippedCount}`;\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" let warnCount = 0;\n"
|
||||||
|
" let okCount = 0;\n"
|
||||||
|
" let skipCount = 0;\n"
|
||||||
|
" if (Array.isArray(data.checks)) {\n"
|
||||||
|
" data.checks.forEach((item) => {\n"
|
||||||
|
" if (!item) {\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" if (item.status === 'ok') {\n"
|
||||||
|
" okCount += 1;\n"
|
||||||
|
" } else if (item.status === 'skipped') {\n"
|
||||||
|
" skipCount += 1;\n"
|
||||||
|
" } else {\n"
|
||||||
|
" warnCount += 1;\n"
|
||||||
|
" }\n"
|
||||||
|
" });\n"
|
||||||
|
" }\n"
|
||||||
|
" if (okCount + warnCount + skipCount > 0) {\n"
|
||||||
|
" doctorSummary.textContent = `ok=${okCount} warn=${warnCount} skipped=${skipCount}`;\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" doctorSummary.textContent = data.status !== undefined ? `status=${data.status}` : `http ${status}`;\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
|
" function renderRoots(data) {\n"
|
||||||
|
" rootsList.textContent = '';\n"
|
||||||
|
" if (!data || !Array.isArray(data.roots)) {\n"
|
||||||
|
" rootsSummary.textContent = 'roots=0';\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" rootsSummary.textContent = `roots=${data.roots.length}`;\n"
|
||||||
|
" const shown = data.roots.slice(0, 10);\n"
|
||||||
|
" rootsList.textContent = shown.join('\\n');\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
|
" function renderSyncStatus(data) {\n"
|
||||||
|
" syncStatusTable.innerHTML = '';\n"
|
||||||
|
" if (!data || !Array.isArray(data.peers)) {\n"
|
||||||
|
" syncStatusSummary.textContent = 'peers=0 remotes=0';\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" let remotes = 0;\n"
|
||||||
|
" data.peers.forEach((peer) => {\n"
|
||||||
|
" if (peer && Array.isArray(peer.remotes)) {\n"
|
||||||
|
" remotes += peer.remotes.length;\n"
|
||||||
|
" }\n"
|
||||||
|
" });\n"
|
||||||
|
" syncStatusSummary.textContent = `peers=${data.peers.length} remotes=${remotes}`;\n"
|
||||||
|
" let html = '<table><thead><tr><th>Peer</th><th>Remote</th><th>Pull</th><th>Push</th></tr></thead><tbody>';\n"
|
||||||
|
" data.peers.forEach((peer) => {\n"
|
||||||
|
" if (!peer || !Array.isArray(peer.remotes)) {\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" peer.remotes.forEach((remote) => {\n"
|
||||||
|
" const pull = remote && remote.pull_cursor ? remote.pull_cursor : null;\n"
|
||||||
|
" const push = remote && remote.push_cursor ? remote.push_cursor : null;\n"
|
||||||
|
" const pullText = pull ? `present=${pull.present}` +\n"
|
||||||
|
" (pull.last_logseq !== undefined ? ` last_logseq=${pull.last_logseq}` : '') : '';\n"
|
||||||
|
" const pushText = push ? `present=${push.present}` +\n"
|
||||||
|
" (push.last_logseq !== undefined ? ` last_logseq=${push.last_logseq}` : '') : '';\n"
|
||||||
|
" html += `<tr><td class=\"mono\">${peer.peer_key || ''}</td>` +\n"
|
||||||
|
" `<td class=\"mono\">${remote.remote_space_id || 'null'}</td>` +\n"
|
||||||
|
" `<td class=\"mono\">${pullText}</td>` +\n"
|
||||||
|
" `<td class=\"mono\">${pushText}</td></tr>`;\n"
|
||||||
|
" });\n"
|
||||||
|
" });\n"
|
||||||
|
" html += '</tbody></table>';\n"
|
||||||
|
" syncStatusTable.innerHTML = html;\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
|
" function renderMountsResolve(data) {\n"
|
||||||
|
" if (!data || !Array.isArray(data.mounts)) {\n"
|
||||||
|
" mountsResolveSummary.textContent = 'mounts=0 pinned=0 track=0';\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" let pinned = 0;\n"
|
||||||
|
" let track = 0;\n"
|
||||||
|
" data.mounts.forEach((mount) => {\n"
|
||||||
|
" if (mount && mount.mode === 'pinned') {\n"
|
||||||
|
" pinned += 1;\n"
|
||||||
|
" } else if (mount && mount.mode === 'track') {\n"
|
||||||
|
" track += 1;\n"
|
||||||
|
" }\n"
|
||||||
|
" });\n"
|
||||||
|
" mountsResolveSummary.textContent = `mounts=${data.mounts.length} pinned=${pinned} track=${track}`;\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
|
" function renderManifest(data, status) {\n"
|
||||||
|
" if (!data) {\n"
|
||||||
|
" if (status === 404) {\n"
|
||||||
|
" manifestSummary.textContent = 'no manifest configured';\n"
|
||||||
|
" }\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" const count = data.manifest && Array.isArray(data.manifest.mounts)\n"
|
||||||
|
" ? data.manifest.mounts.length : 0;\n"
|
||||||
|
" manifestSummary.textContent = `ref=${data.manifest_ref || '-'} mounts=${count}`;\n"
|
||||||
|
" }\n"
|
||||||
|
"\n"
|
||||||
|
" refreshBtn.addEventListener('click', () => {\n"
|
||||||
|
" loadWorkspace();\n"
|
||||||
|
" });\n"
|
||||||
" syncBtn.addEventListener('click', syncMounts);\n"
|
" syncBtn.addEventListener('click', syncMounts);\n"
|
||||||
|
" doctorFetch.addEventListener('click', () => {\n"
|
||||||
|
" fetchPanel('/v1/space/doctor', doctorStatus, doctorSummary, doctorRaw, doctorDetails, renderDoctor);\n"
|
||||||
|
" });\n"
|
||||||
|
" rootsFetch.addEventListener('click', () => {\n"
|
||||||
|
" fetchPanel('/v1/space/roots', rootsStatus, rootsSummary, rootsRaw, rootsDetails, renderRoots);\n"
|
||||||
|
" });\n"
|
||||||
|
" syncStatusFetch.addEventListener('click', () => {\n"
|
||||||
|
" fetchPanel('/v1/space/sync/status', syncStatus, syncStatusSummary, syncStatusRaw, syncStatusDetails, renderSyncStatus);\n"
|
||||||
|
" });\n"
|
||||||
|
" mountsResolveFetch.addEventListener('click', () => {\n"
|
||||||
|
" fetchPanel('/v1/space/mounts/resolve', mountsResolveStatus, mountsResolveSummary, mountsResolveRaw, mountsResolveDetails, renderMountsResolve);\n"
|
||||||
|
" });\n"
|
||||||
|
" manifestFetch.addEventListener('click', () => {\n"
|
||||||
|
" fetchPanel('/v1/space/manifest', manifestStatus, manifestSummary, manifestRaw, manifestDetails, renderManifest);\n"
|
||||||
|
" });\n"
|
||||||
" window.addEventListener('load', loadWorkspace);\n"
|
" window.addEventListener('load', loadWorkspace);\n"
|
||||||
" </script>\n"
|
" </script>\n"
|
||||||
"</body>\n"
|
"</body>\n"
|
||||||
"</html>\n";
|
"</html>\n";
|
||||||
static const char k_amduatd_contract_v1_json[] =
|
static const char k_amduatd_contract_v1_json[] =
|
||||||
"{"
|
"{"
|
||||||
"\"contract\":\"AMDUATD/API/1\","
|
"\"contract\":\"AMDUATD/API/1\","
|
||||||
|
|
@ -785,6 +1093,59 @@ static const char k_amduatd_contract_v1_json[] =
|
||||||
"},"
|
"},"
|
||||||
"\"peers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/schemas/space_sync_status_peer\"}}"
|
"\"peers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/schemas/space_sync_status_peer\"}}"
|
||||||
"}"
|
"}"
|
||||||
|
"},"
|
||||||
|
"\"space_workspace_response\":{"
|
||||||
|
"\"type\":\"object\","
|
||||||
|
"\"required\":[\"effective_space\",\"store_backend\",\"federation\",\"capabilities\",\"manifest_ref\",\"manifest\",\"mounts\"],"
|
||||||
|
"\"properties\":{"
|
||||||
|
"\"effective_space\":{\"type\":\"object\"},"
|
||||||
|
"\"store_backend\":{\"type\":\"string\"},"
|
||||||
|
"\"federation\":{"
|
||||||
|
"\"type\":\"object\","
|
||||||
|
"\"required\":[\"enabled\",\"transport\"],"
|
||||||
|
"\"properties\":{"
|
||||||
|
"\"enabled\":{\"type\":\"boolean\"},"
|
||||||
|
"\"transport\":{\"type\":\"string\"}"
|
||||||
|
"}"
|
||||||
|
"},"
|
||||||
|
"\"capabilities\":{"
|
||||||
|
"\"type\":\"object\","
|
||||||
|
"\"required\":[\"supported_ops\"],"
|
||||||
|
"\"properties\":{"
|
||||||
|
"\"supported_ops\":{"
|
||||||
|
"\"type\":\"object\","
|
||||||
|
"\"properties\":{"
|
||||||
|
"\"put\":{\"type\":\"boolean\"},"
|
||||||
|
"\"get\":{\"type\":\"boolean\"},"
|
||||||
|
"\"put_indexed\":{\"type\":\"boolean\"},"
|
||||||
|
"\"get_indexed\":{\"type\":\"boolean\"},"
|
||||||
|
"\"tombstone\":{\"type\":\"boolean\"},"
|
||||||
|
"\"tombstone_lift\":{\"type\":\"boolean\"},"
|
||||||
|
"\"log_scan\":{\"type\":\"boolean\"},"
|
||||||
|
"\"current_state\":{\"type\":\"boolean\"},"
|
||||||
|
"\"validate_config\":{\"type\":\"boolean\"}"
|
||||||
|
"}"
|
||||||
|
"},"
|
||||||
|
"\"implemented_ops\":{"
|
||||||
|
"\"type\":\"object\","
|
||||||
|
"\"properties\":{"
|
||||||
|
"\"put\":{\"type\":\"boolean\"},"
|
||||||
|
"\"get\":{\"type\":\"boolean\"},"
|
||||||
|
"\"put_indexed\":{\"type\":\"boolean\"},"
|
||||||
|
"\"get_indexed\":{\"type\":\"boolean\"},"
|
||||||
|
"\"tombstone\":{\"type\":\"boolean\"},"
|
||||||
|
"\"tombstone_lift\":{\"type\":\"boolean\"},"
|
||||||
|
"\"log_scan\":{\"type\":\"boolean\"},"
|
||||||
|
"\"current_state\":{\"type\":\"boolean\"},"
|
||||||
|
"\"validate_config\":{\"type\":\"boolean\"}"
|
||||||
|
"}"
|
||||||
|
"}"
|
||||||
|
"}"
|
||||||
|
"},"
|
||||||
|
"\"manifest_ref\":{\"type\":\"string\"},"
|
||||||
|
"\"manifest\":{\"type\":\"object\"},"
|
||||||
|
"\"mounts\":{\"type\":\"array\"}"
|
||||||
|
"}"
|
||||||
"}"
|
"}"
|
||||||
"}"
|
"}"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,81 @@ static bool amduatd_workspace_buf_append_char(amduatd_workspace_buf_t *b,
|
||||||
|
|
||||||
static bool amduatd_workspace_append_capabilities(
|
static bool amduatd_workspace_append_capabilities(
|
||||||
amduatd_workspace_buf_t *b,
|
amduatd_workspace_buf_t *b,
|
||||||
const amduat_asl_store_t *store) {
|
const amduat_asl_store_t *store,
|
||||||
|
amduatd_store_backend_t store_backend) {
|
||||||
const amduat_asl_store_ops_t *ops = store != NULL ? &store->ops : NULL;
|
const amduat_asl_store_ops_t *ops = store != NULL ? &store->ops : NULL;
|
||||||
|
amduatd_store_caps_t caps;
|
||||||
|
if (!amduatd_store_caps_supported(store_backend, &caps)) {
|
||||||
|
memset(&caps, 0, sizeof(caps));
|
||||||
|
}
|
||||||
if (!amduatd_workspace_buf_append_cstr(b, ",\"capabilities\":{")) {
|
if (!amduatd_workspace_buf_append_cstr(b, ",\"capabilities\":{")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!amduatd_workspace_buf_append_cstr(b, "\"store_ops\":{")) {
|
if (!amduatd_workspace_buf_append_cstr(b, "\"supported_ops\":{")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
"\"put\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(b, caps.put ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"get\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(b, caps.get ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"put_indexed\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.put_indexed ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"get_indexed\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.get_indexed ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"tombstone\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.tombstone ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"tombstone_lift\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.tombstone_lift ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"log_scan\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.log_scan ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"current_state\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.current_state ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
b,
|
||||||
|
",\"validate_config\":") ||
|
||||||
|
!amduatd_workspace_buf_append_cstr(
|
||||||
|
b, caps.validate_config ? "true" : "false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!amduatd_workspace_buf_append_cstr(b, "},\"implemented_ops\":{")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!amduatd_workspace_buf_append_cstr(
|
if (!amduatd_workspace_buf_append_cstr(
|
||||||
|
|
@ -428,7 +497,7 @@ amduatd_space_workspace_status_t amduatd_space_workspace_get(
|
||||||
if (!amduatd_workspace_buf_append_cstr(&b, "}")) {
|
if (!amduatd_workspace_buf_append_cstr(&b, "}")) {
|
||||||
goto workspace_cleanup;
|
goto workspace_cleanup;
|
||||||
}
|
}
|
||||||
if (!amduatd_workspace_append_capabilities(&b, store)) {
|
if (!amduatd_workspace_append_capabilities(&b, store, store_backend)) {
|
||||||
goto workspace_cleanup;
|
goto workspace_cleanup;
|
||||||
}
|
}
|
||||||
if (!amduatd_workspace_buf_append_cstr(&b, ",\"manifest_ref\":\"") ||
|
if (!amduatd_workspace_buf_append_cstr(&b, ",\"manifest_ref\":\"") ||
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,33 @@ const char *amduatd_store_backend_name(amduatd_store_backend_t backend) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool amduatd_store_caps_supported(amduatd_store_backend_t backend,
|
||||||
|
amduatd_store_caps_t *out_caps) {
|
||||||
|
if (out_caps == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memset(out_caps, 0, sizeof(*out_caps));
|
||||||
|
if (backend == AMDUATD_STORE_BACKEND_FS) {
|
||||||
|
out_caps->get = true;
|
||||||
|
out_caps->put = true;
|
||||||
|
out_caps->validate_config = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (backend == AMDUATD_STORE_BACKEND_INDEX) {
|
||||||
|
out_caps->get = true;
|
||||||
|
out_caps->put = true;
|
||||||
|
out_caps->get_indexed = true;
|
||||||
|
out_caps->put_indexed = true;
|
||||||
|
out_caps->log_scan = true;
|
||||||
|
out_caps->current_state = true;
|
||||||
|
out_caps->tombstone = true;
|
||||||
|
out_caps->tombstone_lift = true;
|
||||||
|
out_caps->validate_config = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool amduatd_store_init(amduat_asl_store_t *store,
|
bool amduatd_store_init(amduat_asl_store_t *store,
|
||||||
amduat_asl_store_fs_config_t *cfg,
|
amduat_asl_store_fs_config_t *cfg,
|
||||||
amduatd_store_ctx_t *ctx,
|
amduatd_store_ctx_t *ctx,
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,26 @@ typedef struct {
|
||||||
amduat_asl_store_index_fs_t index_fs;
|
amduat_asl_store_index_fs_t index_fs;
|
||||||
} amduatd_store_ctx_t;
|
} amduatd_store_ctx_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool get;
|
||||||
|
bool put;
|
||||||
|
bool get_indexed;
|
||||||
|
bool put_indexed;
|
||||||
|
bool log_scan;
|
||||||
|
bool current_state;
|
||||||
|
bool tombstone;
|
||||||
|
bool tombstone_lift;
|
||||||
|
bool validate_config;
|
||||||
|
} amduatd_store_caps_t;
|
||||||
|
|
||||||
bool amduatd_store_backend_parse(const char *value,
|
bool amduatd_store_backend_parse(const char *value,
|
||||||
amduatd_store_backend_t *out_backend);
|
amduatd_store_backend_t *out_backend);
|
||||||
|
|
||||||
const char *amduatd_store_backend_name(amduatd_store_backend_t backend);
|
const char *amduatd_store_backend_name(amduatd_store_backend_t backend);
|
||||||
|
|
||||||
|
bool amduatd_store_caps_supported(amduatd_store_backend_t backend,
|
||||||
|
amduatd_store_caps_t *out_caps);
|
||||||
|
|
||||||
bool amduatd_store_init(amduat_asl_store_t *store,
|
bool amduatd_store_init(amduat_asl_store_t *store,
|
||||||
amduat_asl_store_fs_config_t *cfg,
|
amduat_asl_store_fs_config_t *cfg,
|
||||||
amduatd_store_ctx_t *ctx,
|
amduatd_store_ctx_t *ctx,
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,13 @@ static int amduatd_test_workspace_snapshot(void) {
|
||||||
"store backend present");
|
"store backend present");
|
||||||
expect(strstr(workspace_json, "\"transport\":\"stub\"") != NULL,
|
expect(strstr(workspace_json, "\"transport\":\"stub\"") != NULL,
|
||||||
"federation transport present");
|
"federation transport present");
|
||||||
|
expect(strstr(workspace_json,
|
||||||
|
"\"supported_ops\":{\"put\":true,\"get\":true,"
|
||||||
|
"\"put_indexed\":false,\"get_indexed\":false,"
|
||||||
|
"\"tombstone\":false,\"tombstone_lift\":false,"
|
||||||
|
"\"log_scan\":false,\"current_state\":false,"
|
||||||
|
"\"validate_config\":true}") != NULL,
|
||||||
|
"supported ops reflect fs backend");
|
||||||
}
|
}
|
||||||
|
|
||||||
free(workspace_json);
|
free(workspace_json);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue