@@ -0,0 +1,4 @@/target/frontend/node_modules/.git/dist
@@ -1,4 +1,5 @@.env/target/dist/frontend/dist/frontend/node_modules
@@ -1,4 +1,6 @@# Dark Furrow# CLAUDE.mdThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.## What is this project?
@@ -36,8 +38,8 @@ There are no linters configured.**Backend:** Single-binary axum app (`src/main.rs`). Two routes: `/` rendersthe page, `/api/content` returns the same content as JSON for the client-sideauto-refresh. Both accept `?season=` for previewing other seasons. Data isloaded once at startup from `data/` and held in `AppState`.auto-refresh. Both accept `?season=` for previewing other seasons. Markdown isloaded once at startup from `content/` and held in `AppState`.**Frontend pipeline:** Vite (run from `frontend/`) builds `frontend/static_src/`into `dist/`. Entry point is `frontend/static_src/index.js` which imports SCSS
@@ -75,7 +77,7 @@ original python implementation in unit tests.**Section assembly:** `src/almanac.rs` is the engine. Builds five sections(sky, garden, kitchen, foraging, folklore), pulls bullets and prose fromthe relevant `data/<topic>/<season>.md` files, picks items via the seededthe relevant `content/<topic>/<season>.md` files, picks items via the seededRNG, and renders to a single HTML string for the template + JSON API.**Request logging:** custom middleware in `src/main.rs::log_requests` prints
@@ -84,8 +86,8 @@ RNG, and renders to a single HTML string for the template + JSON API.per request. The `.layer()` is applied after all routes so it covers the`nest_service` static-file mount too.**Content:** `data/` holds markdown for every topic-season combination plusseasons, haiku, moods, and moon-tips. The `data/wisdom/` files exist on disk**Content:** `content/` holds markdown for every topic-season combination plusseasons, haiku, moods, and moon-tips. The `content/wisdom/` files exist on diskbut are not read by anything (they were tied to the removed time rotation).## Page structure
@@ -95,7 +97,7 @@ part they care about. The current sections, in order, are:1. **sky** - calculated sun/moon/daylight + moon-phase gardening tip + a sky lore line + a storms lore line. The italic intro is the season's mood for the current time of day (from `data/moods/`). the current time of day (from `content/moods/`).2. **garden** - planting picks ("in the ground now"), indoor starts when the season has them, and a couple of weekly chores ("this week").3. **kitchen** - what's "in season" plus one "tonight" highlight.
@@ -128,13 +130,13 @@ darkfurrow.com/│ ├── styles/base.scss # @font-face + the whole stylesheet│ └── public/ # copied as-is to dist/ (favicon, og, sw, fonts/)├── templates/ # minijinja-compatible jinja2├── data/ # markdown content (one file per topic-season)├── content/ # markdown (one file per topic-season)├── dist/ # vite build output (gitignored, served at /static/)├── target/ # cargo build output (gitignored)└── samplefiles/ # Caddyfile.sample, env.sample, post-receive.sample```The binary reads `templates/`, `dist/`, and `data/` from the current workingThe binary reads `templates/`, `dist/`, and `content/` from the current workingdirectory by default. Override the project root with `DARKFURROW_ROOT=<path>`.## Content the page should surface
@@ -358,12 +358,15 @@ dependencies = [ "chrono", "chrono-tz", "comrak", "dotenvy", "minijinja", "serde", "serde_json", "tokio", "tower", "tower-http", "tracing", "tracing-subscriber",][[package]]
@@ -415,6 +418,12 @@ version = "1.6.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"[[package]]name = "dotenvy"version = "0.15.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"[[package]]name = "entities"version = "1.0.1"
@@ -680,6 +689,12 @@ dependencies = [ "wasm-bindgen",][[package]]name = "lazy_static"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"[[package]]name = "libc"version = "0.2.186"
@@ -713,6 +728,15 @@ version = "0.4.29"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"[[package]]name = "matchers"version = "0.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"dependencies = [ "regex-automata",][[package]]name = "matchit"version = "0.8.4"
@@ -778,6 +802,15 @@ dependencies = [ "windows-sys",][[package]]name = "nu-ansi-term"version = "0.50.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"dependencies = [ "windows-sys",][[package]]name = "num-conv"version = "0.2.1"
@@ -1086,6 +1119,15 @@ dependencies = [ "serde",][[package]]name = "sharded-slab"version = "0.1.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"dependencies = [ "lazy_static",][[package]]name = "shell-words"version = "1.1.1"
@@ -1227,6 +1269,15 @@ dependencies = [ "syn",][[package]]name = "thread_local"version = "1.1.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"dependencies = [ "cfg-if",][[package]]name = "time"version = "0.3.47"
@@ -1375,9 +1426,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"dependencies = [ "log", "pin-project-lite", "tracing-attributes", "tracing-core",][[package]]name = "tracing-attributes"version = "0.1.31"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "tracing-core"version = "0.1.36"
@@ -1385,6 +1448,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"dependencies = [ "once_cell", "valuable",][[package]]name = "tracing-log"version = "0.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"dependencies = [ "log", "once_cell", "tracing-core",][[package]]name = "tracing-subscriber"version = "0.3.23"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log",][[package]]
@@ -1426,6 +1519,12 @@ version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"[[package]]name = "valuable"version = "0.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"[[package]]name = "walkdir"version = "2.5.0"
@@ -15,6 +15,9 @@ serde_json = "1"chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }chrono-tz = "0.10"anyhow = "1"dotenvy = "0.15"tracing = "0.1"tracing-subscriber = { version = "0.3", features = ["env-filter"] }[profile.release]lto = true
@@ -26,7 +26,7 @@ WORKDIR /appCOPY --from=builder /app/darkfurrow ./darkfurrowCOPY --from=builder /app/dist ./distCOPY templates ./templatesCOPY data ./dataCOPY content ./contentRUN addgroup -S -g 1000 app && \ adduser -S -h /app -s /sbin/nologin -u 1000 -G app app && \
@@ -19,7 +19,7 @@ build: frontend/node_modulesstart: PORT=$(PORT) ./target/release/darkfurrow# `data/` holds the almanac content (tracked in git, copied into the image),# `content/` holds the almanac markdown (tracked in git, copied into the image),# so do NOT wipe it.clean: rm -rf target dist frontend/node_modules
renamed
data/bugs/early-fall.md → content/bugs/early-fall.md
renamed
data/bugs/early-spring.md → content/bugs/early-spring.md
renamed
data/bugs/early-summer.md → content/bugs/early-summer.md
renamed
data/bugs/late-fall.md → content/bugs/late-fall.md
renamed
data/bugs/late-spring.md → content/bugs/late-spring.md
renamed
data/bugs/midsummer.md → content/bugs/midsummer.md
renamed
data/bugs/winter.md → content/bugs/winter.md
renamed
data/chores/early-fall.md → content/chores/early-fall.md
renamed
data/chores/early-spring.md → content/chores/early-spring.md
renamed
data/chores/early-summer.md → content/chores/early-summer.md
renamed
data/chores/late-fall.md → content/chores/late-fall.md
renamed
data/chores/late-spring.md → content/chores/late-spring.md
renamed
data/chores/midsummer.md → content/chores/midsummer.md
renamed
data/chores/winter.md → content/chores/winter.md
renamed
data/foraging/early-fall.md → content/foraging/early-fall.md
renamed
data/foraging/early-spring.md → content/foraging/early-spring.md
renamed
data/foraging/early-summer.md → content/foraging/early-summer.md
renamed
data/foraging/late-fall.md → content/foraging/late-fall.md
renamed
data/foraging/late-spring.md → content/foraging/late-spring.md
renamed
data/foraging/midsummer.md → content/foraging/midsummer.md
renamed
data/foraging/winter.md → content/foraging/winter.md
renamed
data/haiku/early-fall.md → content/haiku/early-fall.md
renamed
data/haiku/early-spring.md → content/haiku/early-spring.md
renamed
data/haiku/early-summer.md → content/haiku/early-summer.md
renamed
data/haiku/late-fall.md → content/haiku/late-fall.md
renamed
data/haiku/late-spring.md → content/haiku/late-spring.md
renamed
data/haiku/midsummer.md → content/haiku/midsummer.md
renamed
data/haiku/winter.md → content/haiku/winter.md
renamed
data/kitchen/early-fall.md → content/kitchen/early-fall.md
renamed
data/kitchen/early-spring.md → content/kitchen/early-spring.md
renamed
data/kitchen/early-summer.md → content/kitchen/early-summer.md
renamed
data/kitchen/late-fall.md → content/kitchen/late-fall.md
renamed
data/kitchen/late-spring.md → content/kitchen/late-spring.md
renamed
data/kitchen/midsummer.md → content/kitchen/midsummer.md
renamed
data/kitchen/winter.md → content/kitchen/winter.md
renamed
data/manifest.json → content/manifest.json
renamed
data/moods/early-fall.md → content/moods/early-fall.md
renamed
data/moods/early-spring.md → content/moods/early-spring.md
renamed
data/moods/early-summer.md → content/moods/early-summer.md
renamed
data/moods/late-fall.md → content/moods/late-fall.md
renamed
data/moods/late-spring.md → content/moods/late-spring.md
renamed
data/moods/midsummer.md → content/moods/midsummer.md
renamed
data/moods/winter.md → content/moods/winter.md
renamed
data/moon-tips.md → content/moon-tips.md
renamed
data/names/early-fall.md → content/names/early-fall.md
renamed
data/names/early-spring.md → content/names/early-spring.md
renamed
data/names/early-summer.md → content/names/early-summer.md
renamed
data/names/late-fall.md → content/names/late-fall.md
renamed
data/names/late-spring.md → content/names/late-spring.md
renamed
data/names/midsummer.md → content/names/midsummer.md
renamed
data/names/winter.md → content/names/winter.md
renamed
data/planting/early-fall.md → content/planting/early-fall.md
renamed
data/planting/early-spring-indoors.md → content/planting/early-spring-indoors.md
renamed
data/planting/early-spring.md → content/planting/early-spring.md
renamed
data/planting/early-summer.md → content/planting/early-summer.md
renamed
data/planting/late-fall.md → content/planting/late-fall.md
renamed
data/planting/late-spring.md → content/planting/late-spring.md
renamed
data/planting/midsummer.md → content/planting/midsummer.md
renamed
data/planting/winter.md → content/planting/winter.md
renamed
data/preserving/early-fall.md → content/preserving/early-fall.md
renamed
data/preserving/early-spring.md → content/preserving/early-spring.md
renamed
data/preserving/early-summer.md → content/preserving/early-summer.md
renamed
data/preserving/late-fall.md → content/preserving/late-fall.md
renamed
data/preserving/late-spring.md → content/preserving/late-spring.md
renamed
data/preserving/midsummer.md → content/preserving/midsummer.md
renamed
data/preserving/winter.md → content/preserving/winter.md
renamed
data/remedies/early-fall.md → content/remedies/early-fall.md
renamed
data/remedies/early-spring.md → content/remedies/early-spring.md
renamed
data/remedies/early-summer.md → content/remedies/early-summer.md
renamed
data/remedies/late-fall.md → content/remedies/late-fall.md
renamed
data/remedies/late-spring.md → content/remedies/late-spring.md
renamed
data/remedies/midsummer.md → content/remedies/midsummer.md
renamed
data/remedies/winter.md → content/remedies/winter.md
renamed
data/seasons/early-fall.md → content/seasons/early-fall.md
renamed
data/seasons/early-spring.md → content/seasons/early-spring.md
renamed
data/seasons/early-summer.md → content/seasons/early-summer.md
renamed
data/seasons/late-fall.md → content/seasons/late-fall.md
renamed
data/seasons/late-spring.md → content/seasons/late-spring.md
renamed
data/seasons/midsummer.md → content/seasons/midsummer.md
renamed
data/seasons/winter.md → content/seasons/winter.md
renamed
data/sky/early-fall.md → content/sky/early-fall.md
renamed
data/sky/early-spring.md → content/sky/early-spring.md
renamed
data/sky/early-summer.md → content/sky/early-summer.md
renamed
data/sky/late-fall.md → content/sky/late-fall.md
renamed
data/sky/late-spring.md → content/sky/late-spring.md
renamed
data/sky/midsummer.md → content/sky/midsummer.md
renamed
data/sky/winter.md → content/sky/winter.md
renamed
data/storms/early-fall.md → content/storms/early-fall.md
renamed
data/storms/early-spring.md → content/storms/early-spring.md
renamed
data/storms/early-summer.md → content/storms/early-summer.md
renamed
data/storms/late-fall.md → content/storms/late-fall.md
renamed
data/storms/late-spring.md → content/storms/late-spring.md
renamed
data/storms/midsummer.md → content/storms/midsummer.md
renamed
data/storms/winter.md → content/storms/winter.md
renamed
data/wisdom/afternoon.md → content/wisdom/afternoon.md
renamed
data/wisdom/dawn.md → content/wisdom/dawn.md
renamed
data/wisdom/evening.md → content/wisdom/evening.md
renamed
data/wisdom/morning.md → content/wisdom/morning.md
renamed
data/wisdom/night.md → content/wisdom/night.md
modified
docker-compose.yml
@@ -2,6 +2,7 @@ services: web: container_name: darkfurrow.com build: . init: true env_file: .env ports: - "127.0.0.1:${PORT}:${PORT}"
@@ -25,11 +25,11 @@ impl AppState { let templates_dir = project_root.join("templates"); let dist_dir = project_root.join("dist"); let data_dir = project_root.join("data"); let content_dir = project_root.join("content"); let manifest_path = dist_dir.join(".vite/manifest.json"); let env = templates::build_env(&templates_dir, &manifest_path); let data = content::load_data(&data_dir).expect("failed to load data"); let data = content::load_data(&content_dir).expect("failed to load data"); Self { env: Arc::new(env),
@@ -12,7 +12,15 @@ mod templates;use std::net::SocketAddr;#[tokio::main]async fn main() {async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); let port: u16 = std::env::var("PORT") .ok() .and_then(|v| v.parse().ok())
@@ -20,7 +28,8 @@ async fn main() { let app = app::router(app::AppState::from_env()); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); eprintln!("darkfurrow listening on http://{addr}"); axum::serve(listener, app).await.unwrap(); let listener = tokio::net::TcpListener::bind(addr).await?; tracing::info!("darkfurrow listening on http://{addr}"); axum::serve(listener, app).await?; Ok(())}