Compare commits
1 Commits
30f9148adb
...
4fe483f68f
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fe483f68f |
@@ -0,0 +1,45 @@
|
||||
# KiddOs v3 — Initial App Log
|
||||
|
||||
**Recorded:** 2026-08-24 22:31 EDT
|
||||
**Repository:** `~/brain/projects/kid_dash`
|
||||
**Source:** `https://git.reynafamily.com/adolforeyna/kid_dash`
|
||||
**Initial revision:** `30f9148adb1f90b1821e86727e75e9fadbc9bd9b`
|
||||
|
||||
## Purpose
|
||||
|
||||
KiddOs v3 is an offline-first React/Vite browser kiosk for Elias. It targets an **800 × 480** children’s display and provides local-only apps backed by browser `localStorage`.
|
||||
|
||||
## Initial app set
|
||||
|
||||
- Journal
|
||||
- CodeLab
|
||||
- Scratch (offline information/app)
|
||||
- Calculator
|
||||
- Clock and timer
|
||||
- Drawing / Play
|
||||
- Files browser
|
||||
|
||||
## Development setup
|
||||
|
||||
- App source: `webapp/src/`
|
||||
- Development command: `npm run dev`
|
||||
- Test command: `npm test`
|
||||
- Build command: `npm run build`
|
||||
- The public development hostname used during initial work was `elias.reynafamily.com`.
|
||||
|
||||
## Initial UI work
|
||||
|
||||
- Added a centered, framed **800 × 480** preview composition for browser review.
|
||||
- At an actual 800 × 480 device viewport, the frame is removed and KiddOs fills the display.
|
||||
- Added CSS zoom configuration for the browser preview.
|
||||
- Added Node tests for the native viewport frame and public HMR configuration.
|
||||
|
||||
## Verification
|
||||
|
||||
At the end of the initial work, the Node tests passed and the Vite production build succeeded.
|
||||
|
||||
## Current operational status
|
||||
|
||||
- The temporary Vite development server was stopped at the user’s request.
|
||||
- The app-specific public Caddy proxy route and its reboot restore entry were removed at the same time.
|
||||
- No public KiddOs server is currently running.
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.adolforeyna.kid-dash-dev</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/npm</string>
|
||||
<string>run</string>
|
||||
<string>dev</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/adolforeyna/brain/projects/kid_dash/webapp</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/adolforeyna/Library/Logs/kid-dash-dev.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/adolforeyna/Library/Logs/kid-dash-dev.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"match": [{"host": ["elias.reynafamily.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [{"dial": "192.168.68.130:5173"}]
|
||||
}],
|
||||
"terminal": true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
HOST="elias.reynafamily.com"
|
||||
ROUTE_JSON="/home/adolforeyna/elias-caddy-route.json"
|
||||
API="http://127.0.0.1:2019/config/apps/http/servers/srv2/routes"
|
||||
|
||||
INDEX=$(curl -fsS "$API" | python3 -c '
|
||||
import json, sys
|
||||
host = sys.argv[1]
|
||||
routes = json.load(sys.stdin)
|
||||
for index, route in enumerate(routes):
|
||||
if any(host in match.get("host", []) for match in route.get("match", [])):
|
||||
print(index)
|
||||
break
|
||||
' "$HOST")
|
||||
|
||||
if [ -n "$INDEX" ]; then
|
||||
curl -fsS -X DELETE "$API/$INDEX"
|
||||
fi
|
||||
curl -fsS -X POST -H "Content-Type: application/json" --data-binary "@$ROUTE_JSON" "$API"
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0 --port 5173",
|
||||
"test": "node --test",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 0.0.0.0 --port 4173"
|
||||
},
|
||||
|
||||
+2
-2
@@ -90,7 +90,7 @@ export default function App(){
|
||||
if(currentApp){
|
||||
const Comp = COMPS[currentApp.comp]
|
||||
return (
|
||||
<div style={{height:'100vh', display:'flex', flexDirection:'column', background:'#2E3440'}}>
|
||||
<div className="kiddos-screen" style={{height:'100%', display:'flex', flexDirection:'column', background:'#2E3440'}}>
|
||||
<header style={{height:'52px', background:'#1E222E', borderBottom:'1px solid #4C566A', display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 12px', flexShrink:0}}>
|
||||
<div style={{display:'flex', gap:'10px', alignItems:'center'}}>
|
||||
<button onClick={()=>setCurrentApp(null)} style={{background:'#3B4252', border:'1px solid #4C566A', color:'#ECEFF4', borderRadius:'8px', padding:'0 10px', height:'36px', cursor:'pointer'}}>🏠 KiddOs</button>
|
||||
@@ -114,7 +114,7 @@ export default function App(){
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display:'flex', flexDirection:'column', height:'100vh', background:'#2E3440', overflow:'hidden'}}>
|
||||
<div className="kiddos-screen" style={{display:'flex', flexDirection:'column', height:'100%', background:'#2E3440', overflow:'hidden'}}>
|
||||
<header style={{height:'56px', background:'#1E222E', borderBottom:'1px solid #4C566A', display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 12px', flexShrink:0}}>
|
||||
<div style={{display:'flex', gap:'10px', alignItems:'center'}}>
|
||||
<div style={{width:'36px', height:'36px', background:'#434C5E', borderRadius:'10px', display:'flex', alignItems:'center', justifyContent:'center', fontWeight:'bold', fontSize:'16px', border:'1px solid #4C566A'}}>E</div>
|
||||
|
||||
+43
-11
@@ -15,16 +15,48 @@
|
||||
--accent-purple: #9D7BD8;
|
||||
--accent-orange: #D08770;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin:0; padding:0; height:100%; overflow:hidden; }
|
||||
body { background:var(--bg); color:var(--text); font-family: system-ui, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; }
|
||||
button { font-family: inherit; cursor:pointer; user-select:none; }
|
||||
button:active { transform: scale(0.97); }
|
||||
::-webkit-scrollbar { width:6px; height:6px; }
|
||||
::-webkit-scrollbar-thumb { background:#4C566A; border-radius:3px; }
|
||||
::-webkit-scrollbar-track { background:transparent; }
|
||||
|
||||
/* 800x480 specific */
|
||||
@media (max-width: 800px), (max-height: 480px) {
|
||||
header { height:48px !important; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #root { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
||||
body {
|
||||
background: #151923;
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
button { font-family: inherit; cursor: pointer; user-select: none; }
|
||||
button:active { transform: scale(0.97); }
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-thumb { background: #4C566A; border-radius: 3px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
.kiddos-stage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
background: radial-gradient(circle at 50% 20%, #3B4252 0, #232A36 48%, #151923 100%);
|
||||
}
|
||||
|
||||
.kiddos-screen {
|
||||
width: 800px !important;
|
||||
height: 480px !important;
|
||||
/* Browser preview: enlarge the 800×480 composition to 160%. */
|
||||
zoom: 1.6;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
border: 10px solid #11151D;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55), inset 0 0 0 1px #59647A;
|
||||
}
|
||||
|
||||
/* The DSI is the native screen: use every pixel and avoid a clipped frame. */
|
||||
@media (max-width: 800px), (max-height: 480px) {
|
||||
.kiddos-stage { min-height: 0; padding: 0; background: var(--bg); }
|
||||
.kiddos-screen { width: 100vw !important; height: 100vh !important; zoom: 1 !important; border: 0; border-radius: 0; box-shadow: none; }
|
||||
header { height: 48px !important; }
|
||||
}
|
||||
|
||||
+3
-1
@@ -5,6 +5,8 @@ import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<div className="kiddos-stage">
|
||||
<App />
|
||||
</div>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const css = await readFile(new URL('../src/index.css', import.meta.url), 'utf8')
|
||||
|
||||
test('keeps the native screen size ahead of inline app dimensions', () => {
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*width:\s*800px\s*!important/s)
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*height:\s*480px\s*!important/s)
|
||||
})
|
||||
|
||||
test('uses the requested 1.6 zoom for the browser preview', () => {
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*zoom:\s*1\.6/s)
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const css = await readFile(new URL('../src/index.css', import.meta.url), 'utf8')
|
||||
|
||||
test('defines a centered 800 by 480 KiddOs screen frame', () => {
|
||||
assert.match(css, /\.kiddos-stage\s*\{[^}]*place-items:\s*center/s)
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*width:\s*800px/s)
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*height:\s*480px/s)
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*border:/s)
|
||||
})
|
||||
|
||||
test('uses the full physical display without a desktop frame at 800 by 480', () => {
|
||||
assert.match(css, /@media\s*\(max-width:\s*800px\),\s*\(max-height:\s*480px\)/s)
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*width:\s*100vw/s)
|
||||
assert.match(css, /\.kiddos-screen\s*\{[^}]*height:\s*100vh/s)
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const viteConfig = await readFile(new URL('../vite.config.js', import.meta.url), 'utf8')
|
||||
|
||||
test('routes HMR through the public HTTPS development host', () => {
|
||||
assert.match(viteConfig, /host:\s*'elias\.reynafamily\.com'/)
|
||||
assert.match(viteConfig, /protocol:\s*'wss'/)
|
||||
assert.match(viteConfig, /clientPort:\s*443/)
|
||||
})
|
||||
@@ -7,8 +7,10 @@ export default defineConfig({
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
hmr: {
|
||||
clientPort: 5173,
|
||||
// overlay shows errors on DSI too
|
||||
// Caddy terminates TLS and proxies WebSocket upgrades from the public dev URL.
|
||||
host: 'elias.reynafamily.com',
|
||||
protocol: 'wss',
|
||||
clientPort: 443,
|
||||
overlay: true
|
||||
},
|
||||
cors: true
|
||||
|
||||
Reference in New Issue
Block a user