fix(review): resolve all review blockers and concerns
- Delete frontend/e2e/nl-search.spec.ts (was left alive; would have crashed CI when Playwright couldn't find the deleted SmartModeToggle) - Fix docs/DEPLOYMENT.md: remove NLP service arrow + key-facts bullet that were accidentally added instead of removed in the prior commit - Clean docs/GLOSSARY.md: remove keyword→tag resolution, PersonHint, TagHint, theme chip entries; trim NameMatches to drop the NlQueryParserService reference - Remove @ConfigurationPropertiesScan from FamilienarchivApplication (all remaining @ConfigurationProperties beans carry @Component) - Remove 12 orphaned i18n keys from de/en/es message files (search_loading_nl, search_chip_*, search_disambiguation_*, etc.) - Fix SearchFilterBar.svelte input padding: pr-20 → pr-4 (SmartModeToggle that justified the right padding is gone) - Delete docs/superpowers/plans/2026-06-07-remove-nlp-search.md (scaffolding artefact; plan files belong in Gitea issues, not the repo) - Add docs/adr/034-remove-nl-search.md documenting the removal decision (supersedes deleted ADR-028 ×2, ADR-034-ollama, ADR-035) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,8 @@ package org.raddatz.familienarchiv;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@ConfigurationPropertiesScan
|
|
||||||
public class FamilienarchivApplication {
|
public class FamilienarchivApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ graph TD
|
|||||||
Backend -->|JDBC :5432| DB[(PostgreSQL 16)]
|
Backend -->|JDBC :5432| DB[(PostgreSQL 16)]
|
||||||
Backend -->|S3 API :9000| MinIO[(MinIO)]
|
Backend -->|S3 API :9000| MinIO[(MinIO)]
|
||||||
Backend -->|HTTP :8000 internal| OCR["OCR Service\nPython FastAPI"]
|
Backend -->|HTTP :8000 internal| OCR["OCR Service\nPython FastAPI"]
|
||||||
Backend -->|HTTP :8001 internal| NLP["NLP Service\nPython FastAPI"]
|
|
||||||
OCR -->|presigned URL| MinIO
|
OCR -->|presigned URL| MinIO
|
||||||
Caddy -->|SSE proxy_pass| Backend
|
Caddy -->|SSE proxy_pass| Backend
|
||||||
```
|
```
|
||||||
@@ -41,7 +40,7 @@ graph TD
|
|||||||
**Key facts:**
|
**Key facts:**
|
||||||
- Caddy terminates TLS and reverse-proxies to frontend (`:3000`) and backend (`:8080`). The Caddyfile is committed at [`infra/caddy/Caddyfile`](../infra/caddy/Caddyfile) and is installed on the host as `/etc/caddy/Caddyfile` (symlink).
|
- Caddy terminates TLS and reverse-proxies to frontend (`:3000`) and backend (`:8080`). The Caddyfile is committed at [`infra/caddy/Caddyfile`](../infra/caddy/Caddyfile) and is installed on the host as `/etc/caddy/Caddyfile` (symlink).
|
||||||
- The host binds all docker-published ports to `127.0.0.1` only; Caddy is the sole external entry point.
|
- The host binds all docker-published ports to `127.0.0.1` only; Caddy is the sole external entry point.
|
||||||
- The OCR service and NLP service have **no published ports** — reachable only on the internal Docker network from the backend.
|
- The OCR service has **no published port** — reachable only on the internal Docker network from the backend.
|
||||||
- SSE notifications transit Caddy (browser → Caddy → backend); the backend is never reachable directly from the public internet. The SvelteKit SSR layer is bypassed for SSE, but Caddy is not.
|
- SSE notifications transit Caddy (browser → Caddy → backend); the backend is never reachable directly from the public internet. The SvelteKit SSR layer is bypassed for SSE, but Caddy is not.
|
||||||
- The Caddyfile responds `404` on `/actuator/*` (defense in depth). Internal monitoring scrapes the backend on the docker network, not through Caddy.
|
- The Caddyfile responds `404` on `/actuator/*` (defense in depth). Internal monitoring scrapes the backend on the docker network, not through Caddy.
|
||||||
- Production and staging cohabit on the same host via docker compose project names: `archiv-production` (ports 8080/3000) and `archiv-staging` (ports 8081/3001).
|
- Production and staging cohabit on the same host via docker compose project names: `archiv-production` (ports 8080/3000) and `archiv-staging` (ports 8081/3001).
|
||||||
|
|||||||
@@ -165,16 +165,7 @@ _See also [Chronik](#chronik-internal)._
|
|||||||
|
|
||||||
**Domain** — a Tier-1 bounded context with its own entities, controller, service, repository, and DTOs. Backend domains: `document`, `person`, `tag`, `user`, `geschichte`, `notification`, `ocr`, `audit`, `dashboard`. Frontend domains mirror this structure under `src/lib/`.
|
**Domain** — a Tier-1 bounded context with its own entities, controller, service, repository, and DTOs. Backend domains: `document`, `person`, `tag`, `user`, `geschichte`, `notification`, `ocr`, `audit`, `dashboard`. Frontend domains mirror this structure under `src/lib/`.
|
||||||
|
|
||||||
|
**NameMatches** — the Person-domain result of `PersonService.resolveByName(name)`: candidate persons split by name-match strength into `direct` and `partial`. A match is **direct** when every query token is a whole-token match (order-independent, alias/maiden-name aware) across all of a person's name components (`firstName`, `lastName`, `alias`, each `PersonNameAlias` first+last, `title`); a **partial** matched the substring fetch but is not direct (e.g. "Cram" → "Clara Cramer").
|
||||||
**keyword→tag resolution** — the post-Ollama step in `NlQueryParserService` where each LLM-extracted keyword is substring-matched against the tag taxonomy via `TagService.findByNameContaining()`. Keywords that hit one or more tags are removed from the FTS text list and become an OR-union tag filter; keywords with no match remain as FTS text. Matching is case-insensitive and traverses the tag hierarchy via the recursive CTE `findDescendantIdsByName`. See ADR-033.
|
|
||||||
|
|
||||||
**PersonHint** — a lightweight `{id, displayName}` pair used in `NlQueryInterpretation` to describe a resolved or ambiguous person without exposing the full `Person` entity to the frontend.
|
|
||||||
|
|
||||||
**NameMatches** — the Person-domain result of `PersonService.resolveByName(name)`: candidate persons split by name-match strength into `direct` and `partial`. A match is **direct** when every query token is a whole-token match (order-independent, alias/maiden-name aware) across all of a person's name components (`firstName`, `lastName`, `alias`, each `PersonNameAlias` first+last, `title`); a **partial** matched the substring fetch but is not direct (e.g. "Cram" → "Clara Cramer"). The vocabulary is deliberately match strength, not the search layer's resolved/ambiguous buckets — `NlQueryParserService` maps one direct → resolved (auto-select), ≥2 direct → ambiguous, partial-only → ambiguous suggestions ("Meintest du …?"), and no candidates → folded into full-text search.
|
|
||||||
|
|
||||||
**TagHint** — a lightweight `{id, name, color?}` triple used in `NlQueryInterpretation.resolvedTags` to describe a tag matched by keyword→tag resolution. `color` is the tag's effective color (one-level inheritance from parent when the tag has no own color), or null if neither tag nor parent has a color.
|
|
||||||
|
|
||||||
**theme chip** `[frontend]` — a removable chip rendered in `InterpretationChipRow` for each entry in `NlQueryInterpretation.resolvedTags` when `tagsApplied` is `true`. Displays "Thema: {tag.name}" (prefix varies by locale). Clicking × removes the tag from the OR-union filter and navigates to `/documents?tag=…&tagOp=OR` with remaining tag and person parameters preserved.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
53
docs/adr/034-remove-nl-search.md
Normal file
53
docs/adr/034-remove-nl-search.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# ADR-034 — Remove NL/smart-search (supersedes ADR-028 ×2, ADR-034-ollama, ADR-035)
|
||||||
|
|
||||||
|
**Date:** 2026-06-07
|
||||||
|
**Status:** Accepted
|
||||||
|
**Issue:** #772
|
||||||
|
**Supersedes:** ADR-028 (nl-search-ollama), ADR-028 (ollama-docker-compose-service), ADR-034 (ollama-production-deployment-and-keep-alive), ADR-035 (rule-based-nlp-service)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The natural-language search feature ("KI-Suche" / smart search) allowed users to enter
|
||||||
|
free-form queries like *"Was hat Walter an Emma im Krieg geschrieben?"* and have them
|
||||||
|
interpreted by an LLM into structured filters (persons, tags, date range, keywords).
|
||||||
|
|
||||||
|
The feature went through two major iterations:
|
||||||
|
1. **Ollama integration** (ADR-028): an `ollama` Docker service running a local LLM
|
||||||
|
(llama3.2/gemma3) parsed queries via a JSON-mode prompt.
|
||||||
|
2. **Rule-based NLP service** (ADR-035): after Ollama proved too slow and unreliable on
|
||||||
|
CPU-only hardware, a Python FastAPI microservice (`nlp-service`, port 8001) replaced
|
||||||
|
it with deterministic regex + spaCy parsing plus a lightweight LLM call.
|
||||||
|
|
||||||
|
Both approaches shared the same fundamental problem: inference on the production server
|
||||||
|
(Hetzner Serverbörse, no GPU, 64 GB RAM, i7-6700) was too slow to be useful, with
|
||||||
|
typical query latencies of 10–30 seconds. Users got better and faster results from
|
||||||
|
the existing keyword search with date/person/tag filters.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Remove the NL search feature entirely.** The Python `nlp-service` microservice, the
|
||||||
|
Spring Boot `search/` package (`NlSearchController`, `NlQueryParserService`,
|
||||||
|
`RestClientNlpClient`, `NlSearchRateLimiter`, and all supporting classes), the frontend
|
||||||
|
NL search components (`SmartModeToggle`, `SmartSearchStatus`, `InterpretationChipRow`,
|
||||||
|
`DisambiguationPicker`), the related Docker Compose services, Prometheus scrape job,
|
||||||
|
Grafana dashboard, and all i18n keys are removed.
|
||||||
|
|
||||||
|
The existing structured search (FTS keyword + person/tag/date/directional filters) is
|
||||||
|
sufficient for the archive's current audience and search workload.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Capability removed:** users can no longer enter free-form natural-language queries.
|
||||||
|
They must use the structured filter bar (keyword text box + person/tag/date/directional
|
||||||
|
dropdowns). For documents where these filters are sufficient, there is no regression.
|
||||||
|
- **Operational simplification:** the Docker Compose stack loses two services
|
||||||
|
(`nlp-service` and previously `ollama`/`ollama-model-init`). Memory budget on the
|
||||||
|
production host is freed. No external model weights need to be kept warm.
|
||||||
|
- **Future reinstatement:** if a GPU-capable host becomes available, re-implementing
|
||||||
|
server-side LLM inference would be straightforward given the clean separation of the
|
||||||
|
`NlSearchController` entry point. However, this ADR deliberately avoids leaving dead
|
||||||
|
infrastructure or stub code in place — start clean if and when that becomes viable.
|
||||||
|
- **No data or schema change:** only query/endpoint code and Docker services are removed.
|
||||||
|
The `documents`, `persons`, and `tags` tables and their FTS indexes are untouched.
|
||||||
@@ -1,768 +0,0 @@
|
|||||||
# Remove NLP/Smart Search Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Remove the NLP/smart-search feature entirely from the codebase — backend search package, frontend components, i18n keys, infrastructure config, and the nlp-service microservice.
|
|
||||||
|
|
||||||
**Architecture:** Pure deletion + targeted edits. No new code. Each task deletes a self-contained layer, then verifies compilation passes before committing. Order: backend first (most isolated), then frontend, then infrastructure, then docs.
|
|
||||||
|
|
||||||
**Tech Stack:** Spring Boot 4 (Java 21, Maven), SvelteKit 2 / Svelte 5, Docker Compose, Paraglide i18n.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Map
|
|
||||||
|
|
||||||
### Delete entirely
|
|
||||||
- `backend/src/main/java/org/raddatz/familienarchiv/search/` — 14 Java source files
|
|
||||||
- `backend/src/test/java/org/raddatz/familienarchiv/search/` — 6 Java test files
|
|
||||||
- `frontend/src/routes/search/SmartModeToggle.svelte` + `.spec.ts`
|
|
||||||
- `frontend/src/routes/search/SmartSearchStatus.svelte` + `.spec.ts`
|
|
||||||
- `frontend/src/routes/search/InterpretationChipRow.svelte` + `.spec.ts`
|
|
||||||
- `frontend/src/routes/search/DisambiguationPicker.svelte` + `.spec.ts`
|
|
||||||
- `frontend/src/routes/search/chip-types.ts`
|
|
||||||
- `frontend/src/routes/documents/theme-chip-removal.ts` + `.spec.ts`
|
|
||||||
- `infra/observability/grafana/provisioning/dashboards/ollama.json`
|
|
||||||
- `nlp-service/` (entire directory)
|
|
||||||
- `docs/adr/028-nl-search-ollama.md`
|
|
||||||
- `docs/adr/028-ollama-docker-compose-service.md`
|
|
||||||
- `docs/adr/034-ollama-production-deployment-and-keep-alive.md`
|
|
||||||
- `docs/adr/035-rule-based-nlp-service.md`
|
|
||||||
|
|
||||||
### Modify
|
|
||||||
- `backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java` — remove 2 enum values
|
|
||||||
- `backend/src/main/resources/application.yaml` — remove `nlp` + `nl-search` config blocks
|
|
||||||
- `backend/src/main/resources/application-dev.yaml` — remove `nlp` config block
|
|
||||||
- `frontend/src/routes/SearchFilterBar.svelte` — remove SmartModeToggle, smartMode prop, smart callbacks
|
|
||||||
- `frontend/src/routes/SearchFilterBar.svelte.spec.ts` — remove smart-mode describe block
|
|
||||||
- `frontend/src/routes/documents/+page.svelte` — remove all NL state, functions, template block
|
|
||||||
- `frontend/src/lib/shared/errors.ts` — remove 2 error codes + their switch cases
|
|
||||||
- `frontend/messages/de.json` — remove 8 smart-search keys
|
|
||||||
- `frontend/messages/en.json` — remove 8 smart-search keys
|
|
||||||
- `frontend/messages/es.json` — remove 8 smart-search keys
|
|
||||||
- `docker-compose.yml` — remove nlp-service block + backend depends_on + env var
|
|
||||||
- `docker-compose.prod.yml` — remove nlp-service block + backend depends_on + env var
|
|
||||||
- `infra/observability/prometheus/prometheus.yml` — remove ollama scrape job
|
|
||||||
- `CLAUDE.md` — remove search package reference + error code entries
|
|
||||||
- `backend/CLAUDE.md` — no change needed (search package already absent from structure)
|
|
||||||
- `frontend/CLAUDE.md` — update routes/search/ description
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Delete backend search package
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Delete: `backend/src/main/java/org/raddatz/familienarchiv/search/` (14 files)
|
|
||||||
- Delete: `backend/src/test/java/org/raddatz/familienarchiv/search/` (6 files)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Delete all source files**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -rf backend/src/main/java/org/raddatz/familienarchiv/search
|
|
||||||
rm -rf backend/src/test/java/org/raddatz/familienarchiv/search
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Verify backend compiles**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend && . ~/.sdkman/candidates/java/current/bin/../.. && source ~/.sdkman/bin/sdkman-init.sh && ./mvnw compile -q
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: BUILD SUCCESS with no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m "refactor(search): delete backend NLP search package"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Remove ErrorCode entries and backend config
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java:138-142`
|
|
||||||
- Modify: `backend/src/main/resources/application.yaml:133-138`
|
|
||||||
- Modify: `backend/src/main/resources/application-dev.yaml:15-17`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove NL Search enum values from ErrorCode.java**
|
|
||||||
|
|
||||||
Remove these lines (138–142):
|
|
||||||
```java
|
|
||||||
// --- NL Search ---
|
|
||||||
/** Ollama is unreachable or timed out. 503 */
|
|
||||||
SMART_SEARCH_UNAVAILABLE,
|
|
||||||
/** NL search rate limit exceeded (5 requests per user per minute). 429 */
|
|
||||||
SMART_SEARCH_RATE_LIMITED,
|
|
||||||
```
|
|
||||||
|
|
||||||
The block between `TAG_MERGE_INVALID_TARGET,` and `// --- Generic ---` becomes empty.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Remove nlp and nl-search config from application.yaml**
|
|
||||||
|
|
||||||
Remove these lines (133–138):
|
|
||||||
```yaml
|
|
||||||
nlp:
|
|
||||||
base-url: http://nlp-service:8001
|
|
||||||
|
|
||||||
nl-search:
|
|
||||||
rate-limit:
|
|
||||||
max-requests-per-minute: 20
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Remove nlp config from application-dev.yaml**
|
|
||||||
|
|
||||||
Remove these lines (15–17):
|
|
||||||
```yaml
|
|
||||||
app:
|
|
||||||
nlp:
|
|
||||||
base-url: http://localhost:8001
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: only remove the `nlp:` sub-key under `app:`, preserving any other `app:` config above it.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify backend still compiles**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend && source ~/.sdkman/bin/sdkman-init.sh && ./mvnw compile -q
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: BUILD SUCCESS.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java \
|
|
||||||
backend/src/main/resources/application.yaml \
|
|
||||||
backend/src/main/resources/application-dev.yaml
|
|
||||||
git commit -m "refactor(search): remove NLP error codes and application config"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Delete frontend NL search components and utilities
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Delete: `frontend/src/routes/search/SmartModeToggle.svelte` + `.spec.ts`
|
|
||||||
- Delete: `frontend/src/routes/search/SmartSearchStatus.svelte` + `.spec.ts`
|
|
||||||
- Delete: `frontend/src/routes/search/InterpretationChipRow.svelte` + `.spec.ts`
|
|
||||||
- Delete: `frontend/src/routes/search/DisambiguationPicker.svelte` + `.spec.ts`
|
|
||||||
- Delete: `frontend/src/routes/search/chip-types.ts`
|
|
||||||
- Delete: `frontend/src/routes/documents/theme-chip-removal.ts` + `.spec.ts`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Delete all NL search components, specs, and utilities**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm frontend/src/routes/search/SmartModeToggle.svelte \
|
|
||||||
frontend/src/routes/search/SmartModeToggle.svelte.spec.ts \
|
|
||||||
frontend/src/routes/search/SmartSearchStatus.svelte \
|
|
||||||
frontend/src/routes/search/SmartSearchStatus.svelte.spec.ts \
|
|
||||||
frontend/src/routes/search/InterpretationChipRow.svelte \
|
|
||||||
frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts \
|
|
||||||
frontend/src/routes/search/DisambiguationPicker.svelte \
|
|
||||||
frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts \
|
|
||||||
frontend/src/routes/search/chip-types.ts \
|
|
||||||
frontend/src/routes/documents/theme-chip-removal.ts \
|
|
||||||
frontend/src/routes/documents/theme-chip-removal.spec.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m "refactor(search): delete frontend NLP search components and utilities"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Remove NL search from SearchFilterBar
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `frontend/src/routes/SearchFilterBar.svelte`
|
|
||||||
- Modify: `frontend/src/routes/SearchFilterBar.svelte.spec.ts:199-233`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Rewrite SearchFilterBar.svelte**
|
|
||||||
|
|
||||||
Replace the entire `<script>` block with:
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import PersonTypeahead from '$lib/person/PersonTypeahead.svelte';
|
|
||||||
import TagInput from '$lib/tag/TagInput.svelte';
|
|
||||||
import DateInput from '$lib/shared/primitives/DateInput.svelte';
|
|
||||||
import SortDropdown from '$lib/shared/primitives/SortDropdown.svelte';
|
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
|
||||||
|
|
||||||
let {
|
|
||||||
q = $bindable(''),
|
|
||||||
from = $bindable(''),
|
|
||||||
to = $bindable(''),
|
|
||||||
senderId = $bindable(''),
|
|
||||||
receiverId = $bindable(''),
|
|
||||||
tagNames = $bindable<{ name: string; id?: string; color?: string; parentId?: string }[]>([]),
|
|
||||||
tagQ = $bindable(''),
|
|
||||||
tagOperator = $bindable<'AND' | 'OR'>('AND'),
|
|
||||||
undated = $bindable(false),
|
|
||||||
undatedCount = 0,
|
|
||||||
sort = $bindable('DATE'),
|
|
||||||
dir = $bindable('desc'),
|
|
||||||
showAdvanced = $bindable(false),
|
|
||||||
initialSenderName = '',
|
|
||||||
initialReceiverName = '',
|
|
||||||
navKey = 0,
|
|
||||||
isLoading = false,
|
|
||||||
onSearch,
|
|
||||||
onSearchImmediate,
|
|
||||||
onfocus,
|
|
||||||
onblur
|
|
||||||
}: {
|
|
||||||
q?: string;
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
senderId?: string;
|
|
||||||
receiverId?: string;
|
|
||||||
tagNames?: { name: string; id?: string; color?: string; parentId?: string }[];
|
|
||||||
tagQ?: string;
|
|
||||||
tagOperator?: 'AND' | 'OR';
|
|
||||||
undated?: boolean;
|
|
||||||
undatedCount?: number;
|
|
||||||
sort?: string;
|
|
||||||
dir?: string;
|
|
||||||
showAdvanced?: boolean;
|
|
||||||
initialSenderName?: string;
|
|
||||||
initialReceiverName?: string;
|
|
||||||
navKey?: number;
|
|
||||||
isLoading?: boolean;
|
|
||||||
onSearch: () => void;
|
|
||||||
onSearchImmediate?: () => void;
|
|
||||||
onfocus?: () => void;
|
|
||||||
onblur?: () => void;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
// Plain (non-reactive) flag — not $state, so no reactive assignment inside $effect
|
|
||||||
let sortDirMounted = false;
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
void sort;
|
|
||||||
void dir;
|
|
||||||
if (!sortDirMounted) {
|
|
||||||
sortDirMounted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSearch();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Update the search input element in the template**
|
|
||||||
|
|
||||||
Replace the `<input type="text" ...>` element (lines 92–105) with:
|
|
||||||
```svelte
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={q}
|
|
||||||
oninput={onSearch}
|
|
||||||
onfocus={onfocus}
|
|
||||||
onblur={onblur}
|
|
||||||
aria-label={m.docs_search_placeholder()}
|
|
||||||
placeholder={m.docs_search_placeholder()}
|
|
||||||
class="block w-full border-line py-2.5 pl-10 pr-20 placeholder-ink-3 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Remove the SmartModeToggle component from the template**
|
|
||||||
|
|
||||||
Delete this line (135):
|
|
||||||
```svelte
|
|
||||||
<SmartModeToggle bind:smartMode={smartMode} onToggle={onModeToggle} />
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Remove smart-mode describe block from SearchFilterBar.svelte.spec.ts**
|
|
||||||
|
|
||||||
Delete lines 199–233 (the entire final `describe` block):
|
|
||||||
```typescript
|
|
||||||
describe('SearchFilterBar – smart-mode chip lifecycle hooks', () => {
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: Run the SearchFilterBar tests to verify they pass**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend && source ~/.nvm/nvm.sh && npm run test -- --project=client src/routes/SearchFilterBar.svelte.spec.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: all tests pass, no failures.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add frontend/src/routes/SearchFilterBar.svelte \
|
|
||||||
frontend/src/routes/SearchFilterBar.svelte.spec.ts
|
|
||||||
git commit -m "refactor(search): remove smart mode from SearchFilterBar"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Remove NL search from documents/+page.svelte
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `frontend/src/routes/documents/+page.svelte`
|
|
||||||
|
|
||||||
This is the largest edit. Remove all NL search state, derived values, functions, and the NL results template block.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove NL search imports (lines 11–16, 23–27)**
|
|
||||||
|
|
||||||
Remove these import lines:
|
|
||||||
```typescript
|
|
||||||
import SmartSearchStatus from '../search/SmartSearchStatus.svelte';
|
|
||||||
import InterpretationChipRow from '../search/InterpretationChipRow.svelte';
|
|
||||||
import type { ChipType } from '../search/chip-types.js';
|
|
||||||
import { buildThemeRemovalUrl } from './theme-chip-removal.js';
|
|
||||||
import DisambiguationPicker from '../search/DisambiguationPicker.svelte';
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove these type aliases:
|
|
||||||
```typescript
|
|
||||||
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
|
|
||||||
type NlSearchResponse = components['schemas']['NlSearchResponse'];
|
|
||||||
type DocumentSearchResult = components['schemas']['DocumentSearchResult'];
|
|
||||||
type PersonHint = components['schemas']['PersonHint'];
|
|
||||||
type SmartSearchErrorCode = 'SMART_SEARCH_UNAVAILABLE' | 'SMART_SEARCH_RATE_LIMITED';
|
|
||||||
```
|
|
||||||
|
|
||||||
Also remove the `import { csrfFetch } from '$lib/shared/cookies';` line — it is only used by `runSmartSearch`.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Remove all NL state and derived values (lines 51–70)**
|
|
||||||
|
|
||||||
Remove these declarations:
|
|
||||||
```typescript
|
|
||||||
// Smart (NL) search — UI-local state, resets on real page navigation (away + back).
|
|
||||||
let smartMode = $state(false);
|
|
||||||
let nlSubmitted = $state(false);
|
|
||||||
let nlLoading = $state(false);
|
|
||||||
let nlError = $state<SmartSearchErrorCode | null>(null);
|
|
||||||
let nlInterpretation = $state<NlQueryInterpretation | null>(null);
|
|
||||||
let nlResult = $state<DocumentSearchResult | null>(null);
|
|
||||||
|
|
||||||
const showNlView = $derived(smartMode && nlSubmitted);
|
|
||||||
const nlHasResults = $derived((nlResult?.items.length ?? 0) > 0);
|
|
||||||
const ambiguousPersons = $derived(nlInterpretation?.ambiguousPersons ?? []);
|
|
||||||
const nlIsAmbiguous = $derived(ambiguousPersons.length > 0);
|
|
||||||
const disambiguationHeading = $derived(
|
|
||||||
ambiguousPersons.length === 1
|
|
||||||
? m.search_disambiguation_did_you_mean({ name: ambiguousPersons[0].displayName })
|
|
||||||
: m.search_disambiguation_heading()
|
|
||||||
);
|
|
||||||
const showDisambiguationCue = $derived(ambiguousPersons.length >= 2);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Remove all NL search functions (lines 202–318)**
|
|
||||||
|
|
||||||
Remove these functions entirely:
|
|
||||||
- `resetNlState()`
|
|
||||||
- `onModeToggle()`
|
|
||||||
- `runSmartSearch()`
|
|
||||||
- `switchToKeywordMode()`
|
|
||||||
- `applyResolvedAndSearch()`
|
|
||||||
- `paramsFromInterpretation()`
|
|
||||||
- `removeChip()`
|
|
||||||
- `selectDisambiguated()`
|
|
||||||
|
|
||||||
- [ ] **Step 4: Update SearchFilterBar usage in the template**
|
|
||||||
|
|
||||||
Replace the SearchFilterBar call with (removing `bind:smartMode`, `onSmartSearch`, `onModeToggle`):
|
|
||||||
```svelte
|
|
||||||
<SearchFilterBar
|
|
||||||
bind:q={q}
|
|
||||||
bind:from={from}
|
|
||||||
bind:to={to}
|
|
||||||
bind:senderId={senderId}
|
|
||||||
bind:receiverId={receiverId}
|
|
||||||
bind:tagNames={tagNames}
|
|
||||||
bind:showAdvanced={showAdvanced}
|
|
||||||
bind:sort={sort}
|
|
||||||
bind:dir={dir}
|
|
||||||
bind:tagQ={tagQ}
|
|
||||||
bind:tagOperator={tagOperator}
|
|
||||||
bind:undated={undated}
|
|
||||||
undatedCount={data.undatedCount ?? 0}
|
|
||||||
initialSenderName={initialSenderName}
|
|
||||||
initialReceiverName={initialReceiverName}
|
|
||||||
navKey={navKey}
|
|
||||||
isLoading={navigating.to !== null}
|
|
||||||
onSearch={handleTextSearch}
|
|
||||||
onSearchImmediate={handleImmediateSearch}
|
|
||||||
onfocus={() => (qFocused = true)}
|
|
||||||
onblur={() => (qFocused = false)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: Remove the NL results template block**
|
|
||||||
|
|
||||||
Replace the entire `{#if showNlView}...{:else}...{/if}` block with just the content of the `{:else}` branch — the `<div class="mt-3 mb-4 hidden lg:block">` block through the closing `{/if}`, unwrapped. The result should be:
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<div class="mt-3 mb-4 hidden lg:block">
|
|
||||||
<TimelineDensityFilter
|
|
||||||
... (keep as-is)
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 flex items-center justify-between gap-4">
|
|
||||||
... (keep as-is)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DocumentList ... (keep as-is) />
|
|
||||||
|
|
||||||
<Pagination ... (keep as-is) />
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 6: Verify frontend type-checks cleanly for this file**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend && source ~/.nvm/nvm.sh && npm run check 2>&1 | grep "documents/+page.svelte"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: no errors for `+page.svelte` (pre-existing errors in other files are acceptable).
|
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add frontend/src/routes/documents/+page.svelte
|
|
||||||
git commit -m "refactor(search): remove NLP smart search from documents page"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Remove error codes from frontend errors.ts
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `frontend/src/lib/shared/errors.ts`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove error code union members (lines 56–57)**
|
|
||||||
|
|
||||||
Remove from the `ErrorCode` type union:
|
|
||||||
```typescript
|
|
||||||
| 'SMART_SEARCH_UNAVAILABLE'
|
|
||||||
| 'SMART_SEARCH_RATE_LIMITED'
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Remove switch cases (lines 183–186)**
|
|
||||||
|
|
||||||
Remove from the `getErrorMessage()` switch:
|
|
||||||
```typescript
|
|
||||||
case 'SMART_SEARCH_UNAVAILABLE':
|
|
||||||
return m.error_smart_search_unavailable();
|
|
||||||
case 'SMART_SEARCH_RATE_LIMITED':
|
|
||||||
return m.error_smart_search_rate_limited();
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add frontend/src/lib/shared/errors.ts
|
|
||||||
git commit -m "refactor(search): remove smart search error codes from frontend"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Remove i18n messages from all three language files
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `frontend/messages/de.json`
|
|
||||||
- Modify: `frontend/messages/en.json`
|
|
||||||
- Modify: `frontend/messages/es.json`
|
|
||||||
|
|
||||||
Remove the same set of keys from each file. The keys to remove are:
|
|
||||||
|
|
||||||
```
|
|
||||||
"error_smart_search_unavailable"
|
|
||||||
"error_smart_search_rate_limited"
|
|
||||||
"smart_search_keywords_not_applied"
|
|
||||||
"search_toggle_smart_label"
|
|
||||||
"search_toggle_smart_label_suffix"
|
|
||||||
"search_toggle_keyword_label"
|
|
||||||
"search_toggle_keyword_label_suffix"
|
|
||||||
"search_error_unavailable"
|
|
||||||
"search_error_unavailable_body"
|
|
||||||
"search_error_rate_limited"
|
|
||||||
"search_error_rate_limited_body"
|
|
||||||
"search_empty_nl"
|
|
||||||
"search_empty_retry_keyword"
|
|
||||||
"search_disambiguation_did_you_mean"
|
|
||||||
"search_disambiguation_heading"
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: not all keys may exist in all three files — remove whichever are present.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove smart-search keys from de.json**
|
|
||||||
|
|
||||||
Open `frontend/messages/de.json` and delete every key listed above.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Remove smart-search keys from en.json**
|
|
||||||
|
|
||||||
Open `frontend/messages/en.json` and delete every key listed above.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Remove smart-search keys from es.json**
|
|
||||||
|
|
||||||
Open `frontend/messages/es.json` and delete every key listed above.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify JSON is still valid**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend && node -e "
|
|
||||||
['messages/de.json','messages/en.json','messages/es.json'].forEach(f => {
|
|
||||||
try { JSON.parse(require('fs').readFileSync(f,'utf8')); console.log(f+': OK'); }
|
|
||||||
catch(e) { console.error(f+': INVALID - '+e.message); process.exit(1); }
|
|
||||||
})
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: all three files print `OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add frontend/messages/de.json frontend/messages/en.json frontend/messages/es.json
|
|
||||||
git commit -m "refactor(search): remove smart search i18n keys from all language files"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Remove nlp-service from docker-compose files
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `docker-compose.yml`
|
|
||||||
- Modify: `docker-compose.prod.yml`
|
|
||||||
|
|
||||||
#### docker-compose.yml
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove the nlp-service service block**
|
|
||||||
|
|
||||||
Delete the entire `nlp-service:` top-level service definition (lines ~148–179, from ` nlp-service:` through its closing line before the next service).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Remove nlp-service from backend depends_on**
|
|
||||||
|
|
||||||
Find the `backend:` service's `depends_on:` list and remove the `nlp-service:` entry.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Remove APP_NLP_BASE_URL from backend environment**
|
|
||||||
|
|
||||||
Find the `backend:` service's `environment:` section and remove:
|
|
||||||
```yaml
|
|
||||||
APP_NLP_BASE_URL: "http://nlp-service:8001"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### docker-compose.prod.yml
|
|
||||||
|
|
||||||
- [ ] **Step 4: Remove the nlp-service service block**
|
|
||||||
|
|
||||||
Delete the entire `nlp-service:` top-level service definition (lines ~206–221, from ` nlp-service:` through its closing line).
|
|
||||||
|
|
||||||
- [ ] **Step 5: Remove nlp-service from backend depends_on**
|
|
||||||
|
|
||||||
Find the `backend:` service's `depends_on:` list and remove the `nlp-service:` entry.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Remove APP_NLP_BASE_URL from backend environment**
|
|
||||||
|
|
||||||
Find the `backend:` service's `environment:` section and remove:
|
|
||||||
```yaml
|
|
||||||
APP_NLP_BASE_URL: http://nlp-service:8001
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 7: Validate compose files parse correctly**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.yml config --quiet && echo "dev: OK"
|
|
||||||
docker compose -f docker-compose.prod.yml config --quiet && echo "prod: OK"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: both print `OK` (warnings are acceptable, errors are not).
|
|
||||||
|
|
||||||
- [ ] **Step 8: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add docker-compose.yml docker-compose.prod.yml
|
|
||||||
git commit -m "refactor(infra): remove nlp-service from docker-compose files"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 9: Remove observability config for Ollama/NLP
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `infra/observability/prometheus/prometheus.yml`
|
|
||||||
- Delete: `infra/observability/grafana/provisioning/dashboards/ollama.json`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove ollama scrape job from prometheus.yml**
|
|
||||||
|
|
||||||
Delete these lines from `infra/observability/prometheus/prometheus.yml`:
|
|
||||||
```yaml
|
|
||||||
- job_name: ollama
|
|
||||||
static_configs:
|
|
||||||
- targets: ['ollama:11434']
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Delete the Grafana Ollama dashboard**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm infra/observability/grafana/provisioning/dashboards/ollama.json
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add infra/observability/prometheus/prometheus.yml
|
|
||||||
git add -A infra/observability/grafana/provisioning/dashboards/
|
|
||||||
git commit -m "refactor(infra): remove Ollama/NLP observability config"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 10: Delete nlp-service directory and ADR docs
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Delete: `nlp-service/` (entire directory)
|
|
||||||
- Delete: `docs/adr/028-nl-search-ollama.md`
|
|
||||||
- Delete: `docs/adr/028-ollama-docker-compose-service.md`
|
|
||||||
- Delete: `docs/adr/034-ollama-production-deployment-and-keep-alive.md`
|
|
||||||
- Delete: `docs/adr/035-rule-based-nlp-service.md`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Delete the nlp-service microservice**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -rf nlp-service/
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Delete NLP/Ollama ADRs**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm docs/adr/028-nl-search-ollama.md \
|
|
||||||
docs/adr/028-ollama-docker-compose-service.md \
|
|
||||||
docs/adr/034-ollama-production-deployment-and-keep-alive.md \
|
|
||||||
docs/adr/035-rule-based-nlp-service.md
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m "refactor(search): delete nlp-service microservice and Ollama ADRs"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 11: Update CLAUDE.md files
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `CLAUDE.md`
|
|
||||||
- Modify: `frontend/CLAUDE.md`
|
|
||||||
|
|
||||||
#### CLAUDE.md (root)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove search package from backend package structure table**
|
|
||||||
|
|
||||||
Delete this row from the Package Structure section:
|
|
||||||
```
|
|
||||||
├── search/ NL search domain — NlSearchController, NlQueryParserService, RestClientOllamaClient, NlSearchRateLimiter
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Remove NLP error codes from Error Handling section**
|
|
||||||
|
|
||||||
In the **Error Handling** LLM reminder, remove:
|
|
||||||
- `SMART_SEARCH_UNAVAILABLE (HTTP 503 — Ollama inference service offline or timed out)`
|
|
||||||
- `SMART_SEARCH_RATE_LIMITED (HTTP 429 — user exceeded 5 NL search requests per minute)`
|
|
||||||
|
|
||||||
from both the Backend Architecture and Frontend Architecture sections.
|
|
||||||
|
|
||||||
#### frontend/CLAUDE.md
|
|
||||||
|
|
||||||
- [ ] **Step 3: Update routes/search/ description**
|
|
||||||
|
|
||||||
Replace:
|
|
||||||
```
|
|
||||||
│ ├── search/ # Smart (NL) search sub-components — SmartModeToggle, InterpretationChipRow, SmartSearchStatus, DisambiguationPicker (no +page; consumed by documents/ and SearchFilterBar)
|
|
||||||
```
|
|
||||||
|
|
||||||
With:
|
|
||||||
```
|
|
||||||
│ ├── search/ # (empty — NL search removed)
|
|
||||||
```
|
|
||||||
|
|
||||||
Or delete the line entirely if the directory will be empty after removing all component files.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add CLAUDE.md frontend/CLAUDE.md
|
|
||||||
git commit -m "docs(claude): remove NLP search references from CLAUDE.md files"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 12: Verify and regenerate API types
|
|
||||||
|
|
||||||
- [ ] **Step 1: Install frontend dependencies if needed**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend && source ~/.nvm/nvm.sh && npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run frontend vitest to verify no regressions**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend && source ~/.nvm/nvm.sh && npm run test -- --reporter=verbose 2>&1 | tail -30
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: all tests pass. The deleted spec files are gone and no longer run.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Start backend to regenerate API types (requires Docker stack)**
|
|
||||||
|
|
||||||
If backend is running locally:
|
|
||||||
```bash
|
|
||||||
cd frontend && source ~/.nvm/nvm.sh && npm run generate:api
|
|
||||||
```
|
|
||||||
|
|
||||||
If not running, skip — the generated types remain valid until the next backend change (no NLP types are referenced after this plan).
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run a final backend compile**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend && source ~/.sdkman/bin/sdkman-init.sh && ./mvnw compile -q
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: BUILD SUCCESS.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit regenerated API types (if generate:api was run)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add frontend/src/lib/generated/
|
|
||||||
git commit -m "chore(api): regenerate API types after NLP search removal"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 13: Check and clean the empty search/ directory
|
|
||||||
|
|
||||||
- [ ] **Step 1: Check if routes/search/ is now empty**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls frontend/src/routes/search/
|
|
||||||
```
|
|
||||||
|
|
||||||
If only `chip-types.ts` was in there (already deleted in Task 3) and all Svelte files are gone, the directory should be empty.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Delete the empty directory**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rmdir frontend/src/routes/search/ 2>/dev/null && echo "deleted" || echo "not empty — check contents"
|
|
||||||
```
|
|
||||||
|
|
||||||
If not empty, list what remains and delete the stray files.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m "refactor(search): remove empty search/ route directory"
|
|
||||||
```
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import AxeBuilder from '@axe-core/playwright';
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
// NL search is mocked at the network boundary — Ollama is not required in CI.
|
|
||||||
// CSRF enforcement is bypassed by page.route (the real request is never sent),
|
|
||||||
// so it is only verified in manual full-stack runs (see issue #739 DevOps notes).
|
|
||||||
const interpretation = {
|
|
||||||
resolvedPersons: [
|
|
||||||
{ id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter Raddatz' },
|
|
||||||
{ id: '22222222-2222-2222-2222-222222222222', displayName: 'Emma Raddatz' }
|
|
||||||
],
|
|
||||||
ambiguousPersons: [],
|
|
||||||
dateFrom: '1914-01-01',
|
|
||||||
dateTo: '1918-12-31',
|
|
||||||
keywords: ['krieg'],
|
|
||||||
resolvedTags: [{ id: '33333333-3333-3333-3333-333333333333', name: 'Weltkrieg', color: 'sage' }],
|
|
||||||
rawQuery: 'Was hat Walter an Emma im Krieg geschrieben?',
|
|
||||||
keywordsApplied: true,
|
|
||||||
tagsApplied: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const nlResponse = {
|
|
||||||
result: {
|
|
||||||
items: [],
|
|
||||||
totalElements: 0,
|
|
||||||
pageNumber: 0,
|
|
||||||
pageSize: 20,
|
|
||||||
totalPages: 0,
|
|
||||||
undatedCount: 0
|
|
||||||
},
|
|
||||||
interpretation
|
|
||||||
};
|
|
||||||
|
|
||||||
test.describe('NL (smart) search — happy path', () => {
|
|
||||||
test('toggle → loading → chips → remove chip re-runs keyword search; axe clean light + dark', async ({
|
|
||||||
page
|
|
||||||
}) => {
|
|
||||||
// Deliberate delay so the loading state is assertable before the response arrives.
|
|
||||||
await page.route('**/api/search/nl', async (route) => {
|
|
||||||
const body = route.request().postDataJSON();
|
|
||||||
expect(body.lang).toBeTruthy();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify(nlResponse)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('/documents');
|
|
||||||
await page.waitForSelector('[data-hydrated]');
|
|
||||||
|
|
||||||
// Switch to smart mode via the toggle pill (keyword label = "Text").
|
|
||||||
await page.getByRole('button', { name: /Text/ }).click();
|
|
||||||
|
|
||||||
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…');
|
|
||||||
await input.fill('Was hat Walter an Emma im Krieg geschrieben?');
|
|
||||||
await input.press('Enter');
|
|
||||||
|
|
||||||
// Loading panel announced to screen readers.
|
|
||||||
await expect(page.getByText(/Archiv wird befragt/)).toBeVisible();
|
|
||||||
|
|
||||||
// Directional chip (Walter → Emma) + keyword chip + theme chip render once the fixture resolves.
|
|
||||||
await expect(page.getByText('→')).toBeVisible();
|
|
||||||
await expect(page.getByText('Stichwort: krieg')).toBeVisible();
|
|
||||||
await expect(page.getByText(/Thema:.*Weltkrieg/)).toBeVisible();
|
|
||||||
|
|
||||||
// Accessibility — light mode.
|
|
||||||
const lightScan = await new AxeBuilder({ page })
|
|
||||||
.include('[data-testid="smart-search-results"]')
|
|
||||||
.analyze();
|
|
||||||
expect(lightScan.violations).toEqual([]);
|
|
||||||
|
|
||||||
// Accessibility — dark mode.
|
|
||||||
await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'));
|
|
||||||
const darkScan = await new AxeBuilder({ page })
|
|
||||||
.include('[data-testid="smart-search-results"]')
|
|
||||||
.analyze();
|
|
||||||
expect(darkScan.violations).toEqual([]);
|
|
||||||
await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'));
|
|
||||||
|
|
||||||
// Removing the keyword chip re-runs a keyword GET with the remaining resolved
|
|
||||||
// params (sender + receiver from the directional pair).
|
|
||||||
await page.getByRole('button', { name: 'Filter entfernen: Stichwort: krieg' }).click();
|
|
||||||
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
|
||||||
await expect(page).toHaveURL(/receiverId=22222222-2222-2222-2222-222222222222/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removing the last theme chip drops tag/tagOp but keeps person params', async ({ page }) => {
|
|
||||||
await page.route('**/api/search/nl', async (route) => {
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify(nlResponse)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('/documents');
|
|
||||||
await page.waitForSelector('[data-hydrated]');
|
|
||||||
await page.getByRole('button', { name: /Text/ }).click();
|
|
||||||
|
|
||||||
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…');
|
|
||||||
await input.fill('Was hat Walter an Emma im Krieg geschrieben?');
|
|
||||||
await input.press('Enter');
|
|
||||||
|
|
||||||
await expect(page.getByText(/Thema:.*Weltkrieg/)).toBeVisible();
|
|
||||||
|
|
||||||
// Remove the single theme chip — URL must carry sender UUID but no tag/tagOp.
|
|
||||||
await page.getByRole('button', { name: 'Filter entfernen: Thema: Weltkrieg' }).click();
|
|
||||||
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
|
||||||
const url = page.url();
|
|
||||||
expect(url).not.toMatch(/tag=/);
|
|
||||||
expect(url).not.toMatch(/tagOp=/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -22,18 +22,6 @@
|
|||||||
"error_forbidden": "Sie haben keine Berechtigung für diese Aktion.",
|
"error_forbidden": "Sie haben keine Berechtigung für diese Aktion.",
|
||||||
"error_csrf_token_missing": "Sitzungsfehler. Bitte laden Sie die Seite neu.",
|
"error_csrf_token_missing": "Sitzungsfehler. Bitte laden Sie die Seite neu.",
|
||||||
"error_too_many_login_attempts": "Zu viele Anmeldeversuche. Bitte versuchen Sie es später erneut.",
|
"error_too_many_login_attempts": "Zu viele Anmeldeversuche. Bitte versuchen Sie es später erneut.",
|
||||||
"search_loading_nl": "Archiv wird befragt…",
|
|
||||||
"search_loading_nl_sub": "Die Anfrage wird analysiert…",
|
|
||||||
"search_switch_to_keyword": "Zur Volltextsuche wechseln",
|
|
||||||
"search_filter_remove_label": "Filter entfernen: {label}",
|
|
||||||
"search_chip_sender": "Absender",
|
|
||||||
"search_chip_date": "Zeitraum",
|
|
||||||
"search_chip_keyword": "Stichwort",
|
|
||||||
"search_chip_theme_prefix": "Thema",
|
|
||||||
"search_chip_directional_label": "Von {from} zu {to}, Filter entfernen",
|
|
||||||
"search_disambiguation_trigger_label": "Mehrere Personen gefunden — zum Auswählen klicken",
|
|
||||||
"search_disambiguation_cue": "(auswählen…)",
|
|
||||||
"search_disambiguation_select_label": "{name} auswählen",
|
|
||||||
"error_validation_error": "Die Eingabe ist ungültig.",
|
"error_validation_error": "Die Eingabe ist ungültig.",
|
||||||
"error_internal_error": "Ein unerwarteter Fehler ist aufgetreten.",
|
"error_internal_error": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||||
"nav_documents": "Dokumente",
|
"nav_documents": "Dokumente",
|
||||||
|
|||||||
@@ -22,18 +22,6 @@
|
|||||||
"error_forbidden": "You do not have permission for this action.",
|
"error_forbidden": "You do not have permission for this action.",
|
||||||
"error_csrf_token_missing": "Session error. Please reload the page.",
|
"error_csrf_token_missing": "Session error. Please reload the page.",
|
||||||
"error_too_many_login_attempts": "Too many login attempts. Please try again later.",
|
"error_too_many_login_attempts": "Too many login attempts. Please try again later.",
|
||||||
"search_loading_nl": "Querying the archive…",
|
|
||||||
"search_loading_nl_sub": "Your request is being analysed…",
|
|
||||||
"search_switch_to_keyword": "Switch to full-text search",
|
|
||||||
"search_filter_remove_label": "Remove filter: {label}",
|
|
||||||
"search_chip_sender": "Sender",
|
|
||||||
"search_chip_date": "Period",
|
|
||||||
"search_chip_keyword": "Keyword",
|
|
||||||
"search_chip_theme_prefix": "Topic",
|
|
||||||
"search_chip_directional_label": "From {from} to {to}, remove filter",
|
|
||||||
"search_disambiguation_trigger_label": "Several people found — click to choose",
|
|
||||||
"search_disambiguation_cue": "(choose…)",
|
|
||||||
"search_disambiguation_select_label": "Select {name}",
|
|
||||||
"error_validation_error": "The input is invalid.",
|
"error_validation_error": "The input is invalid.",
|
||||||
"error_internal_error": "An unexpected error occurred.",
|
"error_internal_error": "An unexpected error occurred.",
|
||||||
"nav_documents": "Documents",
|
"nav_documents": "Documents",
|
||||||
|
|||||||
@@ -22,18 +22,6 @@
|
|||||||
"error_forbidden": "No tiene permiso para realizar esta acción.",
|
"error_forbidden": "No tiene permiso para realizar esta acción.",
|
||||||
"error_csrf_token_missing": "Error de sesión. Recargue la página.",
|
"error_csrf_token_missing": "Error de sesión. Recargue la página.",
|
||||||
"error_too_many_login_attempts": "Demasiados intentos. Por favor, inténtelo más tarde.",
|
"error_too_many_login_attempts": "Demasiados intentos. Por favor, inténtelo más tarde.",
|
||||||
"search_loading_nl": "Consultando el archivo…",
|
|
||||||
"search_loading_nl_sub": "Su solicitud está siendo analizada…",
|
|
||||||
"search_switch_to_keyword": "Cambiar a búsqueda de texto completo",
|
|
||||||
"search_filter_remove_label": "Eliminar filtro: {label}",
|
|
||||||
"search_chip_sender": "Remitente",
|
|
||||||
"search_chip_date": "Período",
|
|
||||||
"search_chip_keyword": "Palabra clave",
|
|
||||||
"search_chip_theme_prefix": "Tema",
|
|
||||||
"search_chip_directional_label": "De {from} a {to}, eliminar filtro",
|
|
||||||
"search_disambiguation_trigger_label": "Se encontraron varias personas — haga clic para elegir",
|
|
||||||
"search_disambiguation_cue": "(elegir…)",
|
|
||||||
"search_disambiguation_select_label": "Seleccionar {name}",
|
|
||||||
"error_validation_error": "La entrada no es válida.",
|
"error_validation_error": "La entrada no es válida.",
|
||||||
"error_internal_error": "Se ha producido un error inesperado.",
|
"error_internal_error": "Se ha producido un error inesperado.",
|
||||||
"nav_documents": "Documentos",
|
"nav_documents": "Documentos",
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ $effect(() => {
|
|||||||
onblur={onblur}
|
onblur={onblur}
|
||||||
aria-label={m.docs_search_placeholder()}
|
aria-label={m.docs_search_placeholder()}
|
||||||
placeholder={m.docs_search_placeholder()}
|
placeholder={m.docs_search_placeholder()}
|
||||||
class="block w-full border-line py-2.5 pr-20 pl-10 placeholder-ink-3 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
class="block w-full border-line py-2.5 pr-4 pl-10 placeholder-ink-3 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
/>
|
/>
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
|
|||||||
Reference in New Issue
Block a user