feat(workshop): local-demo landing + consolidated Phase 1 + live Telegram setup
Reframes the app for the local single-team demo and reworks onboarding. Landing (local-demo chrome): - Drop Lecture / staff sign-in / programme / stat tiles / bottom CTA. - Hero → "an APESS agent on the edge"; CTA → "Start the workshop". - Presenter keeps redclaw.dev + [email protected] contact. Phase strip: - Colour by position off the active phase (earlier=done, current=active, later=pending) so a stale/persisted flag can't light up a future phase green (fixes "phase 2 green while on phase 1"). Phase 1 (Team registration) — consolidated onboarding: - After the board connects, reveal Say hi (moved from Phase 2), Extra channels (Telegram wizard), and a Voice enable/disable toggle. - Phase 2 "Meet your node" thins to Open-your-node + Pick-your-domain; removed the Lock-it-down card. New session `channels {telegram, voice}` (persist v3, non-destructive migration). Live Telegram apply (browser → APESS API → board ZeroClaw gateway): - Wizard Finish pushes the bot token to the running node and restarts it: PUT /api/config/prop channels.telegram.main.bot_token (gateway auto-creates the alias + enc2-encrypts the secret) then POST /admin/reload (in-place subsystem reload → the Telegram channel starts). - api: bridge configureTelegram + POST /nodes/:teamId/telegram; client configureTelegram(); wizard shows applying/error and only saves on success. Remote /admin/reload needs the board's gateway.allow_remote_admin (+ pairing); on a fully-open board the reload step 403s. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6386884842
commit
063116507b
+4
-132
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -7,38 +6,14 @@ import { PhaseStrip } from '@/components/PhaseStrip'
|
||||
import { OpenYourNode } from '@/components/OpenYourNode'
|
||||
import { DomainPicker } from '@/components/DomainPicker'
|
||||
import { useSession } from '@/store/session'
|
||||
import { sayHi } from '@/lib/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type HiState = 'idle' | 'running' | 'ok'
|
||||
|
||||
export function EnvSetup() {
|
||||
const navigate = useNavigate()
|
||||
const teamId = useSession((s) => s.teamId)
|
||||
const device = useSession((s) => s.device)
|
||||
const domain = useSession((s) => s.domain)
|
||||
const completePhase = useSession((s) => s.completePhase)
|
||||
const [hi, setHi] = useState<HiState>('idle')
|
||||
const [reply, setReply] = useState('')
|
||||
const [hiError, setHiError] = useState<string | null>(null)
|
||||
|
||||
const saidHi = async () => {
|
||||
if (hi === 'running') return
|
||||
setHi('running')
|
||||
setHiError(null)
|
||||
setReply('')
|
||||
try {
|
||||
// Chat with the agent on the team's own board; a reply means it's live.
|
||||
const r = await sayHi(teamId, 'cloud')
|
||||
setReply(r || '(your node replied)')
|
||||
setHi('ok')
|
||||
} catch (e) {
|
||||
setHiError(e instanceof Error ? e.message : 'Could not reach your node.')
|
||||
setHi('idle')
|
||||
}
|
||||
}
|
||||
|
||||
const ready = device.connected && hi === 'ok' && domain.trim().length > 0
|
||||
const ready = device.connected && domain.trim().length > 0
|
||||
|
||||
const onProceed = () => {
|
||||
completePhase('setup')
|
||||
@@ -66,8 +41,8 @@ export function EnvSetup() {
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Meet your node</h1>
|
||||
<p className="text-sm text-muted-foreground mt-2 max-w-xl">
|
||||
Your board is a node running its own agent. Open it, say hi, name the domain it’s
|
||||
for — then you’re clear for Module 1.
|
||||
Open your node’s own dashboard and name the domain it’s for — then
|
||||
you’re clear for Module 1.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -81,65 +56,7 @@ export function EnvSetup() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2 — Say hi to your agent */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Say hi to your agent</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Say hi to the agent running on your board. When it replies, your node is live and
|
||||
listening — and you’re clear to name its domain.
|
||||
</p>
|
||||
|
||||
{/* the exchange */}
|
||||
{(hi !== 'idle' || reply) && (
|
||||
<div className="space-y-2" data-testid="say-hi-chat">
|
||||
<div className="flex justify-end">
|
||||
<span className="rounded-lg bg-primary/10 text-foreground px-3 py-1.5 text-sm max-w-[80%]">Hi 👋</span>
|
||||
</div>
|
||||
{hi === 'running' && (
|
||||
<div className="flex items-center gap-2 font-mono text-[11px] text-muted-foreground">
|
||||
<span className="w-2 h-2 rounded-full bg-amber animate-pulse" />
|
||||
your node is thinking…
|
||||
</div>
|
||||
)}
|
||||
{reply && (
|
||||
<div className="flex justify-start">
|
||||
<span className="rounded-lg border border-teal/40 bg-teal/5 px-3 py-1.5 text-sm max-w-[80%]" data-testid="node-reply">
|
||||
{reply}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={hi === 'ok' ? 'outline' : 'default'}
|
||||
className="w-full justify-start"
|
||||
disabled={!device.connected || hi === 'running'}
|
||||
onClick={saidHi}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full mr-3',
|
||||
hi === 'ok' ? 'bg-teal' : hi === 'running' ? 'bg-amber animate-pulse' : 'bg-muted-foreground',
|
||||
)}
|
||||
/>
|
||||
{hi === 'ok' ? 'Your node replied ✓' : hi === 'running' ? 'Waiting for your node…' : 'Say hi to your agent'}
|
||||
</Button>
|
||||
{hiError && (
|
||||
<p role="alert" className="text-xs text-red-500 leading-relaxed">{hiError}</p>
|
||||
)}
|
||||
{!device.connected && (
|
||||
<p className="font-mono text-[10px] text-muted-foreground leading-relaxed">
|
||||
Bind your board in team registration first.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3 — Pick your domain */}
|
||||
{/* 2 — Pick your domain */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Pick your domain</CardTitle>
|
||||
@@ -149,51 +66,6 @@ export function EnvSetup() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 4 — Extra channels (optional) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
Extra channels <span className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">optional</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground leading-relaxed">
|
||||
<p>
|
||||
Your dashboard is the main way in, but you can reach your node other ways too:
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<span className="font-medium text-foreground">Telegram</span> — make a bot with{' '}
|
||||
<span className="font-mono text-xs">@BotFather</span>, then paste its token in the node
|
||||
dashboard → Config → channels.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-foreground">Voice</span> — use the mic button in the
|
||||
node dashboard to talk to it directly.
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 5 — Lock it down (policy moment) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Lock it down</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground leading-relaxed">
|
||||
<p>
|
||||
Your board boots <span className="font-medium text-foreground">open</span> so setup is
|
||||
frictionless — anyone on the LAN can reach it right now. That’s your first{' '}
|
||||
<span className="font-medium text-foreground">policy</span> decision: when setup is done,
|
||||
harden it so only your group can talk to it.
|
||||
</p>
|
||||
<p>
|
||||
Run <span className="font-mono text-xs">zeroclaw-lockdown.sh</span> on the board — it mints
|
||||
a pair code your group uses to reconnect. Leave it open for now; you’ll revisit this
|
||||
once you’ve designed the agent’s policies.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button size="lg" disabled={!ready} onClick={onProceed}>
|
||||
Proceed to Module 1 →
|
||||
|
||||
Reference in New Issue
Block a user