feat(staging): Add visual test indicators for staging environment
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Adds staging-only UI elements: environment banner, TEST badges on nav
items, and floating test panel with feature checklist. Controlled by
STAGING=true env var — zero impact on production.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-04 10:57:34 +01:00
parent f318956429
commit 09ef2b62a5
2 changed files with 226 additions and 0 deletions

16
app.py
View File

@ -51,6 +51,19 @@ else:
# Używana we wszystkich miejscach wyświetlających liczbę firm
COMPANY_COUNT_MARKETING = 150
# ============================================================
# STAGING TEST FEATURES
# ============================================================
# Features currently being tested on staging environment.
# Only rendered when STAGING=true in .env. Edit this dict to update.
STAGING_TEST_FEATURES = {
'board_module': {
'name': 'Moduł Rada Izby',
'description': 'Zarządzanie posiedzeniami, dokumenty, rola OFFICE_MANAGER',
'nav_item': 'Rada',
},
}
# Configure logging with in-memory buffer for debug panel
class DebugLogHandler(logging.Handler):
"""Custom handler that stores logs in memory for real-time viewing"""
@ -319,10 +332,13 @@ def load_user(user_id):
@app.context_processor
def inject_globals():
"""Inject global variables into all templates"""
is_staging = os.getenv('STAGING') == 'true'
return {
'current_year': datetime.now().year,
'now': datetime.now(), # Must be value, not method - templates use now.strftime()
'COMPANY_COUNT': COMPANY_COUNT_MARKETING, # Liczba podmiotów (cel marketingowy)
'is_staging': is_staging,
'staging_features': STAGING_TEST_FEATURES if is_staging else {},
}

View File

@ -1063,6 +1063,158 @@
}
}
{% if is_staging %}
/* ============================================================
* STAGING ENVIRONMENT INDICATORS
* ============================================================ */
.staging-banner {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
text-align: center;
padding: 6px var(--spacing-md);
font-size: 12px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
z-index: 1000;
position: relative;
}
.nav-test-badge {
display: inline-block;
background: #f97316;
color: white;
font-size: 9px;
font-weight: 700;
padding: 1px 5px;
border-radius: 3px;
margin-left: 4px;
vertical-align: middle;
text-transform: uppercase;
letter-spacing: 0.5px;
animation: staging-pulse 2s ease-in-out infinite;
}
@keyframes staging-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.staging-panel-toggle {
position: fixed;
bottom: 20px;
right: 20px;
width: 48px;
height: 48px;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow: var(--shadow-lg);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.staging-panel-toggle:hover {
transform: scale(1.1);
}
.staging-panel-toggle svg {
width: 24px;
height: 24px;
}
.staging-panel {
position: fixed;
bottom: 80px;
right: 20px;
width: 320px;
max-height: 400px;
background: var(--surface);
border: 2px solid #f97316;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 9998;
display: none;
overflow: hidden;
}
.staging-panel.open {
display: block;
}
.staging-panel-header {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
}
.staging-panel-close {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 0;
}
.staging-panel-body {
padding: 12px 16px;
overflow-y: auto;
max-height: 320px;
}
.staging-feature-item {
padding: 10px 0;
border-bottom: 1px solid var(--border);
}
.staging-feature-item:last-child {
border-bottom: none;
}
.staging-feature-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.staging-feature-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: #f97316;
display: inline-block;
flex-shrink: 0;
}
.staging-feature-desc {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
line-height: 1.4;
}
@media (max-width: 768px) {
.staging-panel {
width: calc(100vw - 40px);
right: 20px;
bottom: 76px;
}
}
{% endif %}
{% block extra_css %}{% endblock %}
</style>
@ -1070,6 +1222,11 @@
<script src="{{ url_for('static', filename='js/analytics-tracker.js') }}" defer></script>
</head>
<body>
{% if is_staging %}
<!-- Staging Environment Banner -->
<div class="staging-banner">&#9888; STAGING &mdash; Środowisko testowe</div>
{% endif %}
<!-- Header -->
<header>
<div class="container">
@ -1820,5 +1977,58 @@
<!-- Connections Map Modal -->
{% include 'connections_modal.html' %}
{% if is_staging %}
<!-- Staging Test Panel -->
<button type="button" class="staging-panel-toggle" onclick="toggleStagingPanel()" title="Panel testowy">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
</button>
<div class="staging-panel" id="stagingPanel">
<div class="staging-panel-header">
<span>Panel testowy</span>
<button type="button" class="staging-panel-close" onclick="toggleStagingPanel()" title="Zamknij panel">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="staging-panel-body">
{% for key, feature in staging_features.items() %}
<div class="staging-feature-item">
<div class="staging-feature-name">
<span class="staging-feature-status"></span>
{{ feature.name }}
</div>
<div class="staging-feature-desc">{{ feature.description }}</div>
</div>
{% endfor %}
</div>
</div>
<script>
function toggleStagingPanel() {
document.getElementById('stagingPanel').classList.toggle('open');
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var panel = document.getElementById('stagingPanel');
if (panel) panel.classList.remove('open');
}
});
document.addEventListener('DOMContentLoaded', function() {
var testNavItems = {{ staging_features.values()|map(attribute='nav_item')|select('defined')|list|tojson }};
document.querySelectorAll('.nav-menu .nav-link, .admin-bar a').forEach(function(link) {
var text = link.textContent.trim();
if (testNavItems.indexOf(text) !== -1) {
var badge = document.createElement('span');
badge.className = 'nav-test-badge';
badge.textContent = 'TEST';
link.appendChild(badge);
}
});
});
</script>
{% endif %}
</body>
</html>