Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commita004c46unknown_key

polish(dashboard): hero with greeting + animated orb + display title (session 2.1)

polish(dashboard): hero with greeting + animated orb + display title (session 2.1)

Phase 2 polish begins — dashboard hero rebuilt to match the
landing/pricing/auth quality bar established in session 1.

Changes to src/routes/dashboard.tsx /dashboard handler:

  - Replaced the plain 28px 'Command Center' h1 + flex-row header
    with a hero card that has visual hierarchy and personality.
  - Time-of-day greeting eyebrow ('Good morning, ccantynz') ties the
    page to the user. Username rendered in accent colour.
  - clamp(28-40px) display title 'Your command center.' with the
    accent words rendered in the same violet→cyan gradient used in
    the landing hero.
  - Sub-line is context-aware: '12 repos — real-time health, AI
    activity, and gate status across everything you own' OR (when
    you have no repos yet) 'Create your first repository to start
    shipping with AI.'
  - 'Import from GitHub' button added to the action row — it was
    missing despite being one of the highest-conversion paths.
  - Hero has a subtle gradient hairline on the top edge (same
    treatment as auth-container — visual consistency across
    'gateway' surfaces) + a soft animated orb in the top-right
    corner (14s breath, honors prefers-reduced-motion).
  - Responsive: stacks at <720px with full-width action buttons.

Inline <style> block scoped to .dash-* so it doesn't bleed into
other dashboard surfaces (intelligence panel, push log) which keep
their existing styling.

Also fixes 2 TypeScript errors in src/__tests__/google-oauth.test.ts
(`as typeof fetch` → `as unknown as typeof fetch` — Bun's fetch
type includes a preconnect property the test mock can omit).

Tests: 2011/2011 pass. tsc clean.
Claude committed on May 17, 2026Parent: 582cdac
2 files changed+13412a004c4683de14cd4ba3863f563a27855536ab3d0
2 changed files+134−12
Modifiedsrc/__tests__/google-oauth.test.ts+3−3View fileUnifiedSplit
115115 JSON.stringify({ access_token: "tok-1", id_token: "id-1" }),
116116 { status: 200, headers: { "content-type": "application/json" } }
117117 );
118 }) as typeof fetch;
118 }) as unknown as typeof fetch;
119119
120120 const result = await exchangeGoogleCode(
121121 {
146146 picture: "https://lh.example/avatar.jpg",
147147 }),
148148 { status: 200, headers: { "content-type": "application/json" } }
149 )) as typeof fetch;
149 )) as unknown as typeof fetch;
150150
151151 const info = await fetchGoogleUserinfo(
152152 { userinfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo" },
170170 picture: null,
171171 }),
172172 { status: 200, headers: { "content-type": "application/json" } }
173 )) as typeof fetch;
173 )) as unknown as typeof fetch;
174174
175175 const info = await fetchGoogleUserinfo(
176176 { userinfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo" },
Modifiedsrc/routes/dashboard.tsx+131−9View fileUnifiedSplit
236236 </form>
237237 </div>
238238 )}
239 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px">
240 <div>
241 <h1 style="font-size: 28px; margin-bottom: 4px">Command Center</h1>
242 <p style="color: var(--text-muted); font-size: 14px">
243 Real-time overview of all your repositories
244 </p>
239 <div class="dash-hero">
240 <div class="dash-hero-bg" aria-hidden="true">
241 <div class="dash-hero-orb" />
245242 </div>
246 <div style="display: flex; gap: var(--space-2)">
247 <a href="/new" class="btn btn-primary">+ New repo</a>
248 <a href="/settings" class="btn">Settings</a>
243 <div class="dash-hero-inner">
244 <div class="dash-hero-text">
245 <div class="dash-hero-eyebrow">
246 {(() => {
247 const hour = new Date().getHours();
248 if (hour < 5) return "Late night,";
249 if (hour < 12) return "Good morning,";
250 if (hour < 17) return "Good afternoon,";
251 if (hour < 21) return "Good evening,";
252 return "Late night,";
253 })()}{" "}
254 <span class="dash-hero-username">{user.username}</span>
255 </div>
256 <h1 class="dash-hero-title">
257 Your{" "}
258 <span class="gradient-text">command center</span>.
259 </h1>
260 <p class="dash-hero-sub">
261 {repos.length === 0
262 ? "Create your first repository to start shipping with AI."
263 : `${repos.length} repo${repos.length === 1 ? "" : "s"} · real-time health, AI activity, and gate status across everything you own.`}
264 </p>
265 </div>
266 <div class="dash-hero-actions">
267 <a href="/new" class="btn btn-primary">+ New repo</a>
268 <a href="/import" class="btn">Import from GitHub</a>
269 <a href="/settings" class="btn">Settings</a>
270 </div>
249271 </div>
250272 </div>
273 <style
274 dangerouslySetInnerHTML={{
275 __html: `
276 .dash-hero {
277 position: relative;
278 margin-bottom: var(--space-6);
279 padding: var(--space-5) var(--space-6) var(--space-5);
280 background: var(--bg-elevated);
281 border: 1px solid var(--border);
282 border-radius: 16px;
283 overflow: hidden;
284 }
285 .dash-hero::before {
286 content: '';
287 position: absolute;
288 top: 0; left: 0; right: 0;
289 height: 2px;
290 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
291 opacity: 0.7;
292 pointer-events: none;
293 }
294 .dash-hero-bg {
295 position: absolute;
296 inset: -20% -10% auto auto;
297 width: 380px;
298 height: 380px;
299 pointer-events: none;
300 z-index: 0;
301 }
302 .dash-hero-orb {
303 position: absolute;
304 inset: 0;
305 background: radial-gradient(circle, rgba(140,109,255,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
306 filter: blur(80px);
307 opacity: 0.7;
308 animation: dashHeroOrb 14s ease-in-out infinite;
309 }
310 @keyframes dashHeroOrb {
311 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
312 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.8; }
313 }
314 @media (prefers-reduced-motion: reduce) {
315 .dash-hero-orb { animation: none; }
316 }
317 .dash-hero-inner {
318 position: relative;
319 z-index: 1;
320 display: flex;
321 justify-content: space-between;
322 align-items: flex-end;
323 gap: var(--space-4);
324 flex-wrap: wrap;
325 }
326 .dash-hero-text { flex: 1; min-width: 280px; }
327 .dash-hero-eyebrow {
328 font-size: 13px;
329 color: var(--text-muted);
330 margin-bottom: var(--space-2);
331 letter-spacing: -0.005em;
332 }
333 .dash-hero-username {
334 color: var(--accent);
335 font-weight: 600;
336 }
337 .dash-hero-title {
338 font-size: clamp(28px, 4vw, 40px);
339 font-family: var(--font-display);
340 font-weight: 800;
341 letter-spacing: -0.028em;
342 line-height: 1.05;
343 margin: 0 0 var(--space-2);
344 color: var(--text-strong);
345 }
346 .dash-hero-title .gradient-text {
347 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
348 -webkit-background-clip: text;
349 background-clip: text;
350 -webkit-text-fill-color: transparent;
351 color: transparent;
352 }
353 .dash-hero-sub {
354 font-size: 15px;
355 color: var(--text-muted);
356 margin: 0;
357 line-height: 1.5;
358 max-width: 580px;
359 }
360 .dash-hero-actions {
361 display: flex;
362 gap: var(--space-2);
363 flex-wrap: wrap;
364 }
365 @media (max-width: 720px) {
366 .dash-hero-inner { flex-direction: column; align-items: flex-start; }
367 .dash-hero-actions { width: 100%; }
368 .dash-hero-actions .btn { flex: 1; min-width: 0; }
369 }
370 `,
371 }}
372 />
251373
252374 {/* ─── L9: AI hours-saved hero widget ─── */}
253375 <AiHoursSavedWidget week={savingsWeek} lifetime={savingsLifetime} />
254376