seren-observatory 0.0.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. seren_observatory-0.0.0.dev0/PKG-INFO +31 -0
  2. seren_observatory-0.0.0.dev0/SerenObservatory.pyproj +123 -0
  3. seren_observatory-0.0.0.dev0/pyproject.toml +86 -0
  4. seren_observatory-0.0.0.dev0/seren-observatory.service.sample +33 -0
  5. seren_observatory-0.0.0.dev0/seren-observatory.yaml.sample +22 -0
  6. seren_observatory-0.0.0.dev0/seren_observatory/__init__.py +23 -0
  7. seren_observatory-0.0.0.dev0/seren_observatory/__main__.py +37 -0
  8. seren_observatory-0.0.0.dev0/seren_observatory/_version.py +24 -0
  9. seren_observatory-0.0.0.dev0/seren_observatory/app.py +172 -0
  10. seren_observatory-0.0.0.dev0/seren_observatory/auth.py +134 -0
  11. seren_observatory-0.0.0.dev0/seren_observatory/config.py +153 -0
  12. seren_observatory-0.0.0.dev0/seren_observatory/lifecycle.py +1072 -0
  13. seren_observatory-0.0.0.dev0/seren_observatory/manifests.py +154 -0
  14. seren_observatory-0.0.0.dev0/seren_observatory/request_log.py +137 -0
  15. seren_observatory-0.0.0.dev0/seren_observatory/requirements.txt +20 -0
  16. seren_observatory-0.0.0.dev0/seren_observatory/service_routes.py +124 -0
  17. seren_observatory-0.0.0.dev0/seren_observatory/services/__init__.py +50 -0
  18. seren_observatory-0.0.0.dev0/seren_observatory/services/comfy.py +110 -0
  19. seren_observatory-0.0.0.dev0/seren_observatory/services/coral.py +89 -0
  20. seren_observatory-0.0.0.dev0/seren_observatory/services/kokoro.py +60 -0
  21. seren_observatory-0.0.0.dev0/seren_observatory/services/llama.py +52 -0
  22. seren_observatory-0.0.0.dev0/seren_observatory/services/whisper.py +66 -0
  23. seren_observatory-0.0.0.dev0/seren_observatory/system_routes.py +496 -0
  24. seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/body.html +29 -0
  25. seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/header_aside.html +6 -0
  26. seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/scripts.js +230 -0
  27. seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/styles.css +227 -0
  28. seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/tabs.html +5 -0
  29. seren_observatory-0.0.0.dev0/seren_observatory.egg-info/PKG-INFO +31 -0
  30. seren_observatory-0.0.0.dev0/seren_observatory.egg-info/SOURCES.txt +39 -0
  31. seren_observatory-0.0.0.dev0/seren_observatory.egg-info/dependency_links.txt +1 -0
  32. seren_observatory-0.0.0.dev0/seren_observatory.egg-info/entry_points.txt +2 -0
  33. seren_observatory-0.0.0.dev0/seren_observatory.egg-info/requires.txt +11 -0
  34. seren_observatory-0.0.0.dev0/seren_observatory.egg-info/top_level.txt +1 -0
  35. seren_observatory-0.0.0.dev0/setup.cfg +4 -0
  36. seren_observatory-0.0.0.dev0/tests/__init__.py +0 -0
  37. seren_observatory-0.0.0.dev0/tests/conftest.py +75 -0
  38. seren_observatory-0.0.0.dev0/tests/test_app.py +72 -0
  39. seren_observatory-0.0.0.dev0/tests/test_auth.py +143 -0
  40. seren_observatory-0.0.0.dev0/tests/test_config.py +110 -0
  41. seren_observatory-0.0.0.dev0/tests/test_manifests.py +106 -0
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: seren-observatory
3
+ Version: 0.0.0.dev0
4
+ Summary: Per-node management plane for the Seren cluster - manifest-driven service lifecycle, status, and orchestration.
5
+ License: GPL-3.0-or-later
6
+ Project-URL: Homepage, https://github.com/ChadRoesler/SerenObservatory
7
+ Project-URL: Repository, https://github.com/ChadRoesler/SerenObservatory
8
+ Project-URL: Bug Tracker, https://github.com/ChadRoesler/SerenObservatory/issues
9
+ Keywords: seren,jetson,observatory,fastapi,llm
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: No Input/Output (Daemon)
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: fastapi>=0.115.0
23
+ Requires-Dist: uvicorn[standard]>=0.32.0
24
+ Requires-Dist: pydantic>=2.9.0
25
+ Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: python-multipart>=0.0.12
27
+ Requires-Dist: seren-meninges<2,>=1.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
31
+ Requires-Dist: httpx2>=0.28; extra == "dev"
@@ -0,0 +1,123 @@
1
+ <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
2
+ <PropertyGroup>
3
+ <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
4
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
5
+ <SchemaVersion>2.0</SchemaVersion>
6
+ <ProjectGuid>{e20990c9-b0f0-46ff-9f69-6537d00b7fbf}</ProjectGuid>
7
+ <ProjectHome>.</ProjectHome>
8
+ <ProjectTypeGuids>{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
9
+ <StartupFile>seren_observatory/app.py</StartupFile>
10
+ <WorkingDirectory>.</WorkingDirectory>
11
+ <OutputPath>dist\</OutputPath>
12
+ <Name>SerenObservatory</Name>
13
+ </PropertyGroup>
14
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
15
+ <DebugSymbols>true</DebugSymbols>
16
+ </PropertyGroup>
17
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
18
+ <DebugSymbols>true</DebugSymbols>
19
+ </PropertyGroup>
20
+ <ItemGroup>
21
+ <Compile Include="seren_observatory\config.py" />
22
+ <Compile Include="seren_observatory\requirements.txt" />
23
+ <Compile Include="seren_observatory\app.py" />
24
+ <Compile Include="seren_observatory\auth.py" />
25
+ <Compile Include="seren_observatory\lifecycle.py" />
26
+ <Compile Include="seren_observatory\manifests.py" />
27
+ <Compile Include="seren_observatory\request_log.py" />
28
+ <Compile Include="seren_observatory\service_routes.py" />
29
+ <Compile Include="seren_observatory\system_routes.py" />
30
+ <Compile Include="seren_observatory\_version.py" />
31
+ <Compile Include="seren_observatory\__init__.py" />
32
+ <Compile Include="seren_observatory\services\comfy.py" />
33
+ <Compile Include="seren_observatory\services\coral.py" />
34
+ <Compile Include="seren_observatory\services\kokoro.py" />
35
+ <Compile Include="seren_observatory\services\llama.py" />
36
+ <Compile Include="seren_observatory\services\whisper.py" />
37
+ <Compile Include="seren_observatory\services\__init__.py" />
38
+ <Compile Include="seren_observatory\__main__.py" />
39
+ <Compile Include="tests\conftest.py" />
40
+ <Compile Include="tests\test_app.py" />
41
+ <Compile Include="tests\test_auth.py" />
42
+ <Compile Include="tests\test_config.py" />
43
+ <Compile Include="tests\test_manifests.py" />
44
+ <Compile Include="tests\__init__.py" />
45
+ </ItemGroup>
46
+ <ItemGroup>
47
+ <None Include="seren_observatory\requirements.txt" />
48
+ <None Include="pyproject.toml" />
49
+ <None Include="README.md" Condition="Exists('README.md')" />
50
+ </ItemGroup>
51
+ <ItemGroup>
52
+ <Folder Include="seren_observatory\" />
53
+ <Folder Include="seren_observatory\services\" />
54
+ <Folder Include="seren_observatory\services\__pycache__\" />
55
+ <Folder Include="seren_observatory\viewer\" />
56
+ <Folder Include="seren_observatory\viewer\ui\" />
57
+ <Folder Include="tests\" />
58
+ </ItemGroup>
59
+ <ItemGroup>
60
+ <Content Include="seren-observatory.service.sample" />
61
+ <Content Include="seren-observatory.yaml.sample" />
62
+ <Content Include="seren_observatory\services\__pycache__\comfy.cpython-311.pyc" />
63
+ <Content Include="seren_observatory\services\__pycache__\coral.cpython-311.pyc" />
64
+ <Content Include="seren_observatory\services\__pycache__\kokoro.cpython-311.pyc" />
65
+ <Content Include="seren_observatory\services\__pycache__\llama.cpython-311.pyc" />
66
+ <Content Include="seren_observatory\services\__pycache__\whisper.cpython-311.pyc" />
67
+ <Content Include="seren_observatory\services\__pycache__\__init__.cpython-311.pyc" />
68
+ <Content Include="seren_observatory\viewer\ui\body.html" />
69
+ <Content Include="seren_observatory\viewer\ui\header_aside.html" />
70
+ <Content Include="seren_observatory\viewer\ui\scripts.js" />
71
+ <Content Include="seren_observatory\viewer\ui\styles.css" />
72
+ <Content Include="seren_observatory\viewer\ui\tabs.html" />
73
+ </ItemGroup>
74
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
75
+ <!--
76
+ Publish target: runs ONLY when you explicitly invoke it.
77
+ VS: right-click project -> Publish (or build configuration set
78
+ to invoke the Publish target via msbuild /t:Publish)
79
+ Command: msbuild SerenObservatory.pyproj /t:Publish /p:Configuration=Release
80
+
81
+ What it does:
82
+ 1. Cleans __pycache__ from observatory/ so they don't end up in the tarball
83
+ 2. Runs `tar` (Windows 10+ ships bsdtar at C:\Windows\System32\tar.exe)
84
+ to create $(PublishTarball) with `observatory/` at the top level — no
85
+ wrapping directory, no metadata files. This is exactly the layout
86
+ install_observatory_common in seren-v5/common.sh expects.
87
+ 3. Verifies the tarball contains observatory/__init__.py at the top level
88
+ so a half-built artifact doesn't sneak through.
89
+
90
+ Why not AfterBuild: that fires on every build (including F5 to debug),
91
+ which would slow your edit-test loop and produce stale tarballs in
92
+ half-finished states. Publish is a deliberate "I'm ready to ship" step.
93
+ -->
94
+ <Target Name="Publish">
95
+ <Message Text="==&gt; Cleaning __pycache__ from seren_observatory/" Importance="high" />
96
+ <ItemGroup>
97
+ <PycacheDirs Include="seren_observatory\**\__pycache__\**" />
98
+ </ItemGroup>
99
+ <Delete Files="@(PycacheDirs)" />
100
+ <RemoveDir Directories="observatory\__pycache__;observatory\services\__pycache__" />
101
+ <Message Text="==&gt; Ensuring publish dir exists: $(SerenSetupDir)" Importance="high" />
102
+ <MakeDir Directories="$(SerenSetupDir)" Condition="!Exists('$(SerenSetupDir)')" />
103
+ <Message Text="==&gt; Building $(PublishTarball)" Importance="high" />
104
+ <!--
105
+ tar.exe (bsdtar on Win10+) syntax:
106
+ -c create
107
+ -z gzip
108
+ -f output file
109
+ We tar from $(ProjectHome) so the path inside the archive is just
110
+ `seren_observatory/...` with no leading directory.
111
+ -->
112
+ <Exec Command="tar -czf &quot;$(PublishTarball)&quot; seren-observatory" WorkingDirectory="$(ProjectHome)" />
113
+ <Message Text="==&gt; Verifying tarball layout" Importance="high" />
114
+ <!--
115
+ Verify seren_observatory/__init__.py is present (correct package layout).
116
+ Verify seren_observatory/requirements.txt is present so nodes always have it
117
+ available for pip install -r after extract.
118
+ -->
119
+ <Exec Command="tar -tzf &quot;$(PublishTarball)&quot; | findstr /B /L &quot;seren_observatory/__init__.py&quot; &gt; nul" WorkingDirectory="$(ProjectHome)" />
120
+ <Exec Command="tar -tzf &quot;$(PublishTarball)&quot; | findstr /B /L &quot;seren_observatory/requirements.txt&quot; &gt; nul" WorkingDirectory="$(ProjectHome)" />
121
+ <Message Text="==&gt; Published: $(PublishTarball)" Importance="high" />
122
+ </Target>
123
+ </Project>
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70", "setuptools-scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ # ── Project metadata ──────────────────────────────────────────────────────
6
+ [project]
7
+ name = "seren-observatory"
8
+ dynamic = ["version"]
9
+ description = "Per-node management plane for the Seren cluster - manifest-driven service lifecycle, status, and orchestration."
10
+ license = { text = "GPL-3.0-or-later" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["seren", "jetson", "observatory", "fastapi", "llm"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: No Input/Output (Daemon)",
16
+ "Framework :: FastAPI",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: System :: Systems Administration",
24
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
25
+ ]
26
+
27
+ dependencies = [
28
+ "fastapi>=0.115.0",
29
+ "uvicorn[standard]>=0.32.0",
30
+ "pydantic>=2.9.0",
31
+ "httpx>=0.27.0",
32
+ "python-multipart>=0.0.12",
33
+ # Shared Seren core: get_version + the viewer baseplate (render_from_dir).
34
+ # NOTE: config + auth deliberately do NOT come from Meninges here - the
35
+ # Observatory's bearer is a ~/.seren/secrets.json safety interlock, not a
36
+ # Meninges-resolved config token. No [keyring] extra: this service never
37
+ # writes secrets to the OS keychain.
38
+ "seren-meninges>=1.0.0,<2",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0",
44
+ "pytest-asyncio>=0.24",
45
+ "httpx2>=0.28", # Starlette TestClient's successor HTTP backend (Tom Christie / pydantic); silences the httpx-deprecation warning
46
+ ]
47
+
48
+ [project.scripts]
49
+ seren-observatory = "seren_observatory.app:main"
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/ChadRoesler/SerenObservatory"
53
+ Repository = "https://github.com/ChadRoesler/SerenObservatory"
54
+ "Bug Tracker" = "https://github.com/ChadRoesler/SerenObservatory/issues"
55
+
56
+ # ── setuptools-scm: version from git tags ────────────────────────────────
57
+ # Tags must be the form v1.2.3 on the repo root.
58
+ # `python -m build` with no tags → 0.1.dev0+g<hash>.
59
+ # SETUPTOOLS_SCM_PRETEND_VERSION overrides for local dev without tags.
60
+ [tool.setuptools_scm]
61
+ # Write version into the installed package so importlib.metadata works.
62
+ # The file is gitignored - never commit it.
63
+ version_file = "seren_observatory/_version.py"
64
+ # Strip the leading 'v' so "v1.2.3" → "1.2.3"
65
+ tag_regex = "^v(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+.*)$"
66
+ # Used when no git tags exist (local dev, fresh clone, make test).
67
+ # SETUPTOOLS_SCM_PRETEND_VERSION overrides this when set (CI release builds).
68
+ fallback_version = "0.0.0.dev0"
69
+
70
+ # ── Package discovery ────────────────────────────────────────────────────
71
+ [tool.setuptools.packages.find]
72
+ where = ["."]
73
+ include = ["seren_observatory*"]
74
+
75
+ # Ship the viewer fragment bricks inside the wheel: the leaf files in
76
+ # viewer/ui/ that render_from_dir snaps onto the SerenMeninges baseplate.
77
+ # Without this, pip strips non-.py files and GET /viewer renders an empty
78
+ # shell on an installed copy (works in a dev checkout, silently breaks once
79
+ # installed - the classic package-data gotcha).
80
+ [tool.setuptools.package-data]
81
+ seren_observatory = ["viewer/ui/*.html", "viewer/ui/*.css", "viewer/ui/*.js"]
82
+
83
+ # ── pytest ───────────────────────────────────────────────────────────────
84
+ [tool.pytest.ini_options]
85
+ testpaths = ["tests"]
86
+ asyncio_mode = "auto"
@@ -0,0 +1,33 @@
1
+ # SerenMemory systemd unit (sample)
2
+ #
3
+ # Install:
4
+ # 1. Edit __TARGET_USER__ and paths below
5
+ # 2. sudo cp seren-observatory.service /etc/systemd/system/
6
+ # 3. sudo systemctl daemon-reload
7
+ # 4. sudo systemctl enable --now seren-observatory
8
+ #
9
+ # Logs: journalctl -u seren-observatory -f
10
+
11
+ [Unit]
12
+ Description=SerenObservatory - A monitoring observatory for LLMs and tools
13
+ After=network-online.target
14
+ Wants=network-online.target
15
+
16
+ [Service]
17
+ Type=simple
18
+ User=__TARGET_USER__
19
+ WorkingDirectory=/home/__TARGET_USER__/seren-observatory
20
+
21
+ # Use the venv's python. Adjust the venv path to wherever you installed.
22
+ ExecStart=/home/__TARGET_USER__/seren-venvs/observatory/bin/python -m seren_observatory --config /home/__TARGET_USER__/seren-observatory/seren-observatory.yaml
23
+
24
+ # Config can also come from env - uncomment + set as needed:
25
+ # Environment=SEREN_AGENT_PORT=7777
26
+ # Environment=SEREN_AGENT_MODEL_URL=http://localhost:8090/v1
27
+ # Environment=SEREN_AGENT_BEARER_TOKEN=
28
+
29
+ Restart=on-failure
30
+ RestartSec=5
31
+
32
+ [Install]
33
+ WantedBy=multi-user.target
@@ -0,0 +1,22 @@
1
+ # ═══════════════════════════════════════════════════════════════════════
2
+ # seren-observatory.yaml - config for SerenObservatory
3
+ # ═══════════════════════════════════════════════════════════════════════
4
+ #
5
+ # Copy to seren-observatory.yaml, edit, and run:
6
+ # python -m seren_observatory --config seren-observatory.yaml
7
+ #
8
+ # Or just run with no config at all - the defaults below are baked in, so
9
+ # `python -m seren_observatory` works zero-config for a local dev spin.
10
+ #
11
+ # Env vars override file values (handy for Docker/systemd):
12
+ # SEREN_AGENT_PORT, SEREN_AGENT_HOST, SEREN_AGENT_BEARER_TOKEN
13
+ #
14
+ # ═══════════════════════════════════════════════════════════════════════
15
+
16
+ server:
17
+ host: 0.0.0.0
18
+ port: 7777
19
+ # Leave empty for no auth (trusted LAN / dev). Set a token to require
20
+ # Authorization: Bearer <token>
21
+ # on every endpoint except / and /health.
22
+ bearer_token: ""
@@ -0,0 +1,23 @@
1
+ """
2
+ seren-observatory - per-Jetson management plane.
3
+
4
+ HTTP API exposing manifest-driven discovery and lifecycle of locally-installed
5
+ Seren services (llama, kokoro, comfy, whisper, coral). Consumed by:
6
+ - SerenRuntimeHost (C#) for chat-app workflows
7
+ - SerenCommandCenter (SCC, future) for cluster orchestration
8
+ - The NUC dashboard for monitoring
9
+
10
+ Routes are versioned under /api/v1/. See service_routes.py and system_routes.py.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from importlib.metadata import PackageNotFoundError, version
15
+
16
+ try:
17
+ __version__: str = version("seren-observatory")
18
+ except PackageNotFoundError:
19
+ # Running from a source checkout without an editable install.
20
+ # Set SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 and run:
21
+ # pip install -e ".[dev]"
22
+ # to resolve this.
23
+ __version__ = "0.0.0.dev"
@@ -0,0 +1,37 @@
1
+ """Entry point for `python -m seren_observatory`.
2
+
3
+ Accepts --config / -c to match the SerenMemory convention (Memory leads, the
4
+ rest follow). Host/port come from the resolved config (which itself layers
5
+ defaults < yaml < env). The bearer token is NOT a config concern - it's loaded
6
+ separately from ~/.seren/secrets.json by auth.load_token(). See config.py.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+
12
+ import uvicorn
13
+
14
+ from .app import create_app
15
+ from .config import load_config
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser(
20
+ prog="seren_observatory",
21
+ description="seren-observatory - per-node management plane.")
22
+ parser.add_argument(
23
+ "--config", "-c", default=None,
24
+ help="Path to seren-observatory.yaml (default: $SEREN_AGENT_CONFIG, then "
25
+ "~/seren-observatory/seren-observatory.yaml, falling back to built-in "
26
+ "defaults of 0.0.0.0:7777).")
27
+ args = parser.parse_args()
28
+
29
+ cfg = load_config(args.config)
30
+ app = create_app(cfg)
31
+
32
+ print(f"[seren-observatory] listening on {cfg.host}:{cfg.port}")
33
+ uvicorn.run(app, host=cfg.host, port=cfg.port, log_level="info")
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.0.dev0'
22
+ __version_tuple__ = version_tuple = (0, 0, 0, 'dev0')
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,172 @@
1
+ """
2
+ seren-observatory - main FastAPI app.
3
+
4
+ Run with:
5
+ seren-observatory # via the installed console script
6
+ python -m seren_observatory # config-aware entry (--config/-c)
7
+ python -m seren_observatory.app # directly from the source tree
8
+ uvicorn seren_observatory.app:app # ASGI server pointing at module app
9
+
10
+ or via systemd / a launcher. Listens on 0.0.0.0:7777 by default.
11
+
12
+ Config: host/port resolve via config.load_config() (defaults < yaml's server:
13
+ block < env vars). The bearer token is loaded SEPARATELY from
14
+ ~/.seren/secrets.json by auth.load_token() - it's a safety interlock, not a
15
+ config field. See config.py for why.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import os
20
+
21
+ from pathlib import Path
22
+
23
+ from fastapi import FastAPI
24
+ from fastapi.responses import HTMLResponse
25
+
26
+ from . import __version__, manifests
27
+ from .auth import BearerAuthMiddleware, load_token
28
+ from .config import ObservatoryConfig, load_config
29
+ from .request_log import RequestLoggingMiddleware
30
+ from .service_routes import register_all_services
31
+ from .system_routes import router as system_router
32
+
33
+ from seren_meninges import get_version
34
+ from seren_meninges.viewer import render_from_dir
35
+
36
+ # Version via the shared SerenMeninges helper: the installed wheel's
37
+ # setuptools-scm metadata, falling back to the package __version__ for an
38
+ # editable/dev checkout where dist metadata may be absent. get_version never
39
+ # raises - the same one-liner the rest of the family uses, replacing the old
40
+ # hand-rolled importlib.metadata block that drifted from the others.
41
+ APP_VERSION = get_version("seren-observatory", fallback=__version__)
42
+
43
+
44
+ def create_app(cfg: ObservatoryConfig | None = None) -> FastAPI:
45
+ # cfg is accepted so the config-aware entry points (python -m seren_observatory)
46
+ # can pass a resolved config. When called with no arg (e.g. the
47
+ # module-level `app` below, or `uvicorn seren_observatory.app:app`), fall back to
48
+ # load_config() so behaviour is identical either way. cfg currently carries
49
+ # host/port; those are consumed by the caller that runs uvicorn, so the app
50
+ # body doesn't need them - but we resolve it anyway so a future need (e.g.
51
+ # surfacing the bind in the root page) has it on hand without another load.
52
+ cfg = cfg or load_config()
53
+
54
+ app = FastAPI(
55
+ title="seren-observatory",
56
+ version=APP_VERSION,
57
+ description="Per-Jetson management plane. Manifest-driven service "
58
+ "lifecycle, status, and orchestration. Bearer token auth "
59
+ "on everything except /api/v1/system/{ping,version}.",
60
+ )
61
+
62
+ # Request logging - wraps every request, captures timing + status +
63
+ # 500 tracebacks. Logs go to BOTH stderr (journalctl) AND a rotating
64
+ # file at ~/seren-logs/observatory-requests.log (no sudo needed to read).
65
+ #
66
+ # MUST be added BEFORE auth so we log auth-rejected requests too -
67
+ # that's actually one of the most useful debug signals ("dashboard
68
+ # is failing - is the bearer token wrong, or is the route 500ing?")
69
+ #
70
+ # Starlette/FastAPI middleware order: LIFO at request time. So adding
71
+ # RequestLoggingMiddleware FIRST means it runs LAST on the way in
72
+ # (closest to the route), and FIRST on the way out - wrapping the
73
+ # entire chain. Adding auth SECOND means auth runs BEFORE logging on
74
+ # request, which is wrong. We want logging OUTERMOST.
75
+ #
76
+ # Correct stack (request flow top→bottom):
77
+ # RequestLoggingMiddleware (logs everything, including 401s)
78
+ # BearerAuthMiddleware (rejects unauthed, logging sees the rejection)
79
+ # <route handler>
80
+ #
81
+ # FastAPI add_middleware adds in REVERSE order at runtime, so we add
82
+ # auth FIRST (will run inner) and logging SECOND (will run outer).
83
+ token = load_token()
84
+ app.add_middleware(BearerAuthMiddleware, expected_token=token)
85
+ app.add_middleware(RequestLoggingMiddleware)
86
+
87
+ # Root info page - no service data, just links + auth status indicator
88
+ @app.get("/", response_class=HTMLResponse)
89
+ async def root() -> str:
90
+ node = manifests.load_node()
91
+ host = (node or {}).get("hostname", "unknown")
92
+ auth_state = "configured" if token else "DISABLED (no token in ~/.seren/secrets.json)"
93
+ return f"""<!doctype html>
94
+ <html><head><title>seren-observatory - {host}</title></head>
95
+ <body style="font-family: system-ui; max-width: 720px; margin: 2rem auto; padding: 0 1rem;">
96
+ <h1>seren-observatory</h1>
97
+ <p>Per-Jetson management plane for the Seren cluster.</p>
98
+ <dl>
99
+ <dt>Hostname:</dt> <dd>{host}</dd>
100
+ <dt>Observatory version:</dt> <dd>{APP_VERSION}</dd>
101
+ <dt>Auth:</dt> <dd>{auth_state}</dd>
102
+ </dl>
103
+ <h2>Endpoints</h2>
104
+ <ul>
105
+ <li><a href="/docs">/docs</a> - interactive API docs (Swagger)</li>
106
+ <li><a href="/api/v1/system/ping">/api/v1/system/ping</a> - public liveness</li>
107
+ <li><a href="/api/v1/system/version">/api/v1/system/version</a> - public version</li>
108
+ <li>/api/v1/system/{{node, services, health, reclaim}} - auth required</li>
109
+ <li>/api/v1/service/{{name}}/{{start, stop, restart, health, status, logs, manifest}} - auth required</li>
110
+ </ul>
111
+ <p>Source of truth: ~/.seren/services/*.json + ~/.seren/node.json</p>
112
+ </body></html>"""
113
+
114
+ # The Observatory glance - on the shared SerenMeninges baseplate.
115
+ @app.get("/viewer", response_class=HTMLResponse)
116
+ async def viewer() -> str:
117
+ # Node vitals, thermals, health rollup, and the service roster with
118
+ # lifecycle controls. PUBLIC route (the HTML shell needs no auth - see
119
+ # auth.PUBLIC_PATHS); its /api/v1/* fetches carry the token from the
120
+ # shell's key modal. With no token provisioned the read-only glance
121
+ # still works (safe GETs stay open); the action buttons fail closed
122
+ # (503) until ~/.seren/secrets.json exists - the deliberate interlock,
123
+ # surfaced in the UI instead of hidden.
124
+ return render_from_dir(
125
+ Path(__file__).resolve().parent / "viewer" / "ui",
126
+ title="seren-observatory",
127
+ brand="Seren<b>Observatory</b>",
128
+ subtitle=f"v{APP_VERSION} · per-node watch plane",
129
+ accent="#6eff70",
130
+ )
131
+
132
+ # System routes (ping, version, node, services, health, reclaim)
133
+ app.include_router(system_router)
134
+
135
+ # Per-service routes - one router per installed service
136
+ mounted = register_all_services(app)
137
+ print(f"[seren-observatory] mounted services: {mounted}")
138
+
139
+ return app
140
+
141
+
142
+ # Module-level app for `uvicorn seren_observatory.app:app`
143
+ app = create_app()
144
+
145
+
146
+ if __name__ == "__main__":
147
+ import uvicorn
148
+
149
+ # Direct `python -m seren_observatory.app` path. Resolve config the same way the
150
+ # console script does so host/port behave identically. (`python -m
151
+ # seren_observatory` -> __main__.py is the preferred, --config-aware entry.)
152
+ _cfg = load_config()
153
+ uvicorn.run(app, host=_cfg.host, port=_cfg.port, log_level="info")
154
+
155
+
156
+ def main() -> None:
157
+ """Console-script entry point: `seren-observatory` (declared in pyproject.toml).
158
+
159
+ Resolves config (defaults < yaml < env) and runs uvicorn. The CLI --config
160
+ flag is handled by __main__.py (`python -m seren_observatory`); the bare
161
+ console-script reads $SEREN_AGENT_CONFIG / the conventional path / defaults.
162
+ """
163
+ import uvicorn
164
+
165
+ cfg = load_config()
166
+ uvicorn.run(
167
+ "seren_observatory.app:app",
168
+ host=cfg.host,
169
+ port=cfg.port,
170
+ log_level="info",
171
+ reload=False,
172
+ )