This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Streamlit web app that renders an interactive star chart for a given date and location. Enter a Korean address, and the app resolves coordinates via vworld API, computes celestial positions with skyfield, and generates a poetic narrative using the Claude API.
# Streamlit app (main entry point)
uv run streamlit run src/thatnightsky/app.py
# Legacy static PNG script
uv run python src/thatnightsky/starchart.py# Build and start (always pass --env-file; CLOUDFLARE_TUNNEL_TOKEN lives in .env at repo root,
# but compose files are under docker/ so Docker Compose won't auto-load it otherwise)
docker compose -f docker/docker-compose.yml --env-file .env up -d --build
# Restart without rebuild
docker compose -f docker/docker-compose.yml --env-file .env up -d
# Stop
docker compose -f docker/docker-compose.yml --env-file .env downRequires a .env file:
VWORLD_API_KEY=... # Korean Spatial Information Open Platform (https://www.vworld.kr)
ANTHROPIC_API_KEY=... # Claude API (used for narrative text generation)
src/thatnightsky/ contains all code including the Streamlit entry point (app.py).
Data flow: QueryInput → compute.run() → SkyData → renderers/*.render_*() → HTML string (SVG+JS)
models.py — Immutable dataclasses defining layer boundaries:
QueryInput: Raw user input (address, time string)ObserverContext: Geocoding result (lat/lng, UTC datetime)StarRecord: Single star's coordinates + projection outputConstellationLine: Constellation line segment (HIP pair + IAU name)ConstellationPosition: Brightness-weighted mean az/alt for a single constellation (used for narrative)SkyData: Fully computed state passed to renderers
compute.py — External API calls and astronomy computation:
- Resolves Korean addresses to lat/lng via vworld API (ROAD → PARCEL fallback)
- Computes star positions using skyfield + stereographic projection
- Parses
resources/constellationship.fabfor constellation line segments - Loads
de421.bspandhip_main.datfromresources/at module import time (non-trivial cost; incurred once per Streamlit process start, not per re-run) _ROOTis resolved asPath(__file__).parent.parent.parent(i.e., repo root)- Public functions:
run()(top-level),geocode_address(),compute_sky_data(),load_constellation_lines()— all callable independently
i18n.py — Two-language (ko/en) translation helper. t(key, lang) looks up _STRINGS dict, falls back to "en" then to the key. Language is detected once via navigator.language JS eval and cached in st.session_state.lang.
narrative.py — Generates Korean poetic prose using Anthropic claude-sonnet-4-6 (model name hardcoded)
theme(user-supplied "이 날의 의미") is sanitized via_sanitize_theme()before inclusion in the prompt — returnsNoneon empty or injection-suspicious input; wrapped in<user_input>XML tags in the user message_IAU_TO_KO: IAU abbreviation → Korean name mapping dict (e.g."Ori"→"오리온"); up to 10 visible constellations passed to the prompt
renderers/svg_2d.py — Primary renderer used by the Streamlit app. Produces a self-contained HTML string (SVG + JS) embedded via st.components.v1.html(). Uses viewBox="-1 0 2 1" with CSS width/height 100% for browser-native scaling — no Plotly relayout hacks. Only stars with alt_deg >= 0 are shown.
renderers/plotly_2d.py — Plotly-based 2D interactive chart renderer; no longer used by the Streamlit app (superseded by svg_2d.py). Horizon is drawn as a data-coordinate circle; CSS controls canvas size.
renderers/static.py — Matplotlib static PNG renderer; used only by starchart.py (legacy), not by the Streamlit app
resources/ — Binary data files (committed to repo):
de421.bsp: NASA JPL ephemeriship_main.dat: Hipparcos star catalogueconstellationship.fab: Constellation line definitions (Stellarium format)
Lint, format, and type checks without modifying files:
uv run ruff check src/
uv run ruff format --check src/
uv run pyright src/
uv run bandit -r src/ -c pyproject.tomlPre-commit hooks run automatically on commit:
ruff(lint + format, with auto-fix)pyright(type checking)bandit(security scan, configured viapyproject.toml)pip-audit(manual stage only, run with--hook-stage manual)
Run manually:
pre-commit run --all-files
pre-commit run pip-audit --hook-stage manualNo automated tests exist in this project. The pre-commit hooks (ruff, pyright, bandit) are the primary quality gate.
app.py stores all inter-run state in st.session_state:
sky_data:SkyData | None— computed on form submit, persists across rerunsnarrative:str | None— Claude-generated text; cleared on each new submiterror_msg:str | None— shown when geocoding failsprivacy_agreed:bool— controls the one-time privacy dialognarrative_count:int— tracks Claude API calls per session; capped at_MAX_NARRATIVES_PER_SESSION = 3input_open:bool— toggles the bottom input panel (mobile collapsed state)show_placeholder:bool— controls placeholder visibility for input fieldssave_triggered:bool/save_seq:int— coordinate the save-as-image flow across rerunstheme:str— persists "이 날의 의미" input across rerunswhen_str:str— persists date/time input across rerunsdefault_input:dict— randomly chosen sample input (address/date/time/theme) shown on first load
Use uv. Never edit pyproject.toml directly to add packages:
uv add <package>