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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-07
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Context

Grogbot currently has a search core library plus CLI and API packages, but no browser-facing frontend. The desired first web experience is intentionally small: a root page for the domain, a Search landing page at `/search`, and a Search results page at `/search/query?q=...`.

The repository is currently Python-based and already uses FastAPI/ASGI patterns. The new web package should fit that ecosystem, render HTML on the server, and read from the server-side replicated SQLite database through `SearchService`. This change explicitly avoids changing the existing `packages/api` package so that the first frontend slice stays focused on human-facing pages instead of JSON API redesign.

## Goals / Non-Goals

**Goals:**
- Add a new `packages/web` package that provides a Python-rendered frontend.
- Provide domain-level HTML routes for `/`, `/search`, and `/search/query`.
- Render the initial search experience on the server, including the first page of results.
- Reuse `SearchService` directly so web and other surfaces share the same search behavior and database configuration.
- Keep the implementation small and easy to evolve into additional top-level systems later.

**Non-Goals:**
- Changing or removing any routes from `packages/api`.
- Introducing a JavaScript-heavy SPA architecture.
- Redesigning search ranking or converting chunk results into document-deduplicated results.
- Defining a final unified public deployment topology for web and API on the same host.

## Decisions

### 1. Build `packages/web` as a Python-rendered ASGI package
- Decision: The frontend will be a Python package in the uv workspace, using server-rendered templates and static asset serving.
- Why: This matches the current Python-only repository, avoids introducing a second toolchain, and is sufficient for the initial search experience.
- Alternative considered: adding a JS/TS frontend package; rejected for now because the first scope is simple and does not justify extra build and deployment complexity.

### 2. Call `SearchService` directly from web routes
- Decision: The web package will resolve configuration the same way as CLI/API and will execute searches through `SearchService` directly.
- Why: This avoids internal HTTP calls, reduces latency and coupling, and keeps the API package out of scope.
- Alternative considered: rendering pages by calling the existing API over HTTP; rejected because it adds unnecessary indirection and would couple page rendering to API route design.

### 3. Use top-level human-facing routes
- Decision: The web package will own `/`, `/search`, and `/search/query`.
- Why: These routes match the intended product shape where each Grogbot system lives at a top-level path segment and Search owns its main interface directly under `/search`.
- Alternative considered: nesting Search UI deeper under another prefix; rejected because it weakens the intended domain structure.

### 4. Server-render `/search/query` from the query string
- Decision: `GET /search/query?q=...` will render HTML with results already present in the response.
- Why: This keeps the first version simple, makes URLs shareable, works without JavaScript, and aligns with the familiar search-engine interaction model.
- Alternative considered: serving an app shell that fetches results client-side after load; rejected because it adds complexity without clear benefit for v1.

### 5. Redirect empty queries back to `/search`
- Decision: Requests to `/search/query` with a missing or blank `q` parameter will redirect to `/search` rather than rendering an empty results page.
- Why: The landing page is the canonical empty-search experience, and redirecting keeps the route semantics clean.
- Alternative considered: rendering a no-results or empty-state page at `/search/query`; rejected because it duplicates the landing-page role.

### 6. Preserve current chunk-level result behavior in the web UI
- Decision: The results page will display the top 25 results returned by `SearchService.search(..., limit=25)` in service order, even when that means duplicate documents appear.
- Why: This keeps the frontend aligned with current engine behavior and avoids introducing document-grouping semantics in the first web change.
- Alternative considered: deduplicating or regrouping results by document in the web layer; rejected for now because it changes presentation semantics and raises ranking questions outside this change.

### 7. Keep deployment coupling loose
- Decision: The design defines the web package and its route behavior, but does not require a specific merged deployment with the existing API package.
- Why: The current package scope is intentionally limited to web. Deployment composition can be decided later without blocking implementation of the frontend itself.
- Alternative considered: coupling this design to an `/api/*` migration or combined host strategy; rejected because that would expand scope into API redesign.

## Risks / Trade-offs

- **[Risk] Route collisions with the existing API if both are later exposed on the same host/path space** → **Mitigation:** treat deployment composition as a later decision and keep API changes out of this change.
- **[Risk] Duplicate documents in results may feel less polished than mainstream search engines** → **Mitigation:** accept this as an explicit v1 trade-off and revisit grouped results in a later change if needed.
- **[Risk] Server-rendered templates may need refactoring if the frontend becomes highly interactive later** → **Mitigation:** keep presentation concerns isolated in `packages/web` so richer client-side behavior can be added incrementally.
- **[Risk] Web requests depend directly on database availability and search-service performance** → **Mitigation:** reuse existing service/config patterns and keep the initial page design lightweight.

## Migration Plan

1. Add `packages/web` to the workspace and create its ASGI entrypoint, templates, and static assets.
2. Implement root and search routes using existing config resolution and `SearchService` access.
3. Deploy the new web package in the chosen environment without changing `packages/api` behavior.
4. If rollback is needed, undeploy or disable the web package; no data migration is required because this change only adds a read-only presentation layer.

## Open Questions

- No blocking product questions remain for this first slice. Public coexistence with the existing API can be decided in a later change if both need to share a single hostname.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Why

Grogbot currently exposes search through the CLI and API, but it does not provide a human-facing web interface. Adding a simple web frontend now creates the first top-level system experience on `www.grogbot.com`, lets users search through a browser without going through the JSON API, and establishes the package and route pattern for future systems.

## What Changes

- Add a new Python-rendered `packages/web` package as a web frontend surface for Grogbot.
- Add a root web page at `/` that acts as the top-level domain entry point for Grogbot systems.
- Add a search landing page at `/search` with a “Grogbot Search” heading, query input, and Search button.
- Add a search results page at `/search/query?q=...` that renders the top 25 search results and includes a query input at the top for running another search.
- Make the web package use `SearchService` directly against the server-side replicated SQLite database instead of going through the API package.
- Redirect `/search/query` requests with a missing or blank `q` parameter back to `/search`.
- Allow duplicate documents in initial results when multiple chunks from the same document rank in the top 25.
- Keep the existing `packages/api` package unchanged and out of scope for this change.

## Capabilities

### New Capabilities
- `search-web`: Python-rendered web pages for Grogbot root navigation and the Search landing/results experience.

### Modified Capabilities
- None.

## Impact

- New package: `packages/web`.
- New Python web dependencies for HTML rendering and static asset serving.
- New public HTML routes at `/`, `/search`, and `/search/query`.
- New templates/static assets for the web frontend.
- No API contract changes and no `packages/api` modifications in this change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## ADDED Requirements

### Requirement: Grogbot root page
The system SHALL provide an HTML page at `/` that serves as the top-level Grogbot domain entry point.

#### Scenario: Root page is available
- **WHEN** a browser requests `GET /`
- **THEN** the system returns an HTML page for the Grogbot root experience

### Requirement: Search landing page
The system SHALL provide an HTML search landing page at `/search` that displays the title "Grogbot Search", a text input for the query, and a Search submit control.

#### Scenario: Search landing page renders form
- **WHEN** a browser requests `GET /search`
- **THEN** the system returns an HTML page containing the text "Grogbot Search"
- **THEN** the page contains a query input field
- **THEN** the page contains a Search submit control

### Requirement: Search results page
The system SHALL provide an HTML search results page at `/search/query?q=<text>` that renders the top 25 search results for the query and includes a query input at the top for running another search.

#### Scenario: Search results page renders ranked results
- **WHEN** a browser requests `GET /search/query?q=hello+world`
- **THEN** the system returns an HTML page containing a query input at the top
- **THEN** the page displays up to 25 ranked search results for `hello world`

#### Scenario: Duplicate documents are preserved in v1 results
- **WHEN** the top 25 ranked search results contain multiple chunks from the same document
- **THEN** the page renders those results in ranked order without deduplicating by document

### Requirement: Empty search redirects to landing page
The system SHALL redirect requests for `/search/query` without a non-blank `q` parameter to `/search`.

#### Scenario: Missing query redirects to search landing
- **WHEN** a browser requests `GET /search/query` without a `q` parameter
- **THEN** the system responds with a redirect to `/search`

#### Scenario: Blank query redirects to search landing
- **WHEN** a browser requests `GET /search/query?q= `
- **THEN** the system responds with a redirect to `/search`
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## 1. Workspace and package setup

- [x] 1.1 Add `packages/web` to the uv workspace and create the package metadata/build configuration.
- [x] 1.2 Create the web package module structure, including the ASGI entrypoint, template directory, and static asset directory.
- [x] 1.3 Add the Python web rendering/static-serving dependencies needed by the new package.

## 2. Root and landing page implementation

- [x] 2.1 Implement the root `/` HTML route for the top-level Grogbot entry page.
- [x] 2.2 Implement the `/search` HTML route with the “Grogbot Search” heading, query input, and Search submit control.
- [x] 2.3 Add the shared base layout and initial CSS needed for the root and search landing pages.

## 3. Search results page integration

- [x] 3.1 Implement the `/search/query` HTML route that reads the `q` parameter and redirects blank or missing queries to `/search`.
- [x] 3.2 Integrate the results route with `SearchService` using the existing configuration/database resolution pattern.
- [x] 3.3 Render the top 25 search results on the results page, preserving service order and allowing duplicate documents.
- [x] 3.4 Add the results-page search box at the top so users can submit a new query from the results screen.

## 4. Verification

- [x] 4.1 Add automated tests for `/`, `/search`, and `/search/query` covering HTML rendering and redirect behavior.
- [x] 4.2 Add automated tests verifying `/search/query` renders up to 25 results and preserves duplicate-document results.
- [x] 4.3 Run the relevant test suite(s) and confirm the new web package works without changing `packages/api`.
30 changes: 30 additions & 0 deletions packages/web/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[project]
name = "grogbot-web"
version = "0.1.0"
description = "Grogbot Python-rendered web frontend"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.110",
"grogbot-search-core",
"jinja2>=3.1",
"uvicorn>=0.29",
]

[project.optional-dependencies]
test = [
"httpx>=0.27",
"pytest>=8.0",
]

[build-system]
requires = ["hatchling>=1.24"]
build-backend = "hatchling.build"

[tool.hatch.build]
include = [
"src/grogbot_web/templates/*.html",
"src/grogbot_web/static/*.css",
]

[tool.hatch.build.targets.wheel]
packages = ["src/grogbot_web"]
5 changes: 5 additions & 0 deletions packages/web/src/grogbot_web/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Grogbot web frontend package."""

from grogbot_web.app import app

__all__ = ["app"]
64 changes: 64 additions & 0 deletions packages/web/src/grogbot_web/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import annotations

from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from grogbot_search import SearchService, load_config

PACKAGE_DIR = Path(__file__).resolve().parent
TEMPLATES_DIR = PACKAGE_DIR / "templates"
STATIC_DIR = PACKAGE_DIR / "static"

app = FastAPI(title="Grogbot Web")
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR)), name="assets")
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))


def search_results(query: str, *, limit: int = 25):
config = load_config()
with SearchService(config.db_path) as service:
return service.search(query, limit=limit)


@app.get("/", response_class=HTMLResponse)
def root_page(request: Request):
return templates.TemplateResponse(
request,
"index.html",
{
"page_title": "Grogbot",
},
)


@app.get("/search", response_class=HTMLResponse)
def search_page(request: Request, q: str = ""):
return templates.TemplateResponse(
request,
"search_landing.html",
{
"page_title": "Grogbot Search",
"query": q.strip(),
},
)


@app.get("/search/query", response_class=HTMLResponse)
def search_query_page(request: Request, q: str | None = None):
query = (q or "").strip()
if not query:
return RedirectResponse(url="/search", status_code=302)

results = search_results(query, limit=25)
return templates.TemplateResponse(
request,
"search_results.html",
{
"page_title": f"{query} - Grogbot Search",
"query": query,
"results": results,
},
)
Loading