CronFrog Logo

Technical Documentation

A complete guide to CronFrog's architecture, codebase, and API surface.

1. Overview

CronFrog is a self-contained job scheduling platform combining a Python backend (FastAPI + APScheduler) with a modern single-page web interface. It allows users to:

  • Create scheduled jobs using standard 5-field cron expressions
  • Execute shell commands or scripts on the host machine
  • Monitor runs in real-time via WebSocket-powered live log streaming
  • Manage everything through a REST API or the built-in web dashboard

The entire application runs as a single process — no external message brokers, no Redis, no Celery. Just Python, SQLite, and a browser.

Key Design Decisions

DecisionRationale
SQLiteZero-configuration, serverless, perfect for single-node deployments
APScheduler (AsyncIO)In-process scheduler sharing the same event loop as FastAPI
WebSocketsEliminates polling; real-time stdout/stderr streaming
Static SPAVanilla JS frontend — no npm, no bundler, no build step
asyncio subprocessNon-blocking execution within the async event loop

2. Architecture

CronFrog follows a monolithic, single-process architecture. The HTTP server, background scheduler, and job executor all run within the same Python process and share a common asyncio event loop.

CronFrog Architecture Overview

How the Three Engines Cooperate

EngineRoleLifecycle
FastAPI HTTP server + WebSocket host Starts with uvicorn.run(), serves API and static files
APScheduler Background cron engine Started in lifespan() context, polls cron triggers on the event loop
Executor Subprocess manager Invoked by APScheduler or the API; spawns asyncio subprocesses

All three share the same event loop, so the scheduler can call execute_job() without thread synchronization, the executor can broadcast logs to WebSocket clients directly, and the API can fire-and-forget a job via asyncio.create_task().

3. Project Structure

cronfrog/
├── run.py                      # Application entry point
├── requirements.txt            # Python dependencies
├── LICENSE                     # MIT License
├── server/                     # Backend Python package
│   ├── __init__.py
│   ├── main.py                 # FastAPI app, lifespan, WebSocket, static mount
│   ├── api.py                  # REST API route definitions
│   ├── database.py             # SQLite data access layer
│   ├── scheduler.py            # APScheduler configuration
│   └── executor.py             # Async subprocess execution + log broadcasting
├── public/                     # Frontend static SPA
│   ├── index.html              # Single-page HTML
│   ├── css/style.css           # Styling
│   ├── js/                     # api.js, app.js, components.js, cron-builder.js, websocket.js
│   └── static/                 # Logo assets
├── data/cronfrog.db            # SQLite database (dev copy)
└── docs/                       # Documentation assets

4. Backend Deep Dive

4.1 Entry Point — run.py

The entry point is deliberately minimal. It ensures the project root is on sys.path, imports the FastAPI app instance from server.main, and starts the Uvicorn ASGI server. Port is configurable via the PORT environment variable (default: 8000).

4.2 Application Bootstrap — server/main.py

Uses FastAPI's lifespan async context manager to:

  • Initialize the database — creates tables if they don't exist
  • Start APScheduler — begins the background event loop
  • Load saved jobs — re-registers all enabled jobs from the database
  • Mount static files — serves the public/ directory at /

CORS middleware is configured to allow all origins. The WebSocket endpoint at /api/ws/runs/{run_id} handles live log connections with a late-join buffer replay.

4.3 REST API — server/api.py

All routes are prefixed with /api and handle CRUD operations for jobs, manual triggers, run history, and stats. When a job is created or updated with enabled=True, it's automatically registered with APScheduler. Manual runs use asyncio.create_task() for fire-and-forget execution.

4.4 Database Layer — server/database.py

A thin Data Access Layer (DAL) over SQLite using Python's built-in sqlite3 module. Each function opens a connection, performs the operation, and closes it. The database defaults to ~/.cronfrog/data/cronfrog.db to prevent Uvicorn --reload restart loops.

4.5 Scheduler — server/scheduler.py

Wraps APScheduler's AsyncIOScheduler. Converts standard 5-field cron expressions into trigger objects using CronTrigger.from_crontab(). On startup, all enabled jobs are re-registered (load_jobs_into_scheduler()).

4.6 Executor — server/executor.py

The most complex module. Manages the full lifecycle of job execution:

  • Creates a run record in the database
  • Merges environment variables and resolves the shell executable
  • Spawns an async subprocess via asyncio.create_subprocess_shell()
  • Reads stdout/stderr concurrently with asyncio.gather()
  • Broadcasts each line to connected WebSocket clients in real-time
  • Finalizes the run record with exit code, output, and status

Maintains three in-memory registries: active_processes (for kill support), active_logs (buffer for late-joining clients), and run_websockets (connected viewers).

5. Database Schema

CronFrog Database Schema

jobs Table

ColumnTypeDescription
idINTEGER PKUnique job identifier (auto-increment)
nameTEXT NOT NULLHuman-readable job name
commandTEXT NOT NULLShell command to execute
cron_expressionTEXTStandard 5-field cron expression
timezoneTEXTTimezone for cron evaluation (default: UTC)
enabledBOOLEANWhether the job is active in the scheduler
working_directoryTEXTWorking directory for the subprocess
shellTEXTShell executable (e.g., /bin/bash)
env_varsTEXTJSON string of environment variables
created_atTIMESTAMPRecord creation time
updated_atTIMESTAMPLast modification time
last_run_atTIMESTAMPMost recent execution timestamp
last_statusTEXTMost recent execution status

job_runs Table

ColumnTypeDescription
idINTEGER PKUnique run identifier (auto-increment)
job_idINTEGER FKParent job reference → jobs(id)
started_atTIMESTAMPExecution start time
finished_atTIMESTAMPExecution end time
exit_codeINTEGERProcess exit code (0 = success)
stdoutTEXTCaptured standard output
stderrTEXTCaptured standard error
statusTEXTrunning, success, or failed

Relationship: Each job can have many run records (1:N). Runs are ordered by id DESC and limited to 50 per query.

6. API Reference

All endpoints are prefixed with /api. The auto-generated Swagger UI is available at /docs when the server is running.

Jobs CRUD

MethodEndpointDescription
GET/api/jobsList all jobs with next run times
POST/api/jobsCreate a new job
GET/api/jobs/{id}Get a single job by ID
PUT/api/jobs/{id}Update a job's configuration
DELETE/api/jobs/{id}Delete a job

Job Control

MethodEndpointDescription
POST/api/jobs/{id}/startEnable and schedule a job
POST/api/jobs/{id}/stopDisable and unschedule a job
POST/api/jobs/{id}/runTrigger immediate execution
POST/api/jobs/{id}/runs/{rid}/killTerminate a running process

Runs & Stats

MethodEndpointDescription
GET/api/jobs/{id}/runsList last 50 runs for a job
GET/api/jobs/{id}/runs/{rid}Get details of a specific run
GET/api/statsDashboard statistics (total, active, failed)

Example Response — GET /api/stats

{
    "total_jobs": 5,
    "active_jobs": 3,
    "failed_runs": 1
}

7. WebSocket Protocol

Endpoint: ws://localhost:8000/api/ws/runs/{run_id}

All messages are JSON-encoded strings pushed from the server to the client.

Message Types

TypeDescriptionExample
stdoutA line of standard output{"type":"stdout","data":"Backup complete\n"}
stderrA line of standard error{"type":"stderr","data":"Warning: low disk\n"}
closeServer closing the stream{"type":"close"}

Late-Join Buffer

If a client connects after a job has started, the server replays all buffered log lines from active_logs[run_id] before streaming new lines. This ensures no output is ever missed.

8. Frontend Deep Dive

The frontend is a vanilla JavaScript SPA with no build step, no framework, and no npm dependencies. It's served directly as static files by FastAPI.

Module Breakdown

FilePurpose
api.jsFetch-based HTTP client wrapping all /api/* calls
app.jsCentral controller with hash-based router (window.location.hash)
components.jsStateless HTML template renderers for stats cards, table rows, badges
cron-builder.jsInteractive cron expression builder with preset tabs
websocket.jsLogStreamer class for WebSocket connections and auto-scrolling terminal

Route Map

HashViewData Loader
#dashboardDashboardFetches stats + active jobs
#jobsJobs listFetches all jobs, applies search filter
#job-edit/{id}Edit formPopulates form from API or resets for new
#job-detail/{id}Detail + terminalLoads runs, connects WebSocket

Design System

The CSS implements a dark-mode glassmorphism theme using CSS custom properties. Key tokens include emerald green (#10b981) as the primary accent, Inter as the sans-serif font, Fira Code for monospace, and translucent backdrop-filter: blur() panels.

9. Job Execution Flow

CronFrog Job Execution Flow

Step-by-Step

  • Trigger: Manual (POST /api/jobs/{id}/run) or scheduled (APScheduler cron trigger)
  • Prepare: Fetch job config, create run record, init log buffer, merge env vars, resolve shell
  • Execute: Spawn subprocess, register in active_processes, launch concurrent stream readers
  • Stream: Each line appended to buffer and pushed via WebSocket to all connected clients
  • Complete: Capture exit code, determine status, write final output to DB, clean up registries, close WebSockets

10. Configuration & Environment

CronFrog is configured entirely through environment variables. No config files needed.

VariableDefaultDescription
PORT8000TCP port for the HTTP/WebSocket server
CRONFROG_DB_PATH~/.cronfrog/data/cronfrog.dbAbsolute path to the SQLite database file

Examples

# Windows (PowerShell)
$env:PORT = 8080
$env:CRONFROG_DB_PATH = "C:\data\cronfrog.db"
python run.py

# Linux / macOS
PORT=8080 CRONFROG_DB_PATH="/data/cronfrog.db" python run.py

11. Dependencies

PackagePurpose
fastapiASGI web framework for REST API + WebSocket support
uvicorn[standard]ASGI server with WebSocket support
apschedulerBackground job scheduler with cron trigger support
websocketsWebSocket protocol implementation (used by Uvicorn)

Additionally uses Python standard library modules: sqlite3, asyncio, json, os, sys, pathlib, datetime, contextlib.