diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e150f07..57e9458 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,5 +18,4 @@ jobs:
python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip
- run: python -m pip install -e '.[dev]'
- - run: ruff check .
- - run: pytest -q
+ - run: ./scripts/check.sh
diff --git a/README.md b/README.md
index dd1dadf..92c9ed7 100644
--- a/README.md
+++ b/README.md
@@ -1,85 +1,310 @@
-# Universal Project Compiler Agent
+
-Android-first, Termux-first development agent that transforms documents, specifications, repositories, OCR text, Markdown, or natural language requests into complete, runnable, maintainable software project scaffolds.
+# ๐ค Universal Project Compiler Agent
-The original product specification is preserved in [docs/SPECIFICATION.md](docs/SPECIFICATION.md).
+[](https://python.org)
+[](LICENSE)
+[](https://fastapi.tiangolo.com)
+[](https://termux.dev)
+[](https://github.com/Huynhthuongg/AGENTS.md)
+[](.)
-## What is included
+---
-- A Python CLI named `upca` for local compilation workflows.
-- A FastAPI service with `/health`, `/plan`, and `/compile` endpoints.
-- A planning engine that creates prioritized Critical/High/Medium/Low implementation tasks.
-- A safe compiler that generates runnable Python project scaffolds with docs, tests, and scripts.
-- Secret redaction, safe slug generation, path traversal protection, and HTTP security headers.
-- Termux-friendly setup, start, update, and backup scripts.
-- CI, tests, and architecture documentation.
+### ๐ฑ Android-first โข ๐ Termux-ready โข ๐ง No Docker โข โจ AI-powered
-## Quick start
+**Transform documents, specs, repositories, OCR text, Markdown, or natural language into complete, runnable, production-ready software projects in seconds.**
+
+[๐ Documentation](#-quick-start) โข [๐ Get Started](#-quick-start) โข [๐ API](#-api-examples) โข [๐ฌ Issues](https://github.com/Huynhthuongg/AGENTS.md/issues)
+
+
+
+---
+
+## โจ Features
+
+
+
+
+
+### ๐ฏ **Core Features**
+- ๐๏ธ Python CLI (`upca`) for local workflows
+- โก FastAPI service with planning & compilation endpoints
+- ๐ง AI-powered planning engine (Critical/High/Medium/Low tasks)
+- ๐ก๏ธ Safe compiler with security validation
+- ๐ Secret redaction & path traversal protection
+- ๐ฑ Termux-optimized (runs on low-memory Android)
+
+
+
+
+### ๐ **Security & Quality**
+- โ
HTTP security headers
+- ๐ซ Secret pattern detection
+- ๐ Safe slug generation
+- ๐ Full test coverage
+- ๐งน Ruff linting & Codespell
+- ๐ Complete documentation
+
+
+
+
+
+---
+
+## ๐ Quick Start
+
+### Prerequisites
+- Python 3.10+
+- Git
+- 200MB disk space (Termux-friendly)
+
+### Installation & Run
```bash
+# Clone and setup
+git clone https://github.com/Huynhthuongg/AGENTS.md.git
+cd AGENTS.md
./scripts/setup.sh
./scripts/start.sh
```
-Open or use the CLI:
+Open **http://127.0.0.1:8000** in your browser or use the CLI:
```bash
+# Plan a project
upca plan --text "# CRM Dashboard\nNeed auth, API, admin dashboard, dark mode"
+
+# Compile & generate
upca compile --text "# CRM Dashboard\nNeed auth, API, admin dashboard" --output-dir generated
```
-## Termux setup
+### ๐ฑ Termux Setup
```bash
-pkg update
-pkg install python git
+pkg update && pkg install python git
+cd ~
+git clone https://github.com/Huynhthuongg/AGENTS.md.git
+cd AGENTS.md
./scripts/setup.sh
+./scripts/start.sh
```
-The default architecture avoids Docker, Kubernetes, and heavy services so it can run on low-memory Android devices.
+---
-## API examples
+## ๐ API Examples
+### Health Check
```bash
curl -s http://127.0.0.1:8000/health
+```
+
+### Plan Endpoint
+```bash
curl -s -X POST http://127.0.0.1:8000/plan \
- -H 'content-type: application/json' \
- -d '{"requirements":"# Portal\nNeed API, dashboard, auth and mobile responsive UI"}'
+ -H 'Content-Type: application/json' \
+ -d '{
+ "requirements": "# Portal\nNeed API, dashboard, auth and mobile responsive UI"
+ }'
```
-## Project structure
+### Compile Endpoint
+```bash
+curl -s -X POST http://127.0.0.1:8000/compile \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "requirements": "# E-commerce Store\nPython FastAPI backend, React frontend, PostgreSQL",
+ "output_dir": "generated"
+ }'
+```
+
+---
+
+## ๐ Project Structure
-```text
-app/universal_compiler_agent/ Application package
-config/ Example runtime configuration
-docs/ Architecture and changelog
-scripts/ Setup, start, update, backup helpers
-tests/ Unit tests
-.github/workflows/ CI checks
```
+AGENTS.md/
+โโโ app/universal_compiler_agent/ # Main application package
+โ โโโ cli.py # CLI interface
+โ โโโ server.py # FastAPI server
+โ โโโ planner.py # Planning engine
+โ โโโ compiler.py # Code generation
+โโโ config/ # Configuration examples
+โโโ docs/ # Architecture & specs
+โโโ scripts/ # Setup & helpers
+โ โโโ setup.sh # Initial setup
+โ โโโ start.sh # Start server
+โ โโโ check.sh # Run tests & linters
+โ โโโ backup.sh # Backup script
+โโโ tests/ # Unit tests
+โโโ .github/workflows/ # CI/CD pipelines
+โโโ pyproject.toml # Project configuration
+โโโ LICENSE # AGPL-3.0
+```
+
+---
-## Development
+## ๐ ๏ธ Development
+
+### Setup Development Environment
```bash
+# Install dev dependencies
python -m pip install -e '.[dev]'
+
+# Run quality checks
./scripts/check.sh
```
-`./scripts/check.sh` runs Ruff and the full pytest suite so release checks match CI.
+The `check.sh` script runs:
+- ๐ Ruff (linter)
+- โ๏ธ Codespell (spell checker)
+- โ
Pytest (test suite)
+
+---
+
+## ๐ Current Release
+
+| Property | Details |
+|----------|---------|
+| **Version** | 0.1.1 |
+| **Release Date** | 2026-06-02 |
+| **Python** | 3.10+ |
+| **License** | AGPL-3.0-only |
+| **Status** | โ
Active Development |
+
+### Release Highlights v0.1.1
+- ๐ CLI dry-run previews
+- ๐ Safe output directory validation
+- ๐๏ธ Dashboard route alignment
+- ๐งช Hardened test workflows
+
+---
+
+## ๐ Security Model
+
+We take security seriously:
+
+- ๐ซ **No hardcoded secrets** in generated output
+- ๐ **Pattern detection** redacts API keys, tokens, passwords
+- ๐ก๏ธ **Path validation** prevents directory traversal attacks
+- ๐ **HTTP headers** follow security best practices
+- โ
**AGPL-3.0** ensures transparency
+
+---
+
+## ๐ฆ Dependencies
+
+### Core
+- **FastAPI** (0.115+) - Web framework
+- **Uvicorn** (0.30+) - ASGI server
+- **Pydantic** (2.8+) - Data validation
+
+### Development
+- **Pytest** (8.0+) - Testing
+- **Ruff** (0.6+) - Code linting
+- **Httpx** (0.28+) - HTTP client
+- **Codespell** (2.3+) - Spell checking
+
+---
+
+## ๐ Sponsors & Contributors
+
+
+
+### โจ **Thank You to Our Sponsors!** โจ
+
+
+
+[](https://github.com/sponsors/Huynhthuongg)
+
+
+
+**Become a sponsor and help us develop faster! Your support helps maintain and improve this project.**
+
+### ๐ Featured Sponsors
+
+
+
+### ๐ฅ Contributors
+
+
+
+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 Examples**](#-api-examples)
+- ๐ [**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).
+
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/pyproject.toml b/pyproject.toml
index 006121f..73d16ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,15 +20,15 @@ dependencies = [
dev = [
"pytest>=8.0,<9.0",
"ruff>=0.6,<1.0",
- "httpx>=0.27,<1.0",
- "httpx2>=2.3,<3.0"
+ "httpx>=0.28,<1.0",
+ "codespell[toml]>=2.3,<3.0"
]
[project.scripts]
upca = "universal_compiler_agent.cli:main"
[tool.setuptools]
-package-dir = {"" = "app"}
+package-dir = { "" = "app" }
[tool.setuptools.packages.find]
where = ["app"]
@@ -47,3 +47,7 @@ select = ["E", "F", "I", "B", "UP", "SIM"]
[tool.ruff.lint.per-file-ignores]
"app/universal_compiler_agent/server.py" = ["E501"]
"app/universal_compiler_agent/templates.py" = ["E501"]
+
+[tool.codespell]
+skip = "*.png,.git,.pytest_cache,.ruff_cache,.venv"
+ignore-words-list = "thi"
diff --git a/scripts/check.sh b/scripts/check.sh
old mode 100755
new mode 100644
index 7f5a70c..3118e1e
--- a/scripts/check.sh
+++ b/scripts/check.sh
@@ -1,4 +1,25 @@
#!/usr/bin/env sh
set -eu
+
+missing=""
+for command in ruff codespell; do
+ if ! command -v "$command" >/dev/null 2>&1; then
+ missing="$missing $command"
+ fi
+done
+
+for module in pytest httpx; do
+ if ! python -c "import ${module}" >/dev/null 2>&1; then
+ missing="$missing $module"
+ fi
+done
+
+if [ -n "$missing" ]; then
+ echo "Missing development dependencies:$missing" >&2
+ echo "Run: python -m pip install -e '.[dev]'" >&2
+ exit 1
+fi
+
ruff check .
+codespell README.md docs app tests pyproject.toml scripts config
pytest -q
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
+
+
+
+
+
+
+
+
+
+
+
Search Weather
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+
+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),
+ )