feat: Scorecard-Werkstatt — Live-Editor unter /v2/scorecard-werkstatt
User-Wunsch: 'Baue eine Entwicklungsseite, wo wir all das in CSS code zusammenschreiben und länger daran arbeiten können ohne jedes mal png erzeugen zu müssen. Können wir hinterher auch nutzen, um irgendwo mal schnell eine Übersicht einzublenden.' Neue Route /v2/scorecard-werkstatt mit Split-Layout: - Links: Live-iframe-Vorschau der /v2/scorecard, mit Zoom-Toolbar (Fit / 40 / 50 / 65 / 80 / 100 %). - Rechts: Drucksachen-Selector (Top-60 Anträge), Format-Pills (Portrait / Square / OG), CSS-Editor-Textarea + Apply-Button. - Apply schreibt das User-CSS als <style>-Element in den iframe → keine Server-Roundtrips, kein PNG-Render, instantane Iteration. - Strg/⌘+Enter im Editor wendet sofort an. Tab fuegt 2 Spaces ein. - Direkt-Link + Iframe-Snippet werden generiert — die Card laesst sich also direkt embedden (z.B. Übersicht in einer anderen App). Plus: Cache-Buster `&_=Date.now()` am Scorecard-Button im v3-Detail, damit die Vorschau-Anzeige nach Layout-Aenderungen nicht weiter eine gecachete Version zeigt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4734e89522
commit
8ae2b92313
25
app/main.py
25
app/main.py
@ -3473,6 +3473,31 @@ async def og_template(request: Request, drucksache: str = ""):
|
||||
})
|
||||
|
||||
|
||||
@app.get("/v2/scorecard-werkstatt", response_class=HTMLResponse)
|
||||
async def scorecard_werkstatt(request: Request):
|
||||
"""Live-Sandbox für Scorecard-Design-Iteration.
|
||||
|
||||
Zeigt eine Vorschau-iframe der /v2/scorecard und einen CSS-Live-Editor.
|
||||
Kein DB-Lookup nötig — der Editor passt nur Styles an, der Inhalt kommt
|
||||
von der gewählten Drucksache. Plus Embed-Link-Generator für die
|
||||
fertige Ansicht in anderen Kontexten.
|
||||
"""
|
||||
rows = await get_all_assessments(None)
|
||||
drucksachen = []
|
||||
for r in rows[:60]:
|
||||
drucksachen.append({
|
||||
"drucksache": r.get("drucksache", ""),
|
||||
"bundesland": r.get("bundesland", ""),
|
||||
"title": (r.get("title") or "")[:80],
|
||||
})
|
||||
response = templates.TemplateResponse("v2/screens/scorecard_werkstatt.html", {
|
||||
"request": request,
|
||||
"drucksachen": drucksachen,
|
||||
})
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
@app.get("/v2/scorecard")
|
||||
async def scorecard_template(
|
||||
request: Request, drucksache: str, bundesland: str = "NRW",
|
||||
|
||||
@ -934,13 +934,13 @@ window.v2ShowMatrixFieldInfo = function(field) {
|
||||
window.location.href = 'mailto:?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
|
||||
};
|
||||
|
||||
/* Scorecard-Preview — Default ist portrait (1080×1350, Instagram 4:5).
|
||||
Format-Param bewusst NICHT gesetzt, damit sich der Server-Default
|
||||
durchsetzt — sonst muss man hier mitziehen wenn der Default sich
|
||||
wieder aendert. */
|
||||
/* Scorecard-Preview — Default portrait (1080×1350). Cache-Buster
|
||||
erzwingt frischen Server-Render, sonst zeigt der Browser eine
|
||||
alte gecachete HTML-Variante. */
|
||||
window.v2DetailShareScorecard = function() {
|
||||
var url = '/v2/scorecard?drucksache=' + encodeURIComponent(DRS)
|
||||
+ '&bundesland=' + encodeURIComponent(BL || 'NRW');
|
||||
+ '&bundesland=' + encodeURIComponent(BL || 'NRW')
|
||||
+ '&_=' + Date.now();
|
||||
window.open(url, '_blank', 'noopener');
|
||||
};
|
||||
|
||||
|
||||
396
app/templates/v2/screens/scorecard_werkstatt.html
Normal file
396
app/templates/v2/screens/scorecard_werkstatt.html
Normal file
@ -0,0 +1,396 @@
|
||||
{% extends "v2/base.html" %}
|
||||
|
||||
{% block title %}Scorecard-Werkstatt — GWÖ-Antragsprüfer{% endblock %}
|
||||
|
||||
{% set v2_active_nav = "" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
.ws-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 480px;
|
||||
gap: 18px;
|
||||
height: calc(100vh - 100px);
|
||||
min-height: 700px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.ws-shell { grid-template-columns: 1fr; height: auto; }
|
||||
}
|
||||
.ws-preview {
|
||||
background: var(--ecg-bg-subtle);
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 4px;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.ws-preview iframe {
|
||||
border: 1px solid var(--ecg-border);
|
||||
background: #fff;
|
||||
/* iframe-Skalierung: 1080×1350 → ein bisschen verkleinert anzeigen,
|
||||
Skalierung pro Format konfigurierbar via JS */
|
||||
transform-origin: top left;
|
||||
background: #fff;
|
||||
}
|
||||
.ws-frame-wrap {
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ws-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
}
|
||||
.ws-section {
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px 14px;
|
||||
background: var(--ecg-card-bg);
|
||||
}
|
||||
.ws-section h3 {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--ecg-blue);
|
||||
margin: 0 0 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ws-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ws-row label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--ecg-dark);
|
||||
opacity: 0.75;
|
||||
min-width: 90px;
|
||||
}
|
||||
.ws-row select,
|
||||
.ws-row input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
background: var(--paper);
|
||||
color: var(--ecg-dark);
|
||||
}
|
||||
.ws-format-pills {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ws-format-pills button {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 3px;
|
||||
background: var(--paper);
|
||||
color: var(--ecg-dark);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ws-format-pills button.active {
|
||||
background: var(--ecg-blue);
|
||||
color: #fff;
|
||||
border-color: var(--ecg-blue);
|
||||
}
|
||||
textarea.ws-css-editor {
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
background: #1f1f1f;
|
||||
color: #d8e4d8;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
tab-size: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ws-css-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ws-css-actions button {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
background: var(--paper);
|
||||
color: var(--ecg-dark);
|
||||
}
|
||||
.ws-css-actions button.primary {
|
||||
background: var(--ecg-blue);
|
||||
border-color: var(--ecg-blue);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ws-meta {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--ecg-dark);
|
||||
opacity: 0.65;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ws-embed-row code {
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
background: var(--ecg-bg-subtle);
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
}
|
||||
.ws-toolbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.ws-toolbar button {
|
||||
padding: 3px 9px;
|
||||
border: 1px solid var(--ecg-border);
|
||||
border-radius: 3px;
|
||||
background: var(--paper);
|
||||
color: var(--ecg-dark);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ws-toolbar span.zoom-info {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--ecg-dark);
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div style="padding:0 0 1rem;">
|
||||
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">Scorecard-Werkstatt</h1>
|
||||
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.7;">
|
||||
Live-Editor für Scorecard-Layout. Drucksache + Format wählen, CSS im rechten Editor anpassen,
|
||||
Apply drücken — Vorschau aktualisiert ohne neue PNG-Render. Auch als Embed-Quelle für
|
||||
eingebettete Übersichten in anderen Kontexten nutzbar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ws-shell">
|
||||
|
||||
{# ── Vorschau links ──────────────────────────────────────────────── #}
|
||||
<div class="ws-preview">
|
||||
<div class="ws-toolbar">
|
||||
<button onclick="wsZoomFit()">Fit</button>
|
||||
<button onclick="wsZoom(0.4)">40 %</button>
|
||||
<button onclick="wsZoom(0.5)">50 %</button>
|
||||
<button onclick="wsZoom(0.65)">65 %</button>
|
||||
<button onclick="wsZoom(0.8)">80 %</button>
|
||||
<button onclick="wsZoom(1.0)">100 %</button>
|
||||
<span class="zoom-info" id="ws-zoom-info">—</span>
|
||||
</div>
|
||||
<div class="ws-frame-wrap" id="ws-frame-wrap">
|
||||
<iframe id="ws-frame" src="about:blank" title="Scorecard-Vorschau"></iframe>
|
||||
</div>
|
||||
<div class="ws-meta" id="ws-meta">Drucksache + Format wählen.</div>
|
||||
</div>
|
||||
|
||||
{# ── Controls + CSS-Editor rechts ────────────────────────────────── #}
|
||||
<div class="ws-controls">
|
||||
|
||||
<div class="ws-section">
|
||||
<h3>Inhalt</h3>
|
||||
<div class="ws-row">
|
||||
<label>Drucksache</label>
|
||||
<select id="ws-drs" onchange="wsRender()">
|
||||
{% for d in drucksachen %}
|
||||
<option value="{{ d.drucksache }}|{{ d.bundesland }}">
|
||||
{{ d.bundesland }} · {{ d.drucksache }} — {{ d.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="ws-row">
|
||||
<label>Format</label>
|
||||
<div class="ws-format-pills">
|
||||
<button data-format="portrait" class="active" onclick="wsSetFormat('portrait')">Portrait 4:5 (1080×1350)</button>
|
||||
<button data-format="square" onclick="wsSetFormat('square')">Square 1:1 (1080×1080)</button>
|
||||
<button data-format="og" onclick="wsSetFormat('og')">OG 16:8 (1200×630)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ws-section" style="display:flex;flex-direction:column;flex:1;min-height:280px;">
|
||||
<h3>CSS-Override (live)</h3>
|
||||
<textarea class="ws-css-editor" id="ws-css" placeholder="/* CSS-Selektoren in den iframe-Kontext.
|
||||
Beispiel:
|
||||
|
||||
.portrait-title { font-size: 28pt; }
|
||||
.portrait-score-num { font-size: 150pt; }
|
||||
.cell.r-0 { background: #c8c8c2; }
|
||||
*/"></textarea>
|
||||
<div class="ws-css-actions">
|
||||
<button class="primary" onclick="wsApplyCss()">Anwenden ⏎</button>
|
||||
<button onclick="wsResetCss()">Zurücksetzen</button>
|
||||
<button onclick="wsCopyCss()">CSS kopieren</button>
|
||||
</div>
|
||||
<div class="ws-meta" style="margin-top:6px;">
|
||||
Tipp: Strg/⌘+Enter wendet das CSS sofort an. Reset entfernt nur den Override —
|
||||
die Server-CSS bleibt.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ws-section ws-embed-row">
|
||||
<h3>Direkt-Link / Embed</h3>
|
||||
<code id="ws-embed-url">—</code>
|
||||
<div class="ws-meta" style="margin-top:6px;">
|
||||
Iframe-Snippet:
|
||||
</div>
|
||||
<code id="ws-embed-iframe" style="margin-top:4px;">—</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block body_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
var drsSel = document.getElementById('ws-drs');
|
||||
var iframe = document.getElementById('ws-frame');
|
||||
var frameWrap = document.getElementById('ws-frame-wrap');
|
||||
var cssEditor = document.getElementById('ws-css');
|
||||
var meta = document.getElementById('ws-meta');
|
||||
var embedUrl = document.getElementById('ws-embed-url');
|
||||
var embedIframe = document.getElementById('ws-embed-iframe');
|
||||
var zoomInfo = document.getElementById('ws-zoom-info');
|
||||
|
||||
var currentFormat = 'portrait';
|
||||
var formatDims = {
|
||||
portrait: [1080, 1350],
|
||||
square: [1080, 1080],
|
||||
og: [1200, 630],
|
||||
};
|
||||
|
||||
function getCurrent() {
|
||||
var v = drsSel.value || '';
|
||||
var parts = v.split('|');
|
||||
return { drs: parts[0] || '', bl: parts[1] || 'NRW' };
|
||||
}
|
||||
|
||||
window.wsSetFormat = function (f) {
|
||||
currentFormat = f;
|
||||
document.querySelectorAll('.ws-format-pills button').forEach(function (b) {
|
||||
b.classList.toggle('active', b.dataset.format === f);
|
||||
});
|
||||
wsRender();
|
||||
};
|
||||
|
||||
window.wsRender = function () {
|
||||
var c = getCurrent();
|
||||
if (!c.drs) return;
|
||||
var src = '/v2/scorecard'
|
||||
+ '?drucksache=' + encodeURIComponent(c.drs)
|
||||
+ '&bundesland=' + encodeURIComponent(c.bl)
|
||||
+ '&format=' + currentFormat
|
||||
+ '&_=' + Date.now();
|
||||
iframe.src = src;
|
||||
var dims = formatDims[currentFormat];
|
||||
iframe.width = dims[0];
|
||||
iframe.height = dims[1];
|
||||
iframe.style.width = dims[0] + 'px';
|
||||
iframe.style.height = dims[1] + 'px';
|
||||
meta.textContent = c.bl + ' · ' + c.drs + ' · ' + currentFormat
|
||||
+ ' (' + dims[0] + '×' + dims[1] + ')';
|
||||
var publicUrl = window.location.origin + '/v2/scorecard'
|
||||
+ '?drucksache=' + encodeURIComponent(c.drs)
|
||||
+ '&bundesland=' + encodeURIComponent(c.bl)
|
||||
+ '&format=' + currentFormat;
|
||||
embedUrl.textContent = publicUrl;
|
||||
embedIframe.textContent = '<iframe src="' + publicUrl + '" '
|
||||
+ 'width="' + dims[0] + '" height="' + dims[1] + '" '
|
||||
+ 'frameborder="0"></iframe>';
|
||||
setTimeout(wsZoomFit, 100);
|
||||
iframe.addEventListener('load', wsApplyCss, { once: true });
|
||||
};
|
||||
|
||||
window.wsApplyCss = function () {
|
||||
var doc = iframe.contentDocument;
|
||||
if (!doc) return;
|
||||
var styleId = 'ws-override-style';
|
||||
var existing = doc.getElementById(styleId);
|
||||
if (existing) existing.remove();
|
||||
var style = doc.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = cssEditor.value;
|
||||
doc.head.appendChild(style);
|
||||
};
|
||||
|
||||
window.wsResetCss = function () {
|
||||
cssEditor.value = '';
|
||||
wsApplyCss();
|
||||
};
|
||||
|
||||
window.wsCopyCss = function () {
|
||||
if (navigator.clipboard && cssEditor.value) {
|
||||
navigator.clipboard.writeText(cssEditor.value);
|
||||
}
|
||||
};
|
||||
|
||||
window.wsZoom = function (factor) {
|
||||
iframe.style.transform = 'scale(' + factor + ')';
|
||||
var dims = formatDims[currentFormat];
|
||||
frameWrap.style.width = (dims[0] * factor) + 'px';
|
||||
frameWrap.style.height = (dims[1] * factor) + 'px';
|
||||
zoomInfo.textContent = Math.round(factor * 100) + '%';
|
||||
};
|
||||
|
||||
window.wsZoomFit = function () {
|
||||
var avail = document.querySelector('.ws-preview').clientWidth - 30;
|
||||
var dims = formatDims[currentFormat];
|
||||
var factor = Math.min(1.0, avail / dims[0]);
|
||||
factor = Math.round(factor * 100) / 100;
|
||||
wsZoom(factor);
|
||||
};
|
||||
|
||||
cssEditor.addEventListener('keydown', function (e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
wsApplyCss();
|
||||
}
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
var s = this.selectionStart;
|
||||
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
|
||||
this.selectionStart = this.selectionEnd = s + 2;
|
||||
}
|
||||
});
|
||||
|
||||
wsRender();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user