🌐 i18n(i18n): add multilingual support with translations
- create i18n.py for managing translations and resolving locale - add German and English translations for various UI components - integrate translation functions into templates for dynamic language support
This commit is contained in:
parent
1aafd6d5a3
commit
cc7c75ba33
11 changed files with 548 additions and 157 deletions
|
|
@ -3,9 +3,9 @@
|
|||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold tracking-tight">Admin-Dashboard</h1>
|
||||
<h1 class="text-lg font-semibold tracking-tight">{{ t("admin.title") }}</h1>
|
||||
<p class="text-xs text-slate-400">
|
||||
Globale Übersicht über alle nicht archivierten Server und Benutzer.
|
||||
{{ t("admin.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -13,36 +13,36 @@
|
|||
{% if stats %}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/70 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-slate-400 mb-1">Benutzer</span>
|
||||
<span class="text-[11px] text-slate-400 mb-1">{{ t("admin.users") }}</span>
|
||||
<span class="text-lg font-semibold text-slate-100">
|
||||
{{ stats.total_users }}
|
||||
</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">Accounts</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">{{ t("admin.users_caption") }}</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/70 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-slate-400 mb-1">Server</span>
|
||||
<span class="text-[11px] text-slate-400 mb-1">{{ t("admin.servers") }}</span>
|
||||
<span class="text-lg font-semibold text-slate-100">
|
||||
{{ stats.total_servers }}
|
||||
</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">nicht archiviert</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">{{ t("admin.servers_caption") }}</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/70 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-slate-400 mb-1">Laufende Kosten</span>
|
||||
<span class="text-[11px] text-slate-400 mb-1">{{ t("admin.costs") }}</span>
|
||||
<span class="text-lg font-semibold text-slate-100">
|
||||
{{ "%.2f"|format(stats.monthly_total) }}
|
||||
{% if stats.monthly_currency %} {{ stats.monthly_currency }}{% endif %}
|
||||
</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">
|
||||
pro Monat{% if stats.mixed_currencies %} (gemischte Währungen){% endif %}
|
||||
{{ t("admin.per_month") }}{% if stats.mixed_currencies %} {{ t("stats.mixed_currencies") }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/70 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-slate-400 mb-1">Vertragstatus</span>
|
||||
<span class="text-[11px] text-slate-400 mb-1">{{ t("admin.contract_status") }}</span>
|
||||
<span class="text-[11px] text-amber-200">
|
||||
Bald auslaufend: {{ stats.expiring_soon }}
|
||||
{{ t("admin.expiring") }}: {{ stats.expiring_soon }}
|
||||
</span>
|
||||
<span class="text-[11px] text-rose-200">
|
||||
Abgelaufen: {{ stats.expired }}
|
||||
{{ t("admin.expired") }}: {{ stats.expired }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -50,16 +50,16 @@
|
|||
|
||||
<!-- Provider breakdown -->
|
||||
<section class="space-y-2 mt-4">
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-200">Nach Provider</h2>
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-200">{{ t("admin.by_provider") }}</h2>
|
||||
<div class="overflow-hidden rounded-xl border border-slate-800 bg-slate-900/60">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-900/80 text-xs uppercase tracking-wide text-slate-400">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Provider</th>
|
||||
<th class="px-3 py-2 text-right">Server</th>
|
||||
<th class="px-3 py-2 text-right">Monatskosten</th>
|
||||
<th class="px-3 py-2 text-right">Laufen bald aus</th>
|
||||
<th class="px-3 py-2 text-right">Abgelaufen</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("admin.provider") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("admin.count") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("admin.monthly_costs") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("admin.expiring_soon") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("admin.expired_count") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
<!-- Contracts: expiring soon & expired -->
|
||||
<section class="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-200">Laufen bald aus (≤ 30 Tage)</h2>
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-200">{{ t("admin.contracts_expiring") }}</h2>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-3 text-xs">
|
||||
{% if expiring_soon_list %}
|
||||
<ul class="space-y-2">
|
||||
|
|
@ -117,13 +117,13 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="text-slate-500">Keine Verträge laufen in den nächsten 30 Tagen aus.</div>
|
||||
<div class="text-slate-500">{{ t("admin.contracts_none_soon") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-200">Abgelaufene Verträge</h2>
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-200">{{ t("admin.contracts_expired") }}</h2>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-3 text-xs">
|
||||
{% if expired_list %}
|
||||
<ul class="space-y-2">
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="text-slate-500">Keine abgelaufenen Verträge gefunden.</div>
|
||||
<div class="text-slate-500">{{ t("admin.contracts_none_expired") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="de" class="dark" id="html-root">
|
||||
<html lang="{{ request.state.locale if request and request.state and request.state.locale else 'de' }}" class="dark" id="html-root">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>FleetLedger</title>
|
||||
|
|
@ -37,52 +37,63 @@
|
|||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold tracking-tight">FleetLedger</div>
|
||||
<div class="text-xs text-slate-400">Deine gemieteten Server im Blick</div>
|
||||
<div class="text-xs text-slate-400">{{ t("brand.tagline") }}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
{% if current_user %}
|
||||
<span class="text-slate-300">
|
||||
Eingeloggt als <span class="font-semibold">{{ current_user.username }}</span>
|
||||
{{ t("nav.logged_in_as") }} <span class="font-semibold">{{ current_user.username }}</span>
|
||||
{% if current_user.is_admin %}
|
||||
<span class="ml-1 px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/40 text-emerald-200 text-[10px]">Admin</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="/admin/dashboard" class="text-slate-300 hover:text-white underline-offset-2 hover:underline">
|
||||
Admin-Dashboard
|
||||
{{ t("nav.admin") }}
|
||||
</a>
|
||||
<a href="/users" class="text-slate-300 hover:text-white underline-offset-2 hover:underline">
|
||||
User
|
||||
{{ t("nav.users") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/" class="text-slate-300 hover:text-white underline-offset-2 hover:underline">
|
||||
Übersicht
|
||||
{{ t("nav.overview") }}
|
||||
</a>
|
||||
<a href="/map" class="text-slate-300 hover:text-white underline-offset-2 hover:underline">
|
||||
Karte
|
||||
{{ t("nav.map") }}
|
||||
</a>
|
||||
<a
|
||||
href="/servers/new"
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-indigo-500 px-3 py-1.5 font-medium hover:bg-indigo-400 focus:outline-none focus:ring focus:ring-indigo-500/40"
|
||||
>
|
||||
<span class="text-lg leading-none">+</span>
|
||||
<span>Server anlegen</span>
|
||||
<span>{{ t("nav.new_server") }}</span>
|
||||
</a>
|
||||
<a
|
||||
href="/logout"
|
||||
class="rounded-lg border border-slate-700 px-2.5 py-1 text-slate-300 hover:border-slate-500 hover:text-white"
|
||||
>
|
||||
Logout
|
||||
{{ t("nav.logout") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/login" class="text-slate-300 hover:text-white underline-offset-2 hover:underline">
|
||||
Login
|
||||
{{ t("nav.login") }}
|
||||
</a>
|
||||
<a href="/register" class="text-slate-300 hover:text-white underline-offset-2 hover:underline">
|
||||
Registrieren
|
||||
{{ t("nav.register") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="flex items-center gap-1 border border-slate-800 rounded-lg px-2 py-1">
|
||||
{% for code, label in languages.items() %}
|
||||
<a
|
||||
href="/lang/{{ code }}"
|
||||
class="px-1 text-xs {% if request.state.locale == code %}text-white font-semibold{% else %}text-slate-400 hover:text-white{% endif %}"
|
||||
>
|
||||
{{ label }}
|
||||
</a>
|
||||
{% if not loop.last %}<span class="text-slate-700">|</span>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="rounded-lg border border-slate-700 bg-slate-900/60 px-2.5 py-1 text-slate-300 hover:border-slate-500 hover:text-white"
|
||||
|
|
@ -102,8 +113,8 @@
|
|||
|
||||
<footer class="border-t border-slate-800 bg-slate-900/60">
|
||||
<div class="max-w-5xl mx-auto px-4 py-3 text-xs text-slate-500 flex justify-between items-center">
|
||||
<span>Selfhosted VPS overview</span>
|
||||
<span>PWA ready · Dark Mode</span>
|
||||
<span>{{ t("footer.left") }}</span>
|
||||
<span>{{ t("footer.right") }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-sm mx-auto mt-8">
|
||||
<h1 class="text-lg font-semibold tracking-tight mb-1">Login</h1>
|
||||
<p class="text-xs text-slate-400 mb-4">Melde dich an, um deine Server zu verwalten.</p>
|
||||
<h1 class="text-lg font-semibold tracking-tight mb-1">{{ t("login.title") }}</h1>
|
||||
<p class="text-xs text-slate-400 mb-4">{{ t("login.subtitle") }}</p>
|
||||
|
||||
{% if error %}
|
||||
<div class="mb-4 text-xs text-rose-200 bg-rose-500/10 border border-rose-500/60 rounded-lg p-3">
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<form method="post" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Benutzername</label>
|
||||
<label class="text-xs text-slate-300">{{ t("login.username") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Passwort</label>
|
||||
<label class="text-xs text-slate-300">{{ t("login.password") }}</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
|
|
@ -31,13 +31,13 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-xs text-slate-400">
|
||||
<span>Noch kein Account? <a href="/register" class="text-indigo-300 hover:text-indigo-200 underline">Registrieren</a></span>
|
||||
<span>{{ t("login.no_account") }} <a href="/register" class="text-indigo-300 hover:text-indigo-200 underline">{{ t("login.register") }}</a></span>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400 focus:outline-none focus:ring focus:ring-indigo-500/40"
|
||||
>
|
||||
Einloggen
|
||||
{{ t("login.submit") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-sm mx-auto mt-8">
|
||||
<h1 class="text-lg font-semibold tracking-tight mb-1">Registrieren</h1>
|
||||
<h1 class="text-lg font-semibold tracking-tight mb-1">{{ t("register.title") }}</h1>
|
||||
<p class="text-xs text-slate-400 mb-4">
|
||||
Erstelle einen neuen Account. Der erste Benutzer wird automatisch Admin.
|
||||
{{ t("register.subtitle") }}
|
||||
</p>
|
||||
|
||||
{% if error %}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<form method="post" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Benutzername</label>
|
||||
<label class="text-xs text-slate-300">{{ t("register.username") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">E-Mail (optional)</label>
|
||||
<label class="text-xs text-slate-300">{{ t("register.email") }}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Passwort</label>
|
||||
<label class="text-xs text-slate-300">{{ t("register.password") }}</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Passwort bestätigen</label>
|
||||
<label class="text-xs text-slate-300">{{ t("register.password_confirm") }}</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password_confirm"
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
type="submit"
|
||||
class="w-full rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400 focus:outline-none focus:ring focus:ring-indigo-500/40"
|
||||
>
|
||||
Account anlegen
|
||||
{{ t("register.submit") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs text-slate-400 mb-1">Server</div>
|
||||
<div class="text-xs text-slate-400 mb-1">{{ t("server_detail.server") }}</div>
|
||||
<h1 class="text-lg font-semibold tracking-tight flex items-center gap-2">
|
||||
{{ server.name }}
|
||||
{% if server.is_expired %}
|
||||
<span class="inline-flex items-center rounded-full bg-rose-600/15 border border-rose-600/60 px-2 py-0.5 text-[10px] text-rose-200">
|
||||
abgelaufen
|
||||
{{ t("status.expired") }}
|
||||
</span>
|
||||
{% elif server.is_expiring_soon %}
|
||||
<span class="inline-flex items-center rounded-full bg-amber-500/15 border border-amber-500/60 px-2 py-0.5 text-[10px] text-amber-200">
|
||||
läuft bald aus
|
||||
{{ t("status.expiring") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
|
@ -21,14 +21,14 @@
|
|||
{% endif %}
|
||||
{% if server.contract_end %}
|
||||
<p class="text-[11px] text-slate-500 mt-1">
|
||||
Vertragsende: {{ server.contract_end }}
|
||||
{{ t("server_detail.contract_end") }}: {{ server.contract_end }}
|
||||
{% if server.days_until_contract_end is not none %}
|
||||
{% if server.days_until_contract_end < 0 %}
|
||||
(vor {{ (server.days_until_contract_end * -1) }} Tagen)
|
||||
({{ t("server_detail.days_ago", days=(server.days_until_contract_end * -1)) }})
|
||||
{% elif server.days_until_contract_end == 0 %}
|
||||
(heute)
|
||||
({{ t("server_detail.today") }})
|
||||
{% else %}
|
||||
(in {{ server.days_until_contract_end }} Tagen)
|
||||
({{ t("server_detail.in_days", days=server.days_until_contract_end) }})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
|
@ -55,15 +55,15 @@
|
|||
href="/servers/{{ server.id }}/edit"
|
||||
class="ml-2 rounded-lg border border-slate-700 px-3 py-1 text-xs text-slate-100 hover:border-slate-500 hover:text-white"
|
||||
>
|
||||
Bearbeiten
|
||||
{{ t("server_detail.edit") }}
|
||||
</a>
|
||||
<form method="post" action="/servers/{{ server.id }}/archive" onsubmit="return confirm('Diesen Server archivieren?');">
|
||||
<form method="post" action="/servers/{{ server.id }}/archive" onsubmit="return confirm('{{ t("server_detail.archive_confirm") }}');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg border border-rose-600 px-3 py-1 text-xs text-rose-200 hover:bg-rose-600/10"
|
||||
>
|
||||
Archivieren
|
||||
{{ t("server_detail.archive") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -72,30 +72,30 @@
|
|||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<!-- Network & Cost -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-2">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Netz & Kosten</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.net_costs") }}</h2>
|
||||
<dl class="text-xs text-slate-300 space-y-1">
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">IPv4</dt>
|
||||
<dt class="text-slate-400">{{ t("field.ipv4") }}</dt>
|
||||
<dd class="text-right">{% if server.ipv4 %}{{ server.ipv4 }}{% else %}–{% endif %}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">IPv6</dt>
|
||||
<dt class="text-slate-400">{{ t("field.ipv6") }}</dt>
|
||||
<dd class="text-right">{% if server.ipv6 %}{{ server.ipv6 }}{% else %}–{% endif %}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">Kosten</dt>
|
||||
<dt class="text-slate-400">{{ t("field.costs") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.price %}
|
||||
{{ "%.2f"|format(server.price) }} {{ server.currency }} /
|
||||
{{ "Monat" if server.billing_period == "monthly" else "Jahr" }}
|
||||
{{ t("price.month") if server.billing_period == "monthly" else t("price.year") }}
|
||||
{% else %}–{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">Vertrag</dt>
|
||||
<dt class="text-slate-400">{{ t("field.contract") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.contract_start %}ab {{ server.contract_start }}{% endif %}
|
||||
{% if server.contract_end %} bis {{ server.contract_end }}{% endif %}
|
||||
{% if server.contract_start %}{{ t("label.contract_start") }} {{ server.contract_start }}{% endif %}
|
||||
{% if server.contract_end %} {{ t("label.contract_end") }} {{ server.contract_end }}{% endif %}
|
||||
{% if not server.contract_start and not server.contract_end %}–{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
|
|
@ -104,23 +104,23 @@
|
|||
|
||||
<!-- Hardware -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-2">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Hardware</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.hardware") }}</h2>
|
||||
<dl class="text-xs text-slate-300 space-y-1">
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">CPU</dt>
|
||||
<dt class="text-slate-400">{{ t("field.cpu") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.cpu_model %}{{ server.cpu_model }}{% else %}–{% endif %}
|
||||
{% if server.cpu_cores %} ({{ server.cpu_cores }} Cores){% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">RAM</dt>
|
||||
<dt class="text-slate-400">{{ t("field.ram") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.ram_mb %}{{ server.ram_mb }} MB{% else %}–{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">Storage</dt>
|
||||
<dt class="text-slate-400">{{ t("field.storage") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.storage_gb %}{{ server.storage_gb }} GB{% else %}–{% endif %}
|
||||
{% if server.storage_type %} ({{ server.storage_type }}){% endif %}
|
||||
|
|
@ -131,30 +131,30 @@
|
|||
|
||||
<!-- Access -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-2">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Zugänge</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.access") }}</h2>
|
||||
<dl class="text-xs text-slate-300 space-y-1">
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">Management</dt>
|
||||
<dt class="text-slate-400">{{ t("field.management") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.mgmt_url %}
|
||||
<a href="{{ server.mgmt_url }}" target="_blank" rel="noopener noreferrer" class="text-indigo-300 hover:text-indigo-200 underline">
|
||||
Console öffnen
|
||||
{{ t("nav.console") }}
|
||||
</a>
|
||||
{% else %}–{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">Mgmt-User</dt>
|
||||
<dt class="text-slate-400">{{ t("field.mgmt_user") }}</dt>
|
||||
<dd class="text-right">{% if server.mgmt_user %}{{ server.mgmt_user }}{% else %}–{% endif %}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">Mgmt-Passwort</dt>
|
||||
<dt class="text-slate-400">{{ t("field.mgmt_password") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.mgmt_password_encrypted %}
|
||||
{% if mgmt_password %}
|
||||
<span class="font-mono text-slate-100">{{ mgmt_password }}</span>
|
||||
{% else %}
|
||||
<span class="text-slate-500">verschlüsselt gespeichert (Key fehlt?)</span>
|
||||
<span class="text-slate-500">{{ t("mgmt.password_encrypted_missing") }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
–
|
||||
|
|
@ -162,11 +162,11 @@
|
|||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">SSH User</dt>
|
||||
<dt class="text-slate-400">{{ t("field.ssh_user") }}</dt>
|
||||
<dd class="text-right">{% if server.ssh_user %}{{ server.ssh_user }}{% else %}–{% endif %}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-slate-400">SSH Key Hint</dt>
|
||||
<dt class="text-slate-400">{{ t("field.ssh_key_hint") }}</dt>
|
||||
<dd class="text-right">
|
||||
{% if server.ssh_key_hint %}
|
||||
<span class="font-mono">{{ server.ssh_key_hint }}</span>
|
||||
|
|
@ -175,22 +175,22 @@
|
|||
</div>
|
||||
</dl>
|
||||
<p class="mt-2 text-[11px] text-slate-500">
|
||||
Hinweis: Es werden nur Key-Namen gespeichert, keine privaten SSH-Schlüssel.
|
||||
{{ t("note.ssh_keys") }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Notes -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-2 md:col-span-2">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Notizen</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.notes") }}</h2>
|
||||
<div class="text-xs text-slate-200 whitespace-pre-wrap">
|
||||
{% if server.notes %}{{ server.notes }}{% else %}<span class="text-slate-500">Keine Notizen.</span>{% endif %}
|
||||
{% if server.notes %}{{ server.notes }}{% else %}<span class="text-slate-500">{{ t("notes.empty") }}</span>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs text-slate-500">
|
||||
<a href="/" class="hover:text-slate-200 underline underline-offset-2">Zurück zur Übersicht</a>
|
||||
<span>Zuletzt aktualisiert: {{ server.updated_at }}</span>
|
||||
<a href="/" class="hover:text-slate-200 underline underline-offset-2">{{ t("back.overview") }}</a>
|
||||
<span>{{ t("updated_at") }} {{ server.updated_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<h1 class="text-lg font-semibold tracking-tight mb-1">
|
||||
{% if server %}Server bearbeiten{% else %}Neuen Server anlegen{% endif %}
|
||||
{% if server %}{{ t("form.title.edit") }}{% else %}{{ t("form.title.new") }}{% endif %}
|
||||
</h1>
|
||||
<p class="text-xs text-slate-400 mb-4">
|
||||
Trage alle relevanten Infos zu deinem VPS / Server ein.
|
||||
{{ t("form.subtitle") }}
|
||||
</p>
|
||||
|
||||
{% if not can_encrypt %}
|
||||
<div class="mb-4 text-xs text-amber-200 bg-amber-500/10 border border-amber-500/60 rounded-lg p-3">
|
||||
ENCRYPTION_KEY ist nicht gesetzt – eingegebene Management-Passwörter werden nicht gespeichert.
|
||||
{{ t("warning.no_encryption_short") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -18,10 +18,10 @@
|
|||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<!-- General -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Allgemein</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.general") }}</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Name *</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.name") }} *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Provider *</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.provider") }} *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="provider"
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Hostname</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.hostname") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="hostname"
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Typ</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.type") }}</label>
|
||||
<select
|
||||
name="type"
|
||||
class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500"
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Location</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.location") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Tags (kommagetrennt)</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.tags") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tags"
|
||||
|
|
@ -91,10 +91,10 @@
|
|||
|
||||
<!-- Network -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Netzwerk</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.network") }}</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">IPv4</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.ipv4") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ipv4"
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">IPv6</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.ipv6") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ipv6"
|
||||
|
|
@ -118,10 +118,10 @@
|
|||
|
||||
<!-- Costs -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Kosten</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.costs") }}</h2>
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Betrag</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.amount") }}</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Währung</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.currency") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="currency"
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Abrechnung</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.billing") }}</label>
|
||||
<select
|
||||
name="billing_period"
|
||||
class="w-full rounded-lg border border-slate-700 bg-slate-950/60 px-3 py-2 text-sm outline-none focus:border-indigo-500"
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Vertragsbeginn</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.contract_start") }}</label>
|
||||
<input
|
||||
type="date"
|
||||
name="contract_start"
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Vertragsende</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.contract_end") }}</label>
|
||||
<input
|
||||
type="date"
|
||||
name="contract_end"
|
||||
|
|
@ -175,10 +175,10 @@
|
|||
|
||||
<!-- Hardware -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Hardware</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.hardware.small") }}</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">CPU-Modell</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.cpu_model") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="cpu_model"
|
||||
|
|
@ -188,7 +188,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">CPU Cores</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.cpu_cores") }}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="cpu_cores"
|
||||
|
|
@ -197,7 +197,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">RAM (MB)</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.ram_mb") }}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="ram_mb"
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Storage (GB)</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.storage_gb") }}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="storage_gb"
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Storage-Typ</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.storage_type") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="storage_type"
|
||||
|
|
@ -229,13 +229,13 @@
|
|||
|
||||
<!-- Access -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Zugänge</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.access.small") }}</h2>
|
||||
<p class="text-xs text-slate-400">
|
||||
SSH: hier nur <strong>Key-Namen</strong> oder Hints eintragen, keine privaten Keys.
|
||||
{{ t("hint.ssh") }}
|
||||
</p>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Management URL</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.mgmt_url") }}</label>
|
||||
<input
|
||||
type="url"
|
||||
name="mgmt_url"
|
||||
|
|
@ -245,7 +245,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Management User</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.mgmt_user") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mgmt_user"
|
||||
|
|
@ -254,7 +254,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">Management Passwort</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.mgmt_password") }}</label>
|
||||
<input
|
||||
type="password"
|
||||
name="mgmt_password"
|
||||
|
|
@ -263,7 +263,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">SSH User</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.ssh_user") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ssh_user"
|
||||
|
|
@ -273,7 +273,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-slate-300">SSH Key Hint</label>
|
||||
<label class="text-xs text-slate-300">{{ t("label.ssh_key_hint") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ssh_key_hint"
|
||||
|
|
@ -287,7 +287,7 @@
|
|||
|
||||
<!-- Notes -->
|
||||
<section class="rounded-xl border border-slate-800 bg-slate-900/70 p-4 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-slate-100">Notizen</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-100">{{ t("section.notes.small") }}</h2>
|
||||
<textarea
|
||||
name="notes"
|
||||
rows="4"
|
||||
|
|
@ -300,13 +300,13 @@
|
|||
<a
|
||||
href="/"
|
||||
class="rounded-lg border border-slate-700 px-4 py-2 text-sm text-slate-200 hover:border-slate-500 hover:text-white"
|
||||
>Abbrechen</a
|
||||
>{{ t("btn.cancel") }}</a
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400 focus:outline-none focus:ring focus:ring-indigo-500/40"
|
||||
>
|
||||
{% if server %}Änderungen speichern{% else %}Speichern{% endif %}
|
||||
{% if server %}{{ t("btn.save_changes") }}{% else %}{{ t("btn.save") }}{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold tracking-tight">Deine Server</h1>
|
||||
<h1 class="text-lg font-semibold tracking-tight">{{ t("server_list.title") }}</h1>
|
||||
<p class="text-xs text-slate-400">
|
||||
Übersicht aller gemieteten VPS, dedizierten Server und Storage-Systeme.
|
||||
{{ t("server_list.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-slate-400">
|
||||
{% if not can_encrypt %}
|
||||
<span class="px-2 py-1 rounded-md border border-amber-500/60 bg-amber-500/10 text-amber-200">
|
||||
Hinweis: ENCRYPTION_KEY nicht gesetzt – Passwörter werden nicht gespeichert.
|
||||
{{ t("warning.no_encryption") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -21,35 +21,35 @@
|
|||
<!-- Small dashboard summary row -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/70 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-slate-400 mb-1">Gesamt</span>
|
||||
<span class="text-[11px] text-slate-400 mb-1">{{ t("stats.total") }}</span>
|
||||
<span class="text-lg font-semibold text-slate-100">
|
||||
{{ stats.total_servers }}
|
||||
</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">aktive Server</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">{{ t("stats.active") }}</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-800 bg-slate-900/70 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-slate-400 mb-1">Laufende Kosten</span>
|
||||
<span class="text-[11px] text-slate-400 mb-1">{{ t("stats.costs") }}</span>
|
||||
<span class="text-lg font-semibold text-slate-100">
|
||||
{{ "%.2f"|format(stats.monthly_total) }}
|
||||
{% if stats.monthly_currency %} {{ stats.monthly_currency }}{% endif %}
|
||||
</span>
|
||||
<span class="text-[11px] text-slate-500 mt-1">
|
||||
pro Monat{% if stats.mixed_currencies %} (gemischte Währungen){% endif %}
|
||||
{{ t("stats.per_month") }}{% if stats.mixed_currencies %} {{ t("stats.mixed_currencies") }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-amber-500/40 bg-amber-500/10 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-amber-200 mb-1">Laufen bald aus</span>
|
||||
<span class="text-[11px] text-amber-200 mb-1">{{ t("stats.expiring_soon") }}</span>
|
||||
<span class="text-lg font-semibold text-amber-100">
|
||||
{{ stats.expiring_soon }}
|
||||
</span>
|
||||
<span class="text-[11px] text-amber-200/80 mt-1">≤ 30 Tage</span>
|
||||
<span class="text-[11px] text-amber-200/80 mt-1">{{ t("stats.expiring_soon_hint") }}</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-rose-600/60 bg-rose-600/10 px-3 py-3 flex flex-col">
|
||||
<span class="text-[11px] text-rose-100 mb-1">Abgelaufen</span>
|
||||
<span class="text-[11px] text-rose-100 mb-1">{{ t("stats.expired") }}</span>
|
||||
<span class="text-lg font-semibold text-rose-50">
|
||||
{{ stats.expired }}
|
||||
</span>
|
||||
<span class="text-[11px] text-rose-100/80 mt-1">Vertrag beendet</span>
|
||||
<span class="text-[11px] text-rose-100/80 mt-1">{{ t("stats.expired_hint") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -59,13 +59,13 @@
|
|||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-900/80">
|
||||
<tr class="text-xs uppercase tracking-wide text-slate-400">
|
||||
<th class="px-3 py-2 text-left">Name</th>
|
||||
<th class="px-3 py-2 text-left">Provider</th>
|
||||
<th class="px-3 py-2 text-left">Type</th>
|
||||
<th class="px-3 py-2 text-left">Location</th>
|
||||
<th class="px-3 py-2 text-left">IPv4</th>
|
||||
<th class="px-3 py-2 text-right">Kosten</th>
|
||||
<th class="px-3 py-2 text-right">Aktion</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("table.name") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("table.provider") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("table.type") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("table.location") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("table.ipv4") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("table.costs") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("table.action") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -83,11 +83,11 @@
|
|||
</div>
|
||||
{% if s.is_expired %}
|
||||
<span class="inline-flex items-center rounded-full bg-rose-600/15 border border-rose-600/60 px-2 py-0.5 text-[10px] text-rose-200">
|
||||
abgelaufen
|
||||
{{ t("status.expired") }}
|
||||
</span>
|
||||
{% elif s.is_expiring_soon %}
|
||||
<span class="inline-flex items-center rounded-full bg-amber-500/15 border border-amber-500/60 px-2 py-0.5 text-[10px] text-amber-200">
|
||||
läuft bald aus
|
||||
{{ t("status.expiring") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -107,14 +107,14 @@
|
|||
<td class="px-3 py-2 text-right text-slate-200">
|
||||
{% if s.price %}
|
||||
{{ "%.2f"|format(s.price) }} {{ s.currency }}
|
||||
<span class="text-[11px] text-slate-400">/ {{ "Monat" if s.billing_period == "monthly" else "Jahr" }}</span>
|
||||
<span class="text-[11px] text-slate-400">/ {{ t("price.month") if s.billing_period == "monthly" else t("price.year") }}</span>
|
||||
{% else %}
|
||||
<span class="text-slate-500">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-right text-slate-200">
|
||||
<a href="/servers/{{ s.id }}/edit" class="text-indigo-300 hover:text-indigo-200 underline text-xs">
|
||||
Bearbeiten
|
||||
{{ t("server_detail.edit") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -124,7 +124,7 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl border border-dashed border-slate-700 bg-slate-900/40 p-6 text-sm text-slate-300">
|
||||
Noch keine Server erfasst. Leg den ersten mit „Server anlegen“ oben rechts an.
|
||||
{{ t("server_list.empty") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@
|
|||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold tracking-tight">Server-Karte</h1>
|
||||
<h1 class="text-lg font-semibold tracking-tight">{{ t("map.title") }}</h1>
|
||||
<p class="text-xs text-slate-400">
|
||||
Zeigt alle nicht archivierten Server mit gesetzter Location auf einer Karte.
|
||||
Für grobe Übersicht reicht die Stadt – keine exakten GPS-Daten notwendig.
|
||||
{{ t("map.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -50,8 +49,7 @@
|
|||
></div>
|
||||
|
||||
<p class="text-[11px] text-slate-500">
|
||||
Hinweis: Marker werden anhand der Location-Namen grob auf der Weltkarte platziert. Mehrere Server
|
||||
in derselben Stadt werden leicht versetzt dargestellt, damit sie klickbar bleiben.
|
||||
{{ t("map.note") }}
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold tracking-tight">Benutzerverwaltung</h1>
|
||||
<h1 class="text-lg font-semibold tracking-tight">{{ t("users.title") }}</h1>
|
||||
<p class="text-xs text-slate-400">
|
||||
Admins können Benutzer aktivieren/deaktivieren. Der eigene Account kann nicht deaktiviert werden.
|
||||
{{ t("users.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -14,11 +14,11 @@
|
|||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-900/80 text-xs uppercase tracking-wide text-slate-400">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Benutzername</th>
|
||||
<th class="px-3 py-2 text-left">E-Mail</th>
|
||||
<th class="px-3 py-2 text-left">Rolle</th>
|
||||
<th class="px-3 py-2 text-left">Status</th>
|
||||
<th class="px-3 py-2 text-right">Aktion</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("users.username") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("users.email") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("users.role") }}</th>
|
||||
<th class="px-3 py-2 text-left">{{ t("users.status") }}</th>
|
||||
<th class="px-3 py-2 text-right">{{ t("users.action") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -31,16 +31,16 @@
|
|||
{% if u.email %}{{ u.email }}{% else %}<span class="text-slate-500">–</span>{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-slate-200">
|
||||
{% if u.is_admin %}Admin{% else %}User{% endif %}
|
||||
{% if u.is_admin %}{{ t("role.admin") }}{% else %}{{ t("role.user") }}{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-slate-200">
|
||||
{% if u.is_active %}
|
||||
<span class="inline-flex rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] text-emerald-200 border border-emerald-500/50">
|
||||
aktiv
|
||||
{{ t("status.active") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex rounded-full bg-slate-800 px-2 py-0.5 text-[11px] text-slate-300 border border-slate-600">
|
||||
deaktiviert
|
||||
{{ t("status.inactive") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
|
@ -52,11 +52,11 @@
|
|||
type="submit"
|
||||
class="rounded-lg border border-slate-700 px-3 py-1 text-xs text-slate-200 hover:border-slate-500 hover:text-white"
|
||||
>
|
||||
{% if u.is_active %}Deaktivieren{% else %}Aktivieren{% endif %}
|
||||
{% if u.is_active %}{{ t("action.deactivate") }}{% else %}{{ t("action.activate") }}{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-[11px] text-slate-500">Eigener Account</span>
|
||||
<span class="text-[11px] text-slate-500">{{ t("users.own_account") }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue