Files
flowdeck/app/templates/team_load.html
T
bruno c718fe06de
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
fix: A20 (partiel) — CSP nonce par requête, unsafe-inline sort de script-src (v7.7.0)
- 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
2026-10-01 10:41:39 -04:00

51 lines
2.1 KiB
HTML

{% from '_icons.html' import fd_icon %}
<!-- Team Load — Notion-style stacked bar chart -->
<div id="view-content" x-data="teamLoad()" class="team-load">
{% for member in team_data %}
<div class="team-member">
<div class="team-count">{{ member.total }}</div>
<div class="team-bar-stack" style="height: {{ member.total * 32 }}px; min-height: 40px;">
{% if member.complete > 0 %}
<div class="team-bar-segment done"
style="height: {{ (member.complete / member.total * 100)|round }}%;"
title="Complete: {{ member.complete }}"></div>
{% endif %}
{% if member.progress > 0 %}
<div class="team-bar-segment progress"
style="height: {{ (member.progress / member.total * 100)|round }}%;"
title="In progress: {{ member.progress }}"></div>
{% endif %}
{% if member.todo > 0 %}
<div class="team-bar-segment todo"
style="height: {{ (member.todo / member.total * 100)|round }}%;"
title="To-do: {{ member.todo }}"></div>
{% endif %}
</div>
<div class="team-avatar" title="{{ member.name }}">{{ member.initial }}</div>
<div style="font-size:12px; color:var(--text-secondary); text-align:center; max-width:60px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
{{ member.name }}
</div>
</div>
{% endfor %}
{% if not team_data %}
<div style="text-align:center; padding:48px; width:100%; color:var(--text-dim);">
<div style="margin-bottom:16px;">{{ fd_icon("users",48) }}</div>
<h3>No team data</h3>
<p style="margin-top:8px;">Assign issues to team members to see their workload.</p>
</div>
{% endif %}
</div>
<div class="status-legend" style="justify-content:center; padding: 0 24px 24px;">
<div class="legend-item"><span class="legend-dot" style="background:var(--blue);"></span> To-do</div>
<div class="legend-item"><span class="legend-dot" style="background:var(--orange);"></span> In progress</div>
<div class="legend-item"><span class="legend-dot" style="background:var(--green);"></span> Complete</div>
</div>
<script nonce="{{ csp_nonce() }}">
function teamLoad() {
return {};
}
</script>