Milestones 0 and 1: foundation and money in (20261007.10)
Foundation: single arm64 container (Hono + SQLite via Drizzle, React + Vite), local sign-in with long-lived sessions and passkeys, roles (read-only, full access, admin), nightly and pre-upgrade backups, light and dark themes, date-based version numbers, and the ideas lightbulb behind IDEAS_ENABLED. Money in: accounts with balances, CSV import with bank detection, a column mapping pop-up for unknown layouts, duplicate checks, transfer pairing and undo; one-tap review with learned rules; activity with search, splits, tags, notes and cash entries. Statement files are blocked from commits by a pre-commit hook; tests use synthetic fixtures only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
18
web/public/favicon.svg
Normal file
18
web/public/favicon.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="beam" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#FBBF24" stop-opacity="0.95"/>
|
||||
<stop offset="1" stop-color="#F97362" stop-opacity="0.18"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="pool" cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0" stop-color="#FCD34D"/>
|
||||
<stop offset="0.55" stop-color="#F97362" stop-opacity="0.6"/>
|
||||
<stop offset="1" stop-color="#F97362" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="116" fill="#0B1020"/>
|
||||
<path d="M222 140h68l110 240a144 40 0 0 1-288 0z" fill="url(#beam)"/>
|
||||
<ellipse cx="256" cy="380" rx="168" ry="50" fill="url(#pool)"/>
|
||||
<rect x="204" y="96" width="104" height="52" rx="20" fill="#818CF8"/>
|
||||
<rect x="230" y="78" width="52" height="24" rx="10" fill="#6366F1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 898 B |
BIN
web/public/icons/apple-touch-icon.png
Normal file
BIN
web/public/icons/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
BIN
web/public/icons/icon-192.png
Normal file
BIN
web/public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
web/public/icons/icon-512.png
Normal file
BIN
web/public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
web/public/icons/icon-maskable-512.png
Normal file
BIN
web/public/icons/icon-maskable-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
16
web/public/manifest.webmanifest
Normal file
16
web/public/manifest.webmanifest
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Spendlight",
|
||||
"short_name": "Spendlight",
|
||||
"description": "Your household's money, in the spotlight.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#0B1020",
|
||||
"theme_color": "#0B1020",
|
||||
"icons": [
|
||||
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||
57
web/public/sw.js
Normal file
57
web/public/sw.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// Spendlight service worker. Makes the home-screen app open instantly and
|
||||
// show its shell offline. It never touches /api: household data is always
|
||||
// fetched fresh and never stored here.
|
||||
|
||||
const SHELL = 'spendlight-shell';
|
||||
const ASSETS = 'spendlight-assets';
|
||||
const MAX_ASSETS = 80;
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting());
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) => Promise.all(keys.filter((k) => k !== SHELL && k !== ASSETS).map((k) => caches.delete(k))))
|
||||
.then(() => self.clients.claim()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const request = event.request;
|
||||
const url = new URL(request.url);
|
||||
if (request.method !== 'GET' || url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
|
||||
|
||||
// Pages: always try the network first so a new version shows up at once.
|
||||
if (request.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
const copy = response.clone();
|
||||
caches.open(SHELL).then((cache) => cache.put('/', copy));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match('/').then((cached) => cached || Response.error())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Versioned files never change, so a cached copy is always right.
|
||||
if (url.pathname.startsWith('/assets/')) {
|
||||
event.respondWith(
|
||||
caches.open(ASSETS).then(async (cache) => {
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
await cache.put(request, response.clone());
|
||||
const keys = await cache.keys();
|
||||
if (keys.length > MAX_ASSETS) await Promise.all(keys.slice(0, keys.length - MAX_ASSETS).map((k) => cache.delete(k)));
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
9
web/public/theme-init.js
Normal file
9
web/public/theme-init.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Runs before the page paints, so dark mode never flashes white.
|
||||
(function () {
|
||||
try {
|
||||
var choice = localStorage.getItem('sl-theme') || 'system';
|
||||
var dark = choice === 'dark' || (choice === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.classList.toggle('dark', dark);
|
||||
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
||||
} catch (e) {}
|
||||
})();
|
||||
Reference in New Issue
Block a user