Compare commits

..

6 Commits

Author SHA1 Message Date
c2b5fa7b2e Cove: historical run backfill, run detail popup, and docs update
- cove_importer.py: add _backfill_colorbar_runs() to reconstruct up to
  27 days of history from the D09F08 28-day colorbar when a new run is
  created; idempotent via external_id deduplication
- routes_cove.py: add GET /cove/run/<run_id>/detail endpoint returning
  structured Cove account info and per-datasource objects for popups
- routes_run_checks.py: add cove_summary to run payload for cove_api runs
  with readable datasource labels; hide mail section for Cove runs
- routes_jobs.py: add source_type to history_rows dict
- job_detail.html: Cove run rows clickable; JS routes to cove_run_detail;
  Cove summary panel added, mail section hidden for Cove runs
- run_checks.html: Cove summary panel added; JS handles cove_summary
- technical-notes-codex.md: document new route, popup behaviour, and
  historical backfill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 14:01:36 +01:00
b4c1ec0bf3 Update changelog with login page layout fix details
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 13:57:00 +01:00
a51650dd6e Auto-commit local changes before build (2026-03-20 13:27:58) 2026-03-20 13:27:58 +01:00
5eb8aeeba6 Auto-commit local changes before build (2026-03-20 13:16:06) 2026-03-20 13:16:06 +01:00
ac9c7ba280 Auto-commit local changes before build (2026-03-20 13:12:46) 2026-03-20 13:12:46 +01:00
bd2e71cd62 Auto-commit local changes before build (2026-03-20 13:04:24) 2026-03-20 13:04:24 +01:00
9 changed files with 96 additions and 8 deletions

View File

@ -163,10 +163,21 @@ def _groups_from_claims(claims: dict) -> set[str]:
return set() return set()
def _captcha_enabled() -> bool:
"""Return True when the login captcha is enabled in SystemSettings."""
try:
s = SystemSettings.query.first()
if s is None:
return True # default on for new installs
return bool(getattr(s, "login_captcha_enabled", True))
except Exception:
return True # fail-safe: keep captcha on if DB unreachable
def captcha_required(func): def captcha_required(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if request.method == "POST": if request.method == "POST" and _captcha_enabled():
expected = session.get("captcha_answer") expected = session.get("captcha_answer")
provided = (request.form.get("captcha") or "").strip() provided = (request.form.get("captcha") or "").strip()
if not expected or provided != expected: if not expected or provided != expected:
@ -184,6 +195,7 @@ def captcha_required(func):
return render_template( return render_template(
"auth/login.html", "auth/login.html",
captcha_question=question, captcha_question=question,
captcha_enabled=True,
username=request.form.get("username", ""), username=request.form.get("username", ""),
entra_sso_enabled=entra_ready, entra_sso_enabled=entra_ready,
) )
@ -195,6 +207,8 @@ def captcha_required(func):
@auth_bp.route("/login", methods=["GET", "POST"]) @auth_bp.route("/login", methods=["GET", "POST"])
@captcha_required @captcha_required
def login(): def login():
captcha_on = _captcha_enabled()
if request.method == "GET": if request.method == "GET":
if not users_exist(): if not users_exist():
return redirect(url_for("auth.initial_setup")) return redirect(url_for("auth.initial_setup"))
@ -211,6 +225,7 @@ def login():
return render_template( return render_template(
"auth/login.html", "auth/login.html",
captcha_question=question, captcha_question=question,
captcha_enabled=captcha_on,
entra_sso_enabled=entra_ready, entra_sso_enabled=entra_ready,
) )
@ -233,6 +248,7 @@ def login():
return render_template( return render_template(
"auth/login.html", "auth/login.html",
captcha_question=question, captcha_question=question,
captcha_enabled=captcha_on,
username=username, username=username,
entra_sso_enabled=entra_ready, entra_sso_enabled=entra_ready,
) )

View File

@ -741,6 +741,7 @@ def settings():
old_ui_timezone = settings.ui_timezone old_ui_timezone = settings.ui_timezone
old_require_daily_dashboard_visit = settings.require_daily_dashboard_visit old_require_daily_dashboard_visit = settings.require_daily_dashboard_visit
old_is_sandbox_environment = settings.is_sandbox_environment old_is_sandbox_environment = settings.is_sandbox_environment
old_login_captcha_enabled = getattr(settings, "login_captcha_enabled", True)
old_graph_tenant_id = settings.graph_tenant_id old_graph_tenant_id = settings.graph_tenant_id
old_graph_client_id = settings.graph_client_id old_graph_client_id = settings.graph_client_id
old_graph_mailbox = settings.graph_mailbox old_graph_mailbox = settings.graph_mailbox
@ -787,6 +788,9 @@ def settings():
# Checkbox: present in form = checked, absent = unchecked. # Checkbox: present in form = checked, absent = unchecked.
settings.is_sandbox_environment = bool(request.form.get("is_sandbox_environment")) settings.is_sandbox_environment = bool(request.form.get("is_sandbox_environment"))
# Login captcha toggle — same form (General tab).
settings.login_captcha_enabled = bool(request.form.get("login_captcha_enabled"))
# Autotask integration # Autotask integration
if "autotask_enabled" in request.form: if "autotask_enabled" in request.form:
settings.autotask_enabled = bool(request.form.get("autotask_enabled")) settings.autotask_enabled = bool(request.form.get("autotask_enabled"))
@ -981,6 +985,8 @@ def settings():
changes_general["require_daily_dashboard_visit"] = {"old": old_require_daily_dashboard_visit, "new": settings.require_daily_dashboard_visit} changes_general["require_daily_dashboard_visit"] = {"old": old_require_daily_dashboard_visit, "new": settings.require_daily_dashboard_visit}
if old_is_sandbox_environment != settings.is_sandbox_environment: if old_is_sandbox_environment != settings.is_sandbox_environment:
changes_general["is_sandbox_environment"] = {"old": old_is_sandbox_environment, "new": settings.is_sandbox_environment} changes_general["is_sandbox_environment"] = {"old": old_is_sandbox_environment, "new": settings.is_sandbox_environment}
if old_login_captcha_enabled != settings.login_captcha_enabled:
changes_general["login_captcha_enabled"] = {"old": old_login_captcha_enabled, "new": settings.login_captcha_enabled}
if changes_general: if changes_general:
_log_admin_event( _log_admin_event(

View File

@ -1447,6 +1447,7 @@ def run_migrations() -> None:
migrate_cc_accounts_repo_unique_key() migrate_cc_accounts_repo_unique_key()
migrate_cc_remove_synthetic_missed_runs() migrate_cc_remove_synthetic_missed_runs()
migrate_entra_sso_settings() migrate_entra_sso_settings()
migrate_system_settings_login_captcha()
print("[migrations] All migrations completed.") print("[migrations] All migrations completed.")
@ -1554,6 +1555,37 @@ def migrate_system_settings_sandbox_environment() -> None:
print(f"[migrations] Failed to migrate system_settings.is_sandbox_environment: {exc}") print(f"[migrations] Failed to migrate system_settings.is_sandbox_environment: {exc}")
def migrate_system_settings_login_captcha() -> None:
"""Add login_captcha_enabled column to system_settings if missing.
Default TRUE so existing installs keep captcha enabled after the upgrade.
"""
table = "system_settings"
column = "login_captcha_enabled"
try:
engine = db.get_engine()
except Exception as exc:
print(f"[migrations] Could not get engine for login_captcha_enabled migration: {exc}")
return
try:
if _column_exists(table, column):
print("[migrations] system_settings.login_captcha_enabled already exists.")
return
with engine.begin() as conn:
conn.execute(
text(
f'ALTER TABLE "{table}" ADD COLUMN {column} BOOLEAN NOT NULL DEFAULT TRUE'
)
)
print("[migrations] migrate_system_settings_login_captcha completed.")
except Exception as exc:
print(f"[migrations] Failed to migrate system_settings.login_captcha_enabled: {exc}")
def migrate_performance_indexes() -> None: def migrate_performance_indexes() -> None:
"""Add performance indexes for frequently queried foreign key columns. """Add performance indexes for frequently queried foreign key columns.

View File

@ -123,6 +123,9 @@ class SystemSettings(db.Model):
# this is not a production environment. # this is not a production environment.
is_sandbox_environment = db.Column(db.Boolean, nullable=False, default=False) is_sandbox_environment = db.Column(db.Boolean, nullable=False, default=False)
# Login page captcha (simple math question). Default True for new installs.
login_captcha_enabled = db.Column(db.Boolean, nullable=False, default=True)
# Cove Data Protection integration settings # Cove Data Protection integration settings
cove_enabled = db.Column(db.Boolean, nullable=False, default=False) cove_enabled = db.Column(db.Boolean, nullable=False, default=False)
cove_api_url = db.Column(db.String(255), nullable=True) # default: https://api.backup.management/jsonapi cove_api_url = db.Column(db.String(255), nullable=True) # default: https://api.backup.management/jsonapi

View File

@ -348,10 +348,13 @@ body.bc-body {
============================================================ */ ============================================================ */
.bc-main-auth .bc-content { .bc-main-auth .bc-content {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
padding: 32px 16px; padding: 32px 16px;
max-width: 480px;
margin: 0 auto;
width: 100%;
} }
/* ============================================================ /* ============================================================

View File

@ -8,13 +8,13 @@
top: 30px; top: 30px;
left: -60px; left: -60px;
width: 250px; width: 250px;
background-color: #dc3545; background-color: rgba(220, 53, 69, 0.45);
color: white; color: rgba(255, 255, 255, 0.9);
text-align: center; text-align: center;
transform: rotate(-45deg); transform: rotate(-45deg);
z-index: 9999; z-index: 9999;
pointer-events: none; /* Banner itself is not clickable, elements behind it are */ pointer-events: none; /* Banner itself is not clickable, elements behind it are */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
} }
.sandbox-banner-text { .sandbox-banner-text {

View File

@ -1,7 +1,6 @@
{% extends "layout/base.html" %} {% extends "layout/base.html" %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div>
<div class="col-md-4">
<h2 class="mb-3">Login</h2> <h2 class="mb-3">Login</h2>
<form method="post"> <form method="post">
<div class="mb-3"> <div class="mb-3">
@ -25,6 +24,7 @@
required required
/> />
</div> </div>
{% if captcha_enabled %}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Captcha: {{ captcha_question }}</label> <label class="form-label">Captcha: {{ captcha_question }}</label>
<input <input
@ -34,6 +34,7 @@
required required
/> />
</div> </div>
{% endif %}
<button type="submit" class="btn btn-primary w-100">Login</button> <button type="submit" class="btn btn-primary w-100">Login</button>
<div class="mt-3 text-center"> <div class="mt-3 text-center">
<a class="btn btn-link" href="{{ url_for('auth.password_reset_request') }}">Forgot password?</a> <a class="btn btn-link" href="{{ url_for('auth.password_reset_request') }}">Forgot password?</a>
@ -46,5 +47,4 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -161,6 +161,17 @@
</div> </div>
</div> </div>
<div class="card mb-3">
<div class="card-header">Security</div>
<div class="card-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="login_captcha_enabled" name="login_captcha_enabled" {% if settings.login_captcha_enabled %}checked{% endif %} />
<label class="form-check-label" for="login_captcha_enabled">Enable login captcha</label>
</div>
<div class="form-text">When enabled, users must solve a simple math question before logging in. Enabled by default.</div>
</div>
</div>
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-primary">Save settings</button> <button type="submit" class="btn btn-primary">Save settings</button>
</div> </div>

View File

@ -2,6 +2,23 @@
This file documents all changes made to this project via Claude Code. This file documents all changes made to this project via Claude Code.
## [2026-03-20] (4)
### Added
- Settings → General: "Security" card with login captcha toggle (`login_captcha_enabled`):
- When disabled, the math captcha is hidden on the login page and not validated
- Default `TRUE` for new installs and existing installs after migration
- Migration `migrate_system_settings_login_captcha()` adds the column with `DEFAULT TRUE`
- Audit-logged when changed (same pattern as other General settings)
### Fixed
- Login page layout broken when flash messages were present (e.g. "You have been logged out"):
- `bc-main-auth .bc-content` now uses `max-width: 480px; margin: 0 auto` as a centered column instead of `align-items: center` on a flex container (which caused Bootstrap `col-md-4` percentage widths to collapse)
- `login.html`: replaced `row justify-content-center / col-md-4` with a plain `<div>` — the parent CSS column handles centering
### Changed
- Sandbox banner: semi-transparent (`rgba(220,53,69,0.45)`) instead of solid red
## [2026-03-20] (3) ## [2026-03-20] (3)
### Added ### Added