Files
astrololo/services/presentation/app/templates/files.html
T
gitea 6466ab89a9
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m12s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 19s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 12m25s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 16s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
feat(dane): interfejs zarządzania plikami baz — trzy poziomy dostępu (DAN-27)
Ekran „Pliki" z trzema poziomami, wpiętymi w kontrolę dostępu z PRE-27:

  „files"        widzi listę i KLIKANIEM decyduje, z których baz program korzysta,
  „files_input"  dokłada wgrywanie i ARCHIWIZACJĘ,
  administrator  kasowanie, przywracanie z archiwum i REGUŁY WALIDACJI.

STAN JEST TERAZ TRWAŁY. DAN-15 trzymał go w zmiennej DISABLED_BASES, bo warstwa
danych nie miała gdzie zapisywać — udział był montowany read-only. Skoro stan ma
być klikany, musi przetrwać restart, więc udział jest zapisywalny, a stan leży
w pliku obok baz (zapis atomowy: plik opisuje CAŁY zbiór, więc obcięcie w połowie
skasowałoby wiedzę o wszystkich naraz). DISABLED_BASES zostaje jako awaryjne
wyłączenie z konfiguracji i odsiewa DODATKOWO — nie odwrotnie, bo inaczej ktoś
z dostępem do ekranu włączyłby bazę wyłączoną świadomie na poziomie wdrożenia.

ARCHIWIZACJA NIE KASUJE. Plik zostaje na dysku, zamrożony, ze znacznikiem czasu;
znika wyłącznie z użytku. To najdalej idąca operacja osoby wgrywającej dane —
kasować może tylko administrator. Test sprawdza, że plik po archiwizacji nadal
istnieje, bo to jest cała istota tej operacji.

WALIDACJA JEST BRAMKĄ DO UŻYTKU, NIE FILTREM NA WEJŚCIU. Plik wgrany zostaje
NIEZALEŻNIE od wyniku — nie tracimy niczego, co ktoś wgrał. Zmienia się tylko to,
czy da się go włączyć. Sprawdzenie biegnie też w chwili włączania, nie tylko przy
wgrywaniu: reguły mogą się zmienić po fakcie.

O WALIDACJI WIE TYLKO ADMINISTRATOR. Pliki wstrzymane są odsiewane W WARSTWIE
DANYCH przy for_admin=False, a nie ukrywane w szablonie — gdyby dochodziły do
przeglądarki, wystarczyłby podgląd źródła, żeby poznać reguły. Odmowa włączenia
wraca do konta bez uprawnień BEZ POWODU, bo powód zdradza regułę. Sekcja reguł
nie trafia nawet do źródła strony. Test parametryzowany po obu niższych poziomach
szuka w odpowiedzi śladów mechanizmu i wymaga, żeby żadnego nie było.

Każdy plik ma policzony sha256 — tożsamość niezależna od nazwy. Wykorzystuje ją
już odrzucanie duplikatów, a w kroku drugim posłuży do pilnowania zgodności
lustra w SQL.

Przy okazji naprawiony błąd, który dopiero co bym wprowadził: Path("") to
Path("."), czyli wartość PRAWDZIWA, więc `Path(os.getenv(...)) or domyślna`
zawsze wybierało pustą zmienną i zapisywało stan do katalogu bieżącego.

Wymaga zapisywalnego udziału — osobny PR w repo deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:39:42 +02:00

127 lines
5.3 KiB
HTML

{% extends "base.html" %}
{% set nav_active = "files" %}
{% block title %}Pliki{% endblock %}
{% block content %}
<p class="muted">
Bazy interpretacyjne, z których korzysta program. Zaznaczona baza bierze udział
w wyszukiwaniu; odznaczona zostaje na dysku, ale program jej nie używa.
</p>
{% if error %}<p class="house-warning">{{ error }}</p>{% endif %}
{% if done %}<p class="done-note">{{ done }}</p>{% endif %}
<div class="meta">Bazy ({{ files | length }})</div>
{% if not files %}
<p class="muted">Nie ma jeszcze żadnego pliku bazy.</p>
{% endif %}
<div class="table-scroll">
<table>
<thead>
<tr>
<th>W użyciu</th><th>Plik</th><th>Stan</th><th>Rozmiar</th><th>Zmieniony</th>
{% if can(request, 'files_input') or is_admin %}<th>Operacje</th>{% endif %}
</tr>
</thead>
<tbody>
{% for f in files %}
<tr class="{{ 'row-archived' if f.status == 'archived' else '' }}{{ ' row-held' if f.status == 'quarantine' else '' }}">
<td>
{# Archiwum jest ZAMROŻONE — nie da się go włączyć bez przywrócenia,
a przywrócić może wyłącznie administrator. #}
{% if f.status in ('active', 'ready') %}
<form method="post" action="/files/use" class="inline">
<input type="hidden" name="path" value="{{ f.path }}">
<input type="hidden" name="use" value="{{ '0' if f.in_use else '1' }}">
<button type="submit" class="toggle {{ 'on' if f.in_use else 'off' }}"
title="{{ 'Odstaw tę bazę' if f.in_use else 'Włącz tę bazę do użytku' }}">
{{ '●' if f.in_use else '○' }}
</button>
</form>
{% else %}<span class="muted"></span>{% endif %}
</td>
<td class="mono">{{ f.name }}</td>
<td>
{{ STATUS_LABELS.get(f.status, f.status) }}
{% if f.status == 'archived' and f.archived_at %}
<span class="muted small">({{ f.archived_at[:16] | replace('T', ' ') }})</span>
{% endif %}
{% if is_admin and f.rejected_for %}
<div class="muted small">nie przeszedł: {{ f.rejected_for | join('; ') }}</div>
{% endif %}
</td>
<td class="mono">{{ '%.2f'|format(f.size_mb) if f.size_mb is not none else '—' }} MB</td>
<td class="mono">{{ f.modified or '—' }}</td>
{% if can(request, 'files_input') or is_admin %}
<td class="ops">
{% if can(request, 'files_input') and f.status in ('active', 'ready') %}
<form method="post" action="/files/archive" class="inline">
<input type="hidden" name="path" value="{{ f.path }}">
<button type="submit" class="ghost"
title="Plik zostaje na dysku, zamrożony ze znacznikiem czasu — znika tylko z użytku.">Archiwizuj</button>
</form>
{% endif %}
{% if is_admin %}
{% if f.status in ('archived', 'quarantine') %}
<form method="post" action="/files/restore" class="inline">
<input type="hidden" name="path" value="{{ f.path }}">
<button type="submit" class="ghost">Przywróć</button>
</form>
{% endif %}
<form method="post" action="/files/delete" class="inline">
<input type="hidden" name="path" value="{{ f.path }}">
<button type="submit" class="danger"
onclick="return confirm('Skasować {{ f.name }} bezpowrotnie? Pliku nie da się odzyskać.')">Skasuj</button>
</form>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if can(request, 'files_input') %}
<div class="meta">Wgraj nową bazę</div>
<form method="post" action="/files/upload" enctype="multipart/form-data" class="account-card">
<div class="grid">
<label>Plik <input type="file" name="upload" required></label>
</div>
<div class="actions"><button type="submit">Wgraj</button></div>
</form>
{% endif %}
{% if is_admin %}
{# Reguły walidacji widzi WYŁĄCZNIE administrator — cała ta sekcja nie trafia
nawet do źródła strony dla pozostałych kont. #}
<div class="meta">Reguły przyjmowania baz</div>
<p class="muted small">
Plik musi je spełnić, żeby dało się go włączyć do użytku. Plik, który ich nie
spełnia, <strong>nie jest kasowany</strong> — czeka na Twoją decyzję.
</p>
<form method="post" action="/files/rules" class="account-card">
<div class="grid">
<label>Dozwolone rozszerzenia (po przecinku)
<input type="text" name="extensions" value="{{ (rules.extensions or []) | join(', ') }}"></label>
<label>Maksymalny rozmiar (MB)
<input type="number" name="max_size_mb" step="1" min="0" value="{{ rules.max_size_mb }}"></label>
<label>Minimalna liczba wierszy
<input type="number" name="min_rows" step="1" min="0" value="{{ rules.min_rows }}"></label>
</div>
<div class="grid">
<label>Wymagane kolumny (po przecinku, puste = bez wymagań)
<input type="text" name="required_columns"
value="{{ (rules.required_columns or []) | join(', ') }}"></label>
</div>
<div class="opts">
<label><input type="checkbox" name="reject_duplicate_content" value="1"
{{ 'checked' if rules.reject_duplicate_content else '' }}>
odrzucaj pliki o treści identycznej z już wgraną</label>
</div>
<div class="actions"><button type="submit">Zapisz reguły</button></div>
</form>
{% endif %}
{% endblock %}