diff --git a/Dockerfile b/Dockerfile index 9485b8c..170b0e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # meta-sort standalone Docker image -# Includes: Redis, nginx, rclone, FFmpeg, Node.js backend, React UI, and Editor +# Includes: Redis, nginx, rclone, FFmpeg, Node.js backend, and React UI # # Build standalone: # docker build -t meta-sort . @@ -24,20 +24,7 @@ RUN npm install COPY packages/meta-sort-ui/ ./ RUN npm run build -# Stage 2: Build Editor -FROM node:21-alpine AS editor-builder - -WORKDIR /build - -# Copy editor package and install -COPY packages/meta-sort-editor/package.json ./ -RUN npm install - -# Copy editor source and build -COPY packages/meta-sort-editor/ ./ -RUN npm run build - -# Stage 3: Build Backend using pnpm workspace +# Stage 2: Build Backend using pnpm workspace FROM node:21-alpine AS backend-builder # Install pnpm @@ -67,7 +54,7 @@ RUN pnpm --filter="@worph/async-utils" build || true # Build the core package RUN pnpm --filter="@meta-sort/core" build -# Stage 4: Runtime +# Stage 3: Runtime FROM ubuntu:22.04 # Container registry metadata @@ -105,7 +92,6 @@ RUN curl https://rclone.org/install.sh | bash RUN mkdir -p \ /app/backend \ /app/ui \ - /app/editor \ /data/watch \ /data/cache \ /data/redis \ @@ -122,10 +108,6 @@ RUN mkdir -p \ COPY --from=ui-builder /build/dist /app/ui RUN chmod -R 755 /app/ui -# Copy built Editor -COPY --from=editor-builder /build/dist /app/editor -RUN chmod -R 755 /app/editor - # Copy built backend with all dependencies COPY --from=backend-builder /build/packages/meta-sort-core/dist /app/backend/dist COPY --from=backend-builder /build/packages/meta-sort-core/package.json /app/backend/ diff --git a/README.md b/README.md index 41b4c86..4b4080a 100644 --- a/README.md +++ b/README.md @@ -361,8 +361,7 @@ async function discoverService(name: string): Promise { │ │ ├── Watches /files/* for new media files ││ │ │ ├── Extracts metadata → writes to KV (Redis in /meta-core/db) ││ │ │ ├── Manages remote mounts INTO /files (NFS, SMB, rclone) ││ -│ │ ├── Leader election (spawns Redis if leader) ││ -│ │ └── Serves monitoring UI (meta-sort-editor) ││ +│ │ └── Leader election (spawns Redis if leader) ││ │ │ ││ │ │ [2] meta-fuse PROCESS-READ ││ │ │ ├── Reads metadata from KV (Redis) ││ @@ -665,9 +664,6 @@ meta-sort/ │ │ │ └── index.ts # Entry point │ │ └── package.json │ │ -│ ├── meta-sort-editor/ # Metadata editor UI (@meta-sort/editor) -│ │ └── ... # React/Vite app -│ │ │ ├── meta-sort-ui/ # Monitoring dashboard (@meta-sort/ui) │ │ └── ... # React/Vite app │ │ diff --git a/docker/nginx.conf b/docker/nginx.conf index b1bffcd..2156770 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -44,18 +44,6 @@ http { try_files $uri $uri/ /index.html; } - # Editor UI - SPA with base path /editor/ - location /editor/ { - alias /app/editor/; - index index.html; - try_files $uri $uri/ /editor/index.html; - } - - # Redirect /editor to /editor/ - location = /editor { - return 301 /editor/; - } - # API routes location /api/ { proxy_pass http://api; diff --git a/packages/meta-sort-core/src/api/UnifiedAPIServer.ts b/packages/meta-sort-core/src/api/UnifiedAPIServer.ts index ec34964..c8ebfbc 100644 --- a/packages/meta-sort-core/src/api/UnifiedAPIServer.ts +++ b/packages/meta-sort-core/src/api/UnifiedAPIServer.ts @@ -13,6 +13,8 @@ import { UnifiedProcessingStateManager } from '../logic/UnifiedProcessingStateMa import { DuplicateResult } from '../logic/DuplicateFinder.js'; import type { IKVClient } from '../kv/IKVClient.js'; import type { KVManager } from '../kv/KVManager.js'; +import type { ExtendedProcessingSnapshot, IKVClientWithPubSub } from '../types/ExtendedInterfaces.js'; +import { getErrorMessage } from '../types/ExtendedInterfaces.js'; import { config } from '../config/EnvConfig.js'; import type { PluginManager } from '../plugin-engine/PluginManager.js'; import type { TaskScheduler } from '../plugin-engine/TaskScheduler.js'; @@ -631,12 +633,12 @@ export class UnifiedAPIServer { // Get snapshot from state manager // Note: VFS-related features have been moved to meta-fuse // totalDone now uses state manager's count of fully processed files - const snapshot = this.unifiedStateManager!.getSnapshot(0, this.fastQueueConcurrency, this.backgroundQueueConcurrency); + const snapshot = this.unifiedStateManager!.getSnapshot(0, this.fastQueueConcurrency, this.backgroundQueueConcurrency) as ExtendedProcessingSnapshot; // Add queue status if available if (this.getQueueStatus) { const queueStatus = this.getQueueStatus(); - (snapshot as any).queueStatus = queueStatus; + snapshot.queueStatus = queueStatus; // Add calculated fields that the UI needs (make API self-describing) // Pipeline: validation (preProcess) → lightProcessing (fast) → hashProcessing (background) @@ -659,7 +661,7 @@ export class UnifiedAPIServer { // Get gate status if container scheduler is available const gateStatus = this.containerPluginScheduler?.getGateStatus(); - (snapshot as any).computed = { + snapshot.computed = { // Pre-process queue (validation - quick extension checks) preProcessRunning, preProcessPending, @@ -693,7 +695,7 @@ export class UnifiedAPIServer { // Add failed files count from metrics const metrics = performanceMetrics.getMetrics(); - (snapshot as any).totalFailed = metrics.totalFailedFiles || 0; + snapshot.totalFailed = metrics.totalFailedFiles || 0; return snapshot; }); @@ -722,7 +724,7 @@ export class UnifiedAPIServer { // Trigger scan callback if available to pick up the file if (this.triggerScanCallback) { // Don't await - just trigger async - this.triggerScanCallback().catch(() => {}); + this.triggerScanCallback().catch(err => console.warn('[API] Scan trigger failed during retry:', getErrorMessage(err))); } return { status: 'ok', message: `File ${filePath} queued for retry` }; @@ -744,7 +746,7 @@ export class UnifiedAPIServer { // Trigger scan callback if available if (this.triggerScanCallback) { - this.triggerScanCallback().catch(() => {}); + this.triggerScanCallback().catch(err => console.warn('[API] Scan trigger failed during retry-all:', getErrorMessage(err))); } return { status: 'ok', message: `${queued} files queued for retry` }; @@ -838,7 +840,7 @@ export class UnifiedAPIServer { this.app.get('/api/stats', async (request, reply) => { try { const hashIds = await this.kvClient!.getAllHashIds(); - const redis = (this.kvClient as any).getRedisClient?.(); + const redis = (this.kvClient as IKVClientWithPubSub).getRedisClient?.() as { info: (section: string) => Promise } | undefined; let memoryUsage = 'N/A'; let memoryUsageBytes = 0; diff --git a/packages/meta-sort-core/src/api/VirtualFileSystem.ts b/packages/meta-sort-core/src/api/VirtualFileSystem.ts index be8dbd1..d779075 100644 --- a/packages/meta-sort-core/src/api/VirtualFileSystem.ts +++ b/packages/meta-sort-core/src/api/VirtualFileSystem.ts @@ -8,6 +8,7 @@ import { EventEmitter } from 'events'; import * as path from 'path'; import * as fs from 'fs'; import { HashMeta } from '@metazla/meta-interface'; +import type { ExtendedHashMeta } from '../types/ExtendedInterfaces.js'; import { IFuseAPI, VirtualDirectory, @@ -149,9 +150,10 @@ export class VirtualFileSystem extends EventEmitter implements IFuseAPI { let mtime: Date; let ctime: Date; - if (metadata && (metadata as any).sizeByte) { + const extMeta = metadata as ExtendedHashMeta | undefined; + if (extMeta && extMeta.sizeByte) { // Use cached metadata (fast path - no disk I/O) - size = parseInt((metadata as any).sizeByte, 10); + size = parseInt(String(extMeta.sizeByte), 10); // Use current time as placeholder for mtime/ctime (will be corrected on next scan) mtime = new Date(); ctime = new Date(); @@ -458,7 +460,8 @@ export class VirtualFileSystem extends EventEmitter implements IFuseAPI { // Only count actual media files (not directories, not metadata files) if (node.type === 'file' && node.sourcePath && node.metadata) { // Check if file has completed processing (both light and hash phases) - if ((node.metadata as any).processingStatus === 'complete') { + const extMeta = node.metadata as ExtendedHashMeta; + if (extMeta.processingStatus === 'complete') { count++; } } diff --git a/packages/meta-sort-core/src/api/api.test.ts b/packages/meta-sort-core/src/api/api.test.ts index 523c3f5..d31f98b 100644 --- a/packages/meta-sort-core/src/api/api.test.ts +++ b/packages/meta-sort-core/src/api/api.test.ts @@ -192,7 +192,7 @@ class MockProcessingStateManager { } // Helpers for tests - addPending(path: string): void { + addDiscovered(path: string): void { this.pending.add(path); } @@ -258,7 +258,7 @@ describe('UnifiedAPIServer', function() { }); mockStateManager.addDone('/files/test/Test Movie.mp4'); - mockStateManager.addPending('/files/test/pending.mp4'); + mockStateManager.addDiscovered('/files/test/pending.mp4'); // Create VFS (no params needed - uses defaults) const vfs = new VirtualFileSystem(); @@ -428,130 +428,6 @@ describe('UnifiedAPIServer', function() { }); }); - // ========================================================================= - // Metadata API (File Browser) - // ========================================================================= - - describe('Metadata API (/api/metadata/*)', function() { - it('GET /api/metadata/hash-ids returns list of hash IDs', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/metadata/hash-ids' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('hashIds'); - expect(Array.isArray(body.hashIds)).to.be.true; - expect(body.hashIds).to.include('testhash001'); - }); - - it('GET /api/metadata/list returns paginated file list', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/metadata/list?limit=10&offset=0' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('files'); - expect(body).to.have.property('total'); - expect(Array.isArray(body.files)).to.be.true; - }); - - it('GET /api/metadata/:hashId returns file metadata', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/metadata/testhash001' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - // API returns metadata flat (not wrapped in { hashId, metadata }) - expect(body).to.have.property('fileName', 'Test Movie.mp4'); - }); - - it('GET /api/metadata/:hashId returns 404 for unknown hash', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/metadata/unknownhash' - }); - - expect(response.statusCode).to.equal(404); - }); - - it('GET /api/metadata/:hashId/property returns specific property', async function() { - // The API uses getProperty with key format /file/{hashId}/{property} - // We need to seed the property in the rawStore - await mockKV.setProperty('/file/testhash001/videoType', 'movie'); - - const response = await app.inject({ - method: 'GET', - url: '/api/metadata/testhash001/property?property=videoType' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('value', 'movie'); - }); - - it('PUT /api/metadata/:hashId updates metadata', async function() { - const response = await app.inject({ - method: 'PUT', - url: '/api/metadata/testhash001', - payload: { - customField: 'customValue' - } - }); - - expect(response.statusCode).to.equal(200); - - // Verify the update - const verify = await app.inject({ - method: 'GET', - url: '/api/metadata/testhash001' - }); - const body = JSON.parse(verify.payload); - // Response is flat metadata - expect(body).to.have.property('customField', 'customValue'); - }); - - it('DELETE /api/metadata/:hashId deletes file metadata', async function() { - // First create a test entry - mockKV.seedData('deleteme', { fileName: 'delete.mp4' }); - - const response = await app.inject({ - method: 'DELETE', - url: '/api/metadata/deleteme' - }); - - expect(response.statusCode).to.equal(200); - - // Verify deletion - const verify = await app.inject({ - method: 'GET', - url: '/api/metadata/deleteme' - }); - expect(verify.statusCode).to.equal(404); - }); - - it('POST /api/metadata/search searches files', async function() { - const response = await app.inject({ - method: 'POST', - url: '/api/metadata/search', - payload: { - property: 'videoType', - propertyValue: 'movie' - } - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('results'); - expect(Array.isArray(body.results)).to.be.true; - }); - }); - // ========================================================================= // Stats API (Dashboard) // ========================================================================= @@ -694,35 +570,6 @@ describe('UnifiedAPIServer', function() { }); }); - // ========================================================================= - // KV Browser API (Editor) - // ========================================================================= - - describe('KV Browser API (/api/kv/*)', function() { - it('GET /api/kv/info returns database info', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/kv/info' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('fileCount'); - }); - - it('GET /api/kv/keys returns paginated keys', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/kv/keys?cursor=0&count=50' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('keys'); - expect(Array.isArray(body.keys)).to.be.true; - }); - }); - // ========================================================================= // Plugins API (Plugins Page) // ========================================================================= diff --git a/packages/meta-sort-core/src/container-plugins/ContainerPluginScheduler.ts b/packages/meta-sort-core/src/container-plugins/ContainerPluginScheduler.ts index 23a29b4..e0f95c6 100644 --- a/packages/meta-sort-core/src/container-plugins/ContainerPluginScheduler.ts +++ b/packages/meta-sort-core/src/container-plugins/ContainerPluginScheduler.ts @@ -19,6 +19,7 @@ import type { } from './types.js'; import type { TaskQueueType } from '../plugin-engine/types.js'; import type { IKVClient } from '../kv/IKVClient.js'; +import { hasPublish, type ExtendedContainerTask } from '../types/ExtendedInterfaces.js'; import { config } from '../config/EnvConfig.js'; /** @@ -218,7 +219,7 @@ export class ContainerPluginScheduler extends EventEmitter { * to be notified when specific plugins complete for a file. */ private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise { - if (!this.kvClient) { + if (!this.kvClient || !hasPublish(this.kvClient)) { return; } @@ -229,7 +230,7 @@ export class ContainerPluginScheduler extends EventEmitter { filePath, timestamp: Date.now() }); - await (this.kvClient as any).publish('meta-sort:plugin:complete', message); + await this.kvClient.publish('meta-sort:plugin:complete', message); } catch (error) { // Silent fail - pub/sub is optional } @@ -867,7 +868,7 @@ export class ContainerPluginScheduler extends EventEmitter { ); // Store existing metadata in task for dispatch - (task as any).existingMeta = existingMeta; + (task as ExtendedContainerTask).existingMeta = existingMeta; this.enqueueTask(task); } diff --git a/packages/meta-sort-core/src/jellyfin/JellyfinNfo.ts b/packages/meta-sort-core/src/jellyfin/JellyfinNfo.ts index e6c96d7..e2b208a 100644 --- a/packages/meta-sort-core/src/jellyfin/JellyfinNfo.ts +++ b/packages/meta-sort-core/src/jellyfin/JellyfinNfo.ts @@ -180,7 +180,6 @@ export class JellyfinNfo { existingData[targetRoot] = root; if (hasChanged) { - //const builder = new Builder({ headless: true, renderOpts: { 'pretty': true } }); const builder = new Builder(); // Convert the updated object back to XML try { diff --git a/packages/meta-sort-core/src/kv/ServiceDiscovery.ts b/packages/meta-sort-core/src/kv/ServiceDiscovery.ts index cef4ded..13f424f 100644 --- a/packages/meta-sort-core/src/kv/ServiceDiscovery.ts +++ b/packages/meta-sort-core/src/kv/ServiceDiscovery.ts @@ -12,6 +12,7 @@ import { promises as fs } from 'fs'; import { hostname } from 'os'; import { join } from 'path'; import type { ServiceInfo } from './IKVClient.js'; +import { isNodeError } from '../types/ExtendedInterfaces.js'; interface ServiceDiscoveryConfig { /** Path to META_CORE_VOLUME (e.g., /meta-core) */ @@ -126,7 +127,7 @@ export class ServiceDiscovery { await fs.writeFile(this.serviceFilePath, JSON.stringify(info, null, 2)); } catch (error) { // If file was deleted, re-register - if ((error as any).code === 'ENOENT') { + if (isNodeError(error) && error.code === 'ENOENT') { await this.register(); await this.updateStatus('running'); } else { @@ -200,7 +201,7 @@ export class ServiceDiscovery { return service; } catch (error) { - if ((error as any).code === 'ENOENT') { + if (isNodeError(error) && error.code === 'ENOENT') { return null; // Service not registered } throw error; @@ -228,7 +229,7 @@ export class ServiceDiscovery { return services; } catch (error) { - if ((error as any).code === 'ENOENT') { + if (isNodeError(error) && error.code === 'ENOENT') { return []; // Services directory doesn't exist yet } throw error; diff --git a/packages/meta-sort-core/src/logic/UnifiedProcessingStateManager.ts b/packages/meta-sort-core/src/logic/UnifiedProcessingStateManager.ts index c5ba9a7..fd55b97 100644 --- a/packages/meta-sort-core/src/logic/UnifiedProcessingStateManager.ts +++ b/packages/meta-sort-core/src/logic/UnifiedProcessingStateManager.ts @@ -82,13 +82,6 @@ export class UnifiedProcessingStateManager { }); } - /** - * @deprecated Use addDiscovered instead - */ - addPending(filePath: string): void { - this.addDiscovered(filePath); - } - /** * Move file from discovered to light processing */ diff --git a/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts b/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts index 274b8dd..8a098a1 100644 --- a/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts +++ b/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts @@ -15,6 +15,7 @@ import {VirtualFileSystem} from "../api/VirtualFileSystem.js"; import {performanceMetrics} from "../metrics/PerformanceMetrics.js"; import {UnifiedProcessingStateManager} from "./UnifiedProcessingStateManager.js"; import type {IKVClient} from "../kv/IKVClient.js"; +import { hasPublish, type ExtendedFileAnalyzer, type ExtendedHashMeta } from "../types/ExtendedInterfaces.js"; import PQueue from "p-queue"; import os from 'os'; import { TaskScheduler, createTaskScheduler, MetadataNodeKVStore } from "../plugin-engine/index.js"; @@ -29,7 +30,6 @@ export class WatchedFileProcessor implements FileProcessorInterface { metaDataToFolderStruct = new MetaDataToFolderStruct(renamingRule); virtualFileSystem: VirtualFileSystem; unifiedStateManager = new UnifiedProcessingStateManager(); - //fileProcessor:FileAnalyzerInterface = new FileProcessor();//TODO only use for debug fileProcessor:FileAnalyzerInterface = new FileProcessorPiscina(); duplicateFinder = new DuplicateFinder(); duplicateResult: DuplicateResult | null = null; @@ -249,7 +249,7 @@ export class WatchedFileProcessor implements FileProcessorInterface { */ private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise { const kvClient = this.getKVClient(); - if (!kvClient || typeof (kvClient as any).publish !== 'function') { + if (!kvClient || !hasPublish(kvClient)) { return; } @@ -260,7 +260,7 @@ export class WatchedFileProcessor implements FileProcessorInterface { filePath, timestamp: Date.now() }); - await (kvClient as any).publish('meta-sort:plugin:complete', message); + await kvClient.publish('meta-sort:plugin:complete', message); } catch (error) { // Silent fail - pub/sub is optional } @@ -298,11 +298,7 @@ export class WatchedFileProcessor implements FileProcessorInterface { // Phase 1: Pre-process (midhash256 computation) // Phase 2: Plugin execution via TaskScheduler (fast/background queues) - // Debug: log queue state before adding - console.log(`[DEBUG queueFile] Before add: size=${this.preProcessQueue.size}, pending=${this.preProcessQueue.pending}`); - await this.preProcessQueue.add(async () => { - console.log(`[DEBUG queueFile] Task started: size=${this.preProcessQueue.size}, pending=${this.preProcessQueue.pending}`); try { // STATE: Start LIGHT processing if (this.unifiedStateManager) { @@ -314,7 +310,8 @@ export class WatchedFileProcessor implements FileProcessorInterface { const { computeMidHash256 } = await import('@metazla/meta-hash'); // Check cache first - const indexLine = (this.fileProcessor as any).indexManager?.getCidForFile( + const extAnalyzer = this.fileProcessor as ExtendedFileAnalyzer; + const indexLine = extAnalyzer.indexManager?.getCidForFile( filePath, fileStats.size, fileStats.mtime.toISOString() @@ -331,7 +328,7 @@ export class WatchedFileProcessor implements FileProcessorInterface { performanceMetrics.recordCacheMiss('midhash256'); // Store in cache for future lookups - (this.fileProcessor as any).indexManager?.addFileCid( + extAnalyzer.indexManager?.addFileCid( filePath, fileStats.size, fileStats.mtime.toISOString(), @@ -570,7 +567,7 @@ export class WatchedFileProcessor implements FileProcessorInterface { for (const metadata of metadataList) { if (metadata && metadata.filePath) { // Mark as unverified - discovery will validate later - (metadata as any)._lastVerified = 0; + (metadata as ExtendedHashMeta)._lastVerified = 0; this.fileProcessor.getDatabase().set(metadata.filePath, metadata); loadedCount++; } diff --git a/packages/meta-sort-core/src/logic/fileProcessor/FileProcessor.ts b/packages/meta-sort-core/src/logic/fileProcessor/FileProcessor.ts index 8428964..ce2def2 100644 --- a/packages/meta-sort-core/src/logic/fileProcessor/FileProcessor.ts +++ b/packages/meta-sort-core/src/logic/fileProcessor/FileProcessor.ts @@ -61,16 +61,12 @@ export class FileProcessor implements FileAnalyzerInterface{ } } - // -------------------------------------------- // Hash logic compute and get the hash - use cache etc... - // -------------------------------------------- const result = await this.fileProcessor.processFile(filePath,hash); - //const result = await this.fileProcessor.processFile(filePath,params.hash); //save the metadata to the cache or disk this.database.set(filePath, result.metadata); this.indexManager.addFileCid(filePath, stats.size, stats.mtime.toISOString(), result.metadata); - //console.log(`File [${current}/${queueSize}] ${filePath} took ${result.time}ms`); } catch (e) { console.error(`Error processing file [No Thread] ${filePath}:`, e); } diff --git a/packages/meta-sort-core/src/logic/fileProcessor/FileProcessorPiscina.ts b/packages/meta-sort-core/src/logic/fileProcessor/FileProcessorPiscina.ts index c8d2c63..c0331bc 100644 --- a/packages/meta-sort-core/src/logic/fileProcessor/FileProcessorPiscina.ts +++ b/packages/meta-sort-core/src/logic/fileProcessor/FileProcessorPiscina.ts @@ -95,7 +95,6 @@ export class FileProcessorPiscina implements FileAnalyzerInterface{ this.piscina = new Piscina({ maxThreads, - //maxThreads: 1,//TODO note for DEBUG must be set to 1 to avoid memory issues filename:workerUrl }); diff --git a/packages/meta-sort-core/src/plugin-engine/PluginContext.ts b/packages/meta-sort-core/src/plugin-engine/PluginContext.ts index 587a26e..cb1159d 100644 --- a/packages/meta-sort-core/src/plugin-engine/PluginContext.ts +++ b/packages/meta-sort-core/src/plugin-engine/PluginContext.ts @@ -301,7 +301,11 @@ export class PluginCacheImpl implements PluginCache { try { const files = await fs.readdir(this.cacheDir); await Promise.all( - files.map(file => fs.unlink(path.join(this.cacheDir, file)).catch(() => {})) + files.map(file => + fs.unlink(path.join(this.cacheDir, file)).catch(err => + console.warn('[PluginCache] Failed to delete cache file:', file, err instanceof Error ? err.message : err) + ) + ) ); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { diff --git a/packages/meta-sort-core/src/types/ExtendedInterfaces.ts b/packages/meta-sort-core/src/types/ExtendedInterfaces.ts new file mode 100644 index 0000000..5126c5a --- /dev/null +++ b/packages/meta-sort-core/src/types/ExtendedInterfaces.ts @@ -0,0 +1,227 @@ +/** + * Extended Interfaces + * + * Type definitions that extend base interfaces with runtime-specific properties + * that are not part of the core interface contracts. These extensions provide + * type safety for implementation-specific features. + */ + +import type { HashMeta } from '@metazla/meta-interface'; +import type { IKVClient } from '../kv/IKVClient.js'; +import type { FileAnalyzerInterface } from '../logic/fileProcessor/FileAnalyzerInterface.js'; +import type { ContainerTask } from '../container-plugins/types.js'; +import type { UnifiedProcessingSnapshot } from '../logic/UnifiedProcessingStateManager.js'; + +// ============================================================================= +// IKVClient Extensions +// ============================================================================= + +/** + * IKVClient with Redis pub/sub capabilities + * + * The base IKVClient interface doesn't include pub/sub methods since they're + * Redis-specific. This extension provides type safety for Redis-based implementations. + */ +export interface IKVClientWithPubSub extends IKVClient { + /** + * Publish a message to a Redis channel + */ + publish(channel: string, message: string): Promise; + + /** + * Get the underlying Redis client (implementation-specific) + */ + getRedisClient?(): unknown; +} + +/** + * Type guard to check if a KVClient supports pub/sub + */ +export function hasPublish(client: IKVClient): client is IKVClientWithPubSub { + return typeof (client as IKVClientWithPubSub).publish === 'function'; +} + +// ============================================================================= +// HashMeta Extensions +// ============================================================================= + +/** + * Extended HashMeta with runtime fields added during processing + * + * These fields are added by the processing pipeline but aren't part of + * the core HashMeta interface defined in meta-interface. + */ +export interface ExtendedHashMeta extends HashMeta { + /** File size in bytes (string from plugins, number internally) */ + sizeByte?: string | number; + + /** Current processing status */ + processingStatus?: 'pending' | 'processing' | 'complete' | 'failed'; + + /** Source file path */ + filePath?: string; + + /** Last verification timestamp (0 = unverified, used during VFS rebuild) */ + _lastVerified?: number; +} + +// ============================================================================= +// FileAnalyzerInterface Extensions +// ============================================================================= + +/** + * Index manager interface for file CID caching + */ +export interface IndexManager { + /** + * Get cached CID for a file based on path, size, and mtime + */ + getCidForFile( + path: string, + size: number, + mtime: string + ): { cid_midhash256?: string } | undefined; + + /** + * Add a file CID to the cache + */ + addFileCid( + path: string, + size: number, + mtime: string, + cid: { cid_midhash256: string } + ): void; +} + +/** + * Extended FileAnalyzerInterface with index manager + * + * FileProcessorPiscina exposes an indexManager property for CID caching + * that isn't part of the base interface. + */ +export interface ExtendedFileAnalyzer extends FileAnalyzerInterface { + /** Index manager for CID caching */ + indexManager?: IndexManager; +} + +// ============================================================================= +// ContainerTask Extensions +// ============================================================================= + +/** + * Extended ContainerTask with runtime metadata + * + * During task dispatch, existing metadata is attached to the task for + * the plugin to access. + */ +export interface ExtendedContainerTask extends ContainerTask { + /** Existing metadata at time of dispatch */ + existingMeta?: Record; +} + +// ============================================================================= +// UnifiedProcessingSnapshot Extensions +// ============================================================================= + +/** + * Queue status structure from getQueueStatus() + */ +export interface QueueStatus { + preProcessQueue: { + size: number; + pending: number; + isPaused: boolean; + }; + fastQueue: { + size: number; + pending: number; + running: number; + isPaused: boolean; + }; + backgroundQueue: { + size: number; + pending: number; + running: number; + isPaused: boolean; + }; +} + +/** + * Computed processing status fields for UI + */ +export interface ComputedProcessingStatus { + preProcessRunning: number; + preProcessPending: number; + preProcessPaused: boolean; + + fastQueueRunning: number; + fastQueuePending: number; + fastQueueTotal: number; + fastQueuePaused: boolean; + + backgroundQueueRunning: number; + backgroundQueuePending: number; + backgroundQueueTotal: number; + backgroundQueuePaused: boolean; + + gateOpen: boolean; + gateStatus: unknown; + pipelinePaused: boolean; + + // Legacy compatibility fields + actualRunningHashWorkers: number; + actualRunningLightWorkers: number; + trueHashQueueSize: number; + hashQueuePaused: boolean; + lightQueuePaused: boolean; +} + +/** + * Extended processing snapshot with queue status and computed fields + * + * The API adds queue status and computed fields to the base snapshot + * for UI consumption. + */ +export interface ExtendedProcessingSnapshot extends UnifiedProcessingSnapshot { + /** Queue status from pipeline */ + queueStatus?: QueueStatus; + + /** Computed status fields for UI */ + computed?: ComputedProcessingStatus; + + /** Total number of failed files */ + totalFailed?: number; +} + +// ============================================================================= +// Error Type Guards +// ============================================================================= + +/** + * Type guard for Node.js filesystem errors + * + * Use this instead of `(error as any).code` to safely check error codes. + */ +export function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error; +} + +/** + * Get error code if present, undefined otherwise + */ +export function getErrorCode(error: unknown): string | undefined { + if (isNodeError(error)) { + return error.code; + } + return undefined; +} + +/** + * Get error message safely from unknown error type + */ +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} diff --git a/packages/meta-sort-ui/src/pages/Welcome.tsx b/packages/meta-sort-ui/src/pages/Welcome.tsx index 7b5685c..70e7c8a 100644 --- a/packages/meta-sort-ui/src/pages/Welcome.tsx +++ b/packages/meta-sort-ui/src/pages/Welcome.tsx @@ -44,17 +44,6 @@ function Welcome() {

Plugins

Manage metadata plugins

- - -
- - - - -
-

Editor

-

Edit file metadata

-
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78ca2df..7102e64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,34 +132,6 @@ importers: specifier: ^5.3.3 version: 5.9.3 - packages/meta-sort-editor: - dependencies: - iso-639-3: - specifier: ^3.0.1 - version: 3.0.1 - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) - devDependencies: - '@types/react': - specifier: ^18.2.48 - version: 18.3.27 - '@types/react-dom': - specifier: ^18.2.18 - version: 18.3.7(@types/react@18.3.27) - '@vitejs/plugin-react': - specifier: ^4.2.1 - version: 4.7.0(vite@5.4.21(@types/node@20.19.27)) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vite: - specifier: ^5.0.12 - version: 5.4.21(@types/node@20.19.27) - packages/meta-sort-ui: dependencies: react: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6d3bc7a..f82e59b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,3 @@ packages: - 'packages/async-utils' - 'packages/meta-sort-core' - 'packages/meta-sort-ui' - - 'packages/meta-sort-editor'