Add Sandbox/Development environment indicator feature

This commit adds a new setting that displays a visual "SANDBOX" banner
when the environment is marked as development or sandbox.

Changes:
- Add is_sandbox_environment boolean column to SystemSettings model
- Add database migration for automatic schema update
- Add Environment section in Settings > General with toggle switch
- Create sandbox.css with diagonal banner styling (non-interactive)
- Inject system_settings into all template contexts
- Add sandbox banner to base.html layout
- Update changelog with feature description

The banner appears as a red diagonal ribbon in the top-left corner and
uses pointer-events: none to remain non-interactive while keeping
underlying elements clickable.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ivo Oskamp 2026-02-06 12:51:53 +01:00
parent d5cbba2f59
commit 0bf1151f01
8 changed files with 100 additions and 1 deletions

View File

@ -439,6 +439,10 @@ def settings():
# Checkbox: present in form = checked, absent = unchecked. # Checkbox: present in form = checked, absent = unchecked.
settings.require_daily_dashboard_visit = bool(request.form.get("require_daily_dashboard_visit")) settings.require_daily_dashboard_visit = bool(request.form.get("require_daily_dashboard_visit"))
# Environment indicator is in the same form (General tab), so process it here.
# Checkbox: present in form = checked, absent = unchecked.
settings.is_sandbox_environment = bool(request.form.get("is_sandbox_environment"))
# 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"))

View File

@ -109,7 +109,11 @@ def get_user_roles() -> list[str]:
@main_bp.app_context_processor @main_bp.app_context_processor
def _inject_role_context(): def _inject_role_context():
return {"active_role": get_active_role(), "user_roles": get_user_roles()} return {
"active_role": get_active_role(),
"user_roles": get_user_roles(),
"system_settings": _get_or_create_settings(),
}
def roles_required(*roles): def roles_required(*roles):

View File

@ -1034,6 +1034,7 @@ def run_migrations() -> None:
migrate_system_settings_daily_jobs_start_date() migrate_system_settings_daily_jobs_start_date()
migrate_system_settings_ui_timezone() migrate_system_settings_ui_timezone()
migrate_system_settings_autotask_integration() migrate_system_settings_autotask_integration()
migrate_system_settings_sandbox_environment()
migrate_customers_autotask_company_mapping() migrate_customers_autotask_company_mapping()
migrate_mail_messages_columns() migrate_mail_messages_columns()
migrate_mail_messages_parse_columns() migrate_mail_messages_parse_columns()
@ -1138,6 +1139,38 @@ def migrate_system_settings_require_daily_dashboard_visit() -> None:
print(f"[migrations] Failed to migrate system_settings.require_daily_dashboard_visit: {exc}") print(f"[migrations] Failed to migrate system_settings.require_daily_dashboard_visit: {exc}")
def migrate_system_settings_sandbox_environment() -> None:
"""Add is_sandbox_environment column to system_settings if missing.
When enabled, a visual banner is displayed on all pages to indicate
this is not a production environment.
"""
table = "system_settings"
column = "is_sandbox_environment"
try:
engine = db.get_engine()
except Exception as exc:
print(f"[migrations] Could not get engine for system_settings sandbox environment migration: {exc}")
return
try:
if _column_exists(table, column):
print("[migrations] system_settings.is_sandbox_environment already exists.")
return
with engine.begin() as conn:
conn.execute(
text(
f'ALTER TABLE "{table}" ADD COLUMN {column} BOOLEAN NOT NULL DEFAULT FALSE'
)
)
print("[migrations] migrate_system_settings_sandbox_environment completed.")
except Exception as exc:
print(f"[migrations] Failed to migrate system_settings.is_sandbox_environment: {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

@ -112,6 +112,11 @@ class SystemSettings(db.Model):
# their first page view each day before they can navigate elsewhere. # their first page view each day before they can navigate elsewhere.
require_daily_dashboard_visit = db.Column(db.Boolean, nullable=False, default=False) require_daily_dashboard_visit = db.Column(db.Boolean, nullable=False, default=False)
# Development/Sandbox environment indicator.
# When enabled, a visual banner is displayed on all pages to indicate
# this is not a production environment.
is_sandbox_environment = db.Column(db.Boolean, nullable=False, default=False)
# Autotask integration settings # Autotask integration settings
autotask_enabled = db.Column(db.Boolean, nullable=False, default=False) autotask_enabled = db.Column(db.Boolean, nullable=False, default=False)
autotask_environment = db.Column(db.String(32), nullable=True) # sandbox | production autotask_environment = db.Column(db.String(32), nullable=True) # sandbox | production

View File

@ -0,0 +1,27 @@
/* Sandbox environment visual indicator banner
* Displays a diagonal "SANDBOX" ribbon in the top-left corner
* when the is_sandbox_environment setting is enabled.
*/
.sandbox-banner {
position: fixed;
top: 30px;
left: -60px;
width: 250px;
background-color: #dc3545;
color: white;
text-align: center;
transform: rotate(-45deg);
z-index: 9999;
pointer-events: none; /* Banner itself is not clickable, elements behind it are */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.sandbox-banner-text {
display: block;
padding: 8px 0;
font-weight: bold;
font-size: 16px;
letter-spacing: 2px;
text-transform: uppercase;
}

View File

@ -11,6 +11,7 @@
/> />
<link rel="stylesheet" href="{{ url_for('static', filename='css/layout.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/layout.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/status-text.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/status-text.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/sandbox.css') }}" />
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}" /> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}" />
{% block head %}{% endblock %} {% block head %}{% endblock %}
@ -52,6 +53,12 @@
</script> </script>
</head> </head>
<body> <body>
{% if system_settings and system_settings.is_sandbox_environment %}
<div class="sandbox-banner">
<span class="sandbox-banner-text">SANDBOX</span>
</div>
{% endif %}
<nav class="navbar navbar-expand-lg fixed-top bg-body-tertiary border-bottom"> <nav class="navbar navbar-expand-lg fixed-top bg-body-tertiary border-bottom">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.dashboard') }}">Backupchecks</a> <a class="navbar-brand" href="{{ url_for('main.dashboard') }}">Backupchecks</a>

View File

@ -150,6 +150,17 @@
</div> </div>
</div> </div>
<div class="card mb-3">
<div class="card-header">Environment</div>
<div class="card-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="is_sandbox_environment" name="is_sandbox_environment" {% if settings.is_sandbox_environment %}checked{% endif %} />
<label class="form-check-label" for="is_sandbox_environment">Mark this as a Sandbox/Development environment</label>
</div>
<div class="form-text">When enabled, a visual banner will be displayed on all pages to indicate this is not a production environment.</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

@ -4,6 +4,14 @@ This file documents all changes made to this project via Claude Code.
## [2026-02-06] ## [2026-02-06]
### Added
- Added Sandbox/Development environment indicator feature:
- New setting in Settings > General > Environment to mark environment as Sandbox/Development
- Visual "SANDBOX" banner displayed diagonally in top-left corner when enabled
- Banner is non-interactive (pointer-events: none) so elements behind it remain clickable
- Setting defaults to OFF for production safety
- Database migration added for automatic schema update on deployment
### Changed ### Changed
- Renamed "Refresh" button to "Search" in Link existing Autotask ticket modal for better clarity (the button performs a search operation) - Renamed "Refresh" button to "Search" in Link existing Autotask ticket modal for better clarity (the button performs a search operation)
- Added ellipsis-field functionality to "Overall remark" and "Remark" fields in Run Checks modal to prevent long text from hiding action buttons (click to expand/collapse) - Added ellipsis-field functionality to "Overall remark" and "Remark" fields in Run Checks modal to prevent long text from hiding action buttons (click to expand/collapse)