Docs: oppdater alle dokumenter til ny SpacetimeDB-cache-arkitektur
- synkronisering.md: ~5s → ~1s, trådbasert warmup med per-kanal config - adapter_moenster.md: omskrevet — dokumenterer nåværende arkitektur + historiske anti-patterns - chat.md: implementeringsstatus oppdatert til mars 2026 - spacetimedb_integrasjon.md: trådbasert warmup og admin-UI dokumentert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ed50d51a9
commit
af8f6f97c2
4 changed files with 74 additions and 58 deletions
|
|
@ -1,26 +1,27 @@
|
||||||
# Erfaring: Adapter-mønster for PG ↔ SpacetimeDB
|
# Erfaring: Adapter-mønster for chat (PG ↔ SpacetimeDB)
|
||||||
|
|
||||||
## 1. Mønsteret som fungerte
|
## 1. Mønsteret
|
||||||
|
|
||||||
Et felles interface (`ChatConnection`) med to implementasjoner:
|
Et felles interface (`ChatConnection`) med to implementasjoner:
|
||||||
- **PG-adapter** — polling hvert 3 sek, full-fetch, ingen ekstern avhengighet utover REST API
|
- **SpacetimeDB-adapter** (primær) — all data fra SpacetimeDB, worker håndterer warmup + sync
|
||||||
- **SpacetimeDB hybrid-adapter** — PG for historikk + SpacetimeDB WebSocket for sanntidspush
|
- **PG-adapter** (fallback) — polling hvert 3 sek, brukes kun når SpacetimeDB ikke er konfigurert
|
||||||
|
|
||||||
Factory-funksjon velger adapter basert på miljøvariabel (`VITE_SPACETIMEDB_URL`).
|
Factory-funksjon velger adapter basert på miljøvariabel (`VITE_SPACETIMEDB_URL`).
|
||||||
|
|
||||||
```
|
```
|
||||||
ChatBlock.svelte → createChat() → PG-adapter (fallback)
|
ChatBlock.svelte → createChat() → SpacetimeDB-adapter (primær)
|
||||||
→ SpacetimeDB hybrid (hvis URL er satt)
|
→ PG-adapter (fallback, readonly)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Fordeler:**
|
**Fordeler:**
|
||||||
- Kan teste PG-adapter isolert uten Docker/SpacetimeDB
|
- Kan teste PG-adapter isolert uten Docker/SpacetimeDB
|
||||||
- Fallback er trivielt — fjern env-variabelen
|
- Fallback er trivielt — fjern env-variabelen
|
||||||
- Komponenten vet ingenting om hvilken adapter som brukes
|
- Komponenten vet ingenting om hvilken adapter som brukes
|
||||||
|
- ChatConnection-interface har `edit()`, `delete()`, `react()` — ingen direkte PG API-kall fra komponenten
|
||||||
|
|
||||||
**Referanse:** `web/src/lib/chat/` — hele mappen er organisert etter dette mønsteret.
|
**Referanse:** `web/src/lib/chat/` — hele mappen er organisert etter dette mønsteret.
|
||||||
|
|
||||||
## 2. Anti-pattern: Lazy wrapper som bytter adapter
|
## 2. Historisk anti-pattern: Lazy wrapper som byttet adapter
|
||||||
|
|
||||||
Vi prøvde først en "lazy wrapper" som startet med PG-adapter og byttet til SpacetimeDB-adapter når tilkoblingen var klar. Problemet:
|
Vi prøvde først en "lazy wrapper" som startet med PG-adapter og byttet til SpacetimeDB-adapter når tilkoblingen var klar. Problemet:
|
||||||
|
|
||||||
|
|
@ -28,38 +29,38 @@ Vi prøvde først en "lazy wrapper" som startet med PG-adapter og byttet til Spa
|
||||||
- Når wrapperen byttet adapter, forsvant meldingene — ny adapter startet med tom liste
|
- Når wrapperen byttet adapter, forsvant meldingene — ny adapter startet med tom liste
|
||||||
- Svelte-komponentene så aldri byttet fordi proxy-referansen ikke oppdaterte seg
|
- Svelte-komponentene så aldri byttet fordi proxy-referansen ikke oppdaterte seg
|
||||||
|
|
||||||
**Lærdom:** Ikke bytt adapter runtime. Velg én ved oppstart. Hybrid-adapteren løser problemet bedre — den bruker begge kilder samtidig i stedet for å bytte mellom dem.
|
**Lærdom:** Ikke bytt adapter runtime. Velg én ved oppstart.
|
||||||
|
|
||||||
## 3. Hybrid fremfor ren SpacetimeDB
|
## 3. Historisk anti-pattern: Hybrid-adapter (PG + SpacetimeDB samtidig)
|
||||||
|
|
||||||
Ren SpacetimeDB-tilnærming krever "oppvarming" — lasting av historikk fra PG inn i SpacetimeDB ved oppstart. Dette skaper:
|
Den andre iterasjonen brukte en hybrid-adapter som hentet historikk fra PG via REST og lyttet på SpacetimeDB for nye meldinger. Dette skapte:
|
||||||
- Kompleksitet i Rust-modulen (load_messages-reducer)
|
|
||||||
- Konfliktrisiko under oppvarming (§8.1 i synkronisering.md)
|
|
||||||
- Tom chat inntil oppvarming er ferdig
|
|
||||||
|
|
||||||
**Hybrid-løsningen:** Frontend henter PG-historikk via REST (umiddelbart tilgjengelig) og lytter på SpacetimeDB kun for *nye* meldinger. Deduplisering skjer i klienten (sjekk mot eksisterende IDer).
|
- Kompleks dedup-logikk (`deletedIds` Set, merge av PG- og ST-meldinger)
|
||||||
|
- Race conditions mellom PG-polling og SpacetimeDB-callbacks
|
||||||
|
- BigInt-konverteringer og workarounds i frontend
|
||||||
|
|
||||||
**Fordeler:**
|
**Løsningen:** SpacetimeDB som cache foran PG. Worker gjør warmup (PG → ST) ved oppstart, frontend snakker kun med SpacetimeDB. Ingen merge-logikk nødvendig.
|
||||||
- Ingen oppvarming nødvendig
|
|
||||||
- Historikk er alltid tilgjengelig, selv om SpacetimeDB er nede
|
|
||||||
- SpacetimeDB-modulen trenger bare håndtere nye meldinger, ikke historikk
|
|
||||||
|
|
||||||
## 4. Graceful degradation — stille feilhåndtering
|
## 4. Nåværende arkitektur
|
||||||
|
|
||||||
SpacetimeDB-adaptere bør **aldri** vise feilmelding til brukeren ved tilkoblingsproblemer. PG-data er allerede lastet — brukeren har en fungerende chat.
|
SpacetimeDB er en varm cache foran PostgreSQL:
|
||||||
|
- **Worker warmup:** Ved oppstart lastes meldinger + reaksjoner fra PG → SpacetimeDB per kanal
|
||||||
|
- **Frontend → SpacetimeDB:** Subscription gir alle meldinger (historikk fra warmup + nye)
|
||||||
|
- **SpacetimeDB → PG:** Sync-worker poller `sync_outbox` hvert sekund
|
||||||
|
- **PG er autoritativ** — ved SpacetimeDB-restart oppvarmes fra PG
|
||||||
|
|
||||||
```typescript
|
**Fordeler over hybrid:**
|
||||||
.onConnectError((_ctx, err) => {
|
- Ingen dedup, merge eller deletedIds
|
||||||
console.warn('[spacetime] connection error, PG-data beholdes:', err);
|
- Frontend-koden er dramatisk enklere
|
||||||
// Ingen error-state til UI — PG-data er intakt
|
- Konsistent datamodell — alt kommer fra én kilde
|
||||||
})
|
- Reaksjoner håndteres via SpacetimeDB-tabeller, ikke PG API
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Anbefaling for neste komponent
|
## 5. Anbefaling for neste komponent
|
||||||
|
|
||||||
Når Kanban eller Whiteboard skal bygges med SpacetimeDB:
|
Når Kanban eller Whiteboard skal bygges med SpacetimeDB:
|
||||||
|
|
||||||
1. **Start med PG-adapter.** Få hele flyten til å fungere med REST/polling først.
|
1. **Start med PG-adapter.** Få hele flyten til å fungere med REST/polling først.
|
||||||
2. **Lag SpacetimeDB hybrid-adapter.** PG for historikk, SpacetimeDB for sanntid.
|
2. **Lag SpacetimeDB-adapter med warmup.** Worker laster data fra PG ved oppstart.
|
||||||
3. **Bruk samme factory-mønster.** Felles interface, env-variabel for valg.
|
3. **Bruk samme factory-mønster.** Felles interface, env-variabel for valg.
|
||||||
4. **Test begge adaptere uavhengig** før du integrerer i UI-komponenten.
|
4. **Legg til warmup-config** i `channels.config` (eller tilsvarende config-felt).
|
||||||
|
5. **Test begge adaptere uavhengig** før du integrerer i UI-komponenten.
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,20 @@ Ny modell:
|
||||||
- **Worker håndterer toveissynk** — ST → PG for nye/redigerte/slettede meldinger og reaksjoner
|
- **Worker håndterer toveissynk** — ST → PG for nye/redigerte/slettede meldinger og reaksjoner
|
||||||
|
|
||||||
### Warmup-flyt
|
### Warmup-flyt
|
||||||
1. Worker starter → `warmup::run()` leser siste N meldinger per kanal fra PG
|
1. Worker starter → `warmup::run()` leser kanaler med config fra PG
|
||||||
2. Kaller `clear_channel` reducer per kanal (unngår duplikater ved restart)
|
2. Per kanal: sjekker `channels.config.warmup_mode` (all/messages/days/none)
|
||||||
3. Kaller `load_messages` reducer med JSON-array (ikke pipe-separert format)
|
3. Kaller `clear_channel` reducer (unngår duplikater ved restart)
|
||||||
4. Laster også reaksjoner via `load_reactions` reducer
|
4. **Trådbasert henting:** Finner kvalifiserende tråder, henter alle meldinger i disse (komplett med svar)
|
||||||
|
- `messages`-modus: de N nyeste trådene (sortert etter siste aktivitet)
|
||||||
|
- `days`-modus: alle tråder med minst én melding i tidsvinduet
|
||||||
|
- Et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter)
|
||||||
|
5. Kaller `load_messages` reducer med JSON-array
|
||||||
|
6. Laster også reaksjoner via `load_reactions` reducer
|
||||||
|
|
||||||
|
### Per-kanal konfigurasjon
|
||||||
|
- Lagres i `channels.config` JSONB: `warmup_mode` + `warmup_value`
|
||||||
|
- Admin-UI: `/admin/channels` — tabell med inline-redigering
|
||||||
|
- Default: `"all"` (last alt). Andre: `"messages"` (siste N tråder), `"days"` (siste N dager), `"none"` (inaktiv)
|
||||||
|
|
||||||
### Sync-flyt (ST → PG)
|
### Sync-flyt (ST → PG)
|
||||||
- SyncOutbox-events prosesseres hver 1. sekund
|
- SyncOutbox-events prosesseres hver 1. sekund
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ messages (
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 SpacetimeDB (sanntid)
|
### 3.2 SpacetimeDB (sanntid)
|
||||||
SpacetimeDB holder aktive channels' meldinger i minnet. Nye meldinger sendes via SpacetimeDB og kringkastes til alle tilkoblede klienter i samme workspace og channel. Synkes til PostgreSQL med ~5 sek forsinkelse (se `docs/infra/synkronisering.md`).
|
SpacetimeDB holder aktive channels' meldinger i minnet som varm cache foran PG. Ved oppstart gjør worker warmup fra PG → SpacetimeDB (per-kanal konfigurasjon). Nye meldinger sendes via SpacetimeDB-reducers og kringkastes til alle tilkoblede klienter. Synkes til PostgreSQL med ~1 sek forsinkelse (se `docs/infra/synkronisering.md`).
|
||||||
|
|
||||||
## 4. Mentions & Autocomplete
|
## 4. Mentions & Autocomplete
|
||||||
Kun aktive når `config.mentions = true`.
|
Kun aktive når `config.mentions = true`.
|
||||||
|
|
@ -114,26 +114,28 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s
|
||||||
|
|
||||||
## 10. Implementeringsstatus
|
## 10. Implementeringsstatus
|
||||||
|
|
||||||
### Ferdig (mars 2025)
|
### Ferdig (mars 2026)
|
||||||
- **ChatBlock.svelte:** Refaktorert til adapter-mønster via `createChat()` factory
|
- **ChatBlock.svelte:** Adapter-mønster via `createChat()` factory. Bruker `chat.edit()`, `chat.delete()`, `chat.react()` — ingen direkte PG API-kall.
|
||||||
- **PG-adapter (`pg.svelte.ts`):** Polling hvert 3. sek, full-fetch (ingen akkumulering). Fungerer som selvstendig fallback.
|
- **SpacetimeDB-adapter (`spacetime.svelte.ts`):** Ren SpacetimeDB-adapter. All data fra SpacetimeDB (historikk via warmup + sanntid). Reaksjoner fra `message_reaction`-tabellen.
|
||||||
- **SpacetimeDB hybrid-adapter (`spacetime.svelte.ts`):** Henter historikk fra PG via REST + lytter på SpacetimeDB WebSocket for sanntidspush. Dedupliserer mot PG-data. Faller tilbake til PG REST ved sending hvis SpacetimeDB er nede.
|
- **PG-adapter (`pg.svelte.ts`):** Polling hvert 3. sek. Readonly fallback når SpacetimeDB ikke er konfigurert.
|
||||||
- **Factory (`create.svelte.ts`):** Velger adapter basert på `VITE_SPACETIMEDB_URL`. SSR-safe med `browser`-guard.
|
- **Factory (`create.svelte.ts`):** Velger adapter basert på `VITE_SPACETIMEDB_URL`. SSR-safe med `browser`-guard.
|
||||||
- **Shared types (`types.ts`):** `Message` og `ChatConnection` interface brukt av begge adaptere.
|
- **Shared types (`types.ts`):** `ChatConnection` interface med `send`, `edit`, `delete`, `react`, `readonly`.
|
||||||
- **SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`):** `ChatMessage`-tabell, `SyncOutbox`, `send_message`-reducer. Publisert som `sidelinja-realtime`.
|
- **SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`):** `ChatMessage`, `MessageReaction`, `SyncOutbox`-tabeller. Reducers: `send_message`, `delete_message`, `edit_message`, `add_reaction`, `remove_reaction`, `load_messages`, `load_reactions`, `clear_channel`, `mark_synced`.
|
||||||
- **TypeScript-bindings:** Generert fra SpacetimeDB-modulen, camelCase-properties.
|
- **Worker warmup (`worker/src/warmup.rs`):** PG → SpacetimeDB ved oppstart. Per-kanal konfig (all/messages/days/none). Trådbasert henting.
|
||||||
- **Autoscroll:** `$effect` som scroller ned ved nye meldinger.
|
- **Worker sync (`worker/src/sync.rs`):** SpacetimeDB → PG hvert sekund. Insert/delete/update meldinger + reaksjoner.
|
||||||
- **Datogruppering:** Visuell datoseparator ("I dag", "I går", dato).
|
- **Admin-side (`/admin/channels`):** Per-kanal warmup-konfigurasjon.
|
||||||
|
- **Tråder:** Komplett trådvisning med datogruppering og autoscroll.
|
||||||
|
- **Reaksjoner:** Via SpacetimeDB-reducers, synket til PG.
|
||||||
|
|
||||||
### Gjenstår
|
### Gjenstår
|
||||||
- **Sync-worker (SpacetimeDB → PG):** Rust-worker som poller `sync_outbox` og persisterer til PostgreSQL. Se `docs/infra/synkronisering.md` §5.1.
|
- **Vedlegg, TTL** — avventer implementering.
|
||||||
- **Tråder, mentions, vedlegg, TTL** — avventer Kunnskapsgraf CRUD (Lag 2).
|
- **Workspace-partisjonering:** SpacetimeDB har `workspace_id` men bruker ikke token ennå.
|
||||||
- **Autentisering:** `author_name`/`author_id` settes ikke automatisk ennå (avventer Authentik-integrasjon).
|
- **Pin/konvertering:** Går fortsatt direkte til PG API (ikke via SpacetimeDB).
|
||||||
|
|
||||||
## 11. Instruks for Claude Code
|
## 11. Instruks for Claude Code
|
||||||
* **Opprettelsesrekkefølge:** Opprett `nodes`-rad → `channels`-rad → (for meldinger) `nodes`-rad → `messages`-rad. Alt i én transaksjon med riktig `workspace_id`.
|
* **Opprettelsesrekkefølge:** Opprett `nodes`-rad → `channels`-rad → (for meldinger) `nodes`-rad → `messages`-rad. Alt i én transaksjon med riktig `workspace_id`.
|
||||||
* **Channel-opprettelse:** Når en Tema, Episode eller Møte opprettes, opprett alltid en default-channel i samme transaksjon.
|
* **Channel-opprettelse:** Når en Tema, Episode eller Møte opprettes, opprett alltid en default-channel i samme transaksjon.
|
||||||
* **Mentions-parsing:** Skjer i SvelteKit server-side ved mottak av meldingen. Parse `#`- og `@`-tags, valider mot noder i workspace-et, og opprett `graph_edges`.
|
* **Mentions-parsing:** Skjer i sync-workeren ved persistering til PG. Parser mention-UUIDs fra HTML body og oppretter `graph_edges`.
|
||||||
* **Config-respekt:** Frontend-komponenten må lese `channel.config` og slå av/på UI-elementer (tråd-knapp, vedlegg-knapp, mention-autocomplete) basert på config.
|
* **Config-respekt:** Frontend-komponenten må lese `channel.config` og slå av/på UI-elementer. `channels.config` inneholder også `warmup_mode`/`warmup_value` for SpacetimeDB-oppvarming.
|
||||||
* **SpacetimeDB er autoritativ** for aktive meldinger, PG for historikk.
|
* **PG er autoritativ** — SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB.
|
||||||
* **Alt er workspace-scopet.** Channels arver workspace via `nodes.workspace_id`.
|
* **Alt er workspace-scopet.** Channels arver workspace via `nodes.workspace_id`.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ SpacetimeDB gir sanntidsopplevelsen, PostgreSQL er langtidsminnet. Denne spec-en
|
||||||
### 1.1 Grunnregel: SpacetimeDB er en ren sanntidsbuffer
|
### 1.1 Grunnregel: SpacetimeDB er en ren sanntidsbuffer
|
||||||
SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten:
|
SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten:
|
||||||
1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller
|
1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller
|
||||||
2. Synkes til PostgreSQL innen ~5 sekunder (chat, kanban, markører).
|
2. Synkes til PostgreSQL innen ~1 sekund (chat, kanban, markører).
|
||||||
|
|
||||||
**Konsekvens for design:** Ethvert SpacetimeDB-modul-skjema skal ha en tilsvarende PostgreSQL-tabell som kan fungere som drop-in erstatning dersom SpacetimeDB fjernes. Frontend-kode som leser fra SpacetimeDB skal kunne peke om til et SvelteKit SSE-endepunkt + REST uten arkitekturendring.
|
**Konsekvens for design:** Ethvert SpacetimeDB-modul-skjema skal ha en tilsvarende PostgreSQL-tabell som kan fungere som drop-in erstatning dersom SpacetimeDB fjernes. Frontend-kode som leser fra SpacetimeDB skal kunne peke om til et SvelteKit SSE-endepunkt + REST uten arkitekturendring.
|
||||||
|
|
||||||
|
|
@ -56,20 +56,23 @@ SpacetimeDB-modulene (Rust) produserer persisterings-events ved dataendringer. E
|
||||||
## 5. Mekanisme
|
## 5. Mekanisme
|
||||||
|
|
||||||
### 5.1 SpacetimeDB → PostgreSQL (persistering)
|
### 5.1 SpacetimeDB → PostgreSQL (persistering)
|
||||||
- SpacetimeDB-modulene kaller en intern `emit_sync_event()`-funksjon ved relevante dataendringer
|
- SpacetimeDB-reducerne legger sync-events i `sync_outbox`-tabellen med tidsstempel og payload
|
||||||
- Events bufres i en SpacetimeDB-tabell (`sync_outbox`) med tidsstempel og payload
|
- Rust-workeren poller `sync_outbox` hvert sekund, leser alle usynkede events, skriver til PostgreSQL, og markerer dem som synket via `mark_synced`-reducer
|
||||||
- Rust-workeren poller `sync_outbox` hvert ~5 sekund, leser alle usynkede events, skriver til PostgreSQL i én transaksjon, og markerer dem som synket
|
|
||||||
- Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører
|
- Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører
|
||||||
|
|
||||||
### 5.2 PostgreSQL → SpacetimeDB (oppvarming)
|
### 5.2 PostgreSQL → SpacetimeDB (oppvarming)
|
||||||
- Ved oppstart (eller reconnect) av SpacetimeDB laster Rust-workeren aktive data fra PG:
|
- Ved oppstart laster Rust-workeren data fra PG per kanal, basert på `channels.config`:
|
||||||
- Aktive temaer med siste N chatmeldinger
|
- `warmup_mode: "all"` — alle meldinger i kanalen
|
||||||
- Kanban-state for pågående episoder
|
- `warmup_mode: "messages"` — de N nyeste *trådene* (komplett med svar)
|
||||||
- Aktør/Tema-navn for autocomplete (read-only cache)
|
- `warmup_mode: "days"` — alle tråder med aktivitet siste N dager (komplett)
|
||||||
- Dette er en enveis-last, ikke kontinuerlig synk. Kunnskapsgrafen oppdateres i SpacetimeDB kun ved oppstart eller eksplisitt refresh
|
- `warmup_mode: "none"` — kanalen hoppes over (arkivert/inaktiv)
|
||||||
|
- Trådbasert henting: et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter)
|
||||||
|
- Konfigureres per kanal via admin-UI (`/admin/channels`) eller `channels.config` JSONB
|
||||||
|
- Kanalen ryddes (`clear_channel`) før lasting for å unngå duplikater ved restart
|
||||||
|
- Reaksjoner lastes også inn via `load_reactions`-reducer
|
||||||
|
|
||||||
## 6. Feilhåndtering
|
## 6. Feilhåndtering
|
||||||
- **SpacetimeDB krasjer:** Data siden siste synk (~5 sek) tapes. Ved restart oppvarmes fra PG
|
- **SpacetimeDB krasjer:** Data siden siste synk (~1 sek) tapes. Ved restart oppvarmes fra PG
|
||||||
- **PostgreSQL nede:** Sanntidsfunksjoner fortsetter å fungere. `sync_outbox` vokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk
|
- **PostgreSQL nede:** Sanntidsfunksjoner fortsetter å fungere. `sync_outbox` vokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk
|
||||||
- **Rust-worker krasjer:** `sync_outbox` akkumuleres. Ved restart plukker workeren opp der den slapp (usynkede events har ingen markering)
|
- **Rust-worker krasjer:** `sync_outbox` akkumuleres. Ved restart plukker workeren opp der den slapp (usynkede events har ingen markering)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue