v0.8.70-beta - De store blijft soepel, en 28 apps erbij
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 17m15s

- filteredStore() werd per repo drie keer per reactieve tick aangeroepen (in de
  x-show, de x-for en de teller in het filtermenu) en liep elke keer alle apps
  langs met een toLowerCase() per veld. Bij elke toetsaanslag. De catalogus ging
  deze reeks van 142 naar 286 apps en dat begon te merken; het filteren gebeurt
  nu een keer per filterstand
- Nog maar zestig kaarten tegelijk in de DOM, met een knop Nog N tonen eronder.
  De limiet valt terug zodra je het filter of de zoekterm wijzigt
- Het zoekveld wacht 200 ms voordat het filtert
- 28 apps in de dunste hoeken: 2FAuth, Passbolt, OpenBao, Trivy, Verdaccio,
  Docker Registry, MkDocs Material, LiteLLM, Flowise, Speaches, Pelican Panel,
  Gameyfin, EmulatorJS, Mainsail, Fluidd, Kanboard, Wekan, Filestash,
  Linkwarden, Slash, RSSHub, Apprise, Mailpit, Dashy, Plausible, NetBird,
  LibreSpeed en PinePods
- Alle 318 images opnieuw tegen hun registry gecontroleerd

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
Ramon 2026-08-01 23:56:11 +02:00
parent 0500e8945b
commit 0c025d657d
63 changed files with 2621 additions and 10 deletions

View file

@ -1,3 +1,36 @@
# v0.8.70-beta — De store blijft soepel, en achtentwintig apps erbij
**Typen in het zoekveld werd traag.** `filteredStore()` is een methode, geen
gecachte getter, en werd per repo drie keer per reactieve tick aangeroepen: in de
`x-show`, in de `x-for` en via de teller in het filtermenu. Elke aanroep liep alle
apps langs met een `toLowerCase()` op naam, map, omschrijving en elke tag — bij
iedere toetsaanslag opnieuw. De catalogus ging deze reeks van 142 naar 286 apps,
en dat begon te merken.
Het filteren gebeurt nu één keer per filterstand en wordt vastgehouden tot je iets
verandert. Daarnaast staan er nog maar zestig kaarten tegelijk in beeld, met een
knop **Nog N tonen** eronder; die limiet valt terug zodra je het filter of de
zoekterm wijzigt, anders sta je na één zoekactie voor de rest van de sessie naar
alles te kijken. En het zoekveld wacht 200 ms voordat het filtert.
**Achtentwintig apps** in de hoeken die het dunst waren:
- **Beveiliging:** 2FAuth (tweestapscodes naast je kluis, niet erin), Passbolt
(wachtwoorden delen in een team), OpenBao, Trivy
- **Ontwikkeling:** Verdaccio, Docker Registry, MkDocs Material
- **AI:** LiteLLM, Flowise, Speaches
- **Spellen:** Pelican Panel, Gameyfin, EmulatorJS
- **3D-printen:** Mainsail en Fluidd — de twee interfaces voor Klipper, waar
OctoPrint niets mee doet
- **Productiviteit:** Kanboard, Wekan, Filestash
- **Documenten:** Linkwarden (bladwijzers mét een gearchiveerde kopie), Slash,
RSSHub
- **Communicatie:** Apprise (één API naar honderd meldingsdiensten), Mailpit
(vangt de mail van je eigen apps op)
- **Verder:** Dashy, Plausible, NetBird, LibreSpeed, PinePods
Alle 318 images in de catalogus zijn opnieuw tegen hun registry gecontroleerd.
# v0.8.60-beta — Backups bevatten nu je gegevens, en Pangolin hoort erbij # v0.8.60-beta — Backups bevatten nu je gegevens, en Pangolin hoort erbij
**Een backup gaf je niets terug.** `backups.create()` maakte een tar van de **Een backup gaf je niets terug.** `backups.create()` maakte een tar van de

View file

@ -16,7 +16,7 @@ IP-adres in je netwerk geven.
| | | | | |
|---|---| |---|---|
| **App-catalogus** | 258 kant-en-klare apps, ingedeeld in categorieën met kleur. Zoeken, filteren en installeren met een formulier in plaats van YAML. | | **App-catalogus** | 286 kant-en-klare apps, ingedeeld in categorieën met kleur. Zoeken, filteren en installeren met een formulier in plaats van YAML. |
| **Stacks beheren** | Starten, stoppen, bijwerken en herstarten — per stack of per losse container, met logs en live CPU-/geheugengebruik. | | **Stacks beheren** | Starten, stoppen, bijwerken en herstarten — per stack of per losse container, met logs en live CPU-/geheugengebruik. |
| **Apps koppelen** | Stacks komen op een gedeeld netwerk, zodat ze elkaar op naam bereiken. Een veld dat naar een andere app verwijst toont een keuzelijst met wat je al draait. | | **Apps koppelen** | Stacks komen op een gedeeld netwerk, zodat ze elkaar op naam bereiken. Een veld dat naar een andere app verwijst toont een keuzelijst met wat je al draait. |
| **Eigen IP per stack** | Met macvlan of ipvlan krijgt een app een eigen adres in je LAN in plaats van poorten op de host. | | **Eigen IP per stack** | Met macvlan of ipvlan krijgt een app een eigen adres in je LAN in plaats van poorten op de host. |

View file

@ -1 +1 @@
0.8.60-beta 0.8.70-beta

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: 2fauth/2fauth:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- APP_KEY=<< app_key >>
- APP_URL=<< app_url >>
- IS_DEMO_APP=false
- TRUSTED_PROXIES=*
volumes:
- << appdata_dir >>/<< service_name >>:/2fauth
ports:
- "<< port >>:8000"
restart: unless-stopped

88
apps/2fauth/template.json Normal file
View file

@ -0,0 +1,88 @@
{
"kind": "compose",
"metadata": {
"name": "2FAuth",
"description": "Bewaart je tweestapscodes op je eigen server, zodat je ze niet kwijt bent met je telefoon. Naast Vaultwarden, niet erin — dan liggen wachtwoord en code niet op dezelfde plek.",
"tags": [
"2fa",
"totp",
"beveiliging"
],
"icon": {
"provider": "selfhst",
"id": "2fauth"
},
"version": {
"name": "latest"
},
"categories": [
"beveiliging"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "2fauth",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8230,
"required": true,
"description": "Standaard 8000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8230",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "app_key",
"type": "str",
"title": "Applicatiesleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "laravel",
"secret_bytes": 32,
"description": "Laravel-formaat; wordt automatisch in de juiste vorm gemaakt"
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: caronc/apprise:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- APPRISE_STATEFUL_MODE=simple
- APPRISE_WORKER_COUNT=1
volumes:
- << appdata_dir >>/<< service_name >>:/config
ports:
- "<< port >>:8000"
restart: unless-stopped

View file

@ -0,0 +1,59 @@
{
"kind": "compose",
"metadata": {
"name": "Apprise",
"description": "Eén API die je bericht doorstuurt naar honderd diensten: Discord, Telegram, ntfy, e-mail, Signal. Je apps hoeven maar één adres te kennen.",
"tags": [
"meldingen",
"api",
"notificaties"
],
"icon": {
"provider": "selfhst",
"id": "apprise"
},
"version": {
"name": "latest"
},
"categories": [
"communicatie"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "apprise",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8251,
"required": true,
"description": "Standaard 8000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: lissy93/dashy:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/app/user-data
ports:
- "<< port >>:8080"
restart: unless-stopped

59
apps/dashy/template.json Normal file
View file

@ -0,0 +1,59 @@
{
"kind": "compose",
"metadata": {
"name": "Dashy",
"description": "Startpagina met statuscontrole per dienst, thema's en widgets. Alles staat in één configuratiebestand dat je ook met de hand kunt bijwerken.",
"tags": [
"dashboard",
"startpagina",
"overzicht"
],
"icon": {
"provider": "selfhst",
"id": "dashy"
},
"version": {
"name": "latest"
},
"categories": [
"beheer"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "dashy",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8252,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/emulatorjs:latest
container_name: << service_name >>
environment:
- PUID=<< puid >>
- PGID=<< pgid >>
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/config
- << data_root >>:/data
ports:
- "<< port >>:80"
- "<< port_admin >>:3000"
restart: unless-stopped

View file

@ -0,0 +1,97 @@
{
"kind": "compose",
"metadata": {
"name": "EmulatorJS",
"description": "Speel je retro-spellen in de browser, zonder installatie. Draait alles van NES tot PlayStation. Let op: Zet je roms onder /data/roms met een submap per systeem.",
"tags": [
"spellen",
"retro",
"emulator"
],
"icon": {
"provider": "selfhst",
"id": "emulatorjs"
},
"version": {
"name": "latest"
},
"categories": [
"spellen",
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "emulatorjs",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8241,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_admin",
"type": "int",
"title": "Beheerinterface",
"default": 8242,
"required": true,
"description": "Hier voeg je spellen toe"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "data_root",
"type": "str",
"title": "Gedeelde datamap",
"default": "/mnt/data",
"required": true,
"description": "Je bibliotheek, gekoppeld als /data",
"help": "Media en downloads horen onder één map te staan, die als /data in elke container komt. Eronder: media/films, media/series, torrents/ en usenet/."
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Gebruiker",
"items": [
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "`id -u` op je server"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: machines/filestash:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- APPLICATION_URL=<< app_url >>
volumes:
- << appdata_dir >>/<< service_name >>:/app/data/state
ports:
- "<< port >>:8334"
restart: unless-stopped

View file

@ -0,0 +1,73 @@
{
"kind": "compose",
"metadata": {
"name": "Filestash",
"description": "Webbestandsbeheer over S3, SFTP, FTP, WebDAV en meer, met een voorvertoning per bestandssoort. Eén venster voor opslag die overal staat.",
"tags": [
"bestanden",
"beheer",
"s3",
"sftp"
],
"icon": {
"provider": "selfhst",
"id": "filestash"
},
"version": {
"name": "latest"
},
"categories": [
"opslag",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "filestash",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8247,
"required": true,
"description": "Standaard 8334; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8247",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: flowiseai/flowise:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- FLOWISE_USERNAME=<< admin_user >>
- FLOWISE_PASSWORD=<< admin_password >>
- DATABASE_PATH=/root/.flowise
volumes:
- << appdata_dir >>/<< service_name >>:/root/.flowise
ports:
- "<< port >>:3000"
restart: unless-stopped

View file

@ -0,0 +1,86 @@
{
"kind": "compose",
"metadata": {
"name": "Flowise",
"description": "Bouw workflows met taalmodellen door blokken aan elkaar te klikken, en zet ze weg als API.",
"tags": [
"ai",
"llm",
"workflows"
],
"icon": {
"provider": "selfhst",
"id": "flowise"
},
"version": {
"name": "latest"
},
"categories": [
"ai",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "flowise",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8237,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "admin_user",
"type": "str",
"title": "Gebruikersnaam",
"default": "admin",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "admin_password",
"type": "str",
"title": "Wachtwoord",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: ghcr.io/fluidd-core/fluidd:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/etc/nginx/conf.d
ports:
- "<< port >>:80"
restart: unless-stopped

59
apps/fluidd/template.json Normal file
View file

@ -0,0 +1,59 @@
{
"kind": "compose",
"metadata": {
"name": "Fluidd",
"description": "Alternatieve webinterface voor Klipper, met een rustiger scherm dan Mainsail. Welke je prettiger vindt is smaak. Let op: Net als Mainsail praat dit met een Moonraker die op de printer draait; die vul je in de interface in.",
"tags": [
"3d-printen",
"klipper",
"printerbeheer"
],
"icon": {
"provider": "selfhst",
"id": "fluidd"
},
"version": {
"name": "latest"
},
"categories": [
"3d-printen"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "fluidd",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8244,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: ghcr.io/gameyfin/gameyfin:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/app/config
- << data_root >>/media:/data/media:ro
ports:
- "<< port >>:8080"
restart: unless-stopped

View file

@ -0,0 +1,68 @@
{
"kind": "compose",
"metadata": {
"name": "Gameyfin",
"description": "Bibliotheek voor je spellencollectie: scant je mappen, haalt omslagen en beschrijvingen op, en laat huisgenoten downloaden.",
"tags": [
"spellen",
"bibliotheek"
],
"icon": {
"provider": "selfhst",
"id": "gameyfin"
},
"version": {
"name": "latest"
},
"categories": [
"spellen",
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "gameyfin",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8240,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "data_root",
"type": "str",
"title": "Gedeelde datamap",
"default": "/mnt/data",
"required": true,
"description": "Je bibliotheek, gekoppeld als /data",
"help": "Media en downloads horen onder één map te staan, die als /data in elke container komt. Eronder: media/films, media/series, torrents/ en usenet/."
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: kanboard/kanboard:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/www/app/data
- << appdata_dir >>/<< service_name >>-plugins:/var/www/app/plugins
ports:
- "<< port >>:80"
restart: unless-stopped

View file

@ -0,0 +1,59 @@
{
"kind": "compose",
"metadata": {
"name": "Kanboard",
"description": "Kanbanbord dat doet wat het zegt en verder niets. Draait op een handvol megabytes en heeft geen database nodig.",
"tags": [
"kanban",
"taken",
"projecten"
],
"icon": {
"provider": "selfhst",
"id": "kanboard"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "kanboard",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8245,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/librespeed:latest
container_name: << service_name >>
environment:
- PUID=<< puid >>
- PGID=<< pgid >>
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/config
ports:
- "<< port >>:80"
restart: unless-stopped

View file

@ -0,0 +1,80 @@
{
"kind": "compose",
"metadata": {
"name": "LibreSpeed",
"description": "Snelheidstest op je eigen server, zodat je meet hoe snel je netwerk is in plaats van hoe snel de dichtstbijzijnde meetserver is.",
"tags": [
"snelheid",
"netwerk",
"meten"
],
"icon": {
"provider": "selfhst",
"id": "librespeed"
},
"version": {
"name": "latest"
},
"categories": [
"netwerk",
"monitoring"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "librespeed",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8255,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Gebruiker",
"items": [
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "`id -u` op je server"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
}
]
}
]
}

View file

@ -0,0 +1,26 @@
services:
<< service_name >>:
image: ghcr.io/linkwarden/linkwarden:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- NEXTAUTH_URL=<< app_url >>/api/v1/auth
- NEXTAUTH_SECRET=<< nextauth_secret >>
- DATABASE_URL=postgresql://linkwarden:<< db_password >>@<< service_name >>-db:5432/linkwarden
volumes:
- << appdata_dir >>/<< service_name >>:/data/data
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=linkwarden
- POSTGRES_USER=linkwarden
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,95 @@
{
"kind": "compose",
"metadata": {
"name": "Linkwarden",
"description": "Bladwijzers mét een gearchiveerde kopie van de pagina, als schermafdruk en als pdf. Wat je bewaart blijft leesbaar, ook als de site verdwijnt.",
"tags": [
"bladwijzers",
"archief",
"lezen"
],
"icon": {
"provider": "selfhst",
"id": "linkwarden"
},
"version": {
"name": "latest"
},
"categories": [
"documenten"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "linkwarden",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8248,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8248",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "nextauth_secret",
"type": "str",
"title": "Sleutel voor de inlogsessies",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,23 @@
services:
<< service_name >>:
image: ghcr.io/berriai/litellm:main-latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- LITELLM_MASTER_KEY=<< master_key >>
- DATABASE_URL=postgresql://litellm:<< db_password >>@<< service_name >>-db:5432/litellm
ports:
- "<< port >>:4000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=litellm
- POSTGRES_USER=litellm
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,85 @@
{
"kind": "compose",
"metadata": {
"name": "LiteLLM",
"description": "Eén API voor OpenAI, Anthropic, Gemini en je eigen Ollama, met een sleutel en een verbruikslimiet per gebruiker.",
"tags": [
"ai",
"llm",
"api",
"proxy"
],
"icon": {
"provider": "selfhst",
"id": "litellm"
},
"version": {
"name": "latest"
},
"categories": [
"ai"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "litellm",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8236,
"required": true,
"description": "Standaard 4000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "master_key",
"type": "str",
"title": "Hoofdsleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Waarmee je de andere sleutels beheert"
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: axllent/mailpit:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- MP_MAX_MESSAGES=5000
- MP_DATABASE=/data/mailpit.db
volumes:
- << appdata_dir >>/<< service_name >>:/data
ports:
- "<< port >>:8025"
- "<< port_smtp >>:1025"
restart: unless-stopped

View file

@ -0,0 +1,67 @@
{
"kind": "compose",
"metadata": {
"name": "Mailpit",
"description": "Vangt de e-mail van je eigen applicaties op zodat je hem kunt bekijken zonder dat er iets de deur uit gaat. Onmisbaar bij het testen van een app die mail stuurt.",
"tags": [
"e-mail",
"testen",
"ontwikkeling"
],
"icon": {
"provider": "selfhst",
"id": "mailpit"
},
"version": {
"name": "latest"
},
"categories": [
"communicatie",
"ontwikkeling"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "mailpit",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8025,
"required": true
},
{
"name": "port_smtp",
"type": "int",
"title": "SMTP-poort",
"default": 1025,
"required": true,
"description": "Hier laat je je applicatie naartoe sturen"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: ghcr.io/mainsail-crew/mainsail:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/usr/share/nginx/html/config
ports:
- "<< port >>:80"
restart: unless-stopped

View file

@ -0,0 +1,59 @@
{
"kind": "compose",
"metadata": {
"name": "Mainsail",
"description": "Webinterface voor Klipper: bedienen, sliceren en de temperatuurgrafieken volgen. Waar OctoPrint stopt bij Marlin, begint dit bij Klipper. Let op: Klipper en Moonraker draaien op de printer zelf, niet in deze container. Vul in de interface het adres van je Moonraker in.",
"tags": [
"3d-printen",
"klipper",
"printerbeheer"
],
"icon": {
"provider": "selfhst",
"id": "mainsail"
},
"version": {
"name": "latest"
},
"categories": [
"3d-printen"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "mainsail",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8243,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: squidfunk/mkdocs-material:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/docs
command: serve --dev-addr=0.0.0.0:8000
ports:
- "<< port >>:8000"
restart: unless-stopped

View file

@ -0,0 +1,60 @@
{
"kind": "compose",
"metadata": {
"name": "MkDocs Material",
"description": "Zet een map met markdown om in een doorzoekbare documentatiesite, en ververst live terwijl je schrijft. Let op: Zet je markdown en je mkdocs.yml in de appdata-map; die wordt als /docs gekoppeld.",
"tags": [
"documentatie",
"markdown",
"ontwikkeling"
],
"icon": {
"provider": "selfhst",
"id": "mkdocs"
},
"version": {
"name": "latest"
},
"categories": [
"ontwikkeling",
"documenten"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "mkdocsmaterial",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8235,
"required": true,
"description": "Standaard 8000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: netbirdio/dashboard:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- NETBIRD_MGMT_API_ENDPOINT=<< mgmt_url >>
- AUTH_AUTHORITY=<< auth_authority >>
- AUTH_CLIENT_ID=<< auth_client_id >>
- AUTH_SUPPORTED_SCOPES=openid profile email
- AUTH_AUDIENCE=<< auth_client_id >>
ports:
- "<< port >>:80"
restart: unless-stopped

View file

@ -0,0 +1,85 @@
{
"kind": "compose",
"metadata": {
"name": "NetBird",
"description": "Beheerpaneel voor een mesh-VPN op WireGuard: je apparaten praten rechtstreeks met elkaar, waar ze ook staan. Let op: Dit is alleen het dashboard. De management-server, signal en een TURN-server draai je ernaast; zie de documentatie van NetBird.",
"tags": [
"vpn",
"wireguard",
"mesh",
"netwerk"
],
"icon": {
"provider": "selfhst",
"id": "netbird"
},
"version": {
"name": "latest"
},
"categories": [
"netwerk",
"beveiliging"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "netbird",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8254,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "mgmt_url",
"type": "str",
"title": "Adres van de management-server",
"default": "https://netbird.example.com:33073",
"required": true
},
{
"name": "auth_authority",
"type": "str",
"title": "OIDC-uitgever",
"default": "https://id.example.com",
"required": true,
"connect": {
"app": "authentik",
"scheme": "https",
"port": 443
},
"help": "NetBird logt in via een externe dienst; wijs dit naar je Authentik, Keycloak of Pocket ID."
},
{
"name": "auth_client_id",
"type": "str",
"title": "OIDC client-id",
"default": "",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: ghcr.io/openbao/openbao:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- BAO_DEV_ROOT_TOKEN_ID=<< root_token >>
volumes:
- << appdata_dir >>/<< service_name >>:/openbao/file
command: server -dev -dev-listen-address=0.0.0.0:8200
ports:
- "<< port >>:8200"
cap_add:
- IPC_LOCK
restart: unless-stopped

View file

@ -0,0 +1,75 @@
{
"kind": "compose",
"metadata": {
"name": "OpenBao",
"description": "Bewaart geheimen voor machines: certificaten, API-sleutels en databasewachtwoorden, met een houdbaarheidsdatum per stuk. De opvolger van HashiCorp Vault. Let op: Draait hier in ontwikkelmodus: alles staat in het geheugen en is weg na een herstart. Voor echt gebruik zet je in het configuratiebestand een opslagbackend en zegel je hem met echte sleutels.",
"tags": [
"geheimen",
"certificaten",
"beveiliging"
],
"icon": {
"provider": "selfhst",
"id": "openbao"
},
"version": {
"name": "latest"
},
"categories": [
"beveiliging",
"ontwikkeling"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "openbao",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8232,
"required": true,
"description": "Standaard 8200; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "root_token",
"type": "str",
"title": "Root-token",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Waarmee je de eerste keer inlogt"
}
]
}
]
}

View file

@ -0,0 +1,30 @@
services:
<< service_name >>:
image: passbolt/passbolt:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- APP_FULL_BASE_URL=<< app_url >>
- DATASOURCES_DEFAULT_HOST=<< service_name >>-db
- DATASOURCES_DEFAULT_USERNAME=passbolt
- DATASOURCES_DEFAULT_PASSWORD=<< db_password >>
- DATASOURCES_DEFAULT_DATABASE=passbolt
volumes:
- << appdata_dir >>/<< service_name >>:/etc/passbolt/jwt
- << appdata_dir >>/<< service_name >>-gpg:/etc/passbolt/gpg
ports:
- "<< port >>:80"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: mariadb:11
container_name: << service_name >>-db
environment:
- MARIADB_DATABASE=passbolt
- MARIADB_USER=passbolt
- MARIADB_PASSWORD=<< db_password >>
- MARIADB_ROOT_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/mysql
restart: unless-stopped

View file

@ -0,0 +1,88 @@
{
"kind": "compose",
"metadata": {
"name": "Passbolt",
"description": "Wachtwoorden delen binnen een team, met rechten per map en een controlespoor. Vaultwarden is voor jezelf, dit voor een groep. Let op: Na de eerste start maak je de beheerder aan met `docker exec passbolt su -m -c \"/usr/share/php/passbolt/bin/cake passbolt register_user -u jij@example.com -r admin\" -s /bin/sh www-data`.",
"tags": [
"wachtwoorden",
"team",
"beveiliging"
],
"icon": {
"provider": "selfhst",
"id": "passbolt"
},
"version": {
"name": "latest"
},
"categories": [
"beveiliging",
"zakelijk"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "passbolt",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8231,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "https://passbolt.example.com",
"required": true,
"description": "Ligt vast na de eerste start: het zit in de uitnodigingen"
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: ghcr.io/pelican-dev/panel:latest
container_name: << service_name >>
environment:
- APP_URL=<< app_url >>
- APP_TIMEZONE=<< timezone >>
- APP_ENV=production
- APP_KEY=<< app_key >>
volumes:
- << appdata_dir >>/<< service_name >>:/pelican-data
ports:
- "<< port >>:80"
restart: unless-stopped

View file

@ -0,0 +1,89 @@
{
"kind": "compose",
"metadata": {
"name": "Pelican Panel",
"description": "Beheerpaneel voor gameservers: meerdere spellen naast elkaar, met rechten per gebruiker en een console per server. De opvolger van Pterodactyl. Let op: Het paneel beheert de servers; de spellen zelf draaien op een of meer Wings-daemons die je apart installeert.",
"tags": [
"spellen",
"gameserver",
"beheer"
],
"icon": {
"provider": "selfhst",
"id": "pelican"
},
"version": {
"name": "latest"
},
"categories": [
"spellen",
"beheer"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "pelican",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8239,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8239",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "app_key",
"type": "str",
"title": "Applicatiesleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "laravel",
"secret_bytes": 32,
"description": "Laravel-formaat; wordt automatisch in de juiste vorm gemaakt"
}
]
}
]
}

View file

@ -0,0 +1,31 @@
services:
<< service_name >>:
image: madeofpendletonwool/pinepods:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- SEARCH_API_URL=https://search.pinepods.online/api/search
- DB_TYPE=postgresql
- DB_HOST=<< service_name >>-db
- DB_PORT=5432
- DB_USER=pinepods
- DB_NAME=pinepods
- DB_PASSWORD=<< db_password >>
- HOSTNAME=<< app_url >>
volumes:
- << appdata_dir >>/<< service_name >>:/opt/pinepods/backend/data
ports:
- "<< port >>:8040"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=pinepods
- POSTGRES_USER=pinepods
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,86 @@
{
"kind": "compose",
"metadata": {
"name": "PinePods",
"description": "Podcasts beheren en afspelen op je eigen server, met je afspeelpositie gedeeld tussen apparaten.",
"tags": [
"podcasts",
"audio",
"media"
],
"icon": {
"provider": "selfhst",
"id": "pinepods"
},
"version": {
"name": "latest"
},
"categories": [
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "pinepods",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8256,
"required": true,
"description": "Standaard 8040; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8256",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
}
]
}
]
}

View file

@ -0,0 +1,40 @@
services:
<< service_name >>:
image: ghcr.io/plausible/community-edition:v3
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- BASE_URL=<< app_url >>
- SECRET_KEY_BASE=<< secret_key_base >>
- TOTP_VAULT_KEY=<< totp_vault_key >>
- DATABASE_URL=postgres://plausible:<< db_password >>@<< service_name >>-db:5432/plausible
- CLICKHOUSE_DATABASE_URL=http://<< service_name >>-clickhouse:8123/plausible_events
command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
ports:
- "<< port >>:8000"
depends_on:
- << service_name >>-db
- << service_name >>-clickhouse
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=plausible
- POSTGRES_USER=plausible
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped
<< service_name >>-clickhouse:
image: clickhouse/clickhouse-server:24.12-alpine
container_name: << service_name >>-clickhouse
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
volumes:
- << appdata_dir >>/<< service_name >>-clickhouse:/var/lib/clickhouse
ulimits:
nofile:
soft: 262144
hard: 262144
restart: unless-stopped

View file

@ -0,0 +1,106 @@
{
"kind": "compose",
"metadata": {
"name": "Plausible",
"description": "Bezoekersstatistieken zonder cookies, met een script van minder dan een kilobyte. Uitgebreider dan Umami, met doelen en trechters. Let op: Clickhouse vraagt om geheugen; reken op 2 GB voor deze stack.",
"tags": [
"analytics",
"bezoekers",
"privacy"
],
"icon": {
"provider": "selfhst",
"id": "plausible"
},
"version": {
"name": "latest"
},
"categories": [
"analytics"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "plausible",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8253,
"required": true,
"description": "Standaard 8000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8253",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "secret_key_base",
"type": "str",
"title": "Sleutel voor de sessies",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "totp_vault_key",
"type": "str",
"title": "Sleutel voor tweestapsverificatie",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "hex",
"secret_bytes": 32
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: registry:2
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- REGISTRY_STORAGE_DELETE_ENABLED=true
volumes:
- << appdata_dir >>/<< service_name >>:/var/lib/registry
ports:
- "<< port >>:5000"
restart: unless-stopped

View file

@ -0,0 +1,60 @@
{
"kind": "compose",
"metadata": {
"name": "Docker Registry",
"description": "Eigen registry voor je images, zodat je ze niet bij Docker Hub hoeft neer te zetten en je server ze lokaal ophaalt. Let op: Zonder reverse proxy met TLS ervoor weigert Docker ernaartoe te pushen, tenzij je hem als insecure-registry opgeeft.",
"tags": [
"docker",
"registry",
"images"
],
"icon": {
"provider": "selfhst",
"id": "docker"
},
"version": {
"name": "latest"
},
"categories": [
"ontwikkeling",
"beheer"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "registry",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Registrypoort",
"default": 8234,
"required": true,
"description": "Standaard 5000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: diygod/rsshub:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- NODE_ENV=production
- CACHE_TYPE=memory
ports:
- "<< port >>:1200"
restart: unless-stopped

51
apps/rsshub/template.json Normal file
View file

@ -0,0 +1,51 @@
{
"kind": "compose",
"metadata": {
"name": "RSSHub",
"description": "Maakt een feed van sites die er geen aanbieden. Meer dan duizend bronnen worden ondersteund; koppel het aan FreshRSS of Miniflux.",
"tags": [
"rss",
"feeds",
"nieuws"
],
"icon": {
"provider": "selfhst",
"id": "rsshub"
},
"version": {
"name": "latest"
},
"categories": [
"documenten"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "rsshub",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8250,
"required": true,
"description": "Standaard 1200; hier verplaatst omdat die poort al bezet is"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: yourselfhosted/slash:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/opt/slash
ports:
- "<< port >>:5231"
restart: unless-stopped

60
apps/slash/template.json Normal file
View file

@ -0,0 +1,60 @@
{
"kind": "compose",
"metadata": {
"name": "Slash",
"description": "Korte links met een eigen naam, en een startpagina waarop ze allemaal staan. Handig voor adressen die je vaak deelt.",
"tags": [
"links",
"startpagina",
"bladwijzers"
],
"icon": {
"provider": "selfhst",
"id": "slash"
},
"version": {
"name": "latest"
},
"categories": [
"documenten",
"beheer"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "slash",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8249,
"required": true,
"description": "Standaard 5231; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: ghcr.io/speaches-ai/speaches:latest-cpu
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/home/ubuntu/.cache/huggingface
ports:
- "<< port >>:8000"
restart: unless-stopped

View file

@ -0,0 +1,61 @@
{
"kind": "compose",
"metadata": {
"name": "Speaches",
"description": "Spraak naar tekst en terug, achter dezelfde API als OpenAI gebruikt. Home Assistant en bestaande tools sluiten er zo op aan. Let op: Deze variant rekent op de processor. Met een videokaart erbij gaat het een orde van grootte sneller.",
"tags": [
"ai",
"spraak",
"transcriptie",
"api"
],
"icon": {
"provider": "selfhst",
"id": "speaches"
},
"version": {
"name": "latest"
},
"categories": [
"ai",
"domotica"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "speaches",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8238,
"required": true,
"description": "Standaard 8000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: aquasec/trivy:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/root/.cache/trivy
command: server --listen 0.0.0.0:4954
ports:
- "<< port >>:4954"
restart: unless-stopped

60
apps/trivy/template.json Normal file
View file

@ -0,0 +1,60 @@
{
"kind": "compose",
"metadata": {
"name": "Trivy",
"description": "Scant je images op bekende kwetsbaarheden en verlopen pakketten. Draait als server zodat je hem vanaf je CI kunt aanroepen.",
"tags": [
"beveiliging",
"scanner",
"containers"
],
"icon": {
"provider": "selfhst",
"id": "trivy"
},
"version": {
"name": "latest"
},
"categories": [
"beveiliging",
"ontwikkeling"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "trivy",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Serverpoort",
"default": 8233,
"required": true,
"description": "Standaard 4954; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: verdaccio/verdaccio:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/verdaccio/storage
ports:
- "<< port >>:4873"
restart: unless-stopped

View file

@ -0,0 +1,58 @@
{
"kind": "compose",
"metadata": {
"name": "Verdaccio",
"description": "Eigen npm-registry die pakketten van npmjs doorgeeft en cachet, en waar je je eigen pakketten naast kunt zetten.",
"tags": [
"npm",
"registry",
"ontwikkeling"
],
"icon": {
"provider": "selfhst",
"id": "verdaccio"
},
"version": {
"name": "latest"
},
"categories": [
"ontwikkeling"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "verdaccio",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 4873,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,24 @@
services:
<< service_name >>:
image: ghcr.io/wekan/wekan:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- ROOT_URL=<< app_url >>
- PORT=8080
- MONGO_URL=mongodb://app:<< db_password >>@<< service_name >>-db:27017/wekan?authSource=admin
- WITH_API=true
ports:
- "<< port >>:8080"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: mongo:8
container_name: << service_name >>-db
environment:
- MONGO_INITDB_ROOT_USERNAME=app
- MONGO_INITDB_ROOT_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/data/db
restart: unless-stopped

86
apps/wekan/template.json Normal file
View file

@ -0,0 +1,86 @@
{
"kind": "compose",
"metadata": {
"name": "Wekan",
"description": "Kanbanbord met swimlanes, checklists en tijdregistratie. Uitgebreider dan Kanboard, met een eigen database.",
"tags": [
"kanban",
"taken",
"projecten"
],
"icon": {
"provider": "selfhst",
"id": "wekan"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "wekan",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8246,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8246",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
}
]
}
]
}

View file

@ -490,7 +490,8 @@ tailwind.config = {
<div class="flex flex-wrap items-center gap-2 mb-4"> <div class="flex flex-wrap items-center gap-2 mb-4">
<div class="relative flex-1 min-w-[200px] max-w-sm"> <div class="relative flex-1 min-w-[200px] max-w-sm">
<span class="mdi mdi-magnify absolute left-3.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark text-lg"></span> <span class="mdi mdi-magnify absolute left-3.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark text-lg"></span>
<input x-model="storeQuery" :placeholder="t('search_apps')" class="form-input !pl-10 !pr-9"> <input x-model.debounce.200ms="storeQuery" @input="storeLimiet = 60"
:placeholder="t('search_apps')" class="form-input !pl-10 !pr-9">
<button type="button" x-show="storeQuery" @click="storeQuery = ''" <button type="button" x-show="storeQuery" @click="storeQuery = ''"
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark hover:text-tx dark:hover:text-tx-dark" class="absolute right-2.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark hover:text-tx dark:hover:text-tx-dark"
:title="t('clear')"> :title="t('clear')">
@ -534,7 +535,7 @@ tailwind.config = {
class="flex items-center gap-2 px-2.5 py-1.5 rounded-xl border text-[11px] font-semibold transition text-left" class="flex items-center gap-2 px-2.5 py-1.5 rounded-xl border text-[11px] font-semibold transition text-left"
:style="storeCat === c.id ? catStyle(c.id) : ''" :style="storeCat === c.id ? catStyle(c.id) : ''"
:class="storeCat === c.id ? '' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'" :class="storeCat === c.id ? '' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'"
@click="storeCat = (storeCat === c.id ? '' : c.id)"> @click="storeCat = (storeCat === c.id ? '' : c.id); storeLimiet = 60">
<span class="mdi shrink-0" :class="c.icon" <span class="mdi shrink-0" :class="c.icon"
:style="storeCat === c.id ? '' : 'color:' + c.color"></span> :style="storeCat === c.id ? '' : 'color:' + c.color"></span>
<span class="truncate flex-1" x-text="c.name"></span> <span class="truncate flex-1" x-text="c.name"></span>
@ -562,7 +563,7 @@ tailwind.config = {
<button type="button" <button type="button"
class="text-[11px] font-mono px-2.5 py-1 rounded-lg border transition inline-flex items-center gap-1.5" class="text-[11px] font-mono px-2.5 py-1 rounded-lg border transition inline-flex items-center gap-1.5"
:class="storeTag === tg.tag ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'" :class="storeTag === tg.tag ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'"
@click="storeTag = (storeTag === tg.tag ? '' : tg.tag)"> @click="storeTag = (storeTag === tg.tag ? '' : tg.tag); storeLimiet = 60">
<span x-text="tg.tag"></span> <span x-text="tg.tag"></span>
<span class="opacity-50 tabular-nums" x-text="tg.count"></span> <span class="opacity-50 tabular-nums" x-text="tg.count"></span>
</button> </button>
@ -618,7 +619,7 @@ tailwind.config = {
<button @click="deleteRepo(repo.id)" class="btn-icon w-9 h-9 !text-er hover:!border-er/40"><span class="mdi mdi-delete-outline"></span></button> <button @click="deleteRepo(repo.id)" class="btn-icon w-9 h-9 !text-er hover:!border-er/40"><span class="mdi mdi-delete-outline"></span></button>
</div> </div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<template x-for="stack in filteredStore(repo.id)" :key="stack.dir"> <template x-for="stack in zichtbareStore(repo.id)" :key="stack.dir">
<div class="card card-hover p-4 flex flex-col"> <div class="card card-hover p-4 flex flex-col">
<div class="flex items-start gap-3 mb-3"> <div class="flex items-start gap-3 mb-3">
<template x-if="stack.logo_url"> <template x-if="stack.logo_url">
@ -676,6 +677,15 @@ tailwind.config = {
</div> </div>
</div> </div>
</template> </template>
<!-- Niet alles tegelijk: 258 kaarten in de DOM is traag en niemand
scrolt eroverheen. -->
<div x-show="storeVerborgen > 0" class="flex justify-center pt-1">
<button type="button" class="btn" @click="meerTonen()">
<span class="mdi mdi-chevron-down"></span>
<span x-text="t('show_more', {n: storeVerborgen})">Meer tonen</span>
</button>
</div>
</div> </div>
</section> </section>
@ -2388,6 +2398,9 @@ function app() {
stackQuery: '', stackCat: '', stackQuery: '', stackCat: '',
storeQuery: '', storeTag: '', storeCat: '', storeQuery: '', storeTag: '', storeCat: '',
filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false, filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false,
// Niet reactief nodig: puur een geheugensteun voor filteredStore().
_storeCache: { sleutel: null, lijsten: {} },
storeLimiet: 60,
categories: [], catMap: {}, categories: [], catMap: {},
images: [], images: [],
audit: [], auditTotal: 0, auditFacets: {}, auditTimer: null, audit: [], auditTotal: 0, auditFacets: {}, auditTimer: null,
@ -2747,10 +2760,25 @@ function app() {
}, },
// Apps filteren op naam, omschrijving en tags; tagfilter werkt aanvullend. // Apps filteren op naam, omschrijving en tags; tagfilter werkt aanvullend.
//
// Dit wordt per repo minstens drie keer per reactieve tick aangeroepen: in
// de x-show, in de x-for en via storeResultaat in het filtermenu. Met 258
// apps betekende dat bij elke toetsaanslag drie keer de hele lijst langs,
// met een toLowerCase() per veld per app. Vandaar de cache: de uitkomst
// hangt alleen van de filterstand af, dus die berekenen we één keer.
filteredStore(repoId) { filteredStore(repoId) {
const lijst = this.storeStacks[repoId] || []; const sleutel = [repoId, this.storeQuery.trim().toLowerCase(),
this.storeCat, this.storeTag].join('');
const cache = this._storeCache;
if (cache.sleutel === sleutel && cache.lijsten[repoId]) {
return cache.lijsten[repoId];
}
if (cache.sleutel !== sleutel) {
cache.sleutel = sleutel;
cache.lijsten = {};
}
const q = this.storeQuery.trim().toLowerCase(); const q = this.storeQuery.trim().toLowerCase();
return lijst.filter(s => { const uit = (this.storeStacks[repoId] || []).filter(s => {
if (this.storeCat && !(s.categories || []).includes(this.storeCat)) return false; if (this.storeCat && !(s.categories || []).includes(this.storeCat)) return false;
if (this.storeTag && !(s.tags || []).includes(this.storeTag)) return false; if (this.storeTag && !(s.tags || []).includes(this.storeTag)) return false;
if (!q) return true; if (!q) return true;
@ -2759,6 +2787,21 @@ function app() {
|| (s.description || '').toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q)
|| (s.tags || []).some(t => String(t).toLowerCase().includes(q)); || (s.tags || []).some(t => String(t).toLowerCase().includes(q));
}); });
cache.lijsten[repoId] = uit;
return uit;
},
// Wat er daadwerkelijk in beeld komt. 258 kaarten tegelijk in de DOM is
// traag op een telefoon, en niemand scrolt er doorheen.
zichtbareStore(repoId) {
return this.filteredStore(repoId).slice(0, this.storeLimiet);
},
meerTonen() {
this.storeLimiet += 60;
},
// Hoeveel er nog wachten achter de knop, over alle repo's heen.
get storeVerborgen() {
return Math.max(0, this.storeResultaat - Object.keys(this.storeStacks || {})
.reduce((n, id) => n + this.zichtbareStore(id).length, 0));
}, },
// Alle labels uit alle repo's met hun aantal, op frequentie gesorteerd // Alle labels uit alle repo's met hun aantal, op frequentie gesorteerd
// zodat de bruikbaarste vooraan staan. Dit was afgekapt op veertien, wat bij // zodat de bruikbaarste vooraan staan. Dit was afgekapt op veertien, wat bij
@ -2836,6 +2879,7 @@ function app() {
this.storeCat = ''; this.storeCat = '';
this.storeTag = ''; this.storeTag = '';
this.filterTagQuery = ''; this.filterTagQuery = '';
this.storeLimiet = 60;
}, },
get filteredStacks() { get filteredStacks() {

View file

@ -449,5 +449,6 @@
"audit_act_install_newt": "Newt installed", "audit_act_install_newt": "Newt installed",
"audit_act_install_pangolin": "Pangolin installed", "audit_act_install_pangolin": "Pangolin installed",
"backup_appdata": "Include app data", "backup_appdata": "Include app data",
"backup_appdata_hint": "The app's own data, plus a dump of every database. Turn this off for a media library or photo archive — there it runs into hundreds of gigabytes and restic or borg is the better tool." "backup_appdata_hint": "The app's own data, plus a dump of every database. Turn this off for a media library or photo archive — there it runs into hundreds of gigabytes and restic or borg is the better tool.",
"show_more": "Show {n} more"
} }

View file

@ -449,5 +449,6 @@
"audit_act_install_newt": "Newt geïnstalleerd", "audit_act_install_newt": "Newt geïnstalleerd",
"audit_act_install_pangolin": "Pangolin geïnstalleerd", "audit_act_install_pangolin": "Pangolin geïnstalleerd",
"backup_appdata": "Appdata meenemen", "backup_appdata": "Appdata meenemen",
"backup_appdata_hint": "De gegevens van de app zelf, plus een dump van elke database. Zet dit uit bij een mediabibliotheek of fotoarchief — daar loopt het in de honderden gigabytes en is restic of borg een beter middel." "backup_appdata_hint": "De gegevens van de app zelf, plus een dump van elke database. Zet dit uit bij een mediabibliotheek of fotoarchief — daar loopt het in de honderden gigabytes en is restic of borg een beter middel.",
"show_more": "Nog {n} tonen"
} }

View file

@ -150,6 +150,29 @@ check('labels hebben een aantal',
c.storeTags[0].tag === 'media' && c.storeTags[0].count === 2); c.storeTags[0].tag === 'media' && c.storeTags[0].count === 2);
check('alle labels blijven beschikbaar', c.storeTags.length === 5); check('alle labels blijven beschikbaar', c.storeTags.length === 5);
// De cache: dezelfde filterstand hoort niet opnieuw gerekend te worden. Dit
// werd per repo drie keer per tick aangeroepen, met een toLowerCase() per veld
// per app — bij elke toetsaanslag.
const eerste = c.filteredStore('r');
check('zelfde filterstand geeft hetzelfde resultaat', c.filteredStore('r') === eerste);
c.storeQuery = 'son';
check('andere zoekterm geeft een nieuwe lijst', c.filteredStore('r') !== eerste);
check('en die klopt', c.filteredStore('r').length === 1);
c.storeQuery = '';
// De limiet: niet alles tegelijk in de DOM.
c.storeLimiet = 2;
check('niet meer dan de limiet', c.zichtbareStore('r').length === 2);
check('de rest wacht achter de knop', c.storeVerborgen === 2);
c.meerTonen();
check('meer tonen verhoogt de limiet', c.storeLimiet === 62);
check('en dan staat alles er', c.zichtbareStore('r').length === 4);
check('niets meer verborgen', c.storeVerborgen === 0);
c.storeLimiet = 2;
c.wisFilters();
check('filters wissen zet de limiet terug', c.storeLimiet === 60);
c.storeCat = 'media'; c.storeCat = 'media';
check('categorie filtert', c.storeResultaat === 2); check('categorie filtert', c.storeResultaat === 2);
check('teller telt de categorie', c.filterCount === 1); check('teller telt de categorie', c.filterCount === 1);