Skip to content

Cauchon/HomeIndexr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HomeIndexr

Local-first dashboard for tracking home prices over time. Scrapes property data directly from Realtor.com's frontdoor/graphql endpoint server-side and stores the latest fetched state on each tracked property in SQLite. The Property view can also backfill Realtor historical AVMs, sparse market events, and tax assessment history. Refreshes record app-observed list-price changes for active listings.

Stack

  • Backend: FastAPI + sqlite3 (stdlib). Scraping is a thin GraphQL client over requests in backend/app/scraper.py.
  • Frontend: React (via UMD + Babel-standalone) served as static files by the backend. No build step.
  • Storage: data/app.db (SQLite, WAL mode). Auto-created on first run.

Setup

Use Python 3.12 for this project.

python3.12 -m venv .venv312
.venv312/bin/python -m pip install \
  fastapi==0.136.1 \
  uvicorn==0.47.0 \
  requests==2.34.1 \
  pydantic==2.13.4

Run locally

./run.sh
# or:
.venv312/bin/python -m uvicorn backend.app.main:app --reload --port 5173

Then open http://127.0.0.1:5173.

run.sh reads PORT (default 5173) and HOST (default 127.0.0.1). Set HOST=0.0.0.0 to reach the app from other devices on your network, e.g. an iPhone over Tailscale: HOST=0.0.0.0 ./run.sh.

Optional AI

AI features are optional and use DeepSeek when enabled. Keep API keys out of SQLite and source control:

cp .env.example .env
# edit .env and set:
# DEEPSEEK_API_KEY=...
# BRAVE_API_KEY=...   # optional, enables web search (see below)

The Admin panel stores only the ai_enabled toggle in SQLite. The backend detects keys from the process environment first, then from the ignored local .env file, and never returns them through the API.

The AI ask endpoint answers primarily from each property's stored context (prices, AVMs, events, taxes, schools). If BRAVE_API_KEY is set, the assistant can also run web searches for facts not in the stored data (neighborhood, local market, etc.), capped per question. Without it, geocoding tools still work and the assistant falls back to local context only.

API

Method Path Purpose
GET /api/properties List properties with current state
GET /api/admin/ai-settings AI enabled/key-present status
PATCH /api/admin/ai-settings Update non-secret AI settings
GET /api/properties/{id} Property + history + events + taxes + schools
POST /api/properties/{id}/ai/ask Ask an AI question about a property (local context + optional web search)
POST /api/properties Add property, refresh current state, and backfill history
PATCH /api/properties/{id} Edit address/display fields and active state
POST /api/properties/{id}/archive Hide from default dashboard and refresh-all
POST /api/properties/{id}/restore Restore an archived property
DELETE /api/properties/{id} Delete property plus history/events/taxes
POST /api/properties/{id}/refresh Refresh current property state
POST /api/properties/{id}/backfill Upsert AVMs, market events, taxes
POST /api/properties/refresh-all Refresh current state for active properties
POST /api/properties/backfill-all Backfill history/events/taxes for all records

POST /api/properties returns one of: matched, candidate_mismatch, no_candidates, error. A candidate_mismatch requires a second call with confirm_mismatch: true to save.

Data model

  • properties — one row per tracked address, including optional user-defined display name, the latest normalized AVM, list/sale prices, listing state, property facts/features, risk flags, match status, and raw Realtor JSON.
  • property_schools — the current Realtor school list for a property, replaced on add/refresh and returned as schools in the detail API response.
  • historical_estimates — monthly historical AVM series keyed by property, source, and date.
  • property_events — Realtor market events such as listed, sold, relisted, listing removed, and price changed.
  • observed_events — app-observed events detected during refresh, currently list-price increases/drops on the same active listing.
  • tax_history — yearly Realtor tax bills and county assessment values.

Archived properties stay in SQLite with their history intact, but the dashboard defaults to active rows and refresh-all skips archived rows. Delete is permanent and relies on SQLite foreign-key cascades to remove history, events, and taxes.

AVM normalization picks the "best" current estimate from either of the two shapes Realtor returns: raw["current_estimates"] (flat snake_case, returned by the legacy search-results path) or raw["estimates"]["currentValues"] (nested camelCase, returned by GetHomeDetails).

Listing state logic

The Properties page uses properties.listing_state, normalized server-side from the latest Realtor raw JSON. The dashboard buckets are:

  1. sold — explicit current status text indicates sold/closed, and the sale date is no more than 180 days old. This wins over stale pending_date values left on sold listings.
  2. pendingpending_date exists, or Realtor status text contains pending, contingent, or under contract, unless the current status is already sold/closed.
  3. for_sale — status text indicates for_sale, active, coming soon, or similar active listing state; as a fallback, a row with both listing_id and list_price is treated as for sale unless it has sold/closed cues.
  4. sold — sale price/date exists without active or pending cues, and the sale date is no more than 180 days old.
  5. off_market — no active/pending signal, or a sold/closed signal whose sale date is older than 180 days.

The sold window is intentionally finite so old Realtor sold records do not stay visually "Sold" forever on the Properties page. Change SOLD_TO_OFF_MARKET_DAYS in backend/app/scraper.py if you want a different threshold.

Property timeline

The Property page is event-oriented:

  • The chart renders Cotality and Quantarium as continuous AVM lines.
  • Realtor listing/sale/price-change records render in the timeline and ownership history strip.
  • App-observed refresh events render in the timeline separately from Realtor market history.
  • The ownership-history strip zooms out across recorded sales while the chart focuses on the denser AVM period.
  • The Timeline tab uses Date, Event, Value, and Change columns. Estimate rows keep low/high range inline with the estimate value; market rows show list/sale/price-change values independently.

Newly added properties populate historical_estimates, property_events, and tax_history before the add response returns. Use Backfill history on a Property page to retry or refresh those imported history rows later.

Manual refreshes compare the previous stored list price with the newly fetched list price. If the property stays on the same active listing and the price changes, the app writes an observed_events row such as Price dropped or Price increased.

Tests

Run the backend unittest suite from the repo root:

PYTHONPATH=backend .venv312/bin/python -m unittest discover -s backend -p 'test_*.py'

Use the manual smoke flow when touching live Realtor fetch behavior:

./run.sh &
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"address":"5907 Cape Hatteras Dr, Houston, TX 77041"}' \
  http://127.0.0.1:5173/api/properties
curl -s http://127.0.0.1:5173/api/properties

Admin panel

The gear icon in the top bar opens the Admin panel (#admin). The first admin function is Refresh jobs, which shows:

  • latest sweep and active-property counts
  • current properties with match/errors/no-candidate issues
  • a recent manual job log stored in localStorage
  • a cadence selector, defaulting to twice per month

The Refresh active now button calls POST /api/properties/refresh-all and then reloads current property state. The cadence selector is stored in localStorage; it does not start background work inside FastAPI. Additional admin functions can be added alongside Refresh jobs later.

Scheduled refreshes

Scheduled refreshes are not implemented in this checkout. The backend exposes the hook that an external scheduler should call:

curl -s -X POST http://127.0.0.1:5173/api/properties/refresh-all

Keep the HomeIndexr server running on the same port the external job calls. If scheduling is added later, use cron, launchd, or another runner outside the FastAPI process; do not add an internal background loop to the app server.

Auth

None. Local-only for v1. The frontend talks to the backend over plain HTTP, and the backend has no user model — easy to bolt a session layer on top later without disturbing storage.

About

Self-hosted home price tracker. Populate list prices, AVMs, market events, and tax history, stores it in SQLite, and charts changes over time. FastAPI + React, no build step.

Resources

Stars

Watchers

Forks

Contributors