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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [0.1.1] - 2026-04-28

### Added

- Optional SQLAlchemy-backed registration assistance and maintenance triage workpaper records via `CIVICPARKS_WORKPAPER_DB_URL`.
- Registration assistance and maintenance triage retrieval endpoints for persisted records.

### Changed

- Dependency-alignment release: moved CivicParks to `civiccore==0.3.0` while preserving the existing v0.1.0 runtime foundation behavior.
Expand Down
22 changes: 22 additions & 0 deletions PRODUCTION_DEPTH_PARKS_WORKPAPER_PERSISTENCE_DONE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Production Depth: Parks Workpaper Persistence

## Summary

CivicParks now supports optional SQLAlchemy-backed registration assistance and maintenance triage records through `CIVICPARKS_WORKPAPER_DB_URL`.

## Shipped

- `ParksWorkpaperRepository` with schema-aware SQLAlchemy tables.
- Persisted registration assistance records with `assistance_id`.
- Persisted maintenance triage records with `triage_id`.
- Retrieval endpoints for both persisted workpapers.
- Actionable `503` guidance when persistence is not configured.

## Still Not Shipped

- Payments.
- Registrations or participant records.
- Reservation writes.
- Crew dispatch.
- Live LLM calls.
- Parks connector runtime.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CivicParks

CivicParks v0.1.1 ships the municipal parks and recreation support foundation for CivicSuite: cited parks policy Q&A, program and facility Q&A, registration assistance that links to existing systems, maintenance request triage for Civic311-style handoff, FastAPI runtime, public sample UI, docs, tests, browser QA, and release gates.
CivicParks v0.1.1 ships the municipal parks and recreation support foundation for CivicSuite: cited parks policy Q&A, program and facility Q&A, registration assistance that links to existing systems, optional database-backed registration/maintenance workpapers, maintenance request triage for Civic311-style handoff, FastAPI runtime, public sample UI, docs, tests, browser QA, and release gates.

It is not a registration system, payment processor, reservation system, participant-record store, work-order system, live LLM runtime, or parks connector.

Expand All @@ -13,4 +13,6 @@ python -m uvicorn civicparks.main:app --host 127.0.0.1 --port 8143

CivicParks v0.1.1 is pinned to `civiccore==0.3.0`.

Set `CIVICPARKS_WORKPAPER_DB_URL` to persist registration assistance and maintenance triage records. Without it, CivicParks remains deterministic and stateless.

Apache 2.0 code. CC BY 4.0 docs.
4 changes: 3 additions & 1 deletion README.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
CivicParks

CivicParks v0.1.1 ships municipal parks and recreation support foundations: cited parks policy Q&A, program and facility Q&A, registration assistance that links to existing systems, maintenance request triage for Civic311-style handoff, FastAPI runtime, public sample UI, docs, tests, browser QA, and release gates.
CivicParks v0.1.1 ships municipal parks and recreation support foundations: cited parks policy Q&A, program and facility Q&A, registration assistance that links to existing systems, optional database-backed registration/maintenance workpapers, maintenance request triage for Civic311-style handoff, FastAPI runtime, public sample UI, docs, tests, browser QA, and release gates.

It is not a registration system, payment processor, reservation system, participant-record store, work-order system, live LLM runtime, or parks connector.

Expand All @@ -10,4 +10,6 @@ python -m uvicorn civicparks.main:app --host 127.0.0.1 --port 8143

CivicParks v0.1.1 is pinned to civiccore==0.3.0.

Set CIVICPARKS_WORKPAPER_DB_URL to persist registration assistance and maintenance triage records. Without it, CivicParks remains deterministic and stateless.

Apache 2.0 code. CC BY 4.0 docs.
4 changes: 3 additions & 1 deletion USER-MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Non-Technical Staff

CivicParks helps parks and recreation staff prepare cited policy answers, answer program and facility questions, draft registration guidance that links residents to existing systems, and triage maintenance requests for staff review.
CivicParks helps parks and recreation staff prepare cited policy answers, answer program and facility questions, draft registration guidance that links residents to existing systems, retrieve optional saved registration/maintenance workpapers, and triage maintenance requests for staff review.

Staff remain responsible for every answer, registration note, facility rule, and maintenance handoff. CivicParks does not process payments, enroll participants, manage participant records, reserve facilities, dispatch crews, replace Civic311, or replace recreation registration systems.

Expand All @@ -17,6 +17,8 @@ python -m uvicorn civicparks.main:app --host 127.0.0.1 --port 8143

Runtime dependency: `civiccore==0.3.0`.

Optional persistence: set `CIVICPARKS_WORKPAPER_DB_URL` to enable SQLAlchemy-backed registration assistance and maintenance triage records. Leave it unset for deterministic stateless operation.

Primary endpoints:

- `GET /health` - service and CivicCore version.
Expand Down
4 changes: 2 additions & 2 deletions USER-MANUAL.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CivicParks User Manual

For staff: use CivicParks to draft cited parks policy answers, answer program and facility questions, prepare registration-link guidance, and triage maintenance requests. Staff approve every output before use.
For staff: use CivicParks to draft cited parks policy answers, answer program and facility questions, prepare registration-link guidance, retrieve optional saved registration/maintenance workpapers, and triage maintenance requests. Staff approve every output before use.

Boundaries: CivicParks does not process payments, enroll participants, manage participant records, reserve facilities, dispatch crews, replace Civic311, or replace recreation registration systems.

For IT: install with python -m pip install -e ".[dev]" and run with python -m uvicorn civicparks.main:app --host 127.0.0.1 --port 8143. Depends on civiccore==0.3.0.
For IT: install with python -m pip install -e ".[dev]" and run with python -m uvicorn civicparks.main:app --host 127.0.0.1 --port 8143. Depends on civiccore==0.3.0. Set CIVICPARKS_WORKPAPER_DB_URL to enable SQLAlchemy-backed registration assistance and maintenance triage records.
67 changes: 63 additions & 4 deletions civicparks/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""FastAPI runtime foundation for CivicParks."""

import os

from civiccore import __version__ as CIVICCORE_VERSION
from fastapi import FastAPI, Response
from fastapi import FastAPI, HTTPException, Response
from fastapi.responses import HTMLResponse
from pydantic import BaseModel

from civicparks import __version__
from civicparks.maintenance import triage_maintenance_request
from civicparks.policy import ParksPolicySource, answer_policy_question
from civicparks.persistence import ParksWorkpaperRepository, StoredMaintenanceTriage, StoredRegistrationAssistance
from civicparks.programs import ParksProgram, answer_program_question
from civicparks.public_ui import render_public_lookup_page
from civicparks.registration import RecreationProgram, draft_registration_assistance
Expand All @@ -18,6 +21,9 @@
description="Parks and recreation policy, program, registration-link, and maintenance-triage foundation.",
)

_workpaper_repository: ParksWorkpaperRepository | None = None
_workpaper_db_url: str | None = None

@app.get("/favicon.ico", include_in_schema=False)
def favicon() -> Response:
"""Return an empty favicon response so browser QA has a clean console."""
Expand Down Expand Up @@ -60,7 +66,8 @@ def root() -> dict[str, str]:
"status": "parks and recreation support foundation",
"message": (
"CivicParks policy Q&A, program Q&A, registration-link assistance, "
"maintenance triage, and public UI foundation are online; payments, "
"maintenance triage, optional database-backed registration/maintenance workpapers, "
"and public UI foundation are online; payments, "
"registrations, participant records, reservation writes, crew dispatch, "
"live LLM calls, and connector runtime are not implemented yet."
),
Expand Down Expand Up @@ -95,9 +102,61 @@ def program_answer(request: ProgramQuestionRequest) -> dict[str, object]:

@app.post("/api/v1/civicparks/registration-assistance")
def registration_assistance(request: RegistrationAssistanceRequest) -> dict[str, object]:
return draft_registration_assistance(request.program).__dict__
if _workpaper_database_url() is not None:
return _stored_registration_response(_get_workpaper_repository().create_registration(program=request.program))
payload = draft_registration_assistance(request.program).__dict__
payload["assistance_id"] = None
return payload

@app.get("/api/v1/civicparks/registration-assistance/{assistance_id}")
def get_registration_assistance(assistance_id: str) -> dict[str, object]:
if _workpaper_database_url() is None:
raise HTTPException(status_code=503, detail={"message":"CivicParks workpaper persistence is not configured.","fix":"Set CIVICPARKS_WORKPAPER_DB_URL to retrieve persisted registration assistance."})
stored = _get_workpaper_repository().get_registration(assistance_id)
if stored is None:
raise HTTPException(status_code=404, detail={"message":"Registration assistance record not found.","fix":"Use an assistance_id returned by POST /api/v1/civicparks/registration-assistance."})
return _stored_registration_response(stored)


@app.post("/api/v1/civicparks/maintenance-triage")
def maintenance_triage(request: MaintenanceTriageRequest) -> dict[str, object]:
return triage_maintenance_request(request.issue, request.location).__dict__
if _workpaper_database_url() is not None:
return _stored_maintenance_response(_get_workpaper_repository().create_maintenance(issue=request.issue, location=request.location))
payload = triage_maintenance_request(request.issue, request.location).__dict__
payload["triage_id"] = None
return payload

@app.get("/api/v1/civicparks/maintenance-triage/{triage_id}")
def get_maintenance_triage(triage_id: str) -> dict[str, object]:
if _workpaper_database_url() is None:
raise HTTPException(status_code=503, detail={"message":"CivicParks workpaper persistence is not configured.","fix":"Set CIVICPARKS_WORKPAPER_DB_URL to retrieve persisted maintenance triage records."})
stored = _get_workpaper_repository().get_maintenance(triage_id)
if stored is None:
raise HTTPException(status_code=404, detail={"message":"Maintenance triage record not found.","fix":"Use a triage_id returned by POST /api/v1/civicparks/maintenance-triage."})
return _stored_maintenance_response(stored)

def _workpaper_database_url() -> str | None:
return os.environ.get("CIVICPARKS_WORKPAPER_DB_URL")

def _get_workpaper_repository() -> ParksWorkpaperRepository:
global _workpaper_db_url, _workpaper_repository
db_url = _workpaper_database_url()
if db_url is None:
raise RuntimeError("CIVICPARKS_WORKPAPER_DB_URL is not configured.")
if _workpaper_repository is None or db_url != _workpaper_db_url:
_dispose_workpaper_repository()
_workpaper_db_url = db_url
_workpaper_repository = ParksWorkpaperRepository(db_url=db_url)
return _workpaper_repository

def _dispose_workpaper_repository() -> None:
global _workpaper_repository
if _workpaper_repository is not None:
_workpaper_repository.engine.dispose()
_workpaper_repository = None

def _stored_registration_response(stored: StoredRegistrationAssistance) -> dict[str, object]:
return {**stored.__dict__, "created_at": stored.created_at.isoformat()}

def _stored_maintenance_response(stored: StoredMaintenanceTriage) -> dict[str, object]:
return {**stored.__dict__, "created_at": stored.created_at.isoformat()}
74 changes: 74 additions & 0 deletions civicparks/persistence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime
from uuid import uuid4

import sqlalchemy as sa
from sqlalchemy import Engine, create_engine

from civicparks.maintenance import triage_maintenance_request
from civicparks.registration import RecreationProgram, draft_registration_assistance


metadata = sa.MetaData()
registration_records = sa.Table("registration_records", metadata, sa.Column("assistance_id", sa.String(36), primary_key=True), sa.Column("program_title", sa.String(255), nullable=False), sa.Column("registration_url", sa.Text(), nullable=False), sa.Column("assistance_text", sa.Text(), nullable=False), sa.Column("staff_review_required", sa.Boolean(), nullable=False), sa.Column("boundary", sa.Text(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), schema="civicparks")
maintenance_records = sa.Table("maintenance_records", metadata, sa.Column("triage_id", sa.String(36), primary_key=True), sa.Column("issue", sa.Text(), nullable=False), sa.Column("suggested_category", sa.String(160), nullable=False), sa.Column("civic311_handoff_ready", sa.Boolean(), nullable=False), sa.Column("staff_review_required", sa.Boolean(), nullable=False), sa.Column("boundary", sa.Text(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), schema="civicparks")


@dataclass(frozen=True)
class StoredRegistrationAssistance:
assistance_id: str
program_title: str
registration_url: str
assistance_text: str
staff_review_required: bool
boundary: str
created_at: datetime


@dataclass(frozen=True)
class StoredMaintenanceTriage:
triage_id: str
issue: str
suggested_category: str
civic311_handoff_ready: bool
staff_review_required: bool
boundary: str
created_at: datetime


class ParksWorkpaperRepository:
def __init__(self, *, db_url: str | None = None, engine: Engine | None = None) -> None:
base_engine = engine or create_engine(db_url or "sqlite+pysqlite:///:memory:", future=True)
if base_engine.dialect.name == "sqlite":
self.engine = base_engine.execution_options(schema_translate_map={"civicparks": None})
else:
self.engine = base_engine
with self.engine.begin() as connection:
connection.execute(sa.text("CREATE SCHEMA IF NOT EXISTS civicparks"))
metadata.create_all(self.engine)

def create_registration(self, *, program: RecreationProgram) -> StoredRegistrationAssistance:
draft = draft_registration_assistance(program)
stored = StoredRegistrationAssistance(str(uuid4()), draft.program_title, draft.registration_url, draft.assistance_text, draft.staff_review_required, draft.boundary, datetime.now(UTC))
with self.engine.begin() as connection:
connection.execute(registration_records.insert().values(**stored.__dict__))
return stored

def get_registration(self, assistance_id: str) -> StoredRegistrationAssistance | None:
with self.engine.begin() as connection:
row = connection.execute(sa.select(registration_records).where(registration_records.c.assistance_id == assistance_id)).mappings().first()
return None if row is None else StoredRegistrationAssistance(**dict(row))

def create_maintenance(self, *, issue: str, location: str) -> StoredMaintenanceTriage:
draft = triage_maintenance_request(issue, location)
stored = StoredMaintenanceTriage(str(uuid4()), draft.issue, draft.suggested_category, draft.civic311_handoff_ready, draft.staff_review_required, draft.boundary, datetime.now(UTC))
with self.engine.begin() as connection:
connection.execute(maintenance_records.insert().values(**stored.__dict__))
return stored

def get_maintenance(self, triage_id: str) -> StoredMaintenanceTriage | None:
with self.engine.begin() as connection:
row = connection.execute(sa.select(maintenance_records).where(maintenance_records.c.triage_id == triage_id)).mappings().first()
return None if row is None else StoredMaintenanceTriage(**dict(row))
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Browser QA: Production Depth Parks Workpaper Persistence

Target: file:///C:/Users/scott/OneDrive/Desktop/Claude/civicparks/docs/index.html

## desktop

Screenshot: C:\Users\scott\OneDrive\Desktop\Claude\civicparks\docs\browser-qa-production-depth-parks-workpaper-persistence-desktop.png

- versionVisible: PASS
- persistenceVisible: PASS
- envVisible: PASS
- boundaryVisible: PASS
- repoVisible: PASS
- consoleClean: PASS
- console messages: none

## mobile

Screenshot: C:\Users\scott\OneDrive\Desktop\Claude\civicparks\docs\browser-qa-production-depth-parks-workpaper-persistence-mobile.png

- versionVisible: PASS
- persistenceVisible: PASS
- envVisible: PASS
- boundaryVisible: PASS
- repoVisible: PASS
- consoleClean: PASS
- console messages: none
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<main>
<span class="badge">Shipping v0.1.1</span>
<h1>Parks help that keeps registration systems authoritative.</h1>
<p>CivicParks helps staff draft cited park-rule answers, explain programs and facilities, link residents to existing registration systems, and triage maintenance requests for staff review.</p>
<p>CivicParks helps staff draft cited park-rule answers, explain programs and facilities, link residents to existing registration systems, persist optional registration/maintenance workpapers, and triage maintenance requests for staff review.</p>
<section class="grid">
<article class="card"><h2>For Staff</h2><p>Draft policy answers, program replies, registration guidance, and maintenance categories that staff verify before use.</p></article>
<article class="card"><h2>For IT</h2><p>FastAPI runtime pinned to <code>civiccore==0.3.0</code>, deterministic tests, and no registration-system writes.</p></article>
<article class="card"><h2>For IT</h2><p>FastAPI runtime pinned to <code>civiccore==0.3.0</code>, optional persistence with <code>CIVICPARKS_WORKPAPER_DB_URL</code>, deterministic tests, and no registration-system writes.</p></article>
<article class="card"><h2>Boundary</h2><p>No payments, registrations, participant records, reservation writes, crew dispatch, live LLM calls, or connector runtime.</p></article>
</section>
<section class="card" style="margin-top:24px">
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ readme = "README.md"
requires-python = ">=3.11"
license = { text = "Apache-2.0" }
authors = [{ name = "CivicSuite contributors" }]
dependencies = ["civiccore==0.3.0", "fastapi>=0.115.0,<1.0.0", "uvicorn[standard]>=0.30.0,<1.0.0"]
dependencies = ["civiccore==0.3.0", "fastapi>=0.115.0,<1.0.0", "sqlalchemy>=2.0.0,<3.0.0", "uvicorn[standard]>=0.30.0,<1.0.0"]

[project.optional-dependencies]
dev = ["build>=1.2.0,<2.0.0", "httpx>=0.27.0,<1.0.0", "pytest>=8.0.0,<9.0.0", "ruff>=0.11.0"]
Expand Down
Loading