Rename product to OrbitWard
This commit is contained in:
+3
-3
@@ -1,6 +1,6 @@
|
||||
ORBITALWARD_ENV=development
|
||||
ORBITALWARD_SECRET_KEY=change-me
|
||||
DATABASE_URL=postgresql+psycopg://orbitalward:orbitalward@postgres:5432/orbitalward
|
||||
ORBITWARD_ENV=development
|
||||
ORBITWARD_SECRET_KEY=change-me
|
||||
DATABASE_URL=postgresql+psycopg://orbitward:orbitward@postgres:5432/orbitward
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
BACKEND_URL=http://localhost:8000
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OrbitalWard Agent Notes
|
||||
# OrbitWard Agent Notes
|
||||
|
||||
OrbitalWard should be built incrementally. Keep the first release focused on a secure monitoring appliance with guided setup, website monitoring, alerts, and notifications.
|
||||
OrbitWard should be built incrementally. Keep the first release focused on a secure monitoring appliance with guided setup, website monitoring, alerts, and notifications.
|
||||
|
||||
## Product Guardrails
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OrbitalWard contributors
|
||||
Copyright (c) 2026 OrbitWard contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# OrbitalWard
|
||||
# OrbitWard
|
||||
|
||||
Beautiful, self-hosted infrastructure monitoring without the enterprise-tool headache.
|
||||
|
||||
OrbitalWard is an open-source monitoring appliance for homelabs, small businesses, and internal IT teams. The first target is a secure and attractive monitoring foundation with guided setup, website checks, alerts, notifications, and clean dashboards. It is not trying to be a full Zabbix or LibreNMS replacement in the first release.
|
||||
OrbitWard is an open-source monitoring appliance for homelabs, small businesses, and internal IT teams. The first target is a secure and attractive monitoring foundation with guided setup, website checks, alerts, notifications, and clean dashboards. It is not trying to be a full Zabbix or LibreNMS replacement in the first release.
|
||||
|
||||
## Current Status
|
||||
|
||||
@@ -57,7 +57,7 @@ Default local admin credentials come from `.env`:
|
||||
- `INITIAL_ADMIN_EMAIL=admin@example.com`
|
||||
- `INITIAL_ADMIN_PASSWORD=change-me`
|
||||
|
||||
Change these values before using OrbitalWard anywhere beyond local development.
|
||||
Change these values before using OrbitWard anywhere beyond local development.
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
|
||||
sqlalchemy.url = postgresql+psycopg://orbitalward:orbitalward@postgres:5432/orbitalward
|
||||
sqlalchemy.url = postgresql+psycopg://orbitward:orbitward@postgres:5432/orbitward
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Initial OrbitalWard schema.
|
||||
"""Initial OrbitWard schema.
|
||||
|
||||
Revision ID: 20260522_0001
|
||||
Revises:
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""OrbitalWard backend package."""
|
||||
"""OrbitWard backend package."""
|
||||
|
||||
@@ -5,4 +5,4 @@ router = APIRouter(tags=["health"])
|
||||
|
||||
@router.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok", "service": "orbitalward-backend"}
|
||||
return {"status": "ok", "service": "orbitward-backend"}
|
||||
|
||||
@@ -14,7 +14,7 @@ router = APIRouter(prefix="/notifications/channels", tags=["notifications"])
|
||||
|
||||
def _channel_to_read(channel: NotificationChannel) -> NotificationChannelRead:
|
||||
settings = dict(channel.settings or {})
|
||||
settings.setdefault("username", "OrbitalWard")
|
||||
settings.setdefault("username", "OrbitWard")
|
||||
return NotificationChannelRead(
|
||||
id=channel.id,
|
||||
name=channel.name,
|
||||
@@ -40,7 +40,7 @@ def create_channel(
|
||||
db: Session = Depends(get_db),
|
||||
) -> NotificationChannelRead:
|
||||
channel_settings = dict(payload.settings or {})
|
||||
channel_settings.setdefault("username", "OrbitalWard")
|
||||
channel_settings.setdefault("username", "OrbitWard")
|
||||
channel = NotificationChannel(
|
||||
name=payload.name,
|
||||
channel_type=payload.channel_type,
|
||||
@@ -70,8 +70,8 @@ def test_channel(
|
||||
response = httpx.post(
|
||||
url,
|
||||
json={
|
||||
"username": (channel.settings or {}).get("username") or "OrbitalWard",
|
||||
"text": f"OrbitalWard test notification for {channel.name}",
|
||||
"username": (channel.settings or {}).get("username") or "OrbitWard",
|
||||
"text": f"OrbitWard test notification for {channel.name}",
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
@@ -20,12 +20,12 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
def create_access_token(subject: str) -> str:
|
||||
expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
payload = {"sub": subject, "exp": expires_at}
|
||||
return jwt.encode(payload, settings.orbitalward_secret_key, algorithm=ALGORITHM)
|
||||
return jwt.encode(payload, settings.orbitward_secret_key, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> str | None:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.orbitalward_secret_key, algorithms=[ALGORITHM])
|
||||
payload = jwt.decode(token, settings.orbitward_secret_key, algorithms=[ALGORITHM])
|
||||
return payload.get("sub")
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic import AnyHttpUrl, Field
|
||||
from pydantic import AliasChoices, AnyHttpUrl, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
orbitalward_env: str = "development"
|
||||
orbitalward_secret_key: str = Field(default="change-me", min_length=8)
|
||||
database_url: str = "postgresql+psycopg://orbitalward:orbitalward@postgres:5432/orbitalward"
|
||||
orbitward_env: str = Field(default="development", validation_alias=AliasChoices("ORBITWARD_ENV", "ORBITALWARD_ENV"))
|
||||
orbitward_secret_key: str = Field(
|
||||
default="change-me",
|
||||
min_length=8,
|
||||
validation_alias=AliasChoices("ORBITWARD_SECRET_KEY", "ORBITALWARD_SECRET_KEY"),
|
||||
)
|
||||
database_url: str = "postgresql+psycopg://orbitward:orbitward@postgres:5432/orbitward"
|
||||
redis_url: str = "redis://redis:6379/0"
|
||||
frontend_url: AnyHttpUrl | str = "http://localhost:5173"
|
||||
backend_url: AnyHttpUrl | str = "http://localhost:8000"
|
||||
|
||||
@@ -7,7 +7,7 @@ from app.core.config import settings
|
||||
|
||||
|
||||
def _fernet() -> Fernet:
|
||||
digest = hashlib.sha256(settings.orbitalward_secret_key.encode("utf-8")).digest()
|
||||
digest = hashlib.sha256(settings.orbitward_secret_key.encode("utf-8")).digest()
|
||||
return Fernet(base64.urlsafe_b64encode(digest))
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="OrbitalWard API",
|
||||
title="OrbitWard API",
|
||||
version="0.1.0",
|
||||
description="Self-hosted infrastructure monitoring API",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "orbitalward-backend"
|
||||
name = "orbitward-backend"
|
||||
version = "0.1.0"
|
||||
description = "OrbitalWard FastAPI backend"
|
||||
description = "OrbitWard FastAPI backend"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"alembic>=1.13.3",
|
||||
|
||||
@@ -11,8 +11,8 @@ def test_notification_channel_does_not_return_saved_secret(client: TestClient, d
|
||||
json={
|
||||
"name": "Operations Webhook",
|
||||
"channel_type": "generic_webhook",
|
||||
"settings": {"username": "OrbitalWard"},
|
||||
"secret": "https://hooks.example.test/orbitalward",
|
||||
"settings": {"username": "OrbitWard"},
|
||||
"secret": "https://hooks.example.test/orbitward",
|
||||
"is_enabled": True,
|
||||
},
|
||||
)
|
||||
@@ -25,8 +25,8 @@ def test_notification_channel_does_not_return_saved_secret(client: TestClient, d
|
||||
|
||||
channel = db_session.get(NotificationChannel, body["id"])
|
||||
assert channel is not None
|
||||
assert channel.encrypted_secret != "https://hooks.example.test/orbitalward"
|
||||
assert decrypt_secret(channel.encrypted_secret) == "https://hooks.example.test/orbitalward"
|
||||
assert channel.encrypted_secret != "https://hooks.example.test/orbitward"
|
||||
assert decrypt_secret(channel.encrypted_secret) == "https://hooks.example.test/orbitward"
|
||||
|
||||
list_response = client.get("/notifications/channels")
|
||||
assert list_response.status_code == 200
|
||||
@@ -42,7 +42,7 @@ def test_notification_channel_update_without_secret_preserves_existing_secret(cl
|
||||
json={
|
||||
"name": "Mattermost",
|
||||
"channel_type": "mattermost",
|
||||
"settings": {"username": "OrbitalWard"},
|
||||
"settings": {"username": "OrbitWard"},
|
||||
"secret": "https://hooks.example.test/mattermost",
|
||||
"is_enabled": True,
|
||||
},
|
||||
@@ -54,7 +54,7 @@ def test_notification_channel_update_without_secret_preserves_existing_secret(cl
|
||||
f"/notifications/channels/{channel_id}",
|
||||
json={
|
||||
"name": "Mattermost Alerts",
|
||||
"settings": {"username": "OrbitalWard Alerts"},
|
||||
"settings": {"username": "OrbitWard Alerts"},
|
||||
"is_enabled": False,
|
||||
},
|
||||
)
|
||||
@@ -62,7 +62,7 @@ def test_notification_channel_update_without_secret_preserves_existing_secret(cl
|
||||
assert update_response.status_code == 200
|
||||
body = update_response.json()
|
||||
assert body["name"] == "Mattermost Alerts"
|
||||
assert body["settings"]["username"] == "OrbitalWard Alerts"
|
||||
assert body["settings"]["username"] == "OrbitWard Alerts"
|
||||
assert body["is_enabled"] is False
|
||||
assert body["has_secret"] is True
|
||||
assert "secret" not in body
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
name: orbitward
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: orbitalward
|
||||
POSTGRES_PASSWORD: orbitalward
|
||||
POSTGRES_DB: orbitalward
|
||||
POSTGRES_USER: orbitward
|
||||
POSTGRES_PASSWORD: orbitward
|
||||
POSTGRES_DB: orbitward
|
||||
volumes:
|
||||
- postgres-dev-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U orbitalward -d orbitalward"]
|
||||
test: ["CMD-SHELL", "pg_isready -U orbitward -d orbitward"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
+6
-4
@@ -1,14 +1,16 @@
|
||||
name: orbitward
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: orbitalward
|
||||
POSTGRES_PASSWORD: orbitalward
|
||||
POSTGRES_DB: orbitalward
|
||||
POSTGRES_USER: orbitward
|
||||
POSTGRES_PASSWORD: orbitward
|
||||
POSTGRES_DB: orbitward
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U orbitalward -d orbitalward"]
|
||||
test: ["CMD-SHELL", "pg_isready -U orbitward -d orbitward"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
+16
-16
@@ -4,9 +4,9 @@ Last updated: 2026-05-26
|
||||
|
||||
## Current Identity
|
||||
|
||||
- Product name: OrbitalWard
|
||||
- Local repository path: `/home/ksmith/projects/OrbitalWard`
|
||||
- Git remote: `https://git.firebugit.com/ksmith/OrbitalWard.git`
|
||||
- Product name: OrbitWard
|
||||
- Local repository path: `/home/ksmith/projects/OrbitalWard` until the working directory is moved
|
||||
- Git remote: `https://git.firebugit.com/ksmith/OrbitWard.git`
|
||||
- Main branch: `main`
|
||||
- Latest pushed commit: check `origin/main` with `git log -1 --oneline origin/main`
|
||||
|
||||
@@ -15,14 +15,14 @@ The project was previously named InfraPulse. Do not reintroduce the old name in
|
||||
## Gitea Access
|
||||
|
||||
- Gitea API base: `https://git.firebugit.com/api/v1`
|
||||
- Repository API path: `/repos/ksmith/OrbitalWard`
|
||||
- Repository API path: `/repos/ksmith/OrbitWard`
|
||||
- Access token file: `/home/ksmith/.codex_security/gitea_token`
|
||||
|
||||
Never print the token value. Read it only inside commands that call the Gitea API.
|
||||
|
||||
## Current Product State
|
||||
|
||||
OrbitalWard is a secure monitoring appliance focused on the v0.1 vertical slice:
|
||||
OrbitWard is a secure monitoring appliance focused on the v0.1 vertical slice:
|
||||
|
||||
- Authenticated FastAPI backend with SQLAlchemy, Alembic, Pydantic, and JWT auth.
|
||||
- React, TypeScript, Vite, and Tailwind frontend.
|
||||
@@ -55,18 +55,18 @@ Recent Docker checks:
|
||||
Earlier rename and monitor work also verified:
|
||||
|
||||
- `docker compose -f docker-compose.dev.yml up -d --build`
|
||||
- Backend health returned `{"status":"ok","service":"orbitalward-backend"}`.
|
||||
- Backend health returned `{"status":"ok","service":"orbitward-backend"}`.
|
||||
- Direct worker probes for TCP and ICMP ping checks passed inside the Docker network.
|
||||
- API probe created and deleted one ping monitor and one TCP monitor successfully.
|
||||
|
||||
The final Compose project uses `orbitalward-*` containers, images, network, and volumes.
|
||||
The final Compose project uses `orbitward-*` containers, images, network, and volumes.
|
||||
|
||||
## Important Implementation Notes
|
||||
|
||||
- `ORBITALWARD_SECRET_KEY` is the encryption/JWT secret environment variable.
|
||||
- `DATABASE_URL` now defaults to the `orbitalward` database/user in Compose.
|
||||
- The frontend local storage key is `orbitalward_token`.
|
||||
- Notification default username is `OrbitalWard`.
|
||||
- `ORBITWARD_SECRET_KEY` is the encryption/JWT secret environment variable.
|
||||
- `DATABASE_URL` now defaults to the `orbitward` database/user in Compose.
|
||||
- The frontend local storage key is `orbitward_token`.
|
||||
- Notification default username is `OrbitWard`.
|
||||
- The TLS expiry check lives in `worker/app/collectors/website.py` and is enabled per monitor through JSON config fields:
|
||||
- `check_tls_expiry`
|
||||
- `tls_warning_days`
|
||||
@@ -75,11 +75,11 @@ The final Compose project uses `orbitalward-*` containers, images, network, and
|
||||
|
||||
Use the Gitea API with the token file above. Useful endpoints:
|
||||
|
||||
- List issues: `GET /repos/ksmith/OrbitalWard/issues?state=all`
|
||||
- Create issue: `POST /repos/ksmith/OrbitalWard/issues`
|
||||
- Update issue: `PATCH /repos/ksmith/OrbitalWard/issues/{index}`
|
||||
- List milestones: `GET /repos/ksmith/OrbitalWard/milestones`
|
||||
- List labels: `GET /repos/ksmith/OrbitalWard/labels`
|
||||
- List issues: `GET /repos/ksmith/OrbitWard/issues?state=all`
|
||||
- Create issue: `POST /repos/ksmith/OrbitWard/issues`
|
||||
- Update issue: `PATCH /repos/ksmith/OrbitWard/issues/{index}`
|
||||
- List milestones: `GET /repos/ksmith/OrbitWard/milestones`
|
||||
- List labels: `GET /repos/ksmith/OrbitWard/labels`
|
||||
|
||||
Issue source docs:
|
||||
|
||||
|
||||
@@ -26,4 +26,4 @@ Initial channels:
|
||||
- Zoom Team Chat incoming webhook
|
||||
- Generic webhook
|
||||
|
||||
Alert messages should be human-readable and include asset, check, status, duration, timestamps, and a link back to OrbitalWard.
|
||||
Alert messages should be human-readable and include asset, check, status, duration, timestamps, and a link back to OrbitWard.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
OrbitalWard is a monorepo with four main areas:
|
||||
OrbitWard is a monorepo with four main areas:
|
||||
|
||||
- `backend`: FastAPI service exposing REST endpoints and owning database access.
|
||||
- `worker`: Background scheduler and collectors for checks and alert evaluation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Discovery Design
|
||||
|
||||
Guided discovery is a core OrbitalWard workflow.
|
||||
Guided discovery is a core OrbitWard workflow.
|
||||
|
||||
```text
|
||||
Add target
|
||||
@@ -16,7 +16,7 @@ Create monitors and optional alert rules
|
||||
|
||||
## Monitor vs Alert Separation
|
||||
|
||||
OrbitalWard must allow monitoring without alerting. Every discovered item should eventually support separate choices:
|
||||
OrbitWard must allow monitoring without alerting. Every discovered item should eventually support separate choices:
|
||||
|
||||
- Collect metric
|
||||
- Graph metric
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
44. Show and graph SNMP interface throughput
|
||||
45. Build asset detail UI for monitors, metrics, and context
|
||||
46. Refine metric-only monitor status semantics
|
||||
47. Rename product from OrbitalWard to OrbitWard
|
||||
47. Rename product to OrbitWard
|
||||
|
||||
## Current Implementation Snapshot
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Plugin Design
|
||||
|
||||
Plugins will let OrbitalWard add collectors and discovery logic without hard-coding every integration into the core API.
|
||||
Plugins will let OrbitWard add collectors and discovery logic without hard-coding every integration into the core API.
|
||||
|
||||
Target shape:
|
||||
|
||||
```python
|
||||
class OrbitalWardPlugin:
|
||||
class OrbitWardPlugin:
|
||||
name: str
|
||||
display_name: str
|
||||
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
# OrbitalWard Progress
|
||||
# OrbitWard Progress
|
||||
|
||||
Last updated: 2026-05-26
|
||||
|
||||
## Current State
|
||||
|
||||
OrbitalWard has a working Docker Compose development stack with PostgreSQL, Redis, FastAPI backend, Python worker, and React/Vite frontend.
|
||||
OrbitWard has a working Docker Compose development stack with PostgreSQL, Redis, FastAPI backend, Python worker, and React/Vite frontend.
|
||||
|
||||
Implemented foundation:
|
||||
|
||||
@@ -35,7 +35,7 @@ Implemented notification slice:
|
||||
|
||||
- Create, edit, test, and delete notification channels from the UI.
|
||||
- Generic webhook, Mattermost, and Zoom Team Chat channel types.
|
||||
- Webhook URLs encrypted at rest using `ORBITALWARD_SECRET_KEY`.
|
||||
- Webhook URLs encrypted at rest using `ORBITWARD_SECRET_KEY`.
|
||||
- Saved webhook URLs are not returned to the UI.
|
||||
- Configurable post username per notification channel.
|
||||
- Worker sends incident open and recovery notifications.
|
||||
@@ -148,4 +148,4 @@ Default local login comes from `.env`:
|
||||
- `INITIAL_ADMIN_EMAIL=admin@example.com`
|
||||
- `INITIAL_ADMIN_PASSWORD=change-me`
|
||||
|
||||
Change these values before using OrbitalWard outside local development.
|
||||
Change these values before using OrbitWard outside local development.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Security
|
||||
|
||||
OrbitalWard must be secure from the beginning because it will store infrastructure credentials.
|
||||
OrbitWard must be secure from the beginning because it will store infrastructure credentials.
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -19,7 +19,7 @@ Credential records are modeled separately from monitors and assets. Secret field
|
||||
|
||||
Rules:
|
||||
|
||||
- Use `ORBITALWARD_SECRET_KEY` from the environment.
|
||||
- Use `ORBITWARD_SECRET_KEY` from the environment.
|
||||
- Never log secrets.
|
||||
- Mask saved secrets in the UI.
|
||||
- Audit credential create, update, and delete events.
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
# OrbitalWard Vision
|
||||
# OrbitWard Vision
|
||||
|
||||
OrbitalWard is a secure, self-hosted monitoring platform for homelabs, small businesses, and internal IT teams.
|
||||
OrbitWard is a secure, self-hosted monitoring platform for homelabs, small businesses, and internal IT teams.
|
||||
|
||||
The v0.1 product should feel like a polished appliance, not a pile of raw monitoring configuration. Users should be guided through adding targets, testing connections, discovering useful items, choosing what to monitor, and separately choosing what should alert.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
OrbitalWard exposes intent, not implementation details.
|
||||
OrbitWard exposes intent, not implementation details.
|
||||
|
||||
Raw SNMP OIDs, probe internals, and collector details belong behind friendly profiles and advanced tools. The normal UI should say things like "Port 5 outbound traffic", "Graph this port", and "Alert if port goes down".
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OrbitalWard</title>
|
||||
<title>OrbitWard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "orbitalward-frontend",
|
||||
"name": "orbitward-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "orbitalward-frontend",
|
||||
"name": "orbitward-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "orbitalward-frontend",
|
||||
"name": "orbitward-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -65,7 +65,7 @@ export function App() {
|
||||
}
|
||||
|
||||
if (auth.loading) {
|
||||
return <div className="flex min-h-screen items-center justify-center bg-[#090d13] text-sm text-slate-300">Loading OrbitalWard...</div>;
|
||||
return <div className="flex min-h-screen items-center justify-center bg-[#090d13] text-sm text-slate-300">Loading OrbitWard...</div>;
|
||||
}
|
||||
|
||||
if (!auth.user || !auth.token) {
|
||||
|
||||
@@ -49,7 +49,7 @@ export function Shell({ children, currentPage, onPageChange, onSignOut, user }:
|
||||
<Shield size={19} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold">OrbitalWard</div>
|
||||
<div className="text-base font-semibold">OrbitWard</div>
|
||||
<div className="text-xs text-slate-400">Monitoring appliance</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,20 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { api, login } from "../api/client";
|
||||
import type { User } from "../types/api";
|
||||
|
||||
const TOKEN_KEY = "orbitalward_token";
|
||||
const TOKEN_KEY = "orbitward_token";
|
||||
const LEGACY_TOKEN_KEY = "orbitalward_token";
|
||||
|
||||
export function useAuth() {
|
||||
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY));
|
||||
const [token, setToken] = useState<string | null>(() => {
|
||||
const currentToken = localStorage.getItem(TOKEN_KEY);
|
||||
if (currentToken) return currentToken;
|
||||
const legacyToken = localStorage.getItem(LEGACY_TOKEN_KEY);
|
||||
if (legacyToken) {
|
||||
localStorage.setItem(TOKEN_KEY, legacyToken);
|
||||
localStorage.removeItem(LEGACY_TOKEN_KEY);
|
||||
}
|
||||
return legacyToken;
|
||||
});
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(Boolean(token));
|
||||
|
||||
@@ -26,6 +36,7 @@ export function useAuth() {
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(LEGACY_TOKEN_KEY);
|
||||
if (!cancelled) {
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
@@ -52,6 +63,7 @@ export function useAuth() {
|
||||
},
|
||||
signOut: () => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(LEGACY_TOKEN_KEY);
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-pulse text-slate-950">
|
||||
<Shield size={22} />
|
||||
</div>
|
||||
<div className="text-lg font-semibold">OrbitalWard</div>
|
||||
<div className="text-lg font-semibold">OrbitWard</div>
|
||||
</div>
|
||||
<div className="max-w-3xl pb-6">
|
||||
<h1 className="max-w-2xl text-4xl font-semibold leading-tight lg:text-6xl">
|
||||
|
||||
@@ -14,7 +14,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [channelType, setChannelType] = useState("generic_webhook");
|
||||
const [url, setUrl] = useState("");
|
||||
const [username, setUsername] = useState("OrbitalWard");
|
||||
const [username, setUsername] = useState("OrbitWard");
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [editingChannelId, setEditingChannelId] = useState<number | null>(null);
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
@@ -38,7 +38,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
await api.updateNotificationChannel(token, editingChannelId, {
|
||||
name,
|
||||
channel_type: channelType,
|
||||
settings: { username: username.trim() || "OrbitalWard" },
|
||||
settings: { username: username.trim() || "OrbitWard" },
|
||||
secret: url.trim() ? url.trim() : undefined,
|
||||
is_enabled: enabled,
|
||||
});
|
||||
@@ -46,7 +46,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
await api.createNotificationChannel(token, {
|
||||
name,
|
||||
channel_type: channelType,
|
||||
settings: { username: username.trim() || "OrbitalWard" },
|
||||
settings: { username: username.trim() || "OrbitWard" },
|
||||
secret: url,
|
||||
is_enabled: enabled,
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
setName(channel.name);
|
||||
setChannelType(channel.channel_type);
|
||||
setUrl("");
|
||||
setUsername(String(channel.settings.username || "OrbitalWard"));
|
||||
setUsername(String(channel.settings.username || "OrbitWard"));
|
||||
setEnabled(channel.is_enabled);
|
||||
setMessage(null);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
setName("");
|
||||
setChannelType("generic_webhook");
|
||||
setUrl("");
|
||||
setUsername("OrbitalWard");
|
||||
setUsername("OrbitWard");
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
<div key={channel.id} className="grid gap-3 p-4 md:grid-cols-[1fr_140px_90px_260px] md:items-center">
|
||||
<div>
|
||||
<div className="font-medium">{channel.name}</div>
|
||||
<div className="text-sm text-slate-400">{String(channel.settings.username || "OrbitalWard")}</div>
|
||||
<div className="text-sm text-slate-400">{String(channel.settings.username || "OrbitWard")}</div>
|
||||
<div className="text-xs text-slate-500">{channel.has_secret ? "Secret stored" : "No secret"}</div>
|
||||
</div>
|
||||
<div className="text-sm text-slate-300">{channel.channel_type}</div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""OrbitalWard worker package."""
|
||||
"""OrbitWard worker package."""
|
||||
|
||||
@@ -78,7 +78,7 @@ def _resolve_ipv4(host: str) -> str:
|
||||
|
||||
|
||||
def _build_icmp_echo_request(identifier: int, sequence: int) -> bytes:
|
||||
payload = b"OrbitalWard ping"
|
||||
payload = b"OrbitWard ping"
|
||||
header = struct.pack("!BBHHH", 8, 0, 0, identifier, sequence)
|
||||
checksum = _icmp_checksum(header + payload)
|
||||
header = struct.pack("!BBHHH", 8, 0, checksum, identifier, sequence)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic import AliasChoices, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
orbitalward_env: str = "development"
|
||||
orbitalward_secret_key: str = "change-me"
|
||||
database_url: str = "postgresql+psycopg://orbitalward:orbitalward@postgres:5432/orbitalward"
|
||||
orbitward_env: str = Field(default="development", validation_alias=AliasChoices("ORBITWARD_ENV", "ORBITALWARD_ENV"))
|
||||
orbitward_secret_key: str = Field(
|
||||
default="change-me",
|
||||
validation_alias=AliasChoices("ORBITWARD_SECRET_KEY", "ORBITALWARD_SECRET_KEY"),
|
||||
)
|
||||
database_url: str = "postgresql+psycopg://orbitward:orbitward@postgres:5432/orbitward"
|
||||
redis_url: str = "redis://redis:6379/0"
|
||||
frontend_url: str = "http://localhost:5173"
|
||||
backend_url: str = "http://localhost:8000"
|
||||
|
||||
@@ -24,7 +24,7 @@ class Scheduler:
|
||||
self._stopped = asyncio.Event()
|
||||
|
||||
async def run(self) -> None:
|
||||
logger.info("OrbitalWard worker started for %s", settings.orbitalward_env)
|
||||
logger.info("OrbitWard worker started for %s", settings.orbitward_env)
|
||||
while not self._stopped.is_set():
|
||||
await self.tick()
|
||||
try:
|
||||
@@ -249,7 +249,7 @@ class Scheduler:
|
||||
await self._post_webhook(
|
||||
url,
|
||||
self._format_incident_message(incident, monitor, event_type),
|
||||
str((channel.settings or {}).get("username") or "OrbitalWard"),
|
||||
str((channel.settings or {}).get("username") or "OrbitWard"),
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
logger.exception("Notification delivery failed for channel %s", channel.id)
|
||||
@@ -296,5 +296,5 @@ class Scheduler:
|
||||
last_message = (incident.details or {}).get("last_message")
|
||||
if last_message:
|
||||
body.append(f"Last response: {last_message}")
|
||||
body.extend(["", f"View in OrbitalWard: {settings.frontend_url}/incidents/{incident.id}"])
|
||||
body.extend(["", f"View in OrbitWard: {settings.frontend_url}/incidents/{incident.id}"])
|
||||
return "\n".join(str(line) for line in body)
|
||||
|
||||
@@ -7,7 +7,7 @@ from app.config import settings
|
||||
|
||||
|
||||
def _fernet() -> Fernet:
|
||||
digest = hashlib.sha256(settings.orbitalward_secret_key.encode("utf-8")).digest()
|
||||
digest = hashlib.sha256(settings.orbitward_secret_key.encode("utf-8")).digest()
|
||||
return Fernet(base64.urlsafe_b64encode(digest))
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "orbitalward-worker"
|
||||
name = "orbitward-worker"
|
||||
version = "0.1.0"
|
||||
description = "OrbitalWard background worker"
|
||||
description = "OrbitWard background worker"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"cryptography>=48.0.0",
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.scheduler import Scheduler
|
||||
|
||||
|
||||
def encrypt_secret(value: str) -> str:
|
||||
digest = hashlib.sha256(settings.orbitalward_secret_key.encode("utf-8")).digest()
|
||||
digest = hashlib.sha256(settings.orbitward_secret_key.encode("utf-8")).digest()
|
||||
return Fernet(base64.urlsafe_b64encode(digest)).encrypt(value.encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
@@ -102,8 +102,8 @@ class SchedulerTestCase(unittest.IsolatedAsyncioTestCase):
|
||||
channel = NotificationChannel(
|
||||
name="Ops Webhook",
|
||||
channel_type="generic_webhook",
|
||||
settings={"username": "OrbitalWard"},
|
||||
encrypted_secret=encrypt_secret("https://hooks.example.test/orbitalward"),
|
||||
settings={"username": "OrbitWard"},
|
||||
encrypted_secret=encrypt_secret("https://hooks.example.test/orbitward"),
|
||||
is_enabled=True,
|
||||
)
|
||||
self.db.add(channel)
|
||||
@@ -121,8 +121,8 @@ class SchedulerTestCase(unittest.IsolatedAsyncioTestCase):
|
||||
assert incident is not None
|
||||
assert incident.status == "open"
|
||||
assert len(scheduler.posts) == 1
|
||||
assert scheduler.posts[0]["url"] == "https://hooks.example.test/orbitalward"
|
||||
assert scheduler.posts[0]["username"] == "OrbitalWard"
|
||||
assert scheduler.posts[0]["url"] == "https://hooks.example.test/orbitward"
|
||||
assert scheduler.posts[0]["username"] == "OrbitWard"
|
||||
assert incident.details["notification_history"][0]["event"] == "opened"
|
||||
|
||||
await scheduler._send_incident_notifications(self.db, incident, monitor, "opened", datetime.now(UTC))
|
||||
|
||||
Reference in New Issue
Block a user