chore(LINT-0001) fix lint errors

This commit is contained in:
Giancarmine Salucci
2026-03-27 03:01:37 +01:00
parent 7f7d806172
commit da661efc91
24 changed files with 114 additions and 69 deletions

View File

@@ -119,3 +119,49 @@ Add subsequent research below this section.
- Risks / follow-ups: - Risks / follow-ups:
- Over-sanitizing punctuation-heavy inputs could erase useful identifiers, so the implementation should preserve searchable alphanumeric and underscore tokens while discarding grammar-breaking punctuation. - Over-sanitizing punctuation-heavy inputs could erase useful identifiers, so the implementation should preserve searchable alphanumeric and underscore tokens while discarding grammar-breaking punctuation.
- Prefix expansion should remain on the final searchable token only so the fix preserves current query-cost expectations and test semantics. - Prefix expansion should remain on the final searchable token only so the fix preserves current query-cost expectations and test semantics.
### 2026-03-27 — LINT-0001 planning research
- Task: Plan the lint-fix iteration covering the reported ESLint and eslint-plugin-svelte violations across Svelte UI, SvelteKit routes, server modules, and Vitest suites.
- Files inspected:
- `package.json`
- `eslint.config.js`
- `docs/FINDINGS.md`
- `prompts/LINT-0001/prompt.yaml`
- `prompts/LINT-0001/progress.yaml`
- `src/lib/components/FolderPicker.svelte`
- `src/lib/components/RepositoryCard.svelte`
- `src/lib/components/search/SnippetCard.svelte`
- `src/lib/server/crawler/local.crawler.test.ts`
- `src/lib/server/embeddings/embedding.service.test.ts`
- `src/lib/server/embeddings/local.provider.ts`
- `src/lib/server/embeddings/provider.ts`
- `src/lib/server/embeddings/registry.ts`
- `src/lib/server/models/context-response.ts`
- `src/lib/server/parser/code.parser.ts`
- `src/lib/server/pipeline/indexing.pipeline.ts`
- `src/lib/server/search/hybrid.search.service.test.ts`
- `src/lib/server/search/query-preprocessor.ts`
- `src/lib/server/services/repository.service.test.ts`
- `src/lib/server/services/version.service.test.ts`
- `src/lib/server/services/version.service.ts`
- `src/routes/+layout.svelte`
- `src/routes/+page.svelte`
- `src/routes/api/v1/libs/search/+server.ts`
- `src/routes/api/v1/settings/embedding/+server.ts`
- `src/routes/repos/[id]/+page.svelte`
- `src/routes/search/+page.svelte`
- `src/routes/settings/+page.svelte`
- Findings:
- The project lint stack is ESLint `^9.39.2` with `typescript-eslint` recommended rules and `eslint-plugin-svelte` recommended plus SvelteKit-aware rules, running over Svelte `^5.51.0` and SvelteKit `^2.50.2`.
- Context7 documentation for `eslint-plugin-svelte` confirms `svelte/no-navigation-without-base` flags root-relative `<a href="/...">` links and `goto('/...')` calls in SvelteKit projects; compliant fixes must use `$app/paths` base-aware links or base-prefixed `goto` calls.
- Context7 documentation for Svelte 5 confirms event handlers are regular element properties such as `onclick`, while side effects belong in `$effect`; repo memory also records that client-only fetch bootstrap should not be moved indiscriminately into `$effect` when `onMount` or load is the correct lifecycle boundary.
- Concrete navigation violations already exist in `src/routes/+layout.svelte`, `src/routes/repos/[id]/+page.svelte`, `src/routes/search/+page.svelte`, and `src/lib/components/RepositoryCard.svelte`, each using hard-coded root-relative internal navigation.
- Static diagnostics currently expose at least one direct TypeScript lint error in `src/lib/server/embeddings/registry.ts`, where `_config` is defined but never used.
- `src/routes/api/v1/libs/search/+server.ts` imports `json` from `@sveltejs/kit` without using it, making that endpoint a concrete unused-import cleanup target.
- `src/lib/server/services/version.service.ts` still uses CommonJS `require(...)` to reach git utilities from TypeScript, which is inconsistent with the repository's ESM style and is a likely lint target under the current ESLint stack.
- The affected Svelte pages and settings UI already use Svelte 5 event-property syntax, so the lint work should preserve that syntax and focus on base-aware navigation, lifecycle correctness, and unused-symbol cleanup rather than regressing to legacy `on:` directives.
- Existing automated coverage for the lint-touching backend areas already lives in `src/lib/server/crawler/local.crawler.test.ts`, `src/lib/server/embeddings/embedding.service.test.ts`, `src/lib/server/search/hybrid.search.service.test.ts`, `src/lib/server/services/repository.service.test.ts`, and `src/lib/server/services/version.service.test.ts`; route and component changes rely on build and lint validation rather than dedicated browser tests in this iteration.
- Risks / follow-ups:
- Base-aware navigation fixes must preserve internal app routing semantics and should not replace intentional external navigation, because SvelteKit `goto(...)` no longer accepts external URLs.
- Settings and search page lifecycle changes must avoid reintroducing SSR-triggered fetches or self-triggered URL loops; client-only bootstrap logic should remain mounted once and URL-sync effects must stay idempotent.

View File

@@ -86,10 +86,9 @@
</div> </div>
{#if open} {#if open}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div <div
role="presentation" role="presentation"
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4" class="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4"
onclick={handleBackdropClick} onclick={handleBackdropClick}
> >
<div <div

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve as resolveRoute } from '$app/paths';
import type { Repository } from '$lib/types'; import type { Repository } from '$lib/types';
let { let {
@@ -76,7 +77,7 @@
{repo.state === 'indexing' ? 'Indexing...' : 'Re-index'} {repo.state === 'indexing' ? 'Indexing...' : 'Re-index'}
</button> </button>
<a <a
href="/repos/{encodeURIComponent(repo.id)}" href={resolveRoute('/repos/[id]', { id: repo.id })}
class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50" class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
> >
Details Details

View File

@@ -13,8 +13,6 @@
snippet.type === 'code' ? (snippet.codeList[0]?.code ?? '') : snippet.text snippet.type === 'code' ? (snippet.codeList[0]?.code ?? '') : snippet.text
); );
const language = $derived(snippet.type === 'code' ? (snippet.codeList[0]?.language ?? '') : null);
const tokenCount = $derived(snippet.tokenCount ?? 0); const tokenCount = $derived(snippet.tokenCount ?? 0);
</script> </script>

View File

@@ -299,7 +299,6 @@ describe('LocalCrawler.crawl() — lock file and minified file exclusions', () =
'src/index.ts': 'export {};', 'src/index.ts': 'export {};',
'dist/vendor.min.js': '!function(e,t){}()' 'dist/vendor.min.js': '!function(e,t){}()'
}); });
const result = await crawlRoot();
// dist/ is pruned by default — test via shouldIndexFile logic only if .gitignore present // dist/ is pruned by default — test via shouldIndexFile logic only if .gitignore present
// Use a custom path outside ignored dirs: // Use a custom path outside ignored dirs:
await fs.rm(root, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true });

View File

@@ -266,7 +266,7 @@ describe('Migration — embedding_profiles', () => {
const { client } = createTestDb(); const { client } = createTestDb();
const row = client const row = client
.prepare("SELECT * FROM embedding_profiles WHERE id = 'local-default'") .prepare("SELECT * FROM embedding_profiles WHERE id = 'local-default'")
.get() as any; .get() as Record<string, unknown>;
expect(row).toBeDefined(); expect(row).toBeDefined();
expect(row.is_default).toBe(1); expect(row.is_default).toBe(1);
expect(row.provider_kind).toBe('local-transformers'); expect(row.provider_kind).toBe('local-transformers');

View File

@@ -9,6 +9,19 @@
import { EmbeddingError, type EmbeddingProvider, type EmbeddingVector } from './provider.js'; import { EmbeddingError, type EmbeddingProvider, type EmbeddingVector } from './provider.js';
type LocalEmbeddingResult = {
data: ArrayLike<number>;
};
type LocalEmbeddingPipeline = (
text: string,
options: Record<string, unknown>
) => Promise<LocalEmbeddingResult>;
type TransformersModule = {
pipeline: (task: string, model: string) => Promise<LocalEmbeddingPipeline>;
};
export class LocalEmbeddingProvider implements EmbeddingProvider { export class LocalEmbeddingProvider implements EmbeddingProvider {
readonly name = 'local'; readonly name = 'local';
readonly model = 'Xenova/all-MiniLM-L6-v2'; readonly model = 'Xenova/all-MiniLM-L6-v2';
@@ -20,13 +33,11 @@ export class LocalEmbeddingProvider implements EmbeddingProvider {
async embed(texts: string[]): Promise<EmbeddingVector[]> { async embed(texts: string[]): Promise<EmbeddingVector[]> {
if (!this.pipeline) { if (!this.pipeline) {
let transformers: { pipeline: Function }; let transformers: TransformersModule;
try { try {
// Dynamic import — only succeeds when @xenova/transformers is installed. // Dynamic import — only succeeds when @xenova/transformers is installed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
transformers = (await import('@xenova/transformers' as any)) as { transformers = (await import('@xenova/transformers' as any)) as TransformersModule;
pipeline: Function;
};
} catch { } catch {
throw new EmbeddingError( throw new EmbeddingError(
'@xenova/transformers is not installed. Install it to use the local embedding provider.' '@xenova/transformers is not installed. Install it to use the local embedding provider.'

View File

@@ -37,7 +37,8 @@ export class NoopEmbeddingProvider implements EmbeddingProvider {
readonly dimensions = 0; readonly dimensions = 0;
readonly model = 'none'; readonly model = 'none';
async embed(_texts: string[]): Promise<EmbeddingVector[]> { async embed(texts: string[]): Promise<EmbeddingVector[]> {
void texts;
return []; return [];
} }

View File

@@ -14,7 +14,7 @@ import type { EmbeddingProfile } from '../db/schema.js';
export type ProviderFactory = (config: Record<string, unknown>) => EmbeddingProvider; export type ProviderFactory = (config: Record<string, unknown>) => EmbeddingProvider;
const PROVIDER_REGISTRY: Record<string, ProviderFactory> = { const PROVIDER_REGISTRY: Record<string, ProviderFactory> = {
'local-transformers': (_config) => new LocalEmbeddingProvider(), 'local-transformers': () => new LocalEmbeddingProvider(),
'openai-compatible': (config) => 'openai-compatible': (config) =>
new OpenAIEmbeddingProvider({ new OpenAIEmbeddingProvider({
baseUrl: config.baseUrl as string, baseUrl: config.baseUrl as string,

View File

@@ -124,7 +124,7 @@ export class CodeListItemDto {
} }
export class CodeSnippetJsonDto { export class CodeSnippetJsonDto {
type: 'code' = 'code'; readonly type = 'code' as const;
title: string | null; title: string | null;
description: string | null; description: string | null;
language: string | null; language: string | null;
@@ -147,7 +147,7 @@ export class CodeSnippetJsonDto {
} }
export class InfoSnippetJsonDto { export class InfoSnippetJsonDto {
type: 'info' = 'info'; readonly type = 'info' as const;
text: string; text: string;
breadcrumb: string | null; breadcrumb: string | null;
pageId: string; pageId: string;

View File

@@ -32,9 +32,9 @@ export const BOUNDARY_PATTERNS: Record<string, RegExp> = {
python: /^(async\s+)?(def|class)\s+\w+/, python: /^(async\s+)?(def|class)\s+\w+/,
go: /^(func|type|var|const)\s+\w+/, go: /^(func|type|var|const)\s+\w+/,
rust: /^(pub(\s*\(crate\))?\s+)?(async\s+)?(fn|impl|struct|enum|trait|type|const|static)\s+\w+/, rust: /^(pub(\s*\(crate\))?\s+)?(async\s+)?(fn|impl|struct|enum|trait|type|const|static)\s+\w+/,
java: /^(\s*(public|private|protected|static|final|abstract|synchronized)\s+)+[\w<>\[\]]+\s+\w+\s*[({]/, java: /^(\s*(public|private|protected|static|final|abstract|synchronized)\s+)+[\w<>[\]]+\s+\w+\s*[({]/,
csharp: csharp:
/^(\s*(public|private|protected|internal|static|override|virtual|abstract|sealed)\s+)+[\w<>\[\]]+\s+\w+\s*[({]/, /^(\s*(public|private|protected|internal|static|override|virtual|abstract|sealed)\s+)+[\w<>[\]]+\s+\w+\s*[({]/,
kotlin: kotlin:
/^(\s*(public|private|protected|internal|override|suspend|inline|open|abstract|sealed)\s+)*(fun|class|object|interface|data class|sealed class|enum class)\s+\w+/, /^(\s*(public|private|protected|internal|override|suspend|inline|open|abstract|sealed)\s+)*(fun|class|object|interface|data class|sealed class|enum class)\s+\w+/,
swift: swift:
@@ -111,7 +111,7 @@ function slidingWindowChunks(content: string, filePath: string, language: string
* followed by colon/equals/brace) and treat each as a boundary. * followed by colon/equals/brace) and treat each as a boundary.
*/ */
function parseConfigFile(content: string, filePath: string, language: string): RawSnippet[] { function parseConfigFile(content: string, filePath: string, language: string): RawSnippet[] {
const topLevelKey = /^[\w"'\-]+\s*[:=\[{]/; const topLevelKey = /^[\w"'-]+\s*[:=[{]/;
const lines = content.split('\n'); const lines = content.split('\n');
const segments: string[] = []; const segments: string[] = [];
let current: string[] = []; let current: string[] = [];

View File

@@ -13,7 +13,7 @@
* - With embeddings : crawl+parse = 80 %, embeddings = 20 % * - With embeddings : crawl+parse = 80 %, embeddings = 20 %
*/ */
import { createHash } from 'node:crypto'; import { createHash, randomUUID } from 'node:crypto';
import type Database from 'better-sqlite3'; import type Database from 'better-sqlite3';
import type { Document, NewDocument, NewSnippet } from '$lib/types'; import type { Document, NewDocument, NewSnippet } from '$lib/types';
import type { crawl as GithubCrawlFn } from '$lib/server/crawler/github.crawler.js'; import type { crawl as GithubCrawlFn } from '$lib/server/crawler/github.crawler.js';
@@ -92,7 +92,7 @@ export class IndexingPipeline {
this.updateRepo(repo.id, { state: 'indexing' }); this.updateRepo(repo.id, { state: 'indexing' });
// ---- Stage 1: Crawl ------------------------------------------------- // ---- Stage 1: Crawl -------------------------------------------------
const crawlResult = await this.crawl(repo, normJob); const crawlResult = await this.crawl(repo);
const totalFiles = crawlResult.totalFiles; const totalFiles = crawlResult.totalFiles;
this.updateJob(job.id, { totalFiles }); this.updateJob(job.id, { totalFiles });
@@ -140,7 +140,7 @@ export class IndexingPipeline {
const checksum = file.sha || sha256(file.content); const checksum = file.sha || sha256(file.content);
// Create new document record. // Create new document record.
const documentId = crypto.randomUUID(); const documentId = randomUUID();
const now = new Date(); const now = new Date();
const newDoc: NewDocument = { const newDoc: NewDocument = {
id: documentId, id: documentId,
@@ -247,10 +247,7 @@ export class IndexingPipeline {
// Private — crawl // Private — crawl
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private async crawl( private async crawl(repo: Repository): Promise<{
repo: Repository,
job: IndexingJob
): Promise<{
files: Array<{ path: string; content: string; sha: string; size: number; language: string }>; files: Array<{ path: string; content: string; sha: string; size: number; language: string }>;
totalFiles: number; totalFiles: number;
}> { }> {

View File

@@ -153,7 +153,7 @@ function makeNoopProvider(): EmbeddingProvider {
name: 'noop', name: 'noop',
dimensions: 0, dimensions: 0,
model: 'none', model: 'none',
async embed(_texts: string[]): Promise<EmbeddingVector[]> { async embed(): Promise<EmbeddingVector[]> {
return []; return [];
}, },
async isAvailable(): Promise<boolean> { async isAvailable(): Promise<boolean> {
@@ -738,7 +738,7 @@ describe('HybridSearchService', () => {
const repoId = seedRepo(client); const repoId = seedRepo(client);
const docId = seedDocument(client, repoId); const docId = seedDocument(client, repoId);
const snippetId = seedSnippet(client, { seedSnippet(client, {
repositoryId: repoId, repositoryId: repoId,
documentId: docId, documentId: docId,
content: 'keyword only test' content: 'keyword only test'
@@ -860,7 +860,7 @@ describe('HybridSearchService', () => {
it('searchMode=semantic returns empty array for blank query', async () => { it('searchMode=semantic returns empty array for blank query', async () => {
const client = createTestDb(); const client = createTestDb();
const repoId = seedRepo(client); const repoId = seedRepo(client);
const docId = seedDocument(client, repoId); seedDocument(client, repoId);
const mockProvider = makeMockProvider([[1, 0, 0, 0]]); const mockProvider = makeMockProvider([[1, 0, 0, 0]]);
@@ -879,7 +879,7 @@ describe('HybridSearchService', () => {
it('searchMode=semantic falls back to empty when provider fails', async () => { it('searchMode=semantic falls back to empty when provider fails', async () => {
const client = createTestDb(); const client = createTestDb();
const repoId = seedRepo(client); const repoId = seedRepo(client);
const docId = seedDocument(client, repoId); seedDocument(client, repoId);
const noopProvider = makeNoopProvider(); const noopProvider = makeNoopProvider();
const searchService = new SearchService(client); const searchService = new SearchService(client);

View File

@@ -46,8 +46,8 @@ export function preprocessQuery(raw: string): string {
.replace(/[()[\]{}]/g, ' ') // Remove grouping characters .replace(/[()[\]{}]/g, ' ') // Remove grouping characters
.replace(/[;:,!?]/g, ' ') // Remove punctuation that breaks FTS .replace(/[;:,!?]/g, ' ') // Remove punctuation that breaks FTS
.replace(/[<>|]/g, ' ') // Remove comparison/pipe chars .replace(/[<>|]/g, ' ') // Remove comparison/pipe chars
.replace(/[\-+*/%]/g, ' ') // Remove operators (but keep underscores) .replace(/[-+*/%]/g, ' ') // Remove operators (but keep underscores)
.replace(/[@#$&^\\~\`]/g, ' '); // Remove special chars .replace(/[@#$&^~`\\]/g, ' '); // Remove special chars
// Split on remaining punctuation (like dots and slashes) but preserve alphanumeric/underscore. // Split on remaining punctuation (like dots and slashes) but preserve alphanumeric/underscore.
const parts = sanitized.split(/[./\s]+/).filter(Boolean); const parts = sanitized.split(/[./\s]+/).filter(Boolean);

View File

@@ -62,21 +62,6 @@ interface RawRepo {
updated_at: number; updated_at: number;
} }
// Raw row shape returned by better-sqlite3 SELECT * FROM indexing_jobs.
interface RawJob {
id: string;
repository_id: string;
version_id: string | null;
status: string;
progress: number;
total_files: number;
processed_files: number;
error: string | null;
started_at: number | null;
completed_at: number | null;
created_at: number;
}
function makeService(client: Database.Database): RepositoryService { function makeService(client: Database.Database): RepositoryService {
return new RepositoryService(client); return new RepositoryService(client);
} }

View File

@@ -6,7 +6,7 @@
* output — the same pattern used by repository.service.test.ts. * output — the same pattern used by repository.service.test.ts.
*/ */
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect } from 'vitest';
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';

View File

@@ -12,6 +12,7 @@ import {
RepositoryVersionEntity RepositoryVersionEntity
} from '$lib/server/models/repository-version.js'; } from '$lib/server/models/repository-version.js';
import { AlreadyExistsError, NotFoundError } from '$lib/server/utils/validation'; import { AlreadyExistsError, NotFoundError } from '$lib/server/utils/validation';
import { resolveTagToCommit, discoverVersionTags } from '$lib/server/utils/git.js';
export class VersionService { export class VersionService {
constructor(private readonly db: Database.Database) {} constructor(private readonly db: Database.Database) {}
@@ -62,7 +63,6 @@ export class VersionService {
let resolvedCommitHash = commitHash; let resolvedCommitHash = commitHash;
if (!resolvedCommitHash && repo.source === 'local') { if (!resolvedCommitHash && repo.source === 'local') {
try { try {
const { resolveTagToCommit } = require('$lib/server/utils/git.js');
resolvedCommitHash = resolveTagToCommit({ repoPath: repo.source_url, tag }); resolvedCommitHash = resolveTagToCommit({ repoPath: repo.source_url, tag });
} catch (error) { } catch (error) {
console.warn( console.warn(
@@ -178,7 +178,6 @@ export class VersionService {
throw new Error('Tag discovery is only supported for local repositories'); throw new Error('Tag discovery is only supported for local repositories');
} }
const { discoverVersionTags, resolveTagToCommit } = require('$lib/server/utils/git.js');
const tags = discoverVersionTags({ repoPath: repo.source_url }); const tags = discoverVersionTags({ repoPath: repo.source_url });
return tags.map((tag: string) => { return tags.map((tag: string) => {

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import './layout.css'; import './layout.css';
import { resolve as resolveRoute } from '$app/paths';
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
let { children } = $props(); let { children } = $props();
@@ -15,7 +16,7 @@
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8"> <div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div class="flex h-14 items-center justify-between"> <div class="flex h-14 items-center justify-between">
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<a href="/" class="flex items-center gap-2 font-semibold text-gray-900"> <a href={resolveRoute('/')} class="flex items-center gap-2 font-semibold text-gray-900">
<svg <svg
class="h-6 w-6 text-blue-600" class="h-6 w-6 text-blue-600"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -31,9 +32,15 @@
</svg> </svg>
<span>TrueRef</span> <span>TrueRef</span>
</a> </a>
<a href="/" class="text-sm text-gray-600 hover:text-gray-900"> Repositories </a> <a href={resolveRoute('/')} class="text-sm text-gray-600 hover:text-gray-900">
<a href="/search" class="text-sm text-gray-600 hover:text-gray-900"> Search </a> Repositories
<a href="/settings" class="text-sm text-gray-600 hover:text-gray-900"> Settings </a> </a>
<a href={resolveRoute('/search')} class="text-sm text-gray-600 hover:text-gray-900">
Search
</a>
<a href={resolveRoute('/settings')} class="text-sm text-gray-600 hover:text-gray-900">
Settings
</a>
</div> </div>
<span class="text-xs text-gray-400">Self-hosted documentation intelligence</span> <span class="text-xs text-gray-400">Self-hosted documentation intelligence</span>
</div> </div>

View File

@@ -1,18 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { Repository } from '$lib/types';
import RepositoryCard from '$lib/components/RepositoryCard.svelte'; import RepositoryCard from '$lib/components/RepositoryCard.svelte';
import AddRepositoryModal from '$lib/components/AddRepositoryModal.svelte'; import AddRepositoryModal from '$lib/components/AddRepositoryModal.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import IndexingProgress from '$lib/components/IndexingProgress.svelte'; import IndexingProgress from '$lib/components/IndexingProgress.svelte';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
type Repository = NonNullable<PageData['repositories']>[number];
// Initialized empty; $effect syncs from data prop on every navigation/reload. // Syncs from data prop on navigation while still allowing temporary local reassignment.
let repositories = $state<Repository[]>([]); let repositories: Repository[] = $derived(data.repositories ?? []);
$effect(() => {
repositories = data.repositories ?? [];
});
let showAddModal = $state(false); let showAddModal = $state(false);
let confirmDeleteId = $state<string | null>(null); let confirmDeleteId = $state<string | null>(null);
let activeJobIds = $state<Record<string, string>>({}); let activeJobIds = $state<Record<string, string>>({});
@@ -25,7 +22,7 @@
async function refreshRepositories() { async function refreshRepositories() {
try { try {
const res = await fetch('/api/v1/libs'); const res = await fetch('/api/v1/libs');
const fetched = await res.json(); const fetched = (await res.json()) as { libraries?: Repository[] };
repositories = fetched.libraries ?? []; repositories = fetched.libraries ?? [];
} catch { } catch {
// keep existing list on network error // keep existing list on network error

View File

@@ -11,7 +11,6 @@
* type (optional) — "json" (default) or "txt" * type (optional) — "json" (default) or "txt"
*/ */
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client'; import { getClient } from '$lib/server/db/client';
import { dtoJsonResponse } from '$lib/server/api/dto-response'; import { dtoJsonResponse } from '$lib/server/api/dto-response';

View File

@@ -141,7 +141,8 @@ export const PUT: RequestHandler = POST;
function sanitizeProfile(profile: EmbeddingProfile): EmbeddingProfile { function sanitizeProfile(profile: EmbeddingProfile): EmbeddingProfile {
const config = profile.config as Record<string, unknown>; const config = profile.config as Record<string, unknown>;
if (config && config.apiKey) { if (config && config.apiKey) {
const { apiKey: _apiKey, ...rest } = config; const rest = { ...config };
delete rest.apiKey;
return { ...profile, config: rest }; return { ...profile, config: rest };
} }
return profile; return profile;

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { resolve as resolveRoute } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { Repository, RepositoryVersion, IndexingJob } from '$lib/types'; import type { Repository, RepositoryVersion, IndexingJob } from '$lib/types';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
@@ -79,7 +81,7 @@
const d = await res.json(); const d = await res.json();
throw new Error(d.error ?? 'Failed to delete repository'); throw new Error(d.error ?? 'Failed to delete repository');
} }
window.location.href = '/'; goto(resolveRoute('/'));
} catch (e) { } catch (e) {
errorMessage = (e as Error).message; errorMessage = (e as Error).message;
} }
@@ -101,7 +103,7 @@
</svelte:head> </svelte:head>
<div class="mb-6"> <div class="mb-6">
<a href="/" class="text-sm text-blue-600 hover:underline">← Repositories</a> <a href={resolveRoute('/')} class="text-sm text-blue-600 hover:underline">← Repositories</a>
</div> </div>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
@@ -124,7 +126,7 @@
<a <a
href={repo.sourceUrl} href={repo.sourceUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="external noopener noreferrer"
class="mt-1 block text-sm text-blue-600 hover:underline" class="mt-1 block text-sm text-blue-600 hover:underline"
> >
{repo.sourceUrl} {repo.sourceUrl}

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve as resolveRoute } from '$app/paths';
import { page } from '$app/state'; import { page } from '$app/state';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import LibraryResult from '$lib/components/search/LibraryResult.svelte'; import LibraryResult from '$lib/components/search/LibraryResult.svelte';
@@ -106,7 +107,9 @@
if (syncUrl) { if (syncUrl) {
goto( goto(
`/search?lib=${encodeURIComponent(selectedLibraryId)}&q=${encodeURIComponent(query)}`, resolveRoute(
`/search?lib=${encodeURIComponent(selectedLibraryId)}&q=${encodeURIComponent(query)}`
),
{ replaceState: true, keepFocus: true } { replaceState: true, keepFocus: true }
); );
} }
@@ -133,7 +136,7 @@
snippetError = null; snippetError = null;
query = ''; query = '';
// Reset URL. // Reset URL.
goto('/search', { replaceState: true, keepFocus: true }); goto(resolveRoute('/search'), { replaceState: true, keepFocus: true });
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -188,7 +188,7 @@
<form class="space-y-4" onsubmit={handleSubmit}> <form class="space-y-4" onsubmit={handleSubmit}>
<!-- Provider selector --> <!-- Provider selector -->
<div class="mb-4 flex gap-2"> <div class="mb-4 flex gap-2">
{#each ['none', 'openai', 'local'] as p} {#each ['none', 'openai', 'local'] as p (p)}
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
@@ -224,7 +224,7 @@
<div class="space-y-3"> <div class="space-y-3">
<!-- Preset buttons --> <!-- Preset buttons -->
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each PROVIDER_PRESETS as preset} {#each PROVIDER_PRESETS as preset (preset.name)}
<button <button
type="button" type="button"
onclick={() => applyPreset(preset)} onclick={() => applyPreset(preset)}