From f8a5b3044599bc1fb777fa07a81a948ec42d5496 Mon Sep 17 00:00:00 2001 From: hmjn Date: Thu, 9 Jul 2026 00:04:33 +0900 Subject: [PATCH 1/3] fix(server): enable http2 dev server --- apps/server/package.json | 1 + apps/server/vite.config.ts | 251 ++++++++++++++++++++++++------------- bun.lock | 11 +- 3 files changed, 173 insertions(+), 90 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index f680b50de..d7b291abc 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -99,6 +99,7 @@ "typedoc": "^0.28.19", "typescript": "rc", "ultracite": "7.8.3", + "vite-plugin-mkcert": "^2.1.0", "vite-plugin-solid": "^2.11.12", "vite-plus": "^0.1.24", "vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.24" diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index ea9a934f9..e82f43fdd 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -1,104 +1,177 @@ import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { RPCHandler } from "@orpc/server/node"; import { defineConfig } from "vite-plus"; +import type { Plugin } from "vite-plus"; import tailwindcss from "@tailwindcss/vite"; import { tanstackStart } from "@tanstack/solid-start/plugin/vite"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; import solidPlugin from "vite-plugin-solid"; import { nitro } from "nitro/vite"; import { devtools } from "@tanstack/devtools-vite"; +import mkcert from "vite-plugin-mkcert"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const bypassSecFetchDestPlugin = () => ({ - name: "bypass-sec-fetch-dest", - configureServer(server: any) { - server.middlewares.use((req: any, _res: any, next: any) => { - if (req.url?.startsWith("/api/")) { - if (req.headers["sec-fetch-dest"] === "image") { - req.headers["x-orig-sec-fetch-dest"] = req.headers["sec-fetch-dest"]; - delete req.headers["sec-fetch-dest"]; - } - } - next(); - }); - }, +type RuntimeImport = (specifier: string) => Promise; + +// Keep these imports out of Vite config bundling; the server modules rely on Bun's runtime TS path resolution. +const runtimeImport = new Function( + "specifier", + "return import(specifier)", +) as RuntimeImport; + +function serverModuleUrl(relativePath: string): string { + return pathToFileURL(path.resolve(__dirname, relativePath)).href; +} + +const bypassSecFetchDestPlugin = (): Plugin => ({ + name: "bypass-sec-fetch-dest", + configureServer(server) { + server.middlewares.use((req, _res, next) => { + if (req.url?.startsWith("/api/")) { + if (req.headers["sec-fetch-dest"] === "image") { + req.headers["x-orig-sec-fetch-dest"] = req.headers["sec-fetch-dest"]; + delete req.headers["sec-fetch-dest"]; + } + } + next(); + }); + }, +}); + +const devOrpcNodeMiddlewarePlugin = (): Plugin => ({ + name: "dev-orpc-node-middleware", + apply: "serve", + async configureServer(server) { + const [{ appRouter }, { bootstrap }, { logger }] = await Promise.all([ + runtimeImport( + serverModuleUrl("src/domain/shared/api-contract.ts"), + ), + runtimeImport( + serverModuleUrl("src/infrastructure/bootstrap.ts"), + ), + runtimeImport( + serverModuleUrl("src/infrastructure/logger.ts"), + ), + ]); + const handler = new RPCHandler(appRouter); + + server.middlewares.use(async (req, res, next) => { + if (!req.url?.startsWith("/api/rpc")) { + next(); + return; + } + + try { + bootstrap(); + const { matched } = await handler.handle(req, res, { + prefix: "/api/rpc", + context: {}, + }); + + if (!matched) { + next(); + } + } catch (error) { + logger.error({ err: error }, "Dev RPC middleware failed"); + next(error); + } + }); + }, }); export default defineConfig({ - resolve: { - alias: { - "@solid-imager/core": path.resolve(__dirname, "../../packages/core/src"), - "@": path.resolve(__dirname, "../../packages/core/src"), - "~": path.resolve(__dirname, "./src"), - }, - tsconfigPaths: true, - }, - plugins: [ - bypassSecFetchDestPlugin(), - devtools(), - nitro(), - tanstackRouter({ - target: "solid", - autoCodeSplitting: true, - routeFileIgnorePattern: ".*/components/.*", - }), - tailwindcss(), - tanstackStart(), - solidPlugin({ ssr: true }), - ], - optimizeDeps: { - exclude: ["bun", "dghs-imgutils-rs", "@lancedb/lancedb"], - }, - customLogger: { - warn(msg, options) { - if (typeof msg === "string" && msg.includes("externalized for browser compatibility")) return; - console.warn(msg, options); - }, - warnOnce(msg, options) { - this.warn(msg, options); - }, - info: console.info, - error: console.error, - clearScreen: () => {}, - hasWarned: false, - }, - build: { - rollupOptions: { - external: [ - "bun", - "dghs-imgutils-rs", - "ffmpeg-static", - "fluent-ffmpeg", - "@electric-sql/pglite", - "archiver", - "@lancedb/lancedb", - "apache-arrow", - "sharp", - ], - }, - }, - ssr: { - noExternal: [ - "@tanstack/solid-router", - "@tanstack/solid-query", - "@tanstack/solid-start", - "@kobalte/core", - "solid-sonner", - "corvu", - "@solid-primitives/.*", - ], - external: [ - "bun", - "@electric-sql/pglite", - "ffmpeg-static", - "ffmpeg-static-static", - "fluent-ffmpeg", - "archiver", - "@lancedb/lancedb", - "apache-arrow", - "dghs-imgutils-rs", - "sharp", - ], - }, + server: { + hmr: { + protocol: "wss", + host: "localhost", + port: 3001, + clientPort: 3001, + }, + }, + resolve: { + alias: { + "@solid-imager/core": path.resolve(__dirname, "../../packages/core/src"), + "@": path.resolve(__dirname, "../../packages/core/src"), + "~": path.resolve(__dirname, "./src"), + }, + tsconfigPaths: true, + }, + plugins: [ + mkcert(), + bypassSecFetchDestPlugin(), + devOrpcNodeMiddlewarePlugin(), + devtools({ + consolePiping: { enabled: false }, + }), + nitro(), + tanstackRouter({ + target: "solid", + autoCodeSplitting: true, + routeFileIgnorePattern: ".*/components/.*", + }), + tailwindcss(), + tanstackStart(), + solidPlugin({ ssr: true }), + ], + optimizeDeps: { + exclude: ["bun", "dghs-imgutils-rs", "@lancedb/lancedb"], + }, + customLogger: { + warn(msg, options) { + if ( + typeof msg === "string" && + msg.includes("externalized for browser compatibility") + ) { + return; + } + console.warn(msg, options); + }, + warnOnce(msg, options) { + this.warn(msg, options); + }, + info: console.info, + error: console.error, + clearScreen: () => {}, + hasWarned: false, + }, + build: { + rollupOptions: { + external: [ + "bun", + "dghs-imgutils-rs", + "ffmpeg-static", + "fluent-ffmpeg", + "@electric-sql/pglite", + "archiver", + "@lancedb/lancedb", + "apache-arrow", + "sharp", + ], + }, + }, + ssr: { + noExternal: [ + "@tanstack/solid-router", + "@tanstack/solid-query", + "@tanstack/solid-start", + "@kobalte/core", + "solid-sonner", + "corvu", + "@solid-primitives/.*", + ], + external: [ + "bun", + "@electric-sql/pglite", + "ffmpeg-static", + "ffmpeg-static-static", + "fluent-ffmpeg", + "archiver", + "@lancedb/lancedb", + "apache-arrow", + "dghs-imgutils-rs", + "sharp", + ], + }, }); diff --git a/bun.lock b/bun.lock index 7df4a6fa2..10bc3b31d 100644 --- a/bun.lock +++ b/bun.lock @@ -105,6 +105,7 @@ "typedoc": "^0.28.19", "typescript": "rc", "ultracite": "7.8.3", + "vite-plugin-mkcert": "^2.1.0", "vite-plugin-solid": "^2.11.12", "vite-plus": "^0.1.24", "vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.24", @@ -1723,7 +1724,7 @@ "super-regex": ["super-regex@1.1.0", "", { "dependencies": { "function-timeout": "^1.0.1", "make-asynchronous": "^1.0.1", "time-span": "^5.1.0" } }, "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "swagger-jsdoc": ["swagger-jsdoc@6.3.0", "", { "dependencies": { "@apidevtools/swagger-parser": "^12.1.0", "commander": "6.2.0", "doctrine": "3.0.0", "glob": "11.1.0", "lodash.mergewith": "^4.6.2", "yaml": "2.0.0-1" }, "bin": { "swagger-jsdoc": "bin/swagger-jsdoc.js" } }, "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA=="], @@ -1799,6 +1800,8 @@ "ultracite": ["ultracite@7.8.3", "", { "dependencies": { "@clack/prompts": "^1.5.1", "commander": "^15.0.0", "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.6", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.0.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-Fsj9aYJfh57uDB6DrdKVafKhF0QNekwqTrZKoTMYePjZp1npyFCnfZVj2SnPZdOcxAGHJfmLYUIDq0YPh8acVA=="], + "undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -1821,6 +1824,8 @@ "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], + "vite-plugin-mkcert": ["vite-plugin-mkcert@2.1.0", "", { "dependencies": { "debug": "^4.4.3", "supports-color": "^10.2.2", "undici": "^8.3.0" }, "peerDependencies": { "vite": ">=3" } }, "sha512-UQS5iyknruwCsvqDPsKRyHIVSVfv/iGdRPmUPaM1JRwNJo8wo/MubzJ66ckWwOLxFU4wW90oZq9GviV87nRHsg=="], + "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], "vite-plus": ["vite-plus@0.1.24", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@oxlint/plugins": "=1.61.0", "@voidzero-dev/vite-plus-core": "0.1.24", "@voidzero-dev/vite-plus-test": "0.1.24", "oxfmt": "=0.52.0", "oxlint": "=1.67.0", "oxlint-tsgolint": "=0.23.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.1.24", "@voidzero-dev/vite-plus-darwin-x64": "0.1.24", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.24", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.24", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.24", "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.24", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.24", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.24" }, "bin": { "vp": "bin/vp", "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint" } }, "sha512-b3fr6WtCiEhetjuzW/4KcEMOAMuZxoxZATWaXKmPzOLf1upG+pzKJOFZTb94D6wiPBlwcjxoaUtF7C3uAN+VjQ=="], @@ -1969,6 +1974,8 @@ "http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="], + "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "make-asynchronous/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -2111,6 +2118,8 @@ "bun-types/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "chalk-template/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "execa/pretty-ms/parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], From 6d6c6f1eeb89e162a4231a028dac7ae3de3b7420 Mon Sep 17 00:00:00 2001 From: hmjn Date: Thu, 9 Jul 2026 23:06:57 +0900 Subject: [PATCH 2/3] format --- .../services/media-source-service.ts | 7 +++++-- .../src/infrastructure/ai/rust-ai-client.ts | 14 ++++++++----- .../jobs/file-watcher-manager.ts | 17 ++++++++++++---- .../services/maintenance-service.test.ts | 20 ++++++++++++------- .../infrastructure/jobs/job-worker.test.ts | 4 +++- .../media-sidebar/media-sidebar-content.tsx | 2 +- packages/core/src/domain/search/logic.ts | 7 ++++++- packages/ui/src/hooks/use-manager-page.ts | 2 -- packages/ui/src/media-sidebar-content.tsx | 12 ++++++----- packages/ui/src/media-sidebar.tsx | 16 +++++++++------ 10 files changed, 67 insertions(+), 34 deletions(-) diff --git a/apps/server/src/application/services/media-source-service.ts b/apps/server/src/application/services/media-source-service.ts index f8ba6b3ac..3260bd2a9 100644 --- a/apps/server/src/application/services/media-source-service.ts +++ b/apps/server/src/application/services/media-source-service.ts @@ -3,8 +3,8 @@ import type { NewMediaSource, } from "@solid-imager/core/domain/repositories/source-repository"; import { ccipVectorService } from "~/application/services/ccip-vector-service"; -import { DrizzleSourceRepository } from "~/infrastructure/repositories/source-repository"; import { logger } from "~/infrastructure/logger"; +import { DrizzleSourceRepository } from "~/infrastructure/repositories/source-repository"; /** * Custom error class for fetch operations. @@ -64,7 +64,10 @@ const deleteSourceServer = async ( try { await ccipVectorService.deleteBySource(mediaSourceId); } catch (err) { - logger.warn({ err, mediaSourceId }, "Failed to delete CCIP vectors during source deletion"); + logger.warn( + { err, mediaSourceId }, + "Failed to delete CCIP vectors during source deletion", + ); } return source ? [source] : []; }; diff --git a/apps/server/src/infrastructure/ai/rust-ai-client.ts b/apps/server/src/infrastructure/ai/rust-ai-client.ts index 2358c1905..ea8ffcaf1 100644 --- a/apps/server/src/infrastructure/ai/rust-ai-client.ts +++ b/apps/server/src/infrastructure/ai/rust-ai-client.ts @@ -2,7 +2,6 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { createClient } from "@solid-imager/client"; import type { IAiClient } from "@solid-imager/core/domain/interfaces/ai-client"; -import { asyncPool } from "@solid-imager/core/utils/async-pool"; import { type CcipDifferenceResponse, type CcipFeatureResponse, @@ -11,6 +10,7 @@ import { type TaggingResponse, taggingResponseSchema, } from "@solid-imager/core/domain/tagging/schemas"; +import { asyncPool } from "@solid-imager/core/utils/async-pool"; import type { appRouter } from "~/domain/shared/api-contract"; function createRemoteOrpcClient(remoteUrl: string, timeoutMs: number) { @@ -279,10 +279,14 @@ export class RustAiClient implements IAiClient { return await nativeModule.ccipDistances(feature, candidates); } } - const settledResults = await asyncPool(candidates, 50, async (candidate) => { - const result = await this.calculateCcipDifference(feature, candidate); - return result.difference; - }); + const settledResults = await asyncPool( + candidates, + 50, + async (candidate) => { + const result = await this.calculateCcipDifference(feature, candidate); + return result.difference; + }, + ); return settledResults.map((r) => { if (r.status === "fulfilled") return r.value; throw r.reason; diff --git a/apps/server/src/infrastructure/jobs/file-watcher-manager.ts b/apps/server/src/infrastructure/jobs/file-watcher-manager.ts index 4883dfd0c..6e769fff2 100644 --- a/apps/server/src/infrastructure/jobs/file-watcher-manager.ts +++ b/apps/server/src/infrastructure/jobs/file-watcher-manager.ts @@ -1,7 +1,7 @@ import path from "node:path"; import chokidar, { type FSWatcher } from "chokidar"; -import { logger } from "~/infrastructure/logger"; import { RealtimeEventBus } from "~/infrastructure/events/realtime-event-bus"; +import { logger } from "~/infrastructure/logger"; const IGNORE_DOTFILES_REGEX = /(^|[/\\])\../; @@ -25,7 +25,10 @@ if (!globalWatchers.__FILE_WATCHERS_CLEANUP_REGISTERED__) { try { await entry.watcher.close(); } catch (err) { - logger.error({ err, path: entry.path }, "Failed to close watcher during cleanup"); + logger.error( + { err, path: entry.path }, + "Failed to close watcher during cleanup", + ); } }), ); @@ -74,7 +77,10 @@ export const FileWatcherManager = { watcher.on("unlink", (filePath) => run(callbacks.onDelete, filePath)); watcher.on("change", (filePath) => run(callbacks.onChange, filePath)); watcher.on("error", (error) => { - const isUuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(mediaSourceId); + const isUuid = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( + mediaSourceId, + ); try { RealtimeEventBus.publishSource(mediaSourceId, "watcher-error", { mediaSourceId: isUuid ? mediaSourceId : undefined, @@ -100,7 +106,10 @@ export const FileWatcherManager = { try { await entry.watcher.close(); } catch (err) { - logger.error({ err, mediaSourceId }, "Failed to close watcher in stop method"); + logger.error( + { err, mediaSourceId }, + "Failed to close watcher in stop method", + ); } watchers.delete(mediaSourceId); }, diff --git a/apps/server/src/tests/unit/application/services/maintenance-service.test.ts b/apps/server/src/tests/unit/application/services/maintenance-service.test.ts index 56695aa7f..bb34179a4 100644 --- a/apps/server/src/tests/unit/application/services/maintenance-service.test.ts +++ b/apps/server/src/tests/unit/application/services/maintenance-service.test.ts @@ -40,13 +40,19 @@ vi.mock("~/application/registry", () => ({ // Mock lancedb-dump-service const mockReadMediaIds = vi.fn(); -vi.mock("~/application/services/lancedb-dump-service", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - readMediaIds: mockReadMediaIds, - }; -}); +vi.mock( + "~/application/services/lancedb-dump-service", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("~/application/services/lancedb-dump-service") + >(); + return { + ...actual, + readMediaIds: mockReadMediaIds, + }; + }, +); // Mock backup-service const mockQueueSourceLanceDBDelta = vi.fn(); diff --git a/apps/server/src/tests/unit/infrastructure/jobs/job-worker.test.ts b/apps/server/src/tests/unit/infrastructure/jobs/job-worker.test.ts index 6fdf3b77e..4726667a5 100644 --- a/apps/server/src/tests/unit/infrastructure/jobs/job-worker.test.ts +++ b/apps/server/src/tests/unit/infrastructure/jobs/job-worker.test.ts @@ -213,7 +213,9 @@ describe("JobWorker", () => { status: "pending", } as Job; - processor = vi.fn().mockResolvedValue({ success: true, parentProcessed: true }); + processor = vi + .fn() + .mockResolvedValue({ success: true, parentProcessed: true }); worker = new JobWorker(jobRepo, processor); worker.updateConfig({ jobs: { concurrency: 1, aiConcurrency: 1, pollIntervalMs: 1000 }, diff --git a/apps/tauri/src/components/media/media-sidebar/media-sidebar-content.tsx b/apps/tauri/src/components/media/media-sidebar/media-sidebar-content.tsx index d995c4f22..5b38ffd70 100644 --- a/apps/tauri/src/components/media/media-sidebar/media-sidebar-content.tsx +++ b/apps/tauri/src/components/media/media-sidebar/media-sidebar-content.tsx @@ -2,6 +2,7 @@ import type { MediaDetails } from "@solid-imager/core/domain/media/schemas"; import { MediaSidebarContent } from "@solid-imager/ui/media-sidebar-content"; import { activateVectorSearch } from "@solid-imager/ui/stores/search-store"; import { useNavigate } from "@tanstack/solid-router"; +import { useBatchJobEvents } from "~/hooks/use-batch-job-events"; import { addCharacterToMedia, createCharacter, @@ -18,7 +19,6 @@ import { createProject, removeProjectFromMedia, } from "~/infrastructure/api-clients/projects-api"; -import { useBatchJobEvents } from "~/hooks/use-batch-job-events"; import { buildMediaContentUrl } from "~/infrastructure/media/thumbnail-runtime"; import { getApiFetch } from "~/infrastructure/tauri-fetch-helpers"; import { client } from "~/orpc-client"; diff --git a/packages/core/src/domain/search/logic.ts b/packages/core/src/domain/search/logic.ts index 81be3d201..d9d53c25e 100644 --- a/packages/core/src/domain/search/logic.ts +++ b/packages/core/src/domain/search/logic.ts @@ -14,7 +14,12 @@ export const calculateNextModeState = ( ): Partial => { if (nextMode === "vector") { const condition = getSearchConditionFromState(currentState); - return { mode: "vector", offset: 0, scrollY: 0, advancedCondition: condition || null }; + return { + mode: "vector", + offset: 0, + scrollY: 0, + advancedCondition: condition || null, + }; } if (nextMode === "pro") { // Switching from simple to pro: populate advancedCondition from current simple filters diff --git a/packages/ui/src/hooks/use-manager-page.ts b/packages/ui/src/hooks/use-manager-page.ts index 9190732d8..79c436393 100644 --- a/packages/ui/src/hooks/use-manager-page.ts +++ b/packages/ui/src/hooks/use-manager-page.ts @@ -292,8 +292,6 @@ export function useManagerPage( } }; - - const invalidateActive = () => { const tab = activeCrudTab(activeTab()); if (tab) { diff --git a/packages/ui/src/media-sidebar-content.tsx b/packages/ui/src/media-sidebar-content.tsx index ff7182fdf..a95d995fa 100644 --- a/packages/ui/src/media-sidebar-content.tsx +++ b/packages/ui/src/media-sidebar-content.tsx @@ -2,15 +2,15 @@ import type { Character } from "@solid-imager/core/domain/characters/schemas"; import type { Ip } from "@solid-imager/core/domain/ips/schemas"; import type { MediaDetails } from "@solid-imager/core/domain/media/schemas"; import type { Project } from "@solid-imager/core/domain/projects/schemas"; -import type { - CcipVectorStatus, - StartCcipExtractionResponse, -} from "@solid-imager/core/domain/tagging/schemas"; import type { JobCompletedEvent, JobFailedEvent, JobProgressEvent, } from "@solid-imager/core/domain/sources/events"; +import type { + CcipVectorStatus, + StartCcipExtractionResponse, +} from "@solid-imager/core/domain/tagging/schemas"; import { createQuery, useQueryClient } from "@tanstack/solid-query"; import type { Accessor, JSX } from "solid-js"; import { MediaSidebar } from "./media-sidebar"; @@ -30,7 +30,9 @@ export type MediaSidebarContentProps = { onClose: () => void; }) => JSX.Element; getCcipVectorStatus?: () => Promise; - startCcipExtraction?: (force: boolean) => Promise; + startCcipExtraction?: ( + force: boolean, + ) => Promise; useCcipJobEvents?: ( activeJobId: Accessor, handlers: { diff --git a/packages/ui/src/media-sidebar.tsx b/packages/ui/src/media-sidebar.tsx index 4a8b5a05f..9ca3031c9 100644 --- a/packages/ui/src/media-sidebar.tsx +++ b/packages/ui/src/media-sidebar.tsx @@ -2,15 +2,15 @@ import type { Character } from "@solid-imager/core/domain/characters/schemas"; import type { Ip } from "@solid-imager/core/domain/ips/schemas"; import type { MediaDetails } from "@solid-imager/core/domain/media/schemas"; import type { Project } from "@solid-imager/core/domain/projects/schemas"; -import type { - CcipVectorStatus, - StartCcipExtractionResponse, -} from "@solid-imager/core/domain/tagging/schemas"; import type { JobCompletedEvent, JobFailedEvent, JobProgressEvent, } from "@solid-imager/core/domain/sources/events"; +import type { + CcipVectorStatus, + StartCcipExtractionResponse, +} from "@solid-imager/core/domain/tagging/schemas"; import { getErrorMessage } from "@solid-imager/core/utils"; import { createEffect, @@ -47,7 +47,9 @@ type MediaSidebarProps = { onClose: () => void; }) => JSX.Element; getCcipVectorStatus?: () => Promise; - startCcipExtraction?: (force: boolean) => Promise; + startCcipExtraction?: ( + force: boolean, + ) => Promise; useCcipJobEvents?: ( activeJobId: () => string | null, handlers: { @@ -90,7 +92,9 @@ export function MediaSidebar(props: MediaSidebarProps) { const [isEditingDescription, setIsEditingDescription] = createSignal(false); const [ccipStatus, setCcipStatus] = createSignal("missing"); - const [activeCcipJobId, setActiveCcipJobId] = createSignal(null); + const [activeCcipJobId, setActiveCcipJobId] = createSignal( + null, + ); const [isExtractingCcip, setIsExtractingCcip] = createSignal(false); const [ccipStatusRequestId, setCcipStatusRequestId] = createSignal(0); const [descriptionValue, setDescriptionValue] = createSignal( From 7ea9389ef667bb9510bdb215664ced15ba032822 Mon Sep 17 00:00:00 2001 From: hmjn Date: Thu, 9 Jul 2026 23:18:03 +0900 Subject: [PATCH 3/3] fix(server): tighten dev rpc route matching --- apps/server/vite.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index e82f43fdd..49b8c91f1 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -58,7 +58,8 @@ const devOrpcNodeMiddlewarePlugin = (): Plugin => ({ const handler = new RPCHandler(appRouter); server.middlewares.use(async (req, res, next) => { - if (!req.url?.startsWith("/api/rpc")) { + const pathname = new URL(req.url ?? "/", "http://localhost").pathname; + if (pathname !== "/api/rpc" && !pathname.startsWith("/api/rpc/")) { next(); return; } @@ -71,7 +72,13 @@ const devOrpcNodeMiddlewarePlugin = (): Plugin => ({ }); if (!matched) { - next(); + logger.warn( + { method: req.method, url: req.url }, + "Unmatched RPC request", + ); + res.statusCode = 404; + res.end("Not Found"); + return; } } catch (error) { logger.error({ err: error }, "Dev RPC middleware failed");