Skip to content

Commit 3c97518

Browse files
Initial Local Nexus Controller system
- Add FastAPI dashboard + API for service/database/key registries - Add SQLite registry storage, port management, and process control - Add import bundle workflow for auto-populating new programs Co-authored-by: Cursor <cursoragent@cursor.com>
0 parents  commit 3c97518

38 files changed

Lines changed: 2700 additions & 0 deletions

.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Local Nexus Controller configuration (DO NOT put real secrets in git)
2+
#
3+
# SQLite database file path (relative to project root is OK)
4+
LOCAL_NEXUS_DB_PATH=data/local_nexus.db
5+
6+
# Web server bind
7+
LOCAL_NEXUS_HOST=127.0.0.1
8+
LOCAL_NEXUS_PORT=5010
9+
10+
# Optional: protect write actions with a token.
11+
# If set, clients must send header: X-Local-Nexus-Token: <token>
12+
LOCAL_NEXUS_TOKEN=
13+
14+
# Port assignment defaults
15+
LOCAL_NEXUS_PORT_RANGE_START=3000
16+
LOCAL_NEXUS_PORT_RANGE_END=3999
17+
18+
# Logs
19+
LOCAL_NEXUS_LOG_DIR=data/logs

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.env
2+
.venv/
3+
__pycache__/
4+
*.pyc
5+
6+
# Local registry + logs
7+
data/*.db
8+
data/*.sqlite
9+
data/logs/*.log
10+
11+
# OS/editor
12+
.DS_Store
13+
Thumbs.db
14+
.vscode/
15+
16+
# Python build artifacts
17+
dist/
18+
build/
19+
*.egg-info/
20+

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Local Nexus Controller
2+
3+
Local Nexus Controller is a **Windows-friendly local dashboard + API** to **register, document, monitor, and control** all locally-hosted programs/services you build.
4+
5+
It provides:
6+
- **Service registry** (SQLite)
7+
- **Database registry** (SQLite)
8+
- **Port management** (assignment + conflict detection)
9+
- **Secrets/keys references** (env-var references only; never store real secrets)
10+
- **Process control** (start/stop/restart; logs captured to `data/logs/`)
11+
- **Web dashboard** with drill-downs and an **Import Bundle** feature for auto-populating new programs
12+
13+
## Quick start (PowerShell)
14+
15+
From `C:\Users\nedpe\LocalNexusController`:
16+
17+
```powershell
18+
python -m venv .venv
19+
.\.venv\Scripts\Activate.ps1
20+
pip install -r requirements.txt
21+
copy .env.example .env
22+
python -m local_nexus_controller
23+
```
24+
25+
Then open:
26+
- Dashboard: `http://127.0.0.1:5010`
27+
- API docs (Swagger): `http://127.0.0.1:5010/docs`
28+
29+
## Import sample registry (4 services + 2 databases)
30+
31+
```powershell
32+
python .\tools\import_bundle.py .\sample_data\import_bundle.json
33+
```
34+
35+
Or use the dashboard:
36+
- Go to **Import** and paste the JSON bundle.
37+
38+
## Import your existing local programs (auto-generated)
39+
40+
This repo includes a bundle based on what was found on your machine:
41+
42+
```powershell
43+
python .\tools\import_bundle.py .\sample_data\import_existing_bundle.json
44+
```
45+
46+
Or use the dashboard:
47+
- Go to **Import** and paste `sample_data/import_existing_bundle.json`.
48+
49+
## Auto-populating new programs you ask me to generate
50+
51+
When you ask for a new program, you (and I) will produce an **Import Bundle JSON** that you can paste into:
52+
- Dashboard → **Import**, or
53+
- `POST /api/import/bundle`
54+
55+
This ensures every new local program is automatically registered, categorized, assigned a port, and optionally assigned a database.
56+
57+
## Notes
58+
59+
- The controller stores **only references** to secrets (e.g., `OPENAI_API_KEY`) and where they are used. It never stores secret values.
60+
- Process control uses stored `start_command`/`stop_command` or the controller can terminate the tracked PID tree.

local_nexus_controller/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = ["__version__"]
2+
3+
__version__ = "0.1.0"

local_nexus_controller/__main__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import uvicorn
2+
3+
from local_nexus_controller.settings import settings
4+
5+
6+
def main() -> None:
7+
uvicorn.run(
8+
"local_nexus_controller.main:app",
9+
host=settings.host,
10+
port=settings.port,
11+
reload=settings.reload,
12+
)
13+
14+
15+
if __name__ == "__main__":
16+
main()

local_nexus_controller/db.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import Generator
5+
6+
from sqlalchemy import text
7+
from sqlmodel import Session, SQLModel, create_engine
8+
9+
from local_nexus_controller.settings import settings
10+
11+
12+
def _ensure_parent_dir(path: Path) -> None:
13+
path.parent.mkdir(parents=True, exist_ok=True)
14+
15+
16+
_ensure_parent_dir(settings.db_path)
17+
engine = create_engine(
18+
f"sqlite:///{settings.db_path.as_posix()}",
19+
connect_args={"check_same_thread": False},
20+
)
21+
22+
23+
def init_db() -> None:
24+
SQLModel.metadata.create_all(engine)
25+
_sqlite_migrate()
26+
27+
28+
def _sqlite_migrate() -> None:
29+
"""
30+
Lightweight, additive migrations for local SQLite.
31+
We only ADD columns; never drop/rename.
32+
"""
33+
34+
with engine.begin() as conn:
35+
# Service.env_overrides (added in v0.2)
36+
cols = [row[1] for row in conn.execute(text("PRAGMA table_info(service)")).fetchall()]
37+
if cols and "env_overrides" not in cols:
38+
conn.execute(text("ALTER TABLE service ADD COLUMN env_overrides TEXT"))
39+
40+
41+
def get_session() -> Generator[Session, None, None]:
42+
with Session(engine) as session:
43+
yield session

local_nexus_controller/main.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from fastapi import FastAPI
6+
from fastapi.staticfiles import StaticFiles
7+
8+
from local_nexus_controller.db import init_db
9+
from local_nexus_controller.routers.api_databases import router as api_databases_router
10+
from local_nexus_controller.routers.api_import import router as api_import_router
11+
from local_nexus_controller.routers.api_keys import router as api_keys_router
12+
from local_nexus_controller.routers.api_ports import router as api_ports_router
13+
from local_nexus_controller.routers.api_services import router as api_services_router
14+
from local_nexus_controller.routers.api_summary import router as api_summary_router
15+
from local_nexus_controller.routers.ui import router as ui_router
16+
17+
18+
app = FastAPI(title="Local Nexus Controller", version="0.1.0")
19+
20+
static_dir = Path(__file__).resolve().parent / "static"
21+
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
22+
23+
app.include_router(ui_router)
24+
app.include_router(api_services_router, prefix="/api/services", tags=["services"])
25+
app.include_router(api_databases_router, prefix="/api/databases", tags=["databases"])
26+
app.include_router(api_ports_router, prefix="/api/ports", tags=["ports"])
27+
app.include_router(api_keys_router, prefix="/api/keys", tags=["keys"])
28+
app.include_router(api_import_router, prefix="/api/import", tags=["import"])
29+
app.include_router(api_summary_router, prefix="/api/summary", tags=["summary"])
30+
31+
32+
@app.on_event("startup")
33+
def _startup() -> None:
34+
init_db()

local_nexus_controller/models.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
from __future__ import annotations
2+
3+
import uuid
4+
from datetime import datetime, timezone
5+
from typing import Any, Optional
6+
7+
from sqlalchemy import Column
8+
from sqlalchemy.types import JSON
9+
from sqlmodel import Field, Relationship, SQLModel
10+
11+
12+
def _now_utc() -> datetime:
13+
return datetime.now(timezone.utc)
14+
15+
16+
def _uuid_str() -> str:
17+
return str(uuid.uuid4())
18+
19+
20+
class Service(SQLModel, table=True):
21+
id: str = Field(default_factory=_uuid_str, primary_key=True, index=True)
22+
23+
name: str = Field(index=True)
24+
description: str = Field(default="")
25+
category: str = Field(default="general", index=True)
26+
27+
tags: list[str] = Field(default_factory=list, sa_column=Column(JSON))
28+
tech_stack: list[str] = Field(default_factory=list, sa_column=Column(JSON))
29+
dependencies: list[str] = Field(default_factory=list, sa_column=Column(JSON))
30+
config_paths: list[str] = Field(default_factory=list, sa_column=Column(JSON))
31+
32+
port: Optional[int] = Field(default=None, index=True)
33+
local_url: Optional[str] = Field(default=None)
34+
healthcheck_url: Optional[str] = Field(default=None)
35+
36+
working_directory: Optional[str] = Field(default=None)
37+
start_command: str = Field(default="")
38+
stop_command: str = Field(default="")
39+
restart_command: str = Field(default="")
40+
41+
# Environment overrides passed at runtime (safe values only; never secrets here).
42+
# Supports placeholders {PORT} and {HOST}.
43+
env_overrides: dict[str, str] = Field(default_factory=dict, sa_column=Column(JSON))
44+
45+
# Runtime tracking
46+
status: str = Field(default="stopped", index=True) # running|stopped|error|unknown
47+
process_pid: Optional[int] = Field(default=None, index=True)
48+
process_started_at: Optional[datetime] = Field(default=None)
49+
last_error: Optional[str] = Field(default=None)
50+
log_path: Optional[str] = Field(default=None)
51+
52+
# Database linkage
53+
database_id: Optional[str] = Field(default=None, foreign_key="database.id", index=True)
54+
database_connection_string: Optional[str] = Field(default=None)
55+
database_schema_overview: Optional[str] = Field(default=None)
56+
57+
created_at: datetime = Field(default_factory=_now_utc)
58+
updated_at: datetime = Field(default_factory=_now_utc)
59+
60+
database: Optional["Database"] = Relationship(back_populates="services")
61+
keys: list["KeyRef"] = Relationship(back_populates="service")
62+
63+
64+
class Database(SQLModel, table=True):
65+
id: str = Field(default_factory=_uuid_str, primary_key=True, index=True)
66+
67+
database_name: str = Field(index=True, unique=True)
68+
type: str = Field(default="sqlite", index=True) # sqlite|postgres|mysql|mongo|...
69+
70+
host: Optional[str] = Field(default="localhost")
71+
port: Optional[int] = Field(default=None)
72+
73+
username_env: Optional[str] = Field(default=None)
74+
password_env: Optional[str] = Field(default=None)
75+
76+
connection_string: Optional[str] = Field(default=None)
77+
schema_overview: Optional[str] = Field(default=None)
78+
79+
created_at: datetime = Field(default_factory=_now_utc)
80+
updated_at: datetime = Field(default_factory=_now_utc)
81+
82+
services: list[Service] = Relationship(back_populates="database")
83+
84+
85+
class KeyRef(SQLModel, table=True):
86+
id: str = Field(default_factory=_uuid_str, primary_key=True, index=True)
87+
88+
service_id: str = Field(foreign_key="service.id", index=True)
89+
key_name: str = Field(index=True)
90+
env_var: str = Field(index=True)
91+
description: str = Field(default="")
92+
93+
created_at: datetime = Field(default_factory=_now_utc)
94+
95+
service: Service = Relationship(back_populates="keys")
96+
97+
98+
# -----------------------
99+
# API schemas (non-table)
100+
# -----------------------
101+
102+
class ServiceCreate(SQLModel):
103+
name: str
104+
description: str = ""
105+
category: str = "general"
106+
tags: list[str] = Field(default_factory=list)
107+
tech_stack: list[str] = Field(default_factory=list)
108+
dependencies: list[str] = Field(default_factory=list)
109+
config_paths: list[str] = Field(default_factory=list)
110+
port: Optional[int] = None
111+
local_url: Optional[str] = None
112+
healthcheck_url: Optional[str] = None
113+
working_directory: Optional[str] = None
114+
start_command: str = ""
115+
stop_command: str = ""
116+
restart_command: str = ""
117+
env_overrides: dict[str, str] = Field(default_factory=dict)
118+
database_id: Optional[str] = None
119+
database_connection_string: Optional[str] = None
120+
database_schema_overview: Optional[str] = None
121+
122+
123+
class ServiceUpdate(SQLModel):
124+
name: Optional[str] = None
125+
description: Optional[str] = None
126+
category: Optional[str] = None
127+
tags: Optional[list[str]] = None
128+
tech_stack: Optional[list[str]] = None
129+
dependencies: Optional[list[str]] = None
130+
config_paths: Optional[list[str]] = None
131+
port: Optional[int] = None
132+
local_url: Optional[str] = None
133+
healthcheck_url: Optional[str] = None
134+
working_directory: Optional[str] = None
135+
start_command: Optional[str] = None
136+
stop_command: Optional[str] = None
137+
restart_command: Optional[str] = None
138+
env_overrides: Optional[dict[str, str]] = None
139+
database_id: Optional[str] = None
140+
database_connection_string: Optional[str] = None
141+
database_schema_overview: Optional[str] = None
142+
status: Optional[str] = None
143+
process_pid: Optional[int] = None
144+
last_error: Optional[str] = None
145+
log_path: Optional[str] = None
146+
147+
148+
class DatabaseCreate(SQLModel):
149+
database_name: str
150+
type: str = "sqlite"
151+
host: Optional[str] = "localhost"
152+
port: Optional[int] = None
153+
username_env: Optional[str] = None
154+
password_env: Optional[str] = None
155+
connection_string: Optional[str] = None
156+
schema_overview: Optional[str] = None
157+
158+
159+
class DatabaseUpdate(SQLModel):
160+
database_name: Optional[str] = None
161+
type: Optional[str] = None
162+
host: Optional[str] = None
163+
port: Optional[int] = None
164+
username_env: Optional[str] = None
165+
password_env: Optional[str] = None
166+
connection_string: Optional[str] = None
167+
schema_overview: Optional[str] = None
168+
169+
170+
class KeyRefCreate(SQLModel):
171+
key_name: str
172+
env_var: str
173+
description: str = ""
174+
175+
176+
class ImportBundle(SQLModel):
177+
"""
178+
This is the JSON shape used for auto-populating new programs.
179+
Paste this into the dashboard Import page or POST it to /api/import/bundle.
180+
"""
181+
182+
service: ServiceCreate
183+
database: Optional[DatabaseCreate] = None
184+
keys: list[KeyRefCreate] = Field(default_factory=list)
185+
requested_port: Optional[int] = None
186+
auto_assign_port: bool = True
187+
auto_create_db: bool = True
188+
meta: dict[str, Any] = Field(default_factory=dict)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# FastAPI routers live here.

0 commit comments

Comments
 (0)