This commit is contained in:
Mateusz Gruszczyński
2026-03-13 11:03:13 +01:00
commit 35571df778
132 changed files with 11197 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<div class="d-flex flex-wrap gap-2 align-items-center">
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.index' else 'btn-outline-primary' }}" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-shield-halved me-1"></i>Start</a>
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint in ['admin.users', 'admin.user_form', 'admin.user_access', 'admin.reset_password'] else 'btn-outline-primary' }}" href="{{ url_for('admin.users') }}"><i class="fa-solid fa-users me-1"></i>Użytkownicy</a>
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint in ['admin.companies', 'admin.company_form'] else 'btn-outline-primary' }}" href="{{ url_for('admin.companies') }}"><i class="fa-solid fa-building me-1"></i>Firmy</a>
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.audit' else 'btn-outline-primary' }}" href="{{ url_for('admin.audit') }}"><i class="fa-solid fa-clipboard-check me-1"></i>Logi audytu</a>
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.global_settings' else 'btn-outline-primary' }}" href="{{ url_for('admin.global_settings') }}"><i class="fa-solid fa-sliders me-1"></i>Ustawienia globalne</a>
<a class="btn btn-sm {{ 'btn-primary' if request.endpoint in ['admin.system_data', 'admin.health'] else 'btn-outline-primary' }}" href="{{ url_for('admin.system_data') }}"><i class="fa-solid fa-microchip me-1"></i>Dane systemowe</a>
<div class="dropdown">
<button class="btn btn-sm {{ 'btn-primary' if request.endpoint == 'admin.maintenance' else 'btn-outline-primary' }} dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-toolbox me-1"></i>Narzędzia
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
<li><a class="dropdown-item" href="{{ url_for('admin.maintenance') }}"><i class="fa-solid fa-screwdriver-wrench me-2"></i>Logi i backupy</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin.system_data') }}#integrations"><i class="fa-solid fa-plug me-2"></i>Integracje</a></li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
{% extends 'base.html' %}
{% block content %}
{% include 'admin/_nav.html' %}
{% block admin_content %}{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends 'admin/admin_base.html' %}
{% block title %}Audit log{% endblock %}
{% block admin_content %}
<div class="card shadow-sm"><div class="card-body"><div class="table-responsive"><table class="table table-sm"><thead><tr><th>Czas</th><th>Akcja</th><th>Typ</th><th>ID</th><th>Szczegóły</th><th>IP</th></tr></thead><tbody>{% for log in logs %}<tr><td>{{ log.created_at }}</td><td>{{ log.action }}</td><td>{{ log.target_type }}</td><td>{{ log.target_id or '' }}</td><td>{{ log.details }}</td><td>{{ log.remote_addr }}</td></tr>{% endfor %}</tbody></table></div></div></div>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-building me-2 text-primary"></i>Firmy{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-center mb-3"><div><h4 class="mb-0">Firmy</h4><div class="text-secondary small">Oddzielne ustawienia KSeF, certyfikaty, powiadomienia i harmonogramy.</div></div><a class="btn btn-primary" href="{{ url_for('admin.company_form') }}"><i class="fa-solid fa-plus me-2"></i>Dodaj firmę</a></div>
<div class="card shadow-sm"><div class="table-responsive"><table class="table table-hover align-middle mb-0"><thead><tr><th>Firma</th><th>Harmonogram</th><th>Status</th><th>Adres / konto</th><th>Notatka</th><th></th></tr></thead><tbody>{% for company in companies %}<tr><td><div class="fw-semibold">{{ company.name }}</div><div class="small text-secondary">NIP: {{ company.tax_id or 'brak' }}{% if company.regon %} · REGON: {{ company.regon }}{% endif %}</div></td><td><span class="badge text-bg-light border">co {{ company.sync_interval_minutes }} min</span> {% if company.sync_enabled %}<span class="badge text-bg-success">włączony</span>{% else %}<span class="badge text-bg-secondary">wyłączony</span>{% endif %}</td><td>{% if company.is_active %}<span class="badge text-bg-success">aktywna</span>{% else %}<span class="badge text-bg-secondary">nieaktywna</span>{% endif %}</td><td class="text-secondary small">{{ company.address or '—' }}<div>{% if company.bank_account %}Konto: {{ company.bank_account }}{% endif %}</div></td><td class="text-secondary small">{{ company.note or '—' }}</td><td class="text-end text-nowrap"><a class="btn btn-sm btn-outline-secondary" href="{{ url_for('dashboard.switch_company', company_id=company.id) }}"><i class="fa-solid fa-check"></i> Wybierz</a> <a class="btn btn-sm btn-outline-primary" href="{{ url_for('admin.company_form', company_id=company.id) }}"><i class="fa-solid fa-pen"></i> Edytuj</a></td></tr>{% endfor %}</tbody></table></div></div>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends 'admin/admin_base.html' %}
{% block title %}{{ 'Edycja firmy' if company else 'Nowa firma' }}{% endblock %}
{% block admin_content %}
<form method="post" class="card shadow-sm border-0">
<div class="card-body p-4">
{{ form.hidden_tag() }}
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
<div>
<h4 class="mb-1">{{ 'Edycja firmy' if company else 'Nowa firma' }}</h4>
<div class="text-secondary">Podaj NIP a następnie klikniej z Pobierz z CEIDG aby wypełnić pola.</div>
</div>
{% if company %}<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('dashboard.switch_company', company_id=company.id) }}">Wybierz tę firmę</a>{% endif %}
</div>
<div class="row g-4">
<div class="col-xl-8">
<div class="card border-0 bg-body-tertiary h-100">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-7">{{ form.name.label(class='form-label') }}{{ form.name(class='form-control', placeholder='Po pobraniu z CEIDG pole uzupełni się automatycznie') }}</div>
<div class="col-md-3">{{ form.tax_id.label(class='form-label') }}{{ form.tax_id(class='form-control', placeholder='NIP') }}</div>
<div class="col-md-2 d-grid">{{ form.fetch_submit(class='btn btn-outline-secondary btn-sm') }}</div>
<div class="col-md-4">{{ form.regon.label(class='form-label') }}{{ form.regon(class='form-control') }}</div>
<div class="col-md-8">{{ form.address.label(class='form-label') }}{{ form.address(class='form-control') }}</div>
<div class="col-md-6">{{ form.bank_account.label(class='form-label') }}{{ form.bank_account(class='form-control', placeholder='np. 11 1111 1111 1111 1111 1111 1111') }}</div>
<div class="col-md-6">{{ form.note.label(class='form-label') }}{{ form.note(class='form-control', rows='3') }}</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card border-0 bg-body-tertiary h-100">
<div class="card-body">
<div class="small text-secondary text-uppercase mb-3">Ustawienia</div>
<div class="form-check form-switch mb-3">{{ form.is_active(class='form-check-input') }} {{ form.is_active.label(class='form-check-label') }}</div>
<div class="form-check form-switch mb-3">{{ form.sync_enabled(class='form-check-input') }} {{ form.sync_enabled.label(class='form-check-label') }}</div>
<div class="form-check form-switch mb-3">{{ form.mock_mode(class='form-check-input') }} {{ form.mock_mode.label(class='form-check-label') }}</div>
<div class="mt-4">{{ form.sync_interval_minutes.label(class='form-label') }}{{ form.sync_interval_minutes(class='form-control') }}</div>
</div>
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">{{ form.submit(class='btn btn-primary') }}</div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-sliders me-2 text-primary"></i>Ustawienia globalne{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
<div><h4 class="mb-1">Ustawienia globalne</h4><div class="text-secondary">Wspólna konfiguracja systemu dla SMTP, Pushover i NFZ oraz model współdzielonego profilu KSeF per firma.</div></div>
</div>
<div class="alert alert-info">KSeF nie jest ustawieniem w pełni globalnym dla całego systemu. Administrator ustawia parametry domyślne oraz osobny profil współdzielony dla aktywnej firmy. Użytkownik może świadomie wybrać profil współdzielony albo własny.</div>
<div class="row g-3">
<div class="col-xl-6"><div class="card"><div class="card-header">SMTP globalne</div><div class="card-body"><form method="post" class="row g-3">{{ mail_form.hidden_tag() }}<div class="col-md-6">{{ mail_form.server.label(class='form-label') }}{{ mail_form.server(class='form-control') }}</div><div class="col-md-3">{{ mail_form.port.label(class='form-label') }}{{ mail_form.port(class='form-control') }}</div><div class="col-md-3">{{ mail_form.username.label(class='form-label') }}{{ mail_form.username(class='form-control') }}</div><div class="col-md-6">{{ mail_form.password.label(class='form-label') }}{{ mail_form.password(class='form-control', placeholder='Pozostaw puste aby zachować hasło') }}</div><div class="col-md-6">{{ mail_form.sender.label(class='form-label') }}{{ mail_form.sender(class='form-control') }}</div><div class="col-md-6">{{ mail_form.security_mode.label(class='form-label') }}{{ mail_form.security_mode(class='form-select') }}</div><div class="col-12">{{ mail_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
<div class="col-xl-6"><div class="card"><div class="card-header">Pushover globalny</div><div class="card-body"><form method="post" class="row g-3">{{ notify_form.hidden_tag() }}<div class="col-md-6">{{ notify_form.pushover_user_key.label(class='form-label') }}{{ notify_form.pushover_user_key(class='form-control') }}</div><div class="col-md-6">{{ notify_form.pushover_api_token.label(class='form-label') }}{{ notify_form.pushover_api_token(class='form-control', placeholder='Pozostaw puste aby zachować token') }}</div><div class="col-md-4">{{ notify_form.min_amount.label(class='form-label') }}{{ notify_form.min_amount(class='form-control') }}</div><div class="col-md-8">{{ notify_form.quiet_hours.label(class='form-label') }}{{ notify_form.quiet_hours(class='form-control') }}</div><div class="col-12 form-check">{{ notify_form.enabled(class='form-check-input') }}{{ notify_form.enabled.label(class='form-check-label') }}</div><div class="col-12">{{ notify_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
<div class="col-xl-6"><div class="card"><div class="card-header">NFZ globalnie</div><div class="card-body"><form method="post">{{ nfz_form.hidden_tag() }}<div class="form-check form-switch fs-5 mb-3">{{ nfz_form.enabled(class='form-check-input') }}{{ nfz_form.enabled.label(class='form-check-label') }}</div>{{ nfz_form.submit(class='btn btn-primary') }}</form></div></div></div>
<div class="col-xl-6"><div class="card"><div class="card-header">Domyślne parametry KSeF</div><div class="card-body"><form method="post" class="row g-3">{{ ksef_defaults_form.hidden_tag() }}<div class="col-md-4">{{ ksef_defaults_form.environment.label(class='form-label') }}{{ ksef_defaults_form.environment(class='form-select') }}</div><div class="col-md-4">{{ ksef_defaults_form.auth_mode.label(class='form-label') }}{{ ksef_defaults_form.auth_mode(class='form-select') }}</div><div class="col-md-4">{{ ksef_defaults_form.client_id.label(class='form-label') }}{{ ksef_defaults_form.client_id(class='form-control') }}</div><div class="col-12"><div class="form-text">Te parametry podpowiadają start nowej konfiguracji, ale nie nadpisują sekretów użytkowników ani współdzielonych profili firm.</div></div><div class="col-12">{{ ksef_defaults_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
<div class="col-12"><div class="card"><div class="card-header">Współdzielony profil KSeF dla aktywnej firmy</div><div class="card-body"><div class="mb-3 small text-secondary">Aktywna firma: <strong>{{ current_company.name if current_company else 'brak' }}</strong>. Ten profil mogą wybrać użytkownicy tej firmy zamiast własnych danych KSeF.</div><form method="post" class="row g-3">{{ shared_ksef_form.hidden_tag() }}<div class="col-md-3">{{ shared_ksef_form.environment.label(class='form-label') }}{{ shared_ksef_form.environment(class='form-select') }}</div><div class="col-md-3">{{ shared_ksef_form.auth_mode.label(class='form-label') }}{{ shared_ksef_form.auth_mode(class='form-select') }}</div><div class="col-md-3">{{ shared_ksef_form.client_id.label(class='form-label') }}{{ shared_ksef_form.client_id(class='form-control') }}</div><div class="col-md-3">{{ shared_ksef_form.certificate_name.label(class='form-label') }}{{ shared_ksef_form.certificate_name(class='form-control') }}</div><div class="col-md-6">{{ shared_ksef_form.token.label(class='form-label') }}{{ shared_ksef_form.token(class='form-control', placeholder='Pozostaw puste aby zachować token') }}<div class="form-text">{{ 'Token zapisany.' if shared_token_configured else 'Brak zapisanego tokena.' }}</div></div><div class="col-md-6">{{ shared_ksef_form.certificate_data.label(class='form-label') }}{{ shared_ksef_form.certificate_data(class='form-control', placeholder='Pozostaw puste aby zachować certyfikat') }}<div class="form-text">{{ 'Certyfikat zapisany.' if shared_cert_configured else 'Brak zapisanego certyfikatu.' }}</div></div><div class="col-12">{{ shared_ksef_form.submit(class='btn btn-primary') }}</div></form></div></div></div>
</div>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-heart-pulse me-2 text-danger"></i>Zdrowie systemu{% endblock %}
{% block admin_content %}
<div class="row g-3">
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-solid fa-database me-2"></i>Baza danych</div><div class="fw-bold fs-5">{{ status.db }}</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-solid fa-envelope me-2"></i>SMTP</div><div class="fw-bold fs-5">{{ status.smtp }}</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-brands fa-redis me-2"></i>Redis</div><div class="fw-bold fs-5">{{ status.redis }}</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm h-100"><div class="card-body"><div class="small text-secondary mb-2"><i class="fa-solid fa-network-wired me-2"></i>KSeF</div><div class="fw-bold fs-5">{{ status.ksef }}</div></div></div></div>
</div>
<div class="card mt-3 shadow-sm"><div class="card-header"><i class="fa-solid fa-link me-2"></i>Status połączenia do API KSeF</div><div class="card-body"><p class="mb-2">{{ status.ksef_message }}</p>{% if status.mock_mode %}<div class="alert alert-info mb-0"><i class="fa-solid fa-flask me-2"></i>Tryb mock jest włączony. Synchronizacja i wystawianie faktur działają lokalnie i nie wysyłają danych do produkcyjnego KSeF.</div>{% endif %}</div></div>
{% endblock %}

View File

@@ -0,0 +1,229 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-shield-halved me-2 text-primary"></i>Panel admina{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
<div>
<h4 class="mb-1">Administracja systemem</h4>
<div class="text-secondary">
Jedno miejsce do zarządzania konfiguracją globalną, firmami, użytkownikami i danymi testowymi.
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card shadow-sm h-100 border-0">
<div class="card-body">
<div class="small text-secondary mb-1">Użytkownicy</div>
<div class="display-6">{{ users }}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100 border-0">
<div class="card-body">
<div class="small text-secondary mb-1">Firmy</div>
<div class="display-6">{{ companies }}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100 border-0">
<div class="card-body">
<div class="small text-secondary mb-1">Firmy z mock</div>
<div class="display-6">{{ mock_enabled }}</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-xl-6">
<div class="card shadow-sm h-100 border-0">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
<div>
<div class="small text-secondary mb-1">Tryb tylko do odczytu</div>
<h5 class="mb-1">Globalna blokada zapisów</h5>
<div class="text-secondary">
Szybkie przełączenie systemu między pracą operacyjną i bezpiecznym trybem tylko do odczytu.
</div>
</div>
<span class="badge rounded-pill {{ 'text-bg-warning' if global_ro else 'text-bg-light border text-secondary' }}">
{{ 'Aktywna' if global_ro else 'Wyłączona' }}
</span>
</div>
<div class="d-flex flex-wrap gap-2">
<form method="post" action="{{ url_for('admin.toggle_global_read_only') }}" class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="enabled" value="1">
<button class="btn btn-sm btn-outline-warning" {{ 'disabled' if global_ro else '' }}>
<i class="fa-solid fa-eye me-1"></i>Włącz R/O
</button>
</form>
<form method="post" action="{{ url_for('admin.toggle_global_read_only') }}" class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="enabled" value="0">
<button class="btn btn-sm btn-outline-secondary" {{ '' if global_ro else 'disabled' }}>
<i class="fa-solid fa-pen me-1"></i>Wyłącz R/O
</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card shadow-sm h-100 border-0">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
<div>
<div class="small text-secondary mb-1">Dane testowe i mock</div>
<h5 class="mb-1">Środowisko demonstracyjne</h5>
<div class="text-secondary">
Generowanie zestawu startowego i szybkie czyszczenie danych do testów prezentacyjnych.
</div>
</div>
<span class="badge rounded-pill text-bg-light border text-secondary">
{{ mock_enabled }} firm
</span>
</div>
<div class="d-flex flex-wrap gap-2">
<form method="post" action="{{ url_for('admin.generate_mock_data') }}" class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm btn-outline-primary">
<i class="fa-solid fa-database me-1"></i>Generuj dane mock
</button>
</form>
<form method="post" action="{{ url_for('admin.clear_mock_data') }}" class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm btn-outline-danger">
<i class="fa-solid fa-trash-can me-1"></i>Usuń dane mock
</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-body-tertiary">
<div class="fw-semibold">
<i class="fa-solid fa-link me-2"></i>API CEIDG
</div>
</div>
<div class="card-body p-4">
<form method="post" action="{{ url_for('admin.save_ceidg_settings') }}" id="ceidg-config-form">
{{ ceidg_form.hidden_tag() }}
{{ ceidg_form.environment(value='production') }}
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="border rounded-3 p-3 h-100 bg-body-tertiary-subtle">
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
<label class="form-label mb-0 fw-semibold">Środowisko CEIDG</label>
<span
class="text-secondary"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="CEIDG działa wyłącznie w środowisku produkcyjnym. Wybór środowiska został ukryty."
>
<i class="fa-regular fa-circle-question"></i>
</span>
</div>
<div class="d-flex align-items-center justify-content-between">
<span class="badge rounded-pill text-bg-success-subtle text-success-emphasis border border-success-subtle">
PROD
</span>
</div>
<div class="form-text mt-2 mb-0">
Tryb produkcyjny jest używany stale.
</div>
</div>
</div>
<div class="col-md-6">
<div class="border rounded-3 p-3 h-100 bg-body-tertiary-subtle">
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
<label class="form-label mb-0 fw-semibold">Status klucza</label>
<span
class="text-secondary"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% if ceidg_api_key_configured %}W systemie zapisano klucz CEIDG. Pozostawienie pustego pola nie usuwa obecnej wartości.{% else %}Klucz CEIDG nie został jeszcze zapisany. Wklej go poniżej i zapisz ustawienia.{% endif %}"
>
<i class="fa-regular fa-circle-question"></i>
</span>
</div>
<div class="d-flex align-items-center justify-content-between">
<span class="badge rounded-pill {{ 'text-bg-success-subtle text-success-emphasis border border-success-subtle' if ceidg_api_key_configured else 'text-bg-danger-subtle text-danger-emphasis border border-danger-subtle' }}">
{{ 'Klucz dodany' if ceidg_api_key_configured else 'Brak klucza' }}
</span>
</div>
<div class="form-text mt-2 mb-0">
{% if ceidg_api_key_configured %}
Klucz jest zapisany w systemie.
{% else %}
Wprowadź klucz i zapisz formularz.
{% endif %}
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
<label class="form-label d-flex align-items-center gap-2" for="{{ ceidg_form.api_key.id }}">
<span>{{ ceidg_form.api_key.label.text }}</span>
<span
class="text-secondary"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Wklej nowy token CEIDG. Pozostawienie pustego pola zachowuje aktualnie zapisany token."
>
<i class="fa-regular fa-circle-question"></i>
</span>
</label>
<div class="input-group">
{{ ceidg_form.api_key(class='form-control', autocomplete='off', placeholder='Wklej nowy token CEIDG') }}
<span class="input-group-text">
<i class="fa-solid fa-key text-secondary"></i>
</span>
</div>
<div class="form-text">
Pozostaw puste, aby zachować zapisany klucz. CEIDG działa stale na PROD.
</div>
</div>
<div class="col-12 pt-2">
{{ ceidg_form.submit(class='btn btn-primary px-4') }}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (window.bootstrap && bootstrap.Tooltip) {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
new bootstrap.Tooltip(el);
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-toolbox me-2 text-primary"></i>Narzędzia administracyjne{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
<div>
<h4 class="mb-1">Logi i backupy</h4>
<div class="text-secondary">Podstrona administracyjna do porządków technicznych, kopii bazy i operacji pomocniczych.</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="small text-secondary mb-2">Baza danych</div>
<h5 class="mb-3">Kopia bazy</h5>
<div class="small text-secondary mb-1">Silnik</div>
<div class="mb-3">{{ backup_meta.engine }}</div>
<div class="small text-secondary mb-1">Katalog backupów</div>
<div class="small text-break mb-3">{{ backup_meta.backup_dir }}</div>
<div class="alert alert-{{ 'success' if backup_meta.sqlite_supported else 'warning' }} py-2 mb-3">
{% if backup_meta.sqlite_supported %}
Kopia z panelu działa bezpośrednio dla SQLite.
{% else %}
Kopia z panelu nie wykonuje natywnego dumpa dla tego silnika.
{% endif %}
</div>
{% if backup_meta.sqlite_path %}
<div class="small text-secondary mb-1">Plik SQLite</div>
<div class="small text-break mb-3">{{ backup_meta.sqlite_path }}</div>
{% endif %}
<ul class="small text-secondary ps-3 mb-3">
{% for note in backup_meta.notes %}
<li>{{ note }}</li>
{% endfor %}
</ul>
<form method="post" action="{{ url_for('admin.database_backup') }}">
{{ backup_form.hidden_tag() }}
{{ backup_form.submit(class='btn btn-primary w-100') }}
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="small text-secondary mb-2">Porządki techniczne</div>
<h5 class="mb-3">Czyszczenie starych logów</h5>
<div class="text-secondary small mb-3">Usuwa rekordy logów i stare rotowane pliki `.log.*` starsze niż wskazana liczba dni.</div>
<form method="post" action="{{ url_for('admin.cleanup_logs') }}">
{{ cleanup_form.hidden_tag() }}
<div class="mb-3">
{{ cleanup_form.days.label(class='form-label') }}
{{ cleanup_form.days(class='form-control') }}
</div>
{{ cleanup_form.submit(class='btn btn-outline-danger w-100') }}
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="small text-secondary mb-2">Szybkie informacje</div>
<h5 class="mb-3">Podsumowanie</h5>
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Użytkownicy</span><strong>{{ users }}</strong></div>
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Firmy</span><strong>{{ companies }}</strong></div>
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Logi audytu</span><strong>{{ audits }}</strong></div>
<div class="d-flex justify-content-between border-bottom py-2 small"><span>Mock aktywny</span><strong>{{ mock_enabled }}</strong></div>
<div class="d-flex justify-content-between py-2 small"><span>Tryb R/O</span><strong>{{ 'ON' if global_ro else 'OFF' }}</strong></div>
<hr>
<div class="small text-secondary">Przy bazach innych niż SQLite przycisk backupu zapisze plik informacyjny, a właściwą kopię należy wykonać narzędziem serwera bazy.</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends 'admin/admin_base.html' %}
{% block title %}Reset hasła{% endblock %}
{% block admin_content %}
<form method="post" class="card shadow-sm"><div class="card-body">{{ form.hidden_tag() }}<p>Użytkownik: <strong>{{ user.email }}</strong></p><div class="mb-3">{{ form.password.label(class='form-label') }}{{ form.password(class='form-control') }}</div><div class="form-check mb-3">{{ form.force_password_change(class='form-check-input') }} {{ form.force_password_change.label(class='form-check-label') }}</div>{{ form.submit(class='btn btn-warning') }}</div></form>
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-microchip me-2 text-primary"></i>Dane systemowe{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
<div>
<h4 class="mb-1">Dane systemowe</h4>
<div class="text-secondary">Skrócony widok techniczny: proces, health, baza, integracje i katalogi.</div>
</div>
</div>
<div class="alert alert-{{ 'warning' if data.health.redis in ['fallback', 'error'] else 'secondary' }} py-2 mb-3">
<div class="d-flex flex-wrap justify-content-between gap-2 align-items-center">
<strong>Redis: {{ data.health.redis|upper }}</strong>
<span class="small text-break">{{ data.health.redis_details or 'brak szczegółów' }}</span>
</div>
</div>
<div class="row g-3 mb-3">
{% for card in data.overview %}
<div class="col-sm-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<div class="small text-secondary text-uppercase mb-1">{{ card.label }}</div>
<div class="fs-4 fw-semibold mb-1">{{ card.value }}</div>
<div class="small text-secondary">{{ card.subvalue }}</div>
</div>
<span class="badge text-bg-{{ card.tone }} rounded-pill"><i class="fa-solid {{ card.icon }}"></i></span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row g-3 mb-3">
<div class="col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-body-tertiary py-2"><strong>Proces i health</strong></div>
<div class="card-body p-3">
<div class="row g-3">
<div class="col-md-6">
<div class="row g-2 small">
<div class="col-6"><div class="text-secondary">CPU</div><div class="fw-semibold">{{ data.process.cpu_percent }}%</div></div>
<div class="col-6"><div class="text-secondary">RAM</div><div class="fw-semibold">{{ data.process.rss_human }}</div></div>
<div class="col-6"><div class="text-secondary">PID</div><div>{{ data.process.pid }}</div></div>
<div class="col-6"><div class="text-secondary">Wątki</div><div>{{ data.process.threads }}</div></div>
<div class="col-6"><div class="text-secondary">Otwarte pliki</div><div>{{ data.process.open_files }}</div></div>
<div class="col-6"><div class="text-secondary">Pamięć hosta</div><div>{{ data.process.system_memory_percent }}%</div></div>
</div>
</div>
<div class="col-md-6">
<div class="row g-2">
{% set ok_values = ['ok', 'mock', 'configured', 'fallback'] %}
{% set health_items = [('Baza', data.health.db), ('SMTP', data.health.smtp), ('Redis', data.health.redis), ('KSeF', data.health.ksef), ('CEIDG', data.health.ceidg)] %}
{% for label, value in health_items %}
<div class="col-6">
<div class="border rounded p-2 h-100 small">
<div class="text-secondary">{{ label }}</div>
<div class="fw-semibold">
<span class="badge text-bg-{{ 'success' if value in ok_values else 'danger' if value == 'error' else 'secondary' }} rounded-pill">{{ value }}</span>
</div>
</div>
</div>
{% endfor %}
<div class="col-12">
<div class="border rounded p-2 small d-flex justify-content-between align-items-center">
<span>Podsumowanie health</span><strong>{{ data.overview[5].value }}</strong>
</div>
</div>
</div>
</div>
</div>
<hr>
<div class="row g-2 small text-secondary">
<div class="col-md-6"><strong>Python:</strong> {{ data.process.python }}</div>
<div class="col-md-6 text-break"><strong>Platforma:</strong> {{ data.process.platform }}</div>
</div>
</div>
</div>
</div>
<div class="col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-body-tertiary py-2"><strong>Aplikacja</strong></div>
<div class="card-body p-3">
<div class="row g-2 mb-3 text-center">
<div class="col-6"><div class="border rounded p-2"><div class="small text-secondary">Użytkownicy</div><div class="fs-4 fw-semibold">{{ data.app.users_count }}</div></div></div>
<div class="col-6"><div class="border rounded p-2"><div class="small text-secondary">Firmy</div><div class="fs-4 fw-semibold">{{ data.app.companies_count }}</div></div></div>
<div class="col-12"><div class="border rounded p-2 d-flex justify-content-between align-items-center"><span>Tryb tylko do odczytu</span><span class="badge text-bg-{{ 'warning' if data.app.read_only_global else 'success' }} rounded-pill">{{ 'ON' if data.app.read_only_global else 'OFF' }}</span></div></div>
</div>
<div class="small text-secondary mb-1">Aktywna firma</div>
<div class="mb-2">{{ data.app.current_company }}</div>
<div class="small text-secondary mb-1">Strefa czasowa</div>
<div class="mb-3">{{ data.app.app_timezone }}</div>
<div class="small text-secondary mb-2">Największe zbiory danych</div>
{% for item in data.app.counts_top[:5] %}
<div class="d-flex justify-content-between small border-bottom py-1"><span>{{ item.label }}</span><strong>{{ item.count }}</strong></div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-body-tertiary py-2"><strong>Baza danych</strong></div>
<div class="card-body p-3 small">
<div class="mb-1"><span class="text-secondary">Silnik:</span> {{ data.database.engine }}</div>
<div class="mb-2 text-break"><span class="text-secondary">Połączenie:</span> {{ data.database.uri }}</div>
{% if data.database.sqlite_path %}
<div class="mb-2 text-break"><span class="text-secondary">SQLite:</span> {{ data.database.sqlite_path }} <span class="text-secondary">({{ data.database.sqlite_size }})</span></div>
{% endif %}
<div class="small text-secondary mb-2">Największe tabele</div>
{% for item in data.database.largest_tables %}
<div class="d-flex justify-content-between border-bottom py-1"><span>{{ item.table }}</span><strong>{{ item.rows }}</strong></div>
{% endfor %}
</div>
</div>
</div>
<div class="col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-body-tertiary py-2"><strong>Katalogi robocze</strong></div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead><tr><th>Katalog</th><th>Rozmiar</th><th>Wolne</th><th class="text-end">Zajęcie</th></tr></thead>
<tbody>
{% for item in data.storage %}
<tr>
<td><div>{{ item.label }}</div><div class="small text-secondary text-break">{{ item.path }}</div></td>
<td>{{ item.size_human }}</td>
<td>{{ item.disk_free }}</td>
<td class="text-end">{{ item.disk_percent }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div id="integrations" class="row g-3 mb-3">
<div class="col-xl-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header d-flex justify-content-between align-items-center bg-body-tertiary py-2">
<strong>Połączenie KSeF</strong>
<span class="badge text-bg-{{ 'success' if data.integrations.ksef.status in ['ok', 'mock'] else 'danger' }} rounded-pill">{{ data.integrations.ksef.status }}</span>
</div>
<div class="card-body p-3 small">
<div class="mb-2"><span class="text-secondary">Komunikat:</span> {{ data.integrations.ksef.message }}</div>
<div class="mb-2 text-break"><span class="text-secondary">Endpoint:</span> {{ data.integrations.ksef.base_url or '—' }}</div>
{% if data.integrations.ksef.auth_mode %}<div class="mb-2"><span class="text-secondary">Tryb autoryzacji:</span> {{ data.integrations.ksef.auth_mode }}</div>{% endif %}
<details>
<summary class="text-secondary">Przykładowa odpowiedź API</summary>
<pre class="small bg-body-tertiary p-3 rounded overflow-auto mt-2" style="max-height:18rem;">{{ json_preview(data.integrations.ksef.sample) }}</pre>
</details>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header d-flex justify-content-between align-items-center bg-body-tertiary py-2">
<strong>Połączenie CEIDG</strong>
<span class="badge text-bg-{{ 'success' if data.integrations.ceidg.status == 'ok' else 'danger' }} rounded-pill">{{ data.integrations.ceidg.status }}</span>
</div>
<div class="card-body p-3 small">
<div class="mb-2"><span class="text-secondary">Komunikat:</span> {{ data.integrations.ceidg.message }}</div>
<div class="mb-2"><span class="text-secondary">Tryb:</span> {{ data.integrations.ceidg.environment }}</div>
<div class="mb-2 text-break"><span class="text-secondary">Endpoint:</span> {{ data.integrations.ceidg.url }}</div>
{% if data.integrations.ceidg.technical_details %}<div class="mb-2 text-break"><span class="text-secondary">Szczegóły:</span> {{ data.integrations.ceidg.technical_details }}</div>{% endif %}
<details>
<summary class="text-secondary">Przykładowa odpowiedź API</summary>
<pre class="small bg-body-tertiary p-3 rounded overflow-auto mt-2" style="max-height:18rem;">{{ json_preview(data.integrations.ceidg.sample) }}</pre>
</details>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-header bg-body-tertiary py-2"><strong>Modele aplikacji</strong></div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead><tr><th>Obiekt</th><th class="text-end">Liczba</th></tr></thead>
<tbody>{% for item in data.app.counts %}<tr><td>{{ item.label }}</td><td class="text-end">{{ item.count }}</td></tr>{% endfor %}</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-header bg-body-tertiary py-2"><strong>Tabele bazy</strong></div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead><tr><th>Tabela</th><th class="text-end">Rekordy</th></tr></thead>
<tbody>{% for item in data.database.table_rows %}<tr><td>{{ item.table }}</td><td class="text-end">{{ item.rows }}</td></tr>{% endfor %}</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends 'admin/admin_base.html' %}
{% block title %}Uprawnienia: {{ user.name }}{% endblock %}
{% block admin_content %}
<div class="row g-4">
<div class="col-lg-5"><form method="post" class="card shadow-sm"><div class="card-body">{{ form.hidden_tag() }}<div class="mb-3">{{ form.company_id.label(class='form-label') }}{{ form.company_id(class='form-select') }}</div><div class="mb-3">{{ form.access_level.label(class='form-label') }}{{ form.access_level(class='form-select') }}</div>{{ form.submit(class='btn btn-primary') }}</div></form></div>
<div class="col-lg-7"><div class="card shadow-sm"><div class="card-body"><table class="table"><thead><tr><th>Firma</th><th>Dostęp</th><th></th></tr></thead><tbody>{% for access in accesses %}<tr><td>{{ access.company.name }}</td><td><span class="badge text-bg-{{ 'warning' if access.access_level=='readonly' else 'success' }}">{{ access.access_level }}</span></td><td class="text-end"><form method="post" action="{{ url_for('admin.delete_access', user_id=user.id, access_id=access.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn btn-sm btn-outline-danger">Usuń</button></form></td></tr>{% endfor %}</tbody></table></div></div></div>
</div>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends 'admin/admin_base.html' %}
{% block title %}{{ 'Edycja użytkownika' if user else 'Nowy użytkownik' }}{% endblock %}
{% block admin_content %}
<form method="post" class="card shadow-sm">
<div class="card-body">{{ form.hidden_tag() }}
<div class="row g-4">
<div class="col-lg-6">
<div class="border rounded-4 p-3 h-100">
<h5 class="mb-3">Dane użytkownika</h5>
<div class="mb-3">{{ form.name.label(class='form-label') }}{{ form.name(class='form-control') }}</div>
<div class="mb-3">{{ form.email.label(class='form-label') }}{{ form.email(class='form-control') }}</div>
<div class="mb-3">{{ form.role.label(class='form-label') }}{{ form.role(class='form-select') }}</div>
<div class="form-check mb-2">{{ form.is_blocked(class='form-check-input') }} {{ form.is_blocked.label(class='form-check-label') }}</div>
</div>
</div>
<div class="col-lg-6">
<div class="border rounded-4 p-3 h-100">
<h5 class="mb-3">Hasło i dostęp startowy</h5>
<div class="mb-3">{{ form.password.label(class='form-label') }}{{ form.password(class='form-control') }}<div class="form-text">Pozostaw puste, aby nie zmieniać hasła.</div></div>
<div class="form-check mb-3">{{ form.force_password_change(class='form-check-input') }} {{ form.force_password_change.label(class='form-check-label') }}</div>
<div class="mb-3">{{ form.company_id.label(class='form-label') }}{{ form.company_id(class='form-select') }}</div>
<div class="mb-3">{{ form.access_level.label(class='form-label') }}{{ form.access_level(class='form-select') }}</div>
</div>
</div>
{% if user %}
<div class="col-12">
<div class="border rounded-4 p-3">
<h5 class="mb-3">Przypisane firmy</h5>
<div class="d-flex gap-2 flex-wrap">{% for access in accesses %}<span class="badge text-bg-{{ 'warning' if access.access_level=='readonly' else 'primary' }} p-2">{{ access.company.name }} / {{ access.access_level }}</span>{% else %}<span class="text-secondary">Brak przypisanych firm.</span>{% endfor %}</div>
<div class="mt-3"><a class="btn btn-outline-secondary" href="{{ url_for('admin.user_access', user_id=user.id) }}">Zarządzaj wieloma firmami</a></div>
</div>
</div>
{% endif %}
</div>
<div class="mt-3">{{ form.submit(class='btn btn-primary') }}</div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends 'admin/admin_base.html' %}
{% block title %}<i class="fa-solid fa-users me-2 text-primary"></i>Użytkownicy{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-center mb-3"><div><h4 class="mb-0">Użytkownicy</h4><div class="text-secondary small">Zarządzanie kontami, blokadami i resetem hasła.</div></div><a class="btn btn-primary" href="{{ url_for('admin.user_form') }}"><i class="fa-solid fa-user-plus me-2"></i>Dodaj użytkownika</a></div>
<div class="card shadow-sm"><div class="table-responsive"><table class="table table-hover align-middle mb-0">
<thead><tr><th>Użytkownik</th><th>Rola</th><th>Status</th><th>Dostęp do firm</th><th></th></tr></thead>
<tbody>{% for user in users %}<tr><td><div class="fw-semibold">{{ user.name }}</div><div class="small text-secondary">{{ user.email }}</div></td><td><span class="badge text-bg-light border">{{ user.role }}</span></td><td>{% if user.is_blocked %}<span class="badge text-bg-danger">zablokowany</span>{% else %}<span class="badge text-bg-success">aktywny</span>{% endif %}{% if user.force_password_change %}<span class="badge text-bg-warning ms-1">zmiana hasła</span>{% endif %}</td><td>{% for access in user.company_access %}<span class="badge text-bg-{{ 'warning' if access.access_level=='readonly' else 'primary' }} me-1 mb-1">{{ access.company.name }} / {{ access.access_level }}</span>{% else %}<span class="text-secondary small">brak przypisanych firm</span>{% endfor %}</td><td class="text-end text-nowrap"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('admin.user_form', user_id=user.id) }}"><i class="fa-solid fa-pen"></i></a> <a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin.user_access', user_id=user.id) }}"><i class="fa-solid fa-key"></i></a> <a class="btn btn-sm btn-outline-warning" href="{{ url_for('admin.reset_password', user_id=user.id) }}"><i class="fa-solid fa-unlock-keyhole"></i></a> <form class="d-inline" method="post" action="{{ url_for('admin.toggle_block', user_id=user.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn btn-sm btn-outline-danger">{% if user.is_blocked %}<i class="fa-solid fa-user-check"></i>{% else %}<i class="fa-solid fa-user-lock"></i>{% endif %}</button></form></td></tr>{% endfor %}</tbody>
</table></div></div>
{% endblock %}