2026-02-23 18:21:23 +00:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>CalTrack</title>
|
2026-02-24 12:19:54 +00:00
|
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
2026-02-23 18:21:23 +00:00
|
|
|
<style>
|
|
|
|
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
|
|
|
|
|
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
|
|
|
|
:root {
|
|
|
|
|
--bg: #0d1117;
|
|
|
|
|
--bg2: #161b22;
|
|
|
|
|
--border: #30363d;
|
|
|
|
|
--green: #3fb950;
|
|
|
|
|
--amber: #d29922;
|
|
|
|
|
--red: #f85149;
|
|
|
|
|
--dim: #484f58;
|
|
|
|
|
--text: #c9d1d9;
|
|
|
|
|
--bright: #f0f6fc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#app {
|
|
|
|
|
max-width: 720px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.box {
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.box + .box { border-top: none; }
|
|
|
|
|
|
|
|
|
|
.box-header {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background: var(--bg2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.box-body { padding: 12px 16px; }
|
|
|
|
|
|
|
|
|
|
.title { color: var(--green); font-weight: bold; }
|
|
|
|
|
.dim { color: var(--dim); }
|
|
|
|
|
.amber { color: var(--amber); }
|
|
|
|
|
.red { color: var(--red); }
|
|
|
|
|
.green { color: var(--green); }
|
|
|
|
|
.bright { color: var(--bright); }
|
|
|
|
|
|
|
|
|
|
/* Header */
|
|
|
|
|
#header-date { color: var(--amber); }
|
|
|
|
|
#header-weight { color: var(--text); }
|
|
|
|
|
|
|
|
|
|
.stats-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Entries */
|
|
|
|
|
.entry {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 2px 0;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.entry:hover { background: var(--bg2); }
|
|
|
|
|
|
|
|
|
|
.entry-time { color: var(--dim); min-width: 50px; }
|
|
|
|
|
|
|
|
|
|
.entry-type {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
min-width: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.entry-type.food { color: var(--green); }
|
|
|
|
|
.entry-type.exercise { color: var(--amber); }
|
|
|
|
|
|
|
|
|
|
.entry-desc { flex: 1; }
|
|
|
|
|
|
|
|
|
|
.entry-kcal { min-width: 90px; text-align: right; }
|
|
|
|
|
.entry-kcal.positive { color: var(--text); }
|
|
|
|
|
.entry-kcal.negative { color: var(--amber); }
|
|
|
|
|
|
|
|
|
|
.entry-delete {
|
|
|
|
|
color: var(--dim);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
border: none;
|
|
|
|
|
background: none;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.entry-delete:hover { color: var(--red); }
|
|
|
|
|
|
|
|
|
|
.empty-log { color: var(--dim); padding: 16px 0; text-align: center; }
|
|
|
|
|
|
|
|
|
|
/* Summary bar */
|
|
|
|
|
.summary-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-track {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-pct { color: var(--amber); min-width: 80px; text-align: right; }
|
|
|
|
|
|
|
|
|
|
/* Input area */
|
|
|
|
|
#input-box .box-body { padding: 8px 16px; }
|
|
|
|
|
|
|
|
|
|
.input-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-prompt { color: var(--green); font-weight: bold; }
|
|
|
|
|
|
|
|
|
|
#main-input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
color: var(--bright);
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
outline: none;
|
|
|
|
|
caret-color: var(--green);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#main-input::placeholder { color: var(--dim); }
|
|
|
|
|
|
|
|
|
|
.mode-indicator {
|
|
|
|
|
color: var(--dim);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Autocomplete */
|
|
|
|
|
.autocomplete {
|
|
|
|
|
display: none;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.autocomplete.active { display: block; }
|
|
|
|
|
|
|
|
|
|
.ac-item {
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ac-item:hover, .ac-item.selected { background: var(--bg2); color: var(--bright); }
|
|
|
|
|
.ac-name { color: var(--text); }
|
|
|
|
|
.ac-kcal { color: var(--dim); }
|
|
|
|
|
|
|
|
|
|
/* Shortcuts */
|
|
|
|
|
.shortcuts {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.shortcut { color: var(--dim); }
|
|
|
|
|
.shortcut kbd {
|
|
|
|
|
color: var(--amber);
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Loading */
|
|
|
|
|
.loading {
|
|
|
|
|
display: none;
|
|
|
|
|
color: var(--amber);
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading.active { display: block; }
|
|
|
|
|
|
|
|
|
|
@keyframes blink { 50% { opacity: 0; } }
|
|
|
|
|
.blink { animation: blink 1s step-end infinite; }
|
|
|
|
|
|
|
|
|
|
/* History overlay */
|
|
|
|
|
.overlay {
|
|
|
|
|
display: none;
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
|
|
|
background: rgba(0,0,0,0.85);
|
|
|
|
|
z-index: 100;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overlay.active { display: flex; }
|
|
|
|
|
|
|
|
|
|
.overlay-content {
|
|
|
|
|
max-width: 720px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overlay .box { border-color: var(--dim); }
|
|
|
|
|
|
|
|
|
|
.overlay-close {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: var(--dim);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overlay-close:hover { color: var(--red); }
|
|
|
|
|
|
|
|
|
|
/* Settings form */
|
|
|
|
|
.form-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-row:last-child { border-bottom: none; }
|
|
|
|
|
|
|
|
|
|
.form-label { color: var(--dim); }
|
|
|
|
|
|
|
|
|
|
.form-input {
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
color: var(--bright);
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
width: 200px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input:focus { outline: 1px solid var(--green); }
|
|
|
|
|
|
|
|
|
|
select.form-input { text-align: left; }
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
background: var(--bg2);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
color: var(--green);
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
padding: 6px 16px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn:hover { background: var(--border); }
|
|
|
|
|
|
|
|
|
|
/* History items */
|
|
|
|
|
.history-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-item:hover { background: var(--bg2); }
|
|
|
|
|
.history-item:last-child { border-bottom: none; }
|
|
|
|
|
|
|
|
|
|
/* Weight history */
|
|
|
|
|
.weight-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 2px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Setup screen */
|
|
|
|
|
#setup-screen {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#setup-screen.active {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#main-screen.hidden {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Navigation hint */
|
|
|
|
|
.nav-hint {
|
|
|
|
|
color: var(--dim);
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Responsive */
|
|
|
|
|
@media (max-width: 500px) {
|
|
|
|
|
body { padding: 8px; font-size: 12px; }
|
|
|
|
|
.stats-row { flex-direction: column; }
|
|
|
|
|
.shortcuts { gap: 8px; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
<div id="app">
|
|
|
|
|
<!-- Setup Screen -->
|
|
|
|
|
<div id="setup-screen">
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
<span class="title">CalTrack - First Run Setup</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<p class="dim" style="margin-bottom:12px">Configure your profile to get started.</p>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Anthropic API Key</span>
|
|
|
|
|
<input type="password" class="form-input" id="setup-apikey" placeholder="sk-ant-..." style="width:280px">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Current Weight (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="setup-weight" value="80" step="0.1">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Target Weight (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="setup-target" value="70" step="0.1">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Height (cm)</span>
|
|
|
|
|
<input type="number" class="form-input" id="setup-height" value="175">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Age</span>
|
|
|
|
|
<input type="number" class="form-input" id="setup-age" value="30">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Sex</span>
|
|
|
|
|
<select class="form-input" id="setup-sex">
|
|
|
|
|
<option value="male">Male</option>
|
|
|
|
|
<option value="female">Female</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Activity Level</span>
|
|
|
|
|
<select class="form-input" id="setup-activity">
|
|
|
|
|
<option value="sedentary">Sedentary</option>
|
|
|
|
|
<option value="light">Light</option>
|
|
|
|
|
<option value="moderate" selected>Moderate</option>
|
|
|
|
|
<option value="active">Active</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Weekly Loss Target (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="setup-loss" value="0.5" step="0.1">
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn" onclick="saveSetup()" style="width:100%">[ Start Tracking ]</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Main Screen -->
|
|
|
|
|
<div id="main-screen" class="hidden">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
<span class="title">CalTrack</span>
|
|
|
|
|
<span id="header-date" class="amber"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div class="stats-row">
|
|
|
|
|
<span>Weight: <span id="header-weight" class="bright">--</span> kg</span>
|
|
|
|
|
<span>Target: <span id="header-target" class="green">--</span> kcal/day</span>
|
|
|
|
|
<span>Deficit: <span id="header-deficit" class="amber">--</span> kcal</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Day Log -->
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
<span>
|
|
|
|
|
<span id="nav-prev" style="cursor:pointer;color:var(--dim)" onclick="navigateDay(-1)"><</span>
|
|
|
|
|
<span id="log-title">Today's Log</span>
|
|
|
|
|
<span id="nav-next" style="cursor:pointer;color:var(--dim)" onclick="navigateDay(1)">></span>
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
<span class="nav-hint" style="margin-right:8px">arrow keys to navigate</span>
|
|
|
|
|
<span style="cursor:pointer;color:var(--dim)" onclick="clearDay()" title="Clear all entries for this day">[clear day]</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div id="entries-list">
|
|
|
|
|
<div class="empty-log">No entries yet. Start typing below!</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Summary -->
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div class="summary-row">
|
|
|
|
|
<span>Consumed: <span id="sum-consumed" class="bright">0</span></span>
|
|
|
|
|
<span>Burned: <span id="sum-burned" class="amber">0</span></span>
|
|
|
|
|
<span>Remaining: <span id="sum-remaining" class="bright">0</span></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="progress-bar">
|
|
|
|
|
<span class="dim">[</span>
|
|
|
|
|
<span id="progress-track" class="progress-track"></span>
|
|
|
|
|
<span class="dim">]</span>
|
|
|
|
|
<span id="progress-pct" class="progress-pct">0%</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Input -->
|
|
|
|
|
<div class="box" id="input-box">
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div class="input-row">
|
|
|
|
|
<span class="input-prompt">></span>
|
|
|
|
|
<input id="main-input" type="text" placeholder="ate a banana and a protein shake..." autocomplete="off">
|
|
|
|
|
<span class="mode-indicator" id="mode-label">[food]</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="loading" class="loading">
|
|
|
|
|
<span class="blink">Estimating calories...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="autocomplete" class="autocomplete"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Shortcuts -->
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div class="shortcuts">
|
|
|
|
|
<span class="shortcut"><kbd>f</kbd>ood</span>
|
|
|
|
|
<span class="shortcut"><kbd>e</kbd>xercise</span>
|
|
|
|
|
<span class="shortcut"><kbd>w</kbd>eigh-in</span>
|
|
|
|
|
<span class="shortcut"><kbd>c</kbd>lear day</span>
|
|
|
|
|
<span class="shortcut"><kbd>h</kbd>istory</span>
|
|
|
|
|
<span class="shortcut"><kbd>s</kbd>ettings</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- History Overlay -->
|
|
|
|
|
<div id="history-overlay" class="overlay">
|
|
|
|
|
<div class="overlay-content">
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
<span class="title">History (Last 30 Days)</span>
|
|
|
|
|
<span class="overlay-close" onclick="closeOverlay('history')">[x]</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="box-body" id="history-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Settings Overlay -->
|
|
|
|
|
<div id="settings-overlay" class="overlay">
|
|
|
|
|
<div class="overlay-content">
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
<span class="title">Settings</span>
|
|
|
|
|
<span class="overlay-close" onclick="closeOverlay('settings')">[x]</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">API Key</span>
|
|
|
|
|
<input type="password" class="form-input" id="s-apikey" style="width:280px">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Current Weight (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="s-weight" step="0.1">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Target Weight (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="s-target" step="0.1">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Height (cm)</span>
|
|
|
|
|
<input type="number" class="form-input" id="s-height">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Age</span>
|
|
|
|
|
<input type="number" class="form-input" id="s-age">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Sex</span>
|
|
|
|
|
<select class="form-input" id="s-sex">
|
|
|
|
|
<option value="male">Male</option>
|
|
|
|
|
<option value="female">Female</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Activity Level</span>
|
|
|
|
|
<select class="form-input" id="s-activity">
|
|
|
|
|
<option value="sedentary">Sedentary</option>
|
|
|
|
|
<option value="light">Light</option>
|
|
|
|
|
<option value="moderate">Moderate</option>
|
|
|
|
|
<option value="active">Active</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Weekly Loss (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="s-loss" step="0.1">
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn" onclick="saveSettings()" style="width:100%">[ Save Settings ]</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Weigh-in Overlay -->
|
|
|
|
|
<div id="weighin-overlay" class="overlay">
|
|
|
|
|
<div class="overlay-content">
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
<span class="title">Log Weigh-In</span>
|
|
|
|
|
<span class="overlay-close" onclick="closeOverlay('weighin')">[x]</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="box-body">
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<span class="form-label">Weight (kg)</span>
|
|
|
|
|
<input type="number" class="form-input" id="w-weight" step="0.1" autofocus>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn" onclick="saveWeighIn()" style="width:100%">[ Log Weight ]</button>
|
|
|
|
|
<div style="margin-top:16px">
|
|
|
|
|
<div class="dim" style="margin-bottom:8px">Recent weigh-ins:</div>
|
|
|
|
|
<div id="weight-history-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// State
|
|
|
|
|
let currentMode = 'food';
|
|
|
|
|
let currentDate = new Date().toISOString().slice(0, 10);
|
|
|
|
|
let todayStr = currentDate;
|
|
|
|
|
let acIndex = -1;
|
|
|
|
|
let acResults = [];
|
|
|
|
|
let acTimeout = null;
|
|
|
|
|
|
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
|
const input = $('main-input');
|
|
|
|
|
|
|
|
|
|
// Format date for display
|
|
|
|
|
function fmtDate(dateStr) {
|
|
|
|
|
const d = new Date(dateStr + 'T12:00:00');
|
|
|
|
|
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fmtKcal(n) {
|
|
|
|
|
return n.toLocaleString();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 19:01:04 +00:00
|
|
|
// Base path detection (works under /caltrack/ or /)
|
|
|
|
|
const basePath = window.location.pathname.replace(/\/$/, '');
|
|
|
|
|
|
2026-02-23 18:21:23 +00:00
|
|
|
// Fetch helpers
|
|
|
|
|
async function api(method, path, body) {
|
|
|
|
|
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
|
|
|
if (body) opts.body = JSON.stringify(body);
|
2026-02-23 19:01:04 +00:00
|
|
|
const res = await fetch(basePath + path, opts);
|
2026-02-23 18:21:23 +00:00
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load day data
|
|
|
|
|
async function loadDay(date) {
|
|
|
|
|
currentDate = date;
|
|
|
|
|
const path = date === todayStr ? '/api/today' : `/api/day/${date}`;
|
|
|
|
|
const data = await api('GET', path);
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
$('header-date').textContent = fmtDate(date);
|
|
|
|
|
$('header-weight').textContent = data.weight ? data.weight.toFixed(1) : '--';
|
|
|
|
|
$('header-target').textContent = fmtKcal(data.target_kcal);
|
|
|
|
|
|
|
|
|
|
$('header-deficit').textContent = `-${fmtKcal(data.deficit)}`;
|
|
|
|
|
|
|
|
|
|
// Log title
|
|
|
|
|
$('log-title').textContent = date === todayStr ? "Today's Log" : fmtDate(date);
|
|
|
|
|
|
|
|
|
|
// Entries
|
|
|
|
|
const list = $('entries-list');
|
|
|
|
|
if (!data.entries || data.entries.length === 0) {
|
|
|
|
|
list.innerHTML = '<div class="empty-log">No entries yet.</div>';
|
|
|
|
|
} else {
|
|
|
|
|
list.innerHTML = data.entries.map(e => `
|
|
|
|
|
<div class="entry">
|
|
|
|
|
<span class="entry-time">${e.time}</span>
|
|
|
|
|
<span class="entry-type ${e.type}">[${e.type === 'food' ? 'F' : 'E'}]</span>
|
|
|
|
|
<span class="entry-desc">${escHtml(e.description)}</span>
|
|
|
|
|
<span class="entry-kcal ${e.type === 'food' ? 'positive' : 'negative'}">${e.type === 'food' ? '+' : '-'}${fmtKcal(e.kcal)} kcal</span>
|
|
|
|
|
<button class="entry-delete" onclick="deleteEntry(${e.id})" title="Delete">x</button>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Summary
|
|
|
|
|
const s = data.summary;
|
|
|
|
|
$('sum-consumed').textContent = fmtKcal(s.total_consumed);
|
|
|
|
|
$('sum-burned').textContent = fmtKcal(s.total_burned);
|
|
|
|
|
const remaining = data.target_kcal - s.net_kcal;
|
|
|
|
|
$('sum-remaining').textContent = fmtKcal(remaining);
|
|
|
|
|
$('sum-remaining').style.color = remaining < 0 ? 'var(--red)' : 'var(--green)';
|
|
|
|
|
|
|
|
|
|
// Progress bar
|
|
|
|
|
const pct = data.target_kcal > 0 ? Math.min(Math.round((s.net_kcal / data.target_kcal) * 100), 150) : 0;
|
|
|
|
|
const barWidth = 40;
|
|
|
|
|
const filled = Math.min(Math.round((pct / 100) * barWidth), barWidth);
|
|
|
|
|
const empty = barWidth - filled;
|
|
|
|
|
const barColor = pct > 100 ? 'var(--red)' : 'var(--green)';
|
|
|
|
|
$('progress-track').innerHTML = `<span style="color:${barColor}">${'='.repeat(filled)}</span><span class="dim">${'-'.repeat(Math.max(0, empty))}</span>`;
|
|
|
|
|
$('progress-pct').textContent = `${pct}% of target`;
|
|
|
|
|
$('progress-pct').style.color = pct > 100 ? 'var(--red)' : 'var(--amber)';
|
|
|
|
|
|
|
|
|
|
// Check if setup needed
|
|
|
|
|
if (!data.config || (!data.config.height_cm && !data.config.age)) {
|
|
|
|
|
showSetup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escHtml(s) {
|
|
|
|
|
const d = document.createElement('div');
|
|
|
|
|
d.textContent = s;
|
|
|
|
|
return d.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Navigate days
|
|
|
|
|
function navigateDay(delta) {
|
|
|
|
|
const d = new Date(currentDate + 'T12:00:00');
|
|
|
|
|
d.setDate(d.getDate() + delta);
|
|
|
|
|
const next = d.toISOString().slice(0, 10);
|
|
|
|
|
if (next > todayStr) return;
|
|
|
|
|
loadDay(next);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add entry
|
|
|
|
|
async function addEntry(desc) {
|
|
|
|
|
if (!desc.trim()) return;
|
|
|
|
|
|
|
|
|
|
$('loading').classList.add('active');
|
|
|
|
|
input.disabled = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const data = await api('POST', '/api/entry', {
|
|
|
|
|
type: currentMode,
|
2026-02-24 12:19:54 +00:00
|
|
|
description: desc.trim(),
|
|
|
|
|
date: currentDate
|
2026-02-23 18:21:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
alert('Error: ' + data.error);
|
|
|
|
|
} else {
|
|
|
|
|
input.value = '';
|
|
|
|
|
await loadDay(currentDate);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert('Network error: ' + e.message);
|
|
|
|
|
} finally {
|
|
|
|
|
$('loading').classList.remove('active');
|
|
|
|
|
input.disabled = false;
|
|
|
|
|
input.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete entry
|
|
|
|
|
async function deleteEntry(id) {
|
|
|
|
|
await api('DELETE', `/api/entry/${id}`);
|
|
|
|
|
loadDay(currentDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear all entries for current day
|
|
|
|
|
async function clearDay() {
|
|
|
|
|
if (!confirm(`Clear all entries for ${fmtDate(currentDate)}?`)) return;
|
|
|
|
|
await api('DELETE', `/api/day/${currentDate}`);
|
|
|
|
|
loadDay(currentDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Autocomplete
|
|
|
|
|
async function fetchAutocomplete(query) {
|
|
|
|
|
if (query.length < 2) {
|
|
|
|
|
hideAutocomplete();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
acResults = await api('GET', `/api/autocomplete?q=${encodeURIComponent(query)}`);
|
|
|
|
|
if (acResults.length > 0) {
|
|
|
|
|
showAutocomplete();
|
|
|
|
|
} else {
|
|
|
|
|
hideAutocomplete();
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
hideAutocomplete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showAutocomplete() {
|
|
|
|
|
const ac = $('autocomplete');
|
|
|
|
|
ac.innerHTML = acResults.map((r, i) => {
|
|
|
|
|
const label = r.brand ? `${r.name} - ${r.brand}` : r.name;
|
|
|
|
|
return `<div class="ac-item${i === acIndex ? ' selected' : ''}" onmousedown="selectAc(${i})">`
|
|
|
|
|
+ `<span class="ac-name">${escHtml(label)}</span>`
|
|
|
|
|
+ `<span class="ac-kcal">${r.kcal_per_serving} kcal</span></div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
ac.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideAutocomplete() {
|
|
|
|
|
$('autocomplete').classList.remove('active');
|
|
|
|
|
acIndex = -1;
|
|
|
|
|
acResults = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectAc(index) {
|
|
|
|
|
if (acResults[index]) {
|
|
|
|
|
const r = acResults[index];
|
|
|
|
|
input.value = r.brand ? `${r.name} (${r.brand})` : r.name;
|
|
|
|
|
hideAutocomplete();
|
|
|
|
|
input.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mode switching
|
|
|
|
|
function setMode(mode) {
|
|
|
|
|
currentMode = mode;
|
|
|
|
|
const labels = { food: '[food]', exercise: '[exercise]' };
|
|
|
|
|
$('mode-label').textContent = labels[mode] || '[food]';
|
|
|
|
|
const placeholders = {
|
|
|
|
|
food: 'ate a banana and a protein shake...',
|
|
|
|
|
exercise: 'ran 5k in 25 minutes...'
|
|
|
|
|
};
|
|
|
|
|
input.placeholder = placeholders[mode] || '';
|
|
|
|
|
input.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Overlays
|
|
|
|
|
function openOverlay(name) {
|
|
|
|
|
$(`${name}-overlay`).classList.add('active');
|
|
|
|
|
if (name === 'history') loadHistory();
|
|
|
|
|
if (name === 'weighin') loadWeightHistory();
|
|
|
|
|
if (name === 'settings') loadSettings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeOverlay(name) {
|
|
|
|
|
$(`${name}-overlay`).classList.remove('active');
|
|
|
|
|
input.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// History
|
|
|
|
|
async function loadHistory() {
|
|
|
|
|
const data = await api('GET', '/api/history');
|
|
|
|
|
const list = $('history-list');
|
|
|
|
|
if (!data || data.length === 0) {
|
|
|
|
|
list.innerHTML = '<div class="dim">No history yet.</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
list.innerHTML = data.map(s => `
|
|
|
|
|
<div class="history-item" onclick="closeOverlay('history');loadDay('${s.date}')">
|
|
|
|
|
<span>${fmtDate(s.date)}</span>
|
|
|
|
|
<span>In: ${fmtKcal(s.total_consumed)} | Out: ${fmtKcal(s.total_burned)} | Net: ${fmtKcal(s.net_kcal)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Weight
|
|
|
|
|
async function loadWeightHistory() {
|
|
|
|
|
const data = await api('GET', '/api/weight-history');
|
|
|
|
|
const list = $('weight-history-list');
|
|
|
|
|
if (!data || data.length === 0) {
|
|
|
|
|
list.innerHTML = '<div class="dim">No weigh-ins yet.</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
list.innerHTML = data.slice(0, 10).map(w => `
|
|
|
|
|
<div class="weight-item">
|
|
|
|
|
<span class="dim">${fmtDate(w.date)}</span>
|
|
|
|
|
<span class="bright">${w.weight_kg.toFixed(1)} kg</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveWeighIn() {
|
|
|
|
|
const weight = parseFloat($('w-weight').value);
|
|
|
|
|
if (!weight || weight <= 0) return;
|
|
|
|
|
|
|
|
|
|
await api('POST', '/api/weighin', { weight_kg: weight });
|
|
|
|
|
closeOverlay('weighin');
|
|
|
|
|
loadDay(currentDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Settings
|
|
|
|
|
async function loadSettings() {
|
|
|
|
|
const data = await api('GET', '/api/settings');
|
|
|
|
|
$('s-weight').value = data.current_weight_kg;
|
|
|
|
|
$('s-target').value = data.target_weight_kg;
|
|
|
|
|
$('s-height').value = data.height_cm;
|
|
|
|
|
$('s-age').value = data.age;
|
|
|
|
|
$('s-sex').value = data.sex;
|
|
|
|
|
$('s-activity').value = data.activity_level;
|
|
|
|
|
$('s-loss').value = data.weekly_loss_kg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveSettings() {
|
|
|
|
|
const apikey = $('s-apikey').value;
|
|
|
|
|
const body = {
|
|
|
|
|
current_weight_kg: parseFloat($('s-weight').value),
|
|
|
|
|
target_weight_kg: parseFloat($('s-target').value),
|
|
|
|
|
height_cm: parseInt($('s-height').value),
|
|
|
|
|
age: parseInt($('s-age').value),
|
|
|
|
|
sex: $('s-sex').value,
|
|
|
|
|
activity_level: $('s-activity').value,
|
|
|
|
|
weekly_loss_kg: parseFloat($('s-loss').value)
|
|
|
|
|
};
|
|
|
|
|
if (apikey) body.anthropic_api_key = apikey;
|
|
|
|
|
|
|
|
|
|
await api('POST', '/api/settings', body);
|
|
|
|
|
closeOverlay('settings');
|
|
|
|
|
loadDay(currentDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Setup
|
|
|
|
|
function showSetup() {
|
|
|
|
|
$('setup-screen').classList.add('active');
|
|
|
|
|
$('main-screen').classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveSetup() {
|
|
|
|
|
const body = {
|
|
|
|
|
anthropic_api_key: $('setup-apikey').value,
|
|
|
|
|
current_weight_kg: parseFloat($('setup-weight').value),
|
|
|
|
|
target_weight_kg: parseFloat($('setup-target').value),
|
|
|
|
|
height_cm: parseInt($('setup-height').value),
|
|
|
|
|
age: parseInt($('setup-age').value),
|
|
|
|
|
sex: $('setup-sex').value,
|
|
|
|
|
activity_level: $('setup-activity').value,
|
|
|
|
|
weekly_loss_kg: parseFloat($('setup-loss').value)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!body.anthropic_api_key) {
|
|
|
|
|
alert('API key is required');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await api('POST', '/api/settings', body);
|
|
|
|
|
$('setup-screen').classList.remove('active');
|
|
|
|
|
$('main-screen').classList.remove('hidden');
|
|
|
|
|
loadDay(todayStr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keyboard handling
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
const overlayActive = document.querySelector('.overlay.active');
|
|
|
|
|
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
if (overlayActive) {
|
|
|
|
|
overlayActive.classList.remove('active');
|
|
|
|
|
input.focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
input.blur();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (overlayActive) return;
|
|
|
|
|
|
|
|
|
|
// If input not focused, handle shortcuts
|
|
|
|
|
if (document.activeElement !== input) {
|
|
|
|
|
switch (e.key) {
|
|
|
|
|
case 'f': setMode('food'); e.preventDefault(); break;
|
|
|
|
|
case 'e': setMode('exercise'); e.preventDefault(); break;
|
|
|
|
|
case 'w': openOverlay('weighin'); e.preventDefault(); break;
|
|
|
|
|
case 'h': openOverlay('history'); e.preventDefault(); break;
|
|
|
|
|
case 's': openOverlay('settings'); e.preventDefault(); break;
|
|
|
|
|
case 'c': clearDay(); e.preventDefault(); break;
|
|
|
|
|
case 'ArrowLeft': navigateDay(-1); e.preventDefault(); break;
|
|
|
|
|
case 'ArrowRight': navigateDay(1); e.preventDefault(); break;
|
|
|
|
|
case '/': case 'i': input.focus(); e.preventDefault(); break;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Input focused
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
if (acIndex >= 0 && acResults[acIndex]) {
|
|
|
|
|
selectAc(acIndex);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (input.value.trim()) {
|
|
|
|
|
addEntry(input.value);
|
|
|
|
|
hideAutocomplete();
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === 'ArrowDown' && acResults.length > 0) {
|
|
|
|
|
acIndex = Math.min(acIndex + 1, acResults.length - 1);
|
|
|
|
|
showAutocomplete();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === 'ArrowUp' && acResults.length > 0) {
|
|
|
|
|
acIndex = Math.max(acIndex - 1, -1);
|
|
|
|
|
showAutocomplete();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === 'Tab') {
|
|
|
|
|
if (acResults.length > 0) {
|
|
|
|
|
if (acIndex < 0) acIndex = 0;
|
|
|
|
|
selectAc(acIndex);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Input event for autocomplete
|
|
|
|
|
input.addEventListener('input', () => {
|
|
|
|
|
clearTimeout(acTimeout);
|
|
|
|
|
acIndex = -1;
|
|
|
|
|
if (currentMode === 'food') {
|
|
|
|
|
acTimeout = setTimeout(() => fetchAutocomplete(input.value), 200);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Focus input on click anywhere on input box
|
|
|
|
|
$('input-box').addEventListener('click', () => input.focus());
|
|
|
|
|
|
|
|
|
|
// Init
|
|
|
|
|
async function init() {
|
|
|
|
|
try {
|
|
|
|
|
const data = await api('GET', '/api/settings');
|
|
|
|
|
if (!data.has_api_key || !data.height_cm || !data.age) {
|
|
|
|
|
showSetup();
|
|
|
|
|
} else {
|
|
|
|
|
$('main-screen').classList.remove('hidden');
|
|
|
|
|
loadDay(todayStr);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
$('main-screen').classList.remove('hidden');
|
|
|
|
|
loadDay(todayStr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|