Files
Dash/openapi.yaml
T
Tomas DvorakandDevin f9fd1d9cf2 feat: domain monitoring (merge plan M2, phase 1)
Port of the domain-intelligence pipeline from the old Beszel fork,
adapted to Dash's SQLite + no-PocketBase architecture:

- internal/domain: RDAP via IANA bootstrap (cached), TCP WHOIS :43
  fallback with multi-format parser, DNS (NS/MX/TXT/CNAME/A/AAAA),
  TLS leaf cert details, ip-api.com geolocation, provider detection
  (DNS/email/hosting/CA). HTML scraping, paid APIs, and exec-based
  whois deliberately dropped.
- API: domains CRUD + synchronous refresh + domain_checks snapshot
  history; refresh runs inside the monitor sweep on interval_h.
- UI: /domains + /domains/:id pages, add/edit dialog, domain board
  widget (favicon + expiry countdown), palette + nav entries,
  domainId picker in the widget dialog.
- Test seam: Server.lookup injects the refresh; domain tests run
  hermetic against a stubbed result.

Verified live: github.com resolves MarkMonitor expiry 2028-10,
Route 53 DNS, GitHub Pages hosting, Sectigo cert with 69d left.

Authored By: TDvorak <[email protected]>

Generated with Devin (https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-21 18:35:47 +02:00

1235 lines
30 KiB
YAML

openapi: 3.1.0
info:
title: Dash API
version: 0.1.0
description: >
Single source of truth for the Dash API. The TypeScript client in
packages/api-client is generated from this file — do not hand-write
API types in the frontend.
servers:
- url: /
paths:
/api/healthz:
get:
operationId: healthz
summary: Liveness + database reachability probe
responses:
"200":
description: Service healthy
content:
application/json:
schema:
$ref: "#/components/schemas/Health"
"503":
description: Database unreachable
content:
application/json:
schema:
$ref: "#/components/schemas/Health"
/api/sections:
get:
operationId: listSections
summary: Full board — sections with nested items and urls, ordered
responses:
"200":
description: Ordered board tree
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Section"
post:
operationId: createSection
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SectionInput"
responses:
"201":
description: Created section
content:
application/json:
schema:
$ref: "#/components/schemas/Section"
/api/sections/{id}:
patch:
operationId: updateSection
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SectionPatch"
responses:
"200":
description: Updated section
content:
application/json:
schema:
$ref: "#/components/schemas/Section"
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteSection
parameters:
- $ref: "#/components/parameters/id"
responses:
"204":
description: Section and its items deleted
/api/sections/reorder:
post:
operationId: reorderSection
summary: Move a section between two neighbors (fractional position)
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Reorder"
responses:
"200":
description: Reordered
content:
application/json:
schema:
$ref: "#/components/schemas/Section"
"404":
$ref: "#/components/responses/NotFound"
/api/items:
post:
operationId: createItem
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemInput"
responses:
"201":
description: Created item
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"400":
$ref: "#/components/responses/BadRequest"
/api/items/{id}:
patch:
operationId: updateItem
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemPatch"
responses:
"200":
description: Updated item
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteItem
parameters:
- $ref: "#/components/parameters/id"
responses:
"204":
description: Item deleted
/api/items/{id}/icon:
post:
operationId: uploadIcon
summary: Upload an icon file; returns the stored icon reference
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
responses:
"200":
description: Icon stored
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"400":
$ref: "#/components/responses/BadRequest"
/api/items/reorder:
post:
operationId: reorderItem
summary: Move an item between two neighbors, optionally across sections
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemReorder"
responses:
"200":
description: Reordered
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"404":
$ref: "#/components/responses/NotFound"
/api/icons/{file}:
get:
operationId: getIcon
summary: Serve an uploaded icon file
parameters:
- name: file
in: path
required: true
schema:
type: string
responses:
"200":
description: Icon bytes
"404":
$ref: "#/components/responses/NotFound"
/api/monitors:
get:
operationId: listMonitors
summary: All monitors with live uptime stats
responses:
"200":
description: Monitors ordered by position
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/MonitorView"
post:
operationId: createMonitor
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MonitorInput"
responses:
"201":
description: Created monitor
content:
application/json:
schema:
$ref: "#/components/schemas/MonitorView"
"400":
$ref: "#/components/responses/BadRequest"
/api/monitors/{id}:
get:
operationId: getMonitor
parameters:
- $ref: "#/components/parameters/id"
responses:
"200":
description: Monitor with stats
content:
application/json:
schema:
$ref: "#/components/schemas/MonitorView"
"404":
$ref: "#/components/responses/NotFound"
patch:
operationId: updateMonitor
summary: Sparse update; `active` toggles pause/resume
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MonitorInput"
responses:
"200":
description: Updated monitor
content:
application/json:
schema:
$ref: "#/components/schemas/MonitorView"
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteMonitor
parameters:
- $ref: "#/components/parameters/id"
responses:
"204":
description: Monitor and its heartbeats deleted
"404":
$ref: "#/components/responses/NotFound"
/api/monitors/{id}/heartbeats:
get:
operationId: monitorHeartbeats
parameters:
- $ref: "#/components/parameters/id"
- name: hours
in: query
schema:
type: integer
default: 24
maximum: 720
responses:
"200":
description: Check history, oldest first
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Heartbeat"
/api/monitors/{id}/check:
post:
operationId: checkMonitorNow
summary: Run one check synchronously
parameters:
- $ref: "#/components/parameters/id"
responses:
"200":
description: Monitor after the check
content:
application/json:
schema:
$ref: "#/components/schemas/MonitorView"
"404":
$ref: "#/components/responses/NotFound"
/api/push/{token}:
get:
operationId: pushMonitorGet
summary: Record an up heartbeat for a push monitor
parameters:
- name: token
in: path
required: true
schema:
type: string
- name: msg
in: query
schema:
type: string
- name: ping
in: query
schema:
type: integer
responses:
"200":
description: Heartbeat recorded
"404":
$ref: "#/components/responses/NotFound"
post:
operationId: pushMonitorPost
summary: Record an up heartbeat for a push monitor
parameters:
- name: token
in: path
required: true
schema:
type: string
- name: msg
in: query
schema:
type: string
- name: ping
in: query
schema:
type: integer
responses:
"200":
description: Heartbeat recorded
"404":
$ref: "#/components/responses/NotFound"
/api/domains:
get:
operationId: listDomains
summary: All tracked domains with expiry countdowns
responses:
"200":
description: Domains ordered by position
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DomainView"
post:
operationId: createDomain
summary: Track a domain; runs the first WHOIS/DNS/SSL lookup inline
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DomainInput"
responses:
"201":
description: Domain after the initial lookup
content:
application/json:
schema:
$ref: "#/components/schemas/DomainView"
"400":
$ref: "#/components/responses/BadRequest"
"409":
description: Domain already tracked
/api/domains/{id}:
get:
operationId: getDomain
parameters:
- $ref: "#/components/parameters/id"
responses:
"200":
description: Domain detail
content:
application/json:
schema:
$ref: "#/components/schemas/DomainView"
"404":
$ref: "#/components/responses/NotFound"
patch:
operationId: updateDomain
summary: Sparse update — settings fields only; lookup data is refreshed separately
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DomainInput"
responses:
"200":
description: Updated domain
content:
application/json:
schema:
$ref: "#/components/schemas/DomainView"
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteDomain
parameters:
- $ref: "#/components/parameters/id"
responses:
"204":
description: Domain and its check history deleted
"404":
$ref: "#/components/responses/NotFound"
/api/domains/{id}/refresh:
post:
operationId: refreshDomain
summary: Re-run WHOIS/RDAP + DNS + TLS + geo lookup now
parameters:
- $ref: "#/components/parameters/id"
responses:
"200":
description: Domain after the refresh
content:
application/json:
schema:
$ref: "#/components/schemas/DomainView"
"404":
$ref: "#/components/responses/NotFound"
/api/domains/{id}/checks:
get:
operationId: domainChecks
summary: Snapshot history for change detection
parameters:
- $ref: "#/components/parameters/id"
- name: limit
in: query
schema:
type: integer
default: 50
maximum: 500
responses:
"200":
description: Snapshots, newest first
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DomainCheck"
/api/widgets/types:
get:
operationId: widgetTypes
summary: Widget registry metadata — drives the add-widget dialog
responses:
"200":
description: Registered widget types
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WidgetType"
/api/widgets/{id}/data:
get:
operationId: widgetData
summary: Live data for one widget item (~30s server-side cache)
parameters:
- $ref: "#/components/parameters/id"
responses:
"200":
description: Widget payload; shape depends on config.type
content:
application/json:
schema:
$ref: "#/components/schemas/WidgetData"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"502":
description: Upstream service unreachable
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/status:
get:
operationId: getStatus
summary: Reachability of each service item's primary URL (~60s server cache)
responses:
"200":
description: Status map keyed by item id
content:
application/json:
schema:
type: object
additionalProperties:
$ref: "#/components/schemas/Status"
/api/settings:
get:
operationId: getSettings
responses:
"200":
description: All settings as key/value map
content:
application/json:
schema:
type: object
additionalProperties: true
put:
operationId: putSettings
summary: Merge settings (keys not sent are preserved)
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
"200":
description: Full settings map after merge
content:
application/json:
schema:
type: object
additionalProperties: true
/api/export:
get:
operationId: exportBoard
summary: Full JSON backup (sections, items, urls, settings)
responses:
"200":
description: Board export
content:
application/json:
schema:
$ref: "#/components/schemas/Export"
/api/import:
post:
operationId: importBoard
summary: Replace board state from an export payload
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Export"
responses:
"200":
description: Imported board tree
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Section"
"400":
$ref: "#/components/responses/BadRequest"
/api/import/external:
post:
operationId: importExternal
summary: >
Import a foreign dashboard config — Homepage services.yml, Homarr
JSON export, or Dashy conf.yml. Format is auto-detected; the board
is replaced and existing settings are preserved.
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
application/json:
schema:
type: string
format: binary
text/yaml:
schema:
type: string
format: binary
responses:
"200":
description: Imported board tree
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Section"
"400":
$ref: "#/components/responses/BadRequest"
components:
parameters:
id:
name: id
in: path
required: true
schema:
type: string
responses:
NotFound:
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
BadRequest:
description: Invalid input
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
schemas:
Health:
type: object
required: [status]
properties:
status:
type: string
example: ok
Error:
type: object
required: [error]
properties:
error:
type: string
Url:
type: object
required: [id, url, label]
properties:
id:
type: string
url:
type: string
format: uri
label:
type: string
description: Tag such as "local" / "external" or a custom label
position:
type: number
UrlInput:
type: object
required: [url]
properties:
url:
type: string
format: uri
label:
type: string
default: ""
Item:
type: object
required: [id, sectionId, kind, name, position, urls]
properties:
id:
type: string
sectionId:
type: string
kind:
type: string
enum: [service, widget]
name:
type: string
icon:
type: string
description: "/api/icons/<file> path or remote URL; empty = letter tile"
position:
type: number
config:
type: object
nullable: true
additionalProperties: true
description: Widget config (Phase 2); null for services
urls:
type: array
items:
$ref: "#/components/schemas/Url"
ItemInput:
type: object
required: [sectionId, name]
properties:
sectionId:
type: string
kind:
type: string
enum: [service, widget]
default: service
name:
type: string
icon:
type: string
config:
type: object
nullable: true
additionalProperties: true
description: Widget config — keys defined by the widget type's fields
urls:
type: array
items:
$ref: "#/components/schemas/UrlInput"
ItemPatch:
type: object
properties:
name:
type: string
icon:
type: string
config:
type: object
nullable: true
additionalProperties: true
urls:
type: array
description: Full replacement of the item's URL list
items:
$ref: "#/components/schemas/UrlInput"
Section:
type: object
required: [id, name, position, collapsed, items]
properties:
id:
type: string
name:
type: string
position:
type: number
collapsed:
type: boolean
items:
type: array
items:
$ref: "#/components/schemas/Item"
SectionInput:
type: object
required: [name]
properties:
name:
type: string
SectionPatch:
type: object
properties:
name:
type: string
collapsed:
type: boolean
Reorder:
type: object
required: [id]
properties:
id:
type: string
beforeId:
type: string
description: Neighbor now before the moved entity; null if first
afterId:
type: string
description: Neighbor now after the moved entity; null if last
ItemReorder:
allOf:
- $ref: "#/components/schemas/Reorder"
- type: object
properties:
sectionId:
type: string
description: Target section (defaults to current)
Status:
type: object
required: [status]
properties:
status:
type: string
enum: [up, down]
latencyMs:
type: integer
checkedAt:
type: string
format: date-time
Monitor:
type: object
required: [id, name, type, active, status]
properties:
id:
type: string
name:
type: string
type:
type: string
enum: [http, tcp, ping, dns, keyword, json, push]
url:
type: string
hostname:
type: string
port:
type: integer
method:
type: string
headers:
type: string
description: JSON object of request headers
body:
type: string
keyword:
type: string
keywordInvert:
type: boolean
jsonQuery:
type: string
description: Dot path into a JSON response, e.g. "data.status"
expected:
type: string
dnsType:
type: string
intervalS:
type: integer
timeoutS:
type: integer
retries:
type: integer
active:
type: boolean
status:
type: string
enum: [up, down, pending, paused, maintenance]
pushToken:
type: string
tags:
type: array
items:
type: string
notes:
type: string
position:
type: number
lastCheck:
type: string
format: date-time
nullable: true
createdAt:
type: string
format: date-time
MonitorInput:
type: object
properties:
name:
type: string
type:
type: string
enum: [http, tcp, ping, dns, keyword, json, push]
url:
type: string
hostname:
type: string
port:
type: integer
method:
type: string
headers:
type: string
body:
type: string
keyword:
type: string
keywordInvert:
type: boolean
jsonQuery:
type: string
expected:
type: string
dnsType:
type: string
intervalS:
type: integer
timeoutS:
type: integer
retries:
type: integer
active:
type: boolean
tags:
type: array
items:
type: string
notes:
type: string
UptimeStats:
type: object
properties:
up:
type: integer
down:
type: integer
uptime24h:
type: number
uptime30d:
type: number
avgPing24h:
type: integer
MonitorView:
allOf:
- $ref: "#/components/schemas/Monitor"
- type: object
properties:
stats:
$ref: "#/components/schemas/UptimeStats"
Heartbeat:
type: object
properties:
id:
type: integer
status:
type: string
enum: [up, down]
pingMs:
type: integer
msg:
type: string
certExpiry:
type: integer
checkedAt:
type: string
format: date-time
Domain:
type: object
required: [id, name, active]
properties:
id:
type: string
name:
type: string
tld:
type: string
active:
type: boolean
autoRenew:
type: boolean
alertDaysBefore:
type: integer
tags:
type: array
items:
type: string
notes:
type: string
expiryDate:
type: string
format: date-time
nullable: true
creationDate:
type: string
format: date-time
nullable: true
updatedDate:
type: string
format: date-time
nullable: true
registrarName:
type: string
registrarId:
type: string
registrarUrl:
type: string
registryDomainId:
type: string
dnssec:
type: string
statuses:
type: array
items:
type: string
privacyEnabled:
type: boolean
transferLock:
type: boolean
registrantName:
type: string
registrantOrg:
type: string
registrantCountry:
type: string
abuseEmail:
type: string
nameServers:
type: array
items:
type: string
mxRecords:
type: array
items:
type: string
txtRecords:
type: array
items:
type: string
cname:
type: string
ipv4:
type: array
items:
type: string
ipv6:
type: array
items:
type: string
sslIssuer:
type: string
sslValidFrom:
type: string
format: date-time
nullable: true
sslValidTo:
type: string
format: date-time
nullable: true
sslSubject:
type: string
sslFingerprint:
type: string
sslKeySize:
type: integer
sslSigAlgo:
type: string
sslAltNames:
type: array
items:
type: string
hostCountry:
type: string
hostCountryCode:
type: string
hostRegion:
type: string
hostCity:
type: string
hostIsp:
type: string
hostOrg:
type: string
hostAs:
type: string
hostLat:
type: number
hostLon:
type: number
dnsProvider:
type: string
emailProvider:
type: string
hostingProvider:
type: string
caProvider:
type: string
headers:
type: object
additionalProperties:
type: string
faviconUrl:
type: string
lookupError:
type: string
intervalH:
type: integer
lastChecked:
type: string
format: date-time
nullable: true
position:
type: number
createdAt:
type: string
format: date-time
DomainInput:
type: object
properties:
name:
type: string
active:
type: boolean
autoRenew:
type: boolean
alertDaysBefore:
type: integer
intervalH:
type: integer
tags:
type: array
items:
type: string
notes:
type: string
DomainView:
allOf:
- $ref: "#/components/schemas/Domain"
- type: object
properties:
daysUntilExpiry:
type: integer
nullable: true
sslDaysUntilExpiry:
type: integer
nullable: true
expiring:
type: boolean
sslExpiring:
type: boolean
DomainCheck:
type: object
properties:
id:
type: integer
expiryDate:
type: string
format: date-time
nullable: true
sslValidTo:
type: string
format: date-time
nullable: true
ipv4:
type: array
items:
type: string
nameServers:
type: array
items:
type: string
checkedAt:
type: string
format: date-time
WidgetField:
type: object
required: [key, label]
properties:
key:
type: string
label:
type: string
secret:
type: boolean
description: Render as password input; never logged
placeholder:
type: string
required:
type: boolean
WidgetType:
type: object
required: [type, name, fields]
properties:
type:
type: string
name:
type: string
description:
type: string
local:
type: boolean
description: Client-rendered (e.g. clock); no /data fetch needed
fields:
type: array
items:
$ref: "#/components/schemas/WidgetField"
WidgetData:
type: object
required: [data]
properties:
data:
type: object
additionalProperties: true
fetchedAt:
type: string
format: date-time
Export:
type: object
required: [version, sections]
properties:
version:
type: integer
settings:
type: object
additionalProperties: true
sections:
type: array
items:
$ref: "#/components/schemas/Section"