mirror of
https://github.com/usetrmnl/inker.git
synced 2026-04-29 13:45:07 -07:00
379 lines
14 KiB
Plaintext
379 lines
14 KiB
Plaintext
// Inker Database Schema
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model Device {
|
|
id Int @id @default(autoincrement())
|
|
name String @map("label")
|
|
friendlyId String? @map("friendly_id")
|
|
macAddress String @unique @map("mac_address")
|
|
apiKey String @unique @map("api_key")
|
|
firmwareVersion String? @map("firmware_version")
|
|
modelId Int? @map("model_id")
|
|
playlistId Int? @map("playlist_id")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
wifi Int @default(0)
|
|
battery Float @default(0)
|
|
refreshRate Int @default(900) @map("refresh_rate")
|
|
imageTimeout Int @default(0) @map("image_timeout")
|
|
width Int @default(0)
|
|
height Int @default(0)
|
|
proxy Boolean @default(false)
|
|
firmwareUpdate Boolean @default(true) @map("firmware_update")
|
|
sleepStartAt String? @map("sleep_start_at")
|
|
sleepStopAt String? @map("sleep_stop_at")
|
|
lastSeenAt DateTime? @map("last_seen_at")
|
|
refreshPending Boolean @default(false) @map("refresh_pending")
|
|
lastScreenId String? @map("last_screen_id") // Track last displayed screen for ghosting prevention
|
|
screenStartedAt DateTime? @map("screen_started_at") // When the current screen started displaying
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
model Model? @relation(fields: [modelId], references: [id])
|
|
playlist Playlist? @relation(fields: [playlistId], references: [id])
|
|
logs DeviceLog[]
|
|
screenAssignments DeviceScreenAssignment[]
|
|
|
|
@@map("devices")
|
|
}
|
|
|
|
model Model {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
label String
|
|
width Int
|
|
height Int
|
|
description String?
|
|
mimeType String @default("image/png") @map("mime_type")
|
|
colors Int @default(2)
|
|
bitDepth Int @default(1) @map("bit_depth")
|
|
rotation Int @default(0)
|
|
offsetX Int @default(0) @map("offset_x")
|
|
offsetY Int @default(0) @map("offset_y")
|
|
kind String @default("terminus")
|
|
scaleFactor Float @default(1.0) @map("scale_factor")
|
|
publishedAt DateTime? @map("published_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
devices Device[]
|
|
screens Screen[]
|
|
|
|
@@map("models")
|
|
}
|
|
|
|
model Screen {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
imageUrl String @map("image_url")
|
|
thumbnailUrl String? @map("thumbnail_url")
|
|
modelId Int? @map("model_id")
|
|
isPublic Boolean @default(false) @map("is_public")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
model Model? @relation(fields: [modelId], references: [id])
|
|
playlistItems PlaylistItem[]
|
|
|
|
@@map("screens")
|
|
}
|
|
|
|
model Playlist {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
isActive Boolean @default(false) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
items PlaylistItem[]
|
|
devices Device[]
|
|
|
|
@@map("playlists")
|
|
}
|
|
|
|
model PlaylistItem {
|
|
id Int @id @default(autoincrement())
|
|
playlistId Int @map("playlist_id")
|
|
screenId Int? @map("screen_id")
|
|
screenDesignId Int? @map("screen_design_id")
|
|
order Int @default(0)
|
|
duration Int @default(60) // seconds
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
pluginInstanceId Int? @map("plugin_instance_id")
|
|
|
|
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
|
|
screen Screen? @relation(fields: [screenId], references: [id], onDelete: Cascade)
|
|
screenDesign ScreenDesign? @relation(fields: [screenDesignId], references: [id], onDelete: Cascade)
|
|
pluginInstance PluginInstance? @relation(fields: [pluginInstanceId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("playlist_items")
|
|
}
|
|
|
|
model Extension {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
type String // webhook, polling, custom
|
|
config Json // JSON configuration
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("extensions")
|
|
}
|
|
|
|
// ========================
|
|
// Data Source & Custom Widget Models
|
|
// ========================
|
|
|
|
// Data Source - external API or RSS feed configuration
|
|
model DataSource {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
type String // "json" | "rss"
|
|
url String // API endpoint or RSS feed URL
|
|
method String @default("GET") // HTTP method for JSON APIs
|
|
headers Json? // Custom headers (e.g., API keys)
|
|
refreshInterval Int @default(300) @map("refresh_interval") // Seconds between fetches
|
|
jsonPath String? @map("json_path") // JSONPath expression to extract data
|
|
isActive Boolean @default(true) @map("is_active")
|
|
lastFetchedAt DateTime? @map("last_fetched_at")
|
|
lastData Json? @map("last_data") // Cached response data
|
|
lastError String? @map("last_error") // Last fetch error message
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
customWidgets CustomWidget[]
|
|
|
|
@@map("data_sources")
|
|
}
|
|
|
|
// Custom Widget - user-defined widget using data from a DataSource
|
|
model CustomWidget {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
dataSourceId Int @map("data_source_id")
|
|
displayType String // "template" | "value" | "list" | "title-value"
|
|
template String? // Template string for "template" display type
|
|
config Json @default("{}") // Layout config (fontSize, alignment, etc.)
|
|
minWidth Int @default(100) @map("min_width")
|
|
minHeight Int @default(50) @map("min_height")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
dataSource DataSource @relation(fields: [dataSourceId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("custom_widgets")
|
|
}
|
|
|
|
model Firmware {
|
|
id Int @id @default(autoincrement())
|
|
version String @unique
|
|
downloadUrl String @map("download_url")
|
|
releaseNotes String? @map("release_notes")
|
|
isStable Boolean @default(false) @map("is_stable")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@map("firmware")
|
|
}
|
|
|
|
model DeviceLog {
|
|
id Int @id @default(autoincrement())
|
|
deviceId Int @map("device_id")
|
|
level String // info, warning, error
|
|
message String
|
|
metadata Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("device_logs")
|
|
}
|
|
|
|
// ========================
|
|
// Screen Designer Models
|
|
// ========================
|
|
|
|
// Widget Template - defines available widget types
|
|
model WidgetTemplate {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique // e.g., "clock", "weather", "text"
|
|
label String // Display name: "Live Clock", "Weather"
|
|
description String?
|
|
category String // "time", "weather", "content", "system"
|
|
defaultConfig Json // Default configuration for this widget type
|
|
minWidth Int @default(100) @map("min_width")
|
|
minHeight Int @default(50) @map("min_height")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
widgets ScreenWidget[]
|
|
|
|
@@map("widget_templates")
|
|
}
|
|
|
|
// Screen Design - a designed screen with widgets
|
|
model ScreenDesign {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String?
|
|
width Int @default(800)
|
|
height Int @default(480)
|
|
background String @default("#FFFFFF") // white for e-ink
|
|
isTemplate Boolean @default(false) @map("is_template")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
widgets ScreenWidget[]
|
|
assignments DeviceScreenAssignment[]
|
|
playlistItems PlaylistItem[]
|
|
|
|
@@map("screen_designs")
|
|
}
|
|
|
|
// Widget instance on a screen
|
|
model ScreenWidget {
|
|
id Int @id @default(autoincrement())
|
|
screenDesignId Int @map("screen_design_id")
|
|
templateId Int @map("template_id")
|
|
x Int @default(0)
|
|
y Int @default(0)
|
|
width Int @default(200)
|
|
height Int @default(100)
|
|
rotation Int @default(0) // Rotation in degrees (0, 90, 180, 270, or any angle)
|
|
config Json // Widget-specific config (timezone, location, etc.)
|
|
zIndex Int @default(0) @map("z_index")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
screenDesign ScreenDesign @relation(fields: [screenDesignId], references: [id], onDelete: Cascade)
|
|
template WidgetTemplate @relation(fields: [templateId], references: [id])
|
|
|
|
@@map("screen_widgets")
|
|
}
|
|
|
|
// Device to Screen Design assignment
|
|
model DeviceScreenAssignment {
|
|
id Int @id @default(autoincrement())
|
|
deviceId Int @map("device_id")
|
|
screenDesignId Int @map("screen_design_id")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
|
screenDesign ScreenDesign @relation(fields: [screenDesignId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([deviceId, screenDesignId])
|
|
@@map("device_screen_assignments")
|
|
}
|
|
|
|
// Application Settings - key/value store for configuration
|
|
model Setting {
|
|
id Int @id @default(autoincrement())
|
|
key String @unique
|
|
value String // Stored as encrypted string for sensitive values
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("settings")
|
|
}
|
|
|
|
// Blocked devices - prevents auto-provisioning of deleted devices
|
|
model BlockedDevice {
|
|
id Int @id @default(autoincrement())
|
|
macAddress String @unique @map("mac_address")
|
|
reason String? // Why device was blocked (e.g., "deleted by admin")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@map("blocked_devices")
|
|
}
|
|
|
|
// ========================
|
|
// Plugin System Models
|
|
// ========================
|
|
|
|
// Plugin - a reusable plugin definition (built-in from TRMNL or user-created)
|
|
model Plugin {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
slug String @unique
|
|
description String?
|
|
icon String? // emoji or image URL
|
|
category String @default("custom") // news, productivity, weather, social, finance, system, custom
|
|
|
|
// Data fetching configuration
|
|
dataStrategy String @default("polling") @map("data_strategy") // polling | webhook | static
|
|
dataUrl String? @map("data_url") // supports {{setting}} interpolation
|
|
dataMethod String @default("GET") @map("data_method")
|
|
dataHeaders Json? @map("data_headers") // supports {{setting}} interpolation
|
|
dataPath String? @map("data_path") // JSONPath to extract from response
|
|
dataTransform String? @map("data_transform") // JS script to transform fetched data into locals
|
|
refreshInterval Int @default(300) @map("refresh_interval") // seconds
|
|
|
|
// Liquid templates for 4 layout sizes (matching TRMNL)
|
|
markupFull String? @map("markup_full")
|
|
markupHalfHorizontal String? @map("markup_half_horizontal")
|
|
markupHalfVertical String? @map("markup_half_vertical")
|
|
markupQuadrant String? @map("markup_quadrant")
|
|
|
|
// Settings schema: [{key, label, type, options?, required?, encrypted?, default?}]
|
|
settingsSchema Json? @map("settings_schema")
|
|
|
|
// OAuth (for plugins that require OAuth2 authentication)
|
|
oauthProvider String? @map("oauth_provider") // "google" | "spotify" | "strava" etc.
|
|
oauthScopes String? @map("oauth_scopes") // space-separated scopes
|
|
|
|
// Metadata
|
|
isInstalled Boolean @default(false) @map("is_installed")
|
|
isBuiltin Boolean @default(false) @map("is_builtin")
|
|
source String @default("inker") // "inker" | "trmnl"
|
|
sourceUrl String? @map("source_url") // GitHub URL of original plugin
|
|
sourceHash String? @map("source_hash") // hash for detecting updates
|
|
version String?
|
|
|
|
instances PluginInstance[]
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("plugins")
|
|
}
|
|
|
|
// Plugin Instance - a configured instance of a plugin (with user settings and cached data)
|
|
model PluginInstance {
|
|
id Int @id @default(autoincrement())
|
|
pluginId Int @map("plugin_id")
|
|
name String? // optional custom name for this instance
|
|
|
|
settings Json @default("{}") // user settings (non-sensitive)
|
|
settingsEncrypted Json @default("{}") @map("settings_encrypted") // encrypted settings (API keys)
|
|
|
|
// OAuth tokens (encrypted)
|
|
oauthToken String? @map("oauth_token") // encrypted access token
|
|
oauthRefreshToken String? @map("oauth_refresh_token") // encrypted refresh token
|
|
oauthExpiresAt DateTime? @map("oauth_expires_at")
|
|
|
|
// Cached data (the locals hash)
|
|
lastData Json? @map("last_data")
|
|
lastFetchedAt DateTime? @map("last_fetched_at")
|
|
lastError String? @map("last_error")
|
|
|
|
plugin Plugin @relation(fields: [pluginId], references: [id], onDelete: Cascade)
|
|
playlistItems PlaylistItem[]
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("plugin_instances")
|
|
}
|