Skip to content

jdbostonbu-ops/AnglerCast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

341 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🎣 AnglerCast

Know what fish are where — and when — from real public occurrence data.

Fish move constantly. AnglerCast shows you the historical sighting record from real public data — with honest sample sizes and confidence, never a guarantee.


Next.js TypeScript React Prisma Neon Postgres Vercel

Vitest Resend OpenAI Leaflet

Built test-first Tests GitHub stars

🌐 Live Site


👤 Author's Profile


📖 About

AnglerCast is a web app with its own domain for fishermen planning a trip who want to know where and when fish can be — not where they're guaranteed to be. Fish are always moving, but anglers and researchers leave a trail of where fish have actually been recorded. AnglerCast turns that real public occurrence data into honest, plain-English guidance.

It's split into Freshwater and Saltwater views, each with a map of real recorded occurrences, an honest historical sighting rate, and an AI that explains the numbers — it never invents them. A separate Explore page provides a conditions-aware travel-time tool.


🧭 The Honest-Data Thesis

This is the principle the whole app is built around, and it is never violated:

Real data computes the facts. The AI explains and assembles — it does not invent.

  • 📌 Locations, fish, months, sighting frequency, sample sizes, and confidence flags are produced by tested code from real occurrence data (GBIF and OBIS).
  • 🗣️ The AI (OpenAI) only phrases recommendations from facts the code already computed. It never invents a location, a species, or a season.
  • 📊 Every rate or recommendation is shown with its sample size and a high/low confidence flag.
  • 🚫 There is never a fabricated "catch probability." The app shows where fish have been recorded — never a guarantee of where they are now.
  • 🧮 One intentional exception: the conditions-aware travel-time / ETA, where the AI computes an estimate from inputs the code provides (origin, destination, conditions, speed) — guarded by a sanity-check test that rejects an impossible ETA.

🧪 Test-Driven Development

AnglerCast was built test-first. Most features followed the same disciplined cycle:

1. 🔴 RED    →  Write a failing test first. Run it. Confirm it fails for the
                 expected reason (missing implementation), not a typo.
2. 💾 commit →  Commit the RED on its own.
3. 🟢 GREEN  →  Write the simplest code that makes that one test pass. Re-run.
4. 💾 commit →  Commit the GREEN separately.
5. 🔁 repeat →  One test at a time. Never weaken or delete a test to pass.

Test-first, provable. AnglerCast was built test-first. For most features, the failing test (RED) was committed before its implementation (GREEN) as a separate commit — so the discipline is verifiable in git history, not just claimed. You can check out any RED commit and run the test to watch it fail because the implementation doesn't exist yet. For example, at commit 604735b the OBIS fetch test imports a module that hasn't been written, so the suite fails to resolve it — RED by construction. A few early features were committed in batches rather than separate RED/GREEN steps; for those, the red-first cycle is recorded in test-report.md. The full sequence of RED commits is visible by filtering the log: git log --oneline | grep "test:".

The rules that kept it honest:

  • 🧱 Test-first, always. No implementation exists before a failing test does.
  • 🔒 External calls are mocked in every unit test — GBIF, OBIS, NOAA, Open-Meteo, USGS, OpenAI, Prisma/Neon, and Resend email. Unit tests never hit the real network or database.
  • 🧩 Pure, tested seams. Core logic lives in small closure-based functions (e.g. computeSightingRate, verifyEmailVerificationCode, checkLoginCredentials) that are tested directly. Route handlers wire those seams together; UI is eyeball-verified on top of green logic.
  • 📒 Two ledgers. Every cycle is recorded in TESTING.md (the RED plan) and RESULTS.md (the RED → GREEN outcomes).
  • The result: a full Vitest suite of 131 passing unit, component, and integration tests across 48 files, with a clean typecheck.

Strict TypeScript throughout — no any, no var, closure-based arrow functions, factory functions over classes (except Next.js route handlers).


✨ Features

Built in this order, each test-first:

  • 📧 Email code verification — hashed, expiring code, required before an account is active
  • 🔐 Login — bcrypt password check + show/hide password toggle
  • 📍 CRUD for saved spots — create, read, update, delete fishing locations
  • 🗑️ Delete confirmation dialog — no accidental deletes
  • 🗄️ Prisma + Neon live database — accounts and spots persist
  • 🎯 Coordinate precision fix — full decimal precision, never truncated
  • 🧭 Nav bar — Freshwater · Saltwater · Explore · Contact, with logout
  • 🏠 Post-login home — two-button entry to Fresh / Salt
  • 🛬 Landing page — rugged outdoor feel
  • 🎣 Recommendation feature — real fish + real location + best month, AI-phrased
  • 🐟 Distinct map markers per species — with a legend
  • Knots / ETA travel time — conditions-aware, sanity-checked
  • 📊 Empirical sighting-rate search — honest historical rate + map

🛠️ Tech Stack

Layer Technology
Framework Next.js (App Router) + React + TypeScript
Backend Next.js API route handlers
Database PostgreSQL on Neon, via Prisma ORM
Auth bcrypt-hashed passwords + email code verification + session cookie
Email Resend (transactional, from a verified domain)
AI OpenAI gpt-4o-mini — explanation / assembly + travel-time
Maps Leaflet (client-side, full-precision markers)
Testing Vitest — unit, component, integration; external calls mocked
Deployment Vercel (app) + Neon (database)

👤 User Profile

A logged-in angler can set a profile that appears in the nav bar and on every catch they post — a display name and an optional profile image.

  • 🪪 Display name — saved to the user record via saveProfileName. Required before posting a catch.
  • 🖼️ Profile image (optional) — a URL is stored on the user record via saveProfileImage. Image uploads themselves are handled separately; only the resolved URL is persisted.
  • 🅰️ Avatar fallbackgetDisplayAvatar returns the image when one is set, and falls back to the uppercase first letter of the user's email when none is set.
  • 🧭 Nav bar integration — when a profile is set, the right side of the nav shows the avatar + display name. When it isn't, a "Set up profile" prompt appears instead.
  • 🛑 Post-gatecanPostCatch({ profileName }) returns { allowed: false, reason: "no profile name" } when the user has no display name. Clicking Post on the catch feed without a profile name opens a "Set up profile" dialog instead of submitting — every angler in the feed has a name and an avatar by construction.

The profile is a small piece of identity that makes the catch feed feel human. The data layer is tested directly; the nav-bar and dialog wiring is eyeball-verified on top of green logic.


💬 Ask AnglerCast — RAG-Powered FAQ

The Explore page includes an AI chat that answers fishing questions grounded in a curated knowledge base. Users type any question — how sighting rate works, why month matters, what gear to bring, safety on the water, how to read tide charts — and get an answer drawn from real documents, not invented.

How it works:

  • 📚 Curated corpus. 12 fishing FAQ markdown files in src/lib/faq/ cover concept explanations (sighting rate, tides, conditions) and "how to use AnglerCast" guides (best month per species, where to fish today). Each doc is grounded in NOAA, USGS, USCG, GBIF, and other authoritative sources.
  • ✂️ Chunking with heading context. chunkMarkdownContent splits each doc on blank lines, filters out paragraphs under 50 characters, and prepends the most recent heading to each chunk so semantic context survives retrieval.
  • 🧮 Embedding + cosine similarity. Each chunk is embedded with OpenAI's text-embedding-3-small. At query time, the user's question is embedded against the corpus and the top 3 chunks are retrieved by cosine similarity.
  • 🎯 Grounded answer. The retrieved chunks are injected into the LLM prompt with strict grounding instructions — the system prompt requires the model to answer only from the retrieved context.
  • 🚫 Honest refusal. If the top retrieved chunk's similarity score falls below threshold, the LLM is never called — the route short-circuits and returns "I don't know based on the provided documents." No hallucinated fishing advice.
  • 🔍 Sources shown. Every answer displays the friendly titles of the FAQ documents it drew from, so you can verify what the answer was based on.

The RAG layer is grounded the same way the rest of AnglerCast is grounded: real text in, real answers out. The AI does not invent fishing knowledge — it explains what the curated docs already say.


📰 Weekly Blog — AI-Written, Source-Grounded

The home page features a weekly fishing blog post that publishes itself. Every Friday, an automated Zapier pipeline writes a fresh, source-grounded article and surfaces it below the seasonal data cards — no manual posting, no redeploy.

The pipeline keeps content generation decoupled from the app:

  • 📅 Zapier Schedule — a weekly trigger fires every Friday at 2 PM.
  • ✍️ AI by Zapier — the post rotates through eight angler themes (conservation & water health, fishing skills, seasonal tips, community stories, gear & care, weather on the water, boating safety, tackle matchmaking). It is constrained to reliable sources only — NOAA, USGS, EPA, state Fish & Wildlife agencies, university extension programs, and peer-reviewed work — and forbidden from inventing statistics, species behavior, or species-specific predictions. The honest-data thesis extends to the written word.
  • 📄 Storage — Zapier writes the post into a Google Sheet (title, date, body), and a Google Apps Script Web App serves that row as a public JSON endpoint.

🔌 Data Sources & APIs

Every fact comes from a real source. The AI explains and assembles; it does not invent.

Source Key Role
GBIF Occurrence (Global Biodiversity Information Facility) keyless Historical species occurrences worldwide (primary)
OBIS Occurrence (Ocean Biodiversity Information System) keyless Marine occurrence records (Darwin Core), merged with GBIF
Open-Meteo Marine keyless Saltwater / ocean conditions (waves, swell, currents)
Open-Meteo Forecast keyless Wind and weather conditions (both water types)
USGS (U.S. Geological Survey) keyless Live water conditions (streamflow, gage height, water temperature) near the location
NOAA CO-OPS (National Oceanic and Atmospheric Administration, Center for Operational Oceanographic Products and Services) keyless High and low tide predictions for the nearest coastal station (saltwater)
OpenAI gpt-4o-mini key Explains conditions and phrases the ETA, species, and tide summaries in plain English

🧠 The AI Layer (OpenAI)

The AI is strictly an explanation and assembly layer, powered by gpt-4o-mini. It receives the already-computed rate, sample size, and confidence and turns them into clear, plain-English guidance for the angler. It is explicitly told the numbers — it never calculates them, and it never invents a location, species, or season. The single computational exception is the conditions-aware travel-time estimate, which is guarded by a sanity-check test.


Try these on the Explore page:

Off Fort Lauderdale (Atlantic): 26.1000, -80.0500 Off Key West (Atlantic side): 24.5000, -81.8000 Gulf of Mexico, off Tampa: 27.7000, -83.0000 Off Naples (Gulf): 26.1000, -81.9000


🗄️ Database — Neon Postgres + Prisma

Accounts and saved spots live in a real PostgreSQL database on Neon (serverless, accessed over a pooled connection in production), through the Prisma ORM.

User  ──< has many >──  SavedSpot
  • Userid, email (unique), passwordHash, isVerified, verificationCodeHash, verificationCodeExpiresAt, createdAt
  • SavedSpotid, userId (FK → User), name, latitude, longitude (full precision), species, waterType, notes?, createdAt

Each saved spot belongs to exactly one user, and a user only ever sees their own spots (every query is scoped by userId). Unit tests mock Prisma; a separate live check confirms a real read/write against Neon.


🔐 Authentication & Login

  • 🔑 Passwords are bcrypt-hashed — never stored in plain text.
  • 👁️ The login form has a show/hide password toggle.
  • ✅ Only verified users can log in — an unverified account is blocked even with the correct password; a wrong password is rejected with a clear message.
  • 🍪 A session cookie keeps you signed in, and Log out is available on every page; logging out returns you to the login page.

📧 Email Verification (Resend)

New accounts start inactive and must verify a one-time code before they can be used. The flow:

1. ✍️  Sign up          → account created with isVerified = false; password bcrypt-hashed
2. 🔢  Generate code    → a random code is stored HASHED, with a future expiry timestamp
3. 📨  Send via Resend  → the code is emailed from a verified @anglercast.fyi sender
4. ⌨️  Enter the code   → on the /verify page
5. ✅  Activate         → correct & unexpired code flips isVerified = true and clears the code
6. 🚫  Guarded          → expired / wrong codes are rejected clearly; a verified account
                           cannot be verified again

The code-generation, hashing, expiry, and verification are all pure tested seams. The email send is mocked in unit tests, so the suite never sends a real email.


📍 Save a Spot

A logged-in angler can save the places they care about:

  • Create a spot with a name, coordinates, species, water type, and optional notes.
  • 📋 Read — see a list of your own saved spots (and only yours).
  • ✏️ Update — No updates on the cards
  • 🗑️ Delete — behind a confirmation dialog (below).

Coordinates are stored exactly as entered — at full precision, never rounded.

🗑️ Delete Confirmation Dialog

Deleting a spot opens a confirmation dialog first. The spot is removed only after you explicitly confirm; canceling closes the dialog and leaves the spot untouched. (Tested: confirm fires the delete handler exactly once; cancel fires nothing.)


🗺️ The Interactive Map

Built with Leaflet, the map renders the real recorded occurrences for a search.

🐟 Distinct species pins

Recorded locations are shown as pins on the map with fish pins.

🎯 Full-precision coordinates

The coordinate rule is critical and enforced everywhere:

  • 📐 Latitude and longitude are preserved at full decimal precision — no toFixed, no rounding, for storage, transport, queries, or display.
  • ⚠️ Rounding a coordinate moves the point miles off and is unsafe — so it's never done. A dedicated test proves a full-precision coordinate survives input → query unchanged.
  • 🗺️ The map is centered on the searched coordinates so it always matches the search, and records at 0,0 (Null Island) are excluded so no marker ever lands in the ocean by mistake.

📊 Empirical Sighting Rate

Pick a species, enter a full-precision latitude/longitude, and choose a month. AnglerCast queries the real occurrence records near those coordinates (GBIF + OBIS) and computes an honest historical sighting rate:

sighting rate  =  matching-month records ÷ total nearby records

It's always shown with:

  • 🔢 the sample size (total records that back the rate), and
  • 🚦 a high / low confidence flag based on how many records there are.

This is a historical sighting rate, never a fabricated "catch probability." The AI explains the number; the code computes it.


⛵ Travel Time (Explore)

The Explore page is a conditions-aware travel-time tool. A fisherman enters their origin coordinates, their destination coordinates (the fishing spot), their boat speed in knots, and the water type. AnglerCast then:

  • 📐 computes the distance from the full-precision coordinates (haversine, in nautical miles),
  • 🌊 fetches real conditions at the destination — Open-Meteo Marine + Forecast for saltwater, USGS + Open-Meteo Forecast for freshwater,
  • 🧠 asks the AI to estimate a conditions-aware ETA and explain the conditions in plain English, and
  • 🚦 guards the AI's number with a sanity check that rejects an impossible ETA.

The result shows the ETA, the distance, the AI's explanation, and the raw conditions it was computed from — so you always see what the estimate is based on.


🔄 Live Weekly Updates

The home page's "Top recorded spots this season" cards are computed live from real GBIF + OBIS occurrence records — each card surfaces the dominant month, the rate, the sample size, and a confidence flag, and the data is refreshed weekly. Nothing is hard-coded or invented.


🌐 Domain & Deployment

  • ☁️ Deployed on Vercel, with the production build regenerating the Prisma client on every deploy.
  • 🌍 Live on a custom domain — anglercast.fyi — with DNS managed in Cloudflare (A + CNAME records, SSL issued automatically).
  • 📧 The domain is verified with Resend (DKIM / SPF / DMARC), so verification emails deliver to real users from @anglercast.fyi.

🚀 Getting Started

# 1. Clone the repo
git clone https://github.com/jdbostonbu-ops/AnglerCast.git
cd AnglerCast

# 2. Install dependencies
npm install

# 3. Configure environment variables (.env)
#    DATABASE_URL      Neon Postgres pooled connection string
#    RESEND_API_KEY    Resend API key
#    EMAIL_FROM        verified sender, e.g. verify@anglercast.fyi
#    OPENAI_API_KEY    OpenAI key

# 4. Set up the database
npx prisma migrate dev

# 5. Run the dev server
npm run dev

Run the test suite:

npx vitest run

🔑 Environment Variables

Variable Purpose
DATABASE_URL Neon PostgreSQL pooled connection string
RESEND_API_KEY Resend API key for sending verification emails
EMAIL_FROM Verified sender address (e.g. verify@anglercast.fyi)
OPENAI_API_KEY OpenAI key for explanations + travel-time

Secrets live in .env (never committed) and are re-entered in Vercel's project settings for production.


📖 A snapshot at TDD Testing Before and After


💬 Feedback

Tried AnglerCast out on the water (or at your desk)? I'd genuinely love to hear what worked and what didn't.


👤 Author

Jacqueline Delgado

🔗 Repository: github.com/jdbostonbu-ops/AnglerCast 🌐 Live site: anglercast.fyi


⭐ Star This Repo

If AnglerCast is useful to you — or you just appreciate honest-data engineering and a fully test-first build — please give it a star. It genuinely helps. 🎣

Fish move constantly. AnglerCast just shows you the honest record.

About

AnglerCast is a Web App for fisherman who want to chase fish. The app uses various APIs to fetch historical sightings of freshwater and saltwater fish. Allow fishermen to save locations, and archive their favorite spots.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors