+
+Thanks to all our contributors! Your contributions make this project better every day.
+
+[](https://github.com/Huynhthuongg/AGENTS.md/graphs/contributors)
+
+
+
+
+
+---
+
+## 📖 Documentation
+
+- 📄 [**Product Specification**](docs/SPECIFICATION.md)
+- 🏗️ [**Architecture Overview**](docs/ARCHITECTURE.md)
+- 📝 [**API Documentation**](docs/API.md)
+- 🔄 [**Changelog**](docs/CHANGELOG.md)
+
+---
+
+## 🐛 Issues & Feedback
+
+Found a bug? Have a feature request? We'd love to hear from you!
+
+- 🐛 [Report a Bug](https://github.com/Huynhthuongg/AGENTS.md/issues/new?template=bug_report.md)
+- ✨ [Request a Feature](https://github.com/Huynhthuongg/AGENTS.md/issues/new?template=feature_request.md)
+- 💬 [Start a Discussion](https://github.com/Huynhthuongg/AGENTS.md/discussions/new)
+
+---
+
+## 📄 License
+
+This project is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0).
+
+This means:
+- ✅ You can use, modify, and distribute this software
+- ✅ You must share your modifications if you use it
+- ✅ You must include the original license
+
+See [LICENSE](LICENSE) for full details.
+
+---
+
+
-## Current release
+### 🚀 Ready to Get Started?
-- Version: 0.1.1
-- Release date: 2026-06-02
-- Release focus: CLI dry-run previews, safe output directory validation, dashboard route alignment, and test workflow hardening.
+[📖 Read the Docs](docs/SPECIFICATION.md) • [💻 Clone the Repo](https://github.com/Huynhthuongg/AGENTS.md) • [⭐ Star on GitHub](https://github.com/Huynhthuongg/AGENTS.md)
-## Security model
+**Made with ❤️ by [Huynhthuongg](https://github.com/Huynhthuongg)**
-- Never hardcode secrets in generated output.
-- Redact common API key, token, secret, and password patterns from persisted requirement snapshots.
-- Reject unsafe generated paths and unsafe API `output_dir` values.
-- Add conservative HTTP headers to API responses.
+
-## License
+
+
+
-AGPL-3.0-only. See [LICENSE](LICENSE).
+
From 00da7acca5bea2249fb5d702b9f114c93e109f56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Wed, 3 Jun 2026 18:22:16 +0700
Subject: [PATCH 03/13] Update pyproject.toml
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 81b73d2..72361d0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,7 @@ dev = [
"ruff>=0.6,<1.0",
"httpx>=0.28,<1.0",
"httpx2>=2.3,<3.0",
- "codespell>=2.3,<3.0"
+ "codespell[toml]>=2.3,<3.0"
]
[project.scripts]
From cd0e43f9c2eb918376d920d4061147404335c859 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Wed, 3 Jun 2026 18:22:31 +0700
Subject: [PATCH 04/13] Update README.md
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 625557c..92c9ed7 100644
--- a/README.md
+++ b/README.md
@@ -265,7 +265,7 @@ Thanks to all our contributors! Your contributions make this project better ever
- 📄 [**Product Specification**](docs/SPECIFICATION.md)
- 🏗️ [**Architecture Overview**](docs/ARCHITECTURE.md)
-- 📝 [**API Documentation**](docs/API.md)
+- 📝 [**API Examples**](#-api-examples)
- 🔄 [**Changelog**](docs/CHANGELOG.md)
---
From 8a06fc6432c58e0468664a18178f67350e72c151 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Wed, 3 Jun 2026 18:41:47 +0700
Subject: [PATCH 05/13] Revert "Merge pull request #25 from Huynhthuongg/main"
From 99a58945e1c038dbd733cba6607db1c1ced4f528 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Wed, 3 Jun 2026 18:54:14 +0700
Subject: [PATCH 06/13] fix: remove non-existent httpx2 dependency
- Remove httpx2>=2.3,<3.0 from pyproject.toml (line 24)
- Remove httpx2 from check.sh module validation (line 11)
- Keep httpx>=0.28,<1.0 which is the correct package
Fixes dependency resolution errors in CI/CD and local setup.
---
pyproject.toml | 3 +--
scripts/check.sh | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
mode change 100755 => 100644 scripts/check.sh
diff --git a/pyproject.toml b/pyproject.toml
index 72361d0..a3a0300 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,7 +21,6 @@ dev = [
"pytest>=8.0,<9.0",
"ruff>=0.6,<1.0",
"httpx>=0.28,<1.0",
- "httpx2>=2.3,<3.0",
"codespell[toml]>=2.3,<3.0"
]
@@ -29,7 +28,7 @@ dev = [
upca = "universal_compiler_agent.cli:main"
[tool.setuptools]
-package-dir = {"" = "app"}
+package-dir = {"": "app"}
[tool.setuptools.packages.find]
where = ["app"]
diff --git a/scripts/check.sh b/scripts/check.sh
old mode 100755
new mode 100644
index 310b178..3118e1e
--- a/scripts/check.sh
+++ b/scripts/check.sh
@@ -8,7 +8,7 @@ for command in ruff codespell; do
fi
done
-for module in pytest httpx2 httpx; do
+for module in pytest httpx; do
if ! python -c "import ${module}" >/dev/null 2>&1; then
missing="$missing $module"
fi
From e931dd96d03b2222b8c2a84b78b0490f211f72fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Wed, 3 Jun 2026 19:01:58 +0700
Subject: [PATCH 07/13] feat: add weather dashboard application
- Add Weather Dashboard with real-time weather fetching
- Integrate OpenWeatherMap API for global weather data
- Implement responsive HTML5 UI with modern design
- Add city search functionality with autocomplete
- Add save/favorite cities with SQLite database
- Include comprehensive weather details (temperature, humidity, wind, pressure, sunrise/sunset)
- Add security headers and error handling
- Responsive design for mobile, tablet, and desktop
- FastAPI with async/await pattern
- Complete API documentation
Files added:
- weather_dashboard/config.py - Configuration management
- weather_dashboard/models.py - Data models (WeatherData, SavedCity, etc.)
- weather_dashboard/weather_service.py - OpenWeatherMap API integration
- weather_dashboard/database.py - SQLite database operations
- weather_dashboard/app.py - FastAPI application and routes
- weather_dashboard/templates.py - Beautiful HTML dashboard
- weather_dashboard/__init__.py - Package initialization
- config/weather.env.example - Environment configuration example
- README_WEATHER.md - Complete documentation
---
README_WEATHER.md | 230 ++++++++++++
config/weather.env.example | 16 +
weather_dashboard/__init__.py | 3 +
weather_dashboard/app.py | 211 +++++++++++
weather_dashboard/config.py | 38 ++
weather_dashboard/database.py | 119 ++++++
weather_dashboard/models.py | 105 ++++++
weather_dashboard/templates.py | 541 +++++++++++++++++++++++++++
weather_dashboard/weather_service.py | 117 ++++++
9 files changed, 1380 insertions(+)
create mode 100644 README_WEATHER.md
create mode 100644 config/weather.env.example
create mode 100644 weather_dashboard/__init__.py
create mode 100644 weather_dashboard/app.py
create mode 100644 weather_dashboard/config.py
create mode 100644 weather_dashboard/database.py
create mode 100644 weather_dashboard/models.py
create mode 100644 weather_dashboard/templates.py
create mode 100644 weather_dashboard/weather_service.py
diff --git a/README_WEATHER.md b/README_WEATHER.md
new file mode 100644
index 0000000..6f769ee
--- /dev/null
+++ b/README_WEATHER.md
@@ -0,0 +1,230 @@
+# 🌤️ Weather Dashboard
+
+A modern, responsive weather dashboard that fetches real-time weather data from OpenWeatherMap API.
+
+## ✨ Features
+
+- 🌍 **Search Cities Worldwide** - Find weather for any city globally
+- 🌤️ **Real-time Weather Data** - Current conditions with temperature, humidity, wind, pressure
+- ⭐ **Save Favorites** - Bookmark your favorite cities to SQLite
+- 📱 **Responsive Design** - Works perfectly on mobile, tablet, and desktop
+- 🔒 **Security** - HTTP security headers, input validation
+- ⚡ **Fast & Async** - Built with FastAPI and aiohttp
+- 🎨 **Modern UI** - Beautiful gradient design with smooth animations
+
+## 🚀 Quick Start
+
+### Prerequisites
+
+- Python 3.10+
+- OpenWeatherMap API key (free tier available)
+
+### Installation
+
+1. Clone and setup
+```bash
+git clone https://github.com/Huynhthuongg/AGENTS.md.git
+cd AGENTS.md
+git checkout feature/weather-dashboard
+```
+
+2. Install dependencies
+```bash
+pip install fastapi uvicorn aiohttp
+```
+
+3. Get your free OpenWeatherMap API key
+- Visit https://openweathermap.org/api
+- Sign up for a free account
+- Generate an API key
+
+4. Set up environment
+```bash
+cp config/weather.env.example .env
+# Edit .env and add your OpenWeatherMap API key
+```
+
+5. Run the application
+```bash
+export OPENWEATHER_API_KEY="your_api_key_here"
+python -m weather_dashboard.app
+```
+
+Open your browser: **http://127.0.0.1:8001**
+
+## 📋 API Endpoints
+
+### `GET /`
+Serves the main dashboard HTML.
+
+### `GET /health`
+Health check endpoint.
+```json
+{"status": "ok", "version": "1.0.0"}
+```
+
+### `GET /api/weather`
+Get current weather for coordinates.
+
+**Parameters:**
+- `lat` (float, required) - Latitude
+- `lon` (float, required) - Longitude
+
+**Response:**
+```json
+{
+ "city": "London",
+ "country": "GB",
+ "temperature": 15.5,
+ "feels_like": 14.2,
+ "temp_min": 13.0,
+ "temp_max": 17.0,
+ "humidity": 72,
+ "pressure": 1013,
+ "wind_speed": 3.5,
+ "wind_deg": 240,
+ "cloudiness": 60,
+ "description": "partly cloudy",
+ "icon": "02d",
+ "icon_url": "https://openweathermap.org/img/wn/02d@2x.png",
+ "sunrise": "2026-06-03T05:30:00",
+ "sunset": "2026-06-03T21:15:00"
+}
+```
+
+### `GET /api/search`
+Search for cities.
+
+**Parameters:**
+- `q` (string, required, min 2 chars) - City name
+- `limit` (int, optional, default 5) - Max results (1-20)
+
+**Response:**
+```json
+[
+ {
+ "name": "London",
+ "latitude": 51.5085,
+ "longitude": -0.1257,
+ "country": "GB",
+ "state": null
+ }
+]
+```
+
+### `GET /api/saved-cities`
+Get all saved cities.
+
+**Response:**
+```json
+[
+ {
+ "id": 1,
+ "city_name": "London",
+ "latitude": 51.5085,
+ "longitude": -0.1257,
+ "added_at": "2026-06-03T10:30:00"
+ }
+]
+```
+
+### `POST /api/saved-cities`
+Save a city to favorites.
+
+**Parameters:**
+- `city_name` (string, required) - City name
+- `latitude` (float, required) - Latitude
+- `longitude` (float, required) - Longitude
+
+### `DELETE /api/saved-cities/{city_id}`
+Delete a saved city.
+
+## 🛠️ Development
+
+### Project Structure
+
+```
+weather_dashboard/
+├── __init__.py # Package initialization
+├── app.py # FastAPI application
+├── config.py # Configuration management
+├── models.py # Data models
+├── weather_service.py # OpenWeatherMap integration
+├── database.py # SQLite operations
+└── templates.py # HTML templates
+```
+
+### Configuration
+
+Edit `.env` file to customize settings:
+
+```bash
+# OpenWeatherMap API
+OPENWEATHER_API_KEY=your_key
+
+# Server
+WEATHER_HOST=127.0.0.1
+WEATHER_PORT=8001
+
+# Database
+DATABASE_URL=sqlite:///weather_dashboard.db
+```
+
+## 🔐 Security
+
+- ✅ HTTP security headers (X-Content-Type-Options, X-Frame-Options, etc.)
+- ✅ Input validation with Pydantic
+- ✅ Safe path validation
+- ✅ Error handling and logging
+- ✅ CORS-friendly
+
+## 📱 Responsive Design
+
+- Desktop: 2-column grid layout
+- Tablet: Responsive grid
+- Mobile: Single column stack
+
+## 🎨 UI Features
+
+- Beautiful gradient background
+- Smooth animations and transitions
+- Loading spinner
+- Error messages
+- Real-time weather icons
+- Weather details cards
+
+## 🐛 Troubleshooting
+
+### "OPENWEATHER_API_KEY environment variable is required"
+```bash
+export OPENWEATHER_API_KEY="your_api_key"
+```
+
+### Search returns empty results
+- Check your internet connection
+- Verify OpenWeatherMap API key is valid
+- Try searching for a major city first
+
+### Port 8001 already in use
+```bash
+export WEATHER_PORT=8002
+```
+
+## 📦 Dependencies
+
+- **FastAPI** - Web framework
+- **Uvicorn** - ASGI server
+- **aiohttp** - Async HTTP client
+- **Pydantic** - Data validation
+
+## 📄 License
+
+AGPL-3.0-only
+
+## 🤝 Contributing
+
+Contributions welcome! Please create a pull request.
+
+---
+
+**Made with ❤️ by Huynhthuongg**
diff --git a/config/weather.env.example b/config/weather.env.example
new file mode 100644
index 0000000..366ab9c
--- /dev/null
+++ b/config/weather.env.example
@@ -0,0 +1,16 @@
+# Weather Dashboard Configuration
+
+# OpenWeatherMap API Configuration
+# Get a free key from https://openweathermap.org/api
+OPENWEATHER_API_KEY=your_api_key_here
+
+# Server Configuration
+WEATHER_HOST=127.0.0.1
+WEATHER_PORT=8001
+WEATHER_DEBUG=False
+
+# Database Configuration
+DATABASE_URL=sqlite:///weather_dashboard.db
+
+# Cache Configuration (in seconds)
+CACHE_TTL=600
diff --git a/weather_dashboard/__init__.py b/weather_dashboard/__init__.py
new file mode 100644
index 0000000..fdbc54f
--- /dev/null
+++ b/weather_dashboard/__init__.py
@@ -0,0 +1,3 @@
+"""Weather Dashboard Application."""
+
+__version__ = "1.0.0"
diff --git a/weather_dashboard/app.py b/weather_dashboard/app.py
new file mode 100644
index 0000000..0df5631
--- /dev/null
+++ b/weather_dashboard/app.py
@@ -0,0 +1,211 @@
+"""FastAPI application for Weather Dashboard."""
+
+from __future__ import annotations
+
+import logging
+from typing import List
+
+from fastapi import FastAPI, HTTPException, Query
+from fastapi.responses import HTMLResponse
+from pydantic import BaseModel
+
+from .config import Settings, load_settings
+from .weather_service import WeatherService
+from .database import DatabaseManager
+from .templates import get_index_html
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# Initialize settings
+settings = load_settings()
+
+# Initialize services
+weather_service = WeatherService(settings)
+db_manager = DatabaseManager()
+
+# Create FastAPI app
+app = FastAPI(
+ title="Weather Dashboard",
+ description="Real-time weather information from OpenWeatherMap",
+ version="1.0.0",
+)
+
+
+# Pydantic models for API
+class WeatherResponse(BaseModel):
+ """Weather response model."""
+
+ city: str
+ country: str
+ temperature: float
+ feels_like: float
+ temp_min: float
+ temp_max: float
+ humidity: int
+ pressure: int
+ wind_speed: float
+ wind_deg: int
+ cloudiness: int
+ description: str
+ icon: str
+ icon_url: str
+ sunrise: str
+ sunset: str
+
+
+class SavedCityResponse(BaseModel):
+ """Saved city response model."""
+
+ id: int
+ city_name: str
+ latitude: float
+ longitude: float
+ added_at: str
+
+
+class SearchResultResponse(BaseModel):
+ """Search result response model."""
+
+ name: str
+ latitude: float
+ longitude: float
+ country: str
+ state: str | None
+
+
+# Security middleware
+@app.middleware("http")
+async def security_headers(request, call_next):
+ """Add security headers to response."""
+ response = await call_next(request)
+ response.headers["X-Content-Type-Options"] = "nosniff"
+ response.headers["X-Frame-Options"] = "DENY"
+ response.headers["X-XSS-Protection"] = "1; mode=block"
+ response.headers["Referrer-Policy"] = "no-referrer"
+ return response
+
+
+# Routes
+@app.get("/", response_class=HTMLResponse)
+async def index() -> str:
+ """Serve the dashboard HTML."""
+ return get_index_html()
+
+
+@app.get("/health")
+async def health() -> dict:
+ """Health check endpoint."""
+ return {"status": "ok", "version": "1.0.0"}
+
+
+@app.get("/api/weather", response_model=WeatherResponse)
+async def get_weather(
+ lat: float = Query(..., description="Latitude"),
+ lon: float = Query(..., description="Longitude"),
+) -> WeatherResponse:
+ """Get current weather for given coordinates."""
+ try:
+ weather_data = await weather_service.get_current_weather(lat, lon)
+ return WeatherResponse(
+ city=weather_data.city,
+ country=weather_data.country,
+ temperature=weather_data.main.temperature,
+ feels_like=weather_data.main.feels_like,
+ temp_min=weather_data.main.temp_min,
+ temp_max=weather_data.main.temp_max,
+ humidity=weather_data.main.humidity,
+ pressure=weather_data.main.pressure,
+ wind_speed=weather_data.wind.speed,
+ wind_deg=weather_data.wind.deg,
+ cloudiness=weather_data.cloudiness,
+ description=weather_data.weather.description,
+ icon=weather_data.weather.icon,
+ icon_url=weather_data.weather_icon_url,
+ sunrise=weather_data.sunrise.isoformat(),
+ sunset=weather_data.sunset.isoformat(),
+ )
+ except Exception as e:
+ logger.error(f"Error fetching weather: {e}")
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@app.get("/api/search", response_model=List[SearchResultResponse])
+async def search_cities(
+ q: str = Query(..., min_length=2, description="City name to search"),
+ limit: int = Query(5, ge=1, le=20),
+) -> List[SearchResultResponse]:
+ """Search for cities by name."""
+ try:
+ results = await weather_service.search_cities(q, limit)
+ return [
+ SearchResultResponse(
+ name=result.name,
+ latitude=result.latitude,
+ longitude=result.longitude,
+ country=result.country,
+ state=result.state,
+ )
+ for result in results
+ ]
+ except Exception as e:
+ logger.error(f"Error searching cities: {e}")
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@app.get("/api/saved-cities", response_model=List[SavedCityResponse])
+async def get_saved_cities() -> List[SavedCityResponse]:
+ """Get all saved cities."""
+ cities = db_manager.get_all_cities()
+ return [
+ SavedCityResponse(
+ id=city.id,
+ city_name=city.city_name,
+ latitude=city.latitude,
+ longitude=city.longitude,
+ added_at=city.added_at.isoformat(),
+ )
+ for city in cities
+ ]
+
+
+@app.post("/api/saved-cities", response_model=SavedCityResponse)
+async def save_city(
+ city_name: str = Query(..., description="City name"),
+ latitude: float = Query(..., description="Latitude"),
+ longitude: float = Query(..., description="Longitude"),
+) -> SavedCityResponse:
+ """Save a city to favorites."""
+ try:
+ saved_city = db_manager.add_city(city_name, latitude, longitude)
+ return SavedCityResponse(
+ id=saved_city.id,
+ city_name=saved_city.city_name,
+ latitude=saved_city.latitude,
+ longitude=saved_city.longitude,
+ added_at=saved_city.added_at.isoformat(),
+ )
+ except Exception as e:
+ logger.error(f"Error saving city: {e}")
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@app.delete("/api/saved-cities/{city_id}")
+async def delete_saved_city(city_id: int) -> dict:
+ """Delete a saved city."""
+ success = db_manager.delete_city(city_id)
+ if not success:
+ raise HTTPException(status_code=404, detail="City not found")
+ return {"status": "deleted"}
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run(
+ app,
+ host=settings.host,
+ port=settings.port,
+ log_level="info",
+ )
diff --git a/weather_dashboard/config.py b/weather_dashboard/config.py
new file mode 100644
index 0000000..fc6c3f6
--- /dev/null
+++ b/weather_dashboard/config.py
@@ -0,0 +1,38 @@
+"""Configuration for Weather Dashboard."""
+
+from dataclasses import dataclass
+from typing import Optional
+import os
+
+
+@dataclass
+class Settings:
+ """Application settings."""
+
+ # API Configuration
+ openweather_api_key: str = os.getenv("OPENWEATHER_API_KEY", "")
+ openweather_base_url: str = "https://api.openweathermap.org/data/2.5"
+
+ # Server Configuration
+ host: str = os.getenv("WEATHER_HOST", "127.0.0.1")
+ port: int = int(os.getenv("WEATHER_PORT", "8001"))
+ debug: bool = os.getenv("WEATHER_DEBUG", "False").lower() == "true"
+
+ # Database Configuration
+ database_url: str = os.getenv("DATABASE_URL", "sqlite:///weather_dashboard.db")
+
+ # Cache Configuration (in seconds)
+ cache_ttl: int = int(os.getenv("CACHE_TTL", "600")) # 10 minutes
+
+ def __post_init__(self):
+ """Validate settings after initialization."""
+ if not self.openweather_api_key:
+ raise ValueError(
+ "OPENWEATHER_API_KEY environment variable is required. "
+ "Get a free key from https://openweathermap.org/api"
+ )
+
+
+def load_settings() -> Settings:
+ """Load and return application settings."""
+ return Settings()
diff --git a/weather_dashboard/database.py b/weather_dashboard/database.py
new file mode 100644
index 0000000..c8d352d
--- /dev/null
+++ b/weather_dashboard/database.py
@@ -0,0 +1,119 @@
+"""Database operations for Weather Dashboard."""
+
+from __future__ import annotations
+
+import sqlite3
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional
+
+from .models import SavedCity
+
+
+class DatabaseManager:
+ """Manages database operations for saved cities."""
+
+ def __init__(self, db_path: str = "weather_dashboard.db"):
+ """Initialize database manager."""
+ self.db_path = db_path
+ self.init_db()
+
+ def init_db(self) -> None:
+ """Initialize database schema."""
+ with sqlite3.connect(self.db_path) as conn:
+ cursor = conn.cursor()
+ cursor.execute(
+ """
+ CREATE TABLE IF NOT EXISTS saved_cities (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ city_name TEXT NOT NULL,
+ latitude REAL NOT NULL,
+ longitude REAL NOT NULL,
+ added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(city_name, latitude, longitude)
+ )
+ """
+ )
+ conn.commit()
+
+ def add_city(self, city_name: str, latitude: float, longitude: float) -> SavedCity:
+ """Add a city to saved cities."""
+ with sqlite3.connect(self.db_path) as conn:
+ cursor = conn.cursor()
+ try:
+ cursor.execute(
+ """
+ INSERT INTO saved_cities (city_name, latitude, longitude)
+ VALUES (?, ?, ?)
+ """,
+ (city_name, latitude, longitude),
+ )
+ conn.commit()
+ city_id = cursor.lastrowid
+
+ return SavedCity(
+ id=city_id,
+ city_name=city_name,
+ latitude=latitude,
+ longitude=longitude,
+ )
+ except sqlite3.IntegrityError:
+ return self.get_city_by_name(city_name)
+
+ def get_all_cities(self) -> List[SavedCity]:
+ """Get all saved cities."""
+ with sqlite3.connect(self.db_path) as conn:
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ cursor.execute(
+ "SELECT id, city_name, latitude, longitude, added_at FROM saved_cities ORDER BY added_at DESC"
+ )
+ rows = cursor.fetchall()
+
+ return [
+ SavedCity(
+ id=row["id"],
+ city_name=row["city_name"],
+ latitude=row["latitude"],
+ longitude=row["longitude"],
+ added_at=datetime.fromisoformat(row["added_at"]),
+ )
+ for row in rows
+ ]
+
+ def get_city_by_name(self, city_name: str) -> Optional[SavedCity]:
+ """Get a saved city by name."""
+ with sqlite3.connect(self.db_path) as conn:
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ cursor.execute(
+ "SELECT id, city_name, latitude, longitude, added_at FROM saved_cities WHERE city_name = ?",
+ (city_name,),
+ )
+ row = cursor.fetchone()
+
+ if row:
+ return SavedCity(
+ id=row["id"],
+ city_name=row["city_name"],
+ latitude=row["latitude"],
+ longitude=row["longitude"],
+ added_at=datetime.fromisoformat(row["added_at"]),
+ )
+ return None
+
+ def delete_city(self, city_id: int) -> bool:
+ """Delete a saved city."""
+ with sqlite3.connect(self.db_path) as conn:
+ cursor = conn.cursor()
+ cursor.execute("DELETE FROM saved_cities WHERE id = ?", (city_id,))
+ conn.commit()
+ return cursor.rowcount > 0
+
+ def delete_city_by_name(self, city_name: str) -> bool:
+ """Delete a saved city by name."""
+ with sqlite3.connect(self.db_path) as conn:
+ cursor = conn.cursor()
+ cursor.execute("DELETE FROM saved_cities WHERE city_name = ?", (city_name,))
+ conn.commit()
+ return cursor.rowcount > 0
diff --git a/weather_dashboard/models.py b/weather_dashboard/models.py
new file mode 100644
index 0000000..ca8107e
--- /dev/null
+++ b/weather_dashboard/models.py
@@ -0,0 +1,105 @@
+"""Data models for Weather Dashboard."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Optional
+
+
+@dataclass(frozen=True)
+class Coordinates:
+ """Geographic coordinates."""
+
+ latitude: float
+ longitude: float
+
+
+@dataclass(frozen=True)
+class WeatherMain:
+ """Main weather data."""
+
+ temperature: float
+ feels_like: float
+ temp_min: float
+ temp_max: float
+ pressure: int
+ humidity: int
+
+
+@dataclass(frozen=True)
+class WeatherDescription:
+ """Weather description data."""
+
+ main: str
+ description: str
+ icon: str
+
+
+@dataclass(frozen=True)
+class Wind:
+ """Wind data."""
+
+ speed: float
+ deg: int
+ gust: Optional[float] = None
+
+
+@dataclass(frozen=True)
+class WeatherData:
+ """Complete weather data for a location."""
+
+ city: str
+ country: str
+ coordinates: Coordinates
+ weather: WeatherDescription
+ main: WeatherMain
+ wind: Wind
+ cloudiness: int
+ sunrise: datetime
+ sunset: datetime
+ timezone: int
+ timestamp: datetime = field(default_factory=datetime.utcnow)
+
+ @property
+ def weather_icon_url(self) -> str:
+ """Get the URL for the weather icon."""
+ return f"https://openweathermap.org/img/wn/{self.weather.icon}@2x.png"
+
+
+@dataclass
+class SavedCity:
+ """Saved city preference."""
+
+ id: Optional[int] = None
+ city_name: str = ""
+ latitude: float = 0.0
+ longitude: float = 0.0
+ added_at: datetime = field(default_factory=datetime.utcnow)
+
+ def to_dict(self) -> dict:
+ """Convert to dictionary."""
+ return {
+ "id": self.id,
+ "city_name": self.city_name,
+ "latitude": self.latitude,
+ "longitude": self.longitude,
+ "added_at": self.added_at.isoformat(),
+ }
+
+
+@dataclass
+class SearchResult:
+ """City search result."""
+
+ name: str
+ latitude: float
+ longitude: float
+ country: str
+ state: Optional[str] = None
+
+ @property
+ def display_name(self) -> str:
+ """Get display name for the city."""
+ parts = [self.name, self.state or "", self.country]
+ return ", ".join(p for p in parts if p)
diff --git a/weather_dashboard/templates.py b/weather_dashboard/templates.py
new file mode 100644
index 0000000..c16b952
--- /dev/null
+++ b/weather_dashboard/templates.py
@@ -0,0 +1,541 @@
+"""HTML templates for Weather Dashboard."""
+
+from __future__ import annotations
+
+HTML_TEMPLATE = """
+
+
+
+
+
+ Weather Dashboard
+
+
+
+
+
+
🌤️ Weather Dashboard
+
Get real-time weather information from around the world
+
+
+
+
+
+
+
Search Weather
+
+
+
+
+
+
+
+
+
+
+
+
Loading weather...
+
+
+
+
+
+
+
+
Saved Cities
+
+
No saved cities yet
+
+
+
+
+
+
+
+
+
+"""
+
+
+def get_index_html() -> str:
+ """Get the index HTML template."""
+ return HTML_TEMPLATE
diff --git a/weather_dashboard/weather_service.py b/weather_dashboard/weather_service.py
new file mode 100644
index 0000000..d728b09
--- /dev/null
+++ b/weather_dashboard/weather_service.py
@@ -0,0 +1,117 @@
+"""Weather API service for fetching weather data."""
+
+from __future__ import annotations
+
+import aiohttp
+from datetime import datetime
+from typing import Optional, List
+import logging
+
+from .config import Settings
+from .models import (
+ WeatherData,
+ Coordinates,
+ WeatherMain,
+ WeatherDescription,
+ Wind,
+ SearchResult,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class WeatherService:
+ """Service for fetching weather data from OpenWeatherMap API."""
+
+ def __init__(self, settings: Settings):
+ """Initialize weather service."""
+ self.settings = settings
+ self.base_url = settings.openweather_base_url
+ self.api_key = settings.openweather_api_key
+
+ async def get_current_weather(
+ self, latitude: float, longitude: float
+ ) -> WeatherData:
+ """Fetch current weather for given coordinates."""
+ url = f"{self.base_url}/weather"
+ params = {
+ "lat": latitude,
+ "lon": longitude,
+ "appid": self.api_key,
+ "units": "metric",
+ }
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
+ if response.status != 200:
+ error_data = await response.json()
+ raise ValueError(f"Weather API error: {error_data.get('message', 'Unknown error')}")
+
+ data = await response.json()
+ return self._parse_weather_response(data)
+
+ async def search_cities(self, query: str, limit: int = 5) -> List[SearchResult]:
+ """Search for cities by name."""
+ url = f"{self.base_url}/../geo/1.0/direct"
+ params = {
+ "q": query,
+ "limit": limit,
+ "appid": self.api_key,
+ }
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
+ if response.status != 200:
+ logger.error(f"City search failed with status {response.status}")
+ return []
+
+ data = await response.json()
+ return [
+ SearchResult(
+ name=item["name"],
+ latitude=item["lat"],
+ longitude=item["lon"],
+ country=item.get("country", ""),
+ state=item.get("state", None),
+ )
+ for item in data
+ ]
+
+ def _parse_weather_response(self, data: dict) -> WeatherData:
+ """Parse OpenWeatherMap API response."""
+ coords = data.get("coord", {})
+ weather = data.get("weather", [{}])[0]
+ main = data.get("main", {})
+ wind = data.get("wind", {})
+ sys = data.get("sys", {})
+
+ return WeatherData(
+ city=data.get("name", "Unknown"),
+ country=sys.get("country", ""),
+ coordinates=Coordinates(
+ latitude=coords.get("lat", 0.0),
+ longitude=coords.get("lon", 0.0),
+ ),
+ weather=WeatherDescription(
+ main=weather.get("main", ""),
+ description=weather.get("description", ""),
+ icon=weather.get("icon", "01d"),
+ ),
+ main=WeatherMain(
+ temperature=main.get("temp", 0.0),
+ feels_like=main.get("feels_like", 0.0),
+ temp_min=main.get("temp_min", 0.0),
+ temp_max=main.get("temp_max", 0.0),
+ pressure=main.get("pressure", 0),
+ humidity=main.get("humidity", 0),
+ ),
+ wind=Wind(
+ speed=wind.get("speed", 0.0),
+ deg=wind.get("deg", 0),
+ gust=wind.get("gust", None),
+ ),
+ cloudiness=data.get("clouds", {}).get("all", 0),
+ sunrise=datetime.fromtimestamp(sys.get("sunrise", 0)),
+ sunset=datetime.fromtimestamp(sys.get("sunset", 0)),
+ timezone=data.get("timezone", 0),
+ )
From e97ee8be2c468a6af9f99a5bc6f9dd7d91ea00cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Sun, 7 Jun 2026 11:14:32 +0700
Subject: [PATCH 08/13] Update pyproject.toml
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index a3a0300..73d16ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,7 +28,7 @@ dev = [
upca = "universal_compiler_agent.cli:main"
[tool.setuptools]
-package-dir = {"": "app"}
+package-dir = { "" = "app" }
[tool.setuptools.packages.find]
where = ["app"]
From 21e936bbc26d75f0b13cf387e5d503673ed761ff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?=
<252359928+Huynhthuongg@users.noreply.github.com>
Date: Mon, 15 Jun 2026 14:05:05 +0700
Subject: [PATCH 09/13] Fix Vercel FastAPI deployment routing
---
README.md | 15 +++++++++++++++
api/index.py | 14 ++++++++++++++
pyproject.toml | 1 +
requirements.txt | 3 +++
tests/test_vercel.py | 25 +++++++++++++++++++++++++
vercel.json | 14 ++++++++++++++
6 files changed, 72 insertions(+)
create mode 100644 api/index.py
create mode 100644 requirements.txt
create mode 100644 tests/test_vercel.py
create mode 100644 vercel.json
diff --git a/README.md b/README.md
index 92c9ed7..db0a022 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,21 @@ cd AGENTS.md
./scripts/start.sh
```
+---
+### ☁️ Vercel Deployment
+
+This repository includes a Vercel serverless entrypoint and routing config, so the FastAPI dashboard is served at `/` instead of returning `404: NOT_FOUND`.
+
+```bash
+# From the project root
+vercel
+```
+
+Vercel uses:
+- `vercel.json` to route every request to the Python serverless function.
+- `api/index.py` as the FastAPI entrypoint.
+- `requirements.txt` to install runtime dependencies.
+
---
## 🔌 API Examples
diff --git a/api/index.py b/api/index.py
new file mode 100644
index 0000000..6e569a4
--- /dev/null
+++ b/api/index.py
@@ -0,0 +1,14 @@
+"""Vercel serverless entrypoint for the FastAPI application."""
+
+from __future__ import annotations
+
+import sys
+from importlib import import_module
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+APP_DIR = ROOT / "app"
+if str(APP_DIR) not in sys.path:
+ sys.path.insert(0, str(APP_DIR))
+
+app = import_module("universal_compiler_agent.server").app
diff --git a/pyproject.toml b/pyproject.toml
index 73d16ad..9ab8350 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,7 @@ dev = [
"pytest>=8.0,<9.0",
"ruff>=0.6,<1.0",
"httpx>=0.28,<1.0",
+ "httpx2>=0.28,<1.0",
"codespell[toml]>=2.3,<3.0"
]
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..96fbc8d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+fastapi>=0.115,<1.0
+uvicorn[standard]>=0.30,<1.0
+pydantic>=2.8,<3.0
diff --git a/tests/test_vercel.py b/tests/test_vercel.py
new file mode 100644
index 0000000..47bc141
--- /dev/null
+++ b/tests/test_vercel.py
@@ -0,0 +1,25 @@
+import importlib.util
+import json
+from pathlib import Path
+
+from fastapi.testclient import TestClient
+
+
+def test_vercel_routes_all_paths_to_fastapi_entrypoint() -> None:
+ config = json.loads(Path("vercel.json").read_text(encoding="utf-8"))
+
+ assert config["builds"] == [{"src": "api/index.py", "use": "@vercel/python"}]
+ assert config["routes"] == [{"src": "/(.*)", "dest": "api/index.py"}]
+
+
+def test_vercel_entrypoint_exposes_fastapi_app() -> None:
+ spec = importlib.util.spec_from_file_location("vercel_entrypoint", "api/index.py")
+ assert spec is not None
+ assert spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ response = TestClient(module.app).get("/")
+
+ assert response.status_code == 200
+ assert "Universal Project Compiler Agent" in response.text
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..b9b875e
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,14 @@
+{
+ "builds": [
+ {
+ "src": "api/index.py",
+ "use": "@vercel/python"
+ }
+ ],
+ "routes": [
+ {
+ "src": "/(.*)",
+ "dest": "api/index.py"
+ }
+ ]
+}
From 11f8e6df06d799f7c30d9708bf9f5cd16572f69f Mon Sep 17 00:00:00 2001
From: Vercel
Date: Mon, 15 Jun 2026 10:13:58 +0000
Subject: [PATCH 10/13] Install and configure Vercel Web Analytics
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implemented Vercel Web Analytics for the Universal Project Compiler Agent
## Summary
Successfully configured Vercel Web Analytics for this Python FastAPI application by adding the official Vercel Analytics CDN script to the HTML template.
## Changes Made
### Modified Files:
- `app/universal_compiler_agent/templates.py`
- Added Vercel Analytics script tag using the CDN approach
- Script placed before closing `