Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 83 additions & 11 deletions 03-frontend/app/app/dev/api-lab/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// License: Apache-2.0

import { useState } from 'react';
import { ARCHITOKEN_API_BASE_URL } from '@/lib/backend-api';
import { ARCHITOKEN_API_BASE_URL, setBackendRequestContext } from '@/lib/backend-api';
import { artifactClient, type Artifact } from '@/lib/artifact-client';
import { generationClient, type GenerationJob } from '@/lib/generation-client';
import {
Expand All @@ -20,12 +20,16 @@ type RunState = 'idle' | 'loading' | 'error';

function describeError(error: unknown): string {
if (typeof error === 'object' && error !== null && 'error' in error) {
return String((error as { error: unknown }).error);
return JSON.stringify(error, null, 2);
}
return error instanceof Error ? error.message : String(error);
}

export default function ApiLabPage() {
const [tenantId, setTenantId] = useState('dev-tenant');
const [projectId, setProjectId] = useState('dev-project');
const [actor, setActor] = useState('frontend-api-lab');
const [rolesText, setRolesText] = useState('admin');
const [capabilities, setCapabilities] = useState<RuntimeCapabilities | null>(null);
const [job, setJob] = useState<GenerationJob | null>(null);
const [artifacts, setArtifacts] = useState<Artifact[]>([]);
Expand All @@ -38,7 +42,22 @@ export default function ApiLabPage() {
setLog((current) => [`${new Date().toISOString()} ${message}`, ...current].slice(0, 12));
};

const applyContext = () => {
setBackendRequestContext({
tenantId,
projectId,
actor,
roles: rolesText
.split(',')
.map((role) => role.trim())
.filter(Boolean),
requestId: `api-lab-${actor}`,
correlationId: `api-lab-${tenantId}-${projectId}`,
});
};

const loadCapabilities = async () => {
applyContext();
setRunState('loading');
setErrorMessage(null);
try {
Expand All @@ -55,38 +74,39 @@ export default function ApiLabPage() {
};

const runGenerationSequence = async () => {
applyContext();
setRunState('loading');
setErrorMessage(null);
try {
const created = await generationClient.create({
moduleId: 'digital_twin',
mode: 'model_to_lightweight_scene',
prompt: 'Create a local preview lightweight scene with property index and identity map.',
actor: 'frontend-api-lab',
actor,
});
appendLog(`job created: ${created.id}`);

const planned = await generationClient.plan(created.id, {
actor: 'frontend-api-lab',
actor,
comment: 'plan from API lab',
});
appendLog(`job planned: ${planned.status}`);

const run = await generationClient.run(planned.id, {
actor: 'frontend-api-lab',
actor,
comment: 'run local mock generator',
});
appendLog(`job run: ${run.status}`);

const reviewed = await generationClient.review(run.id, {
reviewer: 'frontend-api-lab',
reviewer: actor,
decision: 'approved',
comment: 'review accepted in API lab',
});
appendLog(`job reviewed: ${reviewed.status}`);

const approved = await generationClient.approve(reviewed.id, {
actor: 'frontend-api-lab',
actor,
comment: 'approve generated preview artifacts',
});
setJob(approved);
Expand All @@ -105,6 +125,7 @@ export default function ApiLabPage() {
};

const submitViewerCommand = async () => {
applyContext();
const artifact = artifacts[0];
if (!artifact) {
appendLog('viewer command skipped: run generation first');
Expand All @@ -120,12 +141,12 @@ export default function ApiLabPage() {
artifactId: artifact.id,
elementIds: ['architoken:demo:001'],
arguments: { color: '#ff6600' },
actor: 'frontend-api-lab',
actor,
});
appendLog(`viewer command created: ${created.id}`);

const acked = await viewerCommandClient.ack(created.id, {
actor: 'frontend-api-lab',
actor,
status: 'executed',
comment: 'dev console executed command contract',
result: { rendered: false, backendContractOnly: true },
Expand All @@ -146,7 +167,7 @@ export default function ApiLabPage() {
<section className="mx-auto flex max-w-6xl flex-col gap-6">
<header className="arch-surface rounded-[var(--arch-radius)] border p-6">
<p className="arch-muted text-sm uppercase tracking-[0.24em]">
Phase 5 Runtime Persistence E2E
Phase 6 Durable Store + RBAC
</p>
<h1 className="mt-2 text-3xl font-semibold">Backend API Lab</h1>
<p className="arch-muted mt-3 max-w-3xl">
Expand All @@ -156,6 +177,48 @@ export default function ApiLabPage() {
<p className="mt-3 font-mono text-sm">API base: {ARCHITOKEN_API_BASE_URL}</p>
</header>

<section className="arch-surface rounded-[var(--arch-radius)] border p-5">
<h2 className="text-xl font-semibold">Request Context</h2>
<div className="mt-4 grid gap-3 md:grid-cols-4">
<label className="text-sm">
<span className="arch-muted block">Tenant</span>
<input
className="mt-1 w-full rounded-xl border px-3 py-2 text-black"
value={tenantId}
onChange={(event) => setTenantId(event.target.value)}
/>
</label>
<label className="text-sm">
<span className="arch-muted block">Project</span>
<input
className="mt-1 w-full rounded-xl border px-3 py-2 text-black"
value={projectId}
onChange={(event) => setProjectId(event.target.value)}
/>
</label>
<label className="text-sm">
<span className="arch-muted block">Actor</span>
<input
className="mt-1 w-full rounded-xl border px-3 py-2 text-black"
value={actor}
onChange={(event) => setActor(event.target.value)}
/>
</label>
<label className="text-sm">
<span className="arch-muted block">Roles</span>
<input
className="mt-1 w-full rounded-xl border px-3 py-2 text-black"
value={rolesText}
onChange={(event) => setRolesText(event.target.value)}
/>
</label>
</div>
<p className="arch-muted mt-3 text-sm">
Requests send X-Tenant-Id, X-Project-Id, X-Actor, X-Roles, X-Request-Id, and
X-Correlation-Id headers through the shared backend-api client.
</p>
</section>

<div className="grid gap-4 md:grid-cols-3">
<button className="arch-btn rounded-2xl px-5 py-4 text-left" onClick={loadCapabilities}>
Load runtime capabilities
Expand All @@ -174,7 +237,9 @@ export default function ApiLabPage() {
{runState === 'error' ? (
<div className="rounded-2xl border border-red-200 bg-red-50 p-4 text-red-700">
<p className="font-semibold">Last API call failed.</p>
<p className="mt-1 text-sm">{errorMessage ?? 'Check the log and backend process.'}</p>
<pre className="mt-2 whitespace-pre-wrap text-sm">
{errorMessage ?? 'Check the log and backend process.'}
</pre>
</div>
) : null}

Expand All @@ -184,6 +249,9 @@ export default function ApiLabPage() {
<p className="arch-muted mt-2 text-sm">
Capability status: {capabilities ? 'loaded' : runState === 'loading' ? 'loading' : 'not loaded'}
</p>
<p className="arch-muted mt-2 text-sm">
Context: {tenantId}/{projectId} as {actor} [{rolesText}]
</p>
<p className="arch-muted mt-2 text-sm">
Modules: {capabilities?.activeModuleIds.join(', ') ?? 'not loaded'}
</p>
Expand All @@ -192,6 +260,10 @@ export default function ApiLabPage() {
{capabilities?.generation.artifactKinds.length ?? 0}; storage:{' '}
{capabilities?.storage.providers.join(', ') ?? 'unknown'}
</p>
<p className="arch-muted mt-2 text-sm">
Store boundary:{' '}
{capabilities?.storeCapabilities.inMemoryOnly ? 'in-memory deterministic' : 'unknown'}
</p>
</article>

<article className="arch-surface rounded-[var(--arch-radius)] border p-5">
Expand Down
6 changes: 6 additions & 0 deletions 03-frontend/lib/artifact-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ export interface ArtifactMetadata {
mimeType: string;
sizeBytes: number;
owner: string;
tenantId: string;
projectId: string;
version: number;
requestId: string;
correlationId: string;
sourceJobId: string | null;
createdByJobId: string | null;
approvalStatus: string;
auditEventId: string | null;
createdAt: string;
updatedAt: string;
}

export interface ArtifactVersion {
Expand Down
50 changes: 45 additions & 5 deletions 03-frontend/lib/backend-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,38 @@ export interface BackendApiError {
code: number;
}

export interface RuntimeRequestContext {
tenantId: string;
projectId: string;
actor: string;
roles: string[];
requestId?: string;
correlationId?: string;
}

export const ARCHITOKEN_API_BASE_URL =
process.env.NEXT_PUBLIC_ARCHITOKEN_API_BASE_URL ?? 'http://localhost:8080';

let activeRequestContext: RuntimeRequestContext = {
tenantId: 'dev-tenant',
projectId: 'dev-project',
actor: 'frontend-api-lab',
roles: ['admin'],
requestId: 'frontend-api-lab',
correlationId: 'frontend-api-lab',
};

export function setBackendRequestContext(context: RuntimeRequestContext): void {
activeRequestContext = {
...context,
roles: context.roles.length > 0 ? context.roles : ['admin'],
};
}

export function getBackendRequestContext(): RuntimeRequestContext {
return activeRequestContext;
}

export function buildQuery(
params: Record<string, string | number | boolean | null | undefined>,
): string {
Expand All @@ -26,13 +55,24 @@ export async function backendRequest<T>(
path: string,
init: RequestInit = {},
): Promise<T> {
const headers = new Headers(init.headers);
headers.set('Accept', 'application/json');
if (init.body) {
headers.set('Content-Type', 'application/json');
}
headers.set('X-Tenant-Id', activeRequestContext.tenantId);
headers.set('X-Project-Id', activeRequestContext.projectId);
headers.set('X-Actor', activeRequestContext.actor);
headers.set('X-Roles', activeRequestContext.roles.join(','));
headers.set('X-Request-Id', activeRequestContext.requestId ?? activeRequestContext.actor);
headers.set(
'X-Correlation-Id',
activeRequestContext.correlationId ?? activeRequestContext.requestId ?? activeRequestContext.actor,
);

const response = await fetch(`${ARCHITOKEN_API_BASE_URL}${path}`, {
...init,
headers: {
Accept: 'application/json',
...(init.body ? { 'Content-Type': 'application/json' } : {}),
...(init.headers ?? {}),
},
headers,
});

if (!response.ok) {
Expand Down
4 changes: 4 additions & 0 deletions 03-frontend/lib/generation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { backendRequest, buildQuery } from './backend-api';
import type { Artifact } from './artifact-client';
import type { RuntimeRequestContext } from './backend-api';

export type GenerationReviewDecision = 'approved' | 'rejected' | 'needs_changes';

Expand Down Expand Up @@ -33,6 +34,9 @@ export interface GenerationJob {
status: string;
artifacts: Artifact[];
lifecycleTransactionId: string | null;
actor: string;
context: RuntimeRequestContext;
version: number;
createdAt: string;
updatedAt: string;
}
Expand Down
13 changes: 13 additions & 0 deletions 03-frontend/lib/runtime-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,25 @@ export interface RuntimeStorageCapabilities {
productionReady: boolean;
}

export interface RuntimeStoreCapabilities {
objectStore: boolean;
transactionStore: boolean;
eventStore: boolean;
registryStore: boolean;
artifactStore: boolean;
viewerCommandStore: boolean;
knowledgeSourceStore: boolean;
inMemoryOnly: boolean;
deterministicPagination: boolean;
}

export interface RuntimeCapabilities {
activeModuleIds: string[];
generation: RuntimeGenerationCapabilities;
viewer: RuntimeViewerCapabilities;
registry: RuntimeRegistryCapabilities;
storage: RuntimeStorageCapabilities;
storeCapabilities: RuntimeStoreCapabilities;
localImplementationMode: 'in_memory_preview';
productionCaveats: string[];
}
Expand Down
3 changes: 3 additions & 0 deletions 03-frontend/lib/viewer-command-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// License: Apache-2.0

import { backendRequest, buildQuery } from './backend-api';
import type { RuntimeRequestContext } from './backend-api';

export type ViewerCommandStatus = 'queued' | 'executed' | 'skipped';

Expand All @@ -13,6 +14,8 @@ export interface ViewerAdapterCommand {
elementIds: string[];
arguments: Record<string, unknown>;
status: ViewerCommandStatus;
context: RuntimeRequestContext;
version: number;
auditEventId: string | null;
acknowledgedBy: string | null;
acknowledgedAt: string | null;
Expand Down
Loading
Loading