| --- | |
| layout: null | |
| sitemap: false | |
| title: "Gerrit User Summit 2026 - Now" | |
| --- | |
| {%- comment -%} | |
| Full-screen display for the venue screens: shows the session in progress | |
| and what comes next, from _data/schedule.yml. Open it in a browser and | |
| click once to go full screen. Add ?now=2026-11-09T10:50 (venue time) to | |
| preview any moment of the summit. | |
| assets/img/qa-slido-qr.svg encodes the Slido event for the maintainers | |
| Q&A: https://app.sli.do/event/5HqT8EqrrawEWt9BQ2C7GE | |
| {%- endcomment -%} | |
| {%- for day in site.data.schedule -%} | |
| {%- for s in day.sessions -%} | |
| {%- if s.talk == "q-a-with-the-maintainers" -%} | |
| {%- assign qa_day = day.day | split: " " | first -%} | |
| {%- assign qa_time = day.date | date: "%Y-%m-%d" | append: " " | append: s.time | date: "%-I:%M %p" -%} | |
| {%- endif -%} | |
| {%- endfor -%} | |
| {%- endfor -%} | |
| {%- assign photos = site.static_files | where_exp: "f", "f.path contains '/assets/img/speakers/'" -%} | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="robots" content="noindex"> | |
| <title>{{ page.title }}</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;0,9..40,800;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="{{ site.baseurl }}/assets/css/base.css"> | |
| <link rel="stylesheet" href="{{ site.baseurl }}/assets/css/now.css"> | |
| </head> | |
| <body class="now"> | |
| <div class="now-grid"></div> | |
| <header class="now-top"> | |
| <img src="{{ site.baseurl }}/assets/img/gus-logo.png" alt="Gerrit User Summit 2026" class="now-logo"> | |
| <div class="now-clock"> | |
| <span class="now-clock-day" id="clock-day"></span> | |
| <span class="now-clock-time" id="clock-time"></span> | |
| </div> | |
| </header> | |
| <main class="now-main"> | |
| <section class="now-stage" id="stage"></section> | |
| <aside class="qa-card"> | |
| <div class="qa-title">Ask the maintainers anything</div> | |
| <div class="qa-qr"> | |
| <img src="{{ site.baseurl }}/assets/img/qa-slido-qr.svg" alt="QR code to submit questions on Slido"> | |
| </div> | |
| <div class="qa-note">Scan to send your questions for the<br><strong>Q&A with the maintainers</strong></div> | |
| {%- if qa_day %} | |
| <div class="qa-when">{{ qa_day }} · {{ qa_time }}</div> | |
| {%- endif %} | |
| </aside> | |
| </main> | |
| <footer class="now-foot"> | |
| <div class="now-org"> | |
| <span class="now-org-label">Hosted by</span> | |
| <img src="{{ site.baseurl }}/assets/img/google-logo.svg" alt="Google" class="now-org-logo google-logo"> | |
| </div> | |
| <div class="now-org"> | |
| <span class="now-org-label">Sponsored by</span> | |
| <img src="{{ site.baseurl }}/assets/img/gerritforge-logo.svg" alt="GerritForge" class="now-org-logo gerritforge-logo"> | |
| </div> | |
| </footer> | |
| <script> | |
| const VENUE_TZ = 'America/Los_Angeles'; | |
| const VENUE_OFFSET = '-08:00'; // PST, the summit is after the DST change | |
| const SCHEDULE_URL = '{{ site.baseurl }}/schedule.json'; | |
| const PHOTOS = { {%- for f in photos -%}{{ f.basename | jsonify }}: {{ site.baseurl | append: f.path | jsonify }}{%- unless forloop.last -%}, {% endunless -%}{%- endfor -%} }; | |
| let schedule = {{ site.data.schedule | jsonify }}; | |
| // ── Clock, optionally shifted by ?now=<venue time> to preview ── | |
| let skew = 0; | |
| const nowParam = new URLSearchParams(location.search).get('now'); | |
| if (nowParam) { | |
| const iso = /(Z|[+-]\d\d:?\d\d)$/i.test(nowParam) ? nowParam : nowParam + VENUE_OFFSET; | |
| const t = Date.parse(iso); | |
| if (!isNaN(t)) skew = t - Date.now(); | |
| } | |
| const now = () => Date.now() + skew; | |
| // ── Schedule as a flat list of sessions with start instants ── | |
| // The last session of a day ("Doors close") only marks its end. | |
| function timeline(days) { | |
| const out = []; | |
| days.forEach(d => d.sessions.forEach((s, i) => out.push(Object.assign({}, s, { | |
| day: d.day, | |
| start: Date.parse(`${d.date}T${s.time}:00${VENUE_OFFSET}`), | |
| dayEnd: i === d.sessions.length - 1 | |
| })))); | |
| return out; | |
| } | |
| let items = timeline(schedule); | |
| // ── Formatting ── | |
| const timeFmt = new Intl.DateTimeFormat('en-US', { timeZone: VENUE_TZ, hour: 'numeric', minute: '2-digit' }); | |
| const dayFmt = new Intl.DateTimeFormat('en-US', { timeZone: VENUE_TZ, weekday: 'long', month: 'long', day: 'numeric' }); | |
| const fmtTime = t => timeFmt.format(t); | |
| const pad = n => String(n).padStart(2, '0'); | |
| function fmtDuration(ms) { | |
| const s = Math.max(0, Math.ceil(ms / 1000)); | |
| const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60); | |
| if (d) return `${d}d ${h}h ${pad(m)}m`; | |
| if (h) return `${h}:${pad(m)}:${pad(s % 60)}`; | |
| return `${pad(m)}:${pad(s % 60)}`; | |
| } | |
| const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); | |
| const initials = name => name.split(/\s+/).map(w => w[0]).slice(0, 2).join(''); | |
| function speakers(list, small) { | |
| if (!list || !list.length) return ''; | |
| const many = list.length > 3; | |
| return `<div class="speakers${many ? ' many' : ''}${small ? ' small' : ''}">` + list.map(sp => { | |
| const avatar = many ? '' : (PHOTOS[sp.id] | |
| ? `<img class="speaker-photo" src="${esc(PHOTOS[sp.id])}" alt="">` | |
| : `<span class="speaker-photo speaker-mono">${esc(initials(sp.name))}</span>`); | |
| const co = sp.company ? `<span class="speaker-co">${esc(sp.company)}</span>` : ''; | |
| return `<div class="speaker">${avatar}<div class="speaker-text"><span class="speaker-name">${esc(sp.name)}</span>${co}</div></div>`; | |
| }).join('') + '</div>'; | |
| } | |
| function upNext(s, label) { | |
| return `<div class="next"> | |
| <div class="next-label">${esc(label || 'Up next')} · ${fmtTime(s.start)}</div> | |
| <div class="next-title">${esc(s.title)}</div> | |
| ${speakers(s.speakers, true)} | |
| </div>`; | |
| } | |
| // ── What is on at instant t ── | |
| function stateAt(t) { | |
| let i = -1; | |
| while (i + 1 < items.length && items[i + 1].start <= t) i++; | |
| if (i === -1) return { kind: 'before', next: items[0], key: 'before' }; | |
| const cur = items[i]; | |
| if (cur.dayEnd) { | |
| return i === items.length - 1 | |
| ? { kind: 'over', key: 'over' } | |
| : { kind: 'overnight', next: items[i + 1], key: 'overnight' + i }; | |
| } | |
| return { kind: cur.talk ? 'talk' : 'pause', cur, next: items[i + 1], key: 'at' + i }; | |
| } | |
| // First talk at or after session index of s | |
| function firstTalkFrom(s) { | |
| return items.slice(items.indexOf(s)).find(x => x.talk && x.day === s.day); | |
| } | |
| function render(st) { | |
| switch (st.kind) { | |
| case 'talk': | |
| return `<div class="stage-label"><span class="live-dot"></span>Now</div> | |
| <h1 class="stage-title">${esc(st.cur.title)}</h1> | |
| ${speakers(st.cur.speakers)} | |
| <div class="progress"><div class="progress-bar" id="bar"></div></div> | |
| <div class="progress-meta"><span>${fmtTime(st.cur.start)}</span><span id="left"></span><span>${fmtTime(st.next.start)}</span></div> | |
| ${upNext(st.next)}`; | |
| case 'pause': | |
| return `<h1 class="stage-title pause-title">${esc(st.cur.title)}</h1> | |
| <div class="countdown-label">${st.next.talk ? 'Next talk in' : esc(st.next.title) + ' in'}</div> | |
| <div class="countdown" id="cd"></div> | |
| ${st.next.dayEnd ? '' : upNext(st.next)}`; | |
| case 'before': | |
| case 'overnight': { | |
| const talk = firstTalkFrom(st.next); | |
| return `<div class="stage-label">${st.kind === 'before' ? 'Welcome' : 'That’s a wrap for today'}</div> | |
| <h1 class="stage-title">${esc(st.next.day)}<br><span class="stage-sub">${esc(st.next.title)} at ${fmtTime(st.next.start)}</span></h1> | |
| <div class="countdown-label">Starts in</div> | |
| <div class="countdown" id="cd"></div> | |
| ${talk ? upNext(talk, 'First talk') : ''}`; | |
| } | |
| case 'over': | |
| return `<div class="stage-label">That’s a wrap</div> | |
| <h1 class="stage-title">Thank you for joining the Gerrit User Summit 2026!</h1>`; | |
| } | |
| } | |
| // ── Tick: rebuild the stage on a new session, update timers every second ── | |
| const stage = document.getElementById('stage'); | |
| let shownKey = null; | |
| function tick() { | |
| const t = now(); | |
| document.getElementById('clock-day').textContent = dayFmt.format(t); | |
| document.getElementById('clock-time').textContent = fmtTime(t); | |
| const st = stateAt(t); | |
| if (st.key !== shownKey) { | |
| stage.innerHTML = render(st); | |
| stage.dataset.kind = st.kind; | |
| shownKey = st.key; | |
| } | |
| const cd = document.getElementById('cd'); | |
| if (cd) cd.textContent = fmtDuration(st.next.start - t); | |
| const bar = document.getElementById('bar'); | |
| if (bar) { | |
| const span = st.next.start - st.cur.start; | |
| bar.style.width = Math.min(100, (t - st.cur.start) / span * 100) + '%'; | |
| const mins = Math.ceil((st.next.start - t) / 60000); | |
| document.getElementById('left').textContent = mins === 1 ? '1 min left' : `${mins} min left`; | |
| } | |
| } | |
| tick(); | |
| setInterval(tick, 1000); | |
| // ── Pick up a redeployed schedule without reloading the page ── | |
| setInterval(async () => { | |
| try { | |
| const res = await fetch(SCHEDULE_URL, { cache: 'no-store' }); | |
| if (!res.ok) return; | |
| const fresh = await res.json(); | |
| if (JSON.stringify(fresh) === JSON.stringify(schedule)) return; | |
| schedule = fresh; | |
| items = timeline(schedule); | |
| shownKey = null; | |
| tick(); | |
| } catch (e) { /* offline: keep the schedule we have */ } | |
| }, 60000); | |
| // ── Kiosk: keep the screen awake, click to go full screen ── | |
| async function keepAwake() { | |
| try { await navigator.wakeLock.request('screen'); } catch (e) { /* unsupported or denied */ } | |
| } | |
| keepAwake(); | |
| document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') keepAwake(); }); | |
| document.addEventListener('click', e => { | |
| if (e.target.closest('a') || document.fullscreenElement) return; | |
| document.documentElement.requestFullscreen().catch(() => {}); | |
| }); | |
| </script> | |
| </body> | |
| </html> |