Public
LikecounterTown
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v127View latest version
Modernize the project structure to align with Val Town best practices.
backend/
├── index.http.ts # Hono app entry, exports app.fetch
├── database/
│ ├── migrations.ts # CREATE TABLE IF NOT EXISTS statements
│ └── queries.ts # trackPageView, getAnalytics, cleanupOldData
├── routes/
│ ├── tracking.ts # /trackPageView, /pixel.gif, /trackingScript
│ ├── stream.ts # SSE /stream endpoint
│ └── static.ts # serveFile for frontend assets
└── lib/
└── CounterTown.ts # CounterTown class + config
frontend/
├── index.html # Extracted from counterTown.tsx
├── index.tsx # App entry (from counterTownClient.tsx)
├── deps.ts # Centralized React 18.2.0 version pins
├── components/
│ ├── Dashboard.tsx
│ ├── Navigation.tsx
│ ├── ConnectionStatus.tsx
│ ├── charts/
│ │ ├── ChartCard.tsx
│ │ └── ChartsSection.tsx
│ └── sections/
│ ├── OverallStatsSection.tsx
│ ├── SitesSection.tsx
│ ├── StatCard.tsx
│ ├── TopPathsSection.tsx
│ ├── TopReferrersSection.tsx
│ ├── VisitorGlobe.tsx
│ └── VisitorOriginsSection.tsx
└── contexts/
└── AnalyticsContext.tsx
shared/
├── types.ts # PageView, SiteStats, AnalyticsData interfaces
└── utils.ts # formatSiteName (browser-safe, no Deno APIs)
README.md
AGENTS.md
ARCHITECTURE.md
Current code uses React 17.0.2. Must upgrade and pin consistently.
frontend/deps.ts:
export { default as React } from "https://esm.sh/react@18.2.0";
export { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
export {
BrowserRouter,
Routes,
Route,
useParams
} from "https://esm.sh/react-router-dom@6.22.0?deps=react@18.2.0,react-dom@18.2.0";
All frontend .tsx files must start with:
/** @jsxImportSource https://esm.sh/react@18.2.0 */
Replace external esm.town imports with relative paths:
- import { initAnalytics } from 'https://esm.town/v/iamseeley/counterTownDb';
+ import { initAnalytics } from './database/queries.ts';
backend/index.http.ts:
import { Hono } from "npm:hono@4";
import { serveFile, readFile } from "https://esm.town/v/std/utils@85-main/index.ts";
import tracking from "./routes/tracking.ts";
import stream from "./routes/stream.ts";
const app = new Hono();
// Re-throw errors for full stack traces
app.onError((err, c) => {
throw err;
});
// API routes first
app.route("/", tracking);
app.route("/", stream);
// Static assets
app.get("/frontend/*", (c) => serveFile(c.req.path, import.meta.url));
app.get("/shared/*", (c) => serveFile(c.req.path, import.meta.url));
// SPA fallback (must be last)
app.get("*", async (c) => {
let html = await readFile("/frontend/index.html", import.meta.url);
// Optional: inject initial data safely
return c.html(html);
});
export default app.fetch;
backend/routes/stream.ts improvements:
- Add heartbeat every 15-30s:
:\n\norevent: ping - Add header:
X-Accel-Buffering: no - Increase poll interval from 5s to 10-30s
- Keep abort handler for cleanup
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
// Send initial data
const data = await getAnalytics();
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
// Heartbeat every 20s
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
}, 20000);
// Data refresh every 15s (reduced from 5s)
const refresh = setInterval(async () => {
const data = await getAnalytics();
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
}, 15000);
req.signal.addEventListener('abort', () => {
clearInterval(heartbeat);
clearInterval(refresh);
controller.close();
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
'Access-Control-Allow-Origin': '*',
},
});
Use JSON script tag instead of inline JS:
<!-- In frontend/index.html --> <script type="application/json" id="initial-data"></script>
// In backend/index.http.ts
const initialData = await getInitialData();
html = html.replace(
'<script type="application/json" id="initial-data"></script>',
`<script type="application/json" id="initial-data">${JSON.stringify(initialData)}</script>`
);
// In frontend/index.tsx
const initialData = JSON.parse(
document.getElementById('initial-data')?.textContent || '{}'
);
shared/types.ts:
export interface PageView {
site: string;
path: string;
timestamp: string;
referrer: string;
userAgent: string;
browser: string;
os: string;
screenSize: string;
language: string;
country?: string;
customData?: Record<string, unknown>;
}
export interface PathStats {
views: number;
referrers: Record<string, number>;
browsers: Record<string, number>;
os: Record<string, number>;
screenSizes: Record<string, number>;
languages: Record<string, number>;
countries: Record<string, number>;
lastUpdated: string;
viewTimestamps: string[];
}
export interface SiteStats {
totalViews: number;
paths: Record<string, PathStats>;
}
export type AnalyticsData = Record<string, SiteStats>;
backend/database/migrations.ts:
- Keep
CREATE TABLE IF NOT EXISTS(idempotent) - Run once per warm instance via module-level lazy init
- Use
sqlite.batch([...], "write")for atomic operations
backend/database/queries.ts:
- Export typed query functions
- Consistent error handling (don't mix return values and throws)
- Use parameterized queries for all user input
- Create
backend/,frontend/,shared/directories - Move files to appropriate locations
- Update all import paths
- Create Hono app in
backend/index.http.ts - Split routes into separate modules
- Implement
serveFilefor static assets - Add SPA fallback route
- Create
frontend/deps.tswith pinned React 18.2.0 - Update all components to use local imports
- Extract HTML to
frontend/index.html - Update to
createRootAPI
- Add SSE heartbeat and longer intervals
- Implement XSS-safe data injection
- Add proper TypeScript types throughout
| Scope | Time |
|---|---|
| File reorganization only | 1-3 hours |
| Full modernization | 1-2 days |
| Risk | Severity | Mitigation |
|---|---|---|
| React version mismatch | High | Centralize pins in deps.ts |
| SSE connection drops | Medium | Add heartbeat, longer intervals |
| XSS in data injection | Medium | Use JSON script tag pattern |
| Breaking existing tracking | High | Test with allowedOrigins before deploying |
- Dashboard loads at
/ - Site-specific view loads at
/:sitename - SSE stream connects and receives updates
- Tracking script loads from
/trackingScript - Page views are recorded via
/trackPageView - Pixel tracking works via
/pixel.gif - Dark/light theme toggle works
- All charts render correctly