feat: Add server management and database migrations

- Add start/stop/restart/status commands to Makefile
- Add health endpoint with uptime tracking
- Add CLI module (esp32-web command)
- Add initial database migration
- Listen on all interfaces (0.0.0.0:5500)

Bump version to 0.1.1
This commit is contained in:
user
2026-02-05 21:03:35 +01:00
parent a676136f5d
commit a8f616970a
9 changed files with 550 additions and 8 deletions

View File

@@ -1,12 +1,19 @@
"""ESP32-Web Flask Application."""
from datetime import datetime, UTC
from flask import Flask
from .config import Config
from .extensions import db, migrate
# Track app start time
_start_time = None
def create_app(config_class=Config):
"""Application factory."""
global _start_time
_start_time = datetime.now(UTC)
app = Flask(__name__)
app.config.from_object(config_class)
@@ -18,10 +25,20 @@ def create_app(config_class=Config):
from .api import bp as api_bp
app.register_blueprint(api_bp, url_prefix='/api/v1')
# Health check
# Health check with uptime
@app.route('/health')
def health():
return {'status': 'ok'}
uptime_seconds = int((datetime.now(UTC) - _start_time).total_seconds())
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
uptime_str = f'{days}d{hours}h{minutes}m'
elif hours > 0:
uptime_str = f'{hours}h{minutes}m{seconds}s'
else:
uptime_str = f'{minutes}m{seconds}s'
return {'status': 'ok', 'uptime': uptime_str, 'uptime_seconds': uptime_seconds}
# Start UDP collector in non-testing mode
if not app.config.get('TESTING'):

139
src/esp32_web/cli.py Normal file
View File

@@ -0,0 +1,139 @@
"""CLI commands for managing the application."""
import os
import signal
import sys
from pathlib import Path
import click
PIDFILE = Path('/tmp/esp32-web.pid')
LOGFILE = Path('/tmp/esp32-web.log')
def get_pid() -> int | None:
"""Get PID from pidfile if running."""
if not PIDFILE.exists():
return None
try:
pid = int(PIDFILE.read_text().strip())
# Check if process exists
os.kill(pid, 0)
return pid
except (ValueError, ProcessLookupError, PermissionError):
PIDFILE.unlink(missing_ok=True)
return None
def is_running() -> bool:
"""Check if server is running."""
return get_pid() is not None
@click.group()
def cli():
"""ESP32-Web server management."""
pass
@cli.command()
@click.option('--port', default=5500, help='Port to listen on')
@click.option('--host', default='0.0.0.0', help='Host to bind to')
@click.option('--debug', is_flag=True, help='Enable debug mode')
def start(port: int, host: str, debug: bool):
"""Start the server."""
if is_running():
click.echo(f'Server already running (PID {get_pid()})')
sys.exit(1)
click.echo(f'Starting server on {host}:{port}...')
pid = os.fork()
if pid > 0:
# Parent - write pidfile and exit
PIDFILE.write_text(str(pid))
click.echo(f'Server started (PID {pid})')
click.echo(f'Logs: {LOGFILE}')
sys.exit(0)
# Child - become daemon
os.setsid()
# Redirect stdout/stderr to logfile
log_fd = os.open(str(LOGFILE), os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
os.dup2(log_fd, sys.stdout.fileno())
os.dup2(log_fd, sys.stderr.fileno())
# Import and run app
from esp32_web import create_app
app = create_app()
app.run(host=host, port=port, debug=debug, use_reloader=False)
@cli.command()
def stop():
"""Stop the server."""
pid = get_pid()
if not pid:
click.echo('Server not running')
sys.exit(1)
click.echo(f'Stopping server (PID {pid})...')
try:
os.kill(pid, signal.SIGTERM)
PIDFILE.unlink(missing_ok=True)
click.echo('Server stopped')
except ProcessLookupError:
PIDFILE.unlink(missing_ok=True)
click.echo('Server was not running')
@cli.command()
@click.option('--port', default=5500, help='Port to listen on')
@click.option('--host', default='0.0.0.0', help='Host to bind to')
@click.option('--debug', is_flag=True, help='Enable debug mode')
@click.pass_context
def restart(ctx, port: int, host: str, debug: bool):
"""Restart the server."""
if is_running():
ctx.invoke(stop)
import time
time.sleep(1)
ctx.invoke(start, port=port, host=host, debug=debug)
@cli.command()
def status():
"""Show server status."""
pid = get_pid()
if pid:
click.echo(f'Server running (PID {pid})')
click.echo(f'Logs: {LOGFILE}')
# Try to get health status
try:
import urllib.request
with urllib.request.urlopen('http://localhost:5500/health', timeout=2) as resp:
click.echo(f'Health: {resp.read().decode()}')
except Exception:
click.echo('Health: unreachable')
else:
click.echo('Server not running')
sys.exit(1)
@cli.command()
@click.option('-n', '--lines', default=50, help='Number of lines to show')
@click.option('-f', '--follow', is_flag=True, help='Follow log output')
def logs(lines: int, follow: bool):
"""Show server logs."""
if not LOGFILE.exists():
click.echo('No logs found')
sys.exit(1)
if follow:
os.execvp('tail', ['tail', '-f', str(LOGFILE)])
else:
os.execvp('tail', ['tail', '-n', str(lines), str(LOGFILE)])
if __name__ == '__main__':
cli()