fix: Bild-Upload pro Artikel-Zeile — Datei wählen ohne Dateinamen tippen

- Jede Artikel-Zeile hat jetzt einen 📷 Upload-Button (row-upload-btn)
- Klick → native File-Picker → Auto-Upload mit dem korrekten Artikel-Dateinamen
- uploadForRow() JS-Funktion: async fetch + Badge-Update + Toast-Feedback
- Bugfix: f.write(file_data) statt f.write(data) im _handle_upload
- Upload-Zone oben bleibt für Bulk/Drag-Drop erhalten
This commit is contained in:
Hermes Agent
2026-06-18 23:59:52 +02:00
parent ebcd338bd4
commit 726f25ff83
2 changed files with 1200 additions and 2 deletions
+54 -2
View File
@@ -225,7 +225,7 @@ def build_index(articles: list[dict]) -> str:
ready_dot(image_ok, "Bild")
)
items_html += f"""<div class="article-row" data-ready="{str(all_ready).lower()}">
items_html += f"""<div class="article-row" data-ready="{str(all_ready).lower()}" data-filename="{a['filename']}">
<label class="checkbox-label">
<input type="checkbox" class="article-check" value="{a['filename']}">
<span class="checkmark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
@@ -237,6 +237,10 @@ def build_index(articles: list[dict]) -> str:
</div>
<span class="readiness">{readiness}</span>
{image_badge}
<label class="row-upload-btn" title="Bild für diesen Artikel hochladen">
<input type="file" accept="image/*" class="row-file-input" onchange="uploadForRow(this, '{a['filename']}')" hidden>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</label>
</div>\n"""
items_html += "</div>\n"
@@ -334,6 +338,14 @@ def build_index(articles: list[dict]) -> str:
.upload-result {{ margin-top: 0.8rem; font-size: 0.82rem; font-family: monospace; word-break: break-all; padding: 0.5rem; background: #f0fdf4; border-radius: 6px; display: none; }}
.upload-result.show {{ display: block; }}
.row-upload-btn {{ display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; color: var(--accent); background: var(--accent-ghost); border: 1px solid transparent; transition: all 0.15s; flex-shrink: 0; margin-left: 0.25rem; }}
.row-upload-btn:hover {{ background: var(--accent); color: #fff; border-color: var(--accent); }}
.row-upload-btn:active {{ transform: scale(0.93); }}
.row-upload-btn.uploading {{ background: var(--success); color: #fff; pointer-events: none; animation: pulse 0.8s infinite; }}
@keyframes pulse {{ 0%,100%{{opacity:1}} 50%{{opacity:0.5}} }}
.row-file-input {{ display: none; }}
.toast {{ position: fixed; bottom: 2rem; right: 2rem; background: var(--success); color: #fff; padding: 0.8rem 1.6rem; border-radius: 10px; font-weight: 500; font-family: var(--font); box-shadow: 0 6px 24px rgba(16,185,129,0.3); opacity: 0; transform: translateY(16px); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); pointer-events: none; z-index: 1000; }}
.toast.show {{ opacity: 1; transform: translateY(0); }}
.toast.error {{ background: var(--danger); box-shadow: 0 6px 24px rgba(192,57,43,0.3); }}
@@ -468,6 +480,46 @@ def build_index(articles: list[dict]) -> str:
}});
}}
async function uploadForRow(input, articleFile) {{
const file = input.files[0];
if (!file) return;
const btn = input.closest('.row-upload-btn');
if (btn) btn.classList.add('uploading');
const formData = new FormData();
formData.append('file', file);
formData.append('article', articleFile);
try {{
const resp = await fetch('/upload?key={ACCESS_TOKEN}', {{ method: 'POST', body: formData }});
const data = await resp.json();
if (btn) btn.classList.remove('uploading');
if (data.ok) {{
showToast('' + file.name + '' + articleFile.split('_2026')[0].replace(/_/g,' ').replace('artikel ',''), false);
// Update image badge (bump count visually)
const row = input.closest('.article-row');
if (row) {{
const badge = row.querySelector('.img-badge');
if (badge) {{
const match = badge.textContent.match(/\\d+/);
const current = match ? parseInt(match[0]) : 0;
badge.textContent = '🖼' + (current + 1);
}} else {{
const info = row.querySelector('.article-info');
if (info) info.insertAdjacentHTML('afterend', '<span class=\"img-badge\">🖼1</span>');
}}
}}
// Clear the file input so the same file can be re-uploaded
input.value = '';
}} else {{
showToast('' + (data.error || 'Fehler'), true);
}}
}} catch(e) {{
if (btn) btn.classList.remove('uploading');
showToast('' + e.message, true);
}}
}}
const uploadZone = document.getElementById('uploadZone');
uploadZone.addEventListener('dragover', e => {{ e.preventDefault(); uploadZone.classList.add('dragover'); }});
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
@@ -797,7 +849,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
filepath = os.path.join(month_dir, unique_name)
with open(filepath, "wb") as f:
f.write(data)
f.write(file_data)
# ── Article-Image Association ──────────────────────────────────────
association_msg = ""