Discover browser world surfaces through Odin
This commit is contained in:
parent
1eb0ef4470
commit
6270999e11
9 changed files with 137 additions and 95 deletions
|
|
@ -182,8 +182,10 @@ Start the browser reference surface runner:
|
||||||
powershell -ExecutionPolicy Bypass -File .\scripts\start-browser-reference.ps1
|
powershell -ExecutionPolicy Bypass -File .\scripts\start-browser-reference.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://127.0.0.1:8891/`. The page can render local fixture and
|
Open `http://127.0.0.1:8798/`. Odin-owned Hermodr serves the browser reference
|
||||||
advertisement surfaces:
|
and lowers Odin-visible provider advertisements and surfaces. Local fixtures
|
||||||
|
remain an offline oracle when the live catalog is unavailable. The page can
|
||||||
|
render:
|
||||||
|
|
||||||
- `Fensalir Direct2D`, a fixture surface for the planned
|
- `Fensalir Direct2D`, a fixture surface for the planned
|
||||||
CultMesh-to-`AquariumUiDocument` lowering.
|
CultMesh-to-`AquariumUiDocument` lowering.
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@ advertisements and typed command/receipt documents are the boundary.
|
||||||
attachment, export consumer, and report artifacts.
|
attachment, export consumer, and report artifacts.
|
||||||
- Eve retains the browser reference as the behavior oracle and a minimal generic
|
- Eve retains the browser reference as the behavior oracle and a minimal generic
|
||||||
interactive-world fixture.
|
interactive-world fixture.
|
||||||
|
- The browser reference starts through Odin-owned Hermodr, preserves advertised
|
||||||
|
provider and surface identity, selects `interactive-world` by capability, and
|
||||||
|
resolves assets through provider-advertised CDN routes. Its live startup path
|
||||||
|
contains no Aetheria identifiers; local catalogs remain offline oracle data.
|
||||||
|
|
||||||
## Remaining Work
|
## Remaining Work
|
||||||
|
|
||||||
|
|
@ -127,9 +131,9 @@ advertisements and typed command/receipt documents are the boundary.
|
||||||
|
|
||||||
### Generic Game Lowering
|
### Generic Game Lowering
|
||||||
|
|
||||||
- Route browser and remaining runtimes through Odin's aggregated provider
|
- Route remaining runtimes through Odin's aggregated provider discovery rather
|
||||||
discovery rather than local catalogs or sibling paths; Unity's direct
|
than local catalogs or sibling paths; browser/Hermodr and Unity direct Verse
|
||||||
CultMesh Verse discovery is the working reference.
|
discovery are the working references.
|
||||||
- Expand world lowering only through generic surface contracts: entities,
|
- Expand world lowering only through generic surface contracts: entities,
|
||||||
transforms, animation state, collision/navigation intent, cameras, effects,
|
transforms, animation state, collision/navigation intent, cameras, effects,
|
||||||
interaction prompts, and command boundaries.
|
interaction prompts, and command boundaries.
|
||||||
|
|
|
||||||
10
packages/eve-browser-lowering/dist/index.js
vendored
10
packages/eve-browser-lowering/dist/index.js
vendored
|
|
@ -836,7 +836,9 @@ async function resolveGravitySurfaceDocuments(node, props, options, state) {
|
||||||
async function fetchGravitySurfaceDocument(request, options) {
|
async function fetchGravitySurfaceDocument(request, options) {
|
||||||
if (typeof fetch !== "function" || typeof window === "undefined")
|
if (typeof fetch !== "function" || typeof window === "undefined")
|
||||||
return undefined;
|
return undefined;
|
||||||
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "aetheria";
|
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "";
|
||||||
|
if (!providerId)
|
||||||
|
return undefined;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set("documentId", request.documentId);
|
params.set("documentId", request.documentId);
|
||||||
if (request.schemaId)
|
if (request.schemaId)
|
||||||
|
|
@ -1525,7 +1527,7 @@ function gravityBodiesFromDocument(document) {
|
||||||
waveSpeed: numberProp(body.waveSpeed, 0),
|
waveSpeed: numberProp(body.waveSpeed, 0),
|
||||||
icon: assetUriFromRef(body.iconAsset) || assetUriFromRef(displayBody?.iconAsset),
|
icon: assetUriFromRef(body.iconAsset) || assetUriFromRef(displayBody?.iconAsset),
|
||||||
iconSize: numberProp(displayBody?.iconSize, numberProp(body.iconSize, 0)),
|
iconSize: numberProp(displayBody?.iconSize, numberProp(body.iconSize, 0)),
|
||||||
tint: "cultmesh://aetheria/assets/textures/tint_splat",
|
tint: "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2224,10 +2226,6 @@ function textClassName(kind, props, node) {
|
||||||
classes.push("detail", "mono");
|
classes.push("detail", "mono");
|
||||||
if (classes.length === 0)
|
if (classes.length === 0)
|
||||||
classes.push("detail", "eve-text");
|
classes.push("detail", "eve-text");
|
||||||
if (node.id === "aetheria.main_menu.root.title")
|
|
||||||
classes.push("eve-root-title");
|
|
||||||
if (node.id === "aetheria.main_menu.root.subtitle")
|
|
||||||
classes.push("eve-root-subtitle");
|
|
||||||
return classes.join(" ");
|
return classes.join(" ");
|
||||||
}
|
}
|
||||||
function applyControlBoxProps(element, props) {
|
function applyControlBoxProps(element, props) {
|
||||||
|
|
|
||||||
|
|
@ -1017,7 +1017,8 @@ async function fetchGravitySurfaceDocument(
|
||||||
options: EveBrowserLoweringOptions,
|
options: EveBrowserLoweringOptions,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
if (typeof fetch !== "function" || typeof window === "undefined") return undefined;
|
if (typeof fetch !== "function" || typeof window === "undefined") return undefined;
|
||||||
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "aetheria";
|
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "";
|
||||||
|
if (!providerId) return undefined;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set("documentId", request.documentId);
|
params.set("documentId", request.documentId);
|
||||||
if (request.schemaId) params.set("schemaId", request.schemaId);
|
if (request.schemaId) params.set("schemaId", request.schemaId);
|
||||||
|
|
@ -1864,7 +1865,7 @@ function gravityBodiesFromDocument(document: Record<string, unknown> | undefined
|
||||||
waveSpeed: numberProp(body.waveSpeed, 0),
|
waveSpeed: numberProp(body.waveSpeed, 0),
|
||||||
icon: assetUriFromRef((body as Record<string, unknown>).iconAsset) || assetUriFromRef(displayBody?.iconAsset),
|
icon: assetUriFromRef((body as Record<string, unknown>).iconAsset) || assetUriFromRef(displayBody?.iconAsset),
|
||||||
iconSize: numberProp(displayBody?.iconSize, numberProp((body as Record<string, unknown>).iconSize, 0)),
|
iconSize: numberProp(displayBody?.iconSize, numberProp((body as Record<string, unknown>).iconSize, 0)),
|
||||||
tint: "cultmesh://aetheria/assets/textures/tint_splat",
|
tint: "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2642,8 +2643,6 @@ function textClassName(kind: string, props: Record<string, unknown>, node: EveSu
|
||||||
if (kind === "label") classes.push("cultui-label");
|
if (kind === "label") classes.push("cultui-label");
|
||||||
if (props.role === "mono") classes.push("detail", "mono");
|
if (props.role === "mono") classes.push("detail", "mono");
|
||||||
if (classes.length === 0) classes.push("detail", "eve-text");
|
if (classes.length === 0) classes.push("detail", "eve-text");
|
||||||
if (node.id === "aetheria.main_menu.root.title") classes.push("eve-root-title");
|
|
||||||
if (node.id === "aetheria.main_menu.root.subtitle") classes.push("eve-root-subtitle");
|
|
||||||
return classes.join(" ");
|
return classes.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,28 @@
|
||||||
param(
|
param(
|
||||||
[int] $Port = 8891
|
[int] $Port = 8798,
|
||||||
|
[string] $OdinRoot = "E:\Projects\Odin",
|
||||||
|
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" })
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
$launcher = Join-Path $OdinRoot "scripts\start-hermodr.ps1"
|
||||||
$webRoot = Join-Path $projectRoot "web"
|
if (-not (Test-Path -LiteralPath $launcher)) {
|
||||||
$logRoot = Join-Path $projectRoot "artifacts\web"
|
throw "Odin-owned Hermodr launcher is missing: $launcher"
|
||||||
New-Item -ItemType Directory -Force $logRoot | Out-Null
|
|
||||||
|
|
||||||
$pidPath = Join-Path $logRoot "eve-browser-reference.pid"
|
|
||||||
$outLog = Join-Path $logRoot "eve-browser-reference.out.log"
|
|
||||||
$errLog = Join-Path $logRoot "eve-browser-reference.err.log"
|
|
||||||
|
|
||||||
if (Test-Path $pidPath) {
|
|
||||||
$oldPid = Get-Content $pidPath -ErrorAction SilentlyContinue
|
|
||||||
if ($oldPid) {
|
|
||||||
Stop-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$nodeScript = @'
|
$healthUrl = "http://127.0.0.1:$Port/health"
|
||||||
const http = require('http');
|
try {
|
||||||
const fs = require('fs');
|
$health = Invoke-RestMethod -Uri $healthUrl -TimeoutSec 2
|
||||||
const path = require('path');
|
if ($health.ok -and $health.service -eq "hermodr-browser-lowering") {
|
||||||
const root = process.argv[2];
|
Write-Host "Eve browser reference already available at http://127.0.0.1:$Port/"
|
||||||
const port = Number(process.argv[3]);
|
exit 0
|
||||||
const types = {'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.mjs':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json; charset=utf-8','.eve':'text/plain; charset=utf-8'};
|
|
||||||
http.createServer((req, res) => {
|
|
||||||
const urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
|
|
||||||
const clean = urlPath === '/' ? '/index.html' : urlPath;
|
|
||||||
const file = path.normalize(path.join(root, clean));
|
|
||||||
if (!file.startsWith(root)) {
|
|
||||||
res.writeHead(403);
|
|
||||||
res.end('forbidden');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
fs.readFile(file, (err, data) => {
|
} catch {
|
||||||
if (err) {
|
# Hermodr is not running on the requested local port.
|
||||||
res.writeHead(404);
|
}
|
||||||
res.end('not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.writeHead(200, {'content-type': types[path.extname(file)] || 'application/octet-stream'});
|
|
||||||
res.end(data);
|
|
||||||
});
|
|
||||||
}).listen(port, '127.0.0.1', () => console.log(`Eve browser reference listening on http://127.0.0.1:${port}`));
|
|
||||||
'@
|
|
||||||
|
|
||||||
$scriptPath = Join-Path $logRoot "serve-eve-reference.cjs"
|
& $launcher -HostAddress "127.0.0.1" -Port $Port -OdinCultMeshUri $OdinCultMeshUri -NoWindow
|
||||||
Set-Content -LiteralPath $scriptPath -Value $nodeScript -Encoding UTF8
|
if (-not $?) {
|
||||||
$proc = Start-Process -FilePath "node" -ArgumentList @($scriptPath, $webRoot, $Port) -WorkingDirectory $projectRoot -RedirectStandardOutput $outLog -RedirectStandardError $errLog -PassThru -WindowStyle Hidden
|
throw "Hermodr failed to start."
|
||||||
$proc.Id | Set-Content -LiteralPath $pidPath
|
}
|
||||||
Start-Sleep -Milliseconds 500
|
Write-Host "Eve browser reference available at http://127.0.0.1:$Port/"
|
||||||
Get-Content $outLog -Tail 5
|
|
||||||
Write-Host "PID $($proc.Id)"
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
"supportedFeatures": [
|
"supportedFeatures": [
|
||||||
"embeddedDocuments",
|
"embeddedDocuments",
|
||||||
"providerAdvertisements",
|
"providerAdvertisements",
|
||||||
|
"odinProviderDiscovery",
|
||||||
|
"genericWorldDiscovery",
|
||||||
|
"providerAssets",
|
||||||
"stateBindings",
|
"stateBindings",
|
||||||
"styleTokens",
|
"styleTokens",
|
||||||
"authorityWitnesses"
|
"authorityWitnesses"
|
||||||
|
|
@ -57,6 +60,8 @@
|
||||||
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
|
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
|
||||||
"evidencePaths": [
|
"evidencePaths": [
|
||||||
"web/surface.js",
|
"web/surface.js",
|
||||||
|
"web/hermodr-provider-catalog.mjs",
|
||||||
|
"scripts/start-browser-reference.ps1",
|
||||||
"packages/eve-browser-lowering/src/index.ts",
|
"packages/eve-browser-lowering/src/index.ts",
|
||||||
"web/fixtures/eve-world-smoke.provider-advertisement.json"
|
"web/fixtures/eve-world-smoke.provider-advertisement.json"
|
||||||
]
|
]
|
||||||
|
|
@ -79,10 +84,11 @@
|
||||||
"test": {
|
"test": {
|
||||||
"ownerRepo": "Eve",
|
"ownerRepo": "Eve",
|
||||||
"status": "node-tests-and-parity-harness",
|
"status": "node-tests-and-parity-harness",
|
||||||
"command": "node --test web/eve-dsl.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
|
"command": "node --test web/*.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
|
||||||
"evidencePaths": [
|
"evidencePaths": [
|
||||||
"web/eve-dsl.test.mjs",
|
"web/eve-dsl.test.mjs",
|
||||||
"web/provider-advertisements.test.mjs",
|
"web/provider-advertisements.test.mjs",
|
||||||
|
"web/hermodr-provider-catalog.test.mjs",
|
||||||
"packages/eve-browser-lowering/test/browser-lowering.test.mjs",
|
"packages/eve-browser-lowering/test/browser-lowering.test.mjs",
|
||||||
"../EveConformance/tools/parity/parity-manifest.json",
|
"../EveConformance/tools/parity/parity-manifest.json",
|
||||||
"scripts/run-parity-harness.ps1"
|
"scripts/run-parity-harness.ps1"
|
||||||
|
|
|
||||||
41
web/hermodr-provider-catalog.mjs
Normal file
41
web/hermodr-provider-catalog.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
export function createHermodrProviderTargets(catalog) {
|
||||||
|
const providers = new Map((catalog?.providers || [])
|
||||||
|
.map(provider => [provider.id || provider.providerId, provider])
|
||||||
|
.filter(([providerId]) => providerId));
|
||||||
|
|
||||||
|
return (catalog?.surfaces || [])
|
||||||
|
.filter(surface => surface?.providerId && surface?.surfaceId)
|
||||||
|
.map(surface => {
|
||||||
|
const provider = providers.get(surface.providerId) || {};
|
||||||
|
const advertisedSurface = (provider.surfaces || [])
|
||||||
|
.find(candidate => candidate.surfaceId === surface.surfaceId) || {};
|
||||||
|
const surfaceId = surface.surfaceId;
|
||||||
|
const surfaceKind = surface.surfaceKind || advertisedSurface.surfaceKind || "";
|
||||||
|
return {
|
||||||
|
...provider,
|
||||||
|
providerId: surface.providerId,
|
||||||
|
targetId: `${surface.providerId}::${surfaceId}`,
|
||||||
|
title: surface.title || advertisedSurface.title || provider.title || surfaceId,
|
||||||
|
kind: provider.kind || "eve.provider",
|
||||||
|
freshness: { state: "odin-visible-cultmesh" },
|
||||||
|
surfaces: [{
|
||||||
|
...advertisedSurface,
|
||||||
|
transport: "hermodr-surface",
|
||||||
|
surfaceId,
|
||||||
|
surfaceKind,
|
||||||
|
url: `/hermodr/surface/${encodeURIComponent(surface.providerId)}?surfaceId=${encodeURIComponent(surfaceId)}`,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectInitialProviderTarget(targets, requestedId = "", preferredSurfaceKind = "interactive-world") {
|
||||||
|
const available = Array.isArray(targets) ? targets : [];
|
||||||
|
return available.find(target =>
|
||||||
|
target.targetId === requestedId ||
|
||||||
|
target.providerId === requestedId ||
|
||||||
|
target.surfaces?.some(surface => surface.surfaceId === requestedId) ||
|
||||||
|
target.aliases?.includes(requestedId))
|
||||||
|
|| available.find(target => target.surfaces?.some(surface => surface.surfaceKind === preferredSurfaceKind))
|
||||||
|
|| available[0];
|
||||||
|
}
|
||||||
38
web/hermodr-provider-catalog.test.mjs
Normal file
38
web/hermodr-provider-catalog.test.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createHermodrProviderTargets,
|
||||||
|
selectInitialProviderTarget,
|
||||||
|
} from "./hermodr-provider-catalog.mjs";
|
||||||
|
|
||||||
|
const catalog = {
|
||||||
|
providers: [{
|
||||||
|
id: "fixture.game",
|
||||||
|
title: "Fixture game",
|
||||||
|
surfaces: [
|
||||||
|
{ surfaceId: "fixture.menu", surfaceKind: "menu" },
|
||||||
|
{ surfaceId: "fixture.world", surfaceKind: "interactive-world" },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
surfaces: [
|
||||||
|
{ providerId: "fixture.game", surfaceId: "fixture.menu", title: "Menu" },
|
||||||
|
{ providerId: "fixture.game", surfaceId: "fixture.world", surfaceKind: "interactive-world", title: "World" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test("preserves provider and advertised surface identity from Hermodr", () => {
|
||||||
|
const targets = createHermodrProviderTargets(catalog);
|
||||||
|
assert.equal(targets.length, 2);
|
||||||
|
assert.equal(targets[1].providerId, "fixture.game");
|
||||||
|
assert.equal(targets[1].targetId, "fixture.game::fixture.world");
|
||||||
|
assert.equal(targets[1].surfaces[0].surfaceId, "fixture.world");
|
||||||
|
assert.equal(targets[1].surfaces[0].surfaceKind, "interactive-world");
|
||||||
|
assert.match(targets[1].surfaces[0].url, /surfaceId=fixture\.world/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selects an interactive world without product identity", () => {
|
||||||
|
const targets = createHermodrProviderTargets(catalog);
|
||||||
|
assert.equal(selectInitialProviderTarget(targets).targetId, "fixture.game::fixture.world");
|
||||||
|
assert.equal(selectInitialProviderTarget(targets, "fixture.menu").targetId, "fixture.game::fixture.menu");
|
||||||
|
});
|
||||||
|
|
@ -5,6 +5,10 @@ import {
|
||||||
} from "../packages/eve-browser-lowering/dist/index.js";
|
} from "../packages/eve-browser-lowering/dist/index.js";
|
||||||
import { compileEveDsl } from "./eve-dsl.js";
|
import { compileEveDsl } from "./eve-dsl.js";
|
||||||
import { mergeProviderAdvertisement } from "./provider-advertisements.mjs";
|
import { mergeProviderAdvertisement } from "./provider-advertisements.mjs";
|
||||||
|
import {
|
||||||
|
createHermodrProviderTargets,
|
||||||
|
selectInitialProviderTarget,
|
||||||
|
} from "./hermodr-provider-catalog.mjs";
|
||||||
|
|
||||||
const statusEl = document.querySelector("#status");
|
const statusEl = document.querySelector("#status");
|
||||||
const app = document.querySelector("#app");
|
const app = document.querySelector("#app");
|
||||||
|
|
@ -19,7 +23,7 @@ let liveHermodr = false;
|
||||||
let openProviderGeneration = 0;
|
let openProviderGeneration = 0;
|
||||||
|
|
||||||
providerSelect.addEventListener("change", () => {
|
providerSelect.addEventListener("change", () => {
|
||||||
const provider = providers.find(candidate => candidate.providerId === providerSelect.value);
|
const provider = providers.find(candidate => (candidate.targetId || candidate.providerId) === providerSelect.value);
|
||||||
if (provider) void openProvider(provider);
|
if (provider) void openProvider(provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -34,18 +38,16 @@ async function bootProviders() {
|
||||||
}
|
}
|
||||||
providerSelect.replaceChildren(...providers.map(provider => {
|
providerSelect.replaceChildren(...providers.map(provider => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = provider.providerId;
|
option.value = provider.targetId || provider.providerId;
|
||||||
option.textContent = provider.title || provider.providerId;
|
option.textContent = provider.title || provider.providerId;
|
||||||
return option;
|
return option;
|
||||||
}));
|
}));
|
||||||
const requestedProviderId = new URLSearchParams(location.search).get("provider");
|
const params = new URLSearchParams(location.search);
|
||||||
const firstProduct = providers.find(provider => provider.providerId === requestedProviderId || provider.aliases?.includes(requestedProviderId))
|
const requestedProviderId = params.get("surface") || params.get("provider") || "";
|
||||||
|| providers.find(provider => provider.providerId === "aetheria")
|
const firstProduct = liveHermodr
|
||||||
|| providers.find(provider => provider.providerId === "aetheria.main_menu.root")
|
? selectInitialProviderTarget(providers, requestedProviderId)
|
||||||
|| providers.find(provider => provider.providerId === "aetheria.inventory.panel")
|
: providers.find(provider => provider.providerId === requestedProviderId || provider.aliases?.includes(requestedProviderId)) || providers[0];
|
||||||
|| providers.find(provider => provider.providerId === "repixelizer")
|
providerSelect.value = firstProduct.targetId || firstProduct.providerId;
|
||||||
|| providers[0];
|
|
||||||
providerSelect.value = firstProduct.providerId;
|
|
||||||
await openProvider(firstProduct);
|
await openProvider(firstProduct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,27 +62,7 @@ async function loadHermodrProviders() {
|
||||||
const response = await fetch("/hermodr/verse/catalog", { cache: "no-store" });
|
const response = await fetch("/hermodr/verse/catalog", { cache: "no-store" });
|
||||||
if (!response.ok) return [];
|
if (!response.ok) return [];
|
||||||
const catalog = await response.json();
|
const catalog = await response.json();
|
||||||
const advertised = new Map();
|
return createHermodrProviderTargets(catalog);
|
||||||
for (const provider of catalog.providers || []) {
|
|
||||||
advertised.set(provider.id || provider.providerId, provider);
|
|
||||||
}
|
|
||||||
return (catalog.surfaces || [])
|
|
||||||
.filter(surface => surface?.providerId && surface?.surface)
|
|
||||||
.map(surface => {
|
|
||||||
const provider = advertised.get(surface.providerId) || {};
|
|
||||||
return {
|
|
||||||
...provider,
|
|
||||||
providerId: surface.providerId,
|
|
||||||
title: surface.title || provider.title || surface.providerId,
|
|
||||||
kind: provider.kind || "eve.surface",
|
|
||||||
freshness: { state: "odin-visible-cultmesh" },
|
|
||||||
surfaces: [{
|
|
||||||
transport: "hermodr-surface",
|
|
||||||
surfaceId: surface.providerId,
|
|
||||||
url: `/hermodr/surface/${encodeURIComponent(surface.providerId)}`,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue