🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
{% extends "base.html" %}{% block title %}Panel Admina – PaganOS{% endblock %}
{% block content %}
<style>
.admin-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.admin-grid.small { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.stat-card {
background: #0d1117;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 1rem 1.2rem;
min-height: 100px;
}
.stat-card h3 {
font-size: 0.85rem;
color: var(--muted);
margin: 0 0 0.5rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-card.big {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
line-height: 1;
}
.stat-card.sub { font-size: 0.8rem; color: var(--muted); margin-top: 0.2rem; }
.stat-card.warn { border-color: #d29922; }
.stat-card.ok { border-color: #2ea043; }
.stat-card.err { border-color: #da3633; }
.pagsync-status {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.7rem;
border-radius: 1rem;
font-size: 0.8rem;
font-weight: 600;
}
.pagsync-status.running { background: #2ea04320; color: #2ea043; }
.pagsync-status.stopped { background: #da363320; color: #da3633; }
.pagsync-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.running.pagsync-dot { background: #2ea043; animation: pulse 2s infinite; }
.stopped.pagsync-dot { background: #da3633; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
.toolbar {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.toolbar.btn {
font-size: 0.82rem;
padding: 0.35rem 0.75rem;
}
</style>
{% include "admin/nav.html" %}
<!-- ═══ DASHBOARD ═══ -->
<div class="admin-grid small">
<div class="stat-card">
<h3> {{ t('admin_repo') }}</h3>
<div class="big">{{ stats.pkg_count or 0 }}</div>
<div class="sub">{{ t('admin_pkgs_stable') }}</div>
</div>
<div class="stat-card">
<h3> {{ t('admin_last_build') }}</h3>
<div class="big" style="font-size:1.1rem;line-height:1.3">
{% if stats.last_build %}
{{ stats.last_build.name }}-{{ stats.last_build.version }}
<div class="sub" style="font-size:0.75rem">{{ stats.last_build.status }} · {{ stats.last_build.time }}</div>
{% else %}
<span class="sub">{{ t('admin_none') }}</span>
{% endif %}
</div>
</div>
<div class="stat-card">
<h3> {{ t('admin_builds_today') }}</h3>
<div class="big">{{ stats.total_builds or 0 }}</div>
<div class="sub">✅ {{ stats.success or 0 }} · ❌ {{ stats.failed or 0 }}</div>
</div>
<div class="stat-card {{ 'ok' if pagsync_running else 'err' }}">
<h3> pagsync</h3>
<span class="pagsync-status {{ 'running' if pagsync_running else 'stopped' }}">
<span class="pagsync-dot"></span> {{ t('admin_pagsync_running') if pagsync_running else t('admin_pagsync_stopped') }}
</span>
<div class="toolbar" style="margin-top:0.5rem">
<form method="POST" action="/admin/pagsync/start" style="margin:0" onsubmit="return confirm('{{ t('admin_confirm_start') }}')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm" style="background:#2ea043;color:#fff" {% if pagsync_running %}disabled{% endif %}>▶ Start</button>
</form>
<form method="POST" action="/admin/pagsync/stop" style="margin:0" onsubmit="return confirm('{{ t('admin_confirm_stop') }}')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm" style="background:#da3633;color:#fff" {% if not pagsync_running %}disabled{% endif %}>⏹ Stop</button>
</form>
</div>
</div>
</div>
<!-- ═══ AKCJE SYSTEMOWE ═══ -->
<div class="card" style="margin-top:1rem">
<div class="card-header"><h2> {{ t('admin_sys_tools') }}</h2></div>
<div class="toolbar" style="padding:0.75rem 1rem;flex-wrap:wrap;gap:0.5rem">
<form method="POST" action="/admin/reset-pagsync" style="margin:0" onsubmit="return confirm('Zresetować stan pagsync? Historia buildów zostanie usunięta.')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm"> {{ t('admin_reset_state') }}</button>
</form>
<form method="POST" action="/admin/clean-cache" style="margin:0" onsubmit="return confirm('Wyczyścić cache builda (/var/cache/pagbuild)?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm"> {{ t('admin_clean_cache') }}</button>
</form>
<form method="POST" action="/admin/reset-build-output" style="margin:0" onsubmit="return confirm('Wyczyścić output builda?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm"> {{ t('admin_clean_output') }}</button>
</form>
<form method="POST" action="/admin/clean-source-cache" style="margin:0" onsubmit="return confirm('Uruchomić pagsync --clean-cache? Usunie nieużywane źródła z cache.')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm"> {{ t('admin_clean_sources') }}</button>
</form>
<form method="POST" action="/admin/run-fix-sha" style="margin:0" onsubmit="return confirm('Uruchomić auto-fix SHA256? Sprawdzi i poprawi sumy kontrolne.')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm"> {{ t('admin_fix_sha') }}</button>
</form>
<form method="POST" action="/admin/run-check-updates" style="margin:0" onsubmit="return confirm('Sprawdzić nowsze wersje pakietów? Bez zmian, tylko raport.')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm"> {{ t('admin_check_updates') }}</button>
</form>
</div>
</div>
<!-- ═══ TREŚCI STRONY ═══ -->
<div class="card" style="margin-top:1rem">
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
<h2> Treści całej strony</h2>
{% if content_modified_count %}<span class="badge" style="background:#f8514920;color:#f85149;border:1px solid #f85149;border-radius:3px;font-size:0.72rem;padding:0.15rem 0.5rem"> {{ content_modified_count }} zmodyfikowanych</span>{% endif %}
</div>
<div style="padding:0.75rem 1rem">
<p style="font-size:0.85rem;color:var(--muted);margin:0 0 0.6rem">Edytuj wszystkie teksty strony (nagłówki, opisy, przyciski) w PL i EN – bez zmiany kodu.</p>
<a class="btn btn-sm" style="background:var(--accent);color:#000" href="/admin/content"> Otwórz edytor treści</a>
</div>
</div>
<!-- ═══ DOKUMENTACJA ═══ -->
<div class="card" style="margin-top:1rem">
<div class="card-header"><h2> Dokumentacja</h2></div>
<div class="toolbar" style="padding:0.75rem 1rem;flex-wrap:wrap;gap:0.5rem">
<a class="btn btn-sm" style="background:var(--accent);color:#000" href="//docs.paganlinux.eu/getting-started/edit?lang=pl"> Edytuj „Szybki start” (getting-started PL)</a>
<a class="btn btn-sm" href="//docs.paganlinux.eu/getting-started?lang=pl"> Podgląd getting-started</a>
<a class="btn btn-sm" href="//docs.paganlinux.eu/"> Przegląd dokumentacji</a>
<a class="btn btn-sm" href="//docs.paganlinux.eu/new"> Nowy dokument</a>
</div>
</div>
<!-- ═══ INSTALACJA DO ROOTFS ═══ -->
<div class="card" style="margin-top:1rem">
<div class="card-header"><h2> Instalacja do rootfs</h2></div>
<div style="padding:0.75rem 1rem">
<form method="POST" action="/admin/install-rootfs" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input name="packages" placeholder="nazwy pakietów, np. glib gobject-introspection" required style="flex:1;min-width:240px;padding:0.4rem;background:#0d1117;border:1px solid var(--border);color:#fff;font-size:0.85rem;border-radius:3px">
<button class="btn btn-sm" style="background:#2ea043;color:#fff"> Zainstaluj do rootfs</button>
</form>
<p style="font-size:0.75rem;color:var(--muted);margin:0.4rem 0 0">Instaluje zbudowane pakiety (stable/*.pag) do środowiska budowania rootfs – nagłówki, pliki.gir itp. Jeśli pakietu nie ma w repo (np. glib się nie zbudował), najpierw zbuduj go w panelu build – pojedynczy build instaluje do rootfs automatycznie.</p>
</div>
</div>
<!-- ═══ UŻYTKOWNICY ═══ -->
<div class="card" style="margin-top:1rem">
<div class="card-header"><h2> {{ t('admin_users_title') }} ({{ user_count }})</h2>
<div style="display:flex;gap:0.5rem">
<a href="/admin/recipes-pending" class="btn btn-sm" style="background:var(--accent);color:#000"> {{ t('admin_submissions') }}</a>
<a href="/admin/commits" class="btn btn-sm" style="background:var(--accent);color:#000"> {{ t('admin_commits_page') }}</a>
</div>
</div>
<div style="padding:0 1rem 0.75rem">
<div style="padding:0.75rem;border-bottom:1px solid var(--border)">
<a class="btn btn-sm" href="/admin/settings">⚙️ {{ t('admin_settings') }}</a>
<span style="font-size:0.8rem;color:var(--muted);margin-left:0.5rem">{{ t('admin_reg_info') }}</span>
</div>
</div>
<form method="POST" action="/admin/user/add" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:flex-end;padding:0 1rem 0.5rem">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div><label style="font-size:0.75rem">{{ t('admin_name') }}</label><input name="username" required style="width:130px"></div>
<div><label style="font-size:0.75rem">{{ t('admin_email') }}</label><input name="email" style="width:180px"></div>
<div><label style="font-size:0.75rem">{{ t('admin_password') }}</label><input name="password" type="password" required style="width:120px"></div>
<label style="font-size:0.8rem;color:var(--muted)"><input type="checkbox" name="is_admin" value="1"> Admin</label>
<button class="btn btn-sm" style="background:#2ea043;color:#fff">+ {{ t('admin_add_user') }}</button>
</form>
<div style="overflow-x:auto">
<table style="width:100%">
<tr><th>{{ t('admin_user') }}</th><th>{{ t('admin_email') }}</th><th>{{ t('admin_role') }}</th><th>{{ t('admin_status') }}</th><th>{{ t('admin_actions') }}</th></tr>
{% for u in users %}
<tr>
<td><strong>{{ u.username }}</strong></td>
<td style="font-size:0.85rem;color:var(--muted)">{{ u.email or '' }}</td>
<td>{{ ' Admin' if u.is_admin else ' User' }}</td>
<td>
{% if u.blocked %}<span style="color:#da3633"></span>
{% elif not u.verified %}<span style="color:#d29922">⚠ {{ t('admin_inactive') }}</span>
{% else %}<span style="color:#2ea043">✅</span>{% endif %}
</td>
<td>
<div style="display:flex;gap:0.3rem;flex-wrap:wrap">
{% if u.username!= 'admin' %}
<form method="POST" action="/admin/user/{{ u.username }}/toggle-admin" style="margin:0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem">{{ '↓' if u.is_admin else '↑' }}</button>
</form>
<form method="POST" action="/admin/user/{{ u.username }}/toggle-block" style="margin:0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem">{{ '' if u.blocked else '' }}</button>
</form>
<form method="POST" action="/admin/user/{{ u.username }}/delete" style="margin:0" onsubmit="return confirm('Usunąć {{ u.username }}?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem;background:#da3633;color:#fff"><svg class="i16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>
</form>
{% if not u.verified %}
<form method="POST" action="/admin/user/{{ u.username }}/activate" style="margin:0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem;background:#2ea043;color:#fff">✅</button>
</form>
{% endif %}
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<!-- ═══ REPOZYTORIA GIT ═══ -->
<div class="card" style="margin-top:1rem">
<div class="card-header"><h2> {{ t('admin_git_repos') }} ({{ disk_repo_count }})</h2>
<span style="font-size:0.8rem;color:var(--muted)">
{{ disk_repos|selectattr('visible')|list|length }} {{ t('public_label') }} ·
{{ disk_repos|rejectattr('visible')|list|length }} {{ t('private_label') }}
</span>
</div>
<!-- Nowe repo -->
<form method="POST" action="/admin/repo/create" style="display:flex;gap:0.5rem;padding:0.75rem 1rem;border-bottom:1px solid var(--border)">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input name="name" placeholder="nazwa-repo" required style="flex:1;max-width:250px">
<button class="btn btn-sm" style="background:#2ea043;color:#fff"> {{ t('admin_create_repo') }}</button>
</form>
<!-- Lista repo -->
<div style="overflow-x:auto">
<table style="width:100%">
<tr><th>Repo</th><th>{{ t('admin_visibility') }}</th><th>{{ t('commits_label') }}</th><th>{{ t('admin_size') }}</th><th>{{ t('admin_actions') }}</th></tr>
{% for r in disk_repos %}
<tr>
<td><strong><a href="/git/{{ r.name }}">{{ r.name }}</a></strong></td>
<td>
<form method="POST" action="/admin/repo/toggle-visibility" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="name" value="{{ r.name }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.2rem 0.5rem">
{{ ' ' + t('repo_public') if r.visible else ' ' + t('repo_private') }}
</button>
</form>
</td>
<td style="color:var(--muted)">{{ r.commits }}</td>
<td style="color:var(--muted);font-size:0.8rem">{{ r.size_fmt }}</td>
<td>
<div style="display:flex;gap:0.3rem">
<a href="/git/{{ r.name }}/settings" class="btn btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem" title="Ustawienia"><svg class="i16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/></svg></a>
<form method="POST" action="/admin/repo/delete" style="margin:0" onsubmit="return confirm('Usunąć {{ r.name }}? BEZPOWROTNIE!')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="name" value="{{ r.name }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem;background:#da3633;color:#fff"><svg class="i16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>
</form>
</div>
</td>
</tr>
{% endfor %}
{% if not disk_repos %}
<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:2rem">{{ t('admin_no_repos') }} </td></tr>
{% endif %}
</table>
</div>
</div>
<!-- ═══ UPRAWNIENIA DO REPO ═══ -->
{% if repos %}
<div class="card" style="margin-top:1rem">
<div class="card-header"><h2> {{ t('admin_permissions') }}</h2></div>
<form method="POST" action="/admin/repo/add-permission" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:flex-end;padding:0.75rem 1rem">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div><label style="font-size:0.75rem">Repo</label><select name="repo" style="width:150px">
{% for dr in disk_repos %}<option value="{{ dr.name }}">{{ dr.name }}</option>{% endfor %}
</select></div>
<div><label style="font-size:0.75rem">{{ t('admin_perm_user') }}</label><input name="username" style="width:150px" required></div>
<div><label style="font-size:0.75rem">{{ t('admin_perm_role') }}</label><select name="role" style="width:120px"><option>owner</option><option>maintainer</option><option>writer</option></select></div>
<button class="btn btn-sm" style="background:#2ea043;color:#fff">+ {{ t('admin_add_user') }}</button>
</form>
<div style="overflow-x:auto">
<table style="width:100%">
<tr><th>Repo</th><th>{{ t('admin_perm_user') }}</th><th>{{ t('admin_perm_role') }}</th><th></th></tr>
{% for p in repos %}
<tr>
<td><strong>{{ p.repo_name }}</strong></td>
<td>{{ p.username }}</td>
<td><span style="font-size:0.8rem;padding:0.1rem 0.4rem;border-radius:3px;background:#1a2233">{{ p.role }}</span></td>
<td>
<form method="POST" action="/admin/repo/remove-permission" style="margin:0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="repo" value="{{ p.repo_name }}">
<input type="hidden" name="username" value="{{ p.username }}">
<button class="btn btn-sm" style="font-size:0.7rem;padding:0.1rem 0.35rem;background:#da3633;color:#fff"><svg class="i16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
<div class="admin-grid small" style="margin-top:1rem">
<div class="stat-card">
<h3> {{ t('admin_disk') }}</h3>
<div class="big" style="font-size:1.3rem">{{ stats.disk_total }}</div>
<div class="sub">{{ t('admin_free') }}: {{ stats.disk_free }}</div>
</div>
<div class="stat-card">
<h3> {{ t('admin_ram') }}</h3>
<div class="big" style="font-size:1.3rem">{{ stats.mem_total }}</div>
<div class="sub">{{ t('admin_free') }}: {{ stats.mem_free }}</div>
</div>
<div class="stat-card">
<h3> {{ t('admin_cpu') }}</h3>
<div class="big" style="font-size:1.3rem">{{ stats.cpu_cores }}</div>
<div class="sub">{{ t('admin_load_avg') }}: {{ stats.load }}</div>
</div>
<div class="stat-card">
<h3> Repozytoria git</h3>
<div class="big" style="font-size:1.3rem">{{ stats.git_repos }}</div>
<div class="sub">w /var/git</div>
</div>
</div>
{% endblock %}