blob: 2727b0afc71fc4af225d64377ab92668268e5bd9 [file] [edit]
---
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&amp;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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>