Add prototype storage diagnostics and smoke test

This commit is contained in:
root 2026-03-14 06:06:35 +00:00
parent 114240c79a
commit a27a822fc2
9 changed files with 631 additions and 43 deletions

View File

@ -43,6 +43,7 @@ cp .env.example .env
See [docs/PROTOTYPE_DEPLOYMENT.md](docs/PROTOTYPE_DEPLOYMENT.md) for the required environment variables and day-to-day commands.
The prototype stack now includes a local Cosmos DB Emulator container, so the default `.env.example` values are wired for single-VM Docker use.
Blob uploads are backed by local Azurite, and the platform exposes prototype diagnostics at `/api/health/dependencies`, `/api/self-test`, and `/api/self-test.json`.
## Current Capability Surface

View File

@ -2,9 +2,11 @@ services:
# ── Azurite Blob Storage (prototype only) ─────────────────────
azurite:
image: mcr.microsoft.com/azure-storage/azurite:3.35.0
command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck
command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --location /data --skipApiVersionCheck
ports:
- '10000:10000'
volumes:
- azurite-data:/data
healthcheck:
test:
[
@ -115,6 +117,9 @@ services:
- PORT=4003
# Local/dev convenience: ensure Cosmos DB + containers exist.
- COSMOS_AUTO_INIT=true
- PLATFORM_SERVICE_URL=http://platform-service:4003
- EXTRACTION_SERVICE_URL=http://extraction-service:4005
- MCP_SERVER_URL=http://mcp-server:4007
depends_on:
azurite:
condition: service_healthy
@ -196,5 +201,6 @@ services:
# ── Volumes ───────────────────────────────────────────────────────
volumes:
azurite-data:
loki-data:
grafana-data:

View File

@ -47,7 +47,8 @@ That script will:
1. Validate the required environment variables.
2. Start the local Cosmos DB emulator.
3. Build and start the rest of the Compose stack.
3. Start Azurite with a persistent Docker volume for blob data.
4. Build and start the rest of the Compose stack.
## Day-To-Day Commands
@ -58,15 +59,20 @@ docker compose logs -f extraction-service
docker compose logs -f mcp-server
docker compose logs -f cosmos-emulator
docker compose logs -f azurite
./scripts/prototype-self-test.sh
docker compose down
```
The Cosmos Data Explorer is exposed on `http://localhost:1234`.
Azurite Blob Storage is exposed on `http://localhost:10000`.
The platform dependency health JSON is exposed on `http://localhost:4003/api/health/dependencies`.
The platform self-test page is exposed on `http://localhost:4003/api/self-test`.
The platform self-test JSON is exposed on `http://localhost:4003/api/self-test.json`.
## Notes
- This is intended for early prototype use on a single machine.
- Do not commit `.env`.
- The Linux emulator is a preview and is only appropriate for local or prototype use.
- Azurite data is persisted in the Docker volume `azurite-data`.
- When the project moves to a more secure environment later, replace the emulator with a real Azure Cosmos DB account and move secrets out of `.env` into a proper secret manager.

124
scripts/prototype-self-test.sh Executable file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="$ROOT/docker-compose.yml"
docker compose -f "$COMPOSE_FILE" exec -T \
-e PLATFORM_INTERNAL_BASE_URL="${PLATFORM_INTERNAL_BASE_URL:-http://platform-service:4003/api}" \
-e PUBLIC_BLOB_BASE_URL="${PUBLIC_BLOB_BASE_URL:-http://localhost:10000/devstoreaccount1}" \
-e INTERNAL_BLOB_BASE_URL="${INTERNAL_BLOB_BASE_URL:-http://azurite:10000/devstoreaccount1}" \
platform-service node --input-type=module <<'NODE'
import { createAccessToken } from './dist/modules/auth/jwt.js';
const baseUrl = process.env.PLATFORM_INTERNAL_BASE_URL;
const publicBlobBaseUrl = process.env.PUBLIC_BLOB_BASE_URL;
const internalBlobBaseUrl = process.env.INTERNAL_BLOB_BASE_URL;
const token = await createAccessToken({
sub: 'prototype-self-test',
email: 'self-test@bytelyst.local',
role: 'admin',
productId: 'lysnrai',
});
const authHeaders = {
Authorization: `Bearer ${token}`,
};
async function request(path, init = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
...authHeaders,
...(init.headers ?? {}),
},
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${path} failed with ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : null;
}
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const blobName = `lysnrai/prototype-self-test/${stamp}.txt`;
const payload = `prototype self-test ${stamp}`;
console.log('Checking dependency health...');
console.log(JSON.stringify(await request('/health/dependencies', { headers: {} }), null, 2));
console.log('Running platform self-test...');
console.log(JSON.stringify(await request('/self-test.json', { headers: {} }), null, 2));
console.log('Fetching blob container metadata...');
console.log(JSON.stringify(await request('/blob/containers'), null, 2));
console.log(`Requesting SAS URL for ${blobName} ...`);
const sasResponse = await request('/blob/sas', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
container: 'attachments',
blobName,
permissions: 'rw',
expiresInMinutes: 10,
}),
});
const uploadUrl = sasResponse.sasUrl.replace(publicBlobBaseUrl, internalBlobBaseUrl);
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'x-ms-blob-type': 'BlockBlob',
'Content-Type': 'text/plain',
},
body: payload,
});
if (!uploadResponse.ok) {
throw new Error(`SAS upload failed with ${uploadResponse.status}: ${await uploadResponse.text()}`);
}
console.log('Listing uploaded blob...');
console.log(
JSON.stringify(
await request(`/blob/list?container=attachments&prefix=${encodeURIComponent('lysnrai/prototype-self-test')}&limit=20`),
null,
2
)
);
console.log('Fetching blob info...');
console.log(
JSON.stringify(
await request(`/blob/info?container=attachments&blobName=${encodeURIComponent(blobName)}`),
null,
2
)
);
console.log('Deleting uploaded blob via API...');
console.log(
JSON.stringify(
await request('/blob/delete', {
method: 'DELETE',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
container: 'attachments',
blobName,
}),
}),
null,
2
)
);
console.log('Prototype self-test finished successfully.');
NODE

View File

@ -4,7 +4,8 @@
* POST /blob/sas generate SAS URL for direct browser upload/download
* GET /blob/list list blobs in a container (with optional prefix)
* DELETE /blob/delete delete a blob
* GET /blob/info/:container/:blobName(*) get blob metadata
* GET /blob/info get blob metadata
* GET /blob/info/:container/* legacy path-style blob metadata lookup
* GET /blob/containers list available containers and their status
*/
@ -17,7 +18,13 @@ import {
isBlobStorageConfigured,
BLOB_CONTAINERS,
} from '../../lib/blob.js';
import { GenerateSasSchema, ListBlobsSchema, DeleteBlobSchema, type BlobInfo } from './types.js';
import {
GenerateSasSchema,
ListBlobsSchema,
DeleteBlobSchema,
BlobInfoQuerySchema,
type BlobInfo,
} from './types.js';
const USER_ALLOWED_CONTAINERS = new Set<string>([
BLOB_CONTAINERS.audio,
@ -57,6 +64,46 @@ function normalizePrefix(input: string | undefined): string | undefined {
}
export async function blobRoutes(app: FastifyInstance) {
async function getBlobInfo(
container: string,
blobName: string,
auth: Awaited<ReturnType<typeof requireAuth>>
) {
if (!Object.values(BLOB_CONTAINERS).includes(container as never)) {
throw new BadRequestError(`Invalid container: ${container}`);
}
const admin = isAdminRole(auth.role);
if (!admin) {
if (!USER_ALLOWED_CONTAINERS.has(container)) {
throw new UnauthorizedError(`Not allowed to access container: ${container}`);
}
const prefix = userPrefix({ productId: auth.productId, sub: auth.sub });
if (!blobName.startsWith(prefix)) {
throw new UnauthorizedError('Not allowed to access blobs outside your user scope');
}
}
const bucket = await getBucket(container);
const exists = await bucket.exists(blobName);
if (!exists) {
throw new NotFoundError(`Blob not found: ${container}/${blobName}`);
}
const blobs = await bucket.list(blobName);
const meta = blobs.find(blob => blob.key === blobName);
return {
name: blobName,
container,
contentType: meta?.contentType,
size: meta?.size ?? 0,
lastModified: meta?.lastModified,
url: blobName,
metadata: meta?.metadata ?? {},
};
}
// Generate SAS URL for direct upload/download
app.post('/blob/sas', async req => {
const auth = await requireAuth(req);
@ -153,43 +200,28 @@ export async function blobRoutes(app: FastifyInstance) {
});
// Get blob metadata/info
app.get('/blob/info', async req => {
const auth = await requireAuth(req);
const parsed = BlobInfoQuerySchema.safeParse(req.query);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
return getBlobInfo(parsed.data.container, parsed.data.blobName, auth);
});
// Legacy path-style blob metadata/info
app.get('/blob/info/:container/:blobName', async req => {
const auth = await requireAuth(req);
const { container, blobName } = req.params as { container: string; blobName: string };
return getBlobInfo(container, blobName, auth);
});
if (!Object.values(BLOB_CONTAINERS).includes(container as never)) {
throw new BadRequestError(`Invalid container: ${container}`);
}
const admin = isAdminRole(auth.role);
if (!admin) {
if (!USER_ALLOWED_CONTAINERS.has(container)) {
throw new UnauthorizedError(`Not allowed to access container: ${container}`);
}
const prefix = userPrefix({ productId: auth.productId, sub: auth.sub });
if (!blobName.startsWith(prefix)) {
throw new UnauthorizedError('Not allowed to access blobs outside your user scope');
}
}
const bucket = await getBucket(container);
const exists = await bucket.exists(blobName);
if (!exists) {
throw new NotFoundError(`Blob not found: ${container}/${blobName}`);
}
const blobs = await bucket.list(blobName);
const meta = blobs.find(b => b.key === blobName);
return {
name: blobName,
container,
contentType: meta?.contentType,
size: meta?.size ?? 0,
lastModified: meta?.lastModified,
url: blobName,
metadata: meta?.metadata ?? {},
};
app.get('/blob/info/:container/*', async req => {
const auth = await requireAuth(req);
const { container } = req.params as { container: string };
const blobName = (req.params as Record<string, string>)['*'];
return getBlobInfo(container, blobName, auth);
});
// List available containers and config status

View File

@ -25,6 +25,11 @@ export const DeleteBlobSchema = z.object({
blobName: z.string().min(1).max(1024),
});
export const BlobInfoQuerySchema = z.object({
container: z.enum(containerNames),
blobName: z.string().min(1).max(1024),
});
export const UploadMetadataSchema = z.object({
container: z.enum(containerNames),
blobName: z.string().min(1).max(1024),
@ -45,4 +50,5 @@ export interface BlobInfo {
export type GenerateSasInput = z.infer<typeof GenerateSasSchema>;
export type ListBlobsInput = z.infer<typeof ListBlobsSchema>;
export type DeleteBlobInput = z.infer<typeof DeleteBlobSchema>;
export type BlobInfoQueryInput = z.infer<typeof BlobInfoQuerySchema>;
export type UploadMetadataInput = z.infer<typeof UploadMetadataSchema>;

View File

@ -6,7 +6,7 @@ import type { ServiceStatus, ServiceHealth, PlatformStatus } from './types.js';
interface ServiceDef {
name: string;
envVar: string;
defaultUrl: string;
defaultUrl?: string;
healthPath: string;
}
@ -23,19 +23,44 @@ const SERVICE_DEFS: ServiceDef[] = [
defaultUrl: 'http://localhost:4005',
healthPath: '/health',
},
{
name: 'MCP Server',
envVar: 'MCP_SERVER_URL',
defaultUrl: 'http://localhost:4007',
healthPath: '/health',
},
{
name: 'Backend API',
envVar: 'BACKEND_URL',
defaultUrl: 'http://localhost:8000',
defaultUrl: undefined,
healthPath: '/health',
},
];
function getEnabledServiceDefs(): ServiceDef[] {
return SERVICE_DEFS.filter(def => {
const configuredUrl = process.env[def.envVar]?.trim();
return Boolean(configuredUrl || def.defaultUrl);
});
}
/**
* Check health of a single service.
*/
async function checkService(def: ServiceDef): Promise<ServiceStatus> {
const baseUrl = process.env[def.envVar] || def.defaultUrl;
const configuredUrl = process.env[def.envVar]?.trim();
const baseUrl = configuredUrl || def.defaultUrl;
if (!baseUrl) {
return {
name: def.name,
url: 'not_configured',
health: 'maintenance',
responseTimeMs: null,
lastCheckedAt: new Date().toISOString(),
error: `${def.envVar} is not configured`,
};
}
const url = `${baseUrl}${def.healthPath}`;
const start = Date.now();
@ -97,13 +122,14 @@ function deriveOverallHealth(services: ServiceStatus[]): ServiceHealth {
* Check all services and return platform-wide status.
*/
export async function checkAllServices(): Promise<PlatformStatus> {
const results = await Promise.allSettled(SERVICE_DEFS.map(checkService));
const enabledDefs = getEnabledServiceDefs();
const results = await Promise.allSettled(enabledDefs.map(checkService));
const services: ServiceStatus[] = results.map((result, i) => {
if (result.status === 'fulfilled') return result.value;
return {
name: SERVICE_DEFS[i].name,
url: SERVICE_DEFS[i].defaultUrl,
name: enabledDefs[i].name,
url: enabledDefs[i].defaultUrl ?? 'not_configured',
health: 'major_outage' as ServiceHealth,
responseTimeMs: null,
lastCheckedAt: new Date().toISOString(),

View File

@ -5,12 +5,211 @@ import { CreateIncidentSchema, UpdateIncidentSchema } from './types.js';
import type { IncidentDoc, IncidentUpdate } from './types.js';
import * as repo from './repository.js';
import { checkAllServices } from './health-checker.js';
import { getDependencyHealthReport, getSelfTestReport } from './self-test.js';
const DEFAULT_PRODUCT_ID = 'lysnrai';
function renderSelfTestPage(): string {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Platform Self-Test</title>
<style>
:root {
color-scheme: light;
--bg: #f7f4ec;
--panel: #fffdf8;
--ink: #1f2933;
--muted: #52606d;
--ok: #1f7a3d;
--fail: #b42318;
--border: #d9d3c4;
}
body {
margin: 0;
padding: 24px;
background: linear-gradient(180deg, #f7f4ec 0%, #f2efe6 100%);
color: var(--ink);
font: 16px/1.5 "Iosevka Web", "IBM Plex Sans", sans-serif;
}
main {
max-width: 960px;
margin: 0 auto;
}
h1 {
margin: 0 0 8px;
font-size: 32px;
}
.lead {
margin: 0 0 24px;
color: var(--muted);
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
}
button {
border: 1px solid var(--ink);
background: var(--ink);
color: white;
padding: 10px 14px;
cursor: pointer;
font: inherit;
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
padding: 16px;
margin-bottom: 16px;
}
.status-ok { color: var(--ok); }
.status-fail { color: var(--fail); }
code, pre {
font: 13px/1.45 "Iosevka Web", "IBM Plex Mono", monospace;
}
pre {
margin: 0;
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 8px 0;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
.pill {
display: inline-block;
border: 1px solid currentColor;
border-radius: 999px;
padding: 2px 8px;
font-size: 12px;
}
</style>
</head>
<body>
<main>
<h1>Platform Self-Test</h1>
<p class="lead">Runs dependency checks plus a real blob upload, download, signed URL, and cleanup cycle.</p>
<div class="toolbar">
<button id="rerun" type="button">Run Self-Test</button>
<a href="/api/health/dependencies">Dependency JSON</a>
<a href="/api/self-test.json">Self-Test JSON</a>
</div>
<section class="panel">
<strong>Summary:</strong>
<span id="summary">Loading</span>
</section>
<section class="panel">
<h2>Dependency Checks</h2>
<div id="checks">Loading</div>
</section>
<section class="panel">
<h2>Service Status</h2>
<div id="services">Loading</div>
</section>
<section class="panel">
<h2>Raw JSON</h2>
<pre id="raw">Loading</pre>
</section>
</main>
<script>
const summary = document.getElementById('summary');
const checks = document.getElementById('checks');
const services = document.getElementById('services');
const raw = document.getElementById('raw');
const button = document.getElementById('rerun');
function statusClass(value) {
return value === 'ok' || value === 'pass' || value === 'operational' ? 'status-ok' : 'status-fail';
}
function renderChecks(items) {
return '<table><thead><tr><th>Name</th><th>Status</th><th>Summary</th><th>Duration</th></tr></thead><tbody>' +
items.map(item =>
'<tr>' +
'<td><code>' + item.name + '</code></td>' +
'<td><span class="pill ' + statusClass(item.status) + '">' + item.status + '</span></td>' +
'<td>' + item.summary + (item.error ? '<br><code>' + item.error + '</code>' : '') + '</td>' +
'<td>' + item.durationMs + 'ms</td>' +
'</tr>'
).join('') +
'</tbody></table>';
}
function renderServices(items) {
return '<table><thead><tr><th>Name</th><th>Health</th><th>URL</th><th>Response</th></tr></thead><tbody>' +
items.map(item =>
'<tr>' +
'<td>' + item.name + '</td>' +
'<td><span class="pill ' + statusClass(item.health) + '">' + item.health + '</span></td>' +
'<td><code>' + item.url + '</code></td>' +
'<td>' + (item.responseTimeMs === null ? 'n/a' : item.responseTimeMs + 'ms') + (item.error ? '<br><code>' + item.error + '</code>' : '') + '</td>' +
'</tr>'
).join('') +
'</tbody></table>';
}
async function load() {
button.disabled = true;
summary.textContent = 'Running…';
try {
const response = await fetch('/api/self-test.json');
const data = await response.json();
summary.innerHTML = '<span class="pill ' + statusClass(data.status) + '">' + data.status + '</span> checked at ' + data.checkedAt;
checks.innerHTML = renderChecks(data.dependencyChecks);
services.innerHTML = renderServices(data.serviceStatus.services);
raw.textContent = JSON.stringify(data, null, 2);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
summary.innerHTML = '<span class="pill status-fail">fail</span> ' + message;
checks.innerHTML = '';
services.innerHTML = '';
raw.textContent = message;
} finally {
button.disabled = false;
}
}
button.addEventListener('click', load);
load();
</script>
</body>
</html>`;
}
export async function statusRoutes(app: FastifyInstance) {
// ── Public endpoints (no auth) ─────────────────────────────
app.get('/health/dependencies', async (_req, reply) => {
const report = await getDependencyHealthReport();
if (report.status !== 'ok') {
reply.code(503);
}
return report;
});
app.get('/self-test.json', async (_req, reply) => {
const report = await getSelfTestReport();
if (report.status !== 'ok') {
reply.code(503);
}
return report;
});
app.get('/self-test', async (_req, reply) => {
reply.type('text/html; charset=utf-8');
return renderSelfTestPage();
});
// Get current platform status (health checks all services)
app.get('/status', async () => {
return checkAllServices();

View File

@ -0,0 +1,188 @@
import { randomUUID } from 'node:crypto';
import { URL } from 'node:url';
import { getStorage } from '@bytelyst/storage';
import { BLOB_CONTAINERS, getBucket, isBlobStorageConfigured } from '../../lib/blob.js';
import { checkAllServices } from './health-checker.js';
import type { PlatformStatus } from './types.js';
export type DiagnosticStatus = 'pass' | 'fail';
export interface DiagnosticCheck {
name: string;
status: DiagnosticStatus;
durationMs: number;
summary: string;
details?: Record<string, unknown>;
error?: string;
}
export interface DiagnosticsReport {
status: 'ok' | 'degraded';
checkedAt: string;
dependencyChecks: DiagnosticCheck[];
serviceStatus: PlatformStatus;
}
function checkStatus(ok: boolean): DiagnosticStatus {
return ok ? 'pass' : 'fail';
}
function reportStatus(checks: DiagnosticCheck[], serviceStatus: PlatformStatus): 'ok' | 'degraded' {
if (checks.some(check => check.status === 'fail')) return 'degraded';
return serviceStatus.overall === 'operational' ? 'ok' : 'degraded';
}
export async function checkBlobStorageReadiness(): Promise<DiagnosticCheck> {
const startedAt = Date.now();
if (!isBlobStorageConfigured()) {
return {
name: 'blob_storage',
status: 'fail',
durationMs: Date.now() - startedAt,
summary: 'Blob storage is not configured',
details: {
configured: false,
provider: process.env.STORAGE_PROVIDER || 'azure',
},
};
}
try {
const storage = await getStorage();
const healthy = await storage.isHealthy();
return {
name: 'blob_storage',
status: checkStatus(healthy),
durationMs: Date.now() - startedAt,
summary: healthy
? 'Blob storage provider is reachable'
: 'Blob storage provider is not reachable',
details: {
configured: true,
provider: process.env.STORAGE_PROVIDER || 'azure',
},
};
} catch (error) {
return {
name: 'blob_storage',
status: 'fail',
durationMs: Date.now() - startedAt,
summary: 'Blob storage readiness check failed',
error: error instanceof Error ? error.message : String(error),
details: {
configured: true,
provider: process.env.STORAGE_PROVIDER || 'azure',
},
};
}
}
export async function runBlobStorageSelfTest(): Promise<DiagnosticCheck> {
const startedAt = Date.now();
const bucket = await getBucket(BLOB_CONTAINERS.attachments);
const key = `self-test/platform-service/${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID()}.txt`;
const payload = `platform-service self-test ${new Date().toISOString()}`;
try {
const storage = await getStorage();
const providerHealthy = await storage.isHealthy();
await bucket.upload(key, payload, {
contentType: 'text/plain',
metadata: {
source: 'platform-service-self-test',
},
});
const existsAfterUpload = await bucket.exists(key);
const listed = await bucket.list(key);
const downloaded = (await bucket.download(key)).toString('utf8');
const signedUrl = await bucket.getSignedUrl(key, { permissions: 'read', expiresIn: 300 });
await bucket.delete(key);
const existsAfterDelete = await bucket.exists(key);
const signedUrlHost = new URL(signedUrl).host;
const listedBlob = listed.find(blob => blob.key === key);
const passed =
providerHealthy &&
existsAfterUpload &&
!!listedBlob &&
downloaded === payload &&
!existsAfterDelete;
return {
name: 'blob_storage_self_test',
status: checkStatus(passed),
durationMs: Date.now() - startedAt,
summary: passed
? 'Blob storage upload/list/download/sign/delete flow passed'
: 'Blob storage self-test reported a failed assertion',
details: {
bucket: BLOB_CONTAINERS.attachments,
key,
provider: process.env.STORAGE_PROVIDER || 'azure',
providerHealthy,
existsAfterUpload,
listed: !!listedBlob,
downloadedMatches: downloaded === payload,
existsAfterDelete,
signedUrlHost,
},
};
} catch (error) {
try {
await bucket.delete(key);
} catch {
// Best-effort cleanup for temporary self-test blobs.
}
return {
name: 'blob_storage_self_test',
status: 'fail',
durationMs: Date.now() - startedAt,
summary: 'Blob storage self-test failed',
error: error instanceof Error ? error.message : String(error),
details: {
bucket: BLOB_CONTAINERS.attachments,
key,
provider: process.env.STORAGE_PROVIDER || 'azure',
},
};
}
}
export async function getDependencyHealthReport(): Promise<DiagnosticsReport> {
const [blobStorage, serviceStatus] = await Promise.all([
checkBlobStorageReadiness(),
checkAllServices(),
]);
const dependencyChecks = [blobStorage];
return {
status: reportStatus(dependencyChecks, serviceStatus),
checkedAt: new Date().toISOString(),
dependencyChecks,
serviceStatus,
};
}
export async function getSelfTestReport(): Promise<DiagnosticsReport> {
const [blobStorage, blobStorageSelfTest, serviceStatus] = await Promise.all([
checkBlobStorageReadiness(),
runBlobStorageSelfTest(),
checkAllServices(),
]);
const dependencyChecks = [blobStorage, blobStorageSelfTest];
return {
status: reportStatus(dependencyChecks, serviceStatus),
checkedAt: new Date().toISOString(),
dependencyChecks,
serviceStatus,
};
}