diff --git a/public/css/style.css b/public/css/style.css index 53a0875..4a04656 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -675,24 +675,40 @@ button:disabled { .timeline-concert { margin-bottom: 1.5rem; } .timeline-songs { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 0.9rem; + display: flex; + flex-direction: column; + gap: 0.6rem; margin-top: 0.5rem; } -.timeline-song-card { +.timeline-song-row { + display: flex; + align-items: flex-start; + gap: 0.9rem; background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--radius-sm); - padding: 0.7rem; + padding: 0.7rem 0.9rem; } +.timeline-song-row .row-index { margin-top: 0.15rem; } + +.timeline-song-main { flex: 1 1 auto; min-width: 0; } + .timeline-song-title { font-weight: 600; } .timeline-embed { - max-width: none; - margin-top: 0.5rem; + width: 200px; + max-width: 200px; + flex: 0 0 auto; + margin-top: 0; +} + +.timeline-no-embed { + flex: 0 0 auto; + width: 200px; + text-align: center; + margin: 0; } /* ---------- Setlist ---------- */ @@ -1060,4 +1076,8 @@ a.back-link:hover { color: var(--accent); } .calendar-header-row span:nth-child(n) { font-size: 0.7rem; } .cal-cell { min-height: 64px; font-size: 0.7rem; } + + .timeline-song-row { flex-wrap: wrap; } + .timeline-embed, + .timeline-no-embed { width: 100%; max-width: none; } } diff --git a/public/js/calendar.js b/public/js/calendar.js index e994726..4af4b9b 100644 --- a/public/js/calendar.js +++ b/public/js/calendar.js @@ -210,7 +210,7 @@ function pollWorkflowStatus(btn, maxMs = 180000, intervalMs = 4000) { const data = await api.get('/api/calendar/workflow-status'); if (data.status === 'success') { clearInterval(timer); - showToast('Calendrier mis à jour !'); + showToast('Calendrier mis à jour !' + (data.message ? ' ' + data.message : ''), false, 10000); resetRefreshButton(btn); await loadSlots(); await loadLastChecked(); diff --git a/public/js/history.js b/public/js/history.js index c0bcf64..19bb598 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -11,17 +11,20 @@ function formatDateShort(dateStr) { return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }); } -function timelineSongCard(song) { +function timelineSongRow(song) { const embed = youtubeEmbedUrl(song.youtube_url); return ` -
-
${escapeHtml(song.title)}
-
${escapeHtml(song.artist)}
+
+
${song.position}
+
+
${escapeHtml(song.title)}
+
${escapeHtml(song.artist)}
+ ${song.note ? `

${escapeHtml(song.note)}

` : ''} + ${song.spotify_url ? `` : ''} +
${embed ? `
` - : `

Pas de lien YouTube

`} - ${song.spotify_url ? `` : ''} - ${song.note ? `

${escapeHtml(song.note)}

` : ''} + : `

Pas de lien YouTube

`}
`; } @@ -37,14 +40,14 @@ function timelineConcertBlock(concert) {

Setlist

${main.length - ? `
${main.map(timelineSongCard).join('')}
` + ? `
${main.map(timelineSongRow).join('')}
` : '

Aucun morceau enregistré.

'}
${encore.length ? `

Rappel

-
${encore.map(timelineSongCard).join('')}
+
${encore.map(timelineSongRow).join('')}
` : ''} Voir / modifier ce concert → diff --git a/src/lib/calendarWorkflowState.js b/src/lib/calendarWorkflowState.js index e8a9bc4..3781579 100644 --- a/src/lib/calendarWorkflowState.js +++ b/src/lib/calendarWorkflowState.js @@ -14,8 +14,8 @@ function setRunning() { state = { status: 'running', triggeredAt: new Date().toISOString(), message: null, node: null }; } -function setSuccess() { - state = { ...state, status: 'success', message: null, node: null }; +function setSuccess(message) { + state = { ...state, status: 'success', message: message || null, node: null }; } function setError(message, node) { diff --git a/src/repositories/calendarRepo.js b/src/repositories/calendarRepo.js index fd2490e..2944575 100644 --- a/src/repositories/calendarRepo.js +++ b/src/repositories/calendarRepo.js @@ -29,8 +29,15 @@ async function upsertPerson(client, name) { return rows[0].id; } +// Returns ingestion counts so callers can surface a diagnostic (e.g. "12 +// slots but 0 availability rows" points at a payload-shape mismatch from +// n8n, since a slot with no matching calendar_availability rows is silently +// excluded from getSlots() below — it would otherwise look like "nothing +// happened" with no error anywhere. async function ingestSlots(slots) { const client = await pool.connect(); + let availabilityRows = 0; + let slotsWithNoPeople = 0; try { await client.query('BEGIN'); for (const slot of slots) { @@ -51,10 +58,23 @@ async function ingestSlots(slots) { const slotId = slotRows[0].id; // people may arrive as a JSON string from n8n's Set-node serialization. - const people = typeof slot.people === 'string' ? JSON.parse(slot.people) : slot.people || []; + let people = typeof slot.people === 'string' ? JSON.parse(slot.people) : slot.people || []; + if (!Array.isArray(people)) people = []; + + if (people.length === 0) { + slotsWithNoPeople += 1; + console.warn('[calendar] ingest: slot has no people entries', { + lower: slot.lower, + upper: slot.upper, + rawPeopleType: typeof slot.people, + }); + } for (const person of people) { - if (!person || !person.name) continue; + if (!person || !person.name) { + console.warn('[calendar] ingest: skipped a person entry with no "name" field', person); + continue; + } const personId = await upsertPerson(client, person.name); await client.query( `INSERT INTO calendar_availability (slot_id, person_id, is_available, checked_at) @@ -64,6 +84,7 @@ async function ingestSlots(slots) { checked_at = excluded.checked_at`, [slotId, personId, !!person.available] ); + availabilityRows += 1; } } await client.query('COMMIT'); @@ -73,6 +94,10 @@ async function ingestSlots(slots) { } finally { client.release(); } + + const summary = { slotsProcessed: slots.length, availabilityRows, slotsWithNoPeople }; + console.log('[calendar] ingest summary:', summary); + return summary; } async function getSlots({ minPeople = 1, personIds = null, weeks = 3 } = {}) { diff --git a/src/routes/calendar.js b/src/routes/calendar.js index c8e9d83..85247e0 100644 --- a/src/routes/calendar.js +++ b/src/routes/calendar.js @@ -78,12 +78,16 @@ router.post('/refresh', (req, res) => { const data = await response.json(); if (!Array.isArray(data.slots)) { + console.error('[calendar] n8n response had no "slots" array. Raw body:', JSON.stringify(data).slice(0, 500)); workflowState.setError('Unexpected response format from n8n'); return; } - await calendarRepo.ingestSlots(data.slots); - workflowState.setSuccess(); + const summary = await calendarRepo.ingestSlots(data.slots); + workflowState.setSuccess( + `${summary.slotsProcessed} créneaux reçus, ${summary.availabilityRows} disponibilités enregistrées` + + (summary.slotsWithNoPeople > 0 ? ` (${summary.slotsWithNoPeople} créneaux sans aucune personne — voir les logs serveur)` : '') + ); } catch (err) { clearTimeout(watchdog); const message = err.name === 'AbortError' ? 'n8n did not respond within 5 minutes' : err.message; diff --git a/src/routes/calendarWebhooks.js b/src/routes/calendarWebhooks.js index 5cac8ac..c362608 100644 --- a/src/routes/calendarWebhooks.js +++ b/src/routes/calendarWebhooks.js @@ -21,9 +21,11 @@ router.post( return res.status(400).json({ error: 'expected_array_of_slots' }); } try { - await calendarRepo.ingestSlots(slots); - workflowState.setSuccess(); - res.json({ ok: true, count: slots.length }); + const summary = await calendarRepo.ingestSlots(slots); + workflowState.setSuccess( + `${summary.slotsProcessed} créneaux reçus, ${summary.availabilityRows} disponibilités enregistrées` + ); + res.json({ ok: true, ...summary }); } catch (err) { workflowState.setError(err.message); res.status(500).json({ error: err.message });