code and doc cleanup

This commit is contained in:
Pierre
2026-01-31 19:48:34 +01:00
parent c7abc09509
commit f220972cb2
18 changed files with 268 additions and 273 deletions
+3 -21
View File
@@ -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/
+1 -5
View File
@@ -361,8 +361,7 @@ async function discoverService(name: string): Promise<ServiceInfo | null> {
│ │ ├── 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
│ │
-12
View File
@@ -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;
@@ -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<string> } | undefined;
let memoryUsage = 'N/A';
let memoryUsageBytes = 0;
@@ -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++;
}
}
+2 -155
View File
@@ -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)
// =========================================================================
@@ -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<void> {
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);
}
@@ -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 {
@@ -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;
@@ -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
*/
@@ -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<void> {
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++;
}
@@ -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);
}
@@ -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
});
@@ -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') {
@@ -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<void>;
/**
* 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<string, string>;
}
// =============================================================================
// 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);
}
@@ -44,17 +44,6 @@ function Welcome() {
<h3>Plugins</h3>
<p>Manage metadata plugins</p>
</Link>
<a href="/editor/" className="link-card">
<div className="link-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</div>
<h3>Editor</h3>
<p>Edit file metadata</p>
</a>
</div>
</div>
-28
View File
@@ -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([email protected])
devDependencies:
'@types/react':
specifier: ^18.2.48
version: 18.3.27
'@types/react-dom':
specifier: ^18.2.18
version: 18.3.7(@types/[email protected])
'@vitejs/plugin-react':
specifier: ^4.2.1
version: 4.7.0([email protected](@types/[email protected]))
typescript:
specifier: ^5.3.3
version: 5.9.3
vite:
specifier: ^5.0.12
version: 5.4.21(@types/[email protected])
packages/meta-sort-ui:
dependencies:
react:
-1
View File
@@ -3,4 +3,3 @@ packages:
- 'packages/async-utils'
- 'packages/meta-sort-core'
- 'packages/meta-sort-ui'
- 'packages/meta-sort-editor'