Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pip install --upgrade pip && pip install -r requirements.txt

**Dev server**: `npm run dev` at <http://localhost:8080>

**Environment** (optional): `NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.com/data/search-index.json` for alternate static data; `NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1` only enables the local dev re-index button
**Environment** (optional): `NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.ravensberg.org/data/search-index.json` for alternate static data; `NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1` only enables the local dev re-index button

**Code Style**: Biome formatter/linter (configured in `/biome.json`). TypeScript strict mode enabled. Use functional components with hooks.

Expand All @@ -50,7 +50,7 @@ pip install --upgrade pip && pip install -r requirements.txt
```bash
# Backend: docker build -t test ./backend && docker run -d -p 8000:8000 -e ENVIRONMENT=development test
# Wait 5s, then: curl http://localhost:8000/api/v1/health
# Frontend: docker build --build-arg NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.com/data/search-index.json -t test ./frontend
# Frontend: docker build --build-arg NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.ravensberg.org/data/search-index.json -t test ./frontend
# docker run -d -p 8080:80 test && sleep 3 && curl http://localhost:8080
```

Expand Down Expand Up @@ -98,7 +98,7 @@ Root: README.md, ARCHITECTURE.md, docker-compose.yml

## Key Implementation Notes

- **CORS**: Backend allows localhost:8080, hadiscover.com. Update `app/main.py` if forking.
- **CORS**: Backend allows localhost:8080, hadiscover.ravensberg.org. Update `app/main.py` if forking.
- **Rate limit**: /api/v1/index limited to once per 10min (in-memory).
- **Indexing**: Runs in background thread; returns immediately.
- **Parsing**: Best-effort YAML parser; gracefully handles errors. Supports both old (`trigger`/`action`/`platform`) and new (`triggers`/`actions`/`trigger`) Home Assistant YAML formats. Automatically deduplicates trigger types.
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/create-github-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ cat >"${RELEASE_NOTES_FILE}" <<EOF

### Runtime

hadiscover.com is served by GitHub Pages using the static search index generated by the Update Database workflow. No production backend API container is required.
hadiscover.ravensberg.org is served by GitHub Pages using the static search index generated by the Update Database workflow. No production backend API container is required.

${GENERATED_NOTES}
EOF
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docker-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.com/data/search-index.json
NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.ravensberg.org/data/search-index.json

- name: Test frontend container starts
run: |
Expand Down Expand Up @@ -153,7 +153,7 @@ jobs:
tags: hadiscover-frontend:test
cache-from: type=gha
build-args: |
NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.com/data/search-index.json
NEXT_PUBLIC_SEARCH_INDEX_URL=https://hadiscover.ravensberg.org/data/search-index.json

- name: Start both containers
run: .github/scripts/start-integration-containers.sh hadiscover-backend:test hadiscover-frontend:test
Expand Down
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ GitHub API
- Installs the database atomically at the configured SQLite `DATABASE_URL` path
- Validates the downloaded file with SQLite before replacing the local database
- Supports `DB_BOOTSTRAP_REQUIRED=true` to fail startup when the release database cannot be installed
- Retained for local/self-hosted API deployments; not used by hadiscover.com production
- Retained for local/self-hosted API deployments; not used by hadiscover.ravensberg.org production

## Database Schema

Expand Down Expand Up @@ -152,7 +152,7 @@ id, key (unique), value, updated_at

**Returns**: `{"status": "healthy"}`

**CORS**: Allows `localhost:8080` and `hadiscover.com`
**CORS**: Allows `http://localhost:8080` and `https://hadiscover.ravensberg.org`

## Key Design Decisions

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

A search engine for discovering Home Assistant automations shared on GitHub.

[Try it now at hadiscover.com](https://hadiscover.com/)
[Try it now at hadiscover.ravensberg.org](https://hadiscover.ravensberg.org/)

[![Screenshot of hadiscover.com web page in dark mode](frontend/assets/screenshot-dark.png)](https://hadiscover.com/)
[![Screenshot of hadiscover.ravensberg.org web page in dark mode](frontend/assets/screenshot-dark.png)](https://hadiscover.ravensberg.org/)

## What is it?

Expand Down
4 changes: 1 addition & 3 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,7 @@ backend/
The local/development API is configured to accept requests from:

- `http://localhost:8080` (local frontend)
- `http://127.0.0.1:8080` (local frontend)
- `https://hadiscover.com` (production frontend)
- `https://www.hadiscover.com` (production frontend)
- `https://hadiscover.ravensberg.org` (production frontend)

If you fork the repository, update the CORS origins in `app/main.py`.

Expand Down
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ async def lifespan(app: FastAPI):
# Configure CORS for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:8080", "https://hadiscover.com"],
allow_origins=["http://localhost:8080", "https://hadiscover.ravensberg.org"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Content-Type", "Authorization"],
Expand Down
55 changes: 55 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for API endpoints."""

import pytest
from app.main import app
from fastapi.testclient import TestClient

Expand All @@ -12,6 +13,60 @@ def test_health_endpoint():
assert response.json() == {"status": "healthy"}


@pytest.mark.parametrize(
"origin", ["http://localhost:8080", "https://hadiscover.ravensberg.org"]
)
def test_cors_allowed_origin(origin: str) -> None:
"""Allow the local and production frontend origins."""
client = TestClient(app)
response = client.get("/api/v1/health", headers={"Origin": origin})

assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == origin
assert response.headers["access-control-allow-credentials"] == "true"


@pytest.mark.parametrize(
"origin", ["http://localhost:8080", "https://hadiscover.ravensberg.org"]
)
@pytest.mark.parametrize("method", ["GET", "POST"])
def test_cors_preflight_allowed_origin(origin: str, method: str) -> None:
"""Allow preflight requests for supported frontend methods and headers."""
client = TestClient(app)
response = client.options(
"/api/v1/health",
headers={
"Origin": origin,
"Access-Control-Request-Method": method,
"Access-Control-Request-Headers": "Content-Type, Authorization",
},
)

assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == origin
assert response.headers["access-control-allow-credentials"] == "true"


def test_cors_disallowed_origin() -> None:
"""Do not grant cross-origin access to unrelated sites."""
client = TestClient(app)
response = client.get("/api/v1/health", headers={"Origin": "https://example.com"})

assert response.status_code == 200
assert "access-control-allow-origin" not in response.headers

preflight = client.options(
"/api/v1/health",
headers={
"Origin": "https://example.com",
"Access-Control-Request-Method": "GET",
},
)

assert preflight.status_code == 400
assert "access-control-allow-origin" not in preflight.headers


def test_root_endpoint():
"""Test root endpoint."""
client = TestClient(app)
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Production docker-compose configuration for the static frontend only.
# The public hadiscover.com deployment is served by GitHub Pages and does not
# The public hadiscover.ravensberg.org deployment is served by GitHub Pages and does not
# require a hadiscover-backend/API container.
# Usage: docker-compose -f docker-compose.prod.yml up -d

Expand All @@ -8,7 +8,7 @@ services:
build:
context: ./frontend
args:
NEXT_PUBLIC_SEARCH_INDEX_URL: ${NEXT_PUBLIC_SEARCH_INDEX_URL:-https://hadiscover.com/data/search-index.json}
NEXT_PUBLIC_SEARCH_INDEX_URL: ${NEXT_PUBLIC_SEARCH_INDEX_URL:-https://hadiscover.ravensberg.org/data/search-index.json}
container_name: web
ports:
- "8080:80"
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ services:
build:
context: ./frontend
args:
NEXT_PUBLIC_SEARCH_INDEX_URL: ${NEXT_PUBLIC_SEARCH_INDEX_URL:-https://hadiscover.com/data/search-index.json}
NEXT_PUBLIC_SEARCH_INDEX_URL: ${NEXT_PUBLIC_SEARCH_INDEX_URL:-https://hadiscover.ravensberg.org/data/search-index.json}

container_name: web
ports:
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Allow: /icon.png
Allow: /apple-icon.png

# Sitemap location (if you add one in the future)
# Sitemap: https://hadiscover.com/sitemap.xml
# Sitemap: https://hadiscover.ravensberg.org/sitemap.xml
Loading