feat(p6): route-admin demo + route smoke; P6 verification green
Task 6.6: apps/route-admin (Vite, port 5176) — create bindings, simulate a message (preview-first, no send), see color-coded per-destination decisions with reason codes + preview, approve/deny REVIEW rows; built on the community-web SDK via CommunityProvider. scripts/smoke-route.mjs: 3 MANUAL bindings → 'party at 9 PM' → children(CHILD) DENY / adults+seniors REVIEW / 0 sends on simulate; approve → exactly one sandbox forward; AUTOMATIC safe binding auto-forwards via RouteProjector. P6 verification: 63 tests green, pnpm -r build all packages+demos, boundary passes and fails on community-web→service, route smoke PASS, prior smokes (realtime/inbox/support/adapter) PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>IIOS P6 — Route Admin</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "route-admin",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 5176",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@insignia/iios-community-web": "workspace:*",
|
||||||
|
"@insignia/iios-kernel-client": "workspace:*",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vite": "^6.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useBindings, useRoutePreview, useRouteDecisions } from '@insignia/iios-community-web';
|
||||||
|
import type { RouteDecision } from '@insignia/iios-kernel-client';
|
||||||
|
|
||||||
|
export const SERVICE = 'http://localhost:3200';
|
||||||
|
|
||||||
|
const STATE_COLORS: Record<string, string> = {
|
||||||
|
ALLOW: '#16a34a',
|
||||||
|
DENY: '#dc2626',
|
||||||
|
REVIEW: '#d97706',
|
||||||
|
SUPPRESS: '#6b7280',
|
||||||
|
SIMULATED: '#2563eb',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Inject a signed dev webhook and poll the raw-event log until it normalizes into an interaction. */
|
||||||
|
async function ingestTestMessage(token: string, text: string): Promise<string> {
|
||||||
|
const inject = await fetch(`${SERVICE}/v1/dev/webhook/WEBHOOK`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ text }),
|
||||||
|
});
|
||||||
|
if (!inject.ok) throw new Error(`inject ${inject.status}`);
|
||||||
|
const { rawEventId } = (await inject.json()) as { rawEventId: string };
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const r = await fetch(`${SERVICE}/v1/adapters/inbound`, { headers: { authorization: `Bearer ${token}` } });
|
||||||
|
const rows = (await r.json()) as Array<{ id: string; interactionId?: string | null }>;
|
||||||
|
const hit = rows.find((x) => x.id === rawEventId);
|
||||||
|
if (hit?.interactionId) return hit.interactionId;
|
||||||
|
await new Promise((res) => setTimeout(res, 150));
|
||||||
|
}
|
||||||
|
throw new Error('interaction never normalized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell: React.CSSProperties = { padding: '6px 10px', borderBottom: '1px solid #eee', verticalAlign: 'top' };
|
||||||
|
const box: React.CSSProperties = { border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 16 };
|
||||||
|
|
||||||
|
export function App({ token }: { token: string }) {
|
||||||
|
const { bindings, createBinding } = useBindings();
|
||||||
|
const preview = useRoutePreview();
|
||||||
|
const { decisions, approve, deny } = useRouteDecisions({ pollMs: 2000 });
|
||||||
|
|
||||||
|
const [origin, setOrigin] = useState('WEBHOOK');
|
||||||
|
const [dest, setDest] = useState('PORTAL');
|
||||||
|
const [destRef, setDestRef] = useState('parents-group');
|
||||||
|
const [restriction, setRestriction] = useState('');
|
||||||
|
const [mode, setMode] = useState('MANUAL');
|
||||||
|
const [text, setText] = useState('There is a party at 9 PM tonight!');
|
||||||
|
const [simRows, setSimRows] = useState<RouteDecision[]>([]);
|
||||||
|
const [busy, setBusy] = useState('');
|
||||||
|
|
||||||
|
const onCreate = async () => {
|
||||||
|
await createBinding({
|
||||||
|
originChannelType: origin,
|
||||||
|
destinationChannelType: dest,
|
||||||
|
destinationRef: destRef || undefined,
|
||||||
|
restrictionProfile: restriction || undefined,
|
||||||
|
mode,
|
||||||
|
requiresReview: true,
|
||||||
|
enabled: true,
|
||||||
|
} as Parameters<typeof createBinding>[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSimulate = async () => {
|
||||||
|
setBusy('ingesting…');
|
||||||
|
try {
|
||||||
|
const interactionId = await ingestTestMessage(token, text);
|
||||||
|
setBusy('simulating…');
|
||||||
|
setSimRows(await preview(interactionId, origin));
|
||||||
|
} catch (e) {
|
||||||
|
setBusy(`error: ${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24, fontFamily: 'sans-serif', maxWidth: 900, margin: '0 auto' }}>
|
||||||
|
<h2>IIOS P6 — Route Admin</h2>
|
||||||
|
<p style={{ color: '#666' }}>Preview-first. Simulation never sends. Restricted destinations are deny-by-default.</p>
|
||||||
|
|
||||||
|
<div style={box}>
|
||||||
|
<h3>1 · Create binding</h3>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
||||||
|
<label>origin <input value={origin} onChange={(e) => setOrigin(e.target.value)} style={{ width: 90 }} /></label>
|
||||||
|
<span>→</span>
|
||||||
|
<label>dest <input value={dest} onChange={(e) => setDest(e.target.value)} style={{ width: 90 }} /></label>
|
||||||
|
<label>destRef <input value={destRef} onChange={(e) => setDestRef(e.target.value)} style={{ width: 130 }} /></label>
|
||||||
|
<label>
|
||||||
|
restriction{' '}
|
||||||
|
<select value={restriction} onChange={(e) => setRestriction(e.target.value)}>
|
||||||
|
<option value="">(none)</option>
|
||||||
|
<option value="CHILD">CHILD</option>
|
||||||
|
<option value="ADULT">ADULT</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
mode{' '}
|
||||||
|
<select value={mode} onChange={(e) => setMode(e.target.value)}>
|
||||||
|
<option>MANUAL</option>
|
||||||
|
<option>AUTOMATIC</option>
|
||||||
|
<option>HYBRID</option>
|
||||||
|
<option>SIMULATION_ONLY</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button onClick={() => void onCreate()}>+ Add</button>
|
||||||
|
</div>
|
||||||
|
<table style={{ borderCollapse: 'collapse', width: '100%', marginTop: 12 }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ textAlign: 'left', color: '#666' }}>
|
||||||
|
<th style={cell}>origin</th><th style={cell}>destination</th><th style={cell}>restriction</th><th style={cell}>mode</th><th style={cell}>enabled</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{bindings.map((b) => (
|
||||||
|
<tr key={b.id}>
|
||||||
|
<td style={cell}>{b.originChannelType}{b.originRef ? `:${b.originRef}` : ''}</td>
|
||||||
|
<td style={cell}>{b.destinationChannelType}{b.destinationRef ? `:${b.destinationRef}` : ''}</td>
|
||||||
|
<td style={cell}>{b.restrictionProfile ?? '—'}</td>
|
||||||
|
<td style={cell}>{b.mode}</td>
|
||||||
|
<td style={cell}>{b.enabled ? '✓' : '—'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={box}>
|
||||||
|
<h3>2 · Simulate a message (no send)</h3>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<input value={text} onChange={(e) => setText(e.target.value)} style={{ flex: 1 }} />
|
||||||
|
<button onClick={() => void onSimulate()}>Simulate on {origin}</button>
|
||||||
|
</div>
|
||||||
|
{busy && <p style={{ color: '#666' }}>{busy}</p>}
|
||||||
|
{simRows.length > 0 && (
|
||||||
|
<table style={{ borderCollapse: 'collapse', width: '100%', marginTop: 12 }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ textAlign: 'left', color: '#666' }}>
|
||||||
|
<th style={cell}>destination</th><th style={cell}>decision</th><th style={cell}>reason codes</th><th style={cell}>preview</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{simRows.map((d) => <DecisionRow key={d.id} d={d} />)}</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={box}>
|
||||||
|
<h3>3 · Pending decisions (approve / deny)</h3>
|
||||||
|
{decisions.length === 0 && <p style={{ color: '#666' }}>No decisions yet — simulate above.</p>}
|
||||||
|
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
|
||||||
|
<tbody>
|
||||||
|
{decisions.map((d) => (
|
||||||
|
<tr key={d.id}>
|
||||||
|
<DecisionCells d={d} />
|
||||||
|
<td style={cell}>
|
||||||
|
{d.decisionState === 'REVIEW' ? (
|
||||||
|
<>
|
||||||
|
<button onClick={() => void approve(d.id)} style={{ marginRight: 6 }}>Approve</button>
|
||||||
|
<button onClick={() => void deny(d.id)}>Deny</button>
|
||||||
|
</>
|
||||||
|
) : d.executed ? '↪ forwarded (sandbox)' : '—'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function payloadOf(d: RouteDecision): { text?: string; destinationRef?: string | null } {
|
||||||
|
return (d.previewPayload ?? {}) as { text?: string; destinationRef?: string | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function DecisionRow({ d }: { d: RouteDecision }) {
|
||||||
|
const p = payloadOf(d);
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td style={cell}>{d.routeBinding ? `${d.routeBinding.destinationChannelType}${d.routeBinding.destinationRef ? `:${d.routeBinding.destinationRef}` : ''}` : (p.destinationRef ?? '—')}</td>
|
||||||
|
<td style={{ ...cell, color: STATE_COLORS[d.decisionState] ?? '#000', fontWeight: 600 }}>{d.decisionState}</td>
|
||||||
|
<td style={cell}>{d.reasonCodes.join(', ') || '—'}</td>
|
||||||
|
<td style={{ ...cell, color: '#555' }}>{p.text ?? '—'}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DecisionCells({ d }: { d: RouteDecision }) {
|
||||||
|
const p = payloadOf(d);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<td style={cell}>{d.routeBinding ? `${d.routeBinding.destinationChannelType}${d.routeBinding.destinationRef ? `:${d.routeBinding.destinationRef}` : ''}` : '—'}</td>
|
||||||
|
<td style={{ ...cell, color: STATE_COLORS[d.decisionState] ?? '#000', fontWeight: 600 }}>{d.decisionState}</td>
|
||||||
|
<td style={cell}>{d.reasonCodes.join(', ') || '—'}</td>
|
||||||
|
<td style={{ ...cell, color: '#555' }}>{p.text ?? '—'}</td>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { CommunityProvider } from '@insignia/iios-community-web';
|
||||||
|
import { App, SERVICE } from './App';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const APP_ID = 'portal-demo';
|
||||||
|
|
||||||
|
async function devToken(userId: string): Promise<string> {
|
||||||
|
const r = await fetch(`${SERVICE}/v1/dev/token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ appId: APP_ID, userId, name: userId }),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`devToken ${r.status} (service running with IIOS_DEV_TOKENS=1?)`);
|
||||||
|
return ((await r.json()) as { token: string }).token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Root() {
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
void devToken('route-admin').then(setToken);
|
||||||
|
}, []);
|
||||||
|
if (!token) return <div style={{ padding: 16, fontFamily: 'sans-serif' }}>loading…</div>;
|
||||||
|
return (
|
||||||
|
<CommunityProvider serviceUrl={SERVICE} token={token}>
|
||||||
|
<App token={token} />
|
||||||
|
</CommunityProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.getElementById('root');
|
||||||
|
if (el) createRoot(el).render(<Root />);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["react", "react-dom"],
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: { port: 5176 },
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// P6 route smoke: preview-first, deny-by-default, approve→sandbox, AUTOMATIC auto-forward.
|
||||||
|
// Requires service running (relay timer on) with IIOS_DEV_TOKENS not needed here —
|
||||||
|
// we mint our own session token + sign webhooks directly. No real network is contacted.
|
||||||
|
import 'dotenv/config';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200';
|
||||||
|
const APP_ID = 'portal-demo';
|
||||||
|
const APP_SECRET = JSON.parse(process.env.APP_SECRETS ?? '{"portal-demo":"dev-secret"}')[APP_ID];
|
||||||
|
const ADAPTER_SECRET = (() => {
|
||||||
|
try { return JSON.parse(process.env.ADAPTER_SECRETS ?? '{}').WEBHOOK ?? 'dev-adapter-secret'; } catch { return 'dev-adapter-secret'; }
|
||||||
|
})();
|
||||||
|
|
||||||
|
const token = jwt.sign({ sub: 'route-admin', appId: APP_ID, orgId: `org_${APP_ID}` }, APP_SECRET, { algorithm: 'HS256', expiresIn: '1h' });
|
||||||
|
const sign = (body) => 'sha256=' + crypto.createHmac('sha256', ADAPTER_SECRET).update(body).digest('hex');
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
const assert = (c, m) => { if (!c) { console.error('✗', m); process.exit(1); } console.log('✓', m); };
|
||||||
|
|
||||||
|
async function req(path, method = 'GET', body) {
|
||||||
|
const r = await fetch(`${SERVICE}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`${method} ${path} ${r.status}: ${await r.text()}`);
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
async function pollUntil(fn, ms = 8000) {
|
||||||
|
const end = Date.now() + ms;
|
||||||
|
while (Date.now() < end) { const v = await fn(); if (v) return v; await sleep(300); }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const outboundCount = async () => (await req('/v1/adapters/outbound')).length;
|
||||||
|
|
||||||
|
async function ingest(text) {
|
||||||
|
const payload = { eventId: `route-sm-${crypto.randomUUID()}`, from: 'sim@parent', text, threadRef: `sm-${Date.now()}` };
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
const wh = await fetch(`${SERVICE}/v1/adapters/WEBHOOK/webhook`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json', 'x-iios-signature': sign(body) },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (wh.status !== 202) throw new Error(`webhook ${wh.status}`);
|
||||||
|
const e = await pollUntil(async () => {
|
||||||
|
const list = await req('/v1/adapters/inbound');
|
||||||
|
const hit = list.find((x) => x.externalEventId === payload.eventId);
|
||||||
|
return hit && hit.interactionId ? hit : null;
|
||||||
|
});
|
||||||
|
if (!e) throw new Error('interaction never normalized');
|
||||||
|
return e.interactionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Phase A: three MANUAL bindings, preview-first ──────────────────────────
|
||||||
|
const mk = (destinationRef, restrictionProfile) => req('/v1/routes/bindings', 'POST', {
|
||||||
|
originChannelType: 'WEBHOOK', destinationChannelType: 'PORTAL', destinationRef,
|
||||||
|
restrictionProfile, mode: 'MANUAL', requiresReview: true, enabled: true,
|
||||||
|
});
|
||||||
|
const adult = await mk('adults', undefined);
|
||||||
|
const seniors = await mk('seniors', undefined);
|
||||||
|
const children = await mk('children', 'CHILD');
|
||||||
|
assert(adult.id && seniors.id && children.id, 'created 3 bindings (adults / seniors / children[CHILD])');
|
||||||
|
|
||||||
|
const base = await outboundCount();
|
||||||
|
const interactionId = await ingest('There is a party at 9 PM tonight!');
|
||||||
|
const { decisions } = await req('/v1/routes/simulate', 'POST', { interactionId, originChannelType: 'WEBHOOK' });
|
||||||
|
const byBinding = Object.fromEntries(decisions.map((d) => [d.routeBindingId, d]));
|
||||||
|
assert(byBinding[children.id]?.decisionState === 'DENY', 'children (restricted) → DENY (deny-by-default)');
|
||||||
|
assert(byBinding[adult.id]?.decisionState === 'REVIEW', 'adults → REVIEW');
|
||||||
|
assert(byBinding[seniors.id]?.decisionState === 'REVIEW', 'seniors → REVIEW');
|
||||||
|
assert((await outboundCount()) === base, 'simulate sent nothing (0 new outbound commands)');
|
||||||
|
|
||||||
|
// ── Phase B: approve a REVIEW decision → forwards to sandbox ────────────────
|
||||||
|
const approved = await req(`/v1/routes/decisions/${byBinding[seniors.id].id}/approve`, 'POST');
|
||||||
|
assert(approved.decisionState === 'ALLOW' && approved.executed, 'approved seniors → ALLOW + executed');
|
||||||
|
assert((await outboundCount()) === base + 1, 'approval produced exactly one sandbox forward');
|
||||||
|
|
||||||
|
// ── Phase C: AUTOMATIC safe binding → auto-forward on ingest ────────────────
|
||||||
|
// originRef 'webhook' = the WEBHOOK adapter's channel id, so the RouteProjector
|
||||||
|
// (which matches on the interaction's channel ref) picks this binding up.
|
||||||
|
await req('/v1/routes/bindings', 'POST', {
|
||||||
|
originChannelType: 'WEBHOOK', originRef: 'webhook', destinationChannelType: 'PORTAL', destinationRef: 'staff',
|
||||||
|
mode: 'AUTOMATIC', requiresReview: false, enabled: true,
|
||||||
|
});
|
||||||
|
const base2 = await outboundCount();
|
||||||
|
await ingest('Team standup notes are posted for everyone.');
|
||||||
|
const grew = await pollUntil(async () => (await outboundCount()) > base2, 8000);
|
||||||
|
assert(grew, 'AUTOMATIC safe binding auto-forwarded (sandbox) via RouteProjector');
|
||||||
|
|
||||||
|
console.log('\nP6 route smoke: PASS');
|
||||||
|
process.exit(0);
|
||||||
Generated
+31
@@ -114,6 +114,37 @@ importers:
|
|||||||
specifier: ^6.0.7
|
specifier: ^6.0.7
|
||||||
version: 6.4.3(@types/node@26.0.1)(jiti@2.7.0)(terser@5.48.0)
|
version: 6.4.3(@types/node@26.0.1)(jiti@2.7.0)(terser@5.48.0)
|
||||||
|
|
||||||
|
apps/route-admin:
|
||||||
|
dependencies:
|
||||||
|
'@insignia/iios-community-web':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/iios-community-web
|
||||||
|
'@insignia/iios-kernel-client':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/iios-kernel-client
|
||||||
|
react:
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.7
|
||||||
|
react-dom:
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.7(react@19.2.7)
|
||||||
|
devDependencies:
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.17
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.3(@types/react@19.2.17)
|
||||||
|
'@vitejs/plugin-react':
|
||||||
|
specifier: ^4.3.4
|
||||||
|
version: 4.7.0(vite@6.4.3(@types/node@26.0.1)(jiti@2.7.0)(terser@5.48.0))
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.3
|
||||||
|
version: 5.9.3
|
||||||
|
vite:
|
||||||
|
specifier: ^6.0.7
|
||||||
|
version: 6.4.3(@types/node@26.0.1)(jiti@2.7.0)(terser@5.48.0)
|
||||||
|
|
||||||
packages/iios-adapter-sdk:
|
packages/iios-adapter-sdk:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@insignia/iios-contracts':
|
'@insignia/iios-contracts':
|
||||||
|
|||||||
Reference in New Issue
Block a user