Zenith

Solar intelligence dashboard for site assessment

On-goingPersonalSolo full-stack engineer6 min read

I built Zenith as a personal, exploratory full-stack project: a Next.js dashboard and Flask API that turn a site location into climatology-based predictions, deterministic financial and engineering figures, and Gemini explanations grounded in a small document set. Predictions stay on sklearn; the LLM only explains or answers with citations, or refuses.

Stack · Next.js · React 19 · TypeScript · Tailwind CSS · shadcn/ui · Recharts · Leaflet · Motion · Python · Flask · scikit-learn · Google Gemini · LangChain · ChromaDB · Vercel · Render

Images coming soon

The brief

Zenith is a personal solar intelligence dashboard I built end to end. You pick a site, run an analysis, and get irradiance Predictions, payback-style financials, engineering estimates, and a natural-language explanation that points back at source documents.

I own the Next.js client, the Flask API, the ML serving path, the RAG ask flow, and the Vercel plus Render deploy. It is exploratory: I am using it to learn how a full stack AI product should behave when numbers and language sit next to each other.

The early sklearn models were trained on NASA POWER weather features for a known solar park. That training work is background; the product story from 2026 onward is the dashboard, the analysis pipeline, and the boundary between ML and the LLM.

What I'm solving

Site assessment tools either spit out opaque numbers or let a chat model invent them. For homeowners, planners, and small industry sites, both fail in different ways: you cannot defend a black box, and you should not stake payback on a paragraph that rewrote the irradiance.

I wanted one flow that:

  • Starts from a map pin and system assumptions
  • Keeps Predictions and finance on deterministic code
  • Uses an LLM only to explain and answer domain questions with citations
  • Still works for Prediction if Gemini is down

Guest Mode stores locations and runs in the browser so you can try the product without an account. Sign-in is not wired yet.

How it works

You land on Zenith, open the dashboard, set coordinates and capacity, then run analysis. Progress tracks stages: location climatology, Prediction, physics, financials, then explanation. Results show charts, cards for output and payback, and an explanation panel. A separate ask path answers solar questions against a small indexed knowledge base and can open the cited markdown.

Saved locations and run history live in Guest Mode so you can compare sites and glance at portfolio-style analytics across analyses you already ran. When the LLM path fails or has no grounding, the UI says so; it does not invent answers to keep the screen looking finished.

Architecture choices

The client is Next.js (App Router), React 19, TypeScript, Tailwind, and shadcn. Charts use Recharts; the map uses Leaflet. The API is Flask on Python with gunicorn in production. Predictions use scikit-learn. Explanation and RAG go through Gemini, with LangChain and Chroma for retrieval. Frontend deploys on Vercel; API on Render.

My design rule is simple: sklearn owns the numbers; Gemini owns language over those numbers or over retrieved docs. /predict and the deterministic stages never call the model. /explain and /ask must ground or refuse. Structured prompts and JSON-shaped responses make failures easier to surface in the UI.

A few moments that mattered:

When I first put explain next to Prediction, the obvious temptation was one "AI summary" that also restated the kWh. That is how invented numbers sneak in. I split the services so the LLM receives the Prediction result and is told not to invent or revise it. If Gemini is unavailable, analysis still returns; explanation degrades.

For hosting, Render's free tier pushed hard choices: train or load models at build, keep physics on an analytical path by default, and treat RAG as installable extras so /ask can 503 cleanly instead of OOMing the box. Product-on RAG is the goal; graceful unavailability is the contract when extras or quota fail.

On the client, streaming the plan over SSE made the long pipeline feel like a real run instead of a silent spinner. Falling back to POST /analyze covers environments where the stream is awkward. Guest preferences and location history stay local so the UX is usable before auth exists.

NASA POWER climatology feeds features for a pin; the models themselves are the older training track. I mention that only so the Prediction path is honest about its data story, not because the write-up is about fitting regressions.

What shipped

Shipped and public:

  • Live dashboard at the Vercel URL, API on Render
  • Open source repo with the full client and backend
  • Analysis flow from location through Prediction, physics, financials, and explanation
  • RAG ask with document listing and source viewing
  • Guest Mode locations, run history, and portfolio-style views over saved analyses
  • Landing and about copy that state the ML versus LLM split in plain language

Still open: real auth, deeper multi-location product features, and continued UI hardening. Status is on-going.

What I'd do again

Draw the ML and LLM boundary in code and prompts before polishing the chat UI. Once users see a fluent paragraph next to a number, they assume the paragraph produced it.

Degrade in public. A clear "explanation unavailable" or "not enough context" builds more trust than a confident filler answer.

Ship a guest path early. For a personal product with no login yet, local storage for sites and runs is enough to demonstrate the loop.

Treat deploy constraints as product design. Free-tier memory limits forced thinner dependency sets and explicit feature flags; that kept Prediction reliable when the language stack could not load.