Files
Dash/openapi/openapi.yaml
T
Tomas Dvorak 17a579880f refactor(frontend): restructure project layout and update API schema
Relocate frontend source code from `next-app/` to `frontend/` to align with the new project structure. This includes removing the old Next.js boilerplate files and establishing a cleaner workspace.

Additionally, updates the OpenAPI specification to include support for the `immich` widget type and its corresponding configuration schema.

- Move frontend files to `frontend/`
- Delete obsolete `next-app/` directory and its configuration
- Add `immich` widget type to `openapi.yaml`
- Update `FrontendPlan.md` with dashboard refactor and UX direction
2026-05-04 12:31:34 +02:00

581 lines
19 KiB
YAML

openapi: 3.1.0
info:
title: Dash Homelab Backend API
version: 0.1.0
license:
name: Private
identifier: LicenseRef-Private
servers:
- url: /
security: []
tags:
- name: Health
description: Process and dependency health checks.
- name: Dashboard
description: Render-ready dashboard data.
- name: Groups
description: Dashboard service groups.
- name: Services
description: Service cards and launch URLs.
- name: Layout
description: Drag/drop ordering persistence.
- name: Assets
description: Uploaded service icon files.
- name: Widgets
description: Dashboard widget instances and data.
paths:
/health:
get:
tags: [Health]
operationId: health
summary: Check backend health
responses:
"200":
description: Backend and database are healthy.
content:
application/json:
schema:
type: object
required: [ok]
properties:
ok: { type: boolean }
"503": { $ref: "#/components/responses/InternalError" }
"400": { $ref: "#/components/responses/ValidationError" }
/api/v1/dashboard:
get:
tags: [Dashboard]
operationId: getDashboard
summary: Get dashboard
responses:
"200":
description: Full dashboard in render order.
content:
application/json:
schema: { $ref: "#/components/schemas/Dashboard" }
"500": { $ref: "#/components/responses/InternalError" }
"400": { $ref: "#/components/responses/ValidationError" }
/api/v1/groups:
get:
tags: [Groups]
operationId: listGroups
summary: List groups
responses:
"200":
description: Groups in sort order.
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/Group" }
"400": { $ref: "#/components/responses/ValidationError" }
post:
tags: [Groups]
operationId: createGroup
summary: Create group
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CreateGroupRequest" }
responses:
"201":
description: Created group.
content:
application/json:
schema: { $ref: "#/components/schemas/Group" }
"400": { $ref: "#/components/responses/ValidationError" }
/api/v1/groups/{groupId}:
parameters:
- $ref: "#/components/parameters/GroupId"
get:
tags: [Groups]
operationId: getGroup
summary: Get group
responses:
"200":
description: Group.
content:
application/json:
schema: { $ref: "#/components/schemas/Group" }
"404": { $ref: "#/components/responses/NotFound" }
patch:
tags: [Groups]
operationId: patchGroup
summary: Patch group
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/PatchGroupRequest" }
responses:
"200":
description: Updated group.
content:
application/json:
schema: { $ref: "#/components/schemas/Group" }
"400": { $ref: "#/components/responses/ValidationError" }
"404": { $ref: "#/components/responses/NotFound" }
delete:
tags: [Groups]
operationId: deleteGroup
summary: Delete group
parameters:
- name: moveServicesToUngrouped
in: query
schema: { type: boolean, default: false }
responses:
"204": { description: Deleted. }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
/api/v1/services:
get:
tags: [Services]
operationId: listServices
summary: List services
responses:
"200":
description: Services with URLs.
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/Service" }
"400": { $ref: "#/components/responses/ValidationError" }
post:
tags: [Services]
operationId: createService
summary: Create service
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ServiceRequest" }
responses:
"201":
description: Created service.
content:
application/json:
schema: { $ref: "#/components/schemas/Service" }
"400": { $ref: "#/components/responses/ValidationError" }
/api/v1/services/{serviceId}:
parameters:
- $ref: "#/components/parameters/ServiceId"
get:
tags: [Services]
operationId: getService
summary: Get service
responses:
"200":
description: Service.
content:
application/json:
schema: { $ref: "#/components/schemas/Service" }
"404": { $ref: "#/components/responses/NotFound" }
patch:
tags: [Services]
operationId: patchService
summary: Patch service
description: Replaces service fields and URL list atomically.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ServiceRequest" }
responses:
"200":
description: Updated service.
content:
application/json:
schema: { $ref: "#/components/schemas/Service" }
"400": { $ref: "#/components/responses/ValidationError" }
"404": { $ref: "#/components/responses/NotFound" }
delete:
tags: [Services]
operationId: deleteService
summary: Delete service
responses:
"204": { description: Deleted. }
"404": { $ref: "#/components/responses/NotFound" }
/api/v1/layout:
put:
tags: [Layout]
operationId: putLayout
summary: Update layout
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/LayoutRequest" }
responses:
"200":
description: Updated dashboard.
content:
application/json:
schema: { $ref: "#/components/schemas/Dashboard" }
"400": { $ref: "#/components/responses/ValidationError" }
"404": { $ref: "#/components/responses/NotFound" }
/api/v1/assets/icons:
post:
tags: [Assets]
operationId: uploadIcon
summary: Upload icon
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
responses:
"201":
description: Uploaded icon asset.
content:
application/json:
schema: { $ref: "#/components/schemas/AssetFile" }
"400": { $ref: "#/components/responses/ValidationError" }
"413": { $ref: "#/components/responses/UploadTooLarge" }
"415": { $ref: "#/components/responses/UnsupportedMediaType" }
/api/v1/widgets:
get:
tags: [Widgets]
operationId: listWidgets
summary: List widgets
responses:
"200":
description: Widget instances.
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/WidgetInstance" }
"400": { $ref: "#/components/responses/ValidationError" }
post:
tags: [Widgets]
operationId: createWidget
summary: Create widget
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetRequest" }
responses:
"201":
description: Created widget.
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetInstance" }
"400": { $ref: "#/components/responses/ValidationError" }
/api/v1/widgets/{widgetId}:
parameters:
- $ref: "#/components/parameters/WidgetId"
get:
tags: [Widgets]
operationId: getWidget
summary: Get widget
responses:
"200":
description: Widget.
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetInstance" }
"404": { $ref: "#/components/responses/NotFound" }
patch:
tags: [Widgets]
operationId: patchWidget
summary: Patch widget
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetRequest" }
responses:
"200":
description: Updated widget.
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetInstance" }
"400": { $ref: "#/components/responses/ValidationError" }
"404": { $ref: "#/components/responses/NotFound" }
delete:
tags: [Widgets]
operationId: deleteWidget
summary: Delete widget
responses:
"204": { description: Deleted. }
"404": { $ref: "#/components/responses/NotFound" }
/api/v1/widgets/{widgetId}/data:
parameters:
- $ref: "#/components/parameters/WidgetId"
get:
tags: [Widgets]
operationId: getWidgetData
summary: Get widget data
responses:
"200":
description: Cached or refreshed widget data.
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetData" }
"404": { $ref: "#/components/responses/NotFound" }
/api/v1/widgets/{widgetId}/refresh:
parameters:
- $ref: "#/components/parameters/WidgetId"
post:
tags: [Widgets]
operationId: refreshWidget
summary: Refresh widget
responses:
"200":
description: Refreshed widget data.
content:
application/json:
schema: { $ref: "#/components/schemas/WidgetData" }
"404": { $ref: "#/components/responses/NotFound" }
components:
parameters:
GroupId:
name: groupId
in: path
required: true
schema: { type: string, format: uuid }
ServiceId:
name: serviceId
in: path
required: true
schema: { type: string, format: uuid }
WidgetId:
name: widgetId
in: path
required: true
schema: { type: string, format: uuid }
responses:
ValidationError:
description: Validation error.
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
NotFound:
description: Resource not found.
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
Conflict:
description: Operation conflicts with current state.
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
UploadTooLarge:
description: Uploaded file is too large.
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
UnsupportedMediaType:
description: Uploaded file type is unsupported.
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
InternalError:
description: Internal server error.
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
schemas:
Dashboard:
type: object
required: [groups, ungroupedServices, widgets]
properties:
groups:
type: array
items: { $ref: "#/components/schemas/Group" }
ungroupedServices:
type: array
items: { $ref: "#/components/schemas/Service" }
widgets:
type: array
items: { $ref: "#/components/schemas/WidgetInstance" }
Group:
type: object
required: [id, name, sortOrder, collapsed, services, createdAt, updatedAt]
properties:
id: { type: string, format: uuid }
name: { type: string, minLength: 1, maxLength: 80 }
sortOrder: { type: integer }
collapsed: { type: boolean }
services:
type: array
items: { $ref: "#/components/schemas/Service" }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
Service:
type: object
required: [id, groupId, name, iconUrl, iconAssetId, sortOrder, urls, createdAt, updatedAt]
properties:
id: { type: string, format: uuid }
groupId: { type: [string, "null"], format: uuid }
name: { type: string, minLength: 1, maxLength: 80 }
iconUrl: { type: [string, "null"], format: uri }
iconAssetId: { type: [string, "null"], format: uuid }
sortOrder: { type: integer }
urls:
type: array
items: { $ref: "#/components/schemas/ServiceUrl" }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
ServiceUrl:
type: object
required: [id, label, kind, url, sortOrder, isPrimary]
properties:
id: { type: string, format: uuid }
label: { type: string, minLength: 1, maxLength: 40 }
kind: { type: string, enum: [local, external, custom] }
url: { type: string, format: uri }
sortOrder: { type: integer }
isPrimary: { type: boolean }
WidgetInstance:
type: object
required: [id, type, title, enabled, sortOrder, config, createdAt, updatedAt]
properties:
id: { type: string, format: uuid }
type: { type: string, enum: [clock, image, pihole, memos, immich] }
title: { type: string, minLength: 1, maxLength: 80 }
enabled: { type: boolean }
sortOrder: { type: integer }
config: { type: object, additionalProperties: true, description: "Pi-hole apiToken is masked in responses." }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
WidgetData:
type: object
required: [widgetId, status]
properties:
widgetId: { type: string, format: uuid }
status: { type: string, enum: [fresh, stale, error] }
data: { type: object, additionalProperties: true }
error: { type: [string, "null"] }
fetchedAt: { type: [string, "null"], format: date-time }
expiresAt: { type: [string, "null"], format: date-time }
AssetFile:
type: object
required: [id, originalName, storedName, mimeType, sizeBytes, publicPath, createdAt]
properties:
id: { type: string, format: uuid }
originalName: { type: string }
storedName: { type: string }
mimeType: { type: string }
sizeBytes: { type: integer }
publicPath: { type: string }
createdAt: { type: string, format: date-time }
CreateGroupRequest:
type: object
required: [name]
properties:
name: { type: string, minLength: 1, maxLength: 80 }
PatchGroupRequest:
type: object
properties:
name: { type: string, minLength: 1, maxLength: 80 }
collapsed: { type: boolean }
ServiceRequest:
type: object
required: [name, urls]
properties:
groupId: { type: [string, "null"], format: uuid }
name: { type: string, minLength: 1, maxLength: 80 }
iconUrl: { type: [string, "null"], format: uri }
iconAssetId: { type: [string, "null"], format: uuid }
urls:
type: array
minItems: 1
items: { $ref: "#/components/schemas/ServiceUrlInput" }
ServiceUrlInput:
type: object
required: [label, kind, url]
properties:
id: { type: string, format: uuid }
label: { type: string, minLength: 1, maxLength: 40 }
kind: { type: string, enum: [local, external, custom] }
url: { type: string, format: uri }
isPrimary: { type: boolean, default: false }
LayoutRequest:
type: object
required: [groupIds, widgetIds, ungroupedServiceIds, groupServices]
properties:
groupIds:
type: array
items: { type: string, format: uuid }
widgetIds:
type: array
items: { type: string, format: uuid }
ungroupedServiceIds:
type: array
items: { type: string, format: uuid }
groupServices:
type: object
additionalProperties:
type: array
items: { type: string, format: uuid }
WidgetRequest:
type: object
required: [type, title, config]
properties:
type: { type: string, enum: [clock, image, pihole, memos, immich] }
title: { type: string, minLength: 1, maxLength: 80 }
enabled: { type: boolean, default: true }
config:
oneOf:
- $ref: "#/components/schemas/ClockWidgetConfig"
- $ref: "#/components/schemas/ImageWidgetConfig"
- $ref: "#/components/schemas/PiHoleWidgetConfig"
- $ref: "#/components/schemas/MemosWidgetConfig"
- $ref: "#/components/schemas/ImmichWidgetConfig"
ClockWidgetConfig:
type: object
properties:
timezones:
type: array
items: { type: string }
ImageWidgetConfig:
type: object
required: [imageUrl]
properties:
imageUrl: { type: string, format: uri }
linkUrl: { type: [string, "null"], format: uri }
PiHoleWidgetConfig:
type: object
required: [baseUrl, apiToken]
properties:
baseUrl: { type: string, format: uri }
apiToken: { type: string, writeOnly: true }
MemosWidgetConfig:
type: object
required: [baseUrl, apiToken]
properties:
baseUrl: { type: string, format: uri }
apiToken: { type: string, writeOnly: true }
pageSize: { type: integer, default: 5 }
ImmichWidgetConfig:
type: object
required: [baseUrl, apiKey]
properties:
baseUrl: { type: string, format: uri }
apiKey: { type: string, writeOnly: true }
ErrorResponse:
type: object
required: [code, message, details]
properties:
code:
type: string
enum:
- validation_error
- not_found
- conflict
- upload_too_large
- unsupported_media_type
- widget_fetch_failed
- internal_error
message: { type: string }
details: { type: ["object", "null"], additionalProperties: true }