Files
FabledSteward/steward/templates/settings/general.html
T
bvandeusenandClaude Opus 5.5 98854d94f9
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 1m11s
CI / unit (push) Successful in 1m31s
CI / publish (push) Successful in 2m34s
feat(platform): tini as PID 1, Hypercorn, a log-level setting, the image on CI's Python, an airgapped boot gate before push (#5159)
- Dockerfile: python:3.14-slim, the Python CI tests on (was 3.13). tini from
  apt is PID 1 (ENTRYPOINT tini -- entrypoint.sh): it reaps the zombies ssh
  and ansible leave and forwards SIGTERM.
- cli: Hypercorn's serve() with one worker (run, stream and scheduler state is
  in-process), a 10 s graceful timeout, and SIGTERM/SIGINT as the shutdown
  trigger. Access and error lines go through Steward's logging. --debug keeps
  Quart's reloading dev server.
- Logging: steward/core/logging_setup.py installs one root handler before
  anything logs (until now a library's basicConfig on import was the only
  configuration). Its level is the `log.level` setting (DEBUG/INFO/WARNING/
  ERROR, default INFO), applied at boot and on save in Settings → General.
- CI publish: build, then "The image boots with no internet" (rule 164,
  snippet #3552), then push, so a broken image never reaches a tag (rule 177).
  On an --internal network with its own Postgres, the image must migrate and
  answer /health, run tini as PID 1, serve first-run setup with its assets
  from /static and no third-party host, and exit 0 on SIGTERM. Logs and
  clean-up run always.

Tests: tests/core/test_logging_setup.py; the smoke step itself.
Not in this commit: the dependency lockfile (it conflicts with rule 154 and
is waiting on the operator).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:29:21 -04:00

95 lines
5.1 KiB
HTML

{# steward/templates/settings/general.html #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% block title %}Settings — General — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("General", "")]) }}{% endblock %}
{% block content %}
{% set active_tab = "general" %}
{% include "settings/_tabs.html" %}
<form method="post" action="/settings/general/">{{ csrf_field() }}
<div class="card" style="max-width:640px;">
<h2 class="section-title" style="margin-bottom:1.25rem;">General</h2>
<div class="form-group">
<label>Public base URL</label>
<input type="text" name="general.public_base_url"
value="{{ settings['general.public_base_url'] }}"
placeholder="https://monitor.example.com">
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">
External URL where this Steward instance is reachable from the outside.
Leave blank to use whatever hostname you're currently browsing via.
Used by install scripts, share links, and alert notifications.
</div>
</div>
<div class="form-group">
<label>Trusted reverse proxies</label>
<input type="number" name="general.trusted_proxy_hops" min="0" max="5"
value="{{ settings['general.trusted_proxy_hops'] }}" style="max-width:120px;">
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">
How many proxies sit in front of Steward (e.g. 1 for Traefik, 2 for Cloudflare in front of Traefik).
Steward believes their X-Forwarded-For and X-Forwarded-Proto headers up to that many hops: it marks
the login cookie Secure and sends HSTS only when they report HTTPS, and throttles logins by the address
they report. 0 trusts no proxy.
</div>
</div>
<div class="form-group">
<label>Session lifetime <span style="color:var(--text-muted);font-size:0.8rem;">(hours)</span></label>
<input type="number" name="session.lifetime_hours" min="1" max="720"
value="{{ settings['session.lifetime_hours'] }}" style="max-width:120px;">
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">A login lasts this long; then Steward asks for the password again.</div>
</div>
<div class="form-group">
<label>Failed logins before a pause</label>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<input type="number" name="login.max_failures" min="1" max="100"
value="{{ settings['login.max_failures'] }}" style="max-width:90px;">
<span style="font-size:0.85rem;color:var(--text-muted);">failures from one address pause its logins for</span>
<input type="number" name="login.lockout_minutes" min="1" max="1440"
value="{{ settings['login.lockout_minutes'] }}" style="max-width:90px;">
<span style="font-size:0.85rem;color:var(--text-muted);">minutes</span>
</div>
</div>
<div class="form-group">
<label>Data retention <span style="color:var(--text-muted);font-size:0.8rem;">(days)</span></label>
<input type="number" name="data.retention_days" min="1"
value="{{ settings['data.retention_days'] }}" style="max-width:120px;">
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">Monitor results, Ansible runs and their logs, alert history, and Traefik and UniFi samples older than this are pruned by the hourly cleanup. Host metrics, Docker data and container logs have their own windows under Settings → Thresholds.</div>
</div>
<div class="form-group">
<label>Audit log retention <span style="color:var(--text-muted);font-size:0.8rem;">(days)</span></label>
<input type="number" name="audit.retention_days" min="1"
value="{{ settings['audit.retention_days'] }}" style="max-width:120px;">
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">Who changed what. Kept longer than operational data; entries older than this are pruned by the hourly cleanup.</div>
</div>
<div class="form-group">
<label>Monitor poll interval <span style="color:var(--text-muted);font-size:0.8rem;">(seconds)</span></label>
<input type="number" name="monitors.poll_interval_seconds" min="10"
value="{{ settings['monitors.poll_interval_seconds'] }}" style="max-width:120px;">
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">Requires restart to take effect.</div>
</div>
<div class="form-group">
<label>Log level</label>
<select name="log.level" style="max-width:160px;">
{% for level in log_levels %}
<option value="{{ level }}" {% if settings['log.level'] == level %}selected{% endif %}>{{ level | capitalize }}</option>
{% endfor %}
</select>
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">What reaches the container log. Applies when saved. Warning hides the line logged for each request.</div>
</div>
</div>
<div style="margin-top:1rem;display:flex;align-items:center;gap:1rem;">
<button type="submit" class="btn">Save</button>
<span style="font-size:0.82rem;color:var(--text-muted);">Changes to session lifetime take effect immediately.</span>
</div>
</form>
{% endblock %}