Files
ShooterHub/templates/sessions/detail.html
2026-03-19 16:42:37 +01:00

610 lines
27 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ session.label }} — The Shooter's Network{% endblock %}
{% block content %}
<div style="display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
<div>
<div style="font-size:0.82rem;color:#888;margin-bottom:.2rem;">
{% if is_owner %}<a href="{{ url_for('sessions.index') }}">Sessions</a> &rsaquo; {% endif %}
{{ session.session_date.strftime('%d %b %Y') }}
{% if session.is_public %}
<span style="background:#e8f5e9;color:#27ae60;font-size:0.75rem;padding:.1rem .45rem;border-radius:3px;margin-left:.4rem;">{{ _('Public') }}</span>
{% endif %}
</div>
<h1 style="margin:0;">{{ session.label }}</h1>
<div style="font-size:0.88rem;color:#666;margin-top:.4rem;">
by {{ session.user.display_name or session.user.email.split('@')[0] }}
</div>
</div>
{% if is_owner %}
<div style="display:flex;gap:.75rem;">
<a href="{{ url_for('sessions.edit', session_id=session.id) }}"
style="background:#f0f4ff;color:#1a1a2e;padding:0.5rem 1.1rem;border-radius:4px;font-size:0.9rem;text-decoration:none;">
{{ _('Edit') }}
</a>
<form method="post" action="{{ url_for('sessions.delete', session_id=session.id) }}"
onsubmit="return confirm('{{ _('Delete this session? This cannot be undone.') | e }}');">
<button type="submit"
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;padding:0.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
{{ _('Delete') }}
</button>
</form>
</div>
{% endif %}
</div>
{# ---- Stats cards ---- #}
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;margin-bottom:2rem;">
{% if session.location_name or session.distance_m %}
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Location') }}</div>
{% if session.location_name %}<div style="font-weight:600;">{{ session.location_name }}</div>{% endif %}
{% if session.distance_m %}<div style="color:#555;font-size:0.9rem;">{{ session.distance_m }} m</div>{% endif %}
</div>
{% endif %}
{% if session.weather_cond or session.weather_temp_c is not none or session.weather_wind_kph is not none %}
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Weather') }}</div>
{% if session.weather_cond %}<div style="font-weight:600;">{{ session.weather_cond.replace('_',' ').title() }}</div>{% endif %}
<div style="color:#555;font-size:0.9rem;">
{% if session.weather_temp_c is not none %}{{ session.weather_temp_c }}°C{% endif %}
{% if session.weather_wind_kph is not none %}&nbsp; {{ session.weather_wind_kph }} {{ _('km/h wind') }}{% endif %}
</div>
</div>
{% endif %}
{% if session.rifle %}
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Rifle / Handgun') }}</div>
<div style="font-weight:600;">{{ session.rifle.name }}</div>
{% if session.rifle.caliber %}<div style="color:#555;font-size:0.9rem;">{{ session.rifle.caliber }}</div>{% endif %}
</div>
{% endif %}
{% if session.scope %}
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Scope') }}</div>
<div style="font-weight:600;">{{ session.scope.name }}</div>
</div>
{% endif %}
{% if session.ammo_brand or session.ammo_weight_gr is not none %}
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Ammunition') }}</div>
{% if session.ammo_brand %}<div style="font-weight:600;">{{ session.ammo_brand }}</div>{% endif %}
<div style="color:#555;font-size:0.9rem;">
{% if session.ammo_weight_gr is not none %}{{ session.ammo_weight_gr }} gr{% endif %}
{% if session.ammo_lot %}&nbsp; lot {{ session.ammo_lot }}{% endif %}
</div>
</div>
{% endif %}
{% if session.shooting_position %}
<div style="background:#f8f9fb;border-radius:6px;padding:1rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:.05em;color:#888;margin-bottom:.4rem;">{{ _('Position') }}</div>
<div style="font-weight:600;">{{ session.shooting_position.replace('_',' ').title() }}</div>
</div>
{% endif %}
</div>
{% if session.notes %}
<h2>{{ _('Notes') }}</h2>
<p style="color:#555;white-space:pre-wrap;">{{ session.notes }}</p>
{% endif %}
{# ---- PRS Stages ---- #}
{% if session.session_type == 'prs' %}
<h2>{{ _('PRS Stages') }}</h2>
{% set stages = session.prs_stages or [] %}
{# Stage table (read + edit combined) #}
<div style="overflow-x:auto;margin-bottom:1rem;">
<table id="prs-table" style="min-width:900px;font-size:0.88rem;">
<thead>
<tr>
<th style="width:36px;"></th>
<th>{{ _('Name') }}</th>
<th style="width:80px;">Dist. (m)</th>
<th style="width:75px;">{{ _('Time (s)') }}</th>
<th style="width:110px;">Position</th>
<th style="width:110px;">{{ _('Elevation Dope') }}</th>
<th style="width:110px;">{{ _('Windage Dope') }}</th>
<th style="width:80px;">{{ _('Hits/Poss.') }}</th>
<th>{{ _('Notes') }}</th>
{% if is_owner %}<th style="width:36px;"></th>{% endif %}
</tr>
</thead>
<tbody id="prs-tbody">
{% for st in stages %}
<tr data-idx="{{ loop.index0 }}">
<td style="text-align:center;font-weight:600;">{{ st.num or loop.index }}</td>
<td>{{ st.name or '' }}</td>
<td style="text-align:center;">{{ st.distance_m or '' }}</td>
<td style="text-align:center;">{{ st.time_s or '' }}</td>
<td>{{ st.position or '' }}</td>
<td style="text-align:center;">{{ st.dope_elevation or '' }}</td>
<td style="text-align:center;">{{ st.dope_windage or '' }}</td>
<td style="text-align:center;">
{% if st.hits is not none %}{{ st.hits }}{% endif %}{% if st.possible %}/{{ st.possible }}{% endif %}
</td>
<td>{{ st.notes or '' }}</td>
{% if is_owner %}<td></td>{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if is_owner %}
<div style="display:flex;gap:.75rem;flex-wrap:wrap;margin-bottom:1.5rem;">
<button onclick="enterEditMode()" id="btn-edit-stages"
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
{{ _('✏ Edit stages') }}
</button>
<a href="{{ url_for('sessions.dope_card', session_id=session.id) }}" target="_blank"
style="background:#27ae60;color:#fff;padding:.5rem 1.1rem;border-radius:4px;font-size:0.9rem;text-decoration:none;">
{{ _('📄 Generate dope card (PDF)') }}
</a>
</div>
{# Hidden form used by JS to save stages #}
<form method="post" action="{{ url_for('sessions.save_stages', session_id=session.id) }}" id="stages-form" style="display:none;">
<input type="hidden" name="stages_json" id="stages-json-input">
</form>
<script>
(function () {
var PRS_POS = [
["prone", "Couché"],
["standing", "Debout"],
["kneeling", "Agenouillé"],
["sitting", "Assis"],
["barricade", "Barricade"],
["rooftop", "Toit"],
["unknown", "Variable"],
];
var stages = {{ (session.prs_stages or []) | tojson }};
function posLabel(slug) {
var m = PRS_POS.find(function(p) { return p[0] === slug; });
return m ? m[1] : slug;
}
function posSelect(val) {
var s = document.createElement('select');
s.style.cssText = 'width:100%;font-size:0.85rem;padding:.2rem;border:1px solid #ccc;border-radius:3px;';
PRS_POS.forEach(function(p) {
var o = document.createElement('option');
o.value = p[0]; o.textContent = p[1];
if (p[0] === val) o.selected = true;
s.appendChild(o);
});
return s;
}
function inp(val, type, placeholder) {
var i = document.createElement('input');
i.type = type || 'text';
i.value = val == null ? '' : val;
if (placeholder) i.placeholder = placeholder;
i.style.cssText = 'width:100%;font-size:0.85rem;padding:.2rem .4rem;border:1px solid #ccc;border-radius:3px;box-sizing:border-box;';
return i;
}
function collectStages() {
var rows = document.querySelectorAll('#prs-tbody tr');
var result = [];
rows.forEach(function(tr, i) {
var cells = tr.querySelectorAll('td');
if (!cells.length) return;
function val(idx) {
var el = cells[idx];
if (!el) return '';
var input = el.querySelector('input,select,textarea');
return input ? (input.value || '') : (el.textContent.trim() || '');
}
result.push({
num: parseInt(val(0)) || i + 1,
name: val(1),
distance_m: parseInt(val(2)) || null,
time_s: parseInt(val(3)) || null,
position: val(4),
dope_elevation: val(5),
dope_windage: val(6),
hits: val(7).split('/')[0] !== '' ? parseInt(val(7).split('/')[0]) : null,
possible: val(7).split('/')[1] !== undefined ? (parseInt(val(7).split('/')[1]) || null) : null,
notes: val(8),
});
});
return result;
}
function addRow(st) {
var tbody = document.getElementById('prs-tbody');
var idx = tbody.rows.length;
st = st || { num: idx + 1 };
var tr = document.createElement('tr');
tr.dataset.idx = idx;
var hitsVal = '';
if (st.hits != null) hitsVal = st.hits;
if (st.possible != null) hitsVal = (hitsVal !== '' ? hitsVal : '') + '/' + st.possible;
[
inp(st.num, 'number'),
inp(st.name),
inp(st.distance_m, 'number'),
inp(st.time_s, 'number'),
posSelect(st.position || ''),
inp(st.dope_elevation, 'text', 'ex : 3.5 MOA'),
inp(st.dope_windage, 'text', 'ex : 0.5 MOA D'),
inp(hitsVal, 'text', 'x/y'),
inp(st.notes),
].forEach(function(el) {
var td = document.createElement('td');
td.appendChild(el);
tr.appendChild(td);
});
// Remove button
var tdDel = document.createElement('td');
tdDel.style.textAlign = 'center';
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = '✕';
btn.style.cssText = 'background:none;border:none;color:#c0392b;cursor:pointer;font-size:1rem;padding:0 .3rem;';
btn.onclick = function() { tr.remove(); renumber(); };
tdDel.appendChild(btn);
tr.appendChild(tdDel);
tbody.appendChild(tr);
}
function renumber() {
document.querySelectorAll('#prs-tbody tr').forEach(function(tr, i) {
var first = tr.querySelector('td:first-child input');
if (first && first.type === 'number') first.value = i + 1;
});
}
window.enterEditMode = function () {
// Replace read-only rows with editable rows
var tbody = document.getElementById('prs-tbody');
tbody.innerHTML = '';
stages.forEach(function(st) { addRow(st); });
if (!stages.length) addRow(null);
document.getElementById('btn-edit-stages').style.display = 'none';
var STR_ADD_STAGE = '{{ _("+ Add stage") | e }}';
var STR_SAVE = '{{ _("💾 Save") | e }}';
var STR_CANCEL = '{{ _("Cancel") | e }}';
// Add / Save / Cancel buttons
var bar = document.createElement('div');
bar.id = 'edit-bar';
bar.style.cssText = 'display:flex;gap:.6rem;flex-wrap:wrap;margin:.75rem 0;';
bar.innerHTML =
'<button type="button" onclick="addRow(null);renumber();" ' +
'style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;padding:.4rem .9rem;font-size:0.88rem;cursor:pointer;">' +
STR_ADD_STAGE + '</button>' +
'<button type="button" onclick="saveStages()" ' +
'style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.4rem .9rem;font-size:0.88rem;cursor:pointer;">' +
STR_SAVE + '</button>' +
'<button type="button" onclick="cancelEdit()" ' +
'style="background:none;color:#666;border:none;font-size:0.88rem;cursor:pointer;padding:.4rem .5rem;">' + STR_CANCEL + '</button>';
document.getElementById('prs-table').after(bar);
};
window.saveStages = function () {
var data = collectStages();
document.getElementById('stages-json-input').value = JSON.stringify(data);
document.getElementById('stages-form').submit();
};
window.cancelEdit = function () {
location.reload();
};
window.addRow = addRow;
window.renumber = renumber;
})();
</script>
{% endif %}
{% endif %}{# end prs #}
{# ---- Photos ---- #}
{% if session.photos or is_owner %}
<h2>{{ _('Photos') }}</h2>
{% if session.photos %}
<div style="display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
{% for photo in session.photos %}
<div>
<div style="position:relative;display:inline-block;">
<img src="{{ photo.photo_url }}"
data-gallery="session-{{ session.id }}"
data-src="{{ photo.photo_url }}"
data-caption="{{ photo.caption or '' }}"
alt="{{ photo.caption or '' }}"
style="height:180px;width:auto;border-radius:6px;object-fit:cover;display:block;">
{% if is_owner %}
<form method="post"
action="{{ url_for('sessions.delete_photo', session_id=session.id, photo_id=photo.id) }}"
onsubmit="return confirm('{{ _('Delete this photo?') | e }}');"
style="position:absolute;top:4px;right:4px;">
<button type="submit"
style="background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:3px;padding:.2rem .45rem;font-size:0.8rem;cursor:pointer;line-height:1.2;">
&#x2715;
</button>
</form>
{% endif %}
</div>
{% if photo.annotations and photo.annotations.stats %}
{% set s = photo.annotations.stats %}
{% set a = photo.annotations %}
<div style="margin-top:.5rem;background:#f8f9fb;border:1px solid #e0e0e0;border-radius:6px;padding:.6rem .75rem;font-size:0.8rem;min-width:180px;">
<div style="font-weight:700;color:#1a1a2e;margin-bottom:.35rem;font-size:0.82rem;">
{{ s.shot_count }} shot{{ 's' if s.shot_count != 1 }}
&middot; {{ s.shooting_distance_m | int }} m
{% if a.clean_barrel %}<span style="background:#e8f5e9;color:#27ae60;border-radius:3px;padding:.05rem .3rem;margin-left:.3rem;">{{ _('clean barrel') }}</span>{% endif %}
</div>
<table style="width:100%;border-collapse:collapse;margin:0;">
<tr>
<td style="color:#666;padding:.15rem 0;border:none;">{{ _('Group ES') }}</td>
<td style="font-weight:600;text-align:right;padding:.15rem 0;border:none;">{{ '%.2f'|format(s.group_size_moa) }} MOA</td>
<td style="color:#888;text-align:right;padding:.15rem 0 .15rem .4rem;border:none;">{{ '%.1f'|format(s.group_size_mm) }} mm</td>
</tr>
<tr>
<td style="color:#666;padding:.15rem 0;border:none;">{{ _('Mean Radius') }}</td>
<td style="font-weight:600;text-align:right;padding:.15rem 0;border:none;">{{ '%.2f'|format(s.mean_radius_moa) }} MOA</td>
<td style="color:#888;text-align:right;padding:.15rem 0 .15rem .4rem;border:none;">{{ '%.1f'|format(s.mean_radius_mm) }} mm</td>
</tr>
<tr>
<td style="color:#666;padding:.15rem 0;border:none;">{{ _('Centre') }}</td>
<td colspan="2" style="text-align:right;padding:.15rem 0;border:none;">
{{ '%.2f'|format(s.center_dist_moa) }} MOA
<span style="color:#888;">({{ '%.1f'|format(s.center_x_mm | abs) }} mm {{ 'R' if s.center_x_mm > 0 else 'L' if s.center_x_mm < 0 else '' }}, {{ '%.1f'|format(s.center_y_mm | abs) }} mm {{ _('low') if s.center_y_mm > 0 else _('high') if s.center_y_mm < 0 else '' }})</span>
</td>
</tr>
</table>
</div>
{% endif %}
{% if photo.caption %}
<div style="font-size:0.78rem;color:#666;margin-top:.25rem;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ photo.caption }}
</div>
{% endif %}
{% if is_owner %}
<div style="display:flex;gap:.35rem;margin-top:.35rem;">
{% for label, deg in [('↺', -90), ('↻', 90), ('180°', 180)] %}
<form method="post" action="{{ url_for('sessions.rotate_photo_view', session_id=session.id, photo_id=photo.id) }}">
<input type="hidden" name="degrees" value="{{ deg }}">
<button type="submit"
style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;padding:.2rem .55rem;font-size:0.8rem;cursor:pointer;">
{{ label }}
</button>
</form>
{% endfor %}
</div>
<a href="{{ url_for('sessions.annotate_photo', session_id=session.id, photo_id=photo.id) }}"
style="display:inline-block;margin-top:.4rem;padding:.3rem .75rem;border-radius:4px;font-size:0.82rem;text-decoration:none;
{% if photo.annotations and photo.annotations.stats %}
background:#e8f5e9;color:#27ae60;border:1px solid #a5d6a7;
{% else %}
background:#1a1a2e;color:#fff;border:1px solid #1a1a2e;
{% endif %}">
{% if photo.annotations and photo.annotations.stats %}&#10003;{% else %}&#9654;{% endif %}
{{ _('Measure group') }}
</a>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if is_owner %}
<form method="post"
action="{{ url_for('sessions.upload_photo', session_id=session.id) }}"
enctype="multipart/form-data"
style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;margin-bottom:2rem;">
<div>
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">{{ _('Add photo') }}</label>
<input type="file" name="photo" accept="image/*" required style="font-size:0.9rem;">
</div>
<div>
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">{{ _('Caption (optional)') }}</label>
<input type="text" name="caption" placeholder="e.g. 300 m target"
style="padding:.45rem .7rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;">
</div>
<button type="submit"
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
{{ _('Upload') }}
</button>
</form>
{% endif %}
{% endif %}
{# ---- Analyses ---- #}
<h2>Analyses{% if analyses %} ({{ analyses|length }}){% endif %}</h2>
{% if analyses_display %}
{% for a, groups_display, overview_chart in analyses_display %}
<details open style="border:1px solid #e0e0e0;border-radius:8px;margin-bottom:1.25rem;overflow:hidden;">
<summary style="display:flex;align-items:center;gap:.75rem;padding:.85rem 1.25rem;
background:#f8f9fb;cursor:pointer;list-style:none;flex-wrap:wrap;">
<span style="font-weight:700;font-size:1rem;color:#1a1a2e;flex:1;">{{ a.title }}</span>
<span style="font-size:0.82rem;color:#666;">{{ a.shot_count }} {{ _('shots') }} &middot; {{ a.group_count }} {{ _('group') if a.group_count == 1 else _('groups') }}</span>
<span style="font-size:0.82rem;color:#888;">{{ "%.2f"|format(a.overall_stats.mean_speed) }} {{ _('m/s mean') }}</span>
<span style="font-size:0.78rem;color:#aaa;">{{ a.created_at.strftime('%d %b %Y') }}</span>
</summary>
<div style="padding:1.25rem 1.5rem;">
{# --- Owner action bar: rename / standalone link / PDF / delete --- #}
{% if is_owner %}
<div style="display:flex;flex-wrap:wrap;gap:.6rem;align-items:center;margin-bottom:1.25rem;">
{# Rename inline form #}
<details style="display:inline;">
<summary style="display:inline-block;padding:.3rem .75rem;background:#f0f4ff;color:#1a1a2e;
border:1px solid #c8d4f0;border-radius:4px;font-size:0.82rem;cursor:pointer;list-style:none;">
{{ _('✏ Rename') }}
</summary>
<form method="post" action="{{ url_for('analyses.rename', analysis_id=a.id) }}"
style="display:flex;gap:.5rem;align-items:center;margin-top:.5rem;">
<input type="text" name="title" value="{{ a.title }}" required
style="padding:.4rem .65rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;min-width:220px;">
<button type="submit"
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.4rem .9rem;font-size:0.85rem;cursor:pointer;">
{{ _('Save') }}
</button>
</form>
</details>
<a href="{{ url_for('analyses.detail', analysis_id=a.id) }}"
style="padding:.3rem .75rem;background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;
border-radius:4px;font-size:0.82rem;text-decoration:none;">
{{ _('Full view') }}
</a>
{% if a.pdf_path %}
<a href="{{ url_for('analyses.download_pdf', analysis_id=a.id) }}"
style="padding:.3rem .75rem;background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;
border-radius:4px;font-size:0.82rem;text-decoration:none;">
&#8615; PDF
</a>
{% endif %}
<form method="post" action="{{ url_for('analyses.delete', analysis_id=a.id) }}"
onsubmit="return confirm('{{ _('Delete this analysis? This cannot be undone.') | e }}');"
style="display:inline;">
<button type="submit"
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;
padding:.3rem .75rem;font-size:0.82rem;cursor:pointer;">
{{ _('Delete') }}
</button>
</form>
</div>
{% endif %}
{# --- Overview chart --- #}
{% if overview_chart %}
<img src="data:image/png;base64,{{ overview_chart }}" class="chart-img" alt="Overview chart"
style="margin-bottom:1.25rem;">
{% endif %}
{# --- Per-group cards --- #}
{% if groups_display %}
{% for gs, chart in groups_display %}
<div class="group-section" style="margin-bottom:1rem;">
<div class="group-meta">
<strong>{{ _('Group %(n)s', n=loop.index) }}</strong>
&nbsp;&middot;&nbsp; {{ gs.count }} {{ _('shots') }}
&nbsp;&middot;&nbsp; {{ _('mean') }} {{ "%.2f"|format(gs.mean_speed) }} m/s
{% if gs.std_speed is not none %}&nbsp;&middot;&nbsp; {{ _('SD') }} {{ "%.2f"|format(gs.std_speed) }}{% endif %}
&nbsp;&middot;&nbsp; {{ _('ES') }} {{ "%.2f"|format(gs.max_speed - gs.min_speed) }}
</div>
<img src="data:image/png;base64,{{ chart }}" class="chart-img" alt="Group {{ loop.index }} chart">
{% if gs.note %}
<div style="margin-top:.75rem;padding:.5rem .75rem;background:#fffbea;border-left:3px solid #f0c040;
border-radius:0 4px 4px 0;font-size:0.88rem;color:#555;white-space:pre-wrap;">{{ gs.note }}</div>
{% endif %}
{% if is_owner %}
<details style="margin-top:.75rem;">
<summary style="font-size:0.82rem;color:#888;cursor:pointer;list-style:none;display:inline;">
&#9998; {{ _('Note') }}
</summary>
<form method="post"
action="{{ url_for('analyses.save_group_note', analysis_id=a.id, group_index=loop.index0) }}"
style="margin-top:.4rem;display:flex;flex-direction:column;gap:.4rem;">
<textarea name="note" rows="2"
style="padding:.45rem .65rem;border:1px solid #ccc;border-radius:4px;font-size:0.88rem;resize:vertical;width:100%;max-width:520px;">{{ gs.note or '' }}</textarea>
<div>
<button type="submit"
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.35rem .85rem;font-size:0.82rem;cursor:pointer;">
{{ _('Save note') }}
</button>
</div>
</form>
</details>
{% endif %}
</div>
{% endfor %}
{% elif groups_display is none %}
<p style="color:#e74c3c;font-size:0.9rem;">{{ _('CSV file missing — cannot display charts.') }}</p>
{% endif %}
{# --- Re-group panel (owner only) --- #}
{% if is_owner %}
<details style="margin-top:1rem;border-top:1px solid #e8e8e8;padding-top:.9rem;">
<summary style="font-size:0.85rem;color:#888;cursor:pointer;list-style:none;">
&#9881; {{ _('Re-group settings') }}
</summary>
<form method="post" action="{{ url_for('analyses.regroup', analysis_id=a.id) }}"
style="margin-top:.75rem;display:flex;flex-direction:column;gap:.75rem;max-width:480px;">
<div>
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">
{{ _('Outlier factor:') }}
<span id="factor_val_{{ a.id }}">{{ a.grouping_outlier_factor or 5 }}</span>
</label>
<input type="range" name="outlier_factor" min="1" max="20" step="0.5"
value="{{ a.grouping_outlier_factor or 5 }}"
style="width:100%;"
oninput="document.getElementById('factor_val_{{ a.id }}').textContent=this.value">
<div style="display:flex;justify-content:space-between;font-size:0.75rem;color:#aaa;">
<span>{{ _('1 (fine)') }}</span><span>{{ _('20 (coarse)') }}</span>
</div>
</div>
<div>
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.3rem;">
{{ _('Manual split indices (JSON array, e.g. [5, 12])') }}
</label>
<input type="text" name="manual_splits"
value="{{ (a.grouping_manual_splits | tojson) if a.grouping_manual_splits else '' }}"
placeholder="e.g. [5, 12]"
style="width:100%;padding:.45rem .7rem;border:1px solid #ccc;border-radius:4px;font-size:0.9rem;">
<div style="font-size:0.75rem;color:#aaa;margin-top:.2rem;">{{ _('Shot positions (0-based) where a new group should always begin.') }}</div>
</div>
<div>
<button type="submit"
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.45rem 1.1rem;font-size:0.88rem;cursor:pointer;">
{{ _('Apply') }}
</button>
</div>
</form>
</details>
{% endif %}
</div>
</details>
{% endfor %}
{% else %}
<p style="color:#888;margin-bottom:1.5rem;">{{ _('No analyses yet.') }}</p>
{% endif %}
{% if is_owner %}
<form method="post"
action="{{ url_for('sessions.upload_csv', session_id=session.id) }}"
enctype="multipart/form-data"
style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;margin-bottom:2rem;">
<div>
<label style="display:block;font-size:.85rem;font-weight:600;color:#444;margin-bottom:.25rem;">{{ _('Upload chronograph CSV') }}</label>
<input type="file" name="csv_file" accept=".csv,text/csv" required style="font-size:0.9rem;">
</div>
<button type="submit"
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.5rem 1.1rem;font-size:0.9rem;cursor:pointer;">
{{ _('Analyse & link') }}
</button>
</form>
{% endif %}
{% endblock %}