10 changed files with 375 additions and 352 deletions
@ -1,350 +1,32 @@ |
|||||||
# OTB Billing — Project State |
Project: OTB Billing |
||||||
|
Version: v0.4.0 |
||||||
Last Updated: 2026-03-09 |
Last Updated: 2026-03-12 |
||||||
Version: v0.3.1 |
Status: Stable release checkpoint |
||||||
Project Path: ~/otb_billing |
|
||||||
|
Current State: |
||||||
--- |
- Flask app runs under systemd as otb_billing.service. |
||||||
|
- Service starts through /home/def/otb_billing/run_dev.sh. |
||||||
# Project Purpose |
- Runtime environment is loaded from /home/def/otb_billing/.env by the shell wrapper. |
||||||
|
- App listens on 0.0.0.0:5050 for mintme webfront proxy access. |
||||||
OTB Billing is a contractor-focused billing system designed to be: |
- /health renders as a styled page. |
||||||
|
- /health.json provides raw machine-readable health data. |
||||||
- self-hosted |
- Aging report is visually acceptable and readable. |
||||||
- portable |
- Reboot persistence confirmed. |
||||||
- database-backed |
|
||||||
- deployable on fresh Linux systems |
Important Paths: |
||||||
- suitable for managed hosting or client-installed deployments |
- Project root: /home/def/otb_billing |
||||||
|
- App entry: /home/def/otb_billing/backend/app.py |
||||||
The system is being built as a practical alternative to restrictive SaaS billing tools, with emphasis on ownership, simplicity, portability, and contractor/operator workflow. |
- Health module: /home/def/otb_billing/backend/health.py |
||||||
|
- Runtime wrapper: /home/def/otb_billing/run_dev.sh |
||||||
Tagline direction: |
- Env file: /home/def/otb_billing/.env |
||||||
|
- Service unit: /etc/systemd/system/otb_billing.service |
||||||
By a contractor, for contractors |
- Repo copy of unit: /home/def/otb_billing/deploy/systemd/otb_billing.service |
||||||
|
|
||||||
--- |
Operations: |
||||||
|
- sudo systemctl status otb_billing |
||||||
# Current Stack |
- sudo systemctl restart otb_billing |
||||||
|
- sudo journalctl -u otb_billing -f |
||||||
Backend: |
|
||||||
Flask |
Release Notes: |
||||||
|
- This version is considered a finished product suitable for normal use. |
||||||
Database: |
- Further work, if any, should be touch-ups and enhancements rather than core stabilization. |
||||||
MariaDB |
|
||||||
|
|
||||||
PDF Engine: |
|
||||||
ReportLab |
|
||||||
|
|
||||||
Primary Port: |
|
||||||
5050 |
|
||||||
|
|
||||||
Dependencies file: |
|
||||||
requirements.txt |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Deployment Philosophy |
|
||||||
|
|
||||||
OTB Billing must remain a deployable product, not just a dev-only app. |
|
||||||
|
|
||||||
Target install model: |
|
||||||
|
|
||||||
fresh server |
|
||||||
→ installer runs |
|
||||||
→ dependencies install |
|
||||||
→ MariaDB setup |
|
||||||
→ schema setup |
|
||||||
→ app launches |
|
||||||
|
|
||||||
This remains a core project rule. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Current Core Features |
|
||||||
|
|
||||||
## Clients |
|
||||||
- create client |
|
||||||
- edit client |
|
||||||
- list clients |
|
||||||
- status field |
|
||||||
- client code support |
|
||||||
|
|
||||||
## Services |
|
||||||
- create service |
|
||||||
- edit service |
|
||||||
- list services |
|
||||||
- service code support |
|
||||||
- service status support |
|
||||||
|
|
||||||
## Invoices |
|
||||||
- create invoice |
|
||||||
- edit invoice |
|
||||||
- list invoices |
|
||||||
- automatic invoice numbering |
|
||||||
- invoice print view |
|
||||||
- invoice PDF download |
|
||||||
- invoice lock after payment activity |
|
||||||
- invoice statuses |
|
||||||
- invoice email sending with PDF attachment |
|
||||||
|
|
||||||
Current invoice statuses: |
|
||||||
- draft |
|
||||||
- pending |
|
||||||
- partial |
|
||||||
- paid |
|
||||||
- overdue |
|
||||||
- cancelled |
|
||||||
|
|
||||||
## Payments |
|
||||||
- record payment |
|
||||||
- edit payment |
|
||||||
- list payments |
|
||||||
- overpayment guard on new payment |
|
||||||
- overpayment guard on payment edit |
|
||||||
- payment status display |
|
||||||
- payment void / reversal workflow |
|
||||||
- invoice recalculation after payment changes |
|
||||||
|
|
||||||
Current payment statuses: |
|
||||||
- confirmed |
|
||||||
- reversed |
|
||||||
|
|
||||||
## Credit Ledger |
|
||||||
- client credit ledger |
|
||||||
- manual credit entries |
|
||||||
- client balance color coding |
|
||||||
- ledger link visible from client list/edit pages |
|
||||||
|
|
||||||
## Invoice Rendering |
|
||||||
- HTML invoice view |
|
||||||
- print-friendly layout |
|
||||||
- PDF invoice generation |
|
||||||
- client details on invoice |
|
||||||
- status badge on invoice |
|
||||||
- totals / paid / remaining display |
|
||||||
- branding/logo support on HTML and PDF |
|
||||||
|
|
||||||
## Exports |
|
||||||
- clients CSV export |
|
||||||
- invoices CSV export |
|
||||||
- payments CSV export |
|
||||||
- filtered invoice CSV export |
|
||||||
- filtered invoice PDF ZIP export |
|
||||||
- accounting package ZIP export |
|
||||||
- revenue report JSON export |
|
||||||
|
|
||||||
## Batch / Print |
|
||||||
- filtered batch invoice print page |
|
||||||
- print-friendly revenue report |
|
||||||
|
|
||||||
## Reports |
|
||||||
- revenue report |
|
||||||
- report frequency selector |
|
||||||
- JSON report export |
|
||||||
- email revenue report JSON |
|
||||||
|
|
||||||
## Settings / Configuration System |
|
||||||
Accessible from: |
|
||||||
/settings |
|
||||||
|
|
||||||
Stored in database table: |
|
||||||
app_settings |
|
||||||
|
|
||||||
### Business Identity Settings |
|
||||||
- business name |
|
||||||
- business tagline |
|
||||||
- business logo URL |
|
||||||
- business email |
|
||||||
- business phone |
|
||||||
- business address |
|
||||||
- business website |
|
||||||
- business registration number |
|
||||||
|
|
||||||
### Tax Settings |
|
||||||
- tax label |
|
||||||
- tax rate |
|
||||||
- tax number |
|
||||||
- local country |
|
||||||
- apply local tax only flag |
|
||||||
|
|
||||||
### Invoice Behavior Settings |
|
||||||
- default currency |
|
||||||
- invoice footer |
|
||||||
- payment terms |
|
||||||
- report frequency |
|
||||||
|
|
||||||
### SMTP / Email Settings |
|
||||||
- SMTP host |
|
||||||
- SMTP port |
|
||||||
- SMTP username |
|
||||||
- SMTP password |
|
||||||
- SMTP from email |
|
||||||
- SMTP from name |
|
||||||
- TLS flag |
|
||||||
- SSL flag |
|
||||||
- report delivery email |
|
||||||
|
|
||||||
## Email Delivery |
|
||||||
- invoice email with PDF attachment |
|
||||||
- revenue report JSON email |
|
||||||
- accounting package email |
|
||||||
|
|
||||||
## Email Logging |
|
||||||
Stored in: |
|
||||||
email_log |
|
||||||
|
|
||||||
Tracks: |
|
||||||
- email_type |
|
||||||
- invoice_id |
|
||||||
- recipient_email |
|
||||||
- subject |
|
||||||
- status |
|
||||||
- error_message |
|
||||||
- sent_at |
|
||||||
|
|
||||||
Currently logs: |
|
||||||
- invoice email sends |
|
||||||
- revenue report email sends |
|
||||||
- accounting package email sends |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Current Known Good State |
|
||||||
|
|
||||||
Confirmed working: |
|
||||||
- dashboard |
|
||||||
- clients |
|
||||||
- services |
|
||||||
- invoice creation |
|
||||||
- auto invoice numbering |
|
||||||
- invoice view |
|
||||||
- invoice PDF generation |
|
||||||
- invoice email with PDF attachment |
|
||||||
- payment entry |
|
||||||
- payment overpayment prevention |
|
||||||
- payment reversal / void |
|
||||||
- payments list with invoice status and remaining balance |
|
||||||
- settings/config page |
|
||||||
- business identity shown on invoice view/PDF |
|
||||||
- logo display in HTML and PDF |
|
||||||
- clients/invoices/payments CSV export |
|
||||||
- filtered invoice export |
|
||||||
- filtered invoice PDF ZIP export |
|
||||||
- batch invoice print |
|
||||||
- revenue report |
|
||||||
- revenue report JSON export |
|
||||||
- revenue report email |
|
||||||
- accounting package ZIP export |
|
||||||
- accounting package email |
|
||||||
- email audit logging |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Requirements |
|
||||||
|
|
||||||
Current requirements.txt should include: |
|
||||||
- Flask |
|
||||||
- mysql-connector-python |
|
||||||
- reportlab |
|
||||||
- python-dateutil |
|
||||||
- pytz |
|
||||||
|
|
||||||
This file must remain complete so installer-driven deployment works in one shot. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Business / Product Direction |
|
||||||
|
|
||||||
This system is intended to grow into a deployable billing product for small operators, hosting providers, and service businesses. |
|
||||||
|
|
||||||
Target strengths versus typical SaaS billing tools: |
|
||||||
- simpler workflow |
|
||||||
- data ownership |
|
||||||
- exportability |
|
||||||
- portability |
|
||||||
- operator-first design |
|
||||||
- no hostage-style software design |
|
||||||
|
|
||||||
Long-term success goal: |
|
||||||
build something users are happy to use and proud to own. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Planned Next Features |
|
||||||
|
|
||||||
## Near-Term |
|
||||||
- invoice defaults from settings |
|
||||||
- improved tax application logic |
|
||||||
- accountant package scheduling / reminders |
|
||||||
- client account statement export |
|
||||||
- backup/install polish |
|
||||||
|
|
||||||
## Medium-Term |
|
||||||
- quote / estimate system |
|
||||||
- recurring invoices |
|
||||||
- reminder workflows |
|
||||||
- better installer/update flow |
|
||||||
- email resend history view |
|
||||||
|
|
||||||
## Long-Term |
|
||||||
- client portal |
|
||||||
- role-based access |
|
||||||
- accountant/export workflows |
|
||||||
- integration paths for vertical forks such as HVAC/customer-service variants |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Advanced Settings Direction |
|
||||||
|
|
||||||
Business identity and SMTP belong in settings UI. |
|
||||||
|
|
||||||
Database credentials should remain installer/config-file driven, not casually editable in standard UI. |
|
||||||
|
|
||||||
If advanced connection settings are ever exposed in UI, they must be clearly marked as dangerous / advanced and should avoid redisplaying stored passwords. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Repository Discipline |
|
||||||
|
|
||||||
For this project going forward: |
|
||||||
- keep PROJECT_STATE.md updated |
|
||||||
- update README.md with version/build notes |
|
||||||
- keep requirements.txt complete |
|
||||||
- make full ZIP backup on version bumps |
|
||||||
- push milestones to git |
|
||||||
|
|
||||||
Example future archive naming: |
|
||||||
otb_billing-v0.3.1.zip |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
# Restart / Run Notes |
|
||||||
|
|
||||||
Development run method: |
|
||||||
|
|
||||||
cd ~/otb_billing |
|
||||||
python3 backend/app.py |
|
||||||
|
|
||||||
During active development, run in a visible terminal so logs stay visible. |
|
||||||
|
|
||||||
Do not rely on hidden/background launch during normal debug workflow. |
|
||||||
|
|
||||||
================================================= |
|
||||||
Version: v0.4.0-dev |
|
||||||
Date: 2026-03-10 |
|
||||||
================================================= |
|
||||||
|
|
||||||
Recurring billing foundation added. |
|
||||||
|
|
||||||
New recurring billing capabilities: |
|
||||||
- subscriptions table auto-created by app |
|
||||||
- subscriptions list page |
|
||||||
- new subscription creation page |
|
||||||
- billing interval support: monthly / quarterly / yearly |
|
||||||
- manual recurring billing run button |
|
||||||
- automatic invoice creation for due subscriptions |
|
||||||
- next_invoice_date auto-advances after invoice creation |
|
||||||
|
|
||||||
Notes: |
|
||||||
- first recurring billing slice is manual-run based |
|
||||||
- cron/systemd timer automation can be added next |
|
||||||
|
|||||||
@ -0,0 +1,137 @@ |
|||||||
|
import os |
||||||
|
import time |
||||||
|
import shutil |
||||||
|
import platform |
||||||
|
from datetime import datetime, timezone |
||||||
|
from zoneinfo import ZoneInfo |
||||||
|
|
||||||
|
from flask import render_template, jsonify |
||||||
|
|
||||||
|
APP_START_TS = time.time() |
||||||
|
|
||||||
|
|
||||||
|
def _read_meminfo(): |
||||||
|
data = {} |
||||||
|
try: |
||||||
|
with open("/proc/meminfo", "r", encoding="utf-8") as f: |
||||||
|
for line in f: |
||||||
|
if ":" not in line: |
||||||
|
continue |
||||||
|
key, val = line.split(":", 1) |
||||||
|
data[key.strip()] = val.strip() |
||||||
|
except Exception: |
||||||
|
pass |
||||||
|
return data |
||||||
|
|
||||||
|
|
||||||
|
def _kb_to_mb(kb_value): |
||||||
|
try: |
||||||
|
return round(int(kb_value) / 1024, 2) |
||||||
|
except Exception: |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def _server_uptime_seconds(): |
||||||
|
try: |
||||||
|
with open("/proc/uptime", "r", encoding="utf-8") as f: |
||||||
|
return int(float(f.read().split()[0])) |
||||||
|
except Exception: |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def _format_duration(seconds): |
||||||
|
if seconds is None: |
||||||
|
return None |
||||||
|
seconds = int(seconds) |
||||||
|
days, rem = divmod(seconds, 86400) |
||||||
|
hours, rem = divmod(rem, 3600) |
||||||
|
minutes, secs = divmod(rem, 60) |
||||||
|
return f"{days}d {hours}h {minutes}m {secs}s" |
||||||
|
|
||||||
|
|
||||||
|
def _health_payload(app): |
||||||
|
now_utc = datetime.now(timezone.utc) |
||||||
|
now_toronto = now_utc.astimezone(ZoneInfo("America/Toronto")) |
||||||
|
|
||||||
|
load1 = load5 = load15 = None |
||||||
|
try: |
||||||
|
load1, load5, load15 = os.getloadavg() |
||||||
|
except Exception: |
||||||
|
pass |
||||||
|
|
||||||
|
meminfo = _read_meminfo() |
||||||
|
mem_total_kb = None |
||||||
|
mem_available_kb = None |
||||||
|
mem_used_kb = None |
||||||
|
mem_used_percent = None |
||||||
|
try: |
||||||
|
mem_total_kb = int(meminfo.get("MemTotal", "0 kB").split()[0]) |
||||||
|
mem_available_kb = int(meminfo.get("MemAvailable", "0 kB").split()[0]) |
||||||
|
mem_used_kb = mem_total_kb - mem_available_kb |
||||||
|
if mem_total_kb > 0: |
||||||
|
mem_used_percent = round((mem_used_kb / mem_total_kb) * 100, 2) |
||||||
|
except Exception: |
||||||
|
pass |
||||||
|
|
||||||
|
disk = shutil.disk_usage("/") |
||||||
|
|
||||||
|
db_ok = False |
||||||
|
db_error = None |
||||||
|
try: |
||||||
|
connector = app.config.get("OTB_HEALTH_DB_CONNECTOR") |
||||||
|
if callable(connector): |
||||||
|
conn = connector() |
||||||
|
cur = conn.cursor() |
||||||
|
cur.execute("SELECT 1") |
||||||
|
cur.fetchone() |
||||||
|
cur.close() |
||||||
|
conn.close() |
||||||
|
db_ok = True |
||||||
|
else: |
||||||
|
db_error = "DB connector not registered" |
||||||
|
except Exception as e: |
||||||
|
db_error = str(e) |
||||||
|
|
||||||
|
app_uptime = int(time.time() - APP_START_TS) |
||||||
|
server_uptime = _server_uptime_seconds() |
||||||
|
|
||||||
|
payload = { |
||||||
|
"status": "ok" if db_ok else "degraded", |
||||||
|
"app_name": "otb_billing", |
||||||
|
"hostname": platform.node(), |
||||||
|
"server_time_utc": now_utc.isoformat(), |
||||||
|
"server_time_toronto": now_toronto.isoformat(), |
||||||
|
"app_uptime_seconds": app_uptime, |
||||||
|
"app_uptime_human": _format_duration(app_uptime), |
||||||
|
"server_uptime_seconds": server_uptime, |
||||||
|
"server_uptime_human": _format_duration(server_uptime), |
||||||
|
"load_average": {"1m": load1, "5m": load5, "15m": load15}, |
||||||
|
"memory": { |
||||||
|
"total_mb": _kb_to_mb(mem_total_kb) if mem_total_kb is not None else None, |
||||||
|
"available_mb": _kb_to_mb(mem_available_kb) if mem_available_kb is not None else None, |
||||||
|
"used_mb": _kb_to_mb(mem_used_kb) if mem_used_kb is not None else None, |
||||||
|
"used_percent": mem_used_percent, |
||||||
|
}, |
||||||
|
"disk_root": { |
||||||
|
"total_gb": round(disk.total / (1024**3), 2), |
||||||
|
"used_gb": round(disk.used / (1024**3), 2), |
||||||
|
"free_gb": round(disk.free / (1024**3), 2), |
||||||
|
"used_percent": round((disk.used / disk.total) * 100, 2) if disk.total else None, |
||||||
|
}, |
||||||
|
"database": {"ok": db_ok, "error": db_error}, |
||||||
|
} |
||||||
|
|
||||||
|
http_code = 200 if db_ok else 503 |
||||||
|
return payload, http_code |
||||||
|
|
||||||
|
|
||||||
|
def register_health_routes(app): |
||||||
|
@app.route("/health", methods=["GET"]) |
||||||
|
def health_page(): |
||||||
|
health, http_code = _health_payload(app) |
||||||
|
return render_template("health.html", health=health), http_code |
||||||
|
|
||||||
|
@app.route("/health.json", methods=["GET"]) |
||||||
|
def health_json(): |
||||||
|
health, http_code = _health_payload(app) |
||||||
|
return jsonify(health), http_code |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
[Unit] |
||||||
|
Description=OTB Billing Flask App |
||||||
|
After=network.target mariadb.service |
||||||
|
Wants=network.target |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
User=def |
||||||
|
Group=def |
||||||
|
WorkingDirectory=/home/def/otb_billing |
||||||
|
ExecStart=/home/def/otb_billing/run_dev.sh |
||||||
|
Restart=always |
||||||
|
RestartSec=3 |
||||||
|
Environment=PYTHONUNBUFFERED=1 |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
set -euo pipefail |
||||||
|
|
||||||
|
cd /home/def/otb_billing |
||||||
|
|
||||||
|
if [ -f .env ]; then |
||||||
|
set -a |
||||||
|
. ./.env |
||||||
|
set +a |
||||||
|
fi |
||||||
|
|
||||||
|
exec python3 backend/app.py |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>System Health - OTB Billing</title> |
||||||
|
<link rel="stylesheet" href="/static/style.css"> |
||||||
|
<style> |
||||||
|
.page-wrap { |
||||||
|
padding: 1.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header h1 { |
||||||
|
margin-bottom: 0.35rem; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header p { |
||||||
|
margin-top: 0; |
||||||
|
opacity: 0.9; |
||||||
|
} |
||||||
|
|
||||||
|
.health-links { |
||||||
|
margin: 1rem 0 1.25rem 0; |
||||||
|
} |
||||||
|
|
||||||
|
.health-links a { |
||||||
|
margin-right: 1rem; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
|
||||||
|
.health-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); |
||||||
|
gap: 1rem; |
||||||
|
margin-top: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.health-card { |
||||||
|
border: 1px solid rgba(255,255,255,0.16); |
||||||
|
border-radius: 14px; |
||||||
|
padding: 1rem 1.1rem; |
||||||
|
background: rgba(255,255,255,0.03); |
||||||
|
box-shadow: 0 6px 18px rgba(0,0,0,0.16); |
||||||
|
} |
||||||
|
|
||||||
|
.health-card h3 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.85rem; |
||||||
|
} |
||||||
|
|
||||||
|
.health-card p { |
||||||
|
margin: 0.45rem 0; |
||||||
|
} |
||||||
|
|
||||||
|
.status-good { |
||||||
|
color: #63d471; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
.status-bad { |
||||||
|
color: #ff7b7b; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
.monoish { |
||||||
|
word-break: break-word; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="page-wrap"> |
||||||
|
<div class="page-header"> |
||||||
|
<h1>System Health</h1> |
||||||
|
<p>Application and server status for OTB Billing.</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-links"> |
||||||
|
<a href="/">Home</a> |
||||||
|
<a href="/health.json">Raw JSON</a> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-grid"> |
||||||
|
<div class="health-card"> |
||||||
|
<h3>Status</h3> |
||||||
|
{% if health.status == "ok" %} |
||||||
|
<p class="status-good">Healthy</p> |
||||||
|
{% else %} |
||||||
|
<p class="status-bad">Degraded</p> |
||||||
|
{% endif %} |
||||||
|
<p><strong>App:</strong> {{ health.app_name }}</p> |
||||||
|
<p><strong>Host:</strong> {{ health.hostname }}</p> |
||||||
|
<p class="monoish"><strong>Toronto Time:</strong> {{ health.server_time_toronto }}</p> |
||||||
|
<p class="monoish"><strong>UTC Time:</strong> {{ health.server_time_utc }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-card"> |
||||||
|
<h3>Database</h3> |
||||||
|
{% if health.database.ok %} |
||||||
|
<p class="status-good">Connected</p> |
||||||
|
{% else %} |
||||||
|
<p class="status-bad">Connection Error</p> |
||||||
|
{% endif %} |
||||||
|
<p class="monoish"><strong>Error:</strong> {{ health.database.error or "None" }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-card"> |
||||||
|
<h3>Uptime</h3> |
||||||
|
<p><strong>Application:</strong> {{ health.app_uptime_human }}</p> |
||||||
|
<p><strong>Server:</strong> {{ health.server_uptime_human }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-card"> |
||||||
|
<h3>Load Average</h3> |
||||||
|
<p><strong>1 min:</strong> {{ health.load_average["1m"] }}</p> |
||||||
|
<p><strong>5 min:</strong> {{ health.load_average["5m"] }}</p> |
||||||
|
<p><strong>15 min:</strong> {{ health.load_average["15m"] }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-card"> |
||||||
|
<h3>Memory</h3> |
||||||
|
<p><strong>Total:</strong> {{ health.memory.total_mb }} MB</p> |
||||||
|
<p><strong>Available:</strong> {{ health.memory.available_mb }} MB</p> |
||||||
|
<p><strong>Used:</strong> {{ health.memory.used_mb }} MB</p> |
||||||
|
<p><strong>Used %:</strong> {{ health.memory.used_percent }}%</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="health-card"> |
||||||
|
<h3>Disk /</h3> |
||||||
|
<p><strong>Total:</strong> {{ health.disk_root.total_gb }} GB</p> |
||||||
|
<p><strong>Used:</strong> {{ health.disk_root.used_gb }} GB</p> |
||||||
|
<p><strong>Free:</strong> {{ health.disk_root.free_gb }} GB</p> |
||||||
|
<p><strong>Used %:</strong> {{ health.disk_root.used_percent }}%</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% include "footer.html" %} |
||||||
|
</body> |
||||||
|
</html> |
||||||
Loading…
Reference in new issue