This file provides guidance when working with code in this repository. The README.md should ALWAYS serve as an accurate, comprehensive piece of documentation for this project. It should describe the broader goals and purpose of this repository along with the technical implementation details. If any aspect of the project changes, the README.md should be updated to reflect that.
This is a Plane Tracker - a standalone web application that displays real-time aircraft information near your location using the adsb.lol API (free, open-source ADS-B data).
- Real-time Aircraft Tracking: Shows planes currently flying within ~100km of your location
- Geolocation-based Search: Automatically detects your location (defaults to central US if unavailable)
- Rate Limit Handling: Smart exponential backoff when encountering API rate limits (429 errors)
- Auto-refresh: Automatically updates data every 60 seconds (adjusts during rate limit cooldowns)
- Manual Refresh: Button to force refresh when rate limit allows
- Detailed Aircraft Info: Shows callsign, ICAO24 code, country, altitude, speed, heading, vertical rate, and position
- Responsive Design: Beautiful gradient UI with cards that work on mobile and desktop
- Error Handling: Clear error messages for rate limits, API errors, and empty results
This is a single-file standalone application (no React, no build process) that runs directly in the browser:
index.html- Complete application with embedded CSS and JavaScript- No backend server required for the core functionality
- No external dependencies (all styling and logic inline)
- Direct API calls to adsb.lol from the browser
The adsb.lol API is free to use. The application handles rate limiting gracefully:
- Default Refresh: Every 60 seconds
- On Rate Limit (429): Exponential backoff starting at 30s, doubling up to max 5 minutes
- Countdown Timer: Shows time until next retry during cooldown
- Manual Refresh: Disabled during rate limit cooldown period
// Backoff progression:
// 1st hit: 30s
// 2nd hit: 60s
// 3rd hit: 120s
// 4th hit: 240s
// 5th+ hit: 300s (max)The application queries the adsb.lol API with a bounding box around your location:
GET https://api.adsb.lol/v2/point/{lat}/{lon}/{radius}
Parameters:
lat/lon: Center point coordinatesradius: Search radius in nautical miles (max 250)
- Modern browser with ES6 support
- Geolocation API support (optional, falls back to default location)
- JavaScript enabled
- No authentication required for adsb.lol API
The site is served via the Zo Site framework (server.ts + Hono) but the tracker itself is static HTML/CSS/JS that could be deployed anywhere:
- Zo Development: Served via Vite middleware mode
- Zo Production: Built to
dist/and served as static files - Standalone: The
index.htmlfile works independently
This is a Zo Site - a web application running on a user's Zo computer that combines:
- Backend: Bun + Hono server with API routes
- Frontend: React + Vite with client-side routing, shadcn/ui components, and Tailwind CSS 4
- Single Process: Vite runs in middleware mode (no separate dev server)
When creating a new site, you can choose from several template variants:
- blank - Minimal starting point with a welcome page
- blog - Blog with markdown post support and listings
- event - Event registration form with SQLite database
- slides - Presentation slides using Reveal.js
- data - Data dashboard with charts (Recharts) and tables
- marketing - Professional landing page with hero, features, pricing
The variant is set via the VITE_ZO_SITE_DEMO_VARIANT environment variable in zosite.json. All variant demo components are included in the template at src/pages/demos/ and the root route shows the selected variant.
To change or remove the demo:
- Delete
src/pages/demos/directory - Update
src/App.tsxto remove variant routing - Create your own pages and routes
.
├── server.ts # Main server (Hono + Vite middleware)
├── index.html # HTML entry point for React
├── vite.config.ts # Vite configuration
├── package.json # Dependencies and scripts
├── zosite.json # Zo deployment config (ports, env vars)
├── public/ # Static assets (images, fonts, favicon)
│ ├── favicon.svg # Site favicon (replace with your own)
│ └── images/
│ └── pegasus.png # Example image (loaded via <img src="/images/pegasus.png">)
├── backend-lib/
│ └── zo-api.ts # Helper for calling Zo API
└── src/
├── main.tsx # React entry point
├── App.tsx # Router setup with variant routing
├── styles.css # Global styles
└── pages/
├── Home.tsx # Original home page
├── Dashboard.tsx # Example dashboard with charts
└── demos/ # Variant demo components
├── blank-demo.tsx
├── blog-demo.tsx
├── event-demo.tsx
├── slides-demo.tsx
├── data-demo.tsx
└── marketing-demo.tsx
Development Mode (bun run dev):
- Single Bun process running
server.ts - Vite in middleware mode transforms files on-the-fly
- API routes:
/api/*handled by Hono - React app: served via Vite transforms (HMR disabled, use
bun --hotfor server restart) - Client-side routing: any non-API, non-file route falls back to
index.html - Environment: Site runs at an internal authenticated URL accessible only to you (private site on your Zo computer)
Production Mode (bun run prod):
- Builds React app to
dist/using Vite - Bun serves static files from
dist/viahono/bunserveStatic - API routes still handled by Hono
- SPA fallback: all non-API routes serve
dist/index.html - Environment: Site is published and accessible to anyone on the internet at a public URL
NEVER use the scripts bun run dev or bun run prod. The Zo system handles running the site in the correct mode based on context. All process management of the server is handled by Zo. Never restart or stop the server manually.
The agent-browser CLI tool lets you preview, navigate, and debug the site running at http://localhost:$PORT (PORT is set by Zo). Use it to verify UI changes, debug routing, or capture screenshots.
Core workflow:
- Navigate to the site:
agent-browser open http://localhost:$PORT - Snapshot the page to get interactive element refs:
agent-browser snapshot -i
- Interact with elements:
agent-browser click @e1 agent-browser fill @e2 "text" agent-browser hover @e3 agent-browser get text @e1 - Re-snapshot after page changes to get updated refs.
Taking screenshots:
agent-browser screenshot
agent-browser screenshot --full-page
agent-browser screenshot --filename debug.pngFor the full list of commands and options, run:
agent-browser --helpNote: Do not tell the user to visit localhost; they already have access via the Zo preview iframe.
This application uses:
- Bun as the runtime (NOT Node.js)
- Hono as the web framework (NOT Express)
Do not use Express patterns. Use Hono equivalents. For file system operations, see the section below.
- JavaScript runtime (NOT Node.js or Deno)
- Use
bun add <package>to install dependencies - Built-in TypeScript support
- Built-in SQLite via
import { Database } from "bun:sqlite" - Process spawning:
Bun.spawn()for running commands
Bun has native APIs for file I/O but uses Node.js APIs for directory operations. Use the correct API for each operation:
| Operation | API | Example |
|---|---|---|
| Read file | Bun.file() |
await Bun.file("data.json").text() |
| Write file | Bun.write() |
await Bun.write("out.txt", content) |
| File exists | Bun.file().exists() |
await Bun.file("x.txt").exists() |
| Read directory | node:fs/promises |
await readdir("./posts") |
| Create directory | node:fs/promises |
await mkdir("dir", { recursive: true }) |
| Glob files | Bun Glob |
new Glob("**/*.md").scan(".") |
// ❌ WRONG - These do NOT exist:
Bun.readdir() // No such API
Bun.readdirSync() // No such API
Bun.mkdir() // No such API
fs.readFileSync() // Works but slower than Bun.file()
// ✅ CORRECT patterns:
import { readdir, mkdir } from "node:fs/promises";
// Reading a file
const content = await Bun.file("config.json").json();
// Writing a file
await Bun.write("output.txt", "Hello");
// Listing directory contents
const files = await readdir("./posts");
// Creating a directory
await mkdir("./uploads", { recursive: true });
// Finding files by pattern
import { Glob } from "bun";
const glob = new Glob("**/*.md");
for await (const file of glob.scan("./posts")) {
console.log(file);
}- Lightweight web framework designed for Bun
- Documentation: https://honojs.dev/llms-small.txt
- Import from
honofor core,hono/bunfor Bun-specific features likeserveStatic
Serving Static Files (Bun-specific):
import { serveStatic } from 'hono/bun'
app.use('/static/*', serveStatic({ root: './' }))
app.use('/favicon.ico', serveStatic({ path: './favicon.ico' }))
app.get('*', serveStatic({ path: './static/fallback.txt' }))
// You can reach outside the project root to files in the user's workspace
app.get('/workspace-file', serveStatic({ path: '../some/dir/file.txt' }))
app.get('/absolute-file', serveStatic({ path: '/home/user/file.txt' }))
// Custom MIME types
app.get('/media/*', serveStatic({
mimes: {
m3u8: 'application/vnd.apple.mpegurl',
ts: 'video/mp2t',
},
}))Hono Routing:
// REST API endpoints
app.get('/', (c) => c.json({ items: [] }))
app.post('/', (c) => c.json({ created: true }, 201))
app.get('/:id', (c) => c.json({ id: c.req.param('id') }))
// Middleware
import { basicAuth } from 'hono/basic-auth'
app.use('/admin/*', basicAuth({ username: 'admin', password: 'secret' }))
// Multiple middlewares are processed in order
app.use(logger())
app.use('/posts/*', cors())
app.post('/posts/*', basicAuth())- React for UI components
- Vite handles bundling and transforms
- Dependencies installed via
bun add(NOT CDN imports) - all packages bundled by Vite - React Router for client-side routing
- Styling: Tailwind CSS 4 configured with
@tailwindcss/viteplugin - UI Components: shadcn/ui already set up and configured - components can be added via
bunx shadcn@latest add <component-name> - Icons: Lucide React icons included and ready to use
- Sample Implementation: See
src/pages/Dashboard.tsxfor an example of shadcn/ui components in use (sidebar, charts, data tables, etc.) - Charts Documentation: See
docs/shadcncharts.mdfor comprehensive chart examples, tooltip configuration, and common patterns. This is REQUIRED reading before building any charts.
Add routes in server.ts before the Vite middleware:
app.get("/api/example", async (c) => {
return c.json({ data: "example" });
});Create components in src/:
// src/components/MyComponent.tsx
import React from "react";
export default function MyComponent() {
return <div>Hello</div>;
}Add routes in src/App.tsx:
import MyPage from "./pages/MyPage";
<Routes>
<Route path="/my-page" element={<MyPage />} />
</Routes>Use the helper in backend-lib/zo-api.ts:
import { callZo } from "./backend-lib/zo-api";
app.post("/api/ask-zo", async (c) => {
const { question } = await c.req.json();
const result = await callZo(question, {
outputFormat: {
type: "object",
properties: { answer: { type: "string" } },
required: ["answer"]
}
});
return c.json(result);
});There are two ways to include static assets like images, fonts, or JSON data:
Place files in the public/ directory. They're served at the root URL path and work identically in dev and production.
public/
├── favicon.svg
├── images/
│ ├── logo.png
│ └── hero.jpg
├── fonts/
│ └── custom.woff2
└── og-image.jpg
Reference them with absolute paths:
<img src="/images/logo.png" alt="Logo" />
<link rel="icon" href="/favicon.svg" />In production, Vite copies the public/ folder contents to dist/ automatically.
Use public/ for: favicons, Open Graph images, downloadable files, fonts, any asset that needs a stable/predictable URL.
Import assets directly in your React components. Vite handles bundling, optimization, and cache-busting via content hashes.
// Images
import heroImage from '@/assets/hero.png';
function Hero() {
return <img src={heroImage} alt="Hero" />;
}
// JSON data
import config from '@/data/config.json';
function Settings() {
return <div>App version: {config.version}</div>;
}
// SVG as component (with ?react suffix)
import Logo from '@/assets/logo.svg?react';
function Header() {
return <Logo className="h-8 w-8" />;
}Place imported assets in src/assets/ or alongside components:
src/
├── assets/
│ ├── hero.png
│ └── logo.svg
├── data/
│ └── config.json
└── components/
└── Header.tsx
Use imports for: component-specific images, icons used in JSX, JSON configuration, any asset that benefits from bundling/tree-shaking.
For files outside the project (e.g., user's workspace files), create an API route:
app.get("/myfile", async (c) => {
const file = Bun.file("/path/to/file");
return new Response(file);
});This application is database-agnostic and doesn't include a database by default. For most use cases, SQLite is recommended.
Using Bun's Built-in SQLite:
import { Database } from "bun:sqlite";
// Create/open database
const db = new Database("mydb.sqlite");
// Create table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
// Insert data
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("John Doe", "john@example.com");
// Query data
const query = db.query("SELECT * FROM users WHERE name = ?");
const users = query.all("John Doe");
// Close when done
db.close();In a Hono route:
app.get("/api/users", (c) => {
const db = new Database("mydb.sqlite");
const users = db.query("SELECT * FROM users").all();
db.close();
return c.json({ users });
});
app.post("/api/users", async (c) => {
const { name, email } = await c.req.json();
const db = new Database("mydb.sqlite");
try {
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run(name, email);
db.close();
return c.json({ success: true }, 201);
} catch (error) {
db.close();
return c.json({ error: "Failed to create user" }, 400);
}
});bunx tsc --noEmit- Type check
- Server code:
server.ts,backend-lib/- runs on Bun - Client code:
src/- runs in browser, bundled by Vite - Install ALL dependencies via
bun add(React, etc.) - Vite bundles them
NODE_ENV=productionswitches to production modeZO_CLIENT_IDENTITY_TOKENrequired for calling Zo APIVITE_ZO_SITE_DEMO_VARIANTdetermines which demo variant is shown (blank, blog, event, slides, data, marketing)- Access server vars via
process.env.VAR_NAMEin server code - Access client vars prefixed with
VITE_viaimport.meta.env.VITE_VAR_NAMEin React code
The server runs on the user's Zo computer and can:
- Read/write any file on the system
- Execute commands via
Bun.spawn() - Access local databases
zosite.json defines:
{
"name": "My Site",
"local_port": 12345,
"entrypoint": "bun run dev",
"env": {
"VITE_ZO_SITE_DEMO_VARIANT": "blank"
},
"publish": {
"label": "My Site",
"type": "http",
"entrypoint": "bun run prod",
"published_port": 12346,
"env": {
"NODE_ENV": "production",
"ZO_CLIENT_IDENTITY_TOKEN": "none",
"VITE_ZO_SITE_DEMO_VARIANT": "blank"
}
}
}- Top-level
env: Environment variables for development mode publish.env: Environment variables for production mode- Variables prefixed with
VITE_are exposed to client-side code via Vite PORTenvironment variable is automatically set to matchlocal_port(orpublished_portin production)
The zosite.json file is auto-generated by Zo. Most fields should not be manually edited.
local_portandpublished_portare assigned by the system when the site is created- Ports are chosen using a hash-based algorithm to avoid conflicts
- The Zo system manages process lifecycle, tunneling, and URL routing based on these ports
- Editing ports or entrypoints will break the site's preview URL and publish functionality
Safe to edit:
name- The display name for the siteenvandpublish.env- Add or modify environment variables as needed
Never edit:
local_port,published_port- System-assigned portsentrypoint,publish.entrypoint- Managed startup commandslabel,type- Service configuration
Private vs Public Access:
- Private (default): Sites run in dev mode behind authentication. Only you can access them via the preview iframe in Zo. This is the normal development experience.
- Public (published): Publishing creates a shareable URL that anyone on the internet can access without authentication.
To publish your site publicly, use the Publish button in the Zo UI or explicitly ask Zo to publish it (e.g., "publish this site", "make it public").
The site exports { fetch, port } from server.ts for Zo's deployment system. The same code runs in both dev and production - mode is controlled by NODE_ENV.