Skip to content

Repository files navigation

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.

Project Notes

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).

Features

  • 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

Architecture

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

Rate Limiting

The adsb.lol API is free to use. The application handles rate limiting gracefully:

  1. Default Refresh: Every 60 seconds
  2. On Rate Limit (429): Exponential backoff starting at 30s, doubling up to max 5 minutes
  3. Countdown Timer: Shows time until next retry during cooldown
  4. 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)

API Usage

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 coordinates
  • radius: Search radius in nautical miles (max 250)

Browser Requirements

  • Modern browser with ES6 support
  • Geolocation API support (optional, falls back to default location)
  • JavaScript enabled
  • No authentication required for adsb.lol API

Deployment

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.html file works independently

Documentation

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)

Template Variants

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:

  1. Delete src/pages/demos/ directory
  2. Update src/App.tsx to remove variant routing
  3. Create your own pages and routes

File Structure

.
├── 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 vs Production

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 --hot for 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/ via hono/bun serveStatic
  • 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.

Viewing, Verification, and Debugging (agent-browser)

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:

  1. Navigate to the site:
    agent-browser open http://localhost:$PORT
  2. Snapshot the page to get interactive element refs:
    agent-browser snapshot -i
  3. Interact with elements:
    agent-browser click @e1
    agent-browser fill @e2 "text"
    agent-browser hover @e3
    agent-browser get text @e1
  4. Re-snapshot after page changes to get updated refs.

Taking screenshots:

agent-browser screenshot
agent-browser screenshot --full-page
agent-browser screenshot --filename debug.png

For the full list of commands and options, run:

agent-browser --help

Note: Do not tell the user to visit localhost; they already have access via the Zo preview iframe.

Key Technologies

⚠️ IMPORTANT: This is BUN + HONO (NOT Node.js + Express)

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.

Bun Runtime

  • 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

File System Operations

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(".")

⚠️ Common Mistakes to Avoid:

// ❌ 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);
}

Hono Framework

  • Lightweight web framework designed for Bun
  • Documentation: https://honojs.dev/llms-small.txt
  • Import from hono for core, hono/bun for Bun-specific features like serveStatic

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 + Vite

  • 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/vite plugin
  • 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.tsx for an example of shadcn/ui components in use (sidebar, charts, data tables, etc.)
  • Charts Documentation: See docs/shadcncharts.md for comprehensive chart examples, tooltip configuration, and common patterns. This is REQUIRED reading before building any charts.

Common Tasks

Adding API Routes

Add routes in server.ts before the Vite middleware:

app.get("/api/example", async (c) => {
  return c.json({ data: "example" });
});

Adding React Components

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>

Calling Zo API from Backend

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);
});

Static Assets

There are two ways to include static assets like images, fonts, or JSON data:

Option 1: The public/ Folder (Recommended for Most Cases)

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.

Option 2: Import in Components (Bundled Assets)

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.

Serving Files from the Workspace

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);
});

Database

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);
  }
});

Scripts

  • bunx tsc --noEmit - Type check

Important Notes

Server-Side vs Client-Side

  • 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

Environment Variables

  • NODE_ENV=production switches to production mode
  • ZO_CLIENT_IDENTITY_TOKEN required for calling Zo API
  • VITE_ZO_SITE_DEMO_VARIANT determines which demo variant is shown (blank, blog, event, slides, data, marketing)
  • Access server vars via process.env.VAR_NAME in server code
  • Access client vars prefixed with VITE_ via import.meta.env.VITE_VAR_NAME in React code

File System Access

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

Configuration

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
  • PORT environment variable is automatically set to match local_port (or published_port in production)

⚠️ IMPORTANT: Do Not Edit zosite.json System Fields

The zosite.json file is auto-generated by Zo. Most fields should not be manually edited.

  • local_port and published_port are 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 site
  • env and publish.env - Add or modify environment variables as needed

Never edit:

  • local_port, published_port - System-assigned ports
  • entrypoint, publish.entrypoint - Managed startup commands
  • label, 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").

Deployment

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages