Initial project scaffold

Full-stack homeschool web app with FastAPI backend, Vue 3 frontend,
MySQL database, and Docker Compose orchestration. Includes JWT auth,
WebSocket real-time TV dashboard, schedule builder, activity logging,
and multi-child support.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
derekcandClaude Sonnet 4.6 committed 2026-02-27 22:56:56 -08:00
1 parent 93e0494864
commit 417b3adfe8
68 files changed
+3919

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/',
withCredentials: true, // send cookies (refresh token)
})
// Attach access token to every request
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Auto-refresh on 401
let refreshing = false
let waitQueue = []
api.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config
if (error.response?.status === 401 && !original._retry) {
if (refreshing) {
return new Promise((resolve, reject) => {
waitQueue.push({ resolve, reject })
}).then(() => api(original))
}
original._retry = true
refreshing = true
try {
const res = await axios.post('/api/auth/refresh', {}, { withCredentials: true })
const token = res.data.access_token
localStorage.setItem('access_token', token)
waitQueue.forEach(({ resolve }) => resolve())
waitQueue = []
original.headers.Authorization = `Bearer ${token}`
return api(original)
} catch (_) {
waitQueue.forEach(({ reject }) => reject())
waitQueue = []
localStorage.removeItem('access_token')
window.location.href = '/login'
} finally {
refreshing = false
}
}
return Promise.reject(error)
}
)
export default api
+50
View File
@@ -0,0 +1,50 @@
import { ref, onUnmounted } from 'vue'
export function useWebSocket(childId, onMessage) {
const connected = ref(false)
let ws = null
let reconnectTimer = null
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const url = `${protocol}://${window.location.host}/ws/${childId}`
ws = new WebSocket(url)
ws.onopen = () => {
connected.value = true
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
onMessage(data)
} catch (e) {
console.warn('WS parse error', e)
}
}
ws.onclose = () => {
connected.value = false
// Reconnect after 3 seconds
reconnectTimer = setTimeout(connect, 3000)
}
ws.onerror = () => {
ws.close()
}
}
function disconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer)
if (ws) ws.close()
}
connect()
onUnmounted(disconnect)
return { connected, disconnect }
}