# Python Tools This image is a Python 3 task runner with ExifTool and a small web control panel for running known, mounted Python maintenance tasks. It starts the control panel with `python -m app`. ## Control Panel The application discovers direct `*.py` files in each non-hidden directory under `/opt/tasks`. Each directory is a task group. Tasks in one group run sequentially; different groups may run concurrently. It records run history and combined output in SQLite under `/var/lib/server-maintenance`, and supports cancellation of active task process groups. The image contains application code only. Mount these paths at runtime: - `/opt/tasks` read-only: reviewed task scripts and their input manifests. - `/opt/credentials` read-only: named credential files used by those scripts. - `/var/lib/server-maintenance`: persistent SQLite data, logs, and task state. Task processes receive a minimal environment. To forward non-secret container settings to tasks, list their names in `SERVER_MAINTENANCE_TASK_ENV`, separated by commas. This allowlist prevents unrelated container configuration from being exposed to every task; credentials should remain in `/opt/credentials`. Tasks are never imported during discovery. Their optional description is the first line of their module docstring. A task must not require stdin or command line arguments, should write useful output, and must return a non-zero exit code on failure. ## Task definitions Each non-hidden directory directly below `/opt/tasks` is a task group. Its direct `*.py` files are the exposed tasks. Use an optional `task-inputs.json` beside those files to define the group's title, task titles and descriptions, task order, and web form fields. It is parsed as data only and currently uses version `1`: ```json { "version": 1, "title": "Example Tasks", "tasks": { "inspect.py": { "title": "Inspect item", "description": "Checks one item without changing it.", "inputs": [ {"name": "identifier", "label": "Identifier", "type": "text", "required": true}, {"name": "attempts", "label": "Attempts", "type": "integer", "minimum": 1, "maximum": 5, "default": 2}, {"name": "mode", "label": "Mode", "type": "choice", "options": ["standard", "extended"]}, {"name": "alerts", "label": "Alerts", "type": "multi_choice", "options": ["email", "webhook"]}, {"name": "review_csv", "label": "Review CSV", "type": "file", "accept": [".csv"], "maximum_bytes": 10485760} ], "wait_for_result": true, "execution": { "field": "mode", "dry_run_value": "review", "dry_run_label": "Review", "execute_value": "apply", "execute_label": "Execute" } } } } ``` `title` is optional on a group and task. A group otherwise uses its directory name; a task otherwise uses its filename without `.py`, with underscores made readable. `description` is optional on a task and otherwise comes from the first non-empty line of its module docstring. The order of keys in `tasks` controls the listed tasks' display order. Include a task as `{}` when only ordering is needed. Unlisted direct Python files follow alphabetically. Each task declaration may contain `title`, `description`, `inputs`, `wait_for_result`, `download_artifacts`, and `execution`. Set `wait_for_result` for short tasks where the form should remain blocked until it reports success or failure. The control panel disables its controls and warns before page unload while such a task runs. `download_artifacts` additionally starts the first non-JSON artifact download after a successful browser run; reserve it for files such as archives that the browser should download automatically. Every input has `name`, `label`, and `type`; `required` defaults to `false`. `default` is allowed for every type except `file`. - `text`: optional `pattern`, a Python-compatible regular expression. - `integer`: optional `minimum`, `maximum`, and positive `step`. - `date` and `datetime`: ISO-formatted browser date controls. - `choice`: `options` is required, as strings or `{ "value", "label" }` objects. - `multi_choice`: the same required `options`; its `default` is an array. - `file`: optional lowercase extension list in `accept` and positive byte cap in `maximum_bytes`. Set `sensitive` to `true` on text or file inputs that must not appear in run history. Text is masked, the database records `[redacted]`, and the original value is available only through a private execution-input file. Uploaded files are likewise retained privately for the run and then removed. For review-first tasks, use `execution`. It produces one split button: click the main part to use its current mode, or use the arrow to select the other mode. A successful review run can be repeated with its saved inputs in the execution mode from the run-detail page. The task must still revalidate the live filesystem before changing anything. ```json { "field": "mode", "dry_run_value": "review", "dry_run_label": "Review", "execute_value": "apply", "execute_label": "Execute" } ``` The server validates every submitted value before a run is queued. It records the validated object in the run database and writes it to a per-run file. Sensitive fields are redacted in the database and use a private `0600` execution file instead. Tasks read the applicable file path from `SERVER_MAINTENANCE_INPUT`; no user value is appended to the command line. A task without an entry in the manifest gets an empty JSON object. Uploaded files are stored in a private per-run directory; their absolute paths are provided in the input JSON and the files are removed when the task finishes, fails, or is cancelled. ## Artifacts Tasks can write files to the directory named by `SERVER_MAINTENANCE_ARTIFACTS`. Run-detail pages show them when the task finishes. JSON artifacts offer an **Open** link that renders the JSON in a new tab and leaves the artifact available; other artifacts offer a download link. The server deletes an artifact after its download transfer ends; any artifact not downloaded is removed after 24 hours. A browser cannot confirm that a downloaded file was retained on the client device. ## Background tasks An optional `background-tasks.json` beside a group's task files declares internal recurring scripts. These scripts must live below the group's `internal/` directory, are not exposed in the UI, and are launched at most once per configured interval. Their latest output is retained below the persistent state directory rather than being added to normal run history. ```json { "version": 1, "tasks": [ {"id": "poll", "script": "internal/poll.py", "interval_seconds": 60} ] } ``` `compose.example.yaml` is suitable for adding the service to an existing Compose stack. Set `PYTHON_TOOLS_VOLUME_ROOT` to the host directory that will hold `state`, `tasks`, and `credentials`; set `SERVER_MAINTENANCE_ALLOWED_PROXY_IPS` to the reverse proxy's direct peer address. Keep the application behind an authenticated reverse proxy.