Files
duffyduck cd9e33bdfe Auge zum Ein-/Ausblenden bei Kennwort und Access Token
Kennwort- und Access-Token-Feld haben jetzt ein Augen-Symbol, mit dem
die Eingabe sichtbar gemacht und kontrolliert werden kann. Umgesetzt
ueber ein wiederverwendbares pw_field-Macro (gilt fuer Einzel- und
Bulk-Formular).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:30:37 +02:00

260 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}Images - Docker Registry{% endblock %}
{# Passwort-Feld mit Auge zum Ein-/Ausblenden der Eingabe. #}
{% macro pw_field(fid, ph='') %}
<div class="pw-wrap">
<input type="password" id="{{ fid }}" name="{{ fid }}" autocomplete="new-password"
{% if ph %}placeholder="{{ ph }}"{% endif %}>
<button type="button" class="pw-toggle" onclick="togglePw('{{ fid }}', this)"
title="Anzeigen/Verbergen" aria-label="Eingabe anzeigen oder verbergen">
<svg class="eye-on" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-off" style="display: none;" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
{% endmacro %}
{# Wiederverwendbarer Zugangsdaten-Block (Benutzername/Kennwort ODER Access Token).
prefix unterscheidet die Felder von Einzel- und Bulk-Formular. #}
{% macro cred_fields(prefix) %}
<p style="margin-top: 0.75rem; font-size: 0.8rem; color: #6b7280;">
Nur bei privaten Quell-Images noetig &ndash; passend ankreuzen, was du hast:
</p>
<div class="form-group" style="margin-top: 0.4rem;">
<label style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600;">
<input type="checkbox" id="{{ prefix }}use_creds" name="{{ prefix }}use_creds"
onchange="toggleCredMode('{{ prefix }}', 'creds')" style="width: auto;">
Benutzername / Kennwort
</label>
</div>
<div id="{{ prefix }}creds_box" style="display: none;">
<div class="form-row" style="margin-top: 0.5rem;">
<div class="form-group">
<label for="{{ prefix }}src_username">Benutzername</label>
<input type="text" id="{{ prefix }}src_username" name="{{ prefix }}src_username" autocomplete="off">
</div>
<div class="form-group">
<label for="{{ prefix }}src_password">Kennwort</label>
{{ pw_field(prefix ~ 'src_password') }}
</div>
</div>
</div>
<div class="form-group" style="margin-top: 0.6rem;">
<label style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600;">
<input type="checkbox" id="{{ prefix }}use_token" name="{{ prefix }}use_token"
onchange="toggleCredMode('{{ prefix }}', 'token')" style="width: auto;">
Access Token
</label>
</div>
<div id="{{ prefix }}token_box" style="display: none;">
<div class="form-group" style="margin-top: 0.5rem;">
<label for="{{ prefix }}src_token">Access Token</label>
{{ pw_field(prefix ~ 'src_token', 'Bearer-Token der Quell-Registry') }}
</div>
</div>
{% endmacro %}
{% block content %}
<div class="card">
<h2>Image aus anderer Registry holen</h2>
<form method="post" action="{{ url_for('pull_image') }}"
onsubmit="showBusy('Image wird geholt &ndash; das kann je nach Groesse einige Minuten dauern. Bitte warten&hellip;');">
<div class="form-row">
<div class="form-group">
<label for="source">Quell-Image</label>
<input type="text" id="source" name="source"
placeholder="z. B. nginx:latest oder bitnami/redis:7" required>
</div>
<button type="submit" class="btn btn-primary">Holen</button>
</div>
{{ cred_fields('') }}
</form>
<div style="margin-top: 1.25rem; font-size: 0.9rem; color: #6b7280; line-height: 1.7;">
<p class="help-step"><strong>So funktioniert es</strong></p>
<p class="help-step">
Trage den Namen eines Images aus einer anderen Registry ein (z. B. von Docker Hub)
und klicke auf <strong>Holen</strong>. Das Image wird dann heruntergeladen und in
diese Registry kopiert (alle Architekturen).
</p>
<p class="help-step">Beispiele fuer das Quell-Image:</p>
<ul style="margin: 0.25rem 0 0.5rem 1.25rem;">
<li><code>nginx:latest</code> &ndash; offizielles Image von Docker Hub</li>
<li><code>bitnami/redis:7</code> &ndash; Image eines Docker-Hub-Benutzers</li>
<li><code>ghcr.io/owner/app:v1</code> &ndash; Image aus einer anderen Registry (z. B. GitHub)</li>
</ul>
<p class="help-step">
<strong>Public Images</strong> (der Normalfall, z. B. von Docker Hub) brauchen
keine Zugangsdaten &ndash; einfach nur den Namen eingeben und Holen.
</p>
<p class="help-step">
Nur bei <strong>privaten</strong> Quell-Images Zugangsdaten angeben &ndash; je nachdem,
was du hast:
</p>
<ul style="margin: 0.25rem 0 0.5rem 1.25rem;">
<li><strong>Benutzername / Kennwort</strong> &ndash; fuer Docker Hub und die meisten
Registries. Bei Docker Hub hier das <em>Access Token als Kennwort</em> eintragen.</li>
<li><strong>Access Token</strong> &ndash; fuer Registries, die einen direkten
Bearer-Token akzeptieren.</li>
</ul>
<p class="help-step">
Ist ein Image mit dem gleichen Digest bereits vorhanden, wird es automatisch
uebersprungen (kein Doppel-Upload &ndash; auch bei <code>latest</code>).
</p>
</div>
</div>
<div class="card">
<h2>Mehrere Images aus docker-compose holen</h2>
<form method="post" action="{{ url_for('pull_compose') }}" enctype="multipart/form-data"
onsubmit="showBusy('Images werden geholt &ndash; bei mehreren oder grossen Images kann das einige Minuten dauern. Bitte warten&hellip;');">
<div class="form-group">
<label for="compose">docker-compose einfuegen</label>
<textarea id="compose" name="compose" rows="10"
placeholder="services:&#10; web:&#10; image: nginx:latest&#10; cache:&#10; image: redis:7"></textarea>
</div>
<div class="form-group" style="margin-top: 0.75rem;">
<label for="compose_file">&hellip; oder eine Datei hochladen</label>
<input type="file" id="compose_file" name="compose_file"
accept=".yml,.yaml,.txt,text/yaml,text/plain">
</div>
{{ cred_fields('bulk_') }}
<div style="margin-top: 0.9rem;">
<button type="submit" class="btn btn-primary">Alle Images holen</button>
</div>
</form>
<div style="margin-top: 1.25rem; font-size: 0.9rem; color: #6b7280; line-height: 1.7;">
<p class="help-step"><strong>So funktioniert es</strong></p>
<p class="help-step">
Fuege den Inhalt einer <code>docker-compose.yml</code> ein oder lade die Datei hoch
und klicke auf <strong>Alle Images holen</strong>. Aus jedem Service wird der
<code>image:</code>-Eintrag gelesen und das Image in diese Registry kopiert.
</p>
<p class="help-step">
Variablen mit Standardwert (z. B. <code>${GRAYLOG_IMAGE:-graylog/graylog:7.1}</code>)
werden automatisch mit ihrem Standardwert aufgeloest. Eintraege mit Variablen
<em>ohne</em> Standardwert (z. B. <code>${TAG}</code>) oder die bereits auf diese
Registry zeigen, werden uebersprungen. Bereits vorhandene Images (gleicher Digest)
werden ebenfalls nicht erneut geladen.
</p>
<p class="help-step">
Etwaige Zugangsdaten gelten fuer <em>alle</em> Images der Compose-Datei.
</p>
</div>
</div>
<div class="card">
<h2>Gespeicherte Images</h2>
{% if repos %}
<table>
<thead>
<tr>
<th>Image</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
{% for repo in repos %}
<tr>
<td><strong>{{ request.host }}/{{ repo.name }}</strong></td>
<td>
{% if repo.tags %}
{% for tag in repo.tags %}
<span class="tag tag-deletable">
{{ tag }}
<form method="post" action="{{ url_for('delete_image') }}" class="tag-delete-form"
onsubmit="return confirm('Image {{ repo.name }}:{{ tag }} wirklich loeschen?');">
<input type="hidden" name="name" value="{{ repo.name }}">
<input type="hidden" name="tag" value="{{ tag }}">
<button type="submit" title="Image loeschen" aria-label="Image loeschen">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
</form>
</span>
{% endfor %}
{% else %}
<span style="color: #9ca3af;">keine Tags</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="empty">Noch keine Images in der Registry vorhanden.</p>
{% endif %}
<div style="margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;">
<form method="post" action="{{ url_for('garbage_collect') }}"
onsubmit="if(!confirm('Garbage Collection jetzt ausfuehren? Loesche waehrenddessen keine Images und pushe nichts.')) return false; showBusy('Speicher wird aufgeraeumt. Bitte warten&hellip;');">
<button type="submit" class="btn btn-secondary">Speicher aufraeumen (Garbage Collection)</button>
</form>
<p style="margin-top: 0.6rem; font-size: 0.85rem; color: #6b7280; line-height: 1.6;">
Beim Loeschen eines Images verschwindet der Tag sofort, der belegte Speicherplatz wird
aber erst hierdurch freigegeben. Entfernt alle Daten, die von keinem Image mehr
referenziert werden, sowie leere Repositories ohne Tags.
<strong>Wichtig:</strong> waehrend der Aufraeumung keine Images hochladen, sonst
koennen frisch hochgeladene Daten verloren gehen.
</p>
</div>
</div>
<div id="busy-overlay"
style="display: none; position: fixed; inset: 0; background: rgba(15,23,42,0.6);
z-index: 100; align-items: center; justify-content: center;">
<div style="background: #fff; padding: 1.5rem 2rem; border-radius: 8px; max-width: 420px;
text-align: center; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
<div class="spinner" style="width: 32px; height: 32px; margin: 0 auto 1rem;
border: 3px solid #e5e7eb; border-top-color: #2563eb; border-radius: 50%;
animation: spin 0.8s linear infinite;"></div>
<p id="busy-text" style="font-size: 0.95rem; color: #374151; line-height: 1.5;">Bitte warten&hellip;</p>
</div>
</div>
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
<script>
// Vollbild-Hinweis fuer langlaufende Aktionen (Pull / Garbage Collection).
function showBusy(htmlText) {
document.getElementById('busy-text').innerHTML = htmlText;
document.getElementById('busy-overlay').style.display = 'flex';
return true;
}
// Auge: Eingabe ein-/ausblenden.
function togglePw(id, btn) {
var inp = document.getElementById(id);
var show = inp.type === 'password';
inp.type = show ? 'text' : 'password';
btn.querySelector('.eye-on').style.display = show ? 'none' : '';
btn.querySelector('.eye-off').style.display = show ? '' : 'none';
}
// Benutzername/Kennwort und Access Token schliessen sich gegenseitig aus.
function toggleCredMode(prefix, mode) {
var creds = document.getElementById(prefix + 'use_creds');
var token = document.getElementById(prefix + 'use_token');
if (mode === 'creds' && creds.checked) { token.checked = false; }
if (mode === 'token' && token.checked) { creds.checked = false; }
document.getElementById(prefix + 'creds_box').style.display = creds.checked ? 'block' : 'none';
document.getElementById(prefix + 'token_box').style.display = token.checked ? 'block' : 'none';
}
</script>
{% endblock %}