import { useState, useEffect } from 'react' import Journal from './apps/Journal.jsx' import Calculator from './apps/Calculator.jsx' import Drawing from './apps/Drawing.jsx' import CodeLab from './apps/CodeLab.jsx' import Clock from './apps/Clock.jsx' import Files from './apps/Files.jsx' import Scratch from './apps/Scratch.jsx' // LOCAL ONLY - per parent boundary: no external IPs, only localhost + file:// // Allowed: http://localhost:5173, http://127.0.0.1:5173, file:// // Blocked: https://scratch.mit.edu etc. Scratch app now offline-only. const APPS = [ { name: 'Journal', icon: '/icons/journal.png', emoji:'📓', color: '#88C0D0', category: 'Create', desc: 'Write daily adventure', comp: 'journal', order: 10 }, { name: 'Code', icon: '/icons/code.png', emoji:'💻', color: '#5E81AC', category: 'Create', desc: 'Make something', comp: 'code', order: 15 }, { name: 'Scratch', icon: '/icons/scratch.png', emoji:'🧱', color: '#88C0D0', category: 'Create', desc: 'Blocks • offline', comp: 'scratch', order: 16 }, { name: 'Calculator', icon: '/icons/calculator.png', emoji:'🧮', color: '#A3BE8C', category: 'Learn', desc: 'Do math', comp: 'calculator', order: 20 }, { name: 'Clock', icon: '/icons/clock.png', emoji:'⏰', color: '#EBCB8B', category: 'System', desc: 'Time & timer', comp: 'clock', order: 22 }, { name: 'Drawing', icon: '/icons/play.png', emoji:'🎨', color: '#9D7BD8', category: 'Play', desc: 'Draw & paint', comp: 'drawing', order: 25 }, { name: 'Play', icon: '/icons/play.png', emoji:'🎮', color: '#9D7BD8', category: 'Play', desc: 'Play ground', comp: 'drawing', order: 26 }, { name: 'Files', icon: '/icons/files.png', emoji:'📁', color: '#5E81AC', category: 'System', desc: 'Browse creations', comp: 'files', order: 30 }, ] const CATS = ['All','Create','Learn','Play','System'] const CAT_COLORS = {All:'#D8DEE9', Create:'#88C0D0', Learn:'#A3BE8C', Play:'#9D7BD8', System:'#5E81AC'} const CAT_ICONS = {All:'☰', Create:'✏', Learn:'✈', Play:'▶', System:'⚙'} const COMPS = { journal: Journal, calculator: Calculator, drawing: Drawing, code: CodeLab, clock: Clock, files: Files, scratch: Scratch, } export default function App(){ const [cat, setCat] = useState(()=> localStorage.getItem('kiddos_cat') || 'All') const [clock, setClock] = useState(new Date()) const [showSettings, setShowSettings] = useState(false) const [currentApp, setCurrentApp] = useState(null) const [recents, setRecents] = useState(()=> JSON.parse(localStorage.getItem('kiddos_recents')||'[]')) const [companionMsg, setCompanionMsg] = useState(()=> localStorage.getItem('kiddos_companion_message')||'') const [stats, setStats] = useState(()=> { const keys = Object.keys(localStorage).filter(k=>k.startsWith('journal_')) return {entries: keys.length} }) useEffect(()=>{ const t=setInterval(()=>setClock(new Date()),1000) return ()=>clearInterval(t) },[]) // Local-only stats polling. Companion is local file ~/launcher_config/companion_message.txt // We mirror via localStorage key kiddos_companion_message if parent sets it. // No external fetch. useEffect(()=>{ const id=setInterval(()=>{ const keys = Object.keys(localStorage).filter(k=>k.startsWith('journal_')) setStats({entries: keys.length}) const msg = localStorage.getItem('kiddos_companion_message') if(msg) setCompanionMsg(msg) },3000) return ()=>clearInterval(id) },[]) useEffect(()=>{ localStorage.setItem('kiddos_cat', cat) },[cat]) const saveRecent = (name)=>{ const r=[name, ...recents.filter(n=>n!==name)].slice(0,8) setRecents(r) localStorage.setItem('kiddos_recents', JSON.stringify(r)) } const openApp = (app)=>{ saveRecent(app.name) setCurrentApp(app) } const filtered = (cat==='All'? APPS : APPS.filter(a=>a.category===cat)).sort((a,b)=>a.order-b.order) const favs = JSON.parse(localStorage.getItem('kiddos_favs')||'["Journal","Code","Calculator"]') const favApps = APPS.filter(a=>favs.includes(a.name)) const recentApps = recents.map(n=>APPS.find(a=>a.name===n)).filter(Boolean).filter(a=>!favs.includes(a.name)) const favToShow = [...favApps, ...recentApps].slice(0,10) if(currentApp){ const Comp = COMPS[currentApp.comp] return (
{currentApp.emoji} {currentApp.name} {currentApp.category}
{clock.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})} • {stats.entries} entries
{Comp ? :
App {currentApp.name} coming soon
}
{showSettings &&
setShowSettings(false)} style={{position:'fixed', inset:0, background:'rgba(0,0,0,0.6)', zIndex:40}}>
} setShowSettings(false)} stats={stats} favs={favs} onFavsChange={(f)=>{localStorage.setItem('kiddos_favs', JSON.stringify(f))}} recents={recents} />
) } return (
E
Hey, Elias! React v3 ⚡ {stats.entries} entries
{companionMsg ? `✨ ${companionMsg}` : 'Offline • localhost:5173 only • No internet • Hot reload'}
{clock.toLocaleString(undefined,{weekday:'short', month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'})}
⭐ Favorites & Recent (tap → hot reload on save) 🔒 localhost only • file:// • offline
{favToShow.map(app=>(
openApp(app)} style={{minWidth:'110px', height:'80px', background:'#232A36', border:'1px solid #EBCB8B', borderRadius:'12px', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:'4px', cursor:'pointer', flexShrink:0}}> {e.target.style.display='none'; e.target.nextSibling.style.display='block'}} style={{width:'32px', height:'32px', borderRadius:'6px'}}/> {app.emoji} {app.name}
))} {favToShow.length===0 && No favorites yet — tap apps below}
{CATS.map(c=>( ))}
{filtered.map(app=>(
openApp(app)} style={{width:'150px', minHeight:'132px', background:'#3B4252', border:'1px solid #4C566A', borderRadius:'16px', padding:'10px', display:'flex', flexDirection:'column', alignItems:'center', gap:'6px', cursor:'pointer'}}>
{e.target.style.display='none'; e.target.parentElement.querySelector('.emoji-fallback').style.display='block'}} style={{width:'48px', height:'48px', borderRadius:'8px'}}/> {app.emoji}
{app.name}
{app.desc}
))}
DSI 800×480 • cage + cog wl • FT5x06 touch • Vite HMR • 🔒 localhost:5173 only
✅ Offline • No external URLs • localStorage • file:// fallback works
{showSettings &&
setShowSettings(false)} style={{position:'fixed', inset:0, background:'rgba(0,0,0,0.6)', zIndex:10, display:'block'}}>
} setShowSettings(false)} stats={stats} favs={favs} onFavsChange={(f)=>{localStorage.setItem('kiddos_favs', JSON.stringify(f)); location.reload()}} recents={recents} />
) } function SettingsDrawer({open, onClose, stats, favs, onFavsChange, recents}){ const [allApps] = useState(()=>APPS.map(a=>a.name)) return (
⚙ KiddOs v3 React

📊 Stats (local)

Journal entries: {stats.entries}
Recents: {recents.join(', ')||'none'}
Touch: {'ontouchstart' in window ? 'Yes ✅' : 'No — try cage may need libinput debug'}
DSI: 800×480 • DRM card1 connector 49
UserAgent: {navigator.userAgent.slice(0,80)}

⭐ Favorites (local only)

{allApps.map(name=>{ const isFav = favs.includes(name) return ( ) })}
Offline • Saved in localStorage

🔒 Network lock — localhost only

Per safety boundary: browser may only load localhost or file:// — no external IPs. This means:

  • ✅ http://localhost:5173 (Vite HMR dev)
  • ✅ http://127.0.0.1:5173
  • ✅ file:///home/elias/KiddOsV3/webapp/dist/index.html (prod)
  • ❌ https://scratch.mit.edu (blocked — now offline Scratch app)

Cog launched with --doc-viewer but no external browse. All app content is local React.

🖥️ Kiosk commands (localhost only)

{`# dev server (SSH 1) — localhost only
cd ~/KiddOsV3/webapp
npm run dev -- --host 0.0.0.0 --port 5173

# DSI (SSH 2) — only localhost, no external
sudo openvt -f -c 1 -s -- env XDG_RUNTIME_DIR=/run/user/1000 cage -s -- \\
  cog --platform=wl http://localhost:5173 --bg-color=#2E3440

# prod file:// (no network at all)
cd ~/KiddOsV3/webapp && npm run build
cage -s -- cog --platform=wl file://$PWD/dist/index.html
`}
          

⚡ Hot reload test

Edit src/App.jsx on laptop via SSH, save, DSI updates in <1s via Vite WebSocket (same host:port, still localhost). Try changing greeting text or card color.

Test: change "Hey, Elias!" to "Hey, Elias! 🔥 HMR works!" in App.jsx and save → watch DSI
) }