3 Commits
0.5.0 ... 0.6.0

Author SHA1 Message Date
ebfcfb969a release: version 0.6.0 🚀
All checks were successful
Upload Python Package / Create Release (push) Successful in 17s
Upload Python Package / deploy (push) Successful in 42s
2026-04-10 20:43:36 +02:00
56b05eb686 feat(api): expose database target in health refs NOISSUE 2026-04-10 20:39:36 +02:00
59a7e9787e fix(db): prefer postgres config in production refs NOISSUE 2026-04-10 20:37:31 +02:00
7 changed files with 101 additions and 13 deletions

View File

@@ -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

View File

@@ -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 |

View File

@@ -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

View File

@@ -1 +1 @@
0.5.0 0.6.0

View File

@@ -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}"

View File

@@ -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:

View File

@@ -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'],
} }