upload volledige repo
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# AGENTS.md
|
||||
|
||||
Dit document beschrijft hoe AI agents in dit project moeten werken.
|
||||
|
||||
## Rollen
|
||||
|
||||
### GPT
|
||||
|
||||
Verantwoordelijk voor:
|
||||
|
||||
- architectuur
|
||||
- analyse
|
||||
- taakopdeling
|
||||
- teststrategie
|
||||
- Codex prompts
|
||||
- review van wijzigingen
|
||||
|
||||
### Codex
|
||||
|
||||
Verantwoordelijk voor:
|
||||
|
||||
- implementeren van kleine wijzigingen
|
||||
- toevoegen van tests
|
||||
- aanpassen van documentatie
|
||||
|
||||
Codex mag **geen architectuur wijzigen** zonder instructie.
|
||||
@@ -0,0 +1,137 @@
|
||||
# API_GOLDEN.md
|
||||
|
||||
Dit document definieert stabiele API responses.
|
||||
Velden mogen niet wijzigen zonder golden tests te updaten.
|
||||
|
||||
## Browse
|
||||
|
||||
### `GET /api/browse`
|
||||
Response shape:
|
||||
```json
|
||||
{
|
||||
"path": "storage1",
|
||||
"directories": [],
|
||||
"files": []
|
||||
}
|
||||
```
|
||||
|
||||
## File Ops (direct)
|
||||
|
||||
### `POST /api/files/mkdir`
|
||||
Success:
|
||||
```json
|
||||
{ "path": "storage1/parent/new_dir" }
|
||||
```
|
||||
|
||||
Conflict (`already_exists`):
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "already_exists",
|
||||
"message": "Target path already exists",
|
||||
"details": { "path": "storage1/parent/new_dir" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Invalid name (`invalid_request`):
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "invalid_request",
|
||||
"message": "Invalid name",
|
||||
"details": { "name": "bad/name" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/files/rename`
|
||||
Success:
|
||||
```json
|
||||
{ "path": "storage1/parent/new_name.ext" }
|
||||
```
|
||||
|
||||
Conflict (`already_exists`) + invalid name (`invalid_request`) gebruiken dezelfde error-shape als mkdir.
|
||||
|
||||
### `POST /api/files/delete`
|
||||
Success:
|
||||
```json
|
||||
{ "path": "storage1/parent/file_or_empty_dir" }
|
||||
```
|
||||
|
||||
Non-empty directory:
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "directory_not_empty",
|
||||
"message": "Directory is not empty",
|
||||
"details": { "path": "storage1/parent/non_empty" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Task-based create endpoints
|
||||
|
||||
### `POST /api/files/copy`
|
||||
### `POST /api/files/move`
|
||||
Success (202):
|
||||
```json
|
||||
{
|
||||
"task_id": "<uuid>",
|
||||
"status": "queued"
|
||||
}
|
||||
```
|
||||
|
||||
## Tasks read endpoints
|
||||
|
||||
### `GET /api/tasks`
|
||||
Response shape:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"operation": "copy",
|
||||
"status": "running",
|
||||
"source": "storage1/a.txt",
|
||||
"destination": "storage2/a.txt",
|
||||
"created_at": "2026-03-10T10:00:00Z",
|
||||
"finished_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/tasks/{task_id}`
|
||||
Response shape:
|
||||
```json
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"operation": "move",
|
||||
"status": "running",
|
||||
"source": "storage1/a.txt",
|
||||
"destination": "storage2/a.txt",
|
||||
"done_bytes": 1024,
|
||||
"total_bytes": 4096,
|
||||
"done_items": null,
|
||||
"total_items": null,
|
||||
"current_item": "storage1/a.txt",
|
||||
"failed_item": null,
|
||||
"error_code": null,
|
||||
"error_message": null,
|
||||
"created_at": "2026-03-10T10:00:00Z",
|
||||
"started_at": "2026-03-10T10:00:01Z",
|
||||
"finished_at": null
|
||||
}
|
||||
```
|
||||
|
||||
Task not found:
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "task_not_found",
|
||||
"message": "Task was not found",
|
||||
"details": { "task_id": "task-missing" }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
# BACKEND_V1_CONSOLIDATION.md
|
||||
|
||||
## Doel
|
||||
Consolidatie van de huidige backend v1 contracten en beperkingen.
|
||||
|
||||
## Huidige endpoints
|
||||
|
||||
Directe endpoints (geen task-creatie):
|
||||
- `GET /api/browse`
|
||||
- `POST /api/files/mkdir`
|
||||
- `POST /api/files/rename`
|
||||
- `POST /api/files/delete`
|
||||
- `GET /api/tasks`
|
||||
- `GET /api/tasks/{task_id}`
|
||||
|
||||
Task-based create endpoints:
|
||||
- `POST /api/files/copy`
|
||||
- `POST /api/files/move`
|
||||
|
||||
## Semantiek per endpoint
|
||||
|
||||
### `GET /api/browse`
|
||||
- Browse van een directory binnen whitelist roots.
|
||||
- Hidden files standaard verborgen, optioneel via `show_hidden=true`.
|
||||
|
||||
### `POST /api/files/mkdir`
|
||||
- Maakt directory op exact `parent_path + name`.
|
||||
- Geen impliciete pad-normalisatie buiten `path_guard` validatie.
|
||||
|
||||
### `POST /api/files/rename`
|
||||
- Hernoemt binnen dezelfde parent directory.
|
||||
- Geen verborgen move naar andere map.
|
||||
|
||||
### `POST /api/files/delete`
|
||||
- Verwijdert file direct.
|
||||
- Verwijdert alleen lege directory.
|
||||
- Non-empty directory => conflict.
|
||||
|
||||
### `POST /api/files/copy`
|
||||
- File-only copy.
|
||||
- `destination` is volledig doelpad (geen "copy into directory").
|
||||
- Task wordt aangemaakt (`202`, `task_id`, `queued`) en daarna uitgevoerd.
|
||||
|
||||
### `POST /api/files/move`
|
||||
- File-only move.
|
||||
- `destination` is volledig doelpad.
|
||||
- Task wordt aangemaakt (`202`, `task_id`, `queued`) en daarna uitgevoerd.
|
||||
- Same-root: native move.
|
||||
- Cross-root: copy + delete binnen dezelfde task.
|
||||
|
||||
### `GET /api/tasks`
|
||||
- Lijst van tasks, gesorteerd op `created_at DESC`.
|
||||
- Item bevat minimaal: `id`, `operation`, `status`, `source`, `destination`, `created_at`, `finished_at`.
|
||||
|
||||
### `GET /api/tasks/{task_id}`
|
||||
- Detailstatus inclusief progress/foutvelden.
|
||||
- Statusset: `queued`, `running`, `completed`, `failed`.
|
||||
|
||||
## Foutmodel per endpointgroep
|
||||
|
||||
### Browse/file-validatie
|
||||
- `invalid_request`
|
||||
- `path_traversal_detected`
|
||||
- `path_outside_whitelist`
|
||||
- `invalid_root_alias`
|
||||
- `path_not_found`
|
||||
- `type_conflict`
|
||||
- `already_exists`
|
||||
- `directory_not_empty` (delete)
|
||||
|
||||
### Task create runtime
|
||||
- validatiefouten vóór task-creatie: directe API-fout, geen task
|
||||
- runtime-fouten tijdens task-uitvoering: task naar `failed` met `io_error`
|
||||
|
||||
### Tasks read
|
||||
- `task_not_found` bij onbekend task id
|
||||
|
||||
## Expliciete v1-beperkingen
|
||||
|
||||
- copy: file-only
|
||||
- move: file-only
|
||||
- delete: alleen file + lege directory
|
||||
- geen recursive delete
|
||||
- geen directory copy/move
|
||||
- geen rollback
|
||||
- geen cancel/retry
|
||||
- geen batch-operaties
|
||||
@@ -0,0 +1,21 @@
|
||||
# BOOKMARKS_V1_CONSOLIDATION.md
|
||||
|
||||
## Endpoints
|
||||
- `POST /api/bookmarks`
|
||||
- `GET /api/bookmarks`
|
||||
- `DELETE /api/bookmarks/{bookmark_id}`
|
||||
|
||||
## Duplicate policy
|
||||
- Een bookmark is uniek op `path`.
|
||||
- Een tweede create met hetzelfde pad geeft `409 already_exists`.
|
||||
|
||||
## Validatie
|
||||
- `path` wordt centraal via `path_guard.resolve_path(...)` gevalideerd.
|
||||
- Dit dekt whitelist, traversal en root-alias validatie.
|
||||
- `label` mag niet leeg zijn (`trim()`), anders `400 invalid_request`.
|
||||
|
||||
## Model v1
|
||||
- `id`
|
||||
- `path`
|
||||
- `label`
|
||||
- `created_at`
|
||||
@@ -0,0 +1,22 @@
|
||||
# CHANGE_POLICY.md
|
||||
|
||||
## Toegestaan zonder toestemming
|
||||
|
||||
- documentatie verbeteren
|
||||
- tests toevoegen
|
||||
- logging verbeteren
|
||||
- kleine bugfixes
|
||||
|
||||
## Eerst voorstel nodig
|
||||
|
||||
- API wijzigingen
|
||||
- dependency toevoegen
|
||||
- database schema wijziging
|
||||
- frontend flow aanpassen
|
||||
|
||||
## Expliciete goedkeuring vereist
|
||||
|
||||
- security model aanpassen
|
||||
- whitelist wijzigen
|
||||
- directory structuur aanpassen
|
||||
- destructieve acties veranderen
|
||||
@@ -0,0 +1,274 @@
|
||||
# COPY_MOVE_TASKS_DESIGN.md
|
||||
|
||||
## Doel
|
||||
Ontwerpvoorstel voor de volgende implementatieslice: `copy`, `move` en `tasks`.
|
||||
|
||||
Dit document bevat alleen ontwerpkeuzes. Er is geen code-implementatie in deze stap.
|
||||
|
||||
## 1. Destination-semantiek (expliciet)
|
||||
|
||||
V1 kiest een strikte semantiek:
|
||||
- `destination` is altijd het volledige beoogde eindpad (inclusief bestandsnaam).
|
||||
- `destination` mag in v1 **niet** geïnterpreteerd worden als "copy/move into existing directory".
|
||||
- Als `destination` al bestaat (file of directory) -> `already_exists` (409).
|
||||
|
||||
Voorbeeld v1:
|
||||
- source: `storage1/a/file.txt`
|
||||
- destination: `storage2/b/file_copy.txt`
|
||||
|
||||
Niet toegestaan als impliciete directory-target in v1:
|
||||
- destination: `storage2/b/` met verwachting dat `file.txt` automatisch eronder komt.
|
||||
|
||||
Reden:
|
||||
- voorkomt ambigu gedrag
|
||||
- eenvoudiger API-contract
|
||||
- minder regressierisico
|
||||
|
||||
## 2. Scopevoorstel `copy` v1
|
||||
|
||||
### Keuze
|
||||
- `copy` ondersteunt in v1 **alleen files**.
|
||||
- directory-recursie schuift door naar een latere fase.
|
||||
|
||||
### Motivatie
|
||||
- Complexiteit: recursieve copy voegt veel edge-cases toe (diepe bomen, symlinks, partial failures).
|
||||
- Testbaarheid: file-only maakt golden/regressie tests kleiner en deterministischer.
|
||||
- Regressierisico: lagere kans op onverwachte performance/security regressies.
|
||||
|
||||
### Conflictgedrag
|
||||
- destination bestaat al -> `already_exists` (409).
|
||||
- geen overwrite/merge in v1.
|
||||
|
||||
### Uitvoering
|
||||
- altijd task-based (async).
|
||||
- create response: `task_id` + `queued`.
|
||||
|
||||
### Foutmodel copy
|
||||
- `invalid_request` (400)
|
||||
- `path_traversal_detected` (403)
|
||||
- `path_outside_whitelist` (403)
|
||||
- `invalid_root_alias` (403)
|
||||
- `path_not_found` (404)
|
||||
- `type_conflict` (409) (source is geen file)
|
||||
- `already_exists` (409)
|
||||
- `io_error` (500)
|
||||
|
||||
## 3. Scopevoorstel `move` v1
|
||||
|
||||
### Rename vs move
|
||||
- `rename` blijft naamwijziging binnen dezelfde parent directory.
|
||||
- `move` is padwijziging naar een ander eindpad (zelfde root of cross-root).
|
||||
|
||||
### Cross-root gedrag
|
||||
- toegestaan als source en destination beide binnen whitelist vallen.
|
||||
- zelfde root: native rename/move waar mogelijk.
|
||||
- cross-root: copy+delete binnen dezelfde task.
|
||||
|
||||
### Keuze
|
||||
- `move` ondersteunt in v1 **alleen files**.
|
||||
- directory-move buiten scope in v1.
|
||||
|
||||
### Conflictgedrag
|
||||
- destination bestaat al -> `already_exists` (409).
|
||||
- geen overwrite in v1.
|
||||
|
||||
### Foutmodel move
|
||||
- `invalid_request` (400)
|
||||
- `path_traversal_detected` (403)
|
||||
- `path_outside_whitelist` (403)
|
||||
- `invalid_root_alias` (403)
|
||||
- `path_not_found` (404)
|
||||
- `type_conflict` (409) (source is geen file)
|
||||
- `already_exists` (409)
|
||||
- `io_error` (500)
|
||||
|
||||
## 4. Symlinkbeleid (copy/move)
|
||||
|
||||
### Source
|
||||
- v1 file-only: source mag geen symlink zijn.
|
||||
- als source symlink resolve’t naar pad buiten whitelist -> geblokkeerd (`path_outside_whitelist`).
|
||||
- als source symlink resolve’t binnen whitelist: in v1 alsnog afwijzen als `type_conflict` om semantiek simpel te houden.
|
||||
|
||||
### Destination
|
||||
- destination wordt via `path_guard` canoniek gevalideerd.
|
||||
- destination parent moet binnen whitelist liggen.
|
||||
- destination parent die via symlink buiten whitelist valt -> blokkeren (`path_outside_whitelist`).
|
||||
|
||||
### Recursieve escapes
|
||||
- Niet van toepassing in v1 (geen directory-recursie).
|
||||
- Voor latere directory-copy geldt: elk bezocht pad moet per entry containment-check krijgen.
|
||||
|
||||
## 5. Task persistence en history
|
||||
|
||||
### Relatie tasks vs history
|
||||
- In v1 zijn `tasks` en `history` aparte modellen/tabelrollen:
|
||||
- `tasks`: actuele en afgeronde task-status/progress
|
||||
- `history`: audit-log van uitgevoerde operaties
|
||||
- history kan vanuit task-completion gevuld worden, maar is niet hetzelfde model.
|
||||
|
||||
### Retentie
|
||||
- v1 bewaart `completed` en `failed` tasks persistent (geen automatische cleanup in scope).
|
||||
|
||||
### Sortering GET /api/tasks
|
||||
- standaard sortering: `created_at DESC` (nieuwste eerst).
|
||||
|
||||
## 6. Taskmodel
|
||||
|
||||
### Wanneer task verplicht is
|
||||
- `copy` en `move` altijd task-based.
|
||||
|
||||
### Statussen
|
||||
- `queued`
|
||||
- `running`
|
||||
- `completed`
|
||||
- `failed`
|
||||
|
||||
### Progress
|
||||
- file-only v1:
|
||||
- `done_bytes`
|
||||
- `total_bytes`
|
||||
- `done_items`/`total_items` optioneel en kunnen `null` blijven in v1.
|
||||
|
||||
### Partial failure
|
||||
- fail-fast.
|
||||
- eerste fatale fout -> `failed`.
|
||||
- geen rollback in v1.
|
||||
- taskdetail bevat `failed_item`, `error_code`, `error_message`.
|
||||
|
||||
### Duplicate create requests (correctie)
|
||||
- v1 gedrag is **niet idempotent**.
|
||||
- twee identieke requests mogen twee verschillende tasks creëren.
|
||||
|
||||
## 7. API-voorstel
|
||||
|
||||
### Endpoints
|
||||
- `POST /api/files/copy`
|
||||
- `POST /api/files/move`
|
||||
- `GET /api/tasks/{task_id}`
|
||||
- `GET /api/tasks`
|
||||
|
||||
### Request/response shapes
|
||||
|
||||
#### POST /api/files/copy
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"source": "storage1/path/file.txt",
|
||||
"destination": "storage2/path/file_copy.txt"
|
||||
}
|
||||
```
|
||||
Response (202):
|
||||
```json
|
||||
{
|
||||
"task_id": "<uuid>",
|
||||
"status": "queued"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/files/move
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"source": "storage1/path/file.txt",
|
||||
"destination": "storage2/path/file_moved.txt"
|
||||
}
|
||||
```
|
||||
Response (202):
|
||||
```json
|
||||
{
|
||||
"task_id": "<uuid>",
|
||||
"status": "queued"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/tasks/{task_id}
|
||||
Response (200):
|
||||
```json
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"operation": "copy",
|
||||
"status": "running",
|
||||
"source": "storage1/a/file.txt",
|
||||
"destination": "storage2/b/file.txt",
|
||||
"done_bytes": 1024,
|
||||
"total_bytes": 4096,
|
||||
"done_items": null,
|
||||
"total_items": null,
|
||||
"current_item": "storage1/a/file.txt",
|
||||
"failed_item": null,
|
||||
"error_code": null,
|
||||
"error_message": null,
|
||||
"created_at": "2026-03-10T00:00:00Z",
|
||||
"started_at": "2026-03-10T00:00:01Z",
|
||||
"finished_at": null
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/tasks
|
||||
Response (200):
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"operation": "move",
|
||||
"status": "completed",
|
||||
"source": "storage1/a/file.txt",
|
||||
"destination": "storage2/b/file.txt",
|
||||
"created_at": "2026-03-10T00:00:00Z",
|
||||
"finished_at": "2026-03-10T00:00:05Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Waarom `source` en `destination` ook in task-list
|
||||
- Ja, opnemen in `GET /api/tasks`.
|
||||
- Reden: operator kan zonder extra detail-calls direct zien wat elke task doet.
|
||||
- Dit maakt UI en troubleshooting eenvoudiger.
|
||||
|
||||
### Error codes (versmald)
|
||||
- `invalid_request` (400)
|
||||
- `path_traversal_detected` (403)
|
||||
- `path_outside_whitelist` (403)
|
||||
- `invalid_root_alias` (403)
|
||||
- `path_not_found` (404)
|
||||
- `task_not_found` (404)
|
||||
- `type_conflict` (409)
|
||||
- `already_exists` (409)
|
||||
- `io_error` (500)
|
||||
|
||||
`internal_error` wordt in v1 niet als aparte publieke code gebruikt; onverwachte fouten worden genormaliseerd naar `io_error` of framework-500.
|
||||
|
||||
## 8. Teststrategie
|
||||
|
||||
### Golden tests
|
||||
- `POST /api/files/copy` success (task create shape)
|
||||
- `POST /api/files/move` success (task create shape)
|
||||
- `already_exists` voor copy/move
|
||||
- `type_conflict` als source geen file is
|
||||
- `invalid_request` voor ongeldige payload
|
||||
- traversal/root errors voor source en destination
|
||||
- `GET /api/tasks/{task_id}` shapes voor `queued/running/completed/failed`
|
||||
- `GET /api/tasks` list shape inclusief `source` en `destination`
|
||||
|
||||
### Regressietests
|
||||
- cross-root move gebruikt copy+delete pad correct
|
||||
- file copy/move met unicode namen
|
||||
- grote files progress updates blijven monotone stijging
|
||||
- duplicate create requests leveren 2 verschillende `task_id`s (niet-idempotent)
|
||||
|
||||
### Securitytests
|
||||
- traversal blokkade op source/destination
|
||||
- destination outside whitelist blokkade
|
||||
- invalid root alias blokkade
|
||||
- symlink source wordt afgewezen
|
||||
- symlinked destination parent buiten whitelist wordt afgewezen
|
||||
|
||||
## Implementatiegrenzen voor volgende stap
|
||||
- Geen wijziging aan bestaande endpoints:
|
||||
- browse
|
||||
- mkdir
|
||||
- rename
|
||||
- delete
|
||||
- Geen nieuwe dependencies zonder expliciete motivatie.
|
||||
@@ -0,0 +1,50 @@
|
||||
# COPY_V1_CONSOLIDATION.md
|
||||
|
||||
## Task lifecycle (copy v1)
|
||||
|
||||
`POST /api/files/copy`:
|
||||
1. Request wordt gevalideerd.
|
||||
2. Bij geldige input wordt direct een task aangemaakt met status `queued`.
|
||||
3. Een achtergrond-runner zet de task op `running` en vult progress (`done_bytes`, `total_bytes`).
|
||||
4. Eindstatus:
|
||||
- `completed` bij succesvolle file copy
|
||||
- `failed` bij runtime I/O fout (`error_code = io_error`)
|
||||
|
||||
## Validatiefouten vs runtime-fouten
|
||||
|
||||
Validatiefouten (voor task-creatie):
|
||||
- `invalid_request`
|
||||
- `path_traversal_detected`
|
||||
- `path_outside_whitelist`
|
||||
- `invalid_root_alias`
|
||||
- `path_not_found`
|
||||
- `type_conflict`
|
||||
- `already_exists`
|
||||
|
||||
Gedrag:
|
||||
- request faalt direct met error response
|
||||
- er wordt geen task aangemaakt
|
||||
|
||||
Runtime-fouten (na task-creatie):
|
||||
- `io_error`
|
||||
|
||||
Gedrag:
|
||||
- request zelf retourneert `202` met `task_id`
|
||||
- task gaat naar `failed`
|
||||
- foutdetails verschijnen via `GET /api/tasks/{task_id}`
|
||||
|
||||
## Copy metadata in v1 (`copy_file(...)`)
|
||||
|
||||
V1 kopieert:
|
||||
- file-inhoud (byte stream)
|
||||
- basic filesystem metadata via `copystat` (mtime/atime/mode waar ondersteund)
|
||||
|
||||
V1 doet niet expliciet:
|
||||
- ownership/ACL normalisatie
|
||||
- extended attributes beleid
|
||||
|
||||
## Destination-semantiek (expliciet)
|
||||
|
||||
`destination` blijft in v1 altijd het volledige doelpad.
|
||||
- Geen impliciete "copy into existing directory" interpretatie.
|
||||
- Als destination al bestaat: `already_exists`.
|
||||
@@ -0,0 +1,49 @@
|
||||
# MKDIR_RENAME_CONSOLIDATION.md
|
||||
|
||||
## Gedragsbevestiging
|
||||
|
||||
- `POST /api/files/rename` werkt alleen binnen dezelfde parent directory.
|
||||
- De target wordt opgebouwd als `parent(source_path) + new_name`.
|
||||
- `rename` laat geen verborgen move toe.
|
||||
- `new_name` accepteert geen padsegmenten (`/`, `\\`, `..`), dus geen directorywissel.
|
||||
- `validate_name` wordt toegepast op:
|
||||
- `mkdir.name`
|
||||
- `rename.new_name`
|
||||
|
||||
## Error model voor mkdir/rename
|
||||
|
||||
Standaard error shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "...",
|
||||
"message": "...",
|
||||
"details": {"...": "..."}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ondersteunde codes in deze slice:
|
||||
|
||||
- `invalid_request`
|
||||
- ongeldige naam (`..`, leeg, of met `/`/`\\`)
|
||||
- HTTP `400`
|
||||
- `path_traversal_detected`
|
||||
- traversal in aangeleverd pad (`../`)
|
||||
- HTTP `403`
|
||||
- `path_not_found`
|
||||
- bronpad of parent pad bestaat niet
|
||||
- HTTP `404`
|
||||
- `already_exists`
|
||||
- doelpad bestaat al (file of directory)
|
||||
- HTTP `409`
|
||||
- `io_error`
|
||||
- onverwachte filesystem fout tijdens operatie
|
||||
- HTTP `500`
|
||||
|
||||
## Scopebevestiging
|
||||
|
||||
Deze consolidatie wijzigt niet:
|
||||
- browse endpoint
|
||||
- delete/copy/move/tasks/frontend
|
||||
@@ -0,0 +1,123 @@
|
||||
# PRODUCT_SPEC.md
|
||||
|
||||
## Project
|
||||
|
||||
Web-based Storage Manager
|
||||
|
||||
Een webapplicatie waarmee gebruikers bestanden op
|
||||
whitelisted storage volumes kunnen beheren.
|
||||
|
||||
De applicatie draait in een containeromgeving.
|
||||
|
||||
---
|
||||
|
||||
# Doel
|
||||
|
||||
Een veilige webinterface bouwen voor filesystem beheer.
|
||||
|
||||
Gebruikers moeten via een browser:
|
||||
|
||||
- directories kunnen browsen
|
||||
- bestanden kunnen kopiëren
|
||||
- bestanden kunnen verplaatsen
|
||||
- bestanden kunnen hernoemen
|
||||
- bestanden kunnen verwijderen
|
||||
- mappen kunnen aanmaken
|
||||
- bookmarks kunnen opslaan
|
||||
|
||||
De applicatie moet geschikt zijn voor gebruik binnen een
|
||||
self-hosted infrastructuur.
|
||||
|
||||
---
|
||||
|
||||
# Architectuur uitgangspunten
|
||||
|
||||
Backend
|
||||
|
||||
Python + FastAPI
|
||||
|
||||
Frontend
|
||||
|
||||
lichte JS UI zonder zware frameworks.
|
||||
|
||||
Database
|
||||
|
||||
SQLite voor:
|
||||
|
||||
- tasks
|
||||
- bookmarks
|
||||
- history
|
||||
|
||||
---
|
||||
|
||||
# Storage model
|
||||
|
||||
De applicatie mag alleen werken binnen
|
||||
**whitelisted root directories**.
|
||||
|
||||
Voorbeeld:
|
||||
|
||||
/mnt/storage1
|
||||
/mnt/storage2
|
||||
/media/archive
|
||||
|
||||
Alle paden moeten binnen deze roots blijven.
|
||||
|
||||
Traversal en symlink escapes moeten geblokkeerd worden.
|
||||
|
||||
---
|
||||
|
||||
# Functionaliteiten
|
||||
|
||||
## Directory browsing
|
||||
|
||||
De UI moet directories kunnen tonen.
|
||||
|
||||
Response moet bevatten:
|
||||
|
||||
- directories
|
||||
- files
|
||||
- metadata (size, modified)
|
||||
|
||||
---
|
||||
|
||||
## File operations
|
||||
|
||||
Ondersteunde acties:
|
||||
|
||||
rename
|
||||
move
|
||||
copy
|
||||
delete
|
||||
create directory
|
||||
|
||||
---
|
||||
|
||||
## Task system
|
||||
|
||||
Langlopende acties (copy/move):
|
||||
|
||||
- krijgen een task id
|
||||
- status wordt opgeslagen
|
||||
- progress kan opgehaald worden via API
|
||||
|
||||
---
|
||||
|
||||
# Security eisen
|
||||
|
||||
- path validation
|
||||
- whitelist enforcement
|
||||
- symlink protection
|
||||
- traversal protection
|
||||
|
||||
---
|
||||
|
||||
# Out of scope
|
||||
|
||||
Deze functionaliteit hoort **niet** bij versie 1:
|
||||
|
||||
- user management
|
||||
- permissions management
|
||||
- cloud storage
|
||||
- distributed storage
|
||||
- multi-node clusters
|
||||
@@ -0,0 +1,20 @@
|
||||
# SAFE_FILES.md
|
||||
|
||||
## Safe to edit
|
||||
|
||||
- backend code
|
||||
- frontend js
|
||||
- tests
|
||||
- documentatie
|
||||
|
||||
## Ask first
|
||||
|
||||
- database schema
|
||||
- container config
|
||||
- security modules
|
||||
|
||||
## Do not edit
|
||||
|
||||
- deployment configs
|
||||
- storage volume mappings
|
||||
- whitelist configuration
|
||||
@@ -0,0 +1,72 @@
|
||||
# STEP1_5_CONSOLIDATION.md
|
||||
|
||||
## Doel
|
||||
Consolidatie van stap 1 t/m 5 (backend skeleton, path_guard, unit tests, browse endpoint, golden tests).
|
||||
|
||||
## 1. Test-run context
|
||||
|
||||
Tests draaien vanuit repository root met:
|
||||
- `PYTHONPATH=webui`
|
||||
|
||||
Definitieve testcommando's voor deze fase:
|
||||
- `PYTHONPATH=webui python3 -m unittest discover -s webui/backend/tests/unit -p "test_*.py" -v`
|
||||
- `PYTHONPATH=webui python3 -m unittest discover -s webui/backend/tests/golden -p "test_*.py" -v`
|
||||
- `PYTHONPATH=webui python3 -m unittest discover -s webui/backend/tests -p "test_*.py" -v`
|
||||
|
||||
## 2. Minimale dependencies voor stap 1-5
|
||||
|
||||
Vastgelegd in:
|
||||
- `webui/backend/requirements.txt`
|
||||
|
||||
Benodigd voor deze fase:
|
||||
- FastAPI app + browse endpoint
|
||||
- unit tests
|
||||
- golden tests
|
||||
|
||||
Dependencies:
|
||||
- `fastapi==0.111.0`
|
||||
- `starlette==0.37.2`
|
||||
- `pydantic==2.12.5`
|
||||
- `httpx==0.27.2`
|
||||
- `anyio==4.4.0`
|
||||
- `sniffio==1.3.1`
|
||||
|
||||
## 3. Minimale wijzigingen in deze fase
|
||||
|
||||
Functioneel gewijzigde bestanden:
|
||||
- `webui/backend/app/api/routes_browse.py`
|
||||
- `webui/backend/app/dependencies.py`
|
||||
- `webui/backend/app/main.py`
|
||||
|
||||
Testmatig gewijzigde bestanden:
|
||||
- `webui/backend/tests/golden/test_api_browse_golden.py`
|
||||
- `webui/backend/tests/golden/test_api_errors_golden.py`
|
||||
|
||||
Waarom golden tests van TestClient naar `httpx.ASGITransport` zijn omgezet:
|
||||
- In deze omgeving blokkeerde de TestClient-runner op sync execution pad.
|
||||
- `httpx.ASGITransport` voert requests in-memory tegen de ASGI app uit zonder die blokkade.
|
||||
- Dit is een testuitvoerbaarheidsfix, geen API-contractwijziging.
|
||||
|
||||
Waarom async wijzigingen nodig waren:
|
||||
- Error-pad en dependency-pad moesten async blijven om runtime-blokkade in deze omgeving te vermijden.
|
||||
- De wijzigingen zijn intern uitvoeringsgericht; het HTTP-contract blijft gelijk.
|
||||
|
||||
## 4. Contractbehoud (expliciet)
|
||||
|
||||
Door deze consolidatiestap is niets gewijzigd aan:
|
||||
- endpointpad: `GET /api/browse`
|
||||
- query parameters: `path`, `show_hidden`
|
||||
- response shape: `path`, `directories[]`, `files[]` met dezelfde velden
|
||||
- error codes: o.a. `invalid_root_alias`, `path_traversal_detected`, `path_not_found`, `path_type_conflict`
|
||||
- hidden files policy: default hidden uit, optioneel via `show_hidden=true`
|
||||
|
||||
## 5. Scopebevestiging
|
||||
|
||||
Niet geïmplementeerd in deze consolidatiestap:
|
||||
- rename
|
||||
- mkdir
|
||||
- delete
|
||||
- copy
|
||||
- move
|
||||
- tasks/worker
|
||||
- frontend
|
||||
@@ -0,0 +1,34 @@
|
||||
# TEST_STRATEGY.md
|
||||
|
||||
## Unit tests
|
||||
|
||||
Test individuele functies:
|
||||
|
||||
- path validation
|
||||
- whitelist checks
|
||||
- helper functions
|
||||
- task status transitions
|
||||
|
||||
## Feature tests
|
||||
|
||||
Test volledige flows:
|
||||
|
||||
- browse directory
|
||||
- copy
|
||||
- move
|
||||
- rename
|
||||
- delete
|
||||
- create folder
|
||||
|
||||
## Regression tests
|
||||
|
||||
Bescherm tegen bekende problemen:
|
||||
|
||||
- path traversal
|
||||
- unicode filenames
|
||||
- grote directories
|
||||
- nested directories
|
||||
|
||||
## API Golden tests
|
||||
|
||||
Controleer dat response formats niet veranderen.
|
||||
@@ -0,0 +1,233 @@
|
||||
# UI_DUAL_PANE_DESIGN.md
|
||||
|
||||
## Doel van deze notitie
|
||||
|
||||
Deze notitie beschrijft de ontwerpstap voor **Dual-pane UI v2** op basis van de huidige v1.1 UI en `UI_VISION_MC.md`.
|
||||
|
||||
Randvoorwaarden voor deze stap:
|
||||
- geen backendwijzigingen
|
||||
- geen nieuwe dependencies
|
||||
- geen multi-select
|
||||
- geen viewer/editor
|
||||
- geen keyboard-uitbreiding (behalve als latere notitie)
|
||||
|
||||
---
|
||||
|
||||
## 1) Ombouw van single-page UI naar twee panelen
|
||||
|
||||
### Huidige situatie (v1.1)
|
||||
- één browsepaneel met één `currentPath`
|
||||
- globale selectie voor dat paneel
|
||||
- acties (mkdir/rename/delete/copy/move) vanuit die ene context
|
||||
|
||||
### Doelsituatie (v2)
|
||||
- twee browsepanelen naast elkaar: `left` en `right`
|
||||
- elk paneel heeft:
|
||||
- eigen path input + go
|
||||
- eigen breadcrumbs
|
||||
- eigen directory/file lijst
|
||||
- eigen geselecteerd item
|
||||
- één paneel is altijd **actief**
|
||||
- acties worden contextueel uitgevoerd vanuit actief paneel
|
||||
- tasks- en bookmarks-sectie blijven gedeeld (onder of naast de panelen)
|
||||
|
||||
### UI-structuur op hoog niveau
|
||||
- Header: algemene status
|
||||
- Main layout:
|
||||
- Paneel links (`left-pane`)
|
||||
- Paneel rechts (`right-pane`)
|
||||
- Utility-kolom (tasks + bookmarks)
|
||||
- Actieknoppen:
|
||||
- per paneel (mkdir)
|
||||
- actieve-paneelacties (rename/delete/copy/move)
|
||||
|
||||
### Bookmark-open gedrag (expliciete keuze)
|
||||
- Een klik op een bookmark opent altijd in het **actieve paneel**.
|
||||
- Motivatie (kort): dit is het meest voorspelbaar, sluit aan op het active/inactive-pane model, en voorkomt verborgen side-effects in het niet-actieve paneel.
|
||||
|
||||
---
|
||||
|
||||
## 2) Benodigde state per paneel
|
||||
|
||||
## State-model
|
||||
|
||||
```js
|
||||
state = {
|
||||
panes: {
|
||||
left: {
|
||||
currentPath: "storage1",
|
||||
showHidden: false,
|
||||
selectedItem: null, // { path, name, kind: "file"|"directory" }
|
||||
entries: { directories: [], files: [] }
|
||||
},
|
||||
right: {
|
||||
currentPath: "storage1",
|
||||
showHidden: false,
|
||||
selectedItem: null,
|
||||
entries: { directories: [], files: [] }
|
||||
}
|
||||
},
|
||||
activePane: "left", // "left" | "right"
|
||||
selectedTaskId: null,
|
||||
pollHandle: null
|
||||
}
|
||||
```
|
||||
|
||||
### Verplichte kernstate
|
||||
- `current path` per paneel
|
||||
- `selected item` per paneel
|
||||
- `active pane` globaal
|
||||
|
||||
### Afgeleide helperstate
|
||||
- `inactivePane = activePane === "left" ? "right" : "left"`
|
||||
- `canRename/canDelete` op basis van selectie actief paneel
|
||||
- `canCopy/canMove` alleen als selectie actief paneel een file is
|
||||
|
||||
---
|
||||
|
||||
## 3) Copy/move van actief paneel naar ander paneel
|
||||
|
||||
## Semantiek v2
|
||||
- Bron komt altijd uit `selectedItem` van `activePane`.
|
||||
- Doelcontext is standaard het **andere paneel**.
|
||||
- `destination` blijft volledig doelpad (geen impliciete shell-achtige "copy into" semantics).
|
||||
|
||||
### Destination-bepaling
|
||||
- `targetBasePath = panes[inactivePane].currentPath`
|
||||
- `destination = targetBasePath + "/" + sourceItem.name`
|
||||
- UI toont dit voorstel in prompt/confirm en laat aanpassen toe.
|
||||
|
||||
### Copy flow
|
||||
1. User selecteert file in actief paneel.
|
||||
2. User kiest `Copy`.
|
||||
3. UI bouwt default destination op basis van inactieve paneel-path + bestandsnaam.
|
||||
4. UI roept `POST /api/files/copy` met `{source, destination}`.
|
||||
5. Bij `202`: tasks refresh + taskdetail naar nieuw task_id.
|
||||
6. Na start: beide panelen refresh (best effort), zodat user contextueel resultaat ziet.
|
||||
|
||||
### Move flow
|
||||
1. Zelfde als copy, maar endpoint `POST /api/files/move`.
|
||||
2. Bij `202`: tasks refresh + taskdetail selecteren.
|
||||
3. Beide panelen refreshen (source kan verdwijnen, destination verschijnen).
|
||||
|
||||
### Refresh-semantiek per actie (expliciet)
|
||||
- `mkdir`: refresh alleen het actieve paneel.
|
||||
- `rename`: refresh alleen het actieve paneel.
|
||||
- `delete`: refresh alleen het actieve paneel.
|
||||
- `copy`: refresh beide panelen.
|
||||
- `move`: refresh beide panelen.
|
||||
|
||||
Rationale (kort):
|
||||
- Lokale mutaties (`mkdir/rename/delete`) zijn paneelgebonden en blijven daardoor snel en voorspelbaar.
|
||||
- Transfer-acties (`copy/move`) raken bron- en doelcontext, dus beide panelen verversen voorkomt stale state.
|
||||
|
||||
### Foutafhandeling
|
||||
- Validatiefouten vóór task-creatie direct tonen op actiegebied actief paneel.
|
||||
- Runtime-fouten via task-status (`failed`) zichtbaar in tasklijst/detail.
|
||||
|
||||
---
|
||||
|
||||
## 4) Bestaande backend-endpoints die hergebruikt worden
|
||||
|
||||
Geen contractwijzigingen; hergebruik van bestaande endpoints:
|
||||
|
||||
Browse/file ops:
|
||||
- `GET /api/browse`
|
||||
- `POST /api/files/mkdir`
|
||||
- `POST /api/files/rename`
|
||||
- `POST /api/files/delete`
|
||||
- `POST /api/files/copy`
|
||||
- `POST /api/files/move`
|
||||
|
||||
Tasks:
|
||||
- `GET /api/tasks`
|
||||
- `GET /api/tasks/{task_id}`
|
||||
|
||||
Bookmarks:
|
||||
- `GET /api/bookmarks`
|
||||
- `POST /api/bookmarks`
|
||||
- `DELETE /api/bookmarks/{bookmark_id}`
|
||||
|
||||
---
|
||||
|
||||
## 5) Waarschijnlijk te wijzigen bestanden
|
||||
|
||||
Primair frontend:
|
||||
- `webui/html/index.html`
|
||||
- layout naar dual-pane
|
||||
- pane-specifieke controls/containers
|
||||
- `webui/html/app.js`
|
||||
- state refactor naar `panes.left/right`
|
||||
- browse/render/actions per pane
|
||||
- active-pane switching
|
||||
- copy/move destination vanuit ander pane
|
||||
- `webui/html/style.css`
|
||||
- twee panelen visueel gelijkwaardig
|
||||
- actieve-paneel-highlight
|
||||
- responsieve fallback (mobiel: gestapeld)
|
||||
|
||||
Tests:
|
||||
- `webui/backend/tests/golden/test_ui_smoke_golden.py`
|
||||
- assertions bijwerken naar dual-pane markup/ids
|
||||
|
||||
Niet gepland in deze stap:
|
||||
- backend python-bestanden
|
||||
- API schemas/routes/services
|
||||
|
||||
---
|
||||
|
||||
## 6) Regressierisico
|
||||
|
||||
## Hoog risico
|
||||
- Stateverwarring tussen links/rechts paneel (actie op verkeerde paneelcontext).
|
||||
- Onjuiste destination-opbouw bij copy/move (per ongeluk vanuit actief i.p.v. inactief pad).
|
||||
|
||||
## Middel risico
|
||||
- Disable/enable logica van knoppen niet synchroon met actieve paneelselectie.
|
||||
- Refresh-volgorde na acties waardoor UI tijdelijk stale data toont.
|
||||
- Bookmarks die per ongeluk in het verkeerde paneel openen als `activePane` niet consequent wordt gebruikt.
|
||||
|
||||
## Laag risico
|
||||
- CSS regressies in mobile layout.
|
||||
- Kleine tekst/label inconsistenties in error/statusweergave.
|
||||
|
||||
## Mitigatie
|
||||
- Duidelijke pane-identifiers in DOM en handlers (`left`/`right`).
|
||||
- Eén centrale helper voor `getActivePane()` en `getInactivePane()`.
|
||||
- Eén centrale helper voor copy/move destination-opbouw.
|
||||
- Smoke tests expliciet op dual-pane hoofdstructuur.
|
||||
|
||||
---
|
||||
|
||||
## 7) Aan te passen/toe te voegen UI smoke tests
|
||||
|
||||
## Aanpassen bestaande smoke test
|
||||
`test_ui_smoke_golden.py`:
|
||||
- huidige checks op enkel browsepaneel vervangen door dual-pane checks.
|
||||
|
||||
Nieuwe/gewijzigde assertions:
|
||||
- UI mount bestaat op `/ui`.
|
||||
- HTML bevat beide hoofdpanelen:
|
||||
- `id="left-pane"`
|
||||
- `id="right-pane"`
|
||||
- HTML bevat actieve-paneel-indicator/container (bijv. class of data-attribute).
|
||||
- Assets blijven gemapt:
|
||||
- `/ui/app.js`
|
||||
- `/ui/style.css`
|
||||
|
||||
## Kleine extra smoke checks (zonder backenduitbreiding)
|
||||
- basisactieknoppen aanwezig voor actieve-paneelacties.
|
||||
- tasks- en bookmarks-panelen blijven aanwezig in HTML.
|
||||
|
||||
Geen end-to-end browserautomatisering in deze stap.
|
||||
|
||||
---
|
||||
|
||||
## Niet in scope voor deze implementatieslice
|
||||
|
||||
- multi-select
|
||||
- viewer/editor
|
||||
- cancel/retry
|
||||
- history UI
|
||||
- nieuwe backend endpoints
|
||||
- keyboard mapping uitbreiding (eventueel later)
|
||||
@@ -0,0 +1,141 @@
|
||||
# UI_VISION_MC.md
|
||||
|
||||
## Doel
|
||||
|
||||
De webapp moet geleidelijk evolueren naar een web-based bestandsbeheerder
|
||||
met een workflow die geïnspireerd is op Midnight Commander.
|
||||
|
||||
Het doel is **niet** om Midnight Commander exact te klonen,
|
||||
maar om de belangrijkste werkprincipes over te nemen:
|
||||
|
||||
- twee panelen naast elkaar
|
||||
- snel wisselen tussen panelen
|
||||
- actief paneel en inactief paneel
|
||||
- copy/move van actief paneel naar ander paneel
|
||||
- keyboard-efficiënte bediening
|
||||
- focus op snelheid en weinig muisafhankelijkheid
|
||||
|
||||
---
|
||||
|
||||
## Ontwerpprincipes
|
||||
|
||||
- web-native uitvoering, geen letterlijke terminal-clone
|
||||
- behoud van bestaande backend API-contracten
|
||||
- incrementele UI-ontwikkeling in kleine stappen
|
||||
- veiligheid en voorspelbaarheid blijven leidend
|
||||
- bestaande werkende functionaliteit mag niet regressief kapot gaan
|
||||
|
||||
---
|
||||
|
||||
## Doelbeeld
|
||||
|
||||
De UI moet uiteindelijk bestaan uit:
|
||||
|
||||
### 1. Dubbel paneel
|
||||
- een linker paneel
|
||||
- een rechter paneel
|
||||
- elk paneel heeft een eigen current path
|
||||
- elk paneel toont directories en files
|
||||
- elk paneel heeft een eigen selectie
|
||||
|
||||
### 2. Actief paneel
|
||||
- één paneel is actief
|
||||
- acties worden gestart vanuit het actieve paneel
|
||||
- het andere paneel is standaard de doelcontext voor copy/move
|
||||
|
||||
### 3. Navigatie
|
||||
- pad boven elk paneel zichtbaar
|
||||
- directorynavigatie per paneel
|
||||
- later uitbreidbaar met keyboard shortcuts
|
||||
|
||||
### 4. Bestandsacties
|
||||
- rename
|
||||
- mkdir
|
||||
- delete
|
||||
- copy
|
||||
- move
|
||||
|
||||
### 5. Taken
|
||||
- tasklijst zichtbaar
|
||||
- status polling blijft mogelijk
|
||||
- copy/move blijven task-based
|
||||
|
||||
---
|
||||
|
||||
## Buiten scope voor de eerstvolgende UI-stap
|
||||
|
||||
Deze dingen worden nu nog niet gebouwd:
|
||||
|
||||
- multi-select
|
||||
- Insert-achtige selectie
|
||||
- viewer
|
||||
- editor
|
||||
- menubalk zoals F9
|
||||
- chmod/chown/symlink tools
|
||||
- zoeken
|
||||
- directory compare
|
||||
- volledige keyboard mapping
|
||||
|
||||
---
|
||||
|
||||
## Eerstvolgende UI-doel
|
||||
|
||||
De eerstvolgende UI-stap is:
|
||||
|
||||
### Dual-pane UI v2
|
||||
|
||||
Deze stap bevat alleen:
|
||||
|
||||
- links en rechts paneel
|
||||
- per paneel eigen current path
|
||||
- actief paneel
|
||||
- visuele actieve paneel-indicatie
|
||||
- copy/move gebruiken standaard het andere paneel als destination context
|
||||
- bestaande backend-endpoints blijven ongewijzigd
|
||||
|
||||
Nog niet in deze stap:
|
||||
|
||||
- multi-select
|
||||
- uitgebreide keyboard controls
|
||||
- viewer/editor
|
||||
- history UI
|
||||
- nieuwe backend features
|
||||
|
||||
---
|
||||
|
||||
## Verwachte impact
|
||||
|
||||
Waarschijnlijk vooral wijziging in:
|
||||
|
||||
- `webui/html/index.html`
|
||||
- `webui/html/app.js`
|
||||
- `webui/html/style.css`
|
||||
|
||||
Backend hergebruik:
|
||||
|
||||
- `GET /api/browse`
|
||||
- `POST /api/files/mkdir`
|
||||
- `POST /api/files/rename`
|
||||
- `POST /api/files/delete`
|
||||
- `POST /api/files/copy`
|
||||
- `POST /api/files/move`
|
||||
- `GET /api/tasks`
|
||||
- `GET /api/tasks/{task_id}`
|
||||
- `GET /api/bookmarks`
|
||||
- `POST /api/bookmarks`
|
||||
- `DELETE /api/bookmarks/{bookmark_id}`
|
||||
|
||||
---
|
||||
|
||||
## Acceptatiecriteria voor de volgende UI-stap
|
||||
|
||||
De dual-pane stap is pas klaar als:
|
||||
|
||||
- beide panelen onafhankelijk kunnen browsen
|
||||
- elk paneel een eigen path toont
|
||||
- één paneel actief is
|
||||
- actief paneel visueel herkenbaar is
|
||||
- copy/move logisch vanuit actief naar ander paneel werken
|
||||
- bestaande functionaliteit niet stuk gaat
|
||||
- bestaande tests blijven slagen
|
||||
- UI smoke tests waar nodig zijn bijgewerkt
|
||||
Reference in New Issue
Block a user