Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebfcfb969a | |||
| 56b05eb686 | |||
| 59a7e9787e |
@@ -4,6 +4,14 @@ Changelog
|
|||||||
|
|
||||||
(unreleased)
|
(unreleased)
|
||||||
------------
|
------------
|
||||||
|
- Feat(api): expose database target in health refs NOISSUE. [Simon
|
||||||
|
Diesenreiter]
|
||||||
|
- Fix(db): prefer postgres config in production refs NOISSUE. [Simon
|
||||||
|
Diesenreiter]
|
||||||
|
|
||||||
|
|
||||||
|
0.5.0 (2026-04-10)
|
||||||
|
------------------
|
||||||
- Feat(dashboard): expose repository urls refs NOISSUE. [Simon
|
- Feat(dashboard): expose repository urls refs NOISSUE. [Simon
|
||||||
Diesenreiter]
|
Diesenreiter]
|
||||||
- Feat(factory): serve dashboard at root and create project repos refs
|
- Feat(factory): serve dashboard at root and create project repos refs
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -54,6 +54,15 @@ GITEA_OWNER=ai-software-factory
|
|||||||
# Optional legacy fixed-repository mode. Leave empty to create one repo per project.
|
# Optional legacy fixed-repository mode. Leave empty to create one repo per project.
|
||||||
GITEA_REPO=
|
GITEA_REPO=
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# In production, provide PostgreSQL settings. They take precedence over the SQLite default.
|
||||||
|
# Setting USE_SQLITE=false is still supported if you want to make the choice explicit.
|
||||||
|
POSTGRES_HOST=postgres.yourserver.com
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=ai_software_factory
|
||||||
|
POSTGRES_PASSWORD=change-me
|
||||||
|
POSTGRES_DB=ai_software_factory
|
||||||
|
|
||||||
# n8n
|
# n8n
|
||||||
N8N_WEBHOOK_URL=http://n8n.yourserver.com/webhook/telegram
|
N8N_WEBHOOK_URL=http://n8n.yourserver.com/webhook/telegram
|
||||||
|
|
||||||
@@ -90,6 +99,8 @@ docker-compose up -d
|
|||||||
|
|
||||||
Check your gitea repository for generated PRs
|
Check your gitea repository for generated PRs
|
||||||
|
|
||||||
|
If you deploy the container with PostgreSQL environment variables set, the service now selects PostgreSQL automatically even though SQLite remains the default for local/test usage.
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
| Endpoint | Method | Description |
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ TELEGRAM_BOT_TOKEN=your_telegram_bot_token
|
|||||||
TELEGRAM_CHAT_ID=your_chat_id
|
TELEGRAM_CHAT_ID=your_chat_id
|
||||||
|
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
|
# In production, provide PostgreSQL settings below. They now take precedence over the SQLite default.
|
||||||
|
# You can also set USE_SQLITE=false explicitly if you want the intent to be obvious.
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_USER=ai_test
|
POSTGRES_USER=ai_test
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.5.0
|
0.6.0
|
||||||
|
|||||||
@@ -66,6 +66,32 @@ class Settings(BaseSettings):
|
|||||||
DB_POOL_RECYCLE: int = 3600
|
DB_POOL_RECYCLE: int = 3600
|
||||||
DB_POOL_TIMEOUT: int = 30
|
DB_POOL_TIMEOUT: int = 30
|
||||||
|
|
||||||
|
@property
|
||||||
|
def postgres_url(self) -> str:
|
||||||
|
"""Get PostgreSQL URL with trimmed whitespace."""
|
||||||
|
return (self.POSTGRES_URL or "").strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def postgres_env_configured(self) -> bool:
|
||||||
|
"""Whether PostgreSQL was explicitly configured via environment variables."""
|
||||||
|
if self.postgres_url:
|
||||||
|
return True
|
||||||
|
postgres_env_keys = (
|
||||||
|
"POSTGRES_HOST",
|
||||||
|
"POSTGRES_PORT",
|
||||||
|
"POSTGRES_USER",
|
||||||
|
"POSTGRES_PASSWORD",
|
||||||
|
"POSTGRES_DB",
|
||||||
|
)
|
||||||
|
return any(bool(os.environ.get(key, "").strip()) for key in postgres_env_keys)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def use_sqlite(self) -> bool:
|
||||||
|
"""Whether SQLite should be used as the active database backend."""
|
||||||
|
if not self.USE_SQLITE:
|
||||||
|
return False
|
||||||
|
return not self.postgres_env_configured
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pool(self) -> dict:
|
def pool(self) -> dict:
|
||||||
"""Get database pool configuration."""
|
"""Get database pool configuration."""
|
||||||
@@ -79,8 +105,10 @@ class Settings(BaseSettings):
|
|||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
"""Get database connection URL."""
|
"""Get database connection URL."""
|
||||||
if self.USE_SQLITE:
|
if self.use_sqlite:
|
||||||
return f"sqlite:///{self.SQLITE_DB_PATH}"
|
return f"sqlite:///{self.SQLITE_DB_PATH}"
|
||||||
|
if self.postgres_url:
|
||||||
|
return self.postgres_url
|
||||||
return (
|
return (
|
||||||
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||||
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
||||||
@@ -89,8 +117,10 @@ class Settings(BaseSettings):
|
|||||||
@property
|
@property
|
||||||
def test_database_url(self) -> str:
|
def test_database_url(self) -> str:
|
||||||
"""Get test database connection URL."""
|
"""Get test database connection URL."""
|
||||||
if self.USE_SQLITE:
|
if self.use_sqlite:
|
||||||
return f"sqlite:///{self.SQLITE_DB_PATH}"
|
return f"sqlite:///{self.SQLITE_DB_PATH}"
|
||||||
|
if self.postgres_url:
|
||||||
|
return self.postgres_url
|
||||||
return (
|
return (
|
||||||
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||||
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_TEST_DB}"
|
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_TEST_DB}"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
@@ -17,10 +18,31 @@ except ImportError:
|
|||||||
from models import Base
|
from models import Base
|
||||||
|
|
||||||
|
|
||||||
|
def get_database_runtime_summary() -> dict[str, str]:
|
||||||
|
"""Return a human-readable summary of the effective database backend."""
|
||||||
|
if settings.use_sqlite:
|
||||||
|
db_path = str(Path(settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db").expanduser().resolve())
|
||||||
|
return {
|
||||||
|
"backend": "sqlite",
|
||||||
|
"target": db_path,
|
||||||
|
"database": db_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = urlparse(settings.database_url)
|
||||||
|
database_name = parsed.path.lstrip("/") or "unknown"
|
||||||
|
host = parsed.hostname or "unknown-host"
|
||||||
|
port = str(parsed.port or 5432)
|
||||||
|
return {
|
||||||
|
"backend": parsed.scheme.split("+", 1)[0] or "postgresql",
|
||||||
|
"target": f"{host}:{port}/{database_name}",
|
||||||
|
"database": database_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_engine() -> Engine:
|
def get_engine() -> Engine:
|
||||||
"""Create and return SQLAlchemy engine with connection pooling."""
|
"""Create and return SQLAlchemy engine with connection pooling."""
|
||||||
# Use SQLite for tests, PostgreSQL for production
|
# Use SQLite for tests, PostgreSQL for production
|
||||||
if settings.USE_SQLITE:
|
if settings.use_sqlite:
|
||||||
db_path = settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db"
|
db_path = settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db"
|
||||||
Path(db_path).expanduser().resolve().parent.mkdir(parents=True, exist_ok=True)
|
Path(db_path).expanduser().resolve().parent.mkdir(parents=True, exist_ok=True)
|
||||||
db_url = f"sqlite:///{db_path}"
|
db_url = f"sqlite:///{db_path}"
|
||||||
@@ -31,7 +53,7 @@ def get_engine() -> Engine:
|
|||||||
echo=settings.LOG_LEVEL == "DEBUG"
|
echo=settings.LOG_LEVEL == "DEBUG"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
db_url = settings.POSTGRES_URL or settings.database_url
|
db_url = settings.database_url
|
||||||
# PostgreSQL-specific configuration
|
# PostgreSQL-specific configuration
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
db_url,
|
db_url,
|
||||||
@@ -43,7 +65,7 @@ def get_engine() -> Engine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Event listener for connection checkout (PostgreSQL only)
|
# Event listener for connection checkout (PostgreSQL only)
|
||||||
if not settings.USE_SQLITE:
|
if not settings.use_sqlite:
|
||||||
@event.listens_for(engine, "checkout")
|
@event.listens_for(engine, "checkout")
|
||||||
def receive_checkout(dbapi_connection, connection_record, connection_proxy):
|
def receive_checkout(dbapi_connection, connection_record, connection_proxy):
|
||||||
"""Log connection checkout for audit purposes."""
|
"""Log connection checkout for audit purposes."""
|
||||||
@@ -100,7 +122,7 @@ def get_alembic_config(database_url: str | None = None) -> Config:
|
|||||||
alembic_ini = package_root / "alembic.ini"
|
alembic_ini = package_root / "alembic.ini"
|
||||||
config = Config(str(alembic_ini))
|
config = Config(str(alembic_ini))
|
||||||
config.set_main_option("script_location", str(package_root / "alembic"))
|
config.set_main_option("script_location", str(package_root / "alembic"))
|
||||||
config.set_main_option("sqlalchemy.url", database_url or (settings.database_url if not settings.USE_SQLITE else f"sqlite:///{settings.SQLITE_DB_PATH or '/tmp/ai_software_factory_test.db'}"))
|
config.set_main_option("sqlalchemy.url", database_url or settings.database_url)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@@ -116,7 +138,7 @@ def run_migrations(database_url: str | None = None) -> dict:
|
|||||||
|
|
||||||
def init_db() -> dict:
|
def init_db() -> dict:
|
||||||
"""Initialize database tables and database if needed."""
|
"""Initialize database tables and database if needed."""
|
||||||
if settings.USE_SQLITE:
|
if settings.use_sqlite:
|
||||||
result = run_migrations()
|
result = run_migrations()
|
||||||
if result["status"] == "success":
|
if result["status"] == "success":
|
||||||
print("SQLite database migrations applied successfully.")
|
print("SQLite database migrations applied successfully.")
|
||||||
@@ -131,7 +153,7 @@ def init_db() -> dict:
|
|||||||
return {'status': 'error', 'message': f'Error: {str(e)}'}
|
return {'status': 'error', 'message': f'Error: {str(e)}'}
|
||||||
else:
|
else:
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
db_url = settings.POSTGRES_URL or settings.database_url
|
db_url = settings.database_url
|
||||||
db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory'
|
db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -180,7 +202,7 @@ def init_db() -> dict:
|
|||||||
|
|
||||||
def drop_db() -> dict:
|
def drop_db() -> dict:
|
||||||
"""Drop all database tables (use with caution!)."""
|
"""Drop all database tables (use with caution!)."""
|
||||||
if settings.USE_SQLITE:
|
if settings.use_sqlite:
|
||||||
engine = get_engine()
|
engine = get_engine()
|
||||||
try:
|
try:
|
||||||
Base.metadata.drop_all(bind=engine)
|
Base.metadata.drop_all(bind=engine)
|
||||||
@@ -190,7 +212,7 @@ def drop_db() -> dict:
|
|||||||
print(f"Error dropping SQLite tables: {str(e)}")
|
print(f"Error dropping SQLite tables: {str(e)}")
|
||||||
return {'status': 'error', 'message': str(e)}
|
return {'status': 'error', 'message': str(e)}
|
||||||
else:
|
else:
|
||||||
db_url = settings.POSTGRES_URL or settings.database_url
|
db_url = settings.database_url
|
||||||
db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory'
|
db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ The NiceGUI frontend provides:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -42,7 +43,18 @@ except ImportError:
|
|||||||
|
|
||||||
__version__ = "0.0.1"
|
__version__ = "0.0.1"
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_app: FastAPI):
|
||||||
|
"""Log resolved runtime configuration when the app starts."""
|
||||||
|
runtime = database_module.get_database_runtime_summary()
|
||||||
|
print(
|
||||||
|
f"Runtime configuration: database_backend={runtime['backend']} target={runtime['target']}"
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
DbSession = Annotated[Session, Depends(database_module.get_db)]
|
DbSession = Annotated[Session, Depends(database_module.get_db)]
|
||||||
PROJECT_ID_PATTERN = re.compile(r"[^a-z0-9]+")
|
PROJECT_ID_PATTERN = re.compile(r"[^a-z0-9]+")
|
||||||
@@ -178,9 +190,12 @@ def read_api_info():
|
|||||||
@app.get('/health')
|
@app.get('/health')
|
||||||
def health_check():
|
def health_check():
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
|
runtime = database_module.get_database_runtime_summary()
|
||||||
return {
|
return {
|
||||||
'status': 'healthy',
|
'status': 'healthy',
|
||||||
'database': 'sqlite' if database_module.settings.USE_SQLITE else 'postgresql',
|
'database': runtime['backend'],
|
||||||
|
'database_target': runtime['target'],
|
||||||
|
'database_name': runtime['database'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user