- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
`script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
(chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
: htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
: vues chart/map déjà BLOQUÉES par la CSP depuis toujours
(commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
JS (A27), resserrer img-src/connect-src
test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)
suite **1037/1037** · `ruff check app tests` OK · docs à jour
61 lines
2.7 KiB
HTML
61 lines
2.7 KiB
HTML
<!-- Kanban Board Fragment — loaded via HTMX -->
|
|
<div id="view-content" class="kanban-container" x-data="kanbanBoard()">
|
|
{% set statuses = [('todo', 'To-do', 'var(--gray)', 0), ('progress', 'In progress', 'var(--blue)', 1), ('done', 'Complete', 'var(--green)', 2)] %}
|
|
{% for status, label, color, _ in statuses %}
|
|
<div class="kanban-column">
|
|
<div class="kanban-column-header">
|
|
<span class="col-dot" style="background: {{ color }};"></span>
|
|
{{ label }}
|
|
<span class="col-count">{{ groups|map(attribute='counts')|map(attribute=status)|sum }}</span>
|
|
</div>
|
|
{% for group in groups %}
|
|
<div class="kanban-group-header" @click="toggleGroup('{{ group.id }}')">
|
|
<span class="group-chevron" :class="{ 'open': !collapsedGroups.includes('{{ group.id }}') }">▶</span>
|
|
{{ group.name }}
|
|
<span class="group-count">{{ group.counts[status] }}</span>
|
|
</div>
|
|
<div class="kanban-cards" x-show="!collapsedGroups.includes('{{ group.id }}')"
|
|
data-group="{{ group.id }}" data-status="{{ status }}"
|
|
id="kanban-{{ group.id }}-{{ status }}">
|
|
{% for card in group.cards if card.status == status %}
|
|
<div class="kanban-card card-status-{{ card.status }}"
|
|
data-card-id="{{ card.id }}"
|
|
@click="openCardDetail('{{ card.id }}')">
|
|
<div class="card-header">
|
|
<span class="card-icon">{{ card.icon }}</span>
|
|
<span class="card-title">{{ card.title }}</span>
|
|
</div>
|
|
<div class="card-meta">
|
|
{% if card.assignee %}
|
|
<div class="card-assignee">
|
|
<div class="card-avatar" title="{{ card.assignee }}">{{ card.assignee[0] }}</div>
|
|
<span>{{ card.assignee }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if card.tag %}
|
|
<span class="card-tag" style="background: {{ card.tag_color }}20; color: {{ card.tag_color }};">{{ card.tag }}</span>
|
|
{% endif %}
|
|
{% if card.due_date %}
|
|
<div class="card-date"><span>{{ fd_icon("calendar",14) }}</span> {{ card.due_date }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
<button class="kanban-new-card" @click="newCard('{{ group.id }}', '{{ status }}')">+ New page</button>
|
|
</div>
|
|
{% endfor %}
|
|
<button class="kanban-new-group" @click="newGroup()" style="margin-top: 12px;">+ New group</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<script nonce="{{ csp_nonce() }}">
|
|
function openCardDetail(id) {
|
|
htmx.ajax('GET', `/api/issues/${owner}/${repo}/${id}?format=html`, {
|
|
target: '#card-modal-content', swap: 'innerHTML'
|
|
});
|
|
document.getElementById('card-modal').style.display = 'flex';
|
|
document.getElementById('card-modal-title').textContent = 'Issue #' + id;
|
|
}
|
|
</script>
|