Compare commits
50 Commits
f884982766
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bdd1cae5ae | |||
|
|
3af86d80c7 | ||
|
8093f563f9
|
|||
|
0db96869e1
|
|||
|
96d59c763d
|
|||
|
b70d768eee
|
|||
|
7fcbf1398a
|
|||
|
17d6ee234d
|
|||
|
ac4eaa83e0
|
|||
|
475e88bffb
|
|||
|
9edda5808f
|
|||
|
30cd1c2815
|
|||
|
ee95be1bb3
|
|||
|
9c46cdf9cf
|
|||
|
4e47111a4d
|
|||
|
9624eab798
|
|||
|
1a2bea01d1
|
|||
|
5db9d6b139
|
|||
|
045cfb46ee
|
|||
|
e168f3ad4a
|
|||
|
411c5648df
|
|||
|
60b4cd79a7
|
|||
|
ba176d2ec1
|
|||
|
fb1fa642af
|
|||
|
4478cdbcb3
|
|||
|
866fd0eacc
|
|||
|
664cae8d68
|
|||
|
003365c0ca
|
|||
|
d7eddd48a7
|
|||
|
b55d99dd9e
|
|||
|
2e63609129
|
|||
|
6e9c5291ba
|
|||
|
6c3ef85cb8
|
|||
|
88b8f7cdee
|
|||
|
2a37483d88
|
|||
|
a67213e669
|
|||
|
90a1f9faf8
|
|||
|
1abfc44908
|
|||
|
f7cc0c38a2
|
|||
|
906e9dc86f
|
|||
|
442bd084ca
|
|||
|
5640ffa990
|
|||
|
6c3701ad32
|
|||
|
09a4aa40f4
|
|||
|
220d25ccab
|
|||
|
189a6c4401
|
|||
|
3be59981ed
|
|||
|
c90d9655d8
|
|||
|
7b28683e73
|
|||
|
d5a53e3532
|
@@ -4,6 +4,11 @@
|
||||
"context": ".",
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/tailscale/codespace/tailscale": {
|
||||
"version": "latest"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
||||
45
.dockerignore
Normal file
45
.dockerignore
Normal file
@@ -0,0 +1,45 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.turbo
|
||||
|
||||
# Development files
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.devcontainer
|
||||
.vscode
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.p8
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
|
||||
# Misc
|
||||
.editorconfig
|
||||
core.*
|
||||
60
.github/workflows/docker-publish.yml
vendored
Normal file
60
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: cr.nym.sh
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
VITE_API_HOST=${{ vars.VITE_API_HOST }}
|
||||
VITE_DEFAULT_LATITUDE=${{ vars.VITE_DEFAULT_LATITUDE }}
|
||||
VITE_DEFAULT_LONGITUDE=${{ vars.VITE_DEFAULT_LONGITUDE }}
|
||||
|
||||
- name: Output image URLs
|
||||
run: |
|
||||
echo "Published images:"
|
||||
echo "${{ steps.meta.outputs.tags }}" | tr ' ' '\n'
|
||||
66
Dockerfile
Normal file
66
Dockerfile
Normal file
@@ -0,0 +1,66 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Base stage with Bun
|
||||
FROM oven/bun:1.3.1-alpine AS base
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies stage
|
||||
FROM base AS deps
|
||||
COPY package.json bun.lock ./
|
||||
COPY apps/backend/package.json ./apps/backend/
|
||||
COPY apps/dashboard/package.json ./apps/dashboard/
|
||||
COPY packages/jrpc/package.json ./packages/jrpc/
|
||||
COPY packages/zigbee/package.json ./packages/zigbee/
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Build dashboard stage
|
||||
FROM base AS dashboard-builder
|
||||
|
||||
# Accept build arguments for Vite environment variables
|
||||
ARG VITE_API_HOST
|
||||
ARG VITE_DEFAULT_LATITUDE
|
||||
ARG VITE_DEFAULT_LONGITUDE
|
||||
|
||||
# Set as environment variables for Vite build
|
||||
ENV VITE_API_HOST=${VITE_API_HOST}
|
||||
ENV VITE_DEFAULT_LATITUDE=${VITE_DEFAULT_LATITUDE}
|
||||
ENV VITE_DEFAULT_LONGITUDE=${VITE_DEFAULT_LONGITUDE}
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/apps/dashboard/node_modules ./apps/dashboard/node_modules
|
||||
COPY --from=deps /app/packages/jrpc/node_modules ./packages/jrpc/node_modules
|
||||
COPY --from=deps /app/packages/zigbee/node_modules ./packages/zigbee/node_modules
|
||||
COPY apps/dashboard ./apps/dashboard
|
||||
COPY packages/jrpc ./packages/jrpc
|
||||
COPY packages/zigbee ./packages/zigbee
|
||||
COPY package.json bun.lock ./
|
||||
COPY biome.json ./
|
||||
WORKDIR /app/apps/dashboard
|
||||
RUN bun run build
|
||||
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy built dashboard
|
||||
COPY --from=dashboard-builder /app/apps/dashboard/dist /app/apps/dashboard/dist
|
||||
|
||||
# Copy backend source (TypeScript runs directly with Bun)
|
||||
COPY apps/backend/src /app/apps/backend/src
|
||||
COPY apps/backend/package.json /app/apps/backend/
|
||||
|
||||
# Copy workspace packages
|
||||
COPY packages/jrpc /app/packages/jrpc
|
||||
COPY --from=deps /app/packages/jrpc/node_modules /app/packages/jrpc/node_modules
|
||||
COPY packages/zigbee /app/packages/zigbee
|
||||
COPY --from=deps /app/packages/zigbee/node_modules /app/packages/zigbee/node_modules
|
||||
|
||||
# Copy backend dependencies
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
COPY --from=deps /app/apps/backend/node_modules /app/apps/backend/node_modules
|
||||
|
||||
WORKDIR /app/apps/backend
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"name": "@eva/backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -9,7 +9,10 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.6.14"
|
||||
"hono": "^4.6.14",
|
||||
"mqtt": "^5.14.1",
|
||||
"@eva/jrpc": "workspace:*",
|
||||
"@eva/zigbee": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
||||
90
apps/backend/src/beszel.ts
Normal file
90
apps/backend/src/beszel.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Hono } from "hono"
|
||||
import { type BeszelContext, beszelAuth } from "./beszel/middleware"
|
||||
|
||||
const beszel = new Hono<BeszelContext>()
|
||||
|
||||
// Apply middleware to all beszel routes
|
||||
beszel.use("*", beszelAuth())
|
||||
|
||||
interface BeszelSystemInfo {
|
||||
name: string
|
||||
status: "up" | "down"
|
||||
info: {
|
||||
cpu: number
|
||||
ram: number
|
||||
disk: number
|
||||
}
|
||||
}
|
||||
|
||||
interface BeszelApiSystem {
|
||||
name: string
|
||||
status: "up" | "down"
|
||||
info: {
|
||||
cpu: number
|
||||
mp: number // memory percentage
|
||||
dp: number // disk percentage
|
||||
}
|
||||
}
|
||||
|
||||
beszel.get("/systems", async (c) => {
|
||||
try {
|
||||
const beszelHost = process.env.BESZEL_HOST
|
||||
const token = c.get("beszelToken")
|
||||
|
||||
if (!beszelHost) {
|
||||
console.error("[Beszel API] BESZEL_HOST environment variable not set")
|
||||
return c.json({ error: "BESZEL_HOST environment variable not set" }, 500)
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${beszelHost}/api/collections/systems/records`, {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error(
|
||||
`[Beszel API] Failed to fetch systems: ${response.status} ${response.statusText}`,
|
||||
errorText ? `- ${errorText}` : "",
|
||||
)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Failed to fetch Beszel data",
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}),
|
||||
{
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { items: BeszelApiSystem[] }
|
||||
|
||||
const systems: BeszelSystemInfo[] = data.items.map((system) => ({
|
||||
name: system.name,
|
||||
status: system.status,
|
||||
info: {
|
||||
cpu: system.info.cpu,
|
||||
ram: system.info.mp,
|
||||
disk: system.info.dp,
|
||||
},
|
||||
}))
|
||||
|
||||
console.log(`[Beszel API] Successfully fetched ${systems.length} systems`)
|
||||
|
||||
return c.json({
|
||||
lastUpdated: new Date().toISOString(),
|
||||
systems,
|
||||
totalSystems: systems.length,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[Beszel API] Internal server error:", error)
|
||||
return c.json({ error: "Internal server error", message: String(error) }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default beszel
|
||||
81
apps/backend/src/beszel/middleware.ts
Normal file
81
apps/backend/src/beszel/middleware.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { MiddlewareHandler } from "hono"
|
||||
|
||||
interface BeszelAuthResponse {
|
||||
token: string
|
||||
}
|
||||
|
||||
export function beszelAuth(): MiddlewareHandler {
|
||||
let cachedToken: string | null = null
|
||||
let tokenExpiry: number | null = null
|
||||
|
||||
// Token lifetime: 50 minutes (tokens typically expire after 1 hour, refresh before that)
|
||||
const TOKEN_LIFETIME_MS = 50 * 60 * 1000
|
||||
|
||||
const authenticate = async (): Promise<string> => {
|
||||
const now = Date.now()
|
||||
|
||||
// Return cached token if it exists and hasn't expired
|
||||
if (cachedToken && tokenExpiry && now < tokenExpiry) {
|
||||
return cachedToken
|
||||
}
|
||||
|
||||
// Log re-authentication for debugging
|
||||
if (cachedToken && tokenExpiry && now >= tokenExpiry) {
|
||||
console.log("[Beszel Auth] Token expired, re-authenticating...")
|
||||
} else {
|
||||
console.log("[Beszel Auth] Initial authentication...")
|
||||
}
|
||||
|
||||
const beszelHost = process.env.BESZEL_HOST
|
||||
const beszelEmail = process.env.BESZEL_EMAIL
|
||||
const beszelPassword = process.env.BESZEL_PASSWORD
|
||||
|
||||
if (!beszelHost || !beszelEmail || !beszelPassword) {
|
||||
throw new Error(
|
||||
"Beszel configuration missing. Set BESZEL_HOST, BESZEL_EMAIL, and BESZEL_PASSWORD environment variables.",
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${beszelHost}/api/collections/users/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identity: beszelEmail,
|
||||
password: beszelPassword,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error(`[Beszel Auth] Authentication failed: ${response.status} - ${errorText}`)
|
||||
throw new Error(`Beszel authentication failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BeszelAuthResponse
|
||||
cachedToken = data.token
|
||||
tokenExpiry = now + TOKEN_LIFETIME_MS
|
||||
|
||||
console.log(`[Beszel Auth] Authentication successful, token valid until ${new Date(tokenExpiry).toISOString()}`)
|
||||
|
||||
return cachedToken
|
||||
}
|
||||
|
||||
return async (c, next) => {
|
||||
try {
|
||||
const token = await authenticate()
|
||||
c.set("beszelToken", token)
|
||||
await next()
|
||||
} catch (error) {
|
||||
console.error("[Beszel Auth] Middleware error:", error)
|
||||
return c.json({ error: "Authentication failed", message: String(error) }, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type BeszelContext = {
|
||||
Variables: {
|
||||
beszelToken: string
|
||||
}
|
||||
}
|
||||
7
apps/backend/src/env.d.ts
vendored
7
apps/backend/src/env.d.ts
vendored
@@ -5,5 +5,12 @@ declare namespace NodeJS {
|
||||
ADP_KEY_ID: string
|
||||
ADP_KEY_PATH: string
|
||||
GEMINI_API_KEY: string
|
||||
BESZEL_HOST?: string
|
||||
BESZEL_EMAIL?: string
|
||||
BESZEL_PASSWORD?: string
|
||||
MQTT_HOST: string
|
||||
MQTT_PORT: number
|
||||
MQTT_USERNAME: string
|
||||
MQTT_PASSWORD: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { Hono } from "hono"
|
||||
import { serveStatic, websocket } from "hono/bun"
|
||||
import { cors } from "hono/cors"
|
||||
import { logger } from "hono/logger"
|
||||
import weather from "./weather"
|
||||
import beszel from "./beszel"
|
||||
import { createMqttClient } from "./mqtt"
|
||||
import tfl from "./tfl"
|
||||
import weather from "./weather"
|
||||
import zigbee from "./zigbee/routes"
|
||||
|
||||
const mqtt = await createMqttClient({
|
||||
host: process.env.MQTT_HOST,
|
||||
port: process.env.MQTT_PORT,
|
||||
username: process.env.MQTT_USERNAME,
|
||||
password: process.env.MQTT_PASSWORD,
|
||||
})
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use("*", logger())
|
||||
app.use("*", cors())
|
||||
|
||||
app.get("/", (c) => {
|
||||
return c.json({ message: "Hello from Bun + Hono!" })
|
||||
})
|
||||
|
||||
app.get("/api/health", (c) => {
|
||||
return c.json({ status: "ok", timestamp: new Date().toISOString() })
|
||||
})
|
||||
@@ -23,7 +30,20 @@ app.route("/api/weather", weather)
|
||||
// Mount TfL routes
|
||||
app.route("/api/tfl", tfl)
|
||||
|
||||
// Mount Beszel routes
|
||||
app.route("/api/beszel", beszel)
|
||||
|
||||
// Mount Zigbee routes
|
||||
app.route("/api/zigbee", zigbee(mqtt))
|
||||
|
||||
// Serve static files from dashboard build
|
||||
app.use("/*", serveStatic({ root: "../dashboard/dist" }))
|
||||
|
||||
// Fallback to index.html for client-side routing
|
||||
app.get("*", serveStatic({ path: "../dashboard/dist/index.html" }))
|
||||
|
||||
export default {
|
||||
port: 8000,
|
||||
fetch: app.fetch,
|
||||
websocket,
|
||||
}
|
||||
|
||||
6
apps/backend/src/jrpc.ts
Normal file
6
apps/backend/src/jrpc.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type JrpcRequest = {
|
||||
jsonrpc: "2.0"
|
||||
method: string
|
||||
params: unknown
|
||||
id: number
|
||||
}
|
||||
15
apps/backend/src/mqtt.ts
Normal file
15
apps/backend/src/mqtt.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import mqtt from "mqtt"
|
||||
|
||||
export async function createMqttClient({
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
}: { host: string; port: number; username: string; password: string }) {
|
||||
return await mqtt.connectAsync({
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from "hono"
|
||||
import { type WeatherKitContext, weatherKitAuth } from "./weather-kit/middleware"
|
||||
import { generateWeatherDescription } from "./weather-kit/gemini"
|
||||
import { getCachedDescription, setCachedDescription } from "./weather-kit/cache"
|
||||
import { generateWeatherDescription } from "./weather-kit/gemini"
|
||||
import { type WeatherKitContext, weatherKitAuth } from "./weather-kit/middleware"
|
||||
|
||||
const weather = new Hono<WeatherKitContext>()
|
||||
|
||||
@@ -97,38 +97,6 @@ weather.get("/hourly/:lat/:lon", async (c) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Availability endpoint - check what datasets are available for a location
|
||||
weather.get("/availability/:lat/:lon", async (c) => {
|
||||
const { lat, lon } = c.req.param()
|
||||
const token = c.get("weatherKitToken")
|
||||
|
||||
try {
|
||||
const url = `https://weatherkit.apple.com/api/v1/availability/${lat}/${lon}`
|
||||
console.log(`Checking availability: ${url}`)
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Availability response status: ${response.status}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error(`Availability error:`, errorText)
|
||||
return c.json({ error: "Failed to check availability", status: response.status }, response.status)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log(`Availability data:`, JSON.stringify(data, null, 2))
|
||||
return c.json(data)
|
||||
} catch (error) {
|
||||
console.error("Availability exception:", error)
|
||||
return c.json({ error: String(error) }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// Complete weather data (all data sets)
|
||||
weather.get("/complete/:lat/:lon", async (c) => {
|
||||
const { lat, lon } = c.req.param()
|
||||
@@ -181,10 +149,10 @@ weather.get("/description/:lat/:lon", async (c) => {
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to fetch weather data" }),
|
||||
{ status: response.status, headers: { "Content-Type": "application/json" } },
|
||||
)
|
||||
return new Response(JSON.stringify({ error: "Failed to fetch weather data" }), {
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
const data = (await response.json()) as any
|
||||
|
||||
1
apps/backend/src/zigbee.ts
Normal file
1
apps/backend/src/zigbee.ts
Normal file
@@ -0,0 +1 @@
|
||||
const BASE_TOPIC = "nexus"
|
||||
50
apps/backend/src/zigbee/controller.ts
Normal file
50
apps/backend/src/zigbee/controller.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { ZigbeeDeviceName } from "@eva/zigbee"
|
||||
import type { MqttClient } from "mqtt"
|
||||
|
||||
export type DeviceMessageListener = (msg: unknown) => void
|
||||
|
||||
export class ZigbeeController {
|
||||
private deviceListeners: Map<string, DeviceMessageListener[]> = new Map()
|
||||
|
||||
constructor(
|
||||
private readonly baseTopic: string,
|
||||
private readonly mqtt: MqttClient,
|
||||
) {
|
||||
this.mqtt.on("message", (topic, message) => {
|
||||
const [baseTopic, deviceName] = topic.split("/")
|
||||
if (baseTopic !== this.baseTopic) {
|
||||
return
|
||||
}
|
||||
const listeners = this.deviceListeners.get(deviceName)
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(JSON.parse(message.toString()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async subscribeToDevice(deviceName: ZigbeeDeviceName, listener: DeviceMessageListener): Promise<void> {
|
||||
await this.mqtt.publishAsync(`${this.baseTopic}/${deviceName}/get`, JSON.stringify({ state: {} }))
|
||||
await this.mqtt.subscribeAsync(`${this.baseTopic}/${deviceName}`)
|
||||
if (!this.deviceListeners.has(deviceName)) {
|
||||
this.deviceListeners.set(deviceName, [])
|
||||
}
|
||||
this.deviceListeners.get(deviceName)?.push(listener)
|
||||
}
|
||||
|
||||
async unsubscribeFromDevice(deviceName: ZigbeeDeviceName, listener: DeviceMessageListener): Promise<void> {
|
||||
await this.mqtt.unsubscribeAsync(`${this.baseTopic}/${deviceName}`)
|
||||
const listeners = this.deviceListeners.get(deviceName)
|
||||
if (listeners) {
|
||||
listeners.splice(listeners.indexOf(listener), 1)
|
||||
if (listeners.length === 0) {
|
||||
this.deviceListeners.delete(deviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setDeviceState(deviceName: ZigbeeDeviceName, state: unknown): Promise<void> {
|
||||
await this.mqtt.publishAsync(`${this.baseTopic}/${deviceName}/set`, JSON.stringify(state))
|
||||
}
|
||||
}
|
||||
18
apps/backend/src/zigbee/middleware.ts
Normal file
18
apps/backend/src/zigbee/middleware.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ZIGBEE_BASE_TOPIC } from "@eva/zigbee"
|
||||
import { createMiddleware } from "hono/factory"
|
||||
import type { MqttClient } from "mqtt"
|
||||
import { ZigbeeController } from "./controller"
|
||||
|
||||
export function zigbeeController(mqtt: MqttClient) {
|
||||
const controller = new ZigbeeController(ZIGBEE_BASE_TOPIC, mqtt)
|
||||
return createMiddleware((c, next) => {
|
||||
c.set("zigbeeController", controller)
|
||||
return next()
|
||||
})
|
||||
}
|
||||
|
||||
export type ZigbeeContext = {
|
||||
Variables: {
|
||||
zigbeeController: ZigbeeController
|
||||
}
|
||||
}
|
||||
36
apps/backend/src/zigbee/routes.ts
Normal file
36
apps/backend/src/zigbee/routes.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Hono } from "hono"
|
||||
import { upgradeWebSocket } from "hono/bun"
|
||||
import type { WSContext } from "hono/ws"
|
||||
import type { MqttClient } from "mqtt"
|
||||
import type { ZigbeeController } from "./controller"
|
||||
import { type ZigbeeContext, zigbeeController } from "./middleware"
|
||||
import { WebSocketHandler } from "./ws"
|
||||
|
||||
export function zigbee(mqtt: MqttClient) {
|
||||
const h = new Hono<ZigbeeContext>()
|
||||
|
||||
h.use("*", zigbeeController(mqtt))
|
||||
|
||||
h.get(
|
||||
"/",
|
||||
upgradeWebSocket((c) => {
|
||||
const controller = c.get("zigbeeController") as ZigbeeController
|
||||
const wsHandler = new WebSocketHandler(controller)
|
||||
return {
|
||||
onOpen: (event, ws) => {
|
||||
wsHandler.handleWebsocketOpen(event, ws)
|
||||
},
|
||||
onMessage: (event, ws) => {
|
||||
wsHandler.handleWebsocketMessage(event, ws)
|
||||
},
|
||||
onClose: (event, ws) => {
|
||||
wsHandler.handleWebsocketClose(ws)
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
export default zigbee
|
||||
56
apps/backend/src/zigbee/ws.ts
Normal file
56
apps/backend/src/zigbee/ws.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { type JrpcRequest, type JrpcResponse, newJrpcRequestId } from "@eva/jrpc"
|
||||
import { ALL_ZIGBEE_DEVICE_NAMES, type ZigbeeDeviceName, type ZigbeeDeviceState } from "@eva/zigbee"
|
||||
import type { WSContext } from "hono/ws"
|
||||
import type { DeviceMessageListener, ZigbeeController } from "./controller"
|
||||
|
||||
export class WebSocketHandler {
|
||||
private deviceListeners: Map<ZigbeeDeviceName, DeviceMessageListener> = new Map()
|
||||
|
||||
constructor(private readonly controller: ZigbeeController) {}
|
||||
|
||||
handleWebsocketOpen(_event: Event, ws: WSContext) {
|
||||
for (const device of ALL_ZIGBEE_DEVICE_NAMES) {
|
||||
const l: DeviceMessageListener = (msg) => {
|
||||
const state = msg as ZigbeeDeviceState<typeof device>
|
||||
const request: JrpcRequest<"showDeviceState"> = {
|
||||
id: newJrpcRequestId(),
|
||||
jsonrpc: "2.0",
|
||||
method: "showDeviceState",
|
||||
params: { deviceName: device, state } as {
|
||||
[K in ZigbeeDeviceName]: { deviceName: K; state: ZigbeeDeviceState<K> }
|
||||
}[ZigbeeDeviceName],
|
||||
}
|
||||
ws.send(JSON.stringify(request))
|
||||
}
|
||||
this.controller.subscribeToDevice(device, l)
|
||||
this.deviceListeners.set(device, l)
|
||||
}
|
||||
}
|
||||
|
||||
async handleWebsocketMessage(event: MessageEvent, ws: WSContext) {
|
||||
const message = JSON.parse(event.data) as JrpcRequest | JrpcResponse
|
||||
if ("method" in message) {
|
||||
await this.handleRequest(message, ws)
|
||||
}
|
||||
}
|
||||
|
||||
handleWebsocketClose(_ws: WSContext) {
|
||||
for (const [device, listener] of this.deviceListeners.entries()) {
|
||||
this.controller.unsubscribeFromDevice(device, listener)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRequest(message: JrpcRequest, ws: WSContext) {
|
||||
switch (message.method) {
|
||||
case "setDeviceState": {
|
||||
await this.controller.setDeviceState(message.params.deviceName, message.params.state)
|
||||
const response: JrpcResponse<"setDeviceState"> = {
|
||||
id: message.id,
|
||||
jsonrpc: "2.0",
|
||||
result: true,
|
||||
}
|
||||
ws.send(JSON.stringify(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
VITE_API_URL=http://localhost:3000
|
||||
VITE_API_HOST=localhost:3000
|
||||
VITE_DEFAULT_LATITUDE=37.7749
|
||||
VITE_DEFAULT_LONGITUDE=-122.4194
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"name": "@eva/dashboard",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -9,7 +9,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eva/jrpc": "workspace:*",
|
||||
"@eva/zigbee": "workspace:*",
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"jotai": "^2.10.3",
|
||||
"lucide-react": "^0.546.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import { type JrpcRequest, type JrpcResponse, newJrpcRequestId } from "@eva/jrpc"
|
||||
import { ZIGBEE_DEVICE, type ZigbeeDeviceName } from "@eva/zigbee"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useEffect, useState } from "react"
|
||||
import Chart from "chart.js/auto"
|
||||
import { useStore } from "jotai"
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
import { beszelSystemsQuery } from "./beszel"
|
||||
import cn from "./components/lib/cn"
|
||||
import { StatusSeverity, getLineColor, getStatusBorderColor, tflDisruptionsQuery } from "./tfl"
|
||||
import { Tile } from "./components/tile"
|
||||
import { Kuromi } from "./kuromi"
|
||||
import {
|
||||
LightControlTile,
|
||||
type LightSceneConfig,
|
||||
LightSceneTile,
|
||||
brightnessStepAtoms,
|
||||
brightnessToStep,
|
||||
stepToBrightness,
|
||||
} from "./light-control"
|
||||
import { StatusSeverity, TubeLine, formatLineName, tflDisruptionsQuery } from "./tfl"
|
||||
import { useAutoTheme } from "./use-auto-theme"
|
||||
import {
|
||||
DEFAULT_LATITUDE,
|
||||
DEFAULT_LONGITUDE,
|
||||
@@ -11,32 +27,141 @@ import {
|
||||
weatherDescriptionQuery,
|
||||
} from "./weather"
|
||||
|
||||
const kuromi = `
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⢠⠋⠉⠉⠒⠲⢤⣀⣠⡀
|
||||
⠀⠀⠀⠀⠀⠀⣀⣀⣀⢀⡠⠖⠋⠉⠀⠀⠀⠀⠉⠉⠢⣄⠀⠀⠀⢀⠼⠤⠇
|
||||
⠀⠀⠀⣀⠔⠊⠁⠀⢨⠏⠀⠀⠀⣠⣶⣶⣦⠀⠀⠀⠀⠀⠱⣄⡴⠃⠀⠀⠀⠀
|
||||
⢸⣉⠿⣁⠀⠀⠀⢀⡇⠀⠀⠀⠀⢿⣽⣿⣼⡠⠤⢄⣀⠀⠀⢱⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠑⢦⡀⢸⠀⠀⠀⡠⠒⠒⠚⠛⠉⠀⢠⣀⡌⠳⡀⡌⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠉⠉⣆⠀⢰⠁⣀⣀⠀⠀⣀⠀⠈⡽⣧⢀⡷⠁⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⡤⢄⠀⠈⠢⣸⣄⢽⣞⡂⠀⠈⠁⣀⡜⠁⣩⡷⠿⠆⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢯⣁⡸⠀⠀⠀⡬⣽⣿⡀⠙⣆⡸⠛⠠⢧⠀⡿⠯⠆⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⣀⡀⠀⠀⡤⠤⣵⠁⢸⣻⡤⠏⠀⠀⠀⠀⢹⠀⠀⠀⡊⠱⣀⠀⠀⠀
|
||||
⠀⠀⢀⠜⠀⢘⠀⠀⠱⠲⢜⣢⣤⣧⠀⠀⠀⠀⠀⢴⠇⠀⠀⠀⠧⠠⠜⠀⠀⠀
|
||||
⠀⠀⠘⠤⠤⠚⠀⠀⠀⠀⠀⠀⢸⠁⠁⠀⣀⠎⠀⠻⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⣀⣀⡴⠤⠄⠴⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
`
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="h-screen bg-black gap-4 text-neutral-200 grid grid-cols-4 grid-rows-5 p-4">
|
||||
<DateTimeTile />
|
||||
<WeatherTile />
|
||||
<TFLTile />
|
||||
</div>
|
||||
)
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||
const wsHost = import.meta.env.VITE_API_HOST || window.location.host
|
||||
const websocket = useRef(new WebSocket(`${wsProtocol}//${wsHost}/api/zigbee`))
|
||||
|
||||
const store = useStore()
|
||||
|
||||
useAutoTheme(DEFAULT_LATITUDE, DEFAULT_LONGITUDE)
|
||||
|
||||
useEffect(() => {
|
||||
const ws = websocket.current
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected")
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket disconnected")
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data) as JrpcRequest | JrpcResponse
|
||||
if ("method" in data) {
|
||||
switch (data.method) {
|
||||
case "showDeviceState": {
|
||||
const { deviceName, state } = data.params
|
||||
const brightnessStepAtom = store.get(brightnessStepAtoms)[deviceName]
|
||||
store.set(brightnessStepAtom, brightnessToStep(state.brightness))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
}, [store])
|
||||
|
||||
function setBrightnessStep(deviceName: ZigbeeDeviceName, step: number) {
|
||||
const ws = websocket.current
|
||||
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn("WebSocket is not open. Current state:", ws.readyState)
|
||||
return
|
||||
}
|
||||
|
||||
const brightness = stepToBrightness(step)
|
||||
|
||||
const req: JrpcRequest<"setDeviceState"> = {
|
||||
id: newJrpcRequestId(),
|
||||
jsonrpc: "2.0",
|
||||
method: "setDeviceState",
|
||||
params: {
|
||||
deviceName,
|
||||
state: step === 0 ? { state: "OFF", brightness: 0 } : { state: "ON", brightness },
|
||||
},
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify(req))
|
||||
}
|
||||
|
||||
function setScene(scene: LightSceneConfig) {
|
||||
const ws = websocket.current
|
||||
for (const [deviceName, state] of Object.entries(scene.deviceStates)) {
|
||||
const req: JrpcRequest<"setDeviceState"> = {
|
||||
id: newJrpcRequestId(),
|
||||
jsonrpc: "2.0",
|
||||
method: "setDeviceState",
|
||||
params: {
|
||||
deviceName: deviceName as ZigbeeDeviceName,
|
||||
state,
|
||||
},
|
||||
}
|
||||
ws.send(JSON.stringify(req))
|
||||
}
|
||||
}
|
||||
|
||||
function Tile({
|
||||
decorations = true,
|
||||
children,
|
||||
className,
|
||||
}: { decorations?: boolean; children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={cn("relative bg-neutral-900 flex flex-col justify-end items-start", className)}>
|
||||
{decorations && (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 w-4 h-[1px] bg-neutral-200" />
|
||||
<div className="absolute top-0 left-0 w-[1px] h-4 bg-neutral-200" />
|
||||
<div className="absolute bottom-0 right-0 w-4 h-[1px] bg-neutral-200" />
|
||||
<div className="absolute bottom-0 right-0 w-[1px] h-4 bg-neutral-200" />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
<div className="h-screen bg-neutral-300 dark:bg-neutral-800 p-2 select-none">
|
||||
<div className="w-full h-full grid grid-cols-4 grid-rows-5 gap-2 bg-neutral-300 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
|
||||
<DateTimeTile />
|
||||
<WeatherTile />
|
||||
|
||||
<TFLTile className="row-start-1 row-span-1" />
|
||||
|
||||
<SystemTile className="row-start-2 row-span-1" systemName="helian" displayName="Helian" />
|
||||
<SystemTile className="row-start-2 row-span-1" systemName="akira" displayName="Akira" />
|
||||
|
||||
<LightSceneTile
|
||||
className="row-start-3 col-start-3 col-span-1 row-span-2"
|
||||
onSceneChange={(scene) => {
|
||||
setScene(scene)
|
||||
}}
|
||||
/>
|
||||
|
||||
<LightControlTile
|
||||
className="row-start-3 col-start-4 col-span-1"
|
||||
deviceName={ZIGBEE_DEVICE.livingRoomFloorLamp}
|
||||
onRequestBrightnessStepChange={(step) => {
|
||||
setBrightnessStep(ZIGBEE_DEVICE.livingRoomFloorLamp, step)
|
||||
}}
|
||||
/>
|
||||
<LightControlTile
|
||||
className="row-start-4 col-start-4 col-span-1"
|
||||
deviceName={ZIGBEE_DEVICE.deskLamp}
|
||||
onRequestBrightnessStepChange={(step) => {
|
||||
setBrightnessStep(ZIGBEE_DEVICE.deskLamp, step)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tile className="row-start-5 col-start-3 col-span-2 row-span-1 flex items-center justify-center overflow-hidden">
|
||||
<Kuromi />
|
||||
</Tile>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -65,8 +190,8 @@ function DateTimeTile() {
|
||||
|
||||
return (
|
||||
<Tile className="col-start-1 row-start-1 col-span-2 row-span-3 p-6">
|
||||
<p className="text-4xl mb-2 font-extralight">{formattedDate}</p>
|
||||
<p className="text-8xl font-bold">{formattedTime}</p>
|
||||
<p className="text-4xl mb-2 font-mono uppercase tracking-tigher">{formattedDate}</p>
|
||||
<p className="text-8xl font-extralight tracking-tight">{formattedTime}</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
@@ -126,7 +251,11 @@ function WeatherTile() {
|
||||
const temperature = Math.round(currentWeather.temperature)
|
||||
const lowTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMin ?? 0)
|
||||
const highTemp = Math.round(dailyForecastData?.forecastDaily?.days[0].temperatureMax ?? 0)
|
||||
const percentage = lowTemp && highTemp ? (temperature - lowTemp) / (highTemp - lowTemp) : 0
|
||||
// Calculate percentage: handle case where lowTemp might be 0 (falsy) by checking for valid numbers
|
||||
const tempRange = highTemp - lowTemp
|
||||
const percentage = tempRange !== 0 && !Number.isNaN(tempRange)
|
||||
? Math.max(0, Math.min(1, (temperature - lowTemp) / tempRange))
|
||||
: 0
|
||||
const highlightIndexStart = Math.floor((1 - percentage) * 23)
|
||||
const WeatherIcon = getWeatherIcon(currentWeather.conditionCode)
|
||||
|
||||
@@ -152,7 +281,7 @@ function WeatherTile() {
|
||||
L:{lowTemp}°
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2 flex-[1]">
|
||||
<div className="flex flex-col space-y-2 flex-[2]">
|
||||
{Array.from({ length: 24 }).map((_, index) => {
|
||||
if (index === highlightIndexStart) {
|
||||
return (
|
||||
@@ -160,18 +289,18 @@ function WeatherTile() {
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
key={index}
|
||||
className={cn("w-10 bg-teal-400 h-[2px]")}
|
||||
className={cn("w-10 bg-teal-500 dark:bg-teal-400 h-[2px]")}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute flex flex-row items-center space-x-1 top-0 right-0 bg-teal-400 text-neutral-900 px-2 py-1 text-2xl font-bold rounded-r-sm translate-x-[calc(100%-1px)]",
|
||||
"absolute flex flex-row items-center space-x-1 top-0 right-0 bg-teal-500 dark:bg-teal-400 text-neutral-200 dark:text-neutral-900 px-2 py-1 text-4xl font-bold rounded-r translate-x-[calc(100%-1px)]",
|
||||
percentage < 0.3
|
||||
? "-translate-y-[calc(100%-2px)] rounded-tl-sm"
|
||||
: "rounded-bl-sm",
|
||||
? "-translate-y-[calc(100%-2px)] rounded-tl"
|
||||
: "rounded-bl",
|
||||
)}
|
||||
>
|
||||
<p className="leading-none translate-y-px">{temperature}°</p>
|
||||
<WeatherIcon className="size-6" strokeWidth={3} />
|
||||
<WeatherIcon className="size-8" strokeWidth={3} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -183,7 +312,7 @@ function WeatherTile() {
|
||||
className={cn(
|
||||
"w-4",
|
||||
index >= highlightIndexStart
|
||||
? "bg-teal-400 w-8 h-[2px]"
|
||||
? "bg-teal-500 dark:bg-teal-400 w-8 h-[2px]"
|
||||
: "bg-neutral-400 w-4 h-[1px]",
|
||||
)}
|
||||
/>
|
||||
@@ -192,7 +321,7 @@ function WeatherTile() {
|
||||
</div>
|
||||
<div className="flex flex-col justify-start h-full space-y-2 flex-[3]">
|
||||
<p
|
||||
className={cn("text-3xl leading-none tracking-tight font-light", {
|
||||
className={cn("text-3xl leading-none tracking-tight font-light pl-4", {
|
||||
"text-red-400": errorWeatherDescription,
|
||||
"animate-pulse": isLoadingWeatherDescription,
|
||||
})}
|
||||
@@ -205,7 +334,16 @@ function WeatherTile() {
|
||||
)
|
||||
}
|
||||
|
||||
function TFLTile() {
|
||||
function TFLTile({ className }: { className?: string }) {
|
||||
const linesIDontCareAbout = [
|
||||
TubeLine.WaterlooCity,
|
||||
TubeLine.Windrush,
|
||||
TubeLine.Lioness,
|
||||
TubeLine.Lioness,
|
||||
TubeLine.Tram,
|
||||
TubeLine.Mildmay,
|
||||
]
|
||||
|
||||
const {
|
||||
data: tflData,
|
||||
isLoading: isLoadingTFL,
|
||||
@@ -215,8 +353,10 @@ function TFLTile() {
|
||||
select: (data) => {
|
||||
data.disruptions.sort((a, b) => {
|
||||
if (a.lineName.match(/northern/i)) return -1
|
||||
if (b.lineName.match(/northern/i)) return 1
|
||||
return a.statusSeverity - b.statusSeverity
|
||||
})
|
||||
data.disruptions = data.disruptions.filter((disruption) => !linesIDontCareAbout.includes(disruption.lineId))
|
||||
return data
|
||||
},
|
||||
refetchInterval: 5 * 60 * 1000, // 5 minutes
|
||||
@@ -225,55 +365,163 @@ function TFLTile() {
|
||||
|
||||
if (isLoadingTFL) {
|
||||
return (
|
||||
<Tile className="col-start-3 h-full row-start-1 col-span-2 row-span-2 flex flex-row justify-start items-center p-8">
|
||||
<p className="text-2xl font-light animate-pulse">Loading TfL</p>
|
||||
<Tile
|
||||
className={cn("h-full col-span-2 row-span-1 flex flex-row justify-start items-center p-8", className)}
|
||||
>
|
||||
<p className="text-2xl font-light animate-pulse">Loading tube status</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
if (errorTFL) {
|
||||
return (
|
||||
<Tile
|
||||
className={cn("h-full col-span-2 row-span-1 flex flex-row justify-start items-center p-8", className)}
|
||||
>
|
||||
<p className="text-2xl font-light text-red-400">Error loading from TfL</p>
|
||||
<p className="text-neutral-400">{errorTFL?.message}</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
if (!tflData) {
|
||||
return (
|
||||
<Tile
|
||||
className={cn("h-full col-span-2 row-span-1 flex flex-row justify-start items-center p-8", className)}
|
||||
>
|
||||
<p className="text-2xl font-light">No TfL data available</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tile
|
||||
decorations={false}
|
||||
className="gap-x-1 col-start-3 h-full row-start-1 col-span-2 row-span-1 grid grid-cols-[min-content_1fr] auto-rows-min overflow-y-auto"
|
||||
className={cn(
|
||||
"pt-1 h-full col-span-2 row-span-1 grid grid-cols-[min-content_1fr] auto-rows-min overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{tflData?.goodService.includes("Northern") && (
|
||||
{tflData.goodService.includes("Northern") && (
|
||||
<TFLDistruptionItem
|
||||
lineId="northern"
|
||||
lineName="Northern"
|
||||
lineId={TubeLine.Northern}
|
||||
reason="Good service"
|
||||
severity={StatusSeverity.GoodService}
|
||||
/>
|
||||
)}
|
||||
{tflData?.disruptions.map((disruption) => (
|
||||
<>
|
||||
{tflData.disruptions.map((disruption) => (
|
||||
<TFLDistruptionItem
|
||||
key={disruption.lineId}
|
||||
lineId={disruption.lineId}
|
||||
lineName={disruption.lineName}
|
||||
reason={disruption.reason ?? "Unknown reason"}
|
||||
severity={disruption.statusSeverity}
|
||||
/>
|
||||
<hr className="col-span-2 border-neutral-700" />
|
||||
</>
|
||||
))}
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
function TFLDistruptionItem({
|
||||
lineId,
|
||||
lineName,
|
||||
reason,
|
||||
severity,
|
||||
}: { lineId: string; lineName: string; reason: string; severity: number }) {
|
||||
function TFLDistruptionItem({ lineId, reason, severity }: { lineId: TubeLine; reason: string; severity: number }) {
|
||||
const lineName = formatLineName(lineId)
|
||||
|
||||
let lineStyleClass: string
|
||||
switch (lineId) {
|
||||
case "bakerloo":
|
||||
lineStyleClass = "bg-amber-700"
|
||||
break
|
||||
case "central":
|
||||
lineStyleClass = "bg-red-600"
|
||||
break
|
||||
case "circle":
|
||||
lineStyleClass = "bg-yellow-400 text-neutral-900"
|
||||
break
|
||||
case "district":
|
||||
lineStyleClass = "bg-green-600"
|
||||
break
|
||||
case "hammersmith-city":
|
||||
lineStyleClass = "bg-pink-400"
|
||||
break
|
||||
case "jubilee":
|
||||
lineStyleClass = "bg-slate-500"
|
||||
break
|
||||
case "metropolitan":
|
||||
lineStyleClass = "bg-purple-800"
|
||||
break
|
||||
case "northern":
|
||||
lineStyleClass = "bg-black text-neutral-200 dark:bg-neutral-200 dark:text-black"
|
||||
break
|
||||
case "piccadilly":
|
||||
lineStyleClass = "bg-blue-900"
|
||||
break
|
||||
case "victoria":
|
||||
lineStyleClass = "bg-sky-500"
|
||||
break
|
||||
case "waterloo-city":
|
||||
lineStyleClass = "bg-teal-500"
|
||||
break
|
||||
case "london-overground":
|
||||
lineStyleClass = "bg-orange-500"
|
||||
break
|
||||
case "dlr":
|
||||
lineStyleClass = "bg-teal-600"
|
||||
break
|
||||
case "elizabeth":
|
||||
lineStyleClass = "bg-purple-600"
|
||||
break
|
||||
case "tram":
|
||||
lineStyleClass = "bg-green-500"
|
||||
break
|
||||
default:
|
||||
lineStyleClass = "bg-gray-500"
|
||||
break
|
||||
}
|
||||
|
||||
let statusBorderClass: string
|
||||
switch (severity) {
|
||||
case StatusSeverity.GoodService:
|
||||
statusBorderClass = "border-green-500"
|
||||
break
|
||||
case StatusSeverity.MinorDelays:
|
||||
statusBorderClass = "border-yellow-500"
|
||||
break
|
||||
case StatusSeverity.Suspended:
|
||||
statusBorderClass = "border-red-600"
|
||||
break
|
||||
case StatusSeverity.PartSuspended:
|
||||
statusBorderClass = "border-red-500"
|
||||
break
|
||||
case StatusSeverity.PlannedClosure:
|
||||
statusBorderClass = "border-orange-600"
|
||||
break
|
||||
case StatusSeverity.PartClosure:
|
||||
statusBorderClass = "border-yellow-500"
|
||||
break
|
||||
case StatusSeverity.SevereDelays:
|
||||
statusBorderClass = "border-red-500"
|
||||
break
|
||||
case StatusSeverity.ReducedService:
|
||||
statusBorderClass = "border-orange-500"
|
||||
break
|
||||
case StatusSeverity.BusService:
|
||||
statusBorderClass = "border-blue-500"
|
||||
break
|
||||
case StatusSeverity.Information:
|
||||
statusBorderClass = "border-blue-400"
|
||||
break
|
||||
case StatusSeverity.ServiceClosed:
|
||||
statusBorderClass = "border-red-700"
|
||||
break
|
||||
default:
|
||||
statusBorderClass = "border-gray-400"
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex items-center justify-center px-2 py-0.5">
|
||||
<p
|
||||
className={cn(
|
||||
"text-xl uppercase font-bold bg-blue-500 w-full text-center px-1 rounded-sm",
|
||||
getLineColor(lineId),
|
||||
getStatusBorderColor(severity),
|
||||
"text-neutral-200 text-sm uppercase w-full text-center px-1 rounded-lg",
|
||||
lineStyleClass,
|
||||
)}
|
||||
>
|
||||
{lineName}
|
||||
@@ -281,8 +529,8 @@ function TFLDistruptionItem({
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"text-xl text-wrap text-neutral-300 leading-tight self-center pr-2 py-1 font-light border-r-4",
|
||||
getStatusBorderColor(severity),
|
||||
"text-xl text-wrap leading-tight self-center pr-2 py-1.5 font-light border-r-4",
|
||||
statusBorderClass,
|
||||
)}
|
||||
>
|
||||
{reason}
|
||||
@@ -291,4 +539,160 @@ function TFLDistruptionItem({
|
||||
)
|
||||
}
|
||||
|
||||
function SystemTile({
|
||||
className,
|
||||
systemName,
|
||||
displayName,
|
||||
}: { className?: string; systemName: string; displayName: string }) {
|
||||
const { data } = useQuery({
|
||||
...beszelSystemsQuery(),
|
||||
refetchInterval: 1000,
|
||||
refetchIntervalInBackground: true,
|
||||
})
|
||||
const chartRef = useRef<Chart | null>(null)
|
||||
|
||||
const beszelSystemsData = data?.systems.find((system) => system.name === systemName)
|
||||
|
||||
const onCanvasRef = (elem: HTMLCanvasElement | null) => {
|
||||
if (!elem || chartRef.current) return
|
||||
|
||||
const cpuFillGradient = elem?.getContext("2d")?.createLinearGradient(0, 0, 0, elem.height)
|
||||
cpuFillGradient?.addColorStop(0, "#2dd4bf")
|
||||
cpuFillGradient?.addColorStop(0.5, "rgba(45, 212, 191, 0)")
|
||||
cpuFillGradient?.addColorStop(1, "rgba(45, 212, 191, 0)")
|
||||
|
||||
const ramFillGradient = elem?.getContext("2d")?.createLinearGradient(0, 0, 0, elem.height)
|
||||
ramFillGradient?.addColorStop(0, "#a78bfa")
|
||||
ramFillGradient?.addColorStop(0.5, "rgba(167, 139, 250, 0)")
|
||||
ramFillGradient?.addColorStop(1, "rgba(167, 139, 250, 0)")
|
||||
|
||||
chartRef.current = new Chart(elem, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: Array.from({ length: 20 }, (_, index) => index),
|
||||
datasets: [
|
||||
{
|
||||
data: Array.from({ length: 20 }, (_, __) => null),
|
||||
fill: true,
|
||||
backgroundColor: cpuFillGradient,
|
||||
borderColor: "#2dd4bf",
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
data: Array.from({ length: 20 }, (_, __) => null),
|
||||
fill: true,
|
||||
backgroundColor: ramFillGradient,
|
||||
borderColor: "#a78bfa",
|
||||
tension: 0.1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: { display: false, min: 0, max: 100 },
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
elements: {
|
||||
point: { radius: 0 },
|
||||
line: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const cpu = beszelSystemsData?.info.cpu
|
||||
const ram = beszelSystemsData?.info.ram
|
||||
if (!chartRef.current || cpu === undefined || ram === undefined) return
|
||||
|
||||
const cpuDataset = chartRef.current.data.datasets[0]
|
||||
const ramDataset = chartRef.current.data.datasets[1]
|
||||
|
||||
const nextCpuData = Array.from({ length: 20 }, (_, i) => {
|
||||
if (i === 19) {
|
||||
return null
|
||||
}
|
||||
return cpuDataset.data[i + 1]
|
||||
})
|
||||
nextCpuData[19] = cpu
|
||||
|
||||
const nextRamData = Array.from({ length: 20 }, (_, i) => {
|
||||
if (i === 19) {
|
||||
return null
|
||||
}
|
||||
return ramDataset.data[i + 1]
|
||||
})
|
||||
nextRamData[19] = ram
|
||||
|
||||
cpuDataset.data = nextCpuData
|
||||
ramDataset.data = nextRamData
|
||||
chartRef.current.update()
|
||||
})
|
||||
|
||||
if (!beszelSystemsData) {
|
||||
return (
|
||||
<Tile className={cn("h-full flex flex-row justify-start items-center p-8", className)}>
|
||||
<p className="text-2xl font-light">No system status available</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
let systemStatusContent: React.ReactNode
|
||||
switch (beszelSystemsData.status) {
|
||||
case "up":
|
||||
systemStatusContent = (
|
||||
<div className="w-full flex-1 min-w-0 basis-0 relative mb-2">
|
||||
<canvas ref={onCanvasRef} className="min-h-0 absolute top-0 left-0 w-full h-full" />
|
||||
</div>
|
||||
)
|
||||
break
|
||||
|
||||
case "down":
|
||||
systemStatusContent = (
|
||||
<div className="w-full flex-1 flex items-center justify-center">
|
||||
<p className="font-mono text-red-500 uppercase font-bold">System offline</p>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
|
||||
<div className="grid grid-cols-6 px-3 pt-2 w-full">
|
||||
<div className="col-span-3 self-start flex flex-row items-center space-x-2">
|
||||
<p className="leading-none tracking-tight text-2xl">{displayName}</p>
|
||||
<div
|
||||
className={cn("size-2 border rounded-full", {
|
||||
"animate-pulse border-green-300 bg-green-500": beszelSystemsData.status === "up",
|
||||
"border-red-300 bg-red-500": beszelSystemsData.status === "down",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col font-mono">
|
||||
<p className="text-neutral-400 text-right leading-none">CPU</p>
|
||||
<p className="text-right">{beszelSystemsData.info.cpu.toFixed(0).padStart(3, "0")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col font-mono">
|
||||
<p className="text-neutral-400 text-right leading-none">RAM</p>
|
||||
<p className="text-right">{beszelSystemsData.info.ram.toFixed(0).padStart(3, "0")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col font-mono">
|
||||
<p className="text-neutral-400 text-right leading-none">DSK</p>
|
||||
<p className="text-right">{beszelSystemsData.info.disk.toFixed(0).padStart(3, "0")}</p>
|
||||
</div>
|
||||
</div>
|
||||
{systemStatusContent}
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
550
apps/dashboard/src/assets/kuromi-frames.json
Normal file
550
apps/dashboard/src/assets/kuromi-frames.json
Normal file
@@ -0,0 +1,550 @@
|
||||
[
|
||||
[
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" #*++*# ",
|
||||
" *=.....= ",
|
||||
" *-.......= ",
|
||||
" %*.........* ",
|
||||
" #-.........* ",
|
||||
" #..........+ ",
|
||||
" #..........* ",
|
||||
" *+.........* ",
|
||||
" %*........+# ",
|
||||
" #*......=# ",
|
||||
" #*+::=**# ",
|
||||
" @%******# ",
|
||||
" *******# ",
|
||||
" %********# ",
|
||||
" **********# ",
|
||||
" ****..-****# ",
|
||||
" #***-....+***# ",
|
||||
" #***......=***# ",
|
||||
" ***-.......:***# ",
|
||||
" @***..........***% ",
|
||||
" #**-...........*** ",
|
||||
" ***.............+** ",
|
||||
" **+..............=** ",
|
||||
" %**................-** ",
|
||||
" ***.................:** ",
|
||||
" **-..................:** ",
|
||||
" %**.....................** ",
|
||||
" #*+.......:..............** ",
|
||||
" **........-...............** ",
|
||||
" **........=..-.............*# ",
|
||||
" %*=........+.+...............+# ##*#% ",
|
||||
" #*.......-+**.................+% *:...=*% ",
|
||||
" *+.....:-+***-:................+# =.......*% ",
|
||||
" *-........**+...................*% #........:* ",
|
||||
" %*........+:*-:...................** :.........* ",
|
||||
" *+.......=..=.:....................* ..........+# ",
|
||||
" *:......:...-.......................* :.........+# ",
|
||||
" %*...........:.......................-# %##+.........*@ ",
|
||||
" %+.......:...:........................=% ###****+=--:*.........* ",
|
||||
" *........:.............................+% *:---...............:*.......*% ",
|
||||
" *........=..............................* +-.........................-*:...=*% ",
|
||||
" +.......=+.......................:-=+++******##% @#****-:.............................:*#***# ",
|
||||
" #:.....:-+++:................-+**+++==-----::--=+****#% %**+=...................................*% ",
|
||||
" #.........+..............-**+--------------::.......:+***%% **.......................................* ",
|
||||
" *.........:...........-*+::----------------::...........:+**% @*.......................................=# ",
|
||||
" *.........:.........=+:.--------------------:..............:+*## #+.......................................* ",
|
||||
" +.................+=.-----------------------:.................-**% *.......................................-* ",
|
||||
" -................:.-------------------------::..................:** *.......................................*# ",
|
||||
" #-...................:-----------------------::.....................*+.......................................* ",
|
||||
" %:.....................::---------------------::....................:-......................................+# ",
|
||||
" *........................:-------------------::....................=.......................................* ",
|
||||
" *:.......+................::-----------------::...................+......................................+* ",
|
||||
" #=.....+..................::=+++=-----------::...................+.....................................:* ",
|
||||
" %*..:+..................=**#%%%#**+---------::........................................................+* ",
|
||||
" #=+.................-*#%%%%%%%%%%*=-------:::......................................................=*% ",
|
||||
" #+.................+*%%%%%%%%%%%%%%*-------::......................................................+* ",
|
||||
" %*.................+*%%%%%%%%%%%%%%%%#------:::....................................:..:............+*# ",
|
||||
" *.................+*%%%%%%%%%%%%%%%%%%*------:::................................-..-.-............:+* ",
|
||||
" *:.................*%%%%%%%%%%%%%%%%%%%%+-----::::................................+.+=.............++% ",
|
||||
" #=.................*%%%%%%%%%%%%%%%%%%%%%%=-----:::.................................**+............=+* ",
|
||||
" #*..................*%%%%%%%%%%%%%%%%%%%%%%*------:::.............................-=****:...........++# ",
|
||||
" *:.................:#%%%#%%%%%%%%%%%%%%%%%%*------::::...............................+*+...........++* ",
|
||||
" #+..................=%%%=.#%%%%%%%%%%%%%%%%%#+::----::::.............................+.*.:.........=++# ",
|
||||
" *...................=%%%...#%%%%%%%%%%%%%%%%%+:::----::::...........................-..+..........:++*% ",
|
||||
" #+...................:%%=....%%%%%%%%%%%%%%%%%=.:::----::::.............................=..........+++# ",
|
||||
" *.....................#%:....%%%%%%%%%%%%%%%%%...:::---:::::............................:.........+++* ",
|
||||
" #*.....................*%*...%%%%%%%%%#=..%%%%%....:::---:::::...........................:........=+++# ",
|
||||
" *:.....................:%%*=%%%%%%%%%%....-%%%%.....::::--::::::.........................:.......:++++ ",
|
||||
" *.......................*%%%%%%%%%%%%#.....%%%*......::::--::::::................................++++# ",
|
||||
" #+........................*%%%%%%%%%%%%....:%%%:.......::::--::::::..............................+++++ ",
|
||||
" *:.........................*%%%%%%%%%%%:...%%%*.........::::--::::::............................+++++# ",
|
||||
" *..............=**=........+%%%=%%%%%%%%#+%%%#:..........::::--:::::::.........................++++++ ",
|
||||
" *...........:*@@@@@*-......*%%=:%.%%%%%%%%%%#=............::::--::::::::......................++++++* ##*% ",
|
||||
" %=.........*@@@@@@@@@*:.....+*#.#%.%%%%%%%%%*=..............::::--::::::::....................+++++++% #**-.# ",
|
||||
" #-.......*@@@@@@@@@@@@*:......-***.%%#*****+.................:::---:::::::::................-+++++++*#**=....* ",
|
||||
" *......:*@@@@+@@@@@@@@@*-........+**%*.......................::::---::::::::::............-++++++++**+.......+ ",
|
||||
" *......*@@@@@+#@@@@@@@@@*:.........:+*........................::::----::::::::::........+*+++++++++*.........- ",
|
||||
" *.....*@@@@#@%=@@@@@@@@@@*:......................:+**=.........::::----:::::::::::........-+**++++*+..........# ",
|
||||
" *....-@@@@@#++==@@@@@@@@@@*:..................=*#@@@@@**:.......::::----:::::::::::::........*#***##*:........+ ",
|
||||
" *....@@@@@@@====+@@@@@@@@@@*:..............=@@@@@@@@@@@@*+......:::::-----:::=::::::::::.....* #*........- ",
|
||||
" *...=@@@@@@@=++==+@@@@@@@@@@*:.......:=*%@@@@@@@@@@@@@@@@%*......::::------::+:::::::::::::::* *.........**# ",
|
||||
" *...%@@@@@@+#@@%==*@@@@@@@@@@**=-:+**%@@@@@@@@@@@@@@@@@@@@@*:.....::::-------+:::::::::::::::* *.....*:..%@#* ",
|
||||
" *...@@@@@@@+#*=====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*-....:::::------+:::::::::::::::* %-.....**+.*@@%* ",
|
||||
" *..-@@@@@@#+@*++====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*:....::::-----+*+-:::::::::::::* *.....+%@@**@@@* ",
|
||||
" *..=@@@@@@+++++++++==@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::--+=-*-=+::::::::::::* @=.....*@@@@#@@@*% ",
|
||||
" *..+@@@@@@++++++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::---+*+-==-::::::::::* #*@*.....-*@@@@@@@@*% ",
|
||||
" #..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+#@@@@%+....::::*-=***+-*---::::::::***%@#......*@@@@@@@@@* ",
|
||||
" ..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++@@@@@@*:...:::-+********=----::::::*%@@+......=#@@@@@@@@@* ",
|
||||
" ..=@@@@@@*++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+===@%@@@@@@*....:::+-*****=*-------:::.*#+........*@@@@@@@@@%* ",
|
||||
" +.-@@@@@@@+++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*======%+@@@@@@#=...:::+:-***--+---------::+.........*@@@@@@@@@@* ",
|
||||
" #..%@@@@@@%**+++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#======+==+%@@@@@@@*...::::+--*=-*-----------==.......-+@@@@@@@@@@@* ",
|
||||
" -.*@@@@@@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*++===+%@+==@@@@@@@@#:...::::*+*+*------------*+++++++++@@@@@@@@@@@* ",
|
||||
" *.*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+++++++@===@@@@@@@@@*...:::::-*--------------*%++++++#@@@@@@@@@@@*# ",
|
||||
" #:=@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@*++++++++@++@@@@@@@@@*....:::::+--------------*@@*+#%@@@@@@@@@@@@#* ",
|
||||
" *.*@@@@@@@@@@@@@@@@@@@@*##*@@@@@@@@@@@@@@@@#+++++++++++@@@@@@@@@#:...::=::+-------------+%@@@*#@@@@@@@@@@@@#* ",
|
||||
" @=*@@@@@@@@@@@@@@@@@@@*#%%%%@@@@@@@@@@@@@@@%+++++++++++@@@@@@@@@@+...::=::+-------------*@@@@@#*@@@@@@@@@@*# ",
|
||||
" *-#@@@@@@@@@@@@@@@@@@@*%%%#@@@@@@@@@@@@@@@@++++++++++%@@@@@@@@@@+....:#:::------------=#@@@@@@#*@@@@@@@@@* ",
|
||||
" **@@@@@@@@@@@@@@@@@@@#*###@@@@@@@@@@@@@@@@++++++++++@@@@@@@@@@@*....=%#:::-----------*@@@@@@@@#*@@@@@@@@* ",
|
||||
" #*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++++++++*@@@@@@@@@@@*....#%%-::----------=#@@@@@@@@@#*@@@@@@%% ",
|
||||
" #*@@@@@@@@@@@@@@@@@@@@@@@@@#*#@@@@@@@@@@@@**++++++@@@@@@@@@@@@*.....#-:::----------*%@@@@@@@@@@#*@@@@@*% ",
|
||||
" **@@@@@@@@@@@@@@@*@@@@@@@%*%##@@@@@@@@@@@@%*****@@@@@@@@@@@@@*.....+:::::--------+#@@@@@@@@@@@@%*%@@@* ",
|
||||
" **@@@@@@@@@@@@@@@*@@@@@@*%#%#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+.....=:::::-------=*@@@@@@@@@@@@@@@*#@%% ****% ",
|
||||
" #**##**#@@@@@@@@@@**%@@*%###@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=.....::::::-------*%@@@@@@@@@@@@@@@@%**%@@%@@@@**@ ",
|
||||
" #*@@@@@@%*%@@@@@@@@@@#*****#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:......::::::-----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*# ",
|
||||
" @*@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.......::::::----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" #%@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.......::::::---+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%........:::::::-*%@@%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=........:::::::*%@@%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" #%@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.........:::::=*#@@@*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" *@@@@@@@@@@@@#*%@@@@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@%...........:::*+..=*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" %#@@@@@@@@@@@%@#*#@@@@@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@@%............:-+-......*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" ##@@@@@@@@@@@@@@%**%@@@@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@@#:..........=+++........*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" %*@@@@@@@@@@@@@@@****#@@@@@@@@@@@*@@@@@@@@@@@*@@@@@%*.........-*+++:........*%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" %*% @@@@*+++++*#@@@@@@@*%@@@@@@@@@@@*@@@%*:......-+*++++-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*#@ @*+++++++++++**#%*@@@@@@@@@@@@*%**...:=+***+++++-.......-*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" @% **###% *-=+++++++++++*@*@@@@@@@@@@@@@********+++++****++==++**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *#*######*:....:=++++**%@@*@@@@@@@@@@@@@*+++++++++++=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++-...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" *######********##%*%@@@@@*@@@@@@@@@@@@*+==-........-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*#####* #* #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||
" **###*@ %** *#@@#@@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||
" #***#***# #*#@*@@@@@@@@@@@@#+..........=@@@@@@@@@@@@@@@@@@@@@## #***********# %*%@@@@@@@%* ",
|
||||
" %%# %**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*@ #**# **%@@@@#*@ ",
|
||||
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**# ",
|
||||
" #*@@@@@@@@@@@@@@%**-...:*####@@@@@%#**# ",
|
||||
" #*@@@@@@@@@@@@@@@@#**+*#####*****#@ ",
|
||||
" #**@@@@@@@@@@@@@%*****######% ",
|
||||
" #***#%%@@%##*## #*###### ",
|
||||
" %##**#%@ *#####* ",
|
||||
" %*###*# ",
|
||||
" %***# "
|
||||
],
|
||||
[
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" #**#@ ",
|
||||
" #*....:+ ",
|
||||
" #+.......= ",
|
||||
" *.........@ ",
|
||||
" #=.........* ",
|
||||
" *:.........+ ",
|
||||
" *:.........* ",
|
||||
" #=.........* ",
|
||||
" *........-# ",
|
||||
" #+......:* ",
|
||||
" #*-..:+** ",
|
||||
" ********% ",
|
||||
" %*******% ",
|
||||
" #********# ",
|
||||
" *********** ",
|
||||
" ****..:***** ",
|
||||
" %***-....-**** ",
|
||||
" ****.......+***% ",
|
||||
" ***=........-***% ",
|
||||
" ***...........***% ",
|
||||
" ***............=**# ",
|
||||
" @**=..............*** #**# ",
|
||||
" #**................+** #=...-*# ",
|
||||
" ***.................-**@ #.......*% ",
|
||||
" **+...................**% .........* ",
|
||||
" @**.....................+*@ -.........+% ",
|
||||
" #**......................=*# -.........-* ",
|
||||
" %*+........................*# =.........:* ",
|
||||
" **-.......-.................*% *.........=# ",
|
||||
" **........+..................+# ##***-........* ",
|
||||
" **......-.*.=.................=# %##**+=....*.......+# ",
|
||||
" *+.......***...................-* #*+=...........*=...:*# ",
|
||||
" #*.......=****-..................-* %--.................=*****% ",
|
||||
" #*........+**.....................:* #+=......................*% ",
|
||||
" **........:*.-.....................:* =...........................* ",
|
||||
" *=.......:.-........................:# @#**=.............................:* ",
|
||||
" *.......:............................:# #**+-................................*# ",
|
||||
" *.....................................-# #*+....................................* ",
|
||||
" %+......................................*####*##*%% *+.....................................:* ",
|
||||
" *-...............................:=****+++==----==+****## *......................................+% ",
|
||||
" *........=....................+**+=-------::...........-+***%% *......................................* ",
|
||||
" *.......:+:...............:+*+-------------:...............:+**# @=.....................................-* ",
|
||||
" *.....:=+++:............=*=.---------------::..................=**%#:.....................................*# ",
|
||||
" %+........+............*+.------------------::.....................+*......................................* ",
|
||||
" #-........=..........+=.---------------------:......................*.....................................+* ",
|
||||
" *:........:........+=.-----------------------::.....................*.....................................*# ",
|
||||
" #:................+.--------------------------::....................*....................................=* ",
|
||||
" #...................:-------------------------::....................=....................................+* ",
|
||||
" *......................::---------------------:::.......................................................=*% ",
|
||||
" #.........................::-------------------:::......................................:...............+* ",
|
||||
" %...........................:::-=+++=-----------::......................................+..............++# ",
|
||||
" *:........+.................-**#####**+--------:::...................................:.*.+............+*% ",
|
||||
" #=......*.................**%%%%%%%%%%*=-------:::...................................***............++* ",
|
||||
" %*:...*................=*%%%%%%%%%%%%%%*-------:::.................................:****:.........:++% ",
|
||||
" %+.+................=*%%%%%%%%%%%%%%%%#------::::.................................-**=..........++*% ",
|
||||
" *:...............-*%%%%%%%%%%%%%%%%%%#------::::................................+-=.=........:++* ",
|
||||
" %+................*%%%%%%%%%%%%%%%%%%%%*------::::..............................-..=..:.......+++% ",
|
||||
" @*................+#%%%%%%%%%%%%%%%%%%%%#+------::::................................=.........=++* ",
|
||||
" *.................*%%%%%%%%%%%%%%%%%%%%%%*:-----:::::...............................=.........+++* ",
|
||||
" %+................:*%%%%%%%%%%%%%%%%%%%%%%*=::----:::::..............................:........++++ ",
|
||||
" *.................=%%%%*%%%%%%%%%%%%%%%%%%%+:::----:::::.....................................=+++* ",
|
||||
" #+.................=%%%..=%%%%%%%%%%%%%%%%%%=.::::---::::::...................................++++% ",
|
||||
" *..................=%%%....%%%%%%%%%%%%%%%%%-..::::---::::::.................................+++++ ",
|
||||
" *+..................:%%+....%%%%%%%%%%%#%%%%%:....:::---:::::::..............................+++++* ",
|
||||
" *....................*%+....%%%%%%%%=....%%%%......::::--:::::::............................=+++++* ",
|
||||
" %*....................+%%...%%%%%%%%%.....%%%%.......::::---:::::::.........................-+++++* ",
|
||||
" *-.....................#%%%%%%%%%%%%%.....%%%*........::::---:::::::.......................-++++++* ",
|
||||
" *.......................%%%%%%%%%%%%%-....%%%-.........::::---::::::::....................=++++++*% ",
|
||||
" #*.......................:#%%%%%%%%%%%%:..*%%*............::::--:::::::::.................++++++++* ",
|
||||
" #+.........................*%%%%%%%%%%%%%%%%#-.............::::---:::::::::...........:-++++++++++# ##*% ",
|
||||
" *:.........................*%%:+%+%%%%%%%%%*=...............::::---:::::::::::..........-****++++* #**:.# ",
|
||||
" *.............-****=.......*%%.#%.%%%%%%%#*-.................::::---::::::::::::............*###*# %#**-....* ",
|
||||
" *...........-#@@@@@#*......-**+%*.%%*****=...................:::::----::::::::::::::........* %#**+.......+ ",
|
||||
"%*.........-@@@@@@@@@%*........:+***%*:........................:::::-----::::::::::::::::::::+ *+..........- ",
|
||||
"#+.......-#@@@@@@@@@@@@*:..........-+*...........:*****=........:::::-----:::-:::::::::::::::+# %*+..........# ",
|
||||
"%=......+*@@@*@@@@@@@@@@*-.....................+*@@@@@@%*+.......:::::------:::::::::::::::::-* #*:........+ ",
|
||||
"#:.....+*@@@@*+@@@@@@@@@@*+..................#@@@@@@@@@@@%*-......::::--------::::::::::::::::* %*........- ",
|
||||
"*......*@@@@%@+%@@@@@@@@@@**.............-*@@@@@@@@@@@@@@@@*+......::::-------=-::::::::::::::* *.........**# ",
|
||||
"*.....+@@@@@+@==%@@@@@@@@@@#*.........=*@@@@@@@@@@@@@@@@@@@@%*.....:::::----+=*=*-::::::::::::* *.....*:..%@%* ",
|
||||
"*.....@@@@@@+=@==#@@@@@@@@@@%*=....+*#@@@@@@@@@@@@@@@@@@@@@@@@*.....:::::--+-+*--*--::::::::::* %-.....**+.*@@%* ",
|
||||
"#....#@@@@@@*=@===*@@@@@@@@@@@#***#@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.....::::-+--***--+---::::::::* *.....+%@@**@@@* ",
|
||||
"%....@@@@@@@+@@@+==+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::+=*******=----::::::* @=.....*@@@@%@@@*% ",
|
||||
"#:...@@@@@@@++@=====+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+%@@@@%*....::-+*******+*--------::-* #*@*.....-#@@@@@@@@*% ",
|
||||
"%=..-@@@@@@+#@@++++==+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+@@@@@@#+....:::+:+***--*-----------* **%@#......*@@@@@@@@@* ",
|
||||
" +..+@@@@@@+++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*==@@@@@@@@*:...:::-:-=**-=-----------=#*%@@+......=#@@@@@@@@@* ",
|
||||
" *..+@@@@@@++++++++++%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+====@+%@@@@@@*....:::+--*=++-----------+*%#+........*@@@@@@@@@%* ",
|
||||
" *..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+===%===+@@@@@@@#=...:::::+*+-------------*-:.........*@@@@@@@@@@* ",
|
||||
" %..=@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@======@===@@@@@@@@@*....:::::---------------*=........-+@@@@@@@@@@@* ",
|
||||
" ..:@@@@@@*++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@++====@@%==@@@@@@@@@%=...:::-::-------------=*++++++++++@@@@@@@@@@@* ",
|
||||
" -..%@@@@@@+++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++%+@+@@@@@@@@@@*....::-::-------------+%@%++++++#@@@@@@@@@@@*# ",
|
||||
" ..*@@@@@@@***++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++*+#+@@@@@@@@@@*....::*:::------------*@@@@*+#%@@@@@@@@@@@@#* ",
|
||||
" :.*@@@@@@@@#**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@%-....:%:::-----------=#@@@@@*#@@@@@@@@@@@@#* ",
|
||||
" #.=@@@@@@@@@@@@@@@@@@@@@@#*#@@@@@@@@@@@@@@@@@*++++++++++@@@@@@@@@@@+....%%%#+:----------*%@@@@@@#*@@@@@@@@@@*# ",
|
||||
" +.*@@@@@@@@@@@@@@@@@@@@*#%%*@@@@@@@@@@@@@@@@%++++++++++@@@@@@@@@@@*....=%*:::---------=*@@@@@@@@#*@@@@@@@@@* ",
|
||||
" #.+@@@@@@@@@@@@@@@@@@@@*%%%%@@@@@@@@@@@@@@@@@+++++++++*@@@@@@@@@@@*.....#:::::--------*@@@@@@@@@@#*@@@@@@@@* ",
|
||||
" @*.#@@@@@@@@@@@@@@@@@@@%*%%%*@@@@@@@@@@@@@@@@%++++++++@@@@@@@@@@@@*.....-:::::-------+#@@@@@@@@@@@#*@@@@@@%% ",
|
||||
" %++@@@@@@@@@@@@@@@@@@@@%***@@@@@@@@@@@@@@@@@@#*****+%@@@@@@@@@@@@*.....:::::::------*@@@@@@@@@@@@@%*@@@@@*% ",
|
||||
" *=*@@@@@@@@@@@@@@@@@@@@@@@@@@@*#@@@@@@@@@@@@@@#**#@@@@@@@@@@@@@@*......::::::-----*%@@@@@@@@@@@@@@%*%@@@* ",
|
||||
" *+%@@@@@@@@@@@@@@@@@@@@@@@@@*%%*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+......::::::----*%@@@@@@@@@@@@@@@@@*#@%% @****# ",
|
||||
" **%**##**#@@@@@@@@#@@@@@@@##%%*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=......:::::::--+#@@@@@@@@@@@@@@@@@@@@**%@@%@@@@**% ",
|
||||
" **@@@@@@%*%@@@@@@@#*@@@@@*%*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:.......::::::-=#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*# ",
|
||||
" @*@@@@@@@@@*#@@@@@@@@***%*#**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#........::::::=#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" #%@@@@@@@@@@*@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:........:::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%.........::::*%@@@@@%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@..........:::*%@@@@@%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" #%@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:..........:...:*#@@@*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" *@@@@@@@@@@@@#@@@@@@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@*.........-+...-+..-*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" ##@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@%*........++..-++=......*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" ##@@@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@%+......=*+-=++++........*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" %*@@@@@@@@@@@%**#@@@@@@@@@@@@@@@@*@@@@@@@@@@@*@@@@%*:....=**+++++++:........*%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" %*% @@@%***%@@@@@@@@@@@*%@@@@@@@@@@@*@@%*=.:=***++++++++-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*#% @*++++++**%@@@@@%*@@@@@@@@@@@@********++++++++++-.......=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" @% **#### *--+++++++++++***@@@@@@@@@@@@@**+++++++++++****++==++**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%# ",
|
||||
" *#*#####%*:....:=++++**%@@*@@@@@@@@@@@@@*+++++++++++=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++-...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" *######********##%*%@@@@@*@@@@@@@@@@@@*+=--........-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*#####* #* #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||
" **###*@ %** *#@@#@@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||
" #***#***# #*#@*@@@@@@@@@@@@#+..........-@@@@@@@@@@@@@@@@@@@@@## #***********# %*%@@@@@@@%* ",
|
||||
" %%* #**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*@ #**# **%@@@@#*@ ",
|
||||
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**# ",
|
||||
" **@@@@@@@@@@@@@@%**=...:*####@@@@@%#**# ",
|
||||
" #*@@@@@@@@@@@@@@@@#**+*#####*****#@ ",
|
||||
" #**@@@@@@@@@@@@@%*****######% ",
|
||||
" #***#%%@@%##*#% **###### ",
|
||||
" %##**##@ *#####* ",
|
||||
" %*###*# ",
|
||||
" %***% "
|
||||
],
|
||||
[
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" #*++*# ",
|
||||
" *=.....= ",
|
||||
" *-.......= ",
|
||||
" %*.........* ",
|
||||
" #-.........* ",
|
||||
" #..........+ ",
|
||||
" #..........* ",
|
||||
" *+.........* ",
|
||||
" %*........+# ",
|
||||
" #*......=# ",
|
||||
" #*+::=**# ",
|
||||
" @%******# ",
|
||||
" *******# ",
|
||||
" %********# ",
|
||||
" **********# ",
|
||||
" ****..-****# ",
|
||||
" #***-....+***# ",
|
||||
" #***......=***# ",
|
||||
" ***-.......:***# ",
|
||||
" @***..........***% ",
|
||||
" #**-...........*** ",
|
||||
" ***.............+** ",
|
||||
" **+..............=** ",
|
||||
" %**................-** ",
|
||||
" ***.................:** ",
|
||||
" **-..................:** ",
|
||||
" %**.....................** ",
|
||||
" #*+.......:..............** ",
|
||||
" **........-...............** ",
|
||||
" **........=..-.............*# ",
|
||||
" %*=........+.+...............+# ##*#% ",
|
||||
" #*.......-+**.................+# *:...=*% ",
|
||||
" *+.....:-+***-:................+# =.......*% ",
|
||||
" *-........**+...................*% #........:* ",
|
||||
" %*........+:*-:...................** :.........* ",
|
||||
" *+.......=..=.:....................* ..........+# ",
|
||||
" *:......:...-.......................* :.........+# ",
|
||||
" %*...........:.......................-# %##+.........*@ ",
|
||||
" %+.......:...:........................=% ###****+=--:*.........* ",
|
||||
" *........:.............................+% *:---...............:*.......*% ",
|
||||
" *........=..............................* +-.........................-*:...=*% ",
|
||||
" +.......=+.......................:-=+++******##% @#****-:.............................:*#***# ",
|
||||
" #:.....:-+++:................-+**+++==-----::--=+****#% %**+=...................................*% ",
|
||||
" #.........+..............-**+--------------::.......:+***%% **.......................................* ",
|
||||
" *.........:...........-*+::----------------::...........:+**% @*.......................................=# ",
|
||||
" *.........:.........=+:.--------------------:..............:+*## #+.......................................* ",
|
||||
" +.................+=.-----------------------:.................-**% *.......................................-* ",
|
||||
" -................:.-------------------------::..................:** *.......................................*# ",
|
||||
" #-...................:-----------------------::.....................*+.......................................* ",
|
||||
" %:.....................::---------------------::....................:-......................................+# ",
|
||||
" *........................:-------------------::....................=.......................................* ",
|
||||
" *:.......+................::-----------------::...................+......................................+* ",
|
||||
" #=.....+..................::=+++==----------::...................+.....................................:* ",
|
||||
" %*..:+..................=**#%%%#**+---------::........................................................+* ",
|
||||
" #=+.................-*#%%%%%%%%%%*=-------:::......................................................=*% ",
|
||||
" #+.................+*%%%%%%%%%%%%%%*-------::......................................................+* ",
|
||||
" %*.................+*%%%%%%%%%%%%%%%%#------:::....................................:..:............+*# ",
|
||||
" *.................+*%%%%%%%%%%%%%%%%%%*------:::................................-..-.-............:+* ",
|
||||
" *:.................*%%%%%%%%%%%%%%%%%%%%+-----::::................................+.+=.............++% ",
|
||||
" #=.................*%%%%%%%%%%%%%%%%%%%%%%=-----:::.................................**+............=+* ",
|
||||
" #*..................*%%%%%%%%%%%%%%%%%%%%%%*------:::.............................-=****-...........++# ",
|
||||
" *:.................:#%%%#%%%%%%%%%%%%%%%%%%*------::::...............................+*+...........++* ",
|
||||
" #+..................=%%%=.#%%%%%%%%%%%%%%%%%#+::----::::.............................+.*.:.........=++# ",
|
||||
" *...................=%%%...#%%%%%%%%%%%%%%%%%+:::----::::...........................-..+..........:++*% ",
|
||||
" #+...................:%%=....%%%%%%%%%%%%%%%%%=.:::----::::.............................=..........+++# ",
|
||||
" *.....................#%:....%%%%%%%%%%%%%%%%%...:::---:::::............................:.........+++* ",
|
||||
" #*.....................*%*...%%%%%%%%%#=..%%%%%....:::---:::::...........................:........=+++# ",
|
||||
" *:.....................-%%*=%%%%%%%%%%....-%%%%.....::::--::::::.........................:.......:++++ ",
|
||||
" *.......................*%%%%%%%%%%%%#.....%%%*......::::--::::::................................++++# ",
|
||||
" #+........................*%%%%%%%%%%%%....:%%%:.......::::--::::::..............................+++++ ",
|
||||
" *:.........................*%%%%%%%%%%%:...%%%*.........::::--::::::............................+++++# ",
|
||||
" *..............=**=........+%%%=%%%%%%%%#+%%%#:..........::::--:::::::.........................++++++ ",
|
||||
" *...........:*@@@@@*-......*%%=:%.%%%%%%%%%%#=............::::--::::::::......................++++++* ##*# ",
|
||||
" %=.........*@@@@@@@@@*:.....+*#.#%.%%%%%%%%%*=..............::::--::::::::....................+++++++% #**-.# ",
|
||||
" #-.......*@@@@@@@@@@@@*:......-***.%%#*****+.................:::---:::::::::................-+++++++*%**=....* ",
|
||||
" *......:*@@@@+@@@@@@@@@*-........+**%*.......................::::---::::::::::............-++++++++**+.......+ ",
|
||||
" *......*@@@@@+#@@@@@@@@@*:.........:+*........................::::----::::::::::........+*+++++++++*.........- ",
|
||||
" *.....*@@@@#@%=@@@@@@@@@@*:......................:+**=.........::::----:::::::::::........-+**++++*+..........# ",
|
||||
" *....-@@@@@#++==@@@@@@@@@@*:..................=*#@@@@@**:.......::::----:::::::::::::........*#***##*:........+ ",
|
||||
" *....@@@@@@@====+@@@@@@@@@@*:..............=@@@@@@@@@@@@*+......:::::-----:::=::::::::::.....* #*........- ",
|
||||
" *...=@@@@@@@=++==+@@@@@@@@@@*:.......:=*%@@@@@@@@@@@@@@@@%*......::::------::+:::::::::::::::* *.........**# ",
|
||||
" *...%@@@@@@+#@@%==*@@@@@@@@@@**=-:+**%@@@@@@@@@@@@@@@@@@@@@*:.....::::-------+:::::::::::::::* *.....*:..%@#* ",
|
||||
" *...@@@@@@@+#*=====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*-....:::::------+:::::::::::::::* %-.....**+.*@@%* ",
|
||||
" *..-@@@@@@#+@*++====#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*:....::::-----+*+-:::::::::::::* *.....+%@@**@@@* ",
|
||||
" *..=@@@@@@+++++++++==@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::--+=-*-=+::::::::::::* @=.....*@@@@#@@@*% ",
|
||||
" *..+@@@@@@++++++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::---+**-==-::::::::::* #*@*.....-*@@@@@@@@*% ",
|
||||
" #..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+#@@@@%+....::::*-=***+-*---::::::::***%@#......*@@@@@@@@@* ",
|
||||
" ..+@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++@@@@@@*:...:::-*********=----::::::*%@@+......=#@@@@@@@@@* ",
|
||||
" ..=@@@@@@*++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+===@%@@@@@@*....:::+-*****=*-------:::.*#+........*@@@@@@@@@%* ",
|
||||
" +.-@@@@@@@+++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*======%+@@@@@@#=...:::+:-***--+---------::+.........*@@@@@@@@@@* ",
|
||||
" #..%@@@@@@%***++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#======+==+%@@@@@@@*...::::+--*=-*-----------==.......-+@@@@@@@@@@@* ",
|
||||
" -.*@@@@@@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*++===+%@+==@@@@@@@@#:...::::*+*+*------------*+++++++++@@@@@@@@@@@* ",
|
||||
" *.*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+++++++@===@@@@@@@@@*...:::::-*--------------*%++++++#@@@@@@@@@@@*# ",
|
||||
" #:=@@@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@*++++++++@++@@@@@@@@@*....:::::+--------------*@@*+#%@@@@@@@@@@@@#* ",
|
||||
" *.*@@@@@@@@@@@@@@@@@@@@*##*@@@@@@@@@@@@@@@@#+++++++++++@@@@@@@@@#:...::=::+-------------+%@@@*#@@@@@@@@@@@@#* ",
|
||||
" @=*@@@@@@@@@@@@@@@@@@@*#%%%%@@@@@@@@@@@@@@@%+++++++++++@@@@@@@@@@+...::=::+-------------*@@@@@#*@@@@@@@@@@*# ",
|
||||
" *-#@@@@@@@@@@@@@@@@@@@*%%%#@@@@@@@@@@@@@@@@++++++++++%@@@@@@@@@@+....:#:::------------=#@@@@@@#*@@@@@@@@@* ",
|
||||
" **@@@@@@@@@@@@@@@@@@@#*###@@@@@@@@@@@@@@@@++++++++++@@@@@@@@@@@*....=%#:::-----------*@@@@@@@@#*@@@@@@@@# ",
|
||||
" #*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%++++++++*@@@@@@@@@@@*....#%%-::----------=#@@@@@@@@@#*@@@@@@%% ",
|
||||
" #*@@@@@@@@@@@@@@@@@@@@@@@@@#*#@@@@@@@@@@@@**++++++@@@@@@@@@@@@*.....#-:::----------*%@@@@@@@@@@#*@@@@@*% ",
|
||||
" **@@@@@@@@@@@@@@@*@@@@@@@%*%##@@@@@@@@@@@@%*****@@@@@@@@@@@@@*.....+:::::--------+#@@@@@@@@@@@@%*%@@@* ",
|
||||
" **@@@@@@@@@@@@@@@*@@@@@@*%#%#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+.....=:::::-------=*@@@@@@@@@@@@@@@*#@%# ****% ",
|
||||
" #**##**#@@@@@@@@@@**%@@*%###@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=.....::::::-------*%@@@@@@@@@@@@@@@@%**%@@%@@@@**@ ",
|
||||
" #*@@@@@@%*%@@@@@@@@@@#*****#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:......::::::-----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*# ",
|
||||
" @*@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.......::::::----*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" #%@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.......::::::---+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%........:::::::-*%@@%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=........:::::::*%@@%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" #%@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.........:::::=*#@@@*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" *@@@@@@@@@@@@#*%@@@@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@%...........:::*+..=*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" %#@@@@@@@@@@@%@**#@@@@@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@@%............:-+-......*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" ##@@@@@@@@@@@@@@%**%@@@@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@@#:..........=+++........*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" #*@@@@@@@@@@@@@@@****#@@@@@@@@@@@*@@@@@@@@@@@*@@@@@%*.........-*+++:........*%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" %*% @@@@*+++++*#%@@@@@@*%@@@@@@@@@@@*@@@%*:......-+*++++-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*#@ @*+++++++++++**#%*@@@@@@@@@@@@*%**...:=+***+++++-.......-*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" @% **###% *-=+++++++++++*%*@@@@@@@@@@@@@********+++++****++==++**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%# ",
|
||||
" *#*######*:....:=++++**%@@*@@@@@@@@@@@@@*+++++++++++=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++-...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" *######********##%*%@@@@@*@@@@@@@@@@@@*+==-........-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*#####* #* #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||
" **###*@ %** *#@@#@@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||
" #***#***# #*#@*@@@@@@@@@@@@#+..........=@@@@@@@@@@@@@@@@@@@@@## #***********# %*%@@@@@@@%* ",
|
||||
" %%# %**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*% #**# **%@@@@#*@ ",
|
||||
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**# ",
|
||||
" #*@@@@@@@@@@@@@@%**=...:*####@@@@@%#**# ",
|
||||
" #*@@@@@@@@@@@@@@@@#**+*#####*****#@ ",
|
||||
" #**@@@@@@@@@@@@@%*****######% ",
|
||||
" #***#%%@@%##*## #*###### ",
|
||||
" %##**#%@ *#####* ",
|
||||
" %*###*# ",
|
||||
" #***% "
|
||||
],
|
||||
[
|
||||
" #**#% ",
|
||||
" %*=...=# ",
|
||||
" %*.......+ ",
|
||||
" *:........# ",
|
||||
" *.........- ",
|
||||
" %+.........- ",
|
||||
" #=.........- ",
|
||||
" +.........+ ",
|
||||
" *.........* ",
|
||||
" #*.......*@ ",
|
||||
" #*:...-*% ",
|
||||
" #******# ",
|
||||
" %******% ",
|
||||
" ******** ",
|
||||
" ********* ",
|
||||
" #****-***** ",
|
||||
" ****...+**** ",
|
||||
" #***:....-***% ",
|
||||
" ***+......:***# ",
|
||||
" ***........:*** ",
|
||||
" #**+.........:*** ",
|
||||
" ***............**# ",
|
||||
" #**=............:**% ",
|
||||
" ***..............:**@ ",
|
||||
" @**=...............:** ",
|
||||
" %**.................-** ",
|
||||
" **=..................-*# ",
|
||||
" %**.......:............=*# ",
|
||||
" #*=.......=.............=* ",
|
||||
" **........+..............+*@ ",
|
||||
" %*+.....-..+..-............+* ",
|
||||
" **.......+.+.+..............*# ",
|
||||
" *+........***................*# ",
|
||||
" %*.......:****+:...............* ",
|
||||
" *+........+**=.................:* ",
|
||||
" *........=.---..................-# ",
|
||||
" #*.......:..-..-..................+% ",
|
||||
" *:..........-......................* ",
|
||||
" *.......:...-.......................* ",
|
||||
" #-.......-...-.......................=# %*# ",
|
||||
" *........=...:........................* @*=-=** ",
|
||||
" +.......:+.............................# -.....:*% ",
|
||||
" #:.....:=+++:...........................-% ........:* ",
|
||||
" *........:+..............................* =.........*%",
|
||||
" *.........:.................-=++***********### -.........=#",
|
||||
" #-......................:+*++-::------------=++***#% +.........-*",
|
||||
" #....................=*+..------------------::....+**** %*=-*%%##*###*****.........+#",
|
||||
" *.................-+:.:---------------------::.......:***% %##******::..........................*.........*%",
|
||||
" *..................:------------------------::..........:**% #**+--..................................:*.......-* ",
|
||||
" #...................:-----------------------::.............+*#- %*........................................+*.....=* ",
|
||||
" *....................:----------------------:...............=*# *:........................................=#**++*# ",
|
||||
" *.......::............:--------------------::................=*# @*.........................................* #@ ",
|
||||
" *.....+................:------------------::..................+*%%=........................................+# ",
|
||||
" %:..*..................::----------------::....................**.........................................*% ",
|
||||
" %++.....................:====------------::....................*........................................+# ",
|
||||
" #=...................:***###***=---------::...................:-........................................* ",
|
||||
" *=...................**%%%%%%%%%#*=-------:::..................+........................................+# ",
|
||||
" #+..................=*%%%%%%%%%%%%%#+-------::..................+........................................* ",
|
||||
" #+..................+*%%%%%%%%%%%%%%%%+------:::.................:.......................................*% ",
|
||||
" @*..................=*%%%%%%%%%%%%%%%%%%+------::........................................................-* ",
|
||||
" @*...................*%%%%%%%%%%%%%%%%%%%%------:::...................................:...................*% ",
|
||||
" *-..................+#%%%%%%%%%%%%%%%%%%%%%-----:::...................................:..................+* ",
|
||||
" #+...................*%%%%%%%%%%%%%%%%%%%%%%*-----:::..................................-.................=* ",
|
||||
" *....................*%%%:%%%%%%%%%%%%%%%%%%*=-----:::.................................=................:+* ",
|
||||
" *=...................:#%%...%%%%%%%%%%%%%%%%%#+-----::::................................+..-.............+* ",
|
||||
" %*....................:%%#...:%%%%%%%%%%%%%%%%%*:-----:::..............................:-*.+.............++# ",
|
||||
" *-.....................#%-....%%%%%%%%%%%%%%%%%*::----::::..............................***.............-+* ",
|
||||
" @*......................*%-...:%%%%%%%%%%%%%%%%%+:::----::::............................-***+-:..........++% ",
|
||||
" #=......................+%%..:%%%%%%%%%%%%%%%%%%..:::----::::............................+*+............++* ",
|
||||
" *........................%%%%%%%%%%%%%#....%%%%%...:::---:::::..........................:.*.=..........++*% ",
|
||||
" %+........................:%%%%%%%%%%%%-....#%%%%....:::---:::::...........................+..-........=++* ",
|
||||
" *:.........................+%%%%%%%%%%%:....#%%%......:::---:::::..........................=..........-++*@ ",
|
||||
" *............=*#%%*+........*%%%%%%%%%%+....%%%#.......:::--::::::.........................-.........:+++% ",
|
||||
" #*.........+@@@@@@@@@*......:#%%.%%%%%%%%-..-%%%=........:::--::::::........................:........:+++* ",
|
||||
" #-......:*@@@@@@@@@@@%+.....+#%#.%++%%%%%%%%%%%+..........:::--::::::.......................:........++++% ",
|
||||
" *......+*@@@@#@@@@@@@@#+.....-*+=%.*%%%%%%%%%#*...........::::--::::::..............................++++##*# ",
|
||||
" *.....=#@@@@@+#@@@@@@@@*-......-**.%%%*#%%%#*=.............::::--::::::............................+++++*-.# ",
|
||||
" *.....@@@@@#%%+@@@@@@@@@*:.......:**%*=-+++-................:::---::::::..........................+++++....* ",
|
||||
" +....@@@@@@**+=*@@@@@@@@@*.........:**.......................:::---:::::::......................:+++++-....+ ",
|
||||
" %=...+@@@@@@@====@@@@@@@@@@*..................................::::---:::::::....................=++++++.....- ",
|
||||
" #-...@@@@@@@#=#===@@@@@@@@@#*......................::..........::::---:::::::..................++++++++......# ",
|
||||
" *:..=@@@@@@@=%@@===@@@@@@@@@*=................-+**#%@#*=.......::::----::::::::..............=+++++++*.......+ ",
|
||||
" *...*@@@@@@+*+@====*@@@@@@@@@*+.........-++%@@@@@@@@@@@%*:......::::----:::::::::.......-.-=++++++++++.......- ",
|
||||
" *...%@@@@@@+@**+====%@@@@@@@@@%**+=***%@@@@@@@@@@@@@@@@@@*=......::::----::::::::::......-*+++++++++*.........**# ",
|
||||
" %...@@@@@@%++++++++==@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+.....::::-----:::::::::::.......+*+++++**.....*:..%@#* ",
|
||||
" ..:@@@@@@*++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+.....::::-----::::::::::::......-******=.....**+.*@@%* ",
|
||||
" ..-@@@@@@+++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=....::::------::::::::::::::...-# @#*.....+%@@**@@@* ",
|
||||
" ..-@@@@@@++++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*-....::::-------:::::::::::::::-* @=.....*@@@@%@@@*# ",
|
||||
" +.-@@@@@@#++++++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*....:::::-----=-::::::::::::::=##*@*.....-*@@@@@@@@*# ",
|
||||
" +.:@@@@@@@++++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*....::::---++*++:::::::::::::=*%@%......*@@@@@@@@@*@ ",
|
||||
" #..%@@@@@@#++++++@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+*@@@@*-...:::::-+--*=-*-:::::::::::+%@+......=#@@@@@@@@@* ",
|
||||
" +.*@@@@@@@@***#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++@@@@@@*....::::+--***--+-::::::::::*+........*@@@@@@@@@@* ",
|
||||
" *.*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=====@#@@@@@#=...::::++****+-*--:::::::::*........*@@@@@@@@@@*@ ",
|
||||
" %-+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=========*+@@@@@@*...::::=********=---::::::.*......-+@@@@@@@@@@@* ",
|
||||
" *-@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+======+===+@@@@@@@*:...:::+:+***+-*-----:::::.*+++++++@@@@@@@@@@@* ",
|
||||
" #=*@@@@@@@@@@@@@@@@@@@****@@@@@@@@@@@@@@@@@++++++#@@#=@@@@@@@@@*...:::+:-**=-==-------:::-++++++#@@@@@@@@@@@*# ",
|
||||
" *+@@@@@@@@@@@@@@@@@@@*%%%*@@@@@@@@@@@@@@@@+++++++%++=@@@@@@@@@*...::::*-+*-=+----------:+%*+#%@@@@@@@@@@@@#* ",
|
||||
" #+#@@@@@@@@@@@@@@@@@@*%%%%@@@@@@@@@@@@@@@@+++++++%#@+@@@@@@@@@*:...::::=***=------------*@@*#@@@@@@@@@@@@#* ",
|
||||
" **@@@@@@@@@@@@@@@@@@#*%%#@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@=...:::::----------------*@@@#*@@@@@@@@@@*% ",
|
||||
" **@@@@@@@@@@@@@@@@@@@**@@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@+...:::::---------------+%@@@@#*@@@@@@@@@* ",
|
||||
" %*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+++++++++++@@@@@@@@@@*....::::---------------*@@@@@@#*@@@@@@@@* ",
|
||||
" **@@@@@@@@@@@@@@*@@@@@@@@***@@@@@@@@@@@@+++++++++@@@@@@@@@@@*....:=::=-------------=#@@@@@@@#*@@@@@@%% ",
|
||||
" #*@@@@@@@@@@@@@@*@@@@@@*#%#%@@@@@@@@@@@**++++++#@@@@@@@@@@@*....:%+::-------------*@@@@@@@@@%*@@@@@*# ",
|
||||
" #*@@@@@@@@@@@@@@*%@@@#*%*%%@@@@@@@@@@@@****++#@@@@@@@@@@@@*...=%%%#-:-----------=#@@@@@@@@@@%*%@@@* ",
|
||||
" #*@@@@@@@@@@@@@@#***%%*%%@@@@@@@@@@@@@@@%##@@@@@@@@@@@@@@+....:%=:::-----------*@@@@@@@@@@@@@*#@%% ****% ",
|
||||
" %***#**#@@@@@@@@@@@@@@#*#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+.....#::::----------*%@@@@@@@@@@@@@@%**%@@%@@@@**% ",
|
||||
" #*@@@@@@%*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:.....+:::::--------=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@*% ",
|
||||
" *@@@@@@@@@*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#......=:::::-------=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" #%@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*......::::::-------*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:......::::::------*%#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *@@@@@@@@@@@@@**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.......:::::::----*%*####@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" #%@@@@@@@@@@@@*%**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-.......:::::::--=*%*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" *@@@@@@@@@@@@#@@%**@@@@@@@@@@@@@@@@@@@@*****#@@@@@@@@@@@@@:........:::::::-++*#*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" ##@@@@@@@@@@@%@@@%@#*%@@@@@@@@@@@@@@@**@@@@@%*%@@@@@@@@@@-.........:::::::*:...*######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" ##@@@@@@@@@@@@@@@#@%+*#@@@@@@@@@@@@*%@@@@@@@@*%@@@@@@@@:..........:::::+=.....*#####*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ",
|
||||
" %*@@@@@@@@@@@@@@@#*++++*%@@@@@@@@*%@@@@@@@@@@*@@@@@@%=...........:::.-......+%*###*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" #*# @@@@*++++++++*#%%@@*%@@@@@@@@@@@*%@@@%+.............-........+#@@@***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %##% @*+++++++++++++***@@@@@@@@@@@@*%%**...........=*+.......-*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" %% #*#### *-=+++++++++++*%#@@@@@@@@@@@@@*+-.....:-=+*****++===+**#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# ",
|
||||
" *%*#####%*:....:=++++**%@@*@@@@@@@@@@@@@*********++++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" %*######*.......-+**%@@@@@*@@@@@@@@@@@@#+++++++++=...#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" *######*********#%*%@@@@@*@@@@@@@@@@@@*++=-:.......-%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@* ",
|
||||
" #*#####* #*@ #*@@@@*@@@@@@@@@@@**............=@@@@@@@@@@@@@@@@@@@@@@@@#%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@%# ",
|
||||
" **###*% %** *#@@#%@@@@@@@@%*@#*...........=@@@@@@@@@@@@@@@@@@@@@@%* #*%%@@@@@@@@@%%**% *%@@@@@@@@@*% ",
|
||||
" #***#***# #*#@*@@@@@@@@@@@@#+..........-@@@@@@@@@@@@@@@@@@@@@%# %***********# #*%@@@@@@@%* ",
|
||||
" @#% %**@@@@@@@@@@@@@#*.........:%@@@@@@@@@@@@@@@@@@%*% #%%# **%@@@@#*% ",
|
||||
" %*@@@@@@@@@@@@@@*-........*@@@@@@@@@@@@@@@@%*% %****** ",
|
||||
" #*@@@@@@@@@@@@@@**:......*#*#@@@@@@@@@@%**% ",
|
||||
" **@@@@@@@@@@@@@@%**=...:*####@@@@@%#**# ",
|
||||
" #*@@@@@@@@@@@@@@@@#**+*#####******% ",
|
||||
" #**@@@@@@@@@@@@@%*****######% ",
|
||||
" #***#%@@@%%#**# #*###### ",
|
||||
" %*#**##% *#####* ",
|
||||
" %*###*% ",
|
||||
" #***% "
|
||||
]
|
||||
]
|
||||
74
apps/dashboard/src/beszel.ts
Normal file
74
apps/dashboard/src/beszel.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Beszel System Stats API TypeScript Types
|
||||
* For server monitoring and system statistics
|
||||
*/
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query"
|
||||
|
||||
const getApiBaseUrl = () => {
|
||||
const protocol = window.location.protocol
|
||||
const host = import.meta.env.VITE_API_HOST || window.location.host
|
||||
return `${protocol}//${host}`
|
||||
}
|
||||
|
||||
const API_BASE_URL = getApiBaseUrl()
|
||||
|
||||
// System Info
|
||||
export interface BeszelSystemInfo {
|
||||
name: string
|
||||
status: "up" | "down"
|
||||
info: {
|
||||
cpu: number
|
||||
ram: number
|
||||
disk: number
|
||||
}
|
||||
}
|
||||
|
||||
// Systems Response
|
||||
export interface BeszelSystemsResponse {
|
||||
lastUpdated: string
|
||||
systems: BeszelSystemInfo[]
|
||||
totalSystems: number
|
||||
}
|
||||
|
||||
// TanStack Query Options
|
||||
|
||||
/**
|
||||
* Query options for fetching Beszel system stats
|
||||
* Returns CPU, RAM, and disk usage for all monitored systems
|
||||
*/
|
||||
export function beszelSystemsQuery() {
|
||||
return queryOptions({
|
||||
queryKey: ["beszel", "systems"],
|
||||
queryFn: async (): Promise<BeszelSystemsResponse> => {
|
||||
const response = await fetch(`${API_BASE_URL}/api/beszel/systems`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch Beszel system stats")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
staleTime: 5 * 1000, // 5 seconds (system stats update frequently)
|
||||
gcTime: 30 * 1000, // 30 seconds
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to format percentage
|
||||
export function formatPercentage(value: number): string {
|
||||
return `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
// Helper function to get usage color based on percentage
|
||||
export function getUsageColor(percentage: number): string {
|
||||
if (percentage >= 90) return "text-red-500"
|
||||
if (percentage >= 75) return "text-orange-500"
|
||||
if (percentage >= 50) return "text-yellow-500"
|
||||
return "text-green-500"
|
||||
}
|
||||
|
||||
// Helper function to get usage background color
|
||||
export function getUsageBackgroundColor(percentage: number): string {
|
||||
if (percentage >= 90) return "bg-red-500"
|
||||
if (percentage >= 75) return "bg-orange-500"
|
||||
if (percentage >= 50) return "bg-yellow-500"
|
||||
return "bg-green-500"
|
||||
}
|
||||
14
apps/dashboard/src/components/tile.tsx
Normal file
14
apps/dashboard/src/components/tile.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import cn from "./lib/cn"
|
||||
|
||||
export function Tile({ children, className }: { children?: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-xl bg-neutral-200 dark:bg-neutral-900 flex flex-col justify-end items-start",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
2
apps/dashboard/src/env.d.ts
vendored
2
apps/dashboard/src/env.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_API_HOST: string;
|
||||
readonly VITE_DEFAULT_LATITUDE: string;
|
||||
readonly VITE_DEFAULT_LONGITUDE: string;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
input[type="range"].brightness-slider {
|
||||
@apply appearance-none w-full bg-transparent;
|
||||
}
|
||||
|
||||
25
apps/dashboard/src/kuromi.tsx
Normal file
25
apps/dashboard/src/kuromi.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import kuromiFrames from "./assets/kuromi-frames.json"
|
||||
|
||||
export function Kuromi() {
|
||||
const [frameIndex, setFrameIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setFrameIndex((prev) => (prev + 1) % kuromiFrames.length)
|
||||
}, 300)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const currentFrame = kuromiFrames[frameIndex]
|
||||
|
||||
return (
|
||||
<pre className="leading-none tracking-[0.6em] select-none font-mono text-black dark:text-white scale-[5%]">
|
||||
{currentFrame.map((line, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: frame lines don't have unique identifiers
|
||||
<div key={index}>{line}</div>
|
||||
))}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
359
apps/dashboard/src/light-control.tsx
Normal file
359
apps/dashboard/src/light-control.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import { ZIGBEE_DEVICE, type ZigbeeDeviceName, ZigbeeDeviceState, type ZigbeeDeviceStates } from "@eva/zigbee"
|
||||
import { useDrag } from "@use-gesture/react"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||
import { CloudyIcon, LightbulbOffIcon, type LucideIcon, MoonStarIcon } from "lucide-react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import cn from "./components/lib/cn"
|
||||
import { Tile } from "./components/tile"
|
||||
|
||||
const LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT = 36
|
||||
|
||||
// Store brightness as step (0-43) to match the 44 bars exactly
|
||||
// Step 0 = OFF, Steps 1-43 map to bars 42-0
|
||||
export const brightnessStepAtoms = atom({
|
||||
[ZIGBEE_DEVICE.deskLamp]: atom(0),
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(0),
|
||||
})
|
||||
|
||||
export const intermediateBrightnessStepAtoms = atom({
|
||||
[ZIGBEE_DEVICE.deskLamp]: atom(-1),
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(-1),
|
||||
})
|
||||
|
||||
const sceneAtom = atom<string | null>(null)
|
||||
|
||||
const DEVICE_FRIENDLY_NAMES = {
|
||||
[ZIGBEE_DEVICE.deskLamp]: "Desk Lamp",
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: "Floor Lamp",
|
||||
} as const
|
||||
|
||||
export type LightSceneConfig = {
|
||||
id: string
|
||||
name: string
|
||||
icon: LucideIcon
|
||||
deviceStates: Partial<ZigbeeDeviceStates>
|
||||
}
|
||||
|
||||
const DEFAULT_SCENES: Record<string, LightSceneConfig> = {
|
||||
"lights-off": {
|
||||
id: "lights-off",
|
||||
name: "Lights off",
|
||||
icon: LightbulbOffIcon,
|
||||
deviceStates: {
|
||||
[ZIGBEE_DEVICE.deskLamp]: {
|
||||
state: "OFF",
|
||||
brightness: 0,
|
||||
},
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||
state: "OFF",
|
||||
brightness: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
evening: {
|
||||
id: "evening",
|
||||
name: "Evening",
|
||||
icon: MoonStarIcon,
|
||||
deviceStates: {
|
||||
[ZIGBEE_DEVICE.deskLamp]: {
|
||||
state: "ON",
|
||||
brightness: 127,
|
||||
},
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||
state: "ON",
|
||||
brightness: 254,
|
||||
},
|
||||
},
|
||||
},
|
||||
gloomy: {
|
||||
id: "gloomy",
|
||||
name: "Gloomy",
|
||||
icon: CloudyIcon,
|
||||
deviceStates: {
|
||||
[ZIGBEE_DEVICE.deskLamp]: {
|
||||
state: "ON",
|
||||
brightness: 50,
|
||||
},
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||
state: "ON",
|
||||
brightness: 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
// Convert brightness (0-254) to step (0-43)
|
||||
// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
|
||||
export function brightnessToStep(brightness: number): number {
|
||||
if (brightness === 0) return 0
|
||||
// Map brightness 1-254 to steps 1-43
|
||||
return Math.max(1, Math.round((brightness / 254) * (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)))
|
||||
}
|
||||
|
||||
// Convert step (0-43) to brightness (0-254)
|
||||
// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
|
||||
export function stepToBrightness(step: number): number {
|
||||
if (step === 0) return 0
|
||||
// Map steps 1-43 to brightness 1-254
|
||||
return Math.max(1, Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 254))
|
||||
}
|
||||
|
||||
export function LightControlTile({
|
||||
deviceName,
|
||||
className,
|
||||
onRequestBrightnessStepChange,
|
||||
}: { deviceName: ZigbeeDeviceName; className?: string; onRequestBrightnessStepChange: (step: number) => void }) {
|
||||
const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
|
||||
// Map step to bar index for thumb position
|
||||
// Step 0 = OFF (no thumb shown, set to invalid index)
|
||||
// Step 1-43 map to bars 42-0
|
||||
const initialHighlightIndexStart =
|
||||
currentBrightnessStep === 0
|
||||
? LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 // No thumb (index out of range, but no bars highlighted)
|
||||
: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - currentBrightnessStep
|
||||
const touchContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const barRefs = useRef<(HTMLDivElement | null)[]>(
|
||||
Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }, () => null),
|
||||
)
|
||||
const setIntermediateBrightnessStep = useSetAtom(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
|
||||
const setScene = useSetAtom(sceneAtom)
|
||||
const store = useStore()
|
||||
|
||||
useEffect(() => {
|
||||
const brightnessStepAtom = store.get(brightnessStepAtoms)[deviceName]
|
||||
if (store.get(brightnessStepAtom) === currentBrightnessStep) {
|
||||
setIntermediateBrightnessStep(-1)
|
||||
}
|
||||
}, [currentBrightnessStep, deviceName, setIntermediateBrightnessStep, store])
|
||||
|
||||
function requestBrightnessStepChange(step: number) {
|
||||
onRequestBrightnessStepChange(step)
|
||||
setScene(null)
|
||||
}
|
||||
|
||||
const bind = useDrag(({ xy: [x], first, last }) => {
|
||||
if (!touchContainerRef.current) return
|
||||
|
||||
if (!first) {
|
||||
touchContainerRef.current.dataset.active = "true"
|
||||
}
|
||||
|
||||
if (last) {
|
||||
delete touchContainerRef.current.dataset.active
|
||||
let thumbIndex = -1
|
||||
for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
|
||||
const bar = barRefs.current[i]
|
||||
if (!bar) continue
|
||||
|
||||
const barRect = bar.getBoundingClientRect()
|
||||
|
||||
if (x >= barRect.left - 2 && x < barRect.right + 2 && thumbIndex === -1) {
|
||||
thumbIndex = i
|
||||
bar.dataset.thumb = "true"
|
||||
} else {
|
||||
delete bar.dataset.thumb
|
||||
}
|
||||
|
||||
delete bar.dataset.touched
|
||||
delete bar.dataset.touchProximity
|
||||
}
|
||||
|
||||
if (thumbIndex !== -1) {
|
||||
// Map bar index to step: bar 42 -> step 1, bar 0 -> step 43
|
||||
const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - thumbIndex
|
||||
requestBrightnessStepChange(step)
|
||||
} else {
|
||||
const firstElement = barRefs.current[barRefs.current.length - 1]
|
||||
const lastElement = barRefs.current[0]
|
||||
if (lastElement && x > lastElement.getBoundingClientRect().right) {
|
||||
lastElement.dataset.thumb = "true"
|
||||
setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
||||
if (last) {
|
||||
requestBrightnessStepChange(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
||||
}
|
||||
} else if (firstElement && x < firstElement.getBoundingClientRect().left) {
|
||||
firstElement.dataset.thumb = "true"
|
||||
setIntermediateBrightnessStep(0)
|
||||
if (last) {
|
||||
requestBrightnessStepChange(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let touchedIndex = -1
|
||||
for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
|
||||
const bar = barRefs.current[i]
|
||||
if (!bar) continue
|
||||
|
||||
const barRect = bar.getBoundingClientRect()
|
||||
|
||||
delete bar.dataset.thumb
|
||||
|
||||
if (x >= barRect.left - 2 && x < barRect.right + 2 && touchedIndex === -1) {
|
||||
touchedIndex = i
|
||||
|
||||
bar.dataset.touched = "true"
|
||||
bar.dataset.highlighted = "false"
|
||||
delete bar.dataset.touchProximity
|
||||
|
||||
const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - i - 1
|
||||
setIntermediateBrightnessStep(step)
|
||||
|
||||
if (barRefs.current[i - 1]) {
|
||||
barRefs.current[i - 1]!.dataset.touchProximity = "close"
|
||||
}
|
||||
if (barRefs.current[i - 2]) {
|
||||
barRefs.current[i - 2]!.dataset.touchProximity = "medium"
|
||||
}
|
||||
if (barRefs.current[i - 3]) {
|
||||
barRefs.current[i - 3]!.dataset.touchProximity = "far"
|
||||
}
|
||||
} else if (barRect.left < x) {
|
||||
if (bar.dataset.touched === "true") {
|
||||
bar.dataset.prevTouched = "true"
|
||||
} else {
|
||||
delete bar.dataset.prevTouched
|
||||
}
|
||||
bar.dataset.touched = "false"
|
||||
bar.dataset.highlighted = "true"
|
||||
if (touchedIndex >= 0) {
|
||||
const diff = i - touchedIndex
|
||||
if (diff === 1) {
|
||||
bar.dataset.touchProximity = "close"
|
||||
} else if (diff === 2) {
|
||||
bar.dataset.touchProximity = "medium"
|
||||
} else if (diff === 3) {
|
||||
bar.dataset.touchProximity = "far"
|
||||
} else {
|
||||
delete bar.dataset.touchProximity
|
||||
}
|
||||
} else {
|
||||
delete bar.dataset.touchProximity
|
||||
}
|
||||
} else if (barRect.right > x) {
|
||||
bar.dataset.highlighted = "false"
|
||||
bar.dataset.touched = "false"
|
||||
if (touchedIndex >= 0) {
|
||||
const diff = i - touchedIndex
|
||||
if (diff === 1) {
|
||||
bar.dataset.touchProximity = "close"
|
||||
} else if (diff === 2) {
|
||||
bar.dataset.touchProximity = "medium"
|
||||
} else if (diff === 3) {
|
||||
bar.dataset.touchProximity = "far"
|
||||
} else {
|
||||
delete bar.dataset.touchProximity
|
||||
}
|
||||
} else {
|
||||
delete bar.dataset.touchProximity
|
||||
}
|
||||
} else {
|
||||
bar.dataset.touched = "false"
|
||||
bar.dataset.highlighted = "false"
|
||||
delete bar.dataset.touchProximity
|
||||
}
|
||||
}
|
||||
|
||||
if (touchedIndex === -1) {
|
||||
const firstElement = barRefs.current[barRefs.current.length - 1]
|
||||
const lastElement = barRefs.current[0]
|
||||
if (lastElement && x > lastElement.getBoundingClientRect().right) {
|
||||
lastElement.dataset.thumb = "true"
|
||||
setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
|
||||
} else if (firstElement && x < firstElement.getBoundingClientRect().left) {
|
||||
firstElement.dataset.thumb = "true"
|
||||
setIntermediateBrightnessStep(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
|
||||
<div
|
||||
{...bind()}
|
||||
ref={touchContainerRef}
|
||||
className="group flex-1 flex flex-row-reverse justify-center items-center touch-none gap-x-1 w-full translate-y-4"
|
||||
>
|
||||
{Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }).map((_, index) => {
|
||||
const highlighted = index > initialHighlightIndexStart
|
||||
return (
|
||||
<div
|
||||
data-highlighted={highlighted}
|
||||
data-thumb={index === initialHighlightIndexStart}
|
||||
data-prev-touched={false}
|
||||
data-touched={false}
|
||||
ref={(ref) => {
|
||||
barRefs.current[index] = ref
|
||||
}}
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
key={index}
|
||||
className="transition-all transition-75 w-[2px] h-[2px] bg-neutral-400 rounded-full data-[highlighted=true]:h-2 data-[touch-proximity=close]:h-6 data-[touch-proximity=medium]:h-4 data-[touch-proximity=far]:h-2 data-[highlighted=true]:bg-teal-500 data-[touched=true]:h-8 data-[touched=true]:w-1 data-[touched=true]:bg-teal-500 data-[touched=true]:transition-none data-[prev-touched=true]:transition-none data-[thumb=true]:h-8 data-[thumb=true]:bg-teal-500"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="px-4 pb-2 w-full flex flex-row items-center justify-center space-x-2">
|
||||
<p className="tracking-tigher uppercase">{DEVICE_FRIENDLY_NAMES[deviceName]}</p>
|
||||
<BrightnessLevelLabel deviceName={deviceName} />
|
||||
</div>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
function BrightnessLevelLabel({ deviceName }: { deviceName: ZigbeeDeviceName }) {
|
||||
const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
|
||||
const intermediateBrightnessStep = useAtomValue(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
|
||||
|
||||
const step = intermediateBrightnessStep === -1 ? currentBrightnessStep : intermediateBrightnessStep
|
||||
|
||||
let label: string
|
||||
if (step === 0) {
|
||||
label = "OFF"
|
||||
} else {
|
||||
// Convert step to percentage: step 1 = ~2%, step 43 = 100%
|
||||
const brightnessPercentage = Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 100)
|
||||
label = `${brightnessPercentage}%`
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"flex-1 text-right font-bold font-mono tracking-tigher",
|
||||
step === 0 ? "text-neutral-400" : "text-teal-500 dark:text-teal-400",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export function LightSceneTile({
|
||||
className,
|
||||
onSceneChange,
|
||||
}: { className?: string; onSceneChange: (scene: LightSceneConfig) => void }) {
|
||||
const [activeSceneId, setActiveSceneId] = useAtom(sceneAtom)
|
||||
return (
|
||||
<Tile className={cn("h-full flex flex-col justify-start items-start p-1 gap-1", className)}>
|
||||
{Object.entries(DEFAULT_SCENES).map(([id, { icon: Icon, name }]) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveSceneId(id)
|
||||
onSceneChange(DEFAULT_SCENES[id])
|
||||
}}
|
||||
key={id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full gap-2 flex flex-row items-end justify-start h-full border tracking-tigher first:rounded-t-lg last:rounded-b-lg transition-all duration-150 active:transition-none",
|
||||
activeSceneId === id
|
||||
? "p-2 border-teal-500 text-teal-500 dark:text-teal-400 border-2 font-bold"
|
||||
: "p-[9px] text-neutral-400 border-neutral-300 dark:border-neutral-800 active:shadow-inner active:bg-neutral-300 dark:active:bg-teal-500 active:text-neutral-900 font-lighter",
|
||||
)}
|
||||
>
|
||||
<Icon size={16} strokeWidth={2} />
|
||||
<p className="text-md tracking-none leading-none uppercase">{name}</p>
|
||||
</button>
|
||||
))}
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
@@ -5,11 +5,17 @@
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query"
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"
|
||||
const getApiBaseUrl = () => {
|
||||
const protocol = window.location.protocol
|
||||
const host = import.meta.env.VITE_API_HOST || window.location.host
|
||||
return `${protocol}//${host}`
|
||||
}
|
||||
|
||||
const API_BASE_URL = getApiBaseUrl()
|
||||
|
||||
// Disruption Summary
|
||||
export interface DisruptionSummary {
|
||||
lineId: string
|
||||
lineId: TubeLine
|
||||
lineName: string
|
||||
mode: string
|
||||
status: string
|
||||
@@ -53,6 +59,27 @@ export enum StatusSeverity {
|
||||
ServiceClosed = 20,
|
||||
}
|
||||
|
||||
export enum TubeLine {
|
||||
Bakerloo = "bakerloo",
|
||||
Central = "central",
|
||||
Circle = "circle",
|
||||
District = "district",
|
||||
HammersmithCity = "hammersmith-city",
|
||||
Jubilee = "jubilee",
|
||||
Metropolitan = "metropolitan",
|
||||
Northern = "northern",
|
||||
Piccadilly = "piccadilly",
|
||||
Victoria = "victoria",
|
||||
WaterlooCity = "waterloo-city",
|
||||
LondonOverground = "london-overground",
|
||||
DLR = "dlr",
|
||||
Elizabeth = "elizabeth",
|
||||
Tram = "tram",
|
||||
Lioness = "lioness",
|
||||
Windrush = "windrush",
|
||||
Mildmay = "mildmay",
|
||||
}
|
||||
|
||||
// Helper function to get severity color
|
||||
export function getSeverityColor(severity: number): string {
|
||||
if (severity >= 10) return "green" // Good Service
|
||||
@@ -90,89 +117,27 @@ export function getSeverityLabel(severity: number): string {
|
||||
}
|
||||
|
||||
// Helper function to format line name for display
|
||||
export function formatLineName(lineId: string): string {
|
||||
const lineNames: Record<string, string> = {
|
||||
bakerloo: "Bakerloo",
|
||||
central: "Central",
|
||||
circle: "Circle",
|
||||
district: "District",
|
||||
"hammersmith-city": "Hammersmith & City",
|
||||
jubilee: "Jubilee",
|
||||
metropolitan: "Metropolitan",
|
||||
northern: "Northern",
|
||||
piccadilly: "Piccadilly",
|
||||
victoria: "Victoria",
|
||||
"waterloo-city": "Waterloo & City",
|
||||
"london-overground": "London Overground",
|
||||
dlr: "DLR",
|
||||
"elizabeth-line": "Elizabeth Line",
|
||||
tram: "Tram",
|
||||
export function formatLineName(line: TubeLine): string {
|
||||
const lineNames: Record<TubeLine, string> = {
|
||||
[TubeLine.Bakerloo]: "Bakerloo",
|
||||
[TubeLine.Central]: "Central",
|
||||
[TubeLine.Circle]: "Circle",
|
||||
[TubeLine.District]: "District",
|
||||
[TubeLine.HammersmithCity]: "H&C",
|
||||
[TubeLine.Jubilee]: "Jubilee",
|
||||
[TubeLine.Metropolitan]: "Met",
|
||||
[TubeLine.Northern]: "Northern",
|
||||
[TubeLine.Piccadilly]: "Piccadilly",
|
||||
[TubeLine.Victoria]: "Victoria",
|
||||
[TubeLine.WaterlooCity]: "W&C",
|
||||
[TubeLine.LondonOverground]: "London Overground",
|
||||
[TubeLine.DLR]: "DLR",
|
||||
[TubeLine.Elizabeth]: "Lizzie",
|
||||
[TubeLine.Tram]: "Tram",
|
||||
}
|
||||
return lineNames[lineId] || lineId
|
||||
return lineNames[line] || line
|
||||
}
|
||||
|
||||
// Map of tube lines to their official TfL colors (as Tailwind classes)
|
||||
export function getLineColor(lineId: string): string {
|
||||
const lineColors: Record<string, string> = {
|
||||
bakerloo: "bg-amber-700",
|
||||
central: "bg-red-600",
|
||||
circle: "bg-yellow-400",
|
||||
district: "bg-green-600",
|
||||
"hammersmith-city": "bg-pink-400",
|
||||
jubilee: "bg-slate-500",
|
||||
metropolitan: "bg-purple-800",
|
||||
northern: "bg-black",
|
||||
piccadilly: "bg-blue-900",
|
||||
victoria: "bg-sky-500",
|
||||
"waterloo-city": "bg-teal-500",
|
||||
"london-overground": "bg-orange-500",
|
||||
dlr: "bg-teal-600",
|
||||
"elizabeth-line": "bg-purple-600",
|
||||
tram: "bg-green-500",
|
||||
}
|
||||
return lineColors[lineId] || "bg-gray-500"
|
||||
}
|
||||
|
||||
// Map of status severity to border colors
|
||||
export function getStatusBorderColor(severity: number): string {
|
||||
const borderColors: Record<number, string> = {
|
||||
[StatusSeverity.SpecialService]: "border-gray-500",
|
||||
[StatusSeverity.Closed]: "border-red-700",
|
||||
[StatusSeverity.Suspended]: "border-red-600",
|
||||
[StatusSeverity.PartSuspended]: "border-red-500",
|
||||
[StatusSeverity.PlannedClosure]: "border-orange-600",
|
||||
[StatusSeverity.PartClosure]: "border-yellow-500",
|
||||
[StatusSeverity.SevereDelays]: "border-red-500",
|
||||
[StatusSeverity.ReducedService]: "border-orange-500",
|
||||
[StatusSeverity.BusService]: "border-blue-500",
|
||||
[StatusSeverity.MinorDelays]: "border-yellow-500",
|
||||
[StatusSeverity.GoodService]: "border-green-500",
|
||||
[StatusSeverity.PartClosed]: "border-orange-600",
|
||||
[StatusSeverity.ExitOnly]: "border-gray-600",
|
||||
[StatusSeverity.NoStepFreeAccess]: "border-gray-500",
|
||||
[StatusSeverity.ChangeOfFrequency]: "border-blue-400",
|
||||
[StatusSeverity.Diverted]: "border-purple-500",
|
||||
[StatusSeverity.NotRunning]: "border-red-600",
|
||||
[StatusSeverity.IssuesReported]: "border-yellow-400",
|
||||
[StatusSeverity.NoIssues]: "border-green-500",
|
||||
[StatusSeverity.Information]: "border-blue-400",
|
||||
[StatusSeverity.ServiceClosed]: "border-red-700",
|
||||
}
|
||||
return borderColors[severity] || "border-gray-400"
|
||||
}
|
||||
|
||||
// Helper function to check if there are any disruptions
|
||||
export function hasDisruptions(data: DisruptionsResponse): boolean {
|
||||
return data.disruptedLines > 0
|
||||
}
|
||||
|
||||
// Helper function to get critical disruptions (severe or worse)
|
||||
export function getCriticalDisruptions(data: DisruptionsResponse): DisruptionSummary[] {
|
||||
return data.disruptions.filter((d) => d.statusSeverity <= 6)
|
||||
}
|
||||
|
||||
// TanStack Query Options
|
||||
|
||||
/**
|
||||
* Query options for fetching current TfL disruptions
|
||||
* Returns disruptions across Tube, Overground, DLR, Elizabeth Line, and Tram
|
||||
|
||||
110
apps/dashboard/src/use-auto-theme.ts
Normal file
110
apps/dashboard/src/use-auto-theme.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useEffect } from "react"
|
||||
|
||||
interface SunTimes {
|
||||
sunrise: Date
|
||||
sunset: Date
|
||||
}
|
||||
|
||||
function calculateSunTimes(latitude: number, longitude: number, date: Date = new Date()): SunTimes {
|
||||
const julianDay = getJulianDay(date)
|
||||
const julianCentury = (julianDay - 2451545) / 36525
|
||||
|
||||
const geomMeanLongSun = (280.46646 + julianCentury * (36000.76983 + julianCentury * 0.0003032)) % 360
|
||||
const geomMeanAnomSun = 357.52911 + julianCentury * (35999.05029 - 0.0001537 * julianCentury)
|
||||
|
||||
const eccentEarthOrbit = 0.016708634 - julianCentury * (0.000042037 + 0.0000001267 * julianCentury)
|
||||
|
||||
const sunEqOfCtr =
|
||||
Math.sin(toRadians(geomMeanAnomSun)) * (1.914602 - julianCentury * (0.004817 + 0.000014 * julianCentury)) +
|
||||
Math.sin(toRadians(2 * geomMeanAnomSun)) * (0.019993 - 0.000101 * julianCentury) +
|
||||
Math.sin(toRadians(3 * geomMeanAnomSun)) * 0.000289
|
||||
|
||||
const sunTrueLong = geomMeanLongSun + sunEqOfCtr
|
||||
const sunAppLong = sunTrueLong - 0.00569 - 0.00478 * Math.sin(toRadians(125.04 - 1934.136 * julianCentury))
|
||||
|
||||
const meanObliqEcliptic = 23 + (26 + (21.448 - julianCentury * (46.815 + julianCentury * (0.00059 - julianCentury * 0.001813))) / 60) / 60
|
||||
|
||||
const obliqCorr = meanObliqEcliptic + 0.00256 * Math.cos(toRadians(125.04 - 1934.136 * julianCentury))
|
||||
|
||||
const sunDeclin = toDegrees(Math.asin(Math.sin(toRadians(obliqCorr)) * Math.sin(toRadians(sunAppLong))))
|
||||
|
||||
const varY = Math.tan(toRadians(obliqCorr / 2)) * Math.tan(toRadians(obliqCorr / 2))
|
||||
|
||||
const eqOfTime =
|
||||
4 *
|
||||
toDegrees(
|
||||
varY * Math.sin(2 * toRadians(geomMeanLongSun)) -
|
||||
2 * eccentEarthOrbit * Math.sin(toRadians(geomMeanAnomSun)) +
|
||||
4 * eccentEarthOrbit * varY * Math.sin(toRadians(geomMeanAnomSun)) * Math.cos(2 * toRadians(geomMeanLongSun)) -
|
||||
0.5 * varY * varY * Math.sin(4 * toRadians(geomMeanLongSun)) -
|
||||
1.25 * eccentEarthOrbit * eccentEarthOrbit * Math.sin(2 * toRadians(geomMeanAnomSun)),
|
||||
)
|
||||
|
||||
const haSunrise = toDegrees(Math.acos(Math.cos(toRadians(90.833)) / (Math.cos(toRadians(latitude)) * Math.cos(toRadians(sunDeclin))) - Math.tan(toRadians(latitude)) * Math.tan(toRadians(sunDeclin))))
|
||||
|
||||
const solarNoon = (720 - 4 * longitude - eqOfTime) / 1440
|
||||
const sunriseTime = solarNoon - (haSunrise * 4) / 1440
|
||||
const sunsetTime = solarNoon + (haSunrise * 4) / 1440
|
||||
|
||||
const sunrise = new Date(date)
|
||||
sunrise.setHours(0, 0, 0, 0)
|
||||
sunrise.setMinutes(sunriseTime * 1440)
|
||||
|
||||
const sunset = new Date(date)
|
||||
sunset.setHours(0, 0, 0, 0)
|
||||
sunset.setMinutes(sunsetTime * 1440)
|
||||
|
||||
return { sunrise, sunset }
|
||||
}
|
||||
|
||||
function getJulianDay(date: Date): number {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
let a = Math.floor((14 - month) / 12)
|
||||
let y = year + 4800 - a
|
||||
let m = month + 12 * a - 3
|
||||
|
||||
let jdn = day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045
|
||||
|
||||
return jdn + (hour - 12) / 24 + minute / 1440 + second / 86400
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return (degrees * Math.PI) / 180
|
||||
}
|
||||
|
||||
function toDegrees(radians: number): number {
|
||||
return (radians * 180) / Math.PI
|
||||
}
|
||||
|
||||
function isDarkMode(latitude: number, longitude: number): boolean {
|
||||
const now = new Date()
|
||||
const { sunrise, sunset } = calculateSunTimes(latitude, longitude, now)
|
||||
|
||||
return now < sunrise || now > sunset
|
||||
}
|
||||
|
||||
export function useAutoTheme(latitude: number, longitude: number) {
|
||||
useEffect(() => {
|
||||
const updateTheme = () => {
|
||||
const shouldBeDark = isDarkMode(latitude, longitude)
|
||||
|
||||
if (shouldBeDark) {
|
||||
document.documentElement.classList.add("dark")
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark")
|
||||
}
|
||||
}
|
||||
|
||||
updateTheme()
|
||||
|
||||
const interval = setInterval(updateTheme, 60000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [latitude, longitude])
|
||||
}
|
||||
@@ -26,7 +26,13 @@ import {
|
||||
Wind,
|
||||
} from "lucide-react"
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000"
|
||||
const getApiBaseUrl = () => {
|
||||
const protocol = window.location.protocol
|
||||
const host = import.meta.env.VITE_API_HOST || window.location.host
|
||||
return `${protocol}//${host}`
|
||||
}
|
||||
|
||||
const API_BASE_URL = getApiBaseUrl()
|
||||
|
||||
export const DEFAULT_LATITUDE = Number(import.meta.env.VITE_DEFAULT_LATITUDE) || 37.7749
|
||||
export const DEFAULT_LONGITUDE = Number(import.meta.env.VITE_DEFAULT_LONGITUDE) || -122.4194
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
|
||||
142
bun.lock
142
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "monorepo",
|
||||
@@ -12,10 +13,13 @@
|
||||
},
|
||||
},
|
||||
"apps/backend": {
|
||||
"name": "backend",
|
||||
"name": "@eva/backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@eva/jrpc": "workspace:*",
|
||||
"@eva/zigbee": "workspace:*",
|
||||
"hono": "^4.6.14",
|
||||
"mqtt": "^5.14.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
@@ -23,10 +27,14 @@
|
||||
},
|
||||
},
|
||||
"apps/dashboard": {
|
||||
"name": "dashboard",
|
||||
"name": "@eva/dashboard",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@eva/jrpc": "workspace:*",
|
||||
"@eva/zigbee": "workspace:*",
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"jotai": "^2.10.3",
|
||||
"lucide-react": "^0.546.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -43,6 +51,28 @@
|
||||
"vite": "^6.0.1",
|
||||
},
|
||||
},
|
||||
"packages/jrpc": {
|
||||
"name": "@eva/jrpc",
|
||||
"dependencies": {
|
||||
"@eva/zigbee": "workspace:*",
|
||||
"nanoid": "^5.1.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
"packages/zigbee": {
|
||||
"name": "@eva/zigbee",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
@@ -79,6 +109,8 @@
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||
@@ -155,6 +187,14 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
|
||||
|
||||
"@eva/backend": ["@eva/backend@workspace:apps/backend"],
|
||||
|
||||
"@eva/dashboard": ["@eva/dashboard@workspace:apps/dashboard"],
|
||||
|
||||
"@eva/jrpc": ["@eva/jrpc@workspace:packages/jrpc"],
|
||||
|
||||
"@eva/zigbee": ["@eva/zigbee@workspace:packages/zigbee"],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
@@ -167,6 +207,8 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
@@ -245,8 +287,18 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
||||
|
||||
"@types/readable-stream": ["@types/readable-stream@4.0.22", "", { "dependencies": { "@types/node": "*" } }, "sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
|
||||
|
||||
"@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
@@ -259,26 +311,36 @@
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||
|
||||
"backend": ["backend@workspace:apps/backend"],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"bl": ["bl@6.1.4", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-ZV/9asSuknOExbM/zPPA8z00lc1ihPKWaStHkkQrxHNeYx+yY+TmF+v80dpv2G0mv3HVXBu7ryoAsxbFFhf4eg=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"broker-factory": ["broker-factory@3.1.10", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1", "worker-factory": "^7.0.46" } }, "sha512-BzqK5GYFhvVFvO13uzPN0SCiOsOQuhMUbsGvTXDJMA2/N4GvIlFdxEuueE+60Zk841bBU5G3+fl2cqYEo0wgGg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
|
||||
|
||||
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
@@ -289,6 +351,10 @@
|
||||
|
||||
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"commist": ["commist@3.2.0", "", {}, "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
@@ -297,8 +363,6 @@
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"dashboard": ["dashboard@workspace:apps/dashboard"],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
||||
@@ -315,8 +379,14 @@
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-unique-numbers": ["fast-unique-numbers@9.0.24", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1" } }, "sha512-Dv0BYn4waOWse94j16rsZ5w/0zoaCa74O3q6IZjMqaXbtT92Q+Sb6pPk+phGzD8Xh+nueQmSRI3tSCaHKidzKw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
@@ -339,8 +409,16 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||
|
||||
"hono": ["hono@4.10.2", "", {}, "sha512-p6fyzl+mQo6uhESLxbF5WlBOAJMDh36PljwlKtP5V1v09NxlqGru3ShK+4wKhSuhuYf8qxMmrivHOa/M7q0sMg=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
@@ -361,6 +439,8 @@
|
||||
|
||||
"jotai": ["jotai@2.15.0", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw=="],
|
||||
|
||||
"js-sdsl": ["js-sdsl@4.3.0", "", {}, "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
@@ -373,7 +453,7 @@
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
|
||||
|
||||
@@ -383,13 +463,19 @@
|
||||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"mqtt": ["mqtt@5.14.1", "", { "dependencies": { "@types/readable-stream": "^4.0.21", "@types/ws": "^8.18.1", "commist": "^3.2.0", "concat-stream": "^2.0.0", "debug": "^4.4.1", "help-me": "^5.0.0", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.2", "number-allocator": "^1.0.14", "readable-stream": "^4.7.0", "rfdc": "^1.4.1", "socks": "^2.8.6", "split2": "^4.2.0", "worker-timers": "^8.0.23", "ws": "^8.18.3" }, "bin": { "mqtt_pub": "build/bin/pub.js", "mqtt_sub": "build/bin/sub.js", "mqtt": "build/bin/mqtt.js" } }, "sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw=="],
|
||||
|
||||
"mqtt-packet": ["mqtt-packet@9.0.2", "", { "dependencies": { "bl": "^6.0.8", "debug": "^4.3.4", "process-nextick-args": "^2.0.1" } }, "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="],
|
||||
|
||||
@@ -397,6 +483,8 @@
|
||||
|
||||
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
|
||||
|
||||
"number-allocator": ["number-allocator@1.0.14", "", { "dependencies": { "debug": "^4.3.1", "js-sdsl": "4.3.0" } }, "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
@@ -431,6 +519,10 @@
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||
@@ -441,16 +533,22 @@
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
@@ -461,12 +559,20 @@
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
|
||||
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -489,6 +595,10 @@
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
@@ -501,21 +611,35 @@
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"worker-factory": ["worker-factory@7.0.46", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1" } }, "sha512-Sr1hq2FMgNa04UVhYQacsw+i58BtMimzDb4+CqYphZ97OfefRpURu0UZ+JxMr/H36VVJBfuVkxTK7MytsanC3w=="],
|
||||
|
||||
"worker-timers": ["worker-timers@8.0.25", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1", "worker-timers-broker": "^8.0.11", "worker-timers-worker": "^9.0.11" } }, "sha512-X7Z5dmM6PlrEnaadtFQOyXHGD/IysPA3HZzaC2koqsU1VI+RvyGmjiiLiUBQixK8PH5R7ilkOzZupWskNRaXmA=="],
|
||||
|
||||
"worker-timers-broker": ["worker-timers-broker@8.0.11", "", { "dependencies": { "@babel/runtime": "^7.28.4", "broker-factory": "^3.1.10", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1", "worker-timers-worker": "^9.0.11" } }, "sha512-uwhxKru8BI9m2tsogxr2fB6POZ8LB2xH+Pu3R0mvQnAZLPgLD6K3IX4LNKPTEgTJ/j5VsuQPB+gLI1NBNKkPlg=="],
|
||||
|
||||
"worker-timers-worker": ["worker-timers-worker@9.0.11", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1", "worker-factory": "^7.0.46" } }, "sha512-pArb5xtgHWImYpXhjg1OFv7JFG0ubmccb73TFoXHXjG830fFj+16N57q9YeBnZX52dn+itRrMoJZ9HaZBVzDaA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
|
||||
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
eva:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# Vite environment variables for dashboard build
|
||||
VITE_API_URL: ${VITE_API_URL:-http://localhost:8000}
|
||||
VITE_DEFAULT_LATITUDE: ${VITE_DEFAULT_LATITUDE:-37.7749}
|
||||
VITE_DEFAULT_LONGITUDE: ${VITE_DEFAULT_LONGITUDE:--122.4194}
|
||||
image: eva-monorepo:latest
|
||||
container_name: eva
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# Uncomment and configure if you need to pass environment variables
|
||||
# env_file:
|
||||
# - apps/backend/.env
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "monorepo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"workspaces": ["apps/*"],
|
||||
"workspaces": ["apps/*", "packages/*"],
|
||||
"scripts": {
|
||||
"dev": "bun run --elide-lines 0 --filter './apps/*' dev",
|
||||
"build": "bun run --filter '*' build",
|
||||
|
||||
34
packages/jrpc/.gitignore
vendored
Normal file
34
packages/jrpc/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
packages/jrpc/README.md
Normal file
15
packages/jrpc/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# @eva/jrpc
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
40
packages/jrpc/index.ts
Normal file
40
packages/jrpc/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ZigbeeDeviceName, ZigbeeDeviceStates } from "@eva/zigbee"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
export type JrpcRequestId = string & { __brand: "JrpcRequestId" }
|
||||
|
||||
export type JrpcSchema = {
|
||||
subscribeToDevice(p: { deviceName: ZigbeeDeviceName }): true
|
||||
unsubscribeFromDevice(p: { deviceName: ZigbeeDeviceName }): true
|
||||
setDeviceState(p: { deviceName: ZigbeeDeviceName; state: unknown }): true
|
||||
showDeviceState<DeviceName extends ZigbeeDeviceName>(
|
||||
p: { [K in ZigbeeDeviceName]: { deviceName: K; state: ZigbeeDeviceStates[K] } }[DeviceName],
|
||||
): ZigbeeDeviceStates[ZigbeeDeviceName]
|
||||
}
|
||||
|
||||
export type JrpcRequest<Method extends keyof JrpcSchema = keyof JrpcSchema> = {
|
||||
[M in keyof JrpcSchema]: {
|
||||
id: JrpcRequestId
|
||||
jsonrpc: "2.0"
|
||||
method: M
|
||||
params: Parameters<JrpcSchema[M]>[0]
|
||||
}
|
||||
}[Method]
|
||||
|
||||
export type JrpcResponse<Method extends keyof JrpcSchema = keyof JrpcSchema> = {
|
||||
[M in keyof JrpcSchema]:
|
||||
| {
|
||||
id: JrpcRequestId
|
||||
jsonrpc: "2.0"
|
||||
result: ReturnType<JrpcSchema[M]>
|
||||
}
|
||||
| {
|
||||
id: JrpcRequestId
|
||||
jsonrpc: "2.0"
|
||||
error: string
|
||||
}
|
||||
}[Method]
|
||||
|
||||
export function newJrpcRequestId(): JrpcRequestId {
|
||||
return nanoid()
|
||||
}
|
||||
15
packages/jrpc/package.json
Normal file
15
packages/jrpc/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@eva/jrpc",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@eva/zigbee": "workspace:*",
|
||||
"nanoid": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
29
packages/jrpc/tsconfig.json
Normal file
29
packages/jrpc/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
34
packages/zigbee/.gitignore
vendored
Normal file
34
packages/zigbee/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
packages/zigbee/README.md
Normal file
15
packages/zigbee/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# @eva/zigbee
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
23
packages/zigbee/index.ts
Normal file
23
packages/zigbee/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const ZIGBEE_BASE_TOPIC = "nexus"
|
||||
|
||||
export const ZIGBEE_DEVICE = {
|
||||
deskLamp: "desk_lamp",
|
||||
livingRoomFloorLamp: "living_room_floor_lamp",
|
||||
} as const
|
||||
|
||||
export type ZigbeeDeviceStates = {
|
||||
[ZIGBEE_DEVICE.deskLamp]: {
|
||||
state: "ON" | "OFF"
|
||||
brightness: number
|
||||
}
|
||||
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
|
||||
brightness: number
|
||||
state: "ON" | "OFF"
|
||||
}
|
||||
}
|
||||
|
||||
export type ZigbeeDeviceName = keyof ZigbeeDeviceStates
|
||||
|
||||
export type ZigbeeDeviceState<DeviceName extends ZigbeeDeviceName = ZigbeeDeviceName> = ZigbeeDeviceStates[DeviceName]
|
||||
|
||||
export const ALL_ZIGBEE_DEVICE_NAMES: ZigbeeDeviceName[] = Object.values(ZIGBEE_DEVICE)
|
||||
11
packages/zigbee/package.json
Normal file
11
packages/zigbee/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@eva/zigbee",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
29
packages/zigbee/tsconfig.json
Normal file
29
packages/zigbee/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user