mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-11 12:01:07 +00:00
Compare commits
11 Commits
copilot/fi
...
logs-front
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1afcfcd32 | ||
|
|
29e6ada90e | ||
|
|
ff1745fccc | ||
|
|
59aaf86104 | ||
|
|
52bc692c79 | ||
|
|
29de405912 | ||
|
|
a723171ef2 | ||
|
|
cfe193cf60 | ||
|
|
a8e6557b09 | ||
|
|
a517aabdd8 | ||
|
|
10c8828aa5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
/hass_frontend/
|
/hass_frontend/
|
||||||
|
/logs/dist/
|
||||||
/translations/
|
/translations/
|
||||||
|
|
||||||
# yarn
|
# yarn
|
||||||
|
|||||||
@@ -327,6 +327,20 @@ module.exports.config = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
logs({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||||
|
return {
|
||||||
|
name: "logs" + nameSuffix(latestBuild),
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.logs_dir, "src/entrypoint.ts"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.logs_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
landingPage({ isProdBuild, latestBuild }) {
|
landingPage({ isProdBuild, latestBuild }) {
|
||||||
return {
|
return {
|
||||||
name: "landing-page" + nameSuffix(latestBuild),
|
name: "landing-page" + nameSuffix(latestBuild),
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ gulp.task(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-logs",
|
||||||
|
gulp.parallel("clean-translations", async () =>
|
||||||
|
deleteSync([paths.logs_output_root, paths.build_dir])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-landing-page",
|
"clean-landing-page",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", async () =>
|
||||||
|
|||||||
@@ -245,6 +245,24 @@ gulp.task(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const LOGS_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"gen-pages-logs-dev",
|
||||||
|
genPagesDevTask(LOGS_PAGE_ENTRIES, paths.logs_dir, paths.logs_output_root)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"gen-pages-logs-prod",
|
||||||
|
genPagesProdTask(
|
||||||
|
LOGS_PAGE_ENTRIES,
|
||||||
|
paths.logs_dir,
|
||||||
|
paths.logs_output_root,
|
||||||
|
paths.logs_output_latest,
|
||||||
|
paths.logs_output_es5
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
|
|||||||
@@ -202,6 +202,16 @@ gulp.task("copy-static-gallery", async () => {
|
|||||||
copyMdiIcons(paths.gallery_output_static);
|
copyMdiIcons(paths.gallery_output_static);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task("copy-static-logs", async () => {
|
||||||
|
// Copy app static files
|
||||||
|
fs.copySync(polyPath("public/static"), paths.logs_output_static);
|
||||||
|
|
||||||
|
copyFonts(paths.logs_output_static);
|
||||||
|
copyTranslations(paths.logs_output_static);
|
||||||
|
copyLocaleData(paths.logs_output_static);
|
||||||
|
copyMdiIcons(paths.logs_output_static);
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task("copy-static-landing-page", async () => {
|
gulp.task("copy-static-landing-page", async () => {
|
||||||
// Copy landing-page static files
|
// Copy landing-page static files
|
||||||
fs.copySync(
|
fs.copySync(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import "./download-translations.js";
|
|||||||
import "./entry-html.js";
|
import "./entry-html.js";
|
||||||
import "./fetch-nightly-translations.js";
|
import "./fetch-nightly-translations.js";
|
||||||
import "./gallery.js";
|
import "./gallery.js";
|
||||||
|
import "./logs.js";
|
||||||
import "./gather-static.js";
|
import "./gather-static.js";
|
||||||
import "./gen-icons-json.js";
|
import "./gen-icons-json.js";
|
||||||
import "./hassio.js";
|
import "./hassio.js";
|
||||||
|
|||||||
39
build-scripts/gulp/logs.js
Normal file
39
build-scripts/gulp/logs.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import gulp from "gulp";
|
||||||
|
import "./clean.js";
|
||||||
|
import "./entry-html.js";
|
||||||
|
import "./gather-static.js";
|
||||||
|
import "./gen-icons-json.js";
|
||||||
|
import "./translations.js";
|
||||||
|
import "./rspack.js";
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-logs",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-logs",
|
||||||
|
gulp.parallel(
|
||||||
|
"gen-icons-json",
|
||||||
|
"gen-pages-logs-dev",
|
||||||
|
"build-translations",
|
||||||
|
"build-locale-data"
|
||||||
|
),
|
||||||
|
"copy-static-logs",
|
||||||
|
"rspack-dev-server-logs"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-logs",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-logs",
|
||||||
|
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||||
|
"copy-static-logs",
|
||||||
|
"rspack-prod-logs",
|
||||||
|
"gen-pages-logs-prod"
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
createGalleryConfig,
|
createGalleryConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createLandingPageConfig,
|
createLandingPageConfig,
|
||||||
|
createLogsConfig,
|
||||||
} from "../rspack.cjs";
|
} from "../rspack.cjs";
|
||||||
|
|
||||||
const bothBuilds = (createConfigFunc, params) => [
|
const bothBuilds = (createConfigFunc, params) => [
|
||||||
@@ -204,6 +205,25 @@ gulp.task("rspack-prod-gallery", () =>
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task("rspack-dev-server-logs", () =>
|
||||||
|
runDevServer({
|
||||||
|
compiler: rspack(
|
||||||
|
createLogsConfig({ isProdBuild: false, latestBuild: true })
|
||||||
|
),
|
||||||
|
contentBase: paths.logs_output_root,
|
||||||
|
port: 5647,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("rspack-prod-logs", () =>
|
||||||
|
prodBuild(
|
||||||
|
bothBuilds(createLogsConfig, {
|
||||||
|
isProdBuild: true,
|
||||||
|
isStatsBuild: env.isStatsBuild(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task("rspack-watch-landing-page", () => {
|
gulp.task("rspack-watch-landing-page", () => {
|
||||||
// This command will run forever because we don't close compiler
|
// This command will run forever because we don't close compiler
|
||||||
rspack(
|
rspack(
|
||||||
|
|||||||
@@ -59,5 +59,11 @@ module.exports = {
|
|||||||
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
||||||
hassio_publicPath: "/api/hassio/app",
|
hassio_publicPath: "/api/hassio/app",
|
||||||
|
|
||||||
|
logs_dir: path.resolve(__dirname, "../logs"),
|
||||||
|
logs_output_root: path.resolve(__dirname, "../logs/dist"),
|
||||||
|
logs_output_static: path.resolve(__dirname, "../logs/dist/static"),
|
||||||
|
logs_output_latest: path.resolve(__dirname, "../logs/dist/frontend_latest"),
|
||||||
|
logs_output_es5: path.resolve(__dirname, "../logs/dist/frontend_es5"),
|
||||||
|
|
||||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,6 +302,11 @@ const createHassioConfig = ({
|
|||||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
|
const createLogsConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||||
|
createRspackConfig(
|
||||||
|
bundle.config.logs({ isProdBuild, latestBuild, isStatsBuild })
|
||||||
|
);
|
||||||
|
|
||||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
@@ -311,6 +316,7 @@ module.exports = {
|
|||||||
createCastConfig,
|
createCastConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createGalleryConfig,
|
createGalleryConfig,
|
||||||
|
createLogsConfig,
|
||||||
createRspackConfig,
|
createRspackConfig,
|
||||||
createLandingPageConfig,
|
createLandingPageConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
9
logs/.dockerignore
Normal file
9
logs/.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
dist/
|
||||||
|
src/
|
||||||
|
node_modules/
|
||||||
|
*.md
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
docker-compose.yaml
|
||||||
|
backend/ha-logs-proxy
|
||||||
|
backend/README.md
|
||||||
47
logs/Dockerfile
Normal file
47
logs/Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
ARG BUILD_FROM
|
||||||
|
FROM $BUILD_FROM AS base
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
jq \
|
||||||
|
curl \
|
||||||
|
go
|
||||||
|
|
||||||
|
# Install Home Assistant CLI
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
ARG CLI_VERSION
|
||||||
|
RUN curl -Lso /usr/bin/ha \
|
||||||
|
"https://github.com/home-assistant/cli/releases/download/${CLI_VERSION}/ha_${BUILD_ARCH}" \
|
||||||
|
&& chmod a+x /usr/bin/ha
|
||||||
|
|
||||||
|
# Build Go backend
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backend/go.mod backend/go.sum* ./
|
||||||
|
RUN go mod download || true
|
||||||
|
|
||||||
|
COPY backend/*.go ./
|
||||||
|
RUN CGO_ENABLED=0 go build -o ha-logs-proxy .
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM $BUILD_FROM
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
jq \
|
||||||
|
curl
|
||||||
|
|
||||||
|
# Copy HA CLI from base
|
||||||
|
COPY --from=base /usr/bin/ha /usr/bin/ha
|
||||||
|
|
||||||
|
# Copy Go backend
|
||||||
|
COPY --from=base /app/ha-logs-proxy /usr/bin/ha-logs-proxy
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
|
||||||
|
# Expose port for backend (5642 = LOGB)
|
||||||
|
EXPOSE 5642
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["/usr/bin/ha-logs-proxy"]
|
||||||
113
logs/README.md
Normal file
113
logs/README.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Home Assistant CLI Docker Container
|
||||||
|
|
||||||
|
A simple multi-architecture Docker container with the Home Assistant CLI installed.
|
||||||
|
|
||||||
|
## Development Usage
|
||||||
|
|
||||||
|
The CLI container is integrated into the `script/develop-logs` workflow. Both flags are required:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev server with CLI container (requires remote_api add-on)
|
||||||
|
script/develop-logs -c http://192.168.1.2 -t your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
When started with credentials, the container runs a Go backend that proxies HA CLI logs commands. The backend API is available at `http://localhost:5642`.
|
||||||
|
|
||||||
|
**Frontend Features:**
|
||||||
|
- Dropdown menu to select log provider (core, supervisor, host, audio, dns, multicast)
|
||||||
|
- Follow mode with WebSocket streaming (`ha core logs --follow`)
|
||||||
|
- Manual refresh to fetch latest logs
|
||||||
|
- Download logs as text file
|
||||||
|
- Line wrapping toggle
|
||||||
|
- Auto-scroll to bottom when following
|
||||||
|
- Error display with retry
|
||||||
|
|
||||||
|
**API Endpoints:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all endpoints
|
||||||
|
curl http://localhost:5642/api/logs
|
||||||
|
|
||||||
|
# Get static logs
|
||||||
|
curl http://localhost:5642/api/logs/core
|
||||||
|
curl http://localhost:5642/api/logs/supervisor
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:5642/health
|
||||||
|
|
||||||
|
# WebSocket streaming (requires websocat or browser)
|
||||||
|
websocat ws://localhost:5642/api/logs/core/follow
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also execute HA CLI commands directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it ha-cli-dev ha info
|
||||||
|
docker exec -it ha-cli-dev ha supervisor info
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop everything with Ctrl+C (both the dev server and backend will stop automatically).
|
||||||
|
|
||||||
|
### Getting API Token
|
||||||
|
|
||||||
|
1. Install the [remote_api add-on](https://github.com/home-assistant/addons/tree/master/remote_api) in Home Assistant
|
||||||
|
2. Check the add-on logs for the generated token
|
||||||
|
3. Use the token with the `-t` flag
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
### Local Build (Single Architecture)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build \
|
||||||
|
--build-arg BUILD_FROM=alpine:3.22 \
|
||||||
|
--build-arg BUILD_ARCH=amd64 \
|
||||||
|
--build-arg CLI_VERSION=4.42.0 \
|
||||||
|
-t ha-cli:local \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Architecture Build
|
||||||
|
|
||||||
|
The `build.yaml` configuration is designed for use with Home Assistant's build system. For local multi-arch builds, use Docker Buildx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||||
|
--build-arg BUILD_FROM=alpine:3.22 \
|
||||||
|
--build-arg CLI_VERSION=4.42.0 \
|
||||||
|
-t ha-cli:latest \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Run CLI Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm ha-cli:local ha help
|
||||||
|
docker run --rm ha-cli:local ha supervisor info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Shell
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it --rm ha-cli:local
|
||||||
|
# Then run: ha <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm ha-cli ha help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update CLI Version
|
||||||
|
|
||||||
|
Edit the `CLI_VERSION` in `build.yaml` or pass it as a build argument:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg CLI_VERSION=4.43.0 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for latest versions at: https://github.com/home-assistant/cli/releases
|
||||||
1
logs/backend/.gitignore
vendored
Normal file
1
logs/backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ha-logs-proxy
|
||||||
108
logs/backend/README.md
Normal file
108
logs/backend/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# HA Logs Proxy Backend
|
||||||
|
|
||||||
|
A Go backend that proxies Home Assistant CLI logs commands through a secure HTTP API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Only allows `logs` commands (security-restricted)
|
||||||
|
- GET endpoints for static logs
|
||||||
|
- WebSocket endpoints for streaming logs (follow mode)
|
||||||
|
- CORS enabled for frontend integration
|
||||||
|
- Simple JSON API
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/logs
|
||||||
|
|
||||||
|
List all available endpoints.
|
||||||
|
|
||||||
|
### GET /api/logs/core
|
||||||
|
|
||||||
|
Get Home Assistant core logs.
|
||||||
|
|
||||||
|
### GET /api/logs/supervisor
|
||||||
|
|
||||||
|
Get Supervisor logs.
|
||||||
|
|
||||||
|
### GET /api/logs/host
|
||||||
|
|
||||||
|
Get host system logs.
|
||||||
|
|
||||||
|
### GET /api/logs/audio
|
||||||
|
|
||||||
|
Get audio logs.
|
||||||
|
|
||||||
|
### GET /api/logs/dns
|
||||||
|
|
||||||
|
Get DNS logs.
|
||||||
|
|
||||||
|
### GET /api/logs/multicast
|
||||||
|
|
||||||
|
Get multicast logs.
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
|
||||||
|
Health check endpoint.
|
||||||
|
|
||||||
|
**Response format (all log endpoints):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"output": "log content here...",
|
||||||
|
"error": "error message if any"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WS /api/logs/*/follow
|
||||||
|
|
||||||
|
WebSocket endpoints for streaming logs in real-time.
|
||||||
|
|
||||||
|
Available endpoints:
|
||||||
|
- `WS /api/logs/core/follow` - Stream core logs
|
||||||
|
- `WS /api/logs/supervisor/follow` - Stream supervisor logs
|
||||||
|
- `WS /api/logs/host/follow` - Stream host logs
|
||||||
|
- `WS /api/logs/audio/follow` - Stream audio logs
|
||||||
|
- `WS /api/logs/dns/follow` - Stream DNS logs
|
||||||
|
- `WS /api/logs/multicast/follow` - Stream multicast logs
|
||||||
|
|
||||||
|
Each WebSocket message contains a single log line as plain text. The connection streams output from `ha {component} logs --follow` command.
|
||||||
|
|
||||||
|
## Running Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
The server starts on port 5642 (LOGB) by default. Override with `PORT` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=3000 go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o ha-logs-proxy
|
||||||
|
./ha-logs-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List endpoints
|
||||||
|
curl http://localhost:5642/api/logs
|
||||||
|
|
||||||
|
# Get core logs
|
||||||
|
curl http://localhost:5642/api/logs/core
|
||||||
|
|
||||||
|
# Get supervisor logs
|
||||||
|
curl http://localhost:5642/api/logs/supervisor
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:5642/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Integration
|
||||||
|
|
||||||
|
The backend is designed to run in the same container as the HA CLI, sharing access to the `ha` command.
|
||||||
5
logs/backend/go.mod
Normal file
5
logs/backend/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/home-assistant/frontend/logs/backend
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require github.com/gorilla/websocket v1.5.3
|
||||||
2
logs/backend/go.sum
Normal file
2
logs/backend/go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
372
logs/backend/main.go
Normal file
372
logs/backend/main.go
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogsResponse struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // Allow all origins (CORS)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHACommand(args []string) (string, error) {
|
||||||
|
cmd := exec.Command("ha", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamHACommandToWS(conn *websocket.Conn, args []string) error {
|
||||||
|
cmd := exec.Command("ha", args...)
|
||||||
|
|
||||||
|
// Get stdout pipe
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start command
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and send output line by line
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for command to finish
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCoreLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute ha core logs command
|
||||||
|
output, err := executeHACommand([]string{"core", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
log.Printf("Error encoding response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSupervisorLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"supervisor", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHostLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"host", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAudioLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"audio", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDNSLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"dns", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMulticastLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"multicast", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCoreLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to core logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"core", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming core logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from core logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSupervisorLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to supervisor logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"supervisor", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming supervisor logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from supervisor logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHostLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to host logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"host", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming host logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from host logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAudioLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to audio logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"audio", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming audio logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from audio logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDNSLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to dns logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"dns", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming dns logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from dns logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMulticastLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to multicast logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"multicast", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming multicast logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from multicast logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := map[string]string{
|
||||||
|
"core": "/api/logs/core",
|
||||||
|
"supervisor": "/api/logs/supervisor",
|
||||||
|
"host": "/api/logs/host",
|
||||||
|
"audio": "/api/logs/audio",
|
||||||
|
"dns": "/api/logs/dns",
|
||||||
|
"multicast": "/api/logs/multicast",
|
||||||
|
"health": "/health",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
json.NewEncoder(w).Encode(endpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "5642"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register handlers
|
||||||
|
http.HandleFunc("/api/logs", listEndpoints)
|
||||||
|
http.HandleFunc("/api/logs/core", handleCoreLogs)
|
||||||
|
http.HandleFunc("/api/logs/supervisor", handleSupervisorLogs)
|
||||||
|
http.HandleFunc("/api/logs/host", handleHostLogs)
|
||||||
|
http.HandleFunc("/api/logs/audio", handleAudioLogs)
|
||||||
|
http.HandleFunc("/api/logs/dns", handleDNSLogs)
|
||||||
|
http.HandleFunc("/api/logs/multicast", handleMulticastLogs)
|
||||||
|
|
||||||
|
// WebSocket follow endpoints
|
||||||
|
http.HandleFunc("/api/logs/core/follow", handleCoreLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/supervisor/follow", handleSupervisorLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/host/follow", handleHostLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/audio/follow", handleAudioLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/dns/follow", handleDNSLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/multicast/follow", handleMulticastLogsFollow)
|
||||||
|
|
||||||
|
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Starting HA Logs Proxy on port %s", port)
|
||||||
|
log.Printf("Available endpoints:")
|
||||||
|
log.Printf(" GET /api/logs - List all endpoints")
|
||||||
|
log.Printf(" GET /api/logs/core - Core logs")
|
||||||
|
log.Printf(" GET /api/logs/supervisor - Supervisor logs")
|
||||||
|
log.Printf(" GET /api/logs/host - Host logs")
|
||||||
|
log.Printf(" GET /api/logs/audio - Audio logs")
|
||||||
|
log.Printf(" GET /api/logs/dns - DNS logs")
|
||||||
|
log.Printf(" GET /api/logs/multicast - Multicast logs")
|
||||||
|
log.Printf(" WS /api/logs/*/follow - Stream logs (WebSocket)")
|
||||||
|
log.Printf(" GET /health - Health check")
|
||||||
|
|
||||||
|
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||||
|
log.Fatalf("Server failed to start: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
logs/build.yaml
Normal file
19
logs/build.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
image: ghcr.io/home-assistant/{arch}-hassio-cli
|
||||||
|
build_from:
|
||||||
|
aarch64: ghcr.io/home-assistant/aarch64-base:3.22
|
||||||
|
armhf: ghcr.io/home-assistant/armhf-base:3.22
|
||||||
|
armv7: ghcr.io/home-assistant/armv7-base:3.22
|
||||||
|
amd64: ghcr.io/home-assistant/amd64-base:3.22
|
||||||
|
i386: ghcr.io/home-assistant/i386-base:3.22
|
||||||
|
args:
|
||||||
|
CLI_VERSION: 4.42.0
|
||||||
|
cosign:
|
||||||
|
enabled: true
|
||||||
|
repository_name: home-assistant/cli
|
||||||
|
repository_owner: home-assistant
|
||||||
|
labels:
|
||||||
|
org.opencontainers.image.title: Home Assistant CLI
|
||||||
|
org.opencontainers.image.description: Home Assistant CLI for container environments
|
||||||
|
org.opencontainers.image.source: https://github.com/home-assistant/cli
|
||||||
|
org.opencontainers.image.licenses: Apache-2.0
|
||||||
16
logs/docker-compose.yaml
Normal file
16
logs/docker-compose.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
ha-cli:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
BUILD_FROM: alpine:3.22
|
||||||
|
BUILD_ARCH: amd64
|
||||||
|
CLI_VERSION: 4.42.0
|
||||||
|
image: ha-cli:local
|
||||||
|
ports:
|
||||||
|
- "5642:5642"
|
||||||
|
environment:
|
||||||
|
- PORT=5642
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
34
logs/src/auto-theme.ts
Normal file
34
logs/src/auto-theme.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { darkSemanticColorStyles } from "../../src/resources/theme/color/semantic.globals";
|
||||||
|
import { darkColorStyles } from "../../src/resources/theme/color/color.globals";
|
||||||
|
|
||||||
|
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
function applyTheme(dark: boolean) {
|
||||||
|
const el = document.documentElement;
|
||||||
|
if (dark) {
|
||||||
|
el.setAttribute("dark", "");
|
||||||
|
} else {
|
||||||
|
el.removeAttribute("dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dark theme styles wrapped in media query
|
||||||
|
// This runs after append-ha-style has loaded the base theme
|
||||||
|
const styleElement = document.createElement("style");
|
||||||
|
styleElement.id = "auto-theme-dark";
|
||||||
|
styleElement.textContent = `
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
${darkSemanticColorStyles.cssText}
|
||||||
|
${darkColorStyles.cssText}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
// Append to head to ensure it comes after base styles
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
// Apply theme on initial load
|
||||||
|
applyTheme(mql.matches);
|
||||||
|
|
||||||
|
// Listen for theme changes
|
||||||
|
mql.addEventListener("change", (e) => {
|
||||||
|
applyTheme(e.matches);
|
||||||
|
});
|
||||||
8
logs/src/entrypoint.ts
Normal file
8
logs/src/entrypoint.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import "./logs-app";
|
||||||
|
|
||||||
|
// Load base styles first, then apply theme
|
||||||
|
import("../../src/resources/append-ha-style").then(() => {
|
||||||
|
import("./auto-theme");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(document.createElement("logs-app"));
|
||||||
37
logs/src/html/index.html.template
Normal file
37
logs/src/html/index.html.template
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#03a9f4" />
|
||||||
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<title>Home Assistant Logs</title>
|
||||||
|
<% for (const entry of latestEntryJS) { %>
|
||||||
|
<script type="module" src="<%= entry %>"></script>
|
||||||
|
<% } %>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
|
color: var(--primary-text-color, #212121);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
background-color: var(--primary-background-color, #111111);
|
||||||
|
color: var(--primary-text-color, #e1e1e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Roboto, Noto, sans-serif;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
24
logs/src/logs-app.ts
Normal file
24
logs/src/logs-app.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { LitElement, css, html } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "./logs-viewer";
|
||||||
|
|
||||||
|
@customElement("logs-app")
|
||||||
|
class LogsApp extends LitElement {
|
||||||
|
render() {
|
||||||
|
return html`<logs-viewer></logs-viewer>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"logs-app": LogsApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
538
logs/src/logs-viewer.ts
Normal file
538
logs/src/logs-viewer.ts
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
import {
|
||||||
|
mdiArrowCollapseDown,
|
||||||
|
mdiChevronDown,
|
||||||
|
mdiCircle,
|
||||||
|
mdiDownload,
|
||||||
|
mdiRefresh,
|
||||||
|
mdiWrap,
|
||||||
|
mdiWrapDisabled,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||||
|
import "../../src/components/ha-ansi-to-html";
|
||||||
|
import type { HaAnsiToHtml } from "../../src/components/ha-ansi-to-html";
|
||||||
|
import "../../src/components/ha-button";
|
||||||
|
import "../../src/components/ha-button-menu";
|
||||||
|
import "../../src/components/ha-card";
|
||||||
|
import "../../src/components/ha-icon-button";
|
||||||
|
import "../../src/components/ha-list-item";
|
||||||
|
import "../../src/components/ha-spinner";
|
||||||
|
import "../../src/components/ha-svg-icon";
|
||||||
|
|
||||||
|
// Data types
|
||||||
|
interface LogProvider {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("logs-viewer")
|
||||||
|
export class LogsViewer extends LitElement {
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _selectedLogProvider?: string;
|
||||||
|
|
||||||
|
@state() private _logProviders: LogProvider[] = [];
|
||||||
|
|
||||||
|
@state() private _loading = false;
|
||||||
|
|
||||||
|
@state() private _wrapLines = true;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _newLogsIndicator?: boolean;
|
||||||
|
|
||||||
|
@query(".error-log") private _logElement?: HTMLElement;
|
||||||
|
|
||||||
|
@query("#scroll-top-marker") private _scrollTopMarkerElement?: HTMLElement;
|
||||||
|
|
||||||
|
@query("#scroll-bottom-marker")
|
||||||
|
private _scrollBottomMarkerElement?: HTMLElement;
|
||||||
|
|
||||||
|
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
|
||||||
|
|
||||||
|
@state() private _scrolledToBottomController =
|
||||||
|
new IntersectionController<boolean>(this, {
|
||||||
|
callback(this: IntersectionController<boolean>, entries) {
|
||||||
|
return entries[0].isIntersecting;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
@state() private _scrolledToTopController =
|
||||||
|
new IntersectionController<boolean>(this, {});
|
||||||
|
|
||||||
|
private _ws: WebSocket | null = null;
|
||||||
|
|
||||||
|
private _apiUrl = `http://${window.location.hostname}:5642`;
|
||||||
|
|
||||||
|
private async _fetchLogs(): Promise<void> {
|
||||||
|
if (!this._selectedLogProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loading = true;
|
||||||
|
this._error = undefined;
|
||||||
|
|
||||||
|
// Stop any existing websocket
|
||||||
|
this._stopFollowing();
|
||||||
|
|
||||||
|
// Clear existing logs
|
||||||
|
this._ansiToHtmlElement?.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, fetch the latest logs
|
||||||
|
const response = await fetch(
|
||||||
|
`${this._apiUrl}/api/logs/${this._selectedLogProvider}`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logText = data.output || "";
|
||||||
|
|
||||||
|
// Parse and display initial logs
|
||||||
|
if (logText.trim()) {
|
||||||
|
this._ansiToHtmlElement?.parseTextToColoredPre(logText);
|
||||||
|
|
||||||
|
// Add divider line
|
||||||
|
this._ansiToHtmlElement?.parseLineToColoredPre(
|
||||||
|
"--- Live logs start here ---"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loading = false;
|
||||||
|
|
||||||
|
// Scroll to bottom after loading
|
||||||
|
this._scrollToBottom();
|
||||||
|
|
||||||
|
// Start streaming
|
||||||
|
this._startFollowing();
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Error loading logs: ${err}`;
|
||||||
|
this._loading = false;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error fetching logs:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchLogProviders(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this._apiUrl}/api/logs`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const providers = await response.json();
|
||||||
|
|
||||||
|
// Define the order (matching backend registration order)
|
||||||
|
const order = ["core", "supervisor", "host", "audio", "dns", "multicast"];
|
||||||
|
|
||||||
|
// Convert object to array of providers, filter out health endpoint, and sort
|
||||||
|
this._logProviders = Object.entries(providers)
|
||||||
|
.filter(([key]) => key !== "health")
|
||||||
|
.map(([key]) => ({
|
||||||
|
key,
|
||||||
|
name: key.charAt(0).toUpperCase() + key.slice(1),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key));
|
||||||
|
|
||||||
|
// Set default provider once loaded
|
||||||
|
if (this._logProviders.length > 0 && !this._selectedLogProvider) {
|
||||||
|
this._selectedLogProvider = this._logProviders[0].key;
|
||||||
|
await this._fetchLogs();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to load log providers: ${err}`;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error fetching log providers:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._fetchLogProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._stopFollowing();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated() {
|
||||||
|
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
|
||||||
|
this._scrolledToTopController.observe(this._scrollTopMarkerElement!);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated() {
|
||||||
|
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
|
||||||
|
this._newLogsIndicator = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectProvider(ev: Event) {
|
||||||
|
const target = ev.currentTarget as any;
|
||||||
|
this._selectedLogProvider = target.provider;
|
||||||
|
this._fetchLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _refresh() {
|
||||||
|
this._fetchLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleLineWrap() {
|
||||||
|
this._wrapLines = !this._wrapLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scrollToBottom(): void {
|
||||||
|
if (this._logElement) {
|
||||||
|
this._newLogsIndicator = false;
|
||||||
|
this._logElement.scrollTo(0, this._logElement.scrollHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startFollowing() {
|
||||||
|
if (!this._selectedLogProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._stopFollowing();
|
||||||
|
this._error = undefined;
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${wsProtocol}//${window.location.hostname}:5642/api/logs/${this._selectedLogProvider}/follow`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this._ws.onopen = () => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log("WebSocket connected");
|
||||||
|
};
|
||||||
|
|
||||||
|
this._ws.onmessage = (event) => {
|
||||||
|
const scrolledToBottom = this._scrolledToBottomController.value;
|
||||||
|
|
||||||
|
// Add the new line to the display
|
||||||
|
this._ansiToHtmlElement?.parseLineToColoredPre(event.data);
|
||||||
|
|
||||||
|
// Auto-scroll if user is at bottom
|
||||||
|
if (scrolledToBottom && this._logElement) {
|
||||||
|
this._scrollToBottom();
|
||||||
|
} else {
|
||||||
|
this._newLogsIndicator = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._ws.onerror = (error) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
this._error = "WebSocket connection error";
|
||||||
|
};
|
||||||
|
|
||||||
|
this._ws.onclose = () => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log("WebSocket disconnected");
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to start following logs: ${err}`;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error starting WebSocket:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopFollowing() {
|
||||||
|
if (this._ws) {
|
||||||
|
this._ws.close();
|
||||||
|
this._ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _downloadLogs() {
|
||||||
|
if (!this._selectedLogProvider || !this._ansiToHtmlElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the text content from the logs
|
||||||
|
const logText =
|
||||||
|
this._ansiToHtmlElement.shadowRoot?.querySelector("pre")?.textContent ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (!logText.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blob from log text
|
||||||
|
const blob = new Blob([logText], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create download link and trigger it
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${this._selectedLogProvider}-logs-${Date.now()}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const currentProvider = this._logProviders.find(
|
||||||
|
(p) => p.key === this._selectedLogProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="toolbar">
|
||||||
|
<ha-button-menu>
|
||||||
|
<ha-button slot="trigger" appearance="filled">
|
||||||
|
<ha-svg-icon slot="end" .path=${mdiChevronDown}></ha-svg-icon>
|
||||||
|
${currentProvider?.name || "Select Provider"}
|
||||||
|
</ha-button>
|
||||||
|
${this._logProviders.map(
|
||||||
|
(provider) => html`
|
||||||
|
<ha-list-item
|
||||||
|
?selected=${provider.key === this._selectedLogProvider}
|
||||||
|
.provider=${provider.key}
|
||||||
|
@click=${this._selectProvider}
|
||||||
|
>
|
||||||
|
${provider.name}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-button-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="error-log-intro">
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="card-header">${currentProvider?.name || "Logs"}</h1>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDownload}
|
||||||
|
@click=${this._downloadLogs}
|
||||||
|
.label=${"Download logs"}
|
||||||
|
.disabled=${!this._ansiToHtmlElement}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${this._wrapLines ? mdiWrapDisabled : mdiWrap}
|
||||||
|
@click=${this._toggleLineWrap}
|
||||||
|
.label=${this._wrapLines ? "Full width" : "Wrap lines"}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiRefresh}
|
||||||
|
@click=${this._refresh}
|
||||||
|
.label=${"Refresh"}
|
||||||
|
.disabled=${!this._selectedLogProvider}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content error-log">
|
||||||
|
<div id="scroll-top-marker"></div>
|
||||||
|
${this._loading
|
||||||
|
? html`<div>Loading logs...</div>`
|
||||||
|
: this._error
|
||||||
|
? html`<div class="error">${this._error}</div>`
|
||||||
|
: nothing}
|
||||||
|
<ha-ansi-to-html
|
||||||
|
?wrap-disabled=${!this._wrapLines}
|
||||||
|
></ha-ansi-to-html>
|
||||||
|
<div id="scroll-bottom-marker"></div>
|
||||||
|
</div>
|
||||||
|
<ha-button
|
||||||
|
class="new-logs-indicator ${classMap({
|
||||||
|
visible:
|
||||||
|
(this._newLogsIndicator &&
|
||||||
|
!this._scrolledToBottomController.value) ||
|
||||||
|
false,
|
||||||
|
})}"
|
||||||
|
size="small"
|
||||||
|
appearance="filled"
|
||||||
|
@click=${this._scrollToBottom}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiArrowCollapseDown}
|
||||||
|
slot="start"
|
||||||
|
></ha-svg-icon>
|
||||||
|
Scroll down
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiArrowCollapseDown}
|
||||||
|
slot="end"
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
|
${this._ws && !this._error
|
||||||
|
? html`<div class="live-indicator">
|
||||||
|
<ha-svg-icon .path=${mdiCircle}></ha-svg-icon>
|
||||||
|
Live
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles: CSSResultGroup = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
padding: var(--ha-space-2) var(--ha-space-4);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--ha-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-log-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-card {
|
||||||
|
padding-top: var(--ha-space-2);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||||
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
|
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||||
|
letter-spacing: -0.012em;
|
||||||
|
line-height: var(--ha-line-height-expanded);
|
||||||
|
display: block;
|
||||||
|
margin-block-start: 0px;
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: calc(100% - 150px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-log {
|
||||||
|
position: relative;
|
||||||
|
font-family: var(--ha-font-family-code);
|
||||||
|
clear: both;
|
||||||
|
text-align: start;
|
||||||
|
padding-top: var(--ha-space-4);
|
||||||
|
padding-bottom: var(--ha-space-4);
|
||||||
|
overflow-y: scroll;
|
||||||
|
min-height: var(--error-log-card-height, calc(100vh - 244px));
|
||||||
|
max-height: var(--error-log-card-height, calc(100vh - 244px));
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-log > div {
|
||||||
|
padding: 0 var(--ha-space-4);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
padding: var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-logs-indicator {
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--ha-space-1);
|
||||||
|
left: var(--ha-space-1);
|
||||||
|
height: 0;
|
||||||
|
transition: height 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-logs-indicator.visible {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breathe {
|
||||||
|
from {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline-end: var(--ha-space-4);
|
||||||
|
border-top-right-radius: var(--ha-space-2);
|
||||||
|
border-top-left-radius: var(--ha-space-2);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-2);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-indicator ha-svg-icon {
|
||||||
|
animation: breathe 1s cubic-bezier(0.5, 0, 1, 1) infinite alternate;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 870px) {
|
||||||
|
.error-log {
|
||||||
|
min-height: var(--error-log-card-height, calc(100vh - 190px));
|
||||||
|
max-height: var(--error-log-card-height, calc(100vh - 190px));
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button-menu {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button::part(label) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-list-item[selected] {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"logs-viewer": LogsViewer;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
script/develop-logs
Executable file
96
script/develop-logs
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run the logs frontend development server
|
||||||
|
|
||||||
|
# Stop on errors
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
SUPERVISOR_ENDPOINT=""
|
||||||
|
SUPERVISOR_API_TOKEN=""
|
||||||
|
|
||||||
|
while getopts "c:t:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
c)
|
||||||
|
SUPERVISOR_ENDPOINT="$OPTARG"
|
||||||
|
;;
|
||||||
|
t)
|
||||||
|
SUPERVISOR_API_TOKEN="$OPTARG"
|
||||||
|
;;
|
||||||
|
h)
|
||||||
|
echo "Usage: $0 -c SUPERVISOR_ENDPOINT -t SUPERVISOR_API_TOKEN"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -c SUPERVISOR_ENDPOINT (e.g., http://192.168.1.2) [required]"
|
||||||
|
echo " -t SUPERVISOR_API_TOKEN (from remote_api add-on) [required]"
|
||||||
|
echo " -h Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 -c http://192.168.1.2 -t your_token_here"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: -$OPTARG" >&2
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate that both -c and -t are provided
|
||||||
|
if [ -z "$SUPERVISOR_ENDPOINT" ] || [ -z "$SUPERVISOR_API_TOKEN" ]; then
|
||||||
|
echo "Error: Both -c and -t are required" >&2
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Shutting down..."
|
||||||
|
echo "Stopping HA CLI container..."
|
||||||
|
docker stop ha-cli-dev 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up trap to cleanup on exit
|
||||||
|
trap cleanup INT TERM EXIT
|
||||||
|
|
||||||
|
# Run HA CLI container
|
||||||
|
echo "Starting HA CLI container..."
|
||||||
|
|
||||||
|
# Build the container if needed
|
||||||
|
if ! docker images | grep -q "ha-cli:local"; then
|
||||||
|
echo "Building HA CLI container..."
|
||||||
|
(cd logs && docker compose build)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up any existing container
|
||||||
|
docker stop ha-cli-dev 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run the container in background (not detached, so it shares stdout)
|
||||||
|
docker run \
|
||||||
|
--name ha-cli-dev \
|
||||||
|
--rm \
|
||||||
|
-p 5642:5642 \
|
||||||
|
-e SUPERVISOR_ENDPOINT="$SUPERVISOR_ENDPOINT" \
|
||||||
|
-e SUPERVISOR_API_TOKEN="$SUPERVISOR_API_TOKEN" \
|
||||||
|
-e PORT=5642 \
|
||||||
|
ha-cli:local &
|
||||||
|
|
||||||
|
# Store the docker process ID
|
||||||
|
DOCKER_PID=$!
|
||||||
|
|
||||||
|
# Wait a moment for container to start
|
||||||
|
sleep 2
|
||||||
|
echo ""
|
||||||
|
echo "HA Logs Backend API: http://localhost:5642"
|
||||||
|
echo " GET /api/logs - List endpoints"
|
||||||
|
echo " GET /api/logs/core - Core logs"
|
||||||
|
echo " GET /api/logs/supervisor - Supervisor logs"
|
||||||
|
echo " GET /health - Health check"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run gulp (this will block until Ctrl+C)
|
||||||
|
./node_modules/.bin/gulp develop-logs
|
||||||
Reference in New Issue
Block a user