feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend
This commit is contained in:
137
ts/registry.ts
137
ts/registry.ts
@@ -4,12 +4,46 @@
|
||||
*/
|
||||
|
||||
import * as plugins from './plugins.ts';
|
||||
import { initDb, closeDb, isDbConnected } from './models/db.ts';
|
||||
import { closeDb, initDb, isDbConnected } from './models/db.ts';
|
||||
import { StackGalleryAuthProvider } from './providers/auth.provider.ts';
|
||||
import { StackGalleryStorageHooks } from './providers/storage.provider.ts';
|
||||
import { ApiRouter } from './api/router.ts';
|
||||
import { getEmbeddedFile } from './embedded-ui.generated.ts';
|
||||
import { ReloadSocketManager } from './reload-socket.ts';
|
||||
import { OpsServer } from './opsserver/classes.opsserver.ts';
|
||||
|
||||
// Bundled UI files (generated by tsbundle with base64ts output mode)
|
||||
let bundledFileMap: Map<string, { data: Uint8Array; contentType: string }> | null = null;
|
||||
try {
|
||||
// @ts-ignore - generated file may not exist yet
|
||||
const { files } = await import('../ts_bundled/bundle.ts');
|
||||
bundledFileMap = new Map();
|
||||
for (const file of files as Array<{ path: string; contentBase64: string }>) {
|
||||
const binary = Uint8Array.from(atob(file.contentBase64), (c) => c.charCodeAt(0));
|
||||
const ext = file.path.split('.').pop() || '';
|
||||
bundledFileMap.set(`/${file.path}`, { data: binary, contentType: getContentType(ext) });
|
||||
}
|
||||
} catch {
|
||||
console.warn('[StackGalleryRegistry] No bundled UI found (ts_bundled/bundle.ts missing)');
|
||||
}
|
||||
|
||||
function getContentType(ext: string): string {
|
||||
const types: Record<string, string> = {
|
||||
html: 'text/html',
|
||||
js: 'application/javascript',
|
||||
css: 'text/css',
|
||||
json: 'application/json',
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
gif: 'image/gif',
|
||||
svg: 'image/svg+xml',
|
||||
ico: 'image/x-icon',
|
||||
woff: 'font/woff',
|
||||
woff2: 'font/woff2',
|
||||
ttf: 'font/ttf',
|
||||
eot: 'application/vnd.ms-fontobject',
|
||||
map: 'application/json',
|
||||
};
|
||||
return types[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
export interface IRegistryConfig {
|
||||
// MongoDB configuration
|
||||
@@ -42,8 +76,7 @@ export class StackGalleryRegistry {
|
||||
private smartRegistry: plugins.smartregistry.SmartRegistry | null = null;
|
||||
private authProvider: StackGalleryAuthProvider | null = null;
|
||||
private storageHooks: StackGalleryStorageHooks | null = null;
|
||||
private apiRouter: ApiRouter | null = null;
|
||||
private reloadSocket: ReloadSocketManager | null = null;
|
||||
private opsServer: OpsServer | null = null;
|
||||
private isInitialized = false;
|
||||
|
||||
constructor(config: IRegistryConfig) {
|
||||
@@ -115,13 +148,11 @@ export class StackGalleryRegistry {
|
||||
});
|
||||
console.log('[StackGalleryRegistry] smartregistry initialized');
|
||||
|
||||
// Initialize API router
|
||||
console.log('[StackGalleryRegistry] Initializing API router...');
|
||||
this.apiRouter = new ApiRouter();
|
||||
console.log('[StackGalleryRegistry] API router initialized');
|
||||
|
||||
// Initialize reload socket for hot reload
|
||||
this.reloadSocket = new ReloadSocketManager();
|
||||
// Initialize OpsServer (TypedRequest handlers)
|
||||
console.log('[StackGalleryRegistry] Initializing OpsServer...');
|
||||
this.opsServer = new OpsServer(this);
|
||||
await this.opsServer.start();
|
||||
console.log('[StackGalleryRegistry] OpsServer initialized');
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('[StackGalleryRegistry] Initialization complete');
|
||||
@@ -144,7 +175,7 @@ export class StackGalleryRegistry {
|
||||
{ port, hostname: host },
|
||||
async (request: Request): Promise<Response> => {
|
||||
return await this.handleRequest(request);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
console.log(`[StackGalleryRegistry] Server running on http://${host}:${port}`);
|
||||
@@ -162,11 +193,14 @@ export class StackGalleryRegistry {
|
||||
return this.healthCheck();
|
||||
}
|
||||
|
||||
// API endpoints (handled by REST API layer)
|
||||
if (path.startsWith('/api/')) {
|
||||
return await this.handleApiRequest(request);
|
||||
// TypedRequest endpoint (handled by OpsServer TypedRouter)
|
||||
if (path === '/typedrequest' && request.method === 'POST') {
|
||||
return await this.handleTypedRequest(request);
|
||||
}
|
||||
|
||||
// Legacy REST API endpoints (keep for backwards compatibility during migration)
|
||||
// TODO: Remove once frontend is fully migrated to TypedRequest
|
||||
|
||||
// Registry protocol endpoints (handled by smartregistry)
|
||||
const registryPaths = [
|
||||
'/-/',
|
||||
@@ -180,8 +214,7 @@ export class StackGalleryRegistry {
|
||||
'/api/v1/gems/',
|
||||
'/gems/',
|
||||
];
|
||||
const isRegistryPath =
|
||||
registryPaths.some((p) => path.startsWith(p)) ||
|
||||
const isRegistryPath = registryPaths.some((p) => path.startsWith(p)) ||
|
||||
(path.startsWith('/@') && !path.startsWith('/@stack'));
|
||||
|
||||
if (this.smartRegistry && isRegistryPath) {
|
||||
@@ -199,11 +232,6 @@ export class StackGalleryRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket upgrade for hot reload
|
||||
if (path === '/ws/reload' && request.headers.get('upgrade') === 'websocket') {
|
||||
return this.reloadSocket!.handleUpgrade(request);
|
||||
}
|
||||
|
||||
// Serve static UI files
|
||||
return this.serveStaticFile(path);
|
||||
}
|
||||
@@ -212,7 +240,7 @@ export class StackGalleryRegistry {
|
||||
* Convert a Deno Request to smartregistry IRequestContext
|
||||
*/
|
||||
private async requestToContext(
|
||||
request: Request
|
||||
request: Request,
|
||||
): Promise<plugins.smartregistry.IRequestContext> {
|
||||
const url = new URL(request.url);
|
||||
const headers: Record<string, string> = {};
|
||||
@@ -285,24 +313,28 @@ export class StackGalleryRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve static files from embedded UI
|
||||
* Serve static files from bundled UI
|
||||
*/
|
||||
private serveStaticFile(path: string): Response {
|
||||
if (!bundledFileMap) {
|
||||
return new Response('UI not bundled. Run tsbundle first.', { status: 404 });
|
||||
}
|
||||
|
||||
const filePath = path === '/' ? '/index.html' : path;
|
||||
|
||||
// Get embedded file
|
||||
const embeddedFile = getEmbeddedFile(filePath);
|
||||
if (embeddedFile) {
|
||||
return new Response(embeddedFile.data as unknown as BodyInit, {
|
||||
// Get bundled file
|
||||
const file = bundledFileMap.get(filePath);
|
||||
if (file) {
|
||||
return new Response(file.data, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': embeddedFile.contentType },
|
||||
headers: { 'Content-Type': file.contentType },
|
||||
});
|
||||
}
|
||||
|
||||
// SPA fallback: serve index.html for unknown paths
|
||||
const indexFile = getEmbeddedFile('/index.html');
|
||||
const indexFile = bundledFileMap.get('/index.html');
|
||||
if (indexFile) {
|
||||
return new Response(indexFile.data as unknown as BodyInit, {
|
||||
return new Response(indexFile.data, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
@@ -312,17 +344,34 @@ export class StackGalleryRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API requests
|
||||
* Handle TypedRequest calls
|
||||
*/
|
||||
private async handleApiRequest(request: Request): Promise<Response> {
|
||||
if (!this.apiRouter) {
|
||||
return new Response(JSON.stringify({ error: 'API router not initialized' }), {
|
||||
private async handleTypedRequest(request: Request): Promise<Response> {
|
||||
if (!this.opsServer) {
|
||||
return new Response(JSON.stringify({ error: 'OpsServer not initialized' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
return await this.apiRouter.handle(request);
|
||||
try {
|
||||
const body = await request.json();
|
||||
const result = await this.opsServer.typedrouter.routeAndAddResponse(body);
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[StackGalleryRegistry] TypedRequest error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Internal server error';
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,6 +401,9 @@ export class StackGalleryRegistry {
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
console.log('[StackGalleryRegistry] Shutting down...');
|
||||
if (this.opsServer) {
|
||||
await this.opsServer.stop();
|
||||
}
|
||||
await closeDb();
|
||||
this.isInitialized = false;
|
||||
console.log('[StackGalleryRegistry] Shutdown complete');
|
||||
@@ -420,9 +472,10 @@ export async function createRegistryFromEnvFile(): Promise<StackGalleryRegistry>
|
||||
const s3Endpoint = `${s3Protocol}://${env.S3_HOST || 'localhost'}:${env.S3_PORT || '9000'}`;
|
||||
|
||||
const config: IRegistryConfig = {
|
||||
mongoUrl:
|
||||
env.MONGODB_URL ||
|
||||
`mongodb://${env.MONGODB_USER}:${env.MONGODB_PASS}@${env.MONGODB_HOST || 'localhost'}:${env.MONGODB_PORT || '27017'}/${env.MONGODB_NAME}?authSource=admin`,
|
||||
mongoUrl: env.MONGODB_URL ||
|
||||
`mongodb://${env.MONGODB_USER}:${env.MONGODB_PASS}@${env.MONGODB_HOST || 'localhost'}:${
|
||||
env.MONGODB_PORT || '27017'
|
||||
}/${env.MONGODB_NAME}?authSource=admin`,
|
||||
mongoDb: env.MONGODB_NAME || 'stackgallery',
|
||||
s3Endpoint: s3Endpoint,
|
||||
s3AccessKey: env.S3_ACCESSKEY || env.S3_ACCESS_KEY || 'minioadmin',
|
||||
@@ -444,7 +497,7 @@ export async function createRegistryFromEnvFile(): Promise<StackGalleryRegistry>
|
||||
} else {
|
||||
console.warn(
|
||||
'[StackGalleryRegistry] Error reading .nogit/env.json, falling back to env vars:',
|
||||
error
|
||||
error,
|
||||
);
|
||||
}
|
||||
return createRegistryFromEnv();
|
||||
|
||||
Reference in New Issue
Block a user