code and doc cleanup
This commit is contained in:
+3
-21
@@ -1,5 +1,5 @@
|
|||||||
# meta-sort standalone Docker image
|
# 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:
|
# Build standalone:
|
||||||
# docker build -t meta-sort .
|
# docker build -t meta-sort .
|
||||||
@@ -24,20 +24,7 @@ RUN npm install
|
|||||||
COPY packages/meta-sort-ui/ ./
|
COPY packages/meta-sort-ui/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Build Editor
|
# Stage 2: Build Backend using pnpm workspace
|
||||||
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
|
|
||||||
FROM node:21-alpine AS backend-builder
|
FROM node:21-alpine AS backend-builder
|
||||||
|
|
||||||
# Install pnpm
|
# Install pnpm
|
||||||
@@ -67,7 +54,7 @@ RUN pnpm --filter="@worph/async-utils" build || true
|
|||||||
# Build the core package
|
# Build the core package
|
||||||
RUN pnpm --filter="@meta-sort/core" build
|
RUN pnpm --filter="@meta-sort/core" build
|
||||||
|
|
||||||
# Stage 4: Runtime
|
# Stage 3: Runtime
|
||||||
FROM ubuntu:22.04
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
# Container registry metadata
|
# Container registry metadata
|
||||||
@@ -105,7 +92,6 @@ RUN curl https://rclone.org/install.sh | bash
|
|||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
/app/backend \
|
/app/backend \
|
||||||
/app/ui \
|
/app/ui \
|
||||||
/app/editor \
|
|
||||||
/data/watch \
|
/data/watch \
|
||||||
/data/cache \
|
/data/cache \
|
||||||
/data/redis \
|
/data/redis \
|
||||||
@@ -122,10 +108,6 @@ RUN mkdir -p \
|
|||||||
COPY --from=ui-builder /build/dist /app/ui
|
COPY --from=ui-builder /build/dist /app/ui
|
||||||
RUN chmod -R 755 /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 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/dist /app/backend/dist
|
||||||
COPY --from=backend-builder /build/packages/meta-sort-core/package.json /app/backend/
|
COPY --from=backend-builder /build/packages/meta-sort-core/package.json /app/backend/
|
||||||
|
|||||||
@@ -361,8 +361,7 @@ async function discoverService(name: string): Promise<ServiceInfo | null> {
|
|||||||
│ │ ├── Watches /files/* for new media files ││
|
│ │ ├── Watches /files/* for new media files ││
|
||||||
│ │ ├── Extracts metadata → writes to KV (Redis in /meta-core/db) ││
|
│ │ ├── Extracts metadata → writes to KV (Redis in /meta-core/db) ││
|
||||||
│ │ ├── Manages remote mounts INTO /files (NFS, SMB, rclone) ││
|
│ │ ├── Manages remote mounts INTO /files (NFS, SMB, rclone) ││
|
||||||
│ │ ├── Leader election (spawns Redis if leader) ││
|
│ │ └── Leader election (spawns Redis if leader) ││
|
||||||
│ │ └── Serves monitoring UI (meta-sort-editor) ││
|
|
||||||
│ │ ││
|
│ │ ││
|
||||||
│ │ [2] meta-fuse PROCESS-READ ││
|
│ │ [2] meta-fuse PROCESS-READ ││
|
||||||
│ │ ├── Reads metadata from KV (Redis) ││
|
│ │ ├── Reads metadata from KV (Redis) ││
|
||||||
@@ -665,9 +664,6 @@ meta-sort/
|
|||||||
│ │ │ └── index.ts # Entry point
|
│ │ │ └── index.ts # Entry point
|
||||||
│ │ └── package.json
|
│ │ └── package.json
|
||||||
│ │
|
│ │
|
||||||
│ ├── meta-sort-editor/ # Metadata editor UI (@meta-sort/editor)
|
|
||||||
│ │ └── ... # React/Vite app
|
|
||||||
│ │
|
|
||||||
│ ├── meta-sort-ui/ # Monitoring dashboard (@meta-sort/ui)
|
│ ├── meta-sort-ui/ # Monitoring dashboard (@meta-sort/ui)
|
||||||
│ │ └── ... # React/Vite app
|
│ │ └── ... # React/Vite app
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -44,18 +44,6 @@ http {
|
|||||||
try_files $uri $uri/ /index.html;
|
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
|
# API routes
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://api;
|
proxy_pass http://api;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { UnifiedProcessingStateManager } from '../logic/UnifiedProcessingStateMa
|
|||||||
import { DuplicateResult } from '../logic/DuplicateFinder.js';
|
import { DuplicateResult } from '../logic/DuplicateFinder.js';
|
||||||
import type { IKVClient } from '../kv/IKVClient.js';
|
import type { IKVClient } from '../kv/IKVClient.js';
|
||||||
import type { KVManager } from '../kv/KVManager.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 { config } from '../config/EnvConfig.js';
|
||||||
import type { PluginManager } from '../plugin-engine/PluginManager.js';
|
import type { PluginManager } from '../plugin-engine/PluginManager.js';
|
||||||
import type { TaskScheduler } from '../plugin-engine/TaskScheduler.js';
|
import type { TaskScheduler } from '../plugin-engine/TaskScheduler.js';
|
||||||
@@ -631,12 +633,12 @@ export class UnifiedAPIServer {
|
|||||||
// Get snapshot from state manager
|
// Get snapshot from state manager
|
||||||
// Note: VFS-related features have been moved to meta-fuse
|
// Note: VFS-related features have been moved to meta-fuse
|
||||||
// totalDone now uses state manager's count of fully processed files
|
// 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
|
// Add queue status if available
|
||||||
if (this.getQueueStatus) {
|
if (this.getQueueStatus) {
|
||||||
const queueStatus = 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)
|
// Add calculated fields that the UI needs (make API self-describing)
|
||||||
// Pipeline: validation (preProcess) → lightProcessing (fast) → hashProcessing (background)
|
// Pipeline: validation (preProcess) → lightProcessing (fast) → hashProcessing (background)
|
||||||
@@ -659,7 +661,7 @@ export class UnifiedAPIServer {
|
|||||||
// Get gate status if container scheduler is available
|
// Get gate status if container scheduler is available
|
||||||
const gateStatus = this.containerPluginScheduler?.getGateStatus();
|
const gateStatus = this.containerPluginScheduler?.getGateStatus();
|
||||||
|
|
||||||
(snapshot as any).computed = {
|
snapshot.computed = {
|
||||||
// Pre-process queue (validation - quick extension checks)
|
// Pre-process queue (validation - quick extension checks)
|
||||||
preProcessRunning,
|
preProcessRunning,
|
||||||
preProcessPending,
|
preProcessPending,
|
||||||
@@ -693,7 +695,7 @@ export class UnifiedAPIServer {
|
|||||||
|
|
||||||
// Add failed files count from metrics
|
// Add failed files count from metrics
|
||||||
const metrics = performanceMetrics.getMetrics();
|
const metrics = performanceMetrics.getMetrics();
|
||||||
(snapshot as any).totalFailed = metrics.totalFailedFiles || 0;
|
snapshot.totalFailed = metrics.totalFailedFiles || 0;
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
});
|
});
|
||||||
@@ -722,7 +724,7 @@ export class UnifiedAPIServer {
|
|||||||
// Trigger scan callback if available to pick up the file
|
// Trigger scan callback if available to pick up the file
|
||||||
if (this.triggerScanCallback) {
|
if (this.triggerScanCallback) {
|
||||||
// Don't await - just trigger async
|
// 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` };
|
return { status: 'ok', message: `File ${filePath} queued for retry` };
|
||||||
@@ -744,7 +746,7 @@ export class UnifiedAPIServer {
|
|||||||
|
|
||||||
// Trigger scan callback if available
|
// Trigger scan callback if available
|
||||||
if (this.triggerScanCallback) {
|
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` };
|
return { status: 'ok', message: `${queued} files queued for retry` };
|
||||||
@@ -838,7 +840,7 @@ export class UnifiedAPIServer {
|
|||||||
this.app.get('/api/stats', async (request, reply) => {
|
this.app.get('/api/stats', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const hashIds = await this.kvClient!.getAllHashIds();
|
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 memoryUsage = 'N/A';
|
||||||
let memoryUsageBytes = 0;
|
let memoryUsageBytes = 0;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { EventEmitter } from 'events';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { HashMeta } from '@metazla/meta-interface';
|
import { HashMeta } from '@metazla/meta-interface';
|
||||||
|
import type { ExtendedHashMeta } from '../types/ExtendedInterfaces.js';
|
||||||
import {
|
import {
|
||||||
IFuseAPI,
|
IFuseAPI,
|
||||||
VirtualDirectory,
|
VirtualDirectory,
|
||||||
@@ -149,9 +150,10 @@ export class VirtualFileSystem extends EventEmitter implements IFuseAPI {
|
|||||||
let mtime: Date;
|
let mtime: Date;
|
||||||
let ctime: 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)
|
// 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)
|
// Use current time as placeholder for mtime/ctime (will be corrected on next scan)
|
||||||
mtime = new Date();
|
mtime = new Date();
|
||||||
ctime = 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)
|
// Only count actual media files (not directories, not metadata files)
|
||||||
if (node.type === 'file' && node.sourcePath && node.metadata) {
|
if (node.type === 'file' && node.sourcePath && node.metadata) {
|
||||||
// Check if file has completed processing (both light and hash phases)
|
// 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++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class MockProcessingStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helpers for tests
|
// Helpers for tests
|
||||||
addPending(path: string): void {
|
addDiscovered(path: string): void {
|
||||||
this.pending.add(path);
|
this.pending.add(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ describe('UnifiedAPIServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mockStateManager.addDone('/files/test/Test Movie.mp4');
|
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)
|
// Create VFS (no params needed - uses defaults)
|
||||||
const vfs = new VirtualFileSystem();
|
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)
|
// 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)
|
// Plugins API (Plugins Page)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type { TaskQueueType } from '../plugin-engine/types.js';
|
import type { TaskQueueType } from '../plugin-engine/types.js';
|
||||||
import type { IKVClient } from '../kv/IKVClient.js';
|
import type { IKVClient } from '../kv/IKVClient.js';
|
||||||
|
import { hasPublish, type ExtendedContainerTask } from '../types/ExtendedInterfaces.js';
|
||||||
import { config } from '../config/EnvConfig.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.
|
* to be notified when specific plugins complete for a file.
|
||||||
*/
|
*/
|
||||||
private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise<void> {
|
private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise<void> {
|
||||||
if (!this.kvClient) {
|
if (!this.kvClient || !hasPublish(this.kvClient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +230,7 @@ export class ContainerPluginScheduler extends EventEmitter {
|
|||||||
filePath,
|
filePath,
|
||||||
timestamp: Date.now()
|
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) {
|
} catch (error) {
|
||||||
// Silent fail - pub/sub is optional
|
// Silent fail - pub/sub is optional
|
||||||
}
|
}
|
||||||
@@ -867,7 +868,7 @@ export class ContainerPluginScheduler extends EventEmitter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Store existing metadata in task for dispatch
|
// Store existing metadata in task for dispatch
|
||||||
(task as any).existingMeta = existingMeta;
|
(task as ExtendedContainerTask).existingMeta = existingMeta;
|
||||||
|
|
||||||
this.enqueueTask(task);
|
this.enqueueTask(task);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ export class JellyfinNfo {
|
|||||||
existingData[targetRoot] = root;
|
existingData[targetRoot] = root;
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
//const builder = new Builder({ headless: true, renderOpts: { 'pretty': true } });
|
|
||||||
const builder = new Builder();
|
const builder = new Builder();
|
||||||
// Convert the updated object back to XML
|
// Convert the updated object back to XML
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { promises as fs } from 'fs';
|
|||||||
import { hostname } from 'os';
|
import { hostname } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import type { ServiceInfo } from './IKVClient.js';
|
import type { ServiceInfo } from './IKVClient.js';
|
||||||
|
import { isNodeError } from '../types/ExtendedInterfaces.js';
|
||||||
|
|
||||||
interface ServiceDiscoveryConfig {
|
interface ServiceDiscoveryConfig {
|
||||||
/** Path to META_CORE_VOLUME (e.g., /meta-core) */
|
/** 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));
|
await fs.writeFile(this.serviceFilePath, JSON.stringify(info, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If file was deleted, re-register
|
// If file was deleted, re-register
|
||||||
if ((error as any).code === 'ENOENT') {
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||||
await this.register();
|
await this.register();
|
||||||
await this.updateStatus('running');
|
await this.updateStatus('running');
|
||||||
} else {
|
} else {
|
||||||
@@ -200,7 +201,7 @@ export class ServiceDiscovery {
|
|||||||
|
|
||||||
return service;
|
return service;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as any).code === 'ENOENT') {
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||||
return null; // Service not registered
|
return null; // Service not registered
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -228,7 +229,7 @@ export class ServiceDiscovery {
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as any).code === 'ENOENT') {
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||||
return []; // Services directory doesn't exist yet
|
return []; // Services directory doesn't exist yet
|
||||||
}
|
}
|
||||||
throw error;
|
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
|
* Move file from discovered to light processing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {VirtualFileSystem} from "../api/VirtualFileSystem.js";
|
|||||||
import {performanceMetrics} from "../metrics/PerformanceMetrics.js";
|
import {performanceMetrics} from "../metrics/PerformanceMetrics.js";
|
||||||
import {UnifiedProcessingStateManager} from "./UnifiedProcessingStateManager.js";
|
import {UnifiedProcessingStateManager} from "./UnifiedProcessingStateManager.js";
|
||||||
import type {IKVClient} from "../kv/IKVClient.js";
|
import type {IKVClient} from "../kv/IKVClient.js";
|
||||||
|
import { hasPublish, type ExtendedFileAnalyzer, type ExtendedHashMeta } from "../types/ExtendedInterfaces.js";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { TaskScheduler, createTaskScheduler, MetadataNodeKVStore } from "../plugin-engine/index.js";
|
import { TaskScheduler, createTaskScheduler, MetadataNodeKVStore } from "../plugin-engine/index.js";
|
||||||
@@ -29,7 +30,6 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
metaDataToFolderStruct = new MetaDataToFolderStruct(renamingRule);
|
metaDataToFolderStruct = new MetaDataToFolderStruct(renamingRule);
|
||||||
virtualFileSystem: VirtualFileSystem;
|
virtualFileSystem: VirtualFileSystem;
|
||||||
unifiedStateManager = new UnifiedProcessingStateManager();
|
unifiedStateManager = new UnifiedProcessingStateManager();
|
||||||
//fileProcessor:FileAnalyzerInterface = new FileProcessor();//TODO only use for debug
|
|
||||||
fileProcessor:FileAnalyzerInterface = new FileProcessorPiscina();
|
fileProcessor:FileAnalyzerInterface = new FileProcessorPiscina();
|
||||||
duplicateFinder = new DuplicateFinder();
|
duplicateFinder = new DuplicateFinder();
|
||||||
duplicateResult: DuplicateResult | null = null;
|
duplicateResult: DuplicateResult | null = null;
|
||||||
@@ -249,7 +249,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
*/
|
*/
|
||||||
private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise<void> {
|
private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise<void> {
|
||||||
const kvClient = this.getKVClient();
|
const kvClient = this.getKVClient();
|
||||||
if (!kvClient || typeof (kvClient as any).publish !== 'function') {
|
if (!kvClient || !hasPublish(kvClient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
filePath,
|
filePath,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
await (kvClient as any).publish('meta-sort:plugin:complete', message);
|
await kvClient.publish('meta-sort:plugin:complete', message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silent fail - pub/sub is optional
|
// Silent fail - pub/sub is optional
|
||||||
}
|
}
|
||||||
@@ -298,11 +298,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
// Phase 1: Pre-process (midhash256 computation)
|
// Phase 1: Pre-process (midhash256 computation)
|
||||||
// Phase 2: Plugin execution via TaskScheduler (fast/background queues)
|
// 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 () => {
|
await this.preProcessQueue.add(async () => {
|
||||||
console.log(`[DEBUG queueFile] Task started: size=${this.preProcessQueue.size}, pending=${this.preProcessQueue.pending}`);
|
|
||||||
try {
|
try {
|
||||||
// STATE: Start LIGHT processing
|
// STATE: Start LIGHT processing
|
||||||
if (this.unifiedStateManager) {
|
if (this.unifiedStateManager) {
|
||||||
@@ -314,7 +310,8 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
const { computeMidHash256 } = await import('@metazla/meta-hash');
|
const { computeMidHash256 } = await import('@metazla/meta-hash');
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const indexLine = (this.fileProcessor as any).indexManager?.getCidForFile(
|
const extAnalyzer = this.fileProcessor as ExtendedFileAnalyzer;
|
||||||
|
const indexLine = extAnalyzer.indexManager?.getCidForFile(
|
||||||
filePath,
|
filePath,
|
||||||
fileStats.size,
|
fileStats.size,
|
||||||
fileStats.mtime.toISOString()
|
fileStats.mtime.toISOString()
|
||||||
@@ -331,7 +328,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
performanceMetrics.recordCacheMiss('midhash256');
|
performanceMetrics.recordCacheMiss('midhash256');
|
||||||
|
|
||||||
// Store in cache for future lookups
|
// Store in cache for future lookups
|
||||||
(this.fileProcessor as any).indexManager?.addFileCid(
|
extAnalyzer.indexManager?.addFileCid(
|
||||||
filePath,
|
filePath,
|
||||||
fileStats.size,
|
fileStats.size,
|
||||||
fileStats.mtime.toISOString(),
|
fileStats.mtime.toISOString(),
|
||||||
@@ -570,7 +567,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
|||||||
for (const metadata of metadataList) {
|
for (const metadata of metadataList) {
|
||||||
if (metadata && metadata.filePath) {
|
if (metadata && metadata.filePath) {
|
||||||
// Mark as unverified - discovery will validate later
|
// Mark as unverified - discovery will validate later
|
||||||
(metadata as any)._lastVerified = 0;
|
(metadata as ExtendedHashMeta)._lastVerified = 0;
|
||||||
this.fileProcessor.getDatabase().set(metadata.filePath, metadata);
|
this.fileProcessor.getDatabase().set(metadata.filePath, metadata);
|
||||||
loadedCount++;
|
loadedCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,16 +61,12 @@ export class FileProcessor implements FileAnalyzerInterface{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------
|
|
||||||
// Hash logic compute and get the hash - use cache etc...
|
// 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,hash);
|
||||||
//const result = await this.fileProcessor.processFile(filePath,params.hash);
|
|
||||||
|
|
||||||
//save the metadata to the cache or disk
|
//save the metadata to the cache or disk
|
||||||
this.database.set(filePath, result.metadata);
|
this.database.set(filePath, result.metadata);
|
||||||
this.indexManager.addFileCid(filePath, stats.size, stats.mtime.toISOString(), 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) {
|
} catch (e) {
|
||||||
console.error(`Error processing file [No Thread] ${filePath}:`, e);
|
console.error(`Error processing file [No Thread] ${filePath}:`, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ export class FileProcessorPiscina implements FileAnalyzerInterface{
|
|||||||
|
|
||||||
this.piscina = new Piscina({
|
this.piscina = new Piscina({
|
||||||
maxThreads,
|
maxThreads,
|
||||||
//maxThreads: 1,//TODO note for DEBUG must be set to 1 to avoid memory issues
|
|
||||||
filename:workerUrl
|
filename:workerUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,11 @@ export class PluginCacheImpl implements PluginCache {
|
|||||||
try {
|
try {
|
||||||
const files = await fs.readdir(this.cacheDir);
|
const files = await fs.readdir(this.cacheDir);
|
||||||
await Promise.all(
|
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) {
|
} catch (error) {
|
||||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
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>
|
<h3>Plugins</h3>
|
||||||
<p>Manage metadata plugins</p>
|
<p>Manage metadata plugins</p>
|
||||||
</Link>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Generated
-28
@@ -132,34 +132,6 @@ importers:
|
|||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.9.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:
|
packages/meta-sort-ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
react:
|
react:
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ packages:
|
|||||||
- 'packages/async-utils'
|
- 'packages/async-utils'
|
||||||
- 'packages/meta-sort-core'
|
- 'packages/meta-sort-core'
|
||||||
- 'packages/meta-sort-ui'
|
- 'packages/meta-sort-ui'
|
||||||
- 'packages/meta-sort-editor'
|
|
||||||
|
|||||||
Reference in New Issue
Block a user