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 (
Per safety boundary: browser may only load localhost or file:// — no external IPs. This means:
Cog launched with --doc-viewer but no external browse. All app content is local React.
{`# 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
`}
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