Skip to content

Commit ad06b52

Browse files
committed
feat: add @link cross-resource associations and Explorer fallback\n\n- Add systemKind@link to 12 system creation payloads in ingest-odas-data-model.py\n (mic array, 7 mics, SSL, SST, config actuator, triangulation engine)\n- Add platform@link and deployedSystems@link to all 5 deployment payloads\n- Add tryLinkFallback() to ResourceDetail.vue (~105 lines) that resolves\n related resources from @link fields when navigation endpoints return 400\n- Fallback paths: system→procedures via systemKind@link, system→deployments\n via platform@link search, deployment→systems via deployedSystems@link,\n procedure→systems via reverse systemKind@link search\n- Add fix-associations.py one-time script for existing server resources\n- Update ingestion-report.md with sections 11-12 documenting @link fields,\n OSH server behavior, and Explorer fallback strategy\n\nOSH quirks documented:\n- systemKind@link and platform@link persist correctly\n- deployedSystems@link silently dropped by OSH\n- /systems/{id}/procedures and /deployments endpoints return 400\n- /deployments?system={id} causes 500 server crash"
1 parent cbbece2 commit ad06b52

File tree

4 files changed

+643
-11
lines changed

4 files changed

+643
-11
lines changed

demo/src/components/ResourceDetail.vue

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,108 @@ function clearFilters(link: RelatedResourceLink) {
159159
if (parentId) fetchRelation(link, String(parentId))
160160
}
161161
162+
/**
163+
* @link fallback: When the server doesn't implement a nested navigation endpoint
164+
* (e.g. /systems/{id}/procedures returns 400), try to resolve related resources
165+
* from @link fields embedded in the parent or by searching the collection.
166+
*
167+
* Supported fallbacks:
168+
* - systems → procedures: follow systemKind@link href to fetch the procedure
169+
* - systems → deployments: search /deployments and filter by platform@link.href
170+
* - deployments → systems: follow deployedSystems@link hrefs to fetch each system
171+
*/
172+
async function tryLinkFallback(link: RelatedResourceLink, parentId: string): Promise<any[]> {
173+
const parentProps = detail.value?.properties || detail.value || {}
174+
175+
// ----- System → Procedure via systemKind@link -----
176+
if (props.resourceType === 'systems' && link.relation === 'procedures') {
177+
const skLink = parentProps['systemKind@link']
178+
if (skLink?.href) {
179+
try {
180+
const acceptType = getContentType('procedures')
181+
// href may be absolute URL — strip base URL to get relative path
182+
let path = skLink.href
183+
if (path.startsWith('http')) {
184+
const idx = path.indexOf('/api/')
185+
if (idx !== -1) path = path.substring(idx + 4) // strip everything before /api/
186+
else path = new URL(path).pathname.replace(/^.*\/sensorhub/, '') // fallback
187+
}
188+
const res = await apiFetch(path, { headers: { 'Accept': acceptType } })
189+
if (res.ok && res.data) {
190+
return [res.data]
191+
}
192+
} catch { /* fallback failed silently */ }
193+
}
194+
}
195+
196+
// ----- System → Deployments via searching deployments for platform@link -----
197+
if (props.resourceType === 'systems' && link.relation === 'deployments') {
198+
try {
199+
const acceptType = getContentType('deployments')
200+
const res = await apiFetch('/deployments?limit=100', { headers: { 'Accept': acceptType } })
201+
if (res.ok && res.data) {
202+
const parsed = parseCollectionResponse(res.data)
203+
const systemUrl = `systems/${parentId}`
204+
const matched = (parsed.items as any[]).filter((dep: any) => {
205+
const props = dep?.properties || dep || {}
206+
// Check platform@link.href
207+
const platformLink = props['platform@link']
208+
if (platformLink?.href && platformLink.href.includes(systemUrl)) return true
209+
// Check deployedSystems@link array
210+
const dsLinks = props['deployedSystems@link']
211+
if (Array.isArray(dsLinks)) {
212+
return dsLinks.some((l: any) => l?.href && l.href.includes(systemUrl))
213+
}
214+
return false
215+
})
216+
if (matched.length > 0) return matched
217+
}
218+
} catch { /* fallback failed silently */ }
219+
}
220+
221+
// ----- Deployment → Systems via deployedSystems@link -----
222+
if (props.resourceType === 'deployments' && link.relation === 'systems') {
223+
const dsLinks = parentProps['deployedSystems@link']
224+
if (Array.isArray(dsLinks) && dsLinks.length > 0) {
225+
const items: any[] = []
226+
for (const l of dsLinks) {
227+
if (!l?.href) continue
228+
try {
229+
let path = l.href
230+
if (path.startsWith('http')) {
231+
const idx = path.indexOf('/api/')
232+
if (idx !== -1) path = path.substring(idx + 4)
233+
}
234+
const acceptType = getContentType('systems')
235+
const res = await apiFetch(path, { headers: { 'Accept': acceptType } })
236+
if (res.ok && res.data) items.push(res.data)
237+
} catch { /* skip failed links */ }
238+
}
239+
if (items.length > 0) return items
240+
}
241+
}
242+
243+
// ----- Procedure → Systems via systemKind@link reverse search -----
244+
if (props.resourceType === 'procedures' && link.relation === 'systems') {
245+
try {
246+
const acceptType = getContentType('systems')
247+
const res = await apiFetch('/systems?limit=100', { headers: { 'Accept': acceptType } })
248+
if (res.ok && res.data) {
249+
const parsed = parseCollectionResponse(res.data)
250+
const procUrl = `procedures/${parentId}`
251+
const matched = (parsed.items as any[]).filter((sys: any) => {
252+
const props = sys?.properties || sys || {}
253+
const skLink = props['systemKind@link']
254+
return skLink?.href && skLink.href.includes(procUrl)
255+
})
256+
if (matched.length > 0) return matched
257+
}
258+
} catch { /* fallback failed silently */ }
259+
}
260+
261+
return []
262+
}
263+
162264
/** Fetch a single related resource collection (with filters and client-side fallback) */
163265
async function fetchRelation(link: RelatedResourceLink, parentId: string) {
164266
const state = getRelState(link.relation)
@@ -185,6 +287,19 @@ async function fetchRelation(link: RelatedResourceLink, parentId: string) {
185287
if (res.status !== 404 && res.status !== 400) {
186288
state.error = res.error || 'Failed to fetch'
187289
}
290+
// --- @link fallback: when server returns 400 (endpoint not implemented),
291+
// attempt to populate the panel from @link fields in the parent resource.
292+
// This handles OSH SensorHub which doesn't implement /systems/{id}/procedures
293+
// or /systems/{id}/deployments navigation endpoints. ---
294+
if (res.status === 400 && detail.value) {
295+
const fallbackItems = await tryLinkFallback(link, parentId)
296+
if (fallbackItems.length > 0) {
297+
state.items = fallbackItems
298+
state.clientSideFallbackDetails.push(
299+
`Server returned 400 for /${link.relation} endpoint — resolved ${fallbackItems.length} item(s) via @link fields`
300+
)
301+
}
302+
}
188303
return
189304
}
190305

0 commit comments

Comments
 (0)