Compare commits

..

11 commits

Author SHA1 Message Date
c7fe67c65b notification: wire notification system into app lifecycle
Some checks failed
continuous-integration/drone/push Build is failing
Configure notification senders (email, webhook, UnifiedPush) and
dispatcher in initUsecases(). Connect the execution callback from the
checker engine to the dispatcher. Pass notification dependencies
through to the API router.

Add NotificationRetentionDays config option (default 90 days).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:14:07 +07:00
8a65bf1af2 notification: add API endpoints for channels, preferences, history, and acknowledgement
New endpoints under /api/notifications:
- CRUD for notification channels (email, webhook, UnifiedPush)
- CRUD for notification preferences (global, per-domain, per-service)
- Notification history listing
- Test notification endpoint

Acknowledgement endpoints added to scoped checker routes:
- POST /api/domains/:domain/checkers/:checkerId/acknowledge
- DELETE /api/domains/:domain/checkers/:checkerId/acknowledge

Thread NotificationController through route declarations for scoped
checker routes (domain and service level).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:09:29 +07:00
196fd1a061 checker: add execution callback for notification integration
Add ExecutionCallbackSetter interface and onComplete field to the
checker engine. After a successful execution, the callback is fired
asynchronously so it never blocks the checker pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:08:13 +07:00
29c1d2502e notification: add dispatcher with state tracking and acknowledgement
The Dispatcher is the core notification logic: it receives execution
callbacks, detects status transitions via persisted NotificationState,
resolves user preferences by specificity (service > domain > global),
respects quiet hours, and dispatches through configured channels.

Acknowledgement support allows users to suppress repeat notifications
until the next state change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:08:13 +07:00
c2225ddc5e notification: add channel senders for email, webhook, and UnifiedPush
Implement ChannelSender interface with three backends:
- EmailSender: reuses existing Mailer, sends Markdown-formatted alerts
- WebhookSender: HTTP POST with JSON payload and optional HMAC signature
- UnifiedPushSender: HTTP POST following the UnifiedPush protocol

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:08:13 +07:00
48455218d5 notification: add storage interfaces and KV implementations
Introduce NotificationChannelStorage, NotificationPreferenceStorage,
NotificationStateStorage, and NotificationRecordStorage interfaces
with KV-based implementations. Extend the Storage composite interface
and add migration 10 (no-op for KV prefix-based storage).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:08:13 +07:00
263df02e4c notification: add notification models and error sentinels
Add NotificationChannel, NotificationPreference, NotificationState,
and NotificationRecord models for the upcoming notification system.
These models support email, webhook, and UnifiedPush channels with
per-user/domain/service preference scoping and state deduplication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:08:13 +07:00
7a51b2e540 Add Prometheus export documentation
Some checks reported errors
continuous-integration/drone/push Build was killed
2026-04-16 17:02:31 +07:00
195509f48a web: add Prometheus metrics URL link to checker config page 2026-04-16 17:02:31 +07:00
92df4a58b4 web-admin: wire dashboard to /metrics with collapsible details
Replaces the three REST count calls with a single Prometheus scrape that
auto-refreshes every 15s, surfaces queue/worker/in-flight/RSS/version/uptime
as featured cards, and tucks counters and Go runtime stats under a
"Show more metrics" Collapse.
2026-04-16 17:02:31 +07:00
6fd9d08195 checkers: add filter predicate to ListExecutionsBy* storage methods
Metrics endpoints now skip incomplete/planned executions by passing a
`doneExecution` filter so only fully-evaluated runs contribute to the
Prometheus output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:02:31 +07:00
43 changed files with 357 additions and 695 deletions

View file

@ -1,14 +0,0 @@
happyDomain
===========
This product bundles third-party components under the following licenses.
The original license texts are reproduced in full below.
{{ range . }}
--------------------------------------------------------------------------------
Module: {{ .Name }}
License: {{ .LicenseName }}
{{ with .LicenseURL }}Source: {{ . }}
{{ end }}
{{ .LicenseText }}
{{ end }}

View file

@ -28,8 +28,8 @@ steps:
commands: commands:
- apk --no-cache add tar - apk --no-cache add tar
- yarn config set network-timeout 100000 - yarn config set network-timeout 100000
- yarn install - yarn --cwd web install
- tar --transform="s@.@./happydomain-${DRONE_COMMIT}@" --exclude-vcs --exclude=./node_modules/.cache --exclude=./web/node_modules/.cache --exclude=./web-admin/node_modules/.cache -czf /dev/shm/happydomain-src.tar.gz ./package.json ./package-lock.json ./.npmrc ./node_modules/ ./web/ ./web-admin/ - tar --transform="s@.@./happydomain-${DRONE_COMMIT}@" --exclude-vcs --exclude=./web/node_modules/.cache -czf /dev/shm/happydomain-src.tar.gz .
- mkdir deploy - mkdir deploy
- mv /dev/shm/happydomain-src.tar.gz deploy - mv /dev/shm/happydomain-src.tar.gz deploy
- yarn --cwd web --offline run svelte-kit sync && yarn --cwd web --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-base/client.gen.ts - yarn --cwd web --offline run svelte-kit sync && yarn --cwd web --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-base/client.gen.ts
@ -37,25 +37,14 @@ steps:
- yarn --cwd web-admin --offline run svelte-kit sync && yarn --cwd web-admin --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-admin/client.gen.ts - yarn --cwd web-admin --offline run svelte-kit sync && yarn --cwd web-admin --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-admin/client.gen.ts
- yarn --cwd web-admin --offline build - yarn --cwd web-admin --offline build
- name: frontend NOTICE
image: node:24-alpine
commands:
- npx --yes generate-license-file@3 --input web/package.json --output deploy/NOTICE.web --ci
- npx --yes generate-license-file@3 --input web-admin/package.json --output deploy/NOTICE.web-admin --ci
- for f in deploy/NOTICE.web deploy/NOTICE.web-admin; do { echo '------'; tail -n +3 "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"; done
when:
event:
- tag
- name: backend-commit - name: backend-commit
image: golang:1-alpine image: golang:1-alpine
commands: commands:
- apk add --no-cache git - apk add --no-cache git
- go build -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/ - go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain - ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
environment: environment:
CGO_ENABLED: 0 CGO_ENABLED: 0
GOFLAGS: "-tags=netgo,swagger,web"
when: when:
event: event:
exclude: exclude:
@ -65,16 +54,10 @@ steps:
image: golang:1-alpine image: golang:1-alpine
commands: commands:
- apk add --no-cache git - apk add --no-cache git
- go build -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/ - go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain - ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
# Generate NOTICE file (merging Go + frontend)
- go install github.com/google/go-licenses@v1.6.0
- go-licenses report ./cmd/happyDomain --template .drone-notice.tpl > deploy/NOTICE.go
- cat deploy/NOTICE.go deploy/NOTICE.web deploy/NOTICE.web-admin > deploy/NOTICE
- rm deploy/NOTICE.go deploy/NOTICE.web deploy/NOTICE.web-admin
environment: environment:
CGO_ENABLED: 0 CGO_ENABLED: 0
GOFLAGS: "-tags=netgo,swagger,web"
when: when:
event: event:
- tag - tag
@ -171,7 +154,11 @@ steps:
from_secret: git_nemunaire_token from_secret: git_nemunaire_token
base_url: https://git.nemunai.re base_url: https://git.nemunai.re
draft: true draft: true
files: deploy/* files:
- happydomain-src.tar.gz
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- happydomain-darwin-${DRONE_STAGE_ARCH}
- happydomain-sbom.spdx.json
when: when:
event: event:
- tag - tag
@ -183,7 +170,11 @@ steps:
from_secret: codeberg_token from_secret: codeberg_token
base_url: https://codeberg.org base_url: https://codeberg.org
draft: true draft: true
files: deploy/* files:
- happydomain-src.tar.gz
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- happydomain-darwin-${DRONE_STAGE_ARCH}
- happydomain-sbom.spdx.json
when: when:
event: event:
- tag - tag
@ -195,7 +186,11 @@ steps:
from_secret: github_release_token from_secret: github_release_token
draft: true draft: true
github_url: https://github.com github_url: https://github.com
files: deploy/* files:
- happydomain-src.tar.gz
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- happydomain-darwin-${DRONE_STAGE_ARCH}
- happydomain-sbom.spdx.json
when: when:
event: event:
- tag - tag
@ -237,8 +232,8 @@ steps:
- name: frontend - name: frontend
image: node:24-alpine image: node:24-alpine
commands: commands:
- npm install --network-timeout=100000
- cd web - cd web
- npm install --network-timeout=100000
- npx svelte-kit sync && npm run generate:api - npx svelte-kit sync && npm run generate:api
- npm test - npm test
- npm run build - npm run build
@ -367,7 +362,9 @@ steps:
base_url: https://git.nemunai.re base_url: https://git.nemunai.re
draft: true draft: true
prerelease: true prerelease: true
files: deploy/* files:
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- happydomain-darwin-${DRONE_STAGE_ARCH}
when: when:
event: event:
- tag - tag
@ -380,7 +377,9 @@ steps:
base_url: https://codeberg.org base_url: https://codeberg.org
draft: true draft: true
prerelease: true prerelease: true
files: deploy/* files:
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- happydomain-darwin-${DRONE_STAGE_ARCH}
when: when:
event: event:
- tag - tag
@ -393,7 +392,9 @@ steps:
draft: true draft: true
prerelease: true prerelease: true
github_url: https://github.com github_url: https://github.com
files: deploy/* files:
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
- happydomain-darwin-${DRONE_STAGE_ARCH}
when: when:
event: event:
- tag - tag

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
./happydomain ./happydomain
./happyDomain ./happyDomain
node_modules

2
go.mod
View file

@ -27,7 +27,7 @@ require (
github.com/libdns/libdns v1.1.1 github.com/libdns/libdns v1.1.1
github.com/miekg/dns v1.1.72 github.com/miekg/dns v1.1.72
github.com/mileusna/useragent v1.3.5 github.com/mileusna/useragent v1.3.5
github.com/oracle/nosql-go-sdk v1.4.8 github.com/oracle/nosql-go-sdk v1.4.7
github.com/ovh/go-ovh v1.9.0 github.com/ovh/go-ovh v1.9.0
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1

11
go.sum
View file

@ -135,10 +135,6 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
@ -476,8 +472,8 @@ github.com/openrdap/rdap v0.9.1 h1:Rv6YbanbiVPsKRvOLdUmlU1AL5+2OFuEFLjFN+mQsCM=
github.com/openrdap/rdap v0.9.1/go.mod h1:vKSiotbsENrjM/vaHXLddXbW8iQkBfa+ldEuYEjyLTQ= github.com/openrdap/rdap v0.9.1/go.mod h1:vKSiotbsENrjM/vaHXLddXbW8iQkBfa+ldEuYEjyLTQ=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/oracle/nosql-go-sdk v1.4.8 h1:eMzz+yNLHvB0GCPAWxe0qYttBJF7Fh0Aup+zectpgc4= github.com/oracle/nosql-go-sdk v1.4.7 h1:dqVBSMulObDj0JHm1mAncTHrQg8wIiQJNC0JRNKPACg=
github.com/oracle/nosql-go-sdk v1.4.8/go.mod h1:xgJE9wxADDbk7vR4FGA4NOt4RNAaIsQOj4sCATmCVXM= github.com/oracle/nosql-go-sdk v1.4.7/go.mod h1:xgJE9wxADDbk7vR4FGA4NOt4RNAaIsQOj4sCATmCVXM=
github.com/oracle/oci-go-sdk/v65 v65.111.0 h1:eDkWg6ZN0uKwWzSekoFcQJhR+C+F/aVdTwr+lGHU9Qk= github.com/oracle/oci-go-sdk/v65 v65.111.0 h1:eDkWg6ZN0uKwWzSekoFcQJhR+C+F/aVdTwr+lGHU9Qk=
github.com/oracle/oci-go-sdk/v65 v65.111.0/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs= github.com/oracle/oci-go-sdk/v65 v65.111.0/go.mod h1:8ZzvzuEG/cFLFZhxg/Mg1w19KqyXBKO3c17QIc5PkGs=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
@ -633,8 +629,6 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
@ -654,6 +648,7 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View file

@ -120,46 +120,6 @@ func (bc *BackupController) DoBackup() (ret happydns.Backup) {
} }
} }
// Checker configurations (positional, one entry per (checker, user?, domain?, service?)).
if cfgIter, err := bc.store.ListAllCheckerConfigurations(); err != nil {
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve CheckerConfigurations: %s", err.Error()))
} else {
defer cfgIter.Close()
for cfgIter.Next() {
ret.CheckerConfigurations = append(ret.CheckerConfigurations, cfgIter.Item())
}
}
// Check plans.
if planIter, err := bc.store.ListAllCheckPlans(); err != nil {
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve CheckPlans: %s", err.Error()))
} else {
defer planIter.Close()
for planIter.Next() {
ret.CheckPlans = append(ret.CheckPlans, planIter.Item())
}
}
// Check evaluations.
if evalIter, err := bc.store.ListAllEvaluations(); err != nil {
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve CheckEvaluations: %s", err.Error()))
} else {
defer evalIter.Close()
for evalIter.Next() {
ret.CheckEvaluations = append(ret.CheckEvaluations, evalIter.Item())
}
}
// Executions.
if execIter, err := bc.store.ListAllExecutions(); err != nil {
ret.Errors = append(ret.Errors, fmt.Sprintf("unable to retrieve Executions: %s", err.Error()))
} else {
defer execIter.Close()
for execIter.Next() {
ret.Executions = append(ret.Executions, execIter.Item())
}
}
return return
} }
@ -215,29 +175,6 @@ func (bc *BackupController) DoRestore(backup *happydns.Backup) (errs error) {
errs = errors.Join(errs, bc.store.UpdateSession(session)) errs = errors.Join(errs, bc.store.UpdateSession(session))
} }
// Checker configurations.
for _, cfg := range backup.CheckerConfigurations {
if cfg == nil {
continue
}
errs = errors.Join(errs, bc.store.UpdateCheckerConfiguration(cfg.CheckName, cfg.UserId, cfg.DomainId, cfg.ServiceId, cfg.Options))
}
// Check plans.
for _, plan := range backup.CheckPlans {
errs = errors.Join(errs, bc.store.RestoreCheckPlan(plan))
}
// Check evaluations (reference plans, restored above).
for _, eval := range backup.CheckEvaluations {
errs = errors.Join(errs, bc.store.RestoreEvaluation(eval))
}
// Executions.
for _, exec := range backup.Executions {
errs = errors.Join(errs, bc.store.RestoreExecution(exec))
}
return return
} }

View file

@ -22,9 +22,6 @@
package controller package controller
import ( import (
"fmt"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/model" "git.happydns.org/happyDomain/model"
@ -44,25 +41,14 @@ func NewTidyController(tidyUpService happydns.TidyUpUseCase) *TidyController {
// //
// @Summary Tidy up the database // @Summary Tidy up the database
// @Schemes // @Schemes
// @Description Performs cleanup and maintenance operations on the database, removing orphaned records and optimizing storage. When drop_invalid is true (default), records that fail to decode (e.g. after a schema change) are deleted; set it to false to only log them. // @Description Performs cleanup and maintenance operations on the database, removing orphaned records and optimizing storage.
// @Tags admin // @Tags admin
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param drop_invalid query bool false "Delete records that fail to decode instead of only logging (default true)"
// @Security securitydefinitions.basic // @Security securitydefinitions.basic
// @Success 200 {boolean} bool // @Success 200 {boolean} bool
// @Failure 500 {object} happydns.ErrorResponse "Internal server error" // @Failure 500 {object} happydns.ErrorResponse "Internal server error"
// @Router /tidy [post] // @Router /tidy [post]
func (tc *TidyController) TidyDB(c *gin.Context) { func (tc *TidyController) TidyDB(c *gin.Context) {
dropInvalid := true happydns.ApiResponse(c, true, tc.tidyUpService.TidyAll())
if v := c.Query("drop_invalid"); v != "" {
parsed, err := strconv.ParseBool(v)
if err != nil {
happydns.ApiResponse(c, nil, fmt.Errorf("drop_invalid must be a boolean: %w", err))
return
}
dropInvalid = parsed
}
happydns.ApiResponse(c, true, tc.tidyUpService.TidyAll(dropInvalid))
} }

View file

@ -244,7 +244,7 @@ func (cc *CheckerController) TriggerCheck(c *gin.Context) {
} else { } else {
go func() { go func() {
if _, err := cc.engine.RunExecution(context.WithoutCancel(c.Request.Context()), exec, plan, req.Options); err != nil { if _, err := cc.engine.RunExecution(context.WithoutCancel(c.Request.Context()), exec, plan, req.Options); err != nil {
log.Printf("async RunExecution error for checker %q execution %s: %v", cname, exec.Id.String(), err) log.Printf("async RunExecution error for checker %q execution %v: %v", cname, exec.Id, err)
} }
}() }()
c.JSON(http.StatusAccepted, exec) c.JSON(http.StatusAccepted, exec)

View file

@ -75,7 +75,7 @@ func SameUserHandler(c *gin.Context) {
user := c.MustGet("user").(*happydns.User) user := c.MustGet("user").(*happydns.User)
if !bytes.Equal(user.Id, myuser.Id) { if !bytes.Equal(user.Id, myuser.Id) {
log.Printf("%s: tries to do action as %s (logged %s)", c.ClientIP(), myuser.Id.String(), user.Id.String()) log.Printf("%s: tries to do action as %s (logged %s)", c.ClientIP(), myuser.Id, user.Id)
c.AbortWithStatusJSON(http.StatusForbidden, happydns.ErrorResponse{Message: "Not authorized"}) c.AbortWithStatusJSON(http.StatusForbidden, happydns.ErrorResponse{Message: "Not authorized"})
return return
} }

View file

@ -559,21 +559,6 @@ func (s *instrumentedStorage) PutCachedObservation(target happydns.CheckTarget,
return s.inner.PutCachedObservation(target, key, entry) return s.inner.PutCachedObservation(target, key, entry)
} }
func (s *instrumentedStorage) RestoreCheckPlan(plan *happydns.CheckPlan) (err error) {
defer observe("restore", "check_plan")(&err)
return s.inner.RestoreCheckPlan(plan)
}
func (s *instrumentedStorage) RestoreEvaluation(eval *happydns.CheckEvaluation) (err error) {
defer observe("restore", "check_evaluation")(&err)
return s.inner.RestoreEvaluation(eval)
}
func (s *instrumentedStorage) RestoreExecution(exec *happydns.Execution) (err error) {
defer observe("restore", "execution")(&err)
return s.inner.RestoreExecution(exec)
}
func (s *instrumentedStorage) PutState(state *happydns.NotificationState) (err error) { func (s *instrumentedStorage) PutState(state *happydns.NotificationState) (err error) {
defer observe("put", "notification_state")(&err) defer observe("put", "notification_state")(&err)
return s.inner.PutState(state) return s.inner.PutState(state)

View file

@ -106,24 +106,6 @@ func (s *KVStorage) CreateEvaluation(eval *happydns.CheckEvaluation) error {
return nil return nil
} }
// RestoreEvaluation writes an evaluation at its existing Id and rebuilds
// its secondary indexes. Used by the backup restore path.
func (s *KVStorage) RestoreEvaluation(eval *happydns.CheckEvaluation) error {
if err := s.db.Put(fmt.Sprintf("chckeval|%s", eval.Id.String()), eval); err != nil {
return err
}
if eval.PlanID != nil {
indexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
if err := s.db.Put(indexKey, true); err != nil {
return err
}
}
checkerIndexKey := fmt.Sprintf("chckeval-chkr|%s|%s|%s", eval.CheckerID, eval.Target.String(), eval.Id.String())
return s.db.Put(checkerIndexKey, true)
}
func (s *KVStorage) DeleteEvaluation(evalID happydns.Identifier) error { func (s *KVStorage) DeleteEvaluation(evalID happydns.Identifier) error {
// Load first to find plan ID for index cleanup. // Load first to find plan ID for index cleanup.
eval, err := s.GetEvaluation(evalID) eval, err := s.GetEvaluation(evalID)

View file

@ -138,16 +138,6 @@ func (s *KVStorage) putCheckPlanIndexes(plan *happydns.CheckPlan) error {
return nil return nil
} }
// RestoreCheckPlan writes a plan at its existing Id and (re)builds its
// secondary indexes. Used by the backup restore path, which must preserve
// the original identifier instead of generating a new one.
func (s *KVStorage) RestoreCheckPlan(plan *happydns.CheckPlan) error {
if err := s.db.Put(fmt.Sprintf("chckpln|%s", plan.Id.String()), plan); err != nil {
return err
}
return s.putCheckPlanIndexes(plan)
}
func (s *KVStorage) DeleteCheckPlan(planID happydns.Identifier) error { func (s *KVStorage) DeleteCheckPlan(planID happydns.Identifier) error {
plan, err := s.GetCheckPlan(planID) plan, err := s.GetCheckPlan(planID)
if err != nil { if err != nil {

View file

@ -123,40 +123,6 @@ func (s *KVStorage) CreateExecution(exec *happydns.Execution) error {
return nil return nil
} }
// RestoreExecution writes an execution at its existing Id and rebuilds
// its secondary indexes. Used by the backup restore path.
func (s *KVStorage) RestoreExecution(exec *happydns.Execution) error {
if err := s.db.Put(fmt.Sprintf("chckexec|%s", exec.Id.String()), exec); err != nil {
return err
}
if exec.PlanID != nil {
indexKey := fmt.Sprintf("chckexec-plan|%s|%s", exec.PlanID.String(), exec.Id.String())
if err := s.db.Put(indexKey, true); err != nil {
return err
}
}
checkerIndexKey := fmt.Sprintf("chckexec-chkr|%s|%s|%s", exec.CheckerID, exec.Target.String(), exec.Id.String())
if err := s.db.Put(checkerIndexKey, true); err != nil {
return err
}
if exec.Target.UserId != "" {
if err := s.db.Put(executionUserIndexKey(exec.Target.UserId, exec.Id.String()), true); err != nil {
return err
}
}
if exec.Target.DomainId != "" {
if err := s.db.Put(executionDomainIndexKey(exec.Target.DomainId, exec.Id.String()), true); err != nil {
return err
}
}
return nil
}
func (s *KVStorage) UpdateExecution(exec *happydns.Execution) error { func (s *KVStorage) UpdateExecution(exec *happydns.Execution) error {
// Load the old record so we can detect changed index keys. // Load the old record so we can detect changed index keys.
old, err := s.GetExecution(exec.Id) old, err := s.GetExecution(exec.Id)

View file

@ -76,7 +76,6 @@ func (it *KVIterator[T]) NextWithError() bool {
} }
return true return true
} }
it.err = nil
return false return false
} }

View file

@ -38,12 +38,12 @@ import (
// abstract.EMail // abstract.EMail
func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([]*happydns.ServiceMessage, error) { func explodeAbstractEMail(dn happydns.Subdomain, in *happydns.ServiceMessage) ([]*happydns.ServiceMessage, error) {
var val struct { var val struct {
MX []map[string]any `json:"mx,omitempty"` MX []map[string]any `json:"mx,omitempty"`
SPF map[string]any `json:"spf,omitempty"` SPF map[string]any `json:"spf,omitempty"`
DKIM map[string]*svcs.DKIM `json:"dkim,omitempty"` DKIM map[string]*svcs.DKIM `json:"dkim,omitempty"`
DMARC *svcs.DMARCFields `json:"dmarc,omitempty"` DMARC *svcs.DMARCFields `json:"dmarc,omitempty"`
MTA_STS *svcs.MTASTSFields `json:"mta_sts,omitempty"` MTA_STS *svcs.MTASTSFields `json:"mta_sts,omitempty"`
TLS_RPT *svcs.TLS_RPTField `json:"tls_rpt,omitempty"` TLS_RPT *svcs.TLS_RPTField `json:"tls_rpt,omitempty"`
} }
err := json.Unmarshal(in.Service, &val) err := json.Unmarshal(in.Service, &val)
@ -815,12 +815,7 @@ func migrateFrom7(s *KVStorage) error {
return nil, err return nil, err
} }
var rr dns.RR rr, err := dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN CNAME %s", val["SubDomain"], helpers.DomainFQDN(val["Target"], "zZzZ.")))
if strings.Contains(val["Target"], "IN\tCNAME") {
rr, err = dns.NewRR(val["Target"])
} else {
rr, err = dns.NewRR(fmt.Sprintf("%s.zZzZ. 0 IN CNAME %s", val["SubDomain"], helpers.DomainFQDN(val["Target"], "zZzZ.")))
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -22,7 +22,6 @@
package database package database
import ( import (
"fmt"
"log" "log"
) )
@ -34,15 +33,6 @@ func migrateFrom9(s *KVStorage) (err error) {
for sessions.Next() { for sessions.Next() {
session := sessions.Item() session := sessions.Item()
if len(session.Id) != 103 {
err = sessions.DropItem()
if err != nil {
return fmt.Errorf("unable to drop invalid session: %s: %w", session.Id, err)
}
log.Printf("Drop invalid session identifier: %s", session.Id)
continue
}
err := s.UpdateSession(session) err := s.UpdateSession(session)
if err != nil { if err != nil {
return err return err

View file

@ -374,11 +374,6 @@ func (s *planStore) UpdateCheckPlan(plan *happydns.CheckPlan) error {
return nil return nil
} }
func (s *planStore) RestoreCheckPlan(plan *happydns.CheckPlan) error {
s.plans[plan.Id.String()] = plan
return nil
}
func (s *planStore) DeleteCheckPlan(planID happydns.Identifier) error { func (s *planStore) DeleteCheckPlan(planID happydns.Identifier) error {
delete(s.plans, planID.String()) delete(s.plans, planID.String())
return nil return nil

View file

@ -102,7 +102,6 @@ func (s *mockExecStore) GetExecution(happydns.Identifier) (*happydns.Execution,
} }
func (s *mockExecStore) CreateExecution(*happydns.Execution) error { return nil } func (s *mockExecStore) CreateExecution(*happydns.Execution) error { return nil }
func (s *mockExecStore) UpdateExecution(*happydns.Execution) error { return nil } func (s *mockExecStore) UpdateExecution(*happydns.Execution) error { return nil }
func (s *mockExecStore) RestoreExecution(*happydns.Execution) error { return nil }
func (s *mockExecStore) DeleteExecutionsByChecker(string, happydns.CheckTarget) error { return nil } func (s *mockExecStore) DeleteExecutionsByChecker(string, happydns.CheckTarget) error { return nil }
func (s *mockExecStore) TidyExecutionIndexes() error { return nil } func (s *mockExecStore) TidyExecutionIndexes() error { return nil }
func (s *mockExecStore) ClearExecutions() error { return nil } func (s *mockExecStore) ClearExecutions() error { return nil }
@ -493,7 +492,6 @@ func (s *mockEvalStore) GetLatestEvaluation(happydns.Identifier) (*happydns.Chec
return nil, nil return nil, nil
} }
func (s *mockEvalStore) CreateEvaluation(*happydns.CheckEvaluation) error { return nil } func (s *mockEvalStore) CreateEvaluation(*happydns.CheckEvaluation) error { return nil }
func (s *mockEvalStore) RestoreEvaluation(*happydns.CheckEvaluation) error { return nil }
func (s *mockEvalStore) DeleteEvaluationsByChecker(string, happydns.CheckTarget) error { return nil } func (s *mockEvalStore) DeleteEvaluationsByChecker(string, happydns.CheckTarget) error { return nil }
func (s *mockEvalStore) TidyEvaluationIndexes() error { return nil } func (s *mockEvalStore) TidyEvaluationIndexes() error { return nil }
func (s *mockEvalStore) ClearEvaluations() error { return nil } func (s *mockEvalStore) ClearEvaluations() error { return nil }

View file

@ -492,7 +492,7 @@ func (s *Scheduler) buildQueue() {
svcTarget := happydns.CheckTarget{UserId: uid.String(), DomainId: did.String(), ServiceId: sid.String(), ServiceType: svc.Type} svcTarget := happydns.CheckTarget{UserId: uid.String(), DomainId: did.String(), ServiceId: sid.String(), ServiceType: svc.Type}
for _, c := range serviceCheckers { for _, c := range serviceCheckers {
if !serviceCheckerApplies(c.def, svc.Type) { if len(c.def.Availability.LimitToServices) > 0 && !slices.Contains(c.def.Availability.LimitToServices, svc.Type) {
continue continue
} }
s.enqueueJob(c.id, c.def, svcTarget, disabledSet, planMap, lastRun) s.enqueueJob(c.id, c.def, svcTarget, disabledSet, planMap, lastRun)
@ -534,7 +534,7 @@ func (s *Scheduler) NotifyDomainChange(domain *happydns.Domain) {
} }
if def.Availability.ApplyToService { if def.Availability.ApplyToService {
for _, svc := range services { for _, svc := range services {
if !serviceCheckerApplies(def, svc.Type) { if len(def.Availability.LimitToServices) > 0 && !slices.Contains(def.Availability.LimitToServices, svc.Type) {
continue continue
} }
svcTarget := happydns.CheckTarget{UserId: uid.String(), DomainId: didStr, ServiceId: svc.Id.String(), ServiceType: svc.Type} svcTarget := happydns.CheckTarget{UserId: uid.String(), DomainId: didStr, ServiceId: svc.Id.String(), ServiceType: svc.Type}
@ -579,7 +579,7 @@ func (s *Scheduler) NotifyDomainChange(domain *happydns.Domain) {
if def.Availability.ApplyToService { if def.Availability.ApplyToService {
for _, svc := range services { for _, svc := range services {
if !serviceCheckerApplies(def, svc.Type) { if len(def.Availability.LimitToServices) > 0 && !slices.Contains(def.Availability.LimitToServices, svc.Type) {
continue continue
} }
sid := svc.Id sid := svc.Id
@ -627,20 +627,10 @@ func (s *Scheduler) NotifyDomainRemoved(domainID happydns.Identifier) {
s.mu.Unlock() s.mu.Unlock()
if n > 0 { if n > 0 {
log.Printf("Scheduler: NotifyDomainRemoved(%s): removed %d jobs", domainID.String(), n) log.Printf("Scheduler: NotifyDomainRemoved(%s): removed %d jobs", domainID, n)
} }
} }
// serviceCheckerApplies reports whether a service-scoped checker should be
// auto-scheduled for the given service type. Auto-scheduling is restricted to
// checkers that explicitly declare the service type in LimitToServices; a
// checker with an empty LimitToServices must be activated manually via a
// CheckPlan.
func serviceCheckerApplies(def *happydns.CheckerDefinition, serviceType string) bool {
return len(def.Availability.LimitToServices) > 0 &&
slices.Contains(def.Availability.LimitToServices, serviceType)
}
// buildPlanIndex builds disabled and plan lookup maps from a slice of plans. // buildPlanIndex builds disabled and plan lookup maps from a slice of plans.
func buildPlanIndex(plans []*happydns.CheckPlan) (disabledSet map[string]bool, planMap map[string]*happydns.CheckPlan) { func buildPlanIndex(plans []*happydns.CheckPlan) (disabledSet map[string]bool, planMap map[string]*happydns.CheckPlan) {
disabledSet = make(map[string]bool) disabledSet = make(map[string]bool)
@ -741,7 +731,7 @@ func (s *Scheduler) loadDomainServices(domain *happydns.Domain) []*happydns.Serv
} }
zone, err := s.zoneStore.GetZone(domain.ZoneHistory[idx]) zone, err := s.zoneStore.GetZone(domain.ZoneHistory[idx])
if err != nil { if err != nil {
log.Printf("Scheduler: failed to load zone %s for domain %s: %v", domain.ZoneHistory[idx].String(), domain.DomainName, err) log.Printf("Scheduler: failed to load zone %s for domain %s: %v", domain.ZoneHistory[idx], domain.DomainName, err)
continue continue
} }
for _, svcs := range zone.Services { for _, svcs := range zone.Services {

View file

@ -121,9 +121,8 @@ func (s *mockPlanStore) CreateCheckPlan(plan *happydns.CheckPlan) error {
s.plans = append(s.plans, plan) s.plans = append(s.plans, plan)
return nil return nil
} }
func (s *mockPlanStore) UpdateCheckPlan(plan *happydns.CheckPlan) error { return nil } func (s *mockPlanStore) UpdateCheckPlan(plan *happydns.CheckPlan) error { return nil }
func (s *mockPlanStore) RestoreCheckPlan(plan *happydns.CheckPlan) error { return nil } func (s *mockPlanStore) DeleteCheckPlan(happydns.Identifier) error { return nil }
func (s *mockPlanStore) DeleteCheckPlan(happydns.Identifier) error { return nil }
func (s *mockPlanStore) TidyCheckPlanIndexes() error { return nil } func (s *mockPlanStore) TidyCheckPlanIndexes() error { return nil }
func (s *mockPlanStore) ClearCheckPlans() error { return nil } func (s *mockPlanStore) ClearCheckPlans() error { return nil }

View file

@ -61,7 +61,6 @@ type CheckPlanStorage interface {
GetCheckPlan(planID happydns.Identifier) (*happydns.CheckPlan, error) GetCheckPlan(planID happydns.Identifier) (*happydns.CheckPlan, error)
CreateCheckPlan(plan *happydns.CheckPlan) error CreateCheckPlan(plan *happydns.CheckPlan) error
UpdateCheckPlan(plan *happydns.CheckPlan) error UpdateCheckPlan(plan *happydns.CheckPlan) error
RestoreCheckPlan(plan *happydns.CheckPlan) error
DeleteCheckPlan(planID happydns.Identifier) error DeleteCheckPlan(planID happydns.Identifier) error
TidyCheckPlanIndexes() error TidyCheckPlanIndexes() error
ClearCheckPlans() error ClearCheckPlans() error
@ -85,7 +84,6 @@ type CheckEvaluationStorage interface {
GetEvaluation(evalID happydns.Identifier) (*happydns.CheckEvaluation, error) GetEvaluation(evalID happydns.Identifier) (*happydns.CheckEvaluation, error)
GetLatestEvaluation(planID happydns.Identifier) (*happydns.CheckEvaluation, error) GetLatestEvaluation(planID happydns.Identifier) (*happydns.CheckEvaluation, error)
CreateEvaluation(eval *happydns.CheckEvaluation) error CreateEvaluation(eval *happydns.CheckEvaluation) error
RestoreEvaluation(eval *happydns.CheckEvaluation) error
DeleteEvaluation(evalID happydns.Identifier) error DeleteEvaluation(evalID happydns.Identifier) error
DeleteEvaluationsByChecker(checkerID string, target happydns.CheckTarget) error DeleteEvaluationsByChecker(checkerID string, target happydns.CheckTarget) error
TidyEvaluationIndexes() error TidyEvaluationIndexes() error
@ -102,7 +100,6 @@ type ExecutionStorage interface {
GetExecution(execID happydns.Identifier) (*happydns.Execution, error) GetExecution(execID happydns.Identifier) (*happydns.Execution, error)
CreateExecution(exec *happydns.Execution) error CreateExecution(exec *happydns.Execution) error
UpdateExecution(exec *happydns.Execution) error UpdateExecution(exec *happydns.Execution) error
RestoreExecution(exec *happydns.Execution) error
DeleteExecution(execID happydns.Identifier) error DeleteExecution(execID happydns.Identifier) error
DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error
TidyExecutionIndexes() error TidyExecutionIndexes() error

View file

@ -209,7 +209,7 @@ func (uc *ZoneCorrectionApplierUsecase) Apply(
if len(domain.ZoneHistory) > 1 { if len(domain.ZoneHistory) > 1 {
prevZone, prevErr := uc.zoneGetter.Get(domain.ZoneHistory[1]) prevZone, prevErr := uc.zoneGetter.Get(domain.ZoneHistory[1])
if prevErr != nil { if prevErr != nil {
log.Printf("ReassociateMetadata: unable to load previous zone %s: %s (metadata will not be transferred)", domain.ZoneHistory[1].String(), prevErr) log.Printf("ReassociateMetadata: unable to load previous zone %s: %s (metadata will not be transferred)", domain.ZoneHistory[1], prevErr)
} else { } else {
zoneUC.ReassociateMetadata(prevZone.Services, services, domain.DomainName, defaultTTL) zoneUC.ReassociateMetadata(prevZone.Services, services, domain.DomainName, defaultTTL)
} }

View file

@ -62,7 +62,7 @@ func (uc *ZoneImporterUsecase) Import(user *happydns.User, domain *happydns.Doma
if len(domain.ZoneHistory) > 0 { if len(domain.ZoneHistory) > 0 {
prevZone, err := uc.zoneGetter.Get(domain.ZoneHistory[0]) prevZone, err := uc.zoneGetter.Get(domain.ZoneHistory[0])
if err != nil { if err != nil {
log.Printf("ReassociateMetadata: unable to load previous zone %s: %s (metadata will not be transferred)", domain.ZoneHistory[0].String(), err) log.Printf("ReassociateMetadata: unable to load previous zone %s: %s (metadata will not be transferred)", domain.ZoneHistory[0], err)
} else { } else {
zoneUC.ReassociateMetadata(prevZone.Services, services, domain.DomainName, defaultTTL) zoneUC.ReassociateMetadata(prevZone.Services, services, domain.DomainName, defaultTTL)
} }

View file

@ -40,33 +40,8 @@ func NewTidyUpUsecase(store storage.Storage) happydns.TidyUpUseCase {
} }
} }
// iterateTidy drives an iterator using NextWithError so Tidy can decide func (tu *tidyUpUsecase) TidyAll() error {
// whether to delete undecodable records (via DropItem) or just log them. for _, tidy := range []func() error{
// handle is only invoked for successfully decoded items.
func iterateTidy[T any](iter happydns.Iterator[T], dropInvalid bool, handle func(*T) error) error {
for iter.NextWithError() {
item := iter.Item()
if item == nil {
key := iter.Key()
log.Printf("KVIterator: error decoding item at key %q: %s", key, iter.Err())
if dropInvalid {
if err := iter.DropItem(); err != nil {
log.Printf("KVIterator: failed to delete invalid item at key %q: %s", key, err)
} else {
log.Printf("KVIterator: dropped invalid item at key %q", key)
}
}
continue
}
if err := handle(item); err != nil {
return err
}
}
return iter.Err()
}
func (tu *tidyUpUsecase) TidyAll(dropInvalid bool) error {
for _, tidy := range []func(bool) error{
tu.TidySessions, tu.TidySessions,
tu.TidyAuthUsers, tu.TidyAuthUsers,
tu.TidyUsers, tu.TidyUsers,
@ -81,22 +56,24 @@ func (tu *tidyUpUsecase) TidyAll(dropInvalid bool) error {
tu.TidySnapshots, tu.TidySnapshots,
tu.TidyObservationCache, tu.TidyObservationCache,
} { } {
if err := tidy(dropInvalid); err != nil { if err := tidy(); err != nil {
return err return err
} }
} }
return nil return nil
} }
func (tu *tidyUpUsecase) TidyAuthUsers(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyAuthUsers() error {
iter, err := tu.store.ListAllAuthUsers() iter, err := tu.store.ListAllAuthUsers()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(userAuth *happydns.UserAuth) error { for iter.Next() {
_, err := tu.store.GetUser(userAuth.Id) userAuth := iter.Item()
_, err = tu.store.GetUser(userAuth.Id)
if errors.Is(err, happydns.ErrUserNotFound) && time.Since(userAuth.CreatedAt) > 24*time.Hour { if errors.Is(err, happydns.ErrUserNotFound) && time.Since(userAuth.CreatedAt) > 24*time.Hour {
// Drop providers of unexistant users // Drop providers of unexistant users
log.Printf("Deleting orphan authuser (user %s not found): %v\n", userAuth.Id.String(), userAuth) log.Printf("Deleting orphan authuser (user %s not found): %v\n", userAuth.Id.String(), userAuth)
@ -104,18 +81,21 @@ func (tu *tidyUpUsecase) TidyAuthUsers(dropInvalid bool) error {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyCheckEvaluations(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyCheckEvaluations() error {
iter, err := tu.store.ListAllEvaluations() iter, err := tu.store.ListAllEvaluations()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
err = iterateTidy(iter, dropInvalid, func(eval *happydns.CheckEvaluation) error { for iter.Next() {
eval := iter.Item()
drop := false drop := false
if eval.Target.UserId != "" { if eval.Target.UserId != "" {
@ -139,34 +119,36 @@ func (tu *tidyUpUsecase) TidyCheckEvaluations(dropInvalid bool) error {
} }
if !drop && eval.PlanID != nil { if !drop && eval.PlanID != nil {
if _, err := tu.store.GetCheckPlan(*eval.PlanID); errors.Is(err, happydns.ErrCheckPlanNotFound) { if _, err = tu.store.GetCheckPlan(*eval.PlanID); errors.Is(err, happydns.ErrCheckPlanNotFound) {
log.Printf("Deleting orphan check evaluation (plan %s not found): %s\n", eval.PlanID.String(), eval.Id.String()) log.Printf("Deleting orphan check evaluation (plan %s not found): %s\n", eval.PlanID.String(), eval.Id.String())
drop = true drop = true
} }
} }
if drop { if drop {
if err := tu.store.DeleteEvaluation(eval.Id); err != nil { if err = tu.store.DeleteEvaluation(eval.Id); err != nil {
return err return err
} }
} }
return nil }
})
if err != nil { if err = iter.Err(); err != nil {
return err return err
} }
return tu.store.TidyEvaluationIndexes() return tu.store.TidyEvaluationIndexes()
} }
func (tu *tidyUpUsecase) TidyCheckPlans(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyCheckPlans() error {
iter, err := tu.store.ListAllCheckPlans() iter, err := tu.store.ListAllCheckPlans()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
err = iterateTidy(iter, dropInvalid, func(plan *happydns.CheckPlan) error { for iter.Next() {
plan := iter.Item()
if plan.Target.UserId != "" { if plan.Target.UserId != "" {
userId, err := happydns.NewIdentifierFromString(plan.Target.UserId) userId, err := happydns.NewIdentifierFromString(plan.Target.UserId)
if err == nil { if err == nil {
@ -175,7 +157,10 @@ func (tu *tidyUpUsecase) TidyCheckPlans(dropInvalid bool) error {
log.Printf("Deleting orphan check plan (user %s not found): %s\n", plan.Target.UserId, plan.Id.String()) log.Printf("Deleting orphan check plan (user %s not found): %s\n", plan.Target.UserId, plan.Id.String())
_ = tu.store.DeleteEvaluationsByChecker(plan.CheckerID, plan.Target) _ = tu.store.DeleteEvaluationsByChecker(plan.CheckerID, plan.Target)
_ = tu.store.DeleteExecutionsByChecker(plan.CheckerID, plan.Target) _ = tu.store.DeleteExecutionsByChecker(plan.CheckerID, plan.Target)
return iter.DropItem() if err = iter.DropItem(); err != nil {
return err
}
continue
} }
} }
} }
@ -188,31 +173,39 @@ func (tu *tidyUpUsecase) TidyCheckPlans(dropInvalid bool) error {
log.Printf("Deleting orphan check plan (domain %s not found): %s\n", plan.Target.DomainId, plan.Id.String()) log.Printf("Deleting orphan check plan (domain %s not found): %s\n", plan.Target.DomainId, plan.Id.String())
_ = tu.store.DeleteEvaluationsByChecker(plan.CheckerID, plan.Target) _ = tu.store.DeleteEvaluationsByChecker(plan.CheckerID, plan.Target)
_ = tu.store.DeleteExecutionsByChecker(plan.CheckerID, plan.Target) _ = tu.store.DeleteExecutionsByChecker(plan.CheckerID, plan.Target)
return iter.DropItem() if err = iter.DropItem(); err != nil {
return err
}
continue
} }
} }
} }
return nil }
})
if err != nil { if err := iter.Err(); err != nil {
return err return err
} }
return tu.store.TidyCheckPlanIndexes() return tu.store.TidyCheckPlanIndexes()
} }
func (tu *tidyUpUsecase) TidyCheckerConfigurations(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyCheckerConfigurations() error {
iter, err := tu.store.ListAllCheckerConfigurations() iter, err := tu.store.ListAllCheckerConfigurations()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(cfg *happydns.CheckerOptionsPositional) error { for iter.Next() {
cfg := iter.Item()
if cfg.UserId != nil { if cfg.UserId != nil {
if _, err := tu.store.GetUser(*cfg.UserId); errors.Is(err, happydns.ErrUserNotFound) { if _, err = tu.store.GetUser(*cfg.UserId); errors.Is(err, happydns.ErrUserNotFound) {
log.Printf("Deleting orphan checker configuration (user %s not found): %s\n", cfg.UserId.String(), cfg.CheckName) log.Printf("Deleting orphan checker configuration (user %s not found): %s\n", cfg.UserId.String(), cfg.CheckName)
return iter.DropItem() if err = iter.DropItem(); err != nil {
return err
}
continue
} else if err != nil { } else if err != nil {
return err return err
} }
@ -222,7 +215,10 @@ func (tu *tidyUpUsecase) TidyCheckerConfigurations(dropInvalid bool) error {
domain, err := tu.store.GetDomain(*cfg.DomainId) domain, err := tu.store.GetDomain(*cfg.DomainId)
if errors.Is(err, happydns.ErrDomainNotFound) { if errors.Is(err, happydns.ErrDomainNotFound) {
log.Printf("Deleting orphan checker configuration (domain %s not found): %s\n", cfg.DomainId.String(), cfg.CheckName) log.Printf("Deleting orphan checker configuration (domain %s not found): %s\n", cfg.DomainId.String(), cfg.CheckName)
return iter.DropItem() if err = iter.DropItem(); err != nil {
return err
}
continue
} else if err != nil { } else if err != nil {
return err return err
} }
@ -257,22 +253,28 @@ func (tu *tidyUpUsecase) TidyCheckerConfigurations(dropInvalid bool) error {
} }
if !found { if !found {
log.Printf("Deleting orphan checker configuration (service %s not found in domain %s): %s\n", cfg.ServiceId.String(), cfg.DomainId.String(), cfg.CheckName) log.Printf("Deleting orphan checker configuration (service %s not found in domain %s): %s\n", cfg.ServiceId.String(), cfg.DomainId.String(), cfg.CheckName)
return iter.DropItem() if err = iter.DropItem(); err != nil {
return err
}
continue
} }
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyExecutions(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyExecutions() error {
iter, err := tu.store.ListAllExecutions() iter, err := tu.store.ListAllExecutions()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
err = iterateTidy(iter, dropInvalid, func(exec *happydns.Execution) error { for iter.Next() {
exec := iter.Item()
drop := false drop := false
if exec.Target.UserId != "" { if exec.Target.UserId != "" {
@ -296,53 +298,58 @@ func (tu *tidyUpUsecase) TidyExecutions(dropInvalid bool) error {
} }
if !drop && exec.PlanID != nil { if !drop && exec.PlanID != nil {
if _, err := tu.store.GetCheckPlan(*exec.PlanID); errors.Is(err, happydns.ErrCheckPlanNotFound) { if _, err = tu.store.GetCheckPlan(*exec.PlanID); errors.Is(err, happydns.ErrCheckPlanNotFound) {
log.Printf("Deleting orphan execution (plan %s not found): %s\n", exec.PlanID.String(), exec.Id.String()) log.Printf("Deleting orphan execution (plan %s not found): %s\n", exec.PlanID.String(), exec.Id.String())
drop = true drop = true
} }
} }
if drop { if drop {
if err := tu.store.DeleteExecution(exec.Id); err != nil { if err = tu.store.DeleteExecution(exec.Id); err != nil {
return err return err
} }
} }
return nil }
})
if err != nil { if err = iter.Err(); err != nil {
return err return err
} }
return tu.store.TidyExecutionIndexes() return tu.store.TidyExecutionIndexes()
} }
func (tu *tidyUpUsecase) TidyObservationCache(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyObservationCache() error {
iter, err := tu.store.ListAllCachedObservations() iter, err := tu.store.ListAllCachedObservations()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(entry *happydns.ObservationCacheEntry) error { for iter.Next() {
if _, err := tu.store.GetSnapshot(entry.SnapshotID); errors.Is(err, happydns.ErrSnapshotNotFound) { entry := iter.Item()
if _, err = tu.store.GetSnapshot(entry.SnapshotID); errors.Is(err, happydns.ErrSnapshotNotFound) {
log.Printf("Deleting stale observation cache entry (snapshot %s not found)\n", entry.SnapshotID.String()) log.Printf("Deleting stale observation cache entry (snapshot %s not found)\n", entry.SnapshotID.String())
if err = iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyDomains(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyDomains() error {
iter, err := tu.store.ListAllDomains() iter, err := tu.store.ListAllDomains()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(domain *happydns.Domain) error { for iter.Next() {
if _, err := tu.store.GetUser(domain.Owner); errors.Is(err, happydns.ErrUserNotFound) { domain := iter.Item()
if _, err = tu.store.GetUser(domain.Owner); errors.Is(err, happydns.ErrUserNotFound) {
// Drop domain of unexistant users // Drop domain of unexistant users
log.Printf("Deleting orphan domain (user %s not found): %v\n", domain.Owner.String(), domain) log.Printf("Deleting orphan domain (user %s not found): %v\n", domain.Owner.String(), domain)
if err = iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
@ -350,45 +357,51 @@ func (tu *tidyUpUsecase) TidyDomains(dropInvalid bool) error {
} }
} }
if _, err := tu.store.GetProvider(domain.ProviderId); errors.Is(err, happydns.ErrProviderNotFound) { if _, err = tu.store.GetProvider(domain.ProviderId); errors.Is(err, happydns.ErrProviderNotFound) {
// Drop domain of unexistant provider // Drop domain of unexistant provider
log.Printf("Deleting orphan domain (provider %s not found): %v\n", domain.ProviderId.String(), domain) log.Printf("Deleting orphan domain (provider %s not found): %v\n", domain.ProviderId.String(), domain)
if err = iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyDomainLogs(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyDomainLogs() error {
iter, err := tu.store.ListAllDomainLogs() iter, err := tu.store.ListAllDomainLogs()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(l *happydns.DomainLogWithDomainId) error { for iter.Next() {
if _, err := tu.store.GetDomain(l.DomainId); errors.Is(err, happydns.ErrDomainNotFound) { l := iter.Item()
if _, err = tu.store.GetDomain(l.DomainId); errors.Is(err, happydns.ErrDomainNotFound) {
// Drop domain of unexistant provider // Drop domain of unexistant provider
log.Printf("Deleting orphan domain log (domain %s not found): %v\n", l.DomainId.String(), l) log.Printf("Deleting orphan domain log (domain %s not found): %v\n", l.DomainId.String(), l)
if err = iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyProviders(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyProviders() error {
iter, err := tu.store.ListAllProviders() iter, err := tu.store.ListAllProviders()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(prvd *happydns.ProviderMessage) error { for iter.Next() {
_, err := tu.store.GetUser(prvd.Owner) prvd := iter.Item()
_, err = tu.store.GetUser(prvd.Owner)
if errors.Is(err, happydns.ErrUserNotFound) { if errors.Is(err, happydns.ErrUserNotFound) {
// Drop providers of unexistant users // Drop providers of unexistant users
log.Printf("Deleting orphan provider (user %s not found): %v\n", prvd.Owner.String(), prvd) log.Printf("Deleting orphan provider (user %s not found): %v\n", prvd.Owner.String(), prvd)
@ -396,19 +409,22 @@ func (tu *tidyUpUsecase) TidyProviders(dropInvalid bool) error {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidySessions(dropInvalid bool) error { func (tu *tidyUpUsecase) TidySessions() error {
iter, err := tu.store.ListAllSessions() iter, err := tu.store.ListAllSessions()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(session *happydns.Session) error { for iter.Next() {
_, err := tu.store.GetUser(session.IdUser) session := iter.Item()
_, err = tu.store.GetUser(session.IdUser)
if errors.Is(err, happydns.ErrUserNotFound) { if errors.Is(err, happydns.ErrUserNotFound) {
// Drop session from unexistant users // Drop session from unexistant users
log.Printf("Deleting orphan session (user %s not found): %v\n", session.IdUser.String(), session) log.Printf("Deleting orphan session (user %s not found): %v\n", session.IdUser.String(), session)
@ -416,11 +432,12 @@ func (tu *tidyUpUsecase) TidySessions(dropInvalid bool) error {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidySnapshots(dropInvalid bool) error { func (tu *tidyUpUsecase) TidySnapshots() error {
// Collect all snapshot IDs referenced by evaluations. // Collect all snapshot IDs referenced by evaluations.
evalIter, err := tu.store.ListAllEvaluations() evalIter, err := tu.store.ListAllEvaluations()
if err != nil { if err != nil {
@ -429,12 +446,13 @@ func (tu *tidyUpUsecase) TidySnapshots(dropInvalid bool) error {
defer evalIter.Close() defer evalIter.Close()
referencedSnapshots := make(map[string]struct{}) referencedSnapshots := make(map[string]struct{})
if err = iterateTidy(evalIter, dropInvalid, func(eval *happydns.CheckEvaluation) error { for evalIter.Next() {
eval := evalIter.Item()
if !eval.SnapshotID.IsEmpty() { if !eval.SnapshotID.IsEmpty() {
referencedSnapshots[eval.SnapshotID.String()] = struct{}{} referencedSnapshots[eval.SnapshotID.String()] = struct{}{}
} }
return nil }
}); err != nil { if err = evalIter.Err(); err != nil {
return err return err
} }
@ -445,39 +463,44 @@ func (tu *tidyUpUsecase) TidySnapshots(dropInvalid bool) error {
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(snap *happydns.ObservationSnapshot) error { for iter.Next() {
snap := iter.Item()
if _, ok := referencedSnapshots[snap.Id.String()]; !ok { if _, ok := referencedSnapshots[snap.Id.String()]; !ok {
log.Printf("Deleting orphan snapshot: %s\n", snap.Id.String()) log.Printf("Deleting orphan snapshot: %s\n", snap.Id.String())
if err := iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyUsers(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyUsers() error {
iter, err := tu.store.ListAllAuthUsers() iter, err := tu.store.ListAllAuthUsers()
if err != nil { if err != nil {
return err return err
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(authUser *happydns.UserAuth) error { for iter.Next() {
authUser := iter.Item()
if authUser.EmailVerification == nil && authUser.LastLoggedIn == nil && time.Since(authUser.CreatedAt) > 7*24*time.Hour { if authUser.EmailVerification == nil && authUser.LastLoggedIn == nil && time.Since(authUser.CreatedAt) > 7*24*time.Hour {
log.Printf("Deleting user with unverified email and no login (created %s): %s\n", authUser.CreatedAt.Format(time.RFC3339), authUser.Email) log.Printf("Deleting user with unverified email and no login (created %s): %s\n", authUser.CreatedAt.Format(time.RFC3339), authUser.Email)
if err := tu.store.DeleteUser(authUser.Id); err != nil && !errors.Is(err, happydns.ErrUserNotFound) { if err = tu.store.DeleteUser(authUser.Id); err != nil && !errors.Is(err, happydns.ErrUserNotFound) {
return err return err
} }
if err := iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }
func (tu *tidyUpUsecase) TidyZones(dropInvalid bool) error { func (tu *tidyUpUsecase) TidyZones() error {
iterdn, err := tu.store.ListAllDomains() iterdn, err := tu.store.ListAllDomains()
if err != nil { if err != nil {
return err return err
@ -485,10 +508,15 @@ func (tu *tidyUpUsecase) TidyZones(dropInvalid bool) error {
defer iterdn.Close() defer iterdn.Close()
var referencedZones []happydns.Identifier var referencedZones []happydns.Identifier
if err = iterateTidy(iterdn, dropInvalid, func(domain *happydns.Domain) error {
referencedZones = append(referencedZones, domain.ZoneHistory...) for iterdn.Next() {
return nil domain := iterdn.Item()
}); err != nil { for _, zh := range domain.ZoneHistory {
referencedZones = append(referencedZones, zh)
}
}
if err = iterdn.Err(); err != nil {
return err return err
} }
@ -498,7 +526,9 @@ func (tu *tidyUpUsecase) TidyZones(dropInvalid bool) error {
} }
defer iter.Close() defer iter.Close()
return iterateTidy(iter, dropInvalid, func(zone *happydns.ZoneMessage) error { for iter.Next() {
zone := iter.Item()
foundZone := false foundZone := false
for _, zid := range referencedZones { for _, zid := range referencedZones {
if zid.Equals(zone.Id) { if zid.Equals(zone.Id) {
@ -510,10 +540,11 @@ func (tu *tidyUpUsecase) TidyZones(dropInvalid bool) error {
if !foundZone { if !foundZone {
// Drop orphan zones // Drop orphan zones
log.Printf("Deleting orphan zone: %s\n", zone.Id.String()) log.Printf("Deleting orphan zone: %s\n", zone.Id.String())
if err := iter.DropItem(); err != nil { if err = iter.DropItem(); err != nil {
return err return err
} }
} }
return nil }
})
return iter.Err()
} }

View file

@ -80,7 +80,7 @@ func TestTidyObservationCache_RemovesStaleEntries(t *testing.T) {
// Run tidy. // Run tidy.
tu := usecase.NewTidyUpUsecase(store) tu := usecase.NewTidyUpUsecase(store)
if err := tu.TidyObservationCache(true); err != nil { if err := tu.TidyObservationCache(); err != nil {
t.Fatalf("TidyObservationCache() error: %v", err) t.Fatalf("TidyObservationCache() error: %v", err)
} }
@ -102,7 +102,7 @@ func TestTidyObservationCache_EmptyCache(t *testing.T) {
} }
tu := usecase.NewTidyUpUsecase(store) tu := usecase.NewTidyUpUsecase(store)
if err := tu.TidyObservationCache(true); err != nil { if err := tu.TidyObservationCache(); err != nil {
t.Fatalf("TidyObservationCache() on empty cache error: %v", err) t.Fatalf("TidyObservationCache() on empty cache error: %v", err)
} }
} }

View file

@ -22,17 +22,13 @@
package happydns package happydns
type Backup struct { type Backup struct {
Version int Version int
Domains []*Domain Domains []*Domain
DomainsLogs map[string][]*DomainLog DomainsLogs map[string][]*DomainLog
Errors []string Errors []string
Providers []*ProviderMessage Providers []*ProviderMessage
Sessions []*Session Sessions []*Session
Users []*User Users []*User
UsersAuth UserAuths UsersAuth UserAuths
Zones []*ZoneMessage Zones []*ZoneMessage
CheckerConfigurations []*CheckerOptionsPositional
CheckPlans []*CheckPlan
CheckEvaluations []*CheckEvaluation
Executions []*Execution
} }

View file

@ -24,21 +24,18 @@ package happydns
import () import ()
type TidyUpUseCase interface { type TidyUpUseCase interface {
// TidyAll runs every tidy pass. When dropInvalid is true, iterators TidyAll() error
// that encounter undecodable records (e.g. legacy schema drift) will TidyAuthUsers() error
// delete those records; otherwise they are only logged. TidyCheckEvaluations() error
TidyAll(dropInvalid bool) error TidyCheckPlans() error
TidyAuthUsers(dropInvalid bool) error TidyCheckerConfigurations() error
TidyCheckEvaluations(dropInvalid bool) error TidyExecutions() error
TidyCheckPlans(dropInvalid bool) error TidyObservationCache() error
TidyCheckerConfigurations(dropInvalid bool) error TidySnapshots() error
TidyExecutions(dropInvalid bool) error TidyDomains() error
TidyObservationCache(dropInvalid bool) error TidyDomainLogs() error
TidySnapshots(dropInvalid bool) error TidyProviders() error
TidyDomains(dropInvalid bool) error TidySessions() error
TidyDomainLogs(dropInvalid bool) error TidyUsers() error
TidyProviders(dropInvalid bool) error TidyZones() error
TidySessions(dropInvalid bool) error
TidyUsers(dropInvalid bool) error
TidyZones(dropInvalid bool) error
} }

View file

@ -1,12 +0,0 @@
{
"name": "happydomain-monorepo",
"version": "0.0.1",
"private": true,
"workspaces": [
"web",
"web-admin"
],
"resolutions": {
"vite": "^8.0.0"
}
}

View file

@ -56,13 +56,6 @@ func GetDomainWhoisInfo(ctx context.Context, domain happydns.Origin) (*happydns.
return nil, err return nil, err
} }
// Some registries (e.g. Verisign for .com) return a "No match" response
// that the parser accepts without error but produces an empty Domain
// field. Treat this as a non-existent domain.
if result.Domain == nil || result.Domain.Domain == "" {
return nil, happydns.ErrDomainDoesNotExist
}
return mapWhoisResult(&result), nil return mapWhoisResult(&result), nil
} }

View file

@ -315,7 +315,6 @@ func inferOperation(name string) string {
{"Count", "count"}, {"Count", "count"},
{"Create", "create"}, {"Create", "create"},
{"Update", "update"}, {"Update", "update"},
{"Restore", "restore"},
{"Delete", "delete"}, {"Delete", "delete"},
{"Clear", "delete"}, {"Clear", "delete"},
{"Set", "set"}, {"Set", "set"},

1
web-admin/.npmrc Symbolic link
View file

@ -0,0 +1 @@
../web/.npmrc

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

1
web-admin/.prettierignore Symbolic link
View file

@ -0,0 +1 @@
../web/.prettierignore

1
web-admin/.prettierrc Symbolic link
View file

@ -0,0 +1 @@
../web/.prettierrc

1
web-admin/eslint.config.js Symbolic link
View file

@ -0,0 +1 @@
../web/eslint.config.js

1
web-admin/node_modules Symbolic link
View file

@ -0,0 +1 @@
../web/node_modules/

1
web-admin/package-lock.json generated Symbolic link
View file

@ -0,0 +1 @@
../web/package-lock.json

View file

@ -1,56 +0,0 @@
{
"name": "@happydomain/web-admin",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"generate:api": "openapi-ts"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/js": "^10.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.0.10",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^17.0.0",
"prettier": "^3.4.0",
"prettier-plugin-svelte": "^3.3.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.0",
"tslib": "^2.8.0",
"typescript": "^5.5.0",
"typescript-eslint": "^8.20.0",
"vite": "^8.0.0",
"vitest": "^4.0.0"
},
"resolutions": {
"vite": "^8.0.0"
},
"type": "module",
"dependencies": {
"@hey-api/openapi-ts": "^0.95.0",
"@sveltestrap/sveltestrap": "^7.0.0",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.13.0",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"html-escaper": "^3.0.0",
"sass": "^1.97.0",
"sass-loader": "^16.0.0",
"sveltekit-i18n": "^2.4.0"
}
}

1
web-admin/package.json Symbolic link
View file

@ -0,0 +1 @@
../web/package.json

View file

View file

@ -1,16 +1,48 @@
{ {
"name": "happydomain-monorepo", "name": "happyDomain",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "happydomain-monorepo", "name": "happyDomain",
"version": "0.0.1", "version": "0.0.1",
"workspaces": [ "dependencies": {
"web", "@hey-api/openapi-ts": "^0.95.0",
"web-admin" "@sveltestrap/sveltestrap": "^7.0.0",
] "bootstrap": "^5.3.0",
"bootstrap-icons": "^1.13.0",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"html-escaper": "^3.0.0",
"sass": "^1.97.0",
"sass-loader": "^16.0.0",
"sveltekit-i18n": "^2.4.0"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/js": "^10.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.0.10",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^17.0.0",
"prettier": "^3.4.0",
"prettier-plugin-svelte": "^3.3.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.0",
"tslib": "^2.8.0",
"typescript": "^5.5.0",
"typescript-eslint": "^8.20.0",
"vite": "^8.0.0",
"vitest": "^4.0.0"
}
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.9.2", "version": "1.9.2",
@ -195,14 +227,6 @@
"node": "^20.19.0 || ^22.13.0 || >=24" "node": "^20.19.0 || ^22.13.0 || >=24"
} }
}, },
"node_modules/@happydomain/web": {
"resolved": "web",
"link": true
},
"node_modules/@happydomain/web-admin": {
"resolved": "web-admin",
"link": true
},
"node_modules/@hey-api/codegen-core": { "node_modules/@hey-api/codegen-core": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.4.tgz", "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.4.tgz",
@ -308,43 +332,29 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/node": { "node_modules/@humanfs/node": {
"version": "0.16.8", "version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@humanfs/core": "^0.19.2", "@humanfs/core": "^0.19.1",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0" "@humanwhocodes/retry": "^0.4.0"
}, },
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@ -431,9 +441,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -1259,17 +1269,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
"integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/scope-manager": "8.58.1",
"@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/type-utils": "8.58.1",
"@typescript-eslint/utils": "8.58.2", "@typescript-eslint/utils": "8.58.1",
"@typescript-eslint/visitor-keys": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.1",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0" "ts-api-utils": "^2.5.0"
@ -1282,7 +1292,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.58.2", "@typescript-eslint/parser": "^8.58.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0" "typescript": ">=4.8.4 <6.1.0"
} }
@ -1298,16 +1308,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/scope-manager": "8.58.1",
"@typescript-eslint/types": "8.58.2", "@typescript-eslint/types": "8.58.1",
"@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.1",
"@typescript-eslint/visitor-keys": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.1",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@ -1323,14 +1333,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
"integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/tsconfig-utils": "^8.58.1",
"@typescript-eslint/types": "^8.58.2", "@typescript-eslint/types": "^8.58.1",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@ -1345,14 +1355,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
"integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.58.2", "@typescript-eslint/types": "8.58.1",
"@typescript-eslint/visitor-keys": "8.58.2" "@typescript-eslint/visitor-keys": "8.58.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1363,9 +1373,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
"integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1380,15 +1390,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
"integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.58.2", "@typescript-eslint/types": "8.58.1",
"@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.1",
"@typescript-eslint/utils": "8.58.2", "@typescript-eslint/utils": "8.58.1",
"debug": "^4.4.3", "debug": "^4.4.3",
"ts-api-utils": "^2.5.0" "ts-api-utils": "^2.5.0"
}, },
@ -1405,9 +1415,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
"integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1419,16 +1429,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
"integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/project-service": "8.58.1",
"@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.1",
"@typescript-eslint/types": "8.58.2", "@typescript-eslint/types": "8.58.1",
"@typescript-eslint/visitor-keys": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.1",
"debug": "^4.4.3", "debug": "^4.4.3",
"minimatch": "^10.2.2", "minimatch": "^10.2.2",
"semver": "^7.7.3", "semver": "^7.7.3",
@ -1447,16 +1457,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
"integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.9.1", "@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/scope-manager": "8.58.1",
"@typescript-eslint/types": "8.58.2", "@typescript-eslint/types": "8.58.1",
"@typescript-eslint/typescript-estree": "8.58.2" "@typescript-eslint/typescript-estree": "8.58.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1471,13 +1481,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
"integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.58.2", "@typescript-eslint/types": "8.58.1",
"eslint-visitor-keys": "^5.0.0" "eslint-visitor-keys": "^5.0.0"
}, },
"engines": { "engines": {
@ -2072,18 +2082,18 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "10.2.1", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz",
"integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.5", "@eslint/config-array": "^0.23.4",
"@eslint/config-helpers": "^0.5.5", "@eslint/config-helpers": "^0.5.4",
"@eslint/core": "^1.2.1", "@eslint/core": "^1.2.0",
"@eslint/plugin-kit": "^0.7.1", "@eslint/plugin-kit": "^0.7.0",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@ -3292,9 +3302,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.10", "version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -3451,9 +3461,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.8.3", "version": "3.8.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -3745,16 +3755,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/std-env": { "node_modules/std-env": {
"version": "4.1.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.55.4", "version": "5.55.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.3.tgz",
"integrity": "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==", "integrity": "sha512-dS1N+i3bA1v+c4UDb750MlN5vCO82G6vxh8HeTsPsTdJ1BLsN1zxSyDlIdBBqUjqZ/BxEwM8UrFf98aaoVnZFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
@ -4083,16 +4093,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.58.2", "version": "8.58.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
"integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/eslint-plugin": "8.58.1",
"@typescript-eslint/parser": "8.58.2", "@typescript-eslint/parser": "8.58.1",
"@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.1",
"@typescript-eslint/utils": "8.58.2" "@typescript-eslint/utils": "8.58.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -4410,86 +4420,6 @@
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT" "license": "MIT"
},
"web": {
"name": "@happydomain/web",
"version": "0.0.1",
"dependencies": {
"@hey-api/openapi-ts": "^0.95.0",
"@sveltestrap/sveltestrap": "^7.0.0",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.13.0",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"html-escaper": "^3.0.0",
"sass": "^1.97.0",
"sass-loader": "^16.0.0",
"sveltekit-i18n": "^2.4.0"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/js": "^10.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.0.10",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^17.0.0",
"prettier": "^3.4.0",
"prettier-plugin-svelte": "^3.3.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.0",
"tslib": "^2.8.0",
"typescript": "^5.5.0",
"typescript-eslint": "^8.20.0",
"vite": "^8.0.0",
"vitest": "^4.0.0"
}
},
"web-admin": {
"name": "@happydomain/web-admin",
"version": "0.0.1",
"dependencies": {
"@hey-api/openapi-ts": "^0.95.0",
"@sveltestrap/sveltestrap": "^7.0.0",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.13.0",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"html-escaper": "^3.0.0",
"sass": "^1.97.0",
"sass-loader": "^16.0.0",
"sveltekit-i18n": "^2.4.0"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/js": "^10.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.0.10",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^17.0.0",
"prettier": "^3.4.0",
"prettier-plugin-svelte": "^3.3.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.0",
"tslib": "^2.8.0",
"typescript": "^5.5.0",
"typescript-eslint": "^8.20.0",
"vite": "^8.0.0",
"vitest": "^4.0.0"
}
} }
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"name": "@happydomain/web", "name": "happyDomain",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {

View file

@ -132,7 +132,7 @@
</div> </div>
{:then sessions} {:then sessions}
<ListGroup> <ListGroup>
{#each [...sessions].sort((a, b) => new Date(b.upd || b.time).getTime() - new Date(a.upd || a.time).getTime()) as session (session.id)} {#each sessions as session (session.id)}
<ListGroupItem class="d-flex align-items-center justify-content-between"> <ListGroupItem class="d-flex align-items-center justify-content-between">
<div class="flex-fill" style="max-width:90%"> <div class="flex-fill" style="max-width:90%">
<div class="text-truncate"> <div class="text-truncate">