Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copy this file to .env in the repo root, then:
# cd infra && docker compose --env-file ../.env up --build
#
# Every value marked REQUIRED_change_me must be replaced before deploying.
# Generate strong random secrets with: openssl rand -hex 32

# ─── Database ──────────────────────────────────────────────────────────────
POSTGRES_USER=liveboard
POSTGRES_PASSWORD=REQUIRED_change_me
POSTGRES_DB=liveboard

# ─── Redis ─────────────────────────────────────────────────────────────────
REDIS_PASSWORD=REQUIRED_change_me

# ─── Backend (apps/api) ────────────────────────────────────────────────────
# Ingest master key. Also doubles as the internal BFF<->API token unless
# INTERNAL_SERVICE_TOKEN is set below. Must be >=32 random chars in production
# (core/config.py rejects weak/default values outright).
API_SECRET_KEY=REQUIRED_change_me

# Optional: a separate token for internal BFF<->API calls, instead of reusing
# API_SECRET_KEY for that purpose. Recommended in production. If unset, the
# API falls back to accepting API_SECRET_KEY for internal calls (see
# apps/api/api/deps.py::_valid_internal_token).
INTERNAL_SERVICE_TOKEN=

# Optional: Cerebras API key for AI-written incident summaries. Leave empty
# to disable AI summaries — anomaly detection still runs, just without the
# generated write-up.
CEREBRAS_API_KEY=

ENVIRONMENT=production

# ─── Frontend (Next.js) ────────────────────────────────────────────────────
# Public URL of the API, reachable from the browser.
NEXT_PUBLIC_API_URL=https://api.yourdomain.com

# Public URL this frontend is deployed at.
NEXTAUTH_URL=https://yourdomain.com

# Session encryption secret for NextAuth/Auth.js.
AUTH_SECRET=REQUIRED_change_me

# Google OAuth. Required for real sign-in in production — without it, and
# with DISABLE_DEV_LOGIN=true (below), nobody can sign in at all.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Restrict who's allowed to sign up. Leave both empty to allow anyone with a
# Google account (open sign-up) — fine for a personal instance, probably not
# for a team/company one.
ALLOWED_EMAILS=
ALLOWED_EMAIL_DOMAIN=

# Hard kill-switch for the dev-login credentials bypass (accepts any email
# with zero verification), independent of NODE_ENV. Always set this to true
# outside of local development.
DISABLE_DEV_LOGIN=true
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
cache: pip

- working-directory: apps/api
run: pip install ruff && ruff check .
run: pip install ruff==0.15.17 && ruff check .

sdk-js:
name: SDK JS — Typecheck & Build
Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
cache: pip

- working-directory: packages/sdk-python
run: pip install ruff httpx && ruff check liveboard/
run: pip install ruff==0.15.17 httpx && ruff check liveboard/

- working-directory: packages/sdk-python
run: python -c "from liveboard import SDK_VERSION, LiveBoardConfig; print('import OK', SDK_VERSION)"
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

# Next.js
/.next/
.agents
.claude
/out/
/build

Expand All @@ -19,7 +21,7 @@ next-env.d.ts
.env.*.local
# Note: .env.example is intentionally NOT listed here

# ─── Python ──────────────────────────────────────────────────────────────────
# ─── Python ─────────────────────────────────────────────────────────────────
__pycache__/
*.py[cod]
*.egg-info/
Expand Down Expand Up @@ -55,4 +57,4 @@ Thumbs.db
# ─── Confidential — never commit the build plan ──────────────────────────────
*.pdf
/checklist.md
.env.example
.ground.md
176 changes: 93 additions & 83 deletions app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use client";

import Link from "next/link";
import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { Suspense, useState } from "react";
import { AlertCircle, Terminal } from "lucide-react";
import { LiveboardIcon } from "@/components/logo";
import { primaryButtonClass, secondaryButtonClass } from "@/components/landing/button-styles";

const hasGoogle =
Boolean(process.env.NEXT_PUBLIC_GOOGLE_ENABLED) ||
Expand Down Expand Up @@ -37,110 +40,117 @@ function SignInForm() {
}

return (
<div className="min-h-screen bg-[#0A0A0A] flex items-center justify-center px-4">
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-sm">
{/* Logo */}
<div className="flex items-center justify-center gap-2.5 mb-8">
<LiveboardIcon size={28} />
<span className="text-xl font-bold tracking-tight text-[#F5F5F5] font-display">
Liveboard
</span>
</div>

<div className="rounded-lg border border-[#1E1E1E] bg-[#111] p-8 space-y-6">
<div className="space-y-1.5 text-center">
<h1 className="text-lg font-semibold text-[#F5F5F5]">Welcome back</h1>
<p className="text-sm text-[#949494]">Sign in to your Liveboard workspace</p>
<Link href="/" className="mb-8 flex items-center justify-center gap-2.5">
<LiveboardIcon size={26} />
<span className="text-lg font-semibold tracking-tight text-foreground">Liveboard</span>
</Link>

<div className="border border-border bg-surface p-8">
<div className="text-center">
<h1 className="text-lg font-semibold text-foreground">Sign in to Liveboard</h1>
<p className="mt-1.5 text-sm text-muted">New here? Signing in creates your workspace automatically.</p>
</div>

{error && (
<div className="rounded border border-red/20 bg-red-dim px-3 py-2 text-xs text-red">
{ERROR_MESSAGES[error] ?? "Authentication error. Please try again."}
<div className="mt-6 flex items-start gap-2 border border-red/25 bg-red-dim px-3 py-2.5 text-xs text-red">
<AlertCircle className="mt-0.5 h-3.5 w-3.5 flex-shrink-0" strokeWidth={1.75} />
<span>{ERROR_MESSAGES[error] ?? "Authentication error. Please try again."}</span>
</div>
)}

{/* Google button — shown when GOOGLE_CLIENT_ID is configured */}
{hasGoogle && (
<button
onClick={() => signIn("google", { callbackUrl })}
className="w-full flex items-center justify-center gap-3 px-4 py-2.5 rounded border border-[#2A2A2A] bg-[#161616] hover:bg-[#1E1E1E] hover:border-[#333] transition-colors text-sm font-medium text-[#F5F5F5]"
>
<GoogleIcon />
Continue with Google
</button>
)}

{/* Dev login form — shown in development when Google isn't set up */}
{isDev && (
<>
{hasGoogle && (
<div className="flex items-center gap-2">
<div className="flex-1 h-px bg-[#1E1E1E]" />
<span className="text-[10px] text-[#808080]">dev login</span>
<div className="flex-1 h-px bg-[#1E1E1E]" />
</div>
)}
<form onSubmit={handleDevLogin} className="space-y-3">
{!hasGoogle && (
<div className="rounded border border-amber-500/20 bg-amber-500/5 px-3 py-2">
<p className="text-[10px] text-amber-400 leading-relaxed">
<span className="font-semibold">Dev mode.</span>{" "}
Google OAuth not configured — sign in with any email below.
Set <code className="font-mono">GOOGLE_CLIENT_ID</code> to enable Google.
</p>
<div className="mt-6 space-y-5">
{hasGoogle && (
<button onClick={() => signIn("google", { callbackUrl })} className={secondaryButtonClass("md", "w-full")}>
<GoogleIcon />
Continue with Google
</button>
)}

{isDev && (
<div>
{hasGoogle && (
<div className="mb-5 flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-[10px] uppercase tracking-wider text-muted-dark">or</span>
<div className="h-px flex-1 bg-border" />
</div>
)}
<div className="space-y-2">
<input
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 rounded border border-[#2A2A2A] bg-[#161616] text-sm text-[#F5F5F5] placeholder-[#333] outline-none focus:border-blue/50 transition-colors"
/>
<input
type="text"
placeholder="Display name (optional)"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 rounded border border-[#2A2A2A] bg-[#161616] text-sm text-[#F5F5F5] placeholder-[#333] outline-none focus:border-blue/50 transition-colors"
/>

<div className="mb-3 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-dark">
<Terminal className="h-3 w-3" strokeWidth={1.75} />
Development sign-in
</div>
<button
type="submit"
disabled={!email || loading}
className="w-full px-4 py-2.5 rounded bg-blue hover:bg-blue-hover disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-sm font-medium text-white"
>
{loading ? "Signing in…" : "Sign in"}
</button>
</form>
</>
)}

{!hasGoogle && !isDev && (
<p className="text-center text-xs text-red">
Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
</p>
)}
<form onSubmit={handleDevLogin} className="space-y-3">
<div>
<label htmlFor="email" className="mb-1.5 block text-xs text-muted">
Email address
</label>
<input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors placeholder:text-muted-dark focus:border-blue"
/>
</div>
<div>
<label htmlFor="name" className="mb-1.5 block text-xs text-muted">
Display name
</label>
<input
id="name"
type="text"
placeholder="Only needed the first time"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors placeholder:text-muted-dark focus:border-blue"
/>
</div>
<button
type="submit"
disabled={!email || loading}
className={primaryButtonClass("md", "w-full disabled:pointer-events-none disabled:opacity-40")}
>
{loading ? "Signing in…" : "Sign in"}
</button>
</form>

<p className="mt-3 text-[11px] leading-relaxed text-muted-dark">
No password needed in development. This form is hidden in production once Google OAuth is configured.
</p>
</div>
)}

{!hasGoogle && !isDev && (
<p className="text-center text-xs text-red">
Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
</p>
)}
</div>

<p className="text-center text-[10px] text-[#808080] leading-relaxed">
By signing in you agree to our{" "}
<span className="text-[#949494] hover:text-[#888] cursor-pointer transition-colors">Terms of Service</span>
{" "}and{" "}
<span className="text-[#949494] hover:text-[#888] cursor-pointer transition-colors">Privacy Policy</span>.
<p className="mt-6 text-center text-[11px] leading-relaxed text-muted-dark">
By signing in you agree to our Terms of Service and Privacy Policy.
</p>
</div>

<p className="mt-6 text-center text-xs text-[#808080]">Open source API observability</p>
</div>
</div>
);
}

export default function SignInPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#0A0A0A]" />}>
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="h-[420px] w-full max-w-sm border border-border bg-surface" />
</div>
}
>
<SignInForm />
</Suspense>
);
Expand Down
Loading
Loading