From 72b03d43f18170363fa031c82cfd721afc141d00 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 30 May 2026 23:32:48 +0530 Subject: [PATCH 01/18] refactor: restructure backend into layered app/ package --- .gitignore | 4 +- api/db/database.py | 20 -------- api/main.py | 42 ----------------- app/__init__.py | 8 ++++ {api => app/api}/__init__.py | 0 {api => app/api}/deps.py | 2 +- app/api/router.py | 12 +++++ {api => app/api}/routes/__init__.py | 0 {api => app/api}/routes/forms.py | 24 +++++----- {api => app/api}/routes/templates.py | 15 +++--- {api/db => app/api/schemas}/__init__.py | 0 {api => app/api}/schemas/common.py | 0 {api => app/api}/schemas/forms.py | 1 - {api => app/api}/schemas/templates.py | 0 {api/errors => app/core}/__init__.py | 0 app/core/config.py | 47 +++++++++++++++++++ {api/schemas => app/core/errors}/__init__.py | 0 {api => app/core}/errors/base.py | 0 {api => app/core}/errors/handlers.py | 2 +- app/core/lifespan.py | 17 +++++++ app/core/logging.py | 14 ++++++ {src => app/db}/__init__.py | 0 app/db/database.py | 14 ++++++ {api => app}/db/init_db.py | 34 +++++++++----- {api => app}/db/repositories.py | 2 +- app/main.py | 34 ++++++++++++++ app/models/__init__.py | 5 ++ {api/db => app/models}/models.py | 0 app/services/__init__.py | 0 {src => app/services}/controller.py | 2 +- {src => app/services}/file_manipulator.py | 4 +- {src => app/services}/filler.py | 2 +- {src => app/services}/inputs/input.txt | 0 {src => app/services}/llm.py | 7 +-- .../services}/outputs/test_output_1.json | 0 {src => app/services}/prompt.txt | 0 .../services}/test/example_template.json | 0 {src => app/services}/test/test_output_1.json | 0 examples/pipeline_demo.py | 27 +++++++++++ src/main.py | 42 ----------------- tests/conftest.py | 10 ++-- tests/test_api.py | 2 +- {src/test => tests}/test_model.py | 0 43 files changed, 238 insertions(+), 155 deletions(-) delete mode 100644 api/db/database.py delete mode 100644 api/main.py create mode 100644 app/__init__.py rename {api => app/api}/__init__.py (100%) rename {api => app/api}/deps.py (51%) create mode 100644 app/api/router.py rename {api => app/api}/routes/__init__.py (100%) rename {api => app/api}/routes/forms.py (85%) rename {api => app/api}/routes/templates.py (95%) rename {api/db => app/api/schemas}/__init__.py (100%) rename {api => app/api}/schemas/common.py (100%) rename {api => app/api}/schemas/forms.py (90%) rename {api => app/api}/schemas/templates.py (100%) rename {api/errors => app/core}/__init__.py (100%) create mode 100644 app/core/config.py rename {api/schemas => app/core/errors}/__init__.py (100%) rename {api => app/core}/errors/base.py (100%) rename {api => app/core}/errors/handlers.py (88%) create mode 100644 app/core/lifespan.py create mode 100644 app/core/logging.py rename {src => app/db}/__init__.py (100%) create mode 100644 app/db/database.py rename {api => app}/db/init_db.py (65%) rename {api => app}/db/repositories.py (93%) create mode 100644 app/main.py create mode 100644 app/models/__init__.py rename {api/db => app/models}/models.py (100%) create mode 100644 app/services/__init__.py rename {src => app/services}/controller.py (87%) rename {src => app/services}/file_manipulator.py (96%) rename {src => app/services}/filler.py (97%) rename {src => app/services}/inputs/input.txt (100%) rename {src => app/services}/llm.py (94%) rename {src => app/services}/outputs/test_output_1.json (100%) rename {src => app/services}/prompt.txt (100%) rename {src => app/services}/test/example_template.json (100%) rename {src => app/services}/test/test_output_1.json (100%) create mode 100644 examples/pipeline_demo.py delete mode 100644 src/main.py rename {src/test => tests}/test_model.py (100%) diff --git a/.gitignore b/.gitignore index b5a9a90..268a0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ src/inputs/*.pdf frontend/release/ # Local Claude Code instructions -CLAUDE.md \ No newline at end of file +CLAUDE.md + +*temp/ \ No newline at end of file diff --git a/api/db/database.py b/api/db/database.py deleted file mode 100644 index 97e6e62..0000000 --- a/api/db/database.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -from sqlmodel import create_engine, Session - -# Define path to the database in the user's home directory -HOME_DIR = os.path.expanduser("~") -APP_DIR = os.path.join(HOME_DIR, ".fireform") -os.makedirs(APP_DIR, exist_ok=True) -DB_PATH = os.path.join(APP_DIR, "fireform.db") - -DATABASE_URL = f"sqlite:///{DB_PATH}" - -engine = create_engine( - DATABASE_URL, - echo=True, - connect_args={"check_same_thread": False}, -) - -def get_session(): - with Session(engine) as session: - yield session \ No newline at end of file diff --git a/api/main.py b/api/main.py deleted file mode 100644 index 0c30bc5..0000000 --- a/api/main.py +++ /dev/null @@ -1,42 +0,0 @@ -from contextlib import asynccontextmanager -import os - -# Disable CUDA to prevent PyTorch from trying to find NVIDIA drivers on Mac Silicon / Docker -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -from fastapi import FastAPI -from api.routes import templates, forms -from api.db.init_db import init_db -from api.errors.handlers import register_exception_handlers -from fastapi.middleware.cors import CORSMiddleware -from api.routes import forms, templates - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup: Initialize the database and seed it if necessary - print("Initializing database...") - init_db() - yield - # Shutdown logic goes here if needed - -app = FastAPI(lifespan=lifespan) - -register_exception_handlers(app) - -default_origins = "http://127.0.0.1:5173,http://localhost:5173" -allowed_origins = [ - origin.strip() - for origin in os.getenv("FRONTEND_ORIGINS", default_origins).split(",") - if origin.strip() -] - -app.add_middleware( - CORSMiddleware, - allow_origins=allowed_origins, - allow_credentials=False, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(templates.router) -app.include_router(forms.router) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..de9379b --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,8 @@ +"""FireForm backend application package.""" + +import os + +# Force CPU before any service module imports torch / rfdetr. Prevents PyTorch +# from probing for NVIDIA drivers on Mac Silicon and inside Docker. Runs here so +# it is guaranteed to execute before `app.main` imports the service layer. +os.environ["CUDA_VISIBLE_DEVICES"] = "" diff --git a/api/__init__.py b/app/api/__init__.py similarity index 100% rename from api/__init__.py rename to app/api/__init__.py diff --git a/api/deps.py b/app/api/deps.py similarity index 51% rename from api/deps.py rename to app/api/deps.py index c2a0b74..4aa9614 100644 --- a/api/deps.py +++ b/app/api/deps.py @@ -1,4 +1,4 @@ -from api.db.database import get_session +from app.db.database import get_session def get_db(): yield from get_session() \ No newline at end of file diff --git a/app/api/router.py b/app/api/router.py new file mode 100644 index 0000000..ba9c0b5 --- /dev/null +++ b/app/api/router.py @@ -0,0 +1,12 @@ +"""Aggregates every route module into a single API router. + +Add new feature routers here; main.py only mounts this one router. +""" + +from fastapi import APIRouter + +from app.api.routes import forms, templates + +api_router = APIRouter() +api_router.include_router(templates.router) +api_router.include_router(forms.router) diff --git a/api/routes/__init__.py b/app/api/routes/__init__.py similarity index 100% rename from api/routes/__init__.py rename to app/api/routes/__init__.py diff --git a/api/routes/forms.py b/app/api/routes/forms.py similarity index 85% rename from api/routes/forms.py rename to app/api/routes/forms.py index 74069ea..7af5da6 100644 --- a/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -1,19 +1,19 @@ -import os - import requests from fastapi import APIRouter, Depends, File, UploadFile from sqlmodel import Session -from api.deps import get_db -from api.schemas.forms import ( + +from app.api.deps import get_db +from app.api.schemas.forms import ( FormFill, FormFillResponse, ModelsResponse, TranscriptionResponse, ) -from api.db.repositories import create_form, get_template -from api.db.models import FormSubmission -from api.errors.base import AppError -from src.controller import Controller +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST +from app.core.errors.base import AppError +from app.db.repositories import create_form, get_template +from app.models import FormSubmission +from app.services.controller import Controller router = APIRouter(prefix="/forms", tags=["forms"]) @@ -48,12 +48,11 @@ def list_models(): """List the Whisper-independent extraction models available in the local Ollama instance, plus the configured default. Used by the Fill Form UI's model picker. Falls back to just the default if Ollama is unreachable.""" - default_model = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") + default_model = OLLAMA_MODEL models: list[str] = [] try: - response = requests.get(f"{ollama_host}/api/tags", timeout=10) + response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10) response.raise_for_status() models = [m["name"] for m in response.json().get("models", []) if m.get("name")] except requests.exceptions.RequestException: @@ -75,8 +74,7 @@ def transcribe(audio: UploadFile = File(...)): audio is streamed straight through to the local STT service and never persisted — no PII leaves the machine. """ - whisper_host = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") - whisper_url = f"{whisper_host}/asr" + whisper_url = f"{WHISPER_HOST}/asr" files = { "audio_file": ( diff --git a/api/routes/templates.py b/app/api/routes/templates.py similarity index 95% rename from api/routes/templates.py rename to app/api/routes/templates.py index 64db2e0..cc98370 100644 --- a/api/routes/templates.py +++ b/app/api/routes/templates.py @@ -5,21 +5,22 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from sqlmodel import Session -from api.deps import get_db -from api.schemas.templates import ( + +from app.api.deps import get_db +from app.api.schemas.templates import ( TemplateCreate, TemplateResponse, TemplateUploadResponse, MakeFillableRequest, MakeFillableResponse, ) -from api.db.repositories import create_template, list_templates -from api.db.models import Template -from src.controller import Controller +from app.core.config import BASE_DIR, DEFAULT_TEMPLATE_DIR +from app.db.repositories import create_template, list_templates +from app.models import Template +from app.services.controller import Controller router = APIRouter(prefix="/templates", tags=["templates"]) -PROJECT_ROOT = Path(__file__).resolve().parents[2] -DEFAULT_TEMPLATE_DIR = "src/inputs" +PROJECT_ROOT = BASE_DIR def _resolve_target_directory(directory: str) -> Path: diff --git a/api/db/__init__.py b/app/api/schemas/__init__.py similarity index 100% rename from api/db/__init__.py rename to app/api/schemas/__init__.py diff --git a/api/schemas/common.py b/app/api/schemas/common.py similarity index 100% rename from api/schemas/common.py rename to app/api/schemas/common.py diff --git a/api/schemas/forms.py b/app/api/schemas/forms.py similarity index 90% rename from api/schemas/forms.py rename to app/api/schemas/forms.py index ca99651..6a833c8 100644 --- a/api/schemas/forms.py +++ b/app/api/schemas/forms.py @@ -4,7 +4,6 @@ class FormFill(BaseModel): template_id: int input_text: str # Optional Ollama model override for this fill; falls back to OLLAMA_MODEL. - # Not persisted (no DB column) — excluded before building FormSubmission. model: str | None = None @field_validator("input_text") diff --git a/api/schemas/templates.py b/app/api/schemas/templates.py similarity index 100% rename from api/schemas/templates.py rename to app/api/schemas/templates.py diff --git a/api/errors/__init__.py b/app/core/__init__.py similarity index 100% rename from api/errors/__init__.py rename to app/core/__init__.py diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..055bbad --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,47 @@ +"""Central configuration. + +Single source of truth for paths, the database URL, external service hosts and +CORS. Read environment once here so the rest of the app imports settings instead +of calling os.getenv() in scattered places. +""" + +import os +from pathlib import Path + +# Repo root. config.py lives at app/core/config.py -> parents[2] is the repo root. +BASE_DIR = Path(__file__).resolve().parents[2] + +# --- App metadata --------------------------------------------------------- +APP_TITLE = "FireForm API" +APP_VERSION = "1.1.0" + +# --- Runtime data paths --------------------------------------------------- +# Uploaded templates and generated PDFs. Project-relative paths the API echoes +# back to the client are resolved against BASE_DIR (the "inside the project" +# guard in the templates routes). Override the data dir with FIREFORM_DATA_DIR. +DATA_DIR = Path(os.getenv("FIREFORM_DATA_DIR", BASE_DIR / "data")).resolve() + +# Directory new uploads land in, as a project-relative string (was "src/inputs" +# before the restructure). Override with FIREFORM_TEMPLATE_DIR. +DEFAULT_TEMPLATE_DIR = os.getenv("FIREFORM_TEMPLATE_DIR", "data/inputs") + +# --- Database ------------------------------------------------------------- +# Keep the SQLite file in the user's home so it survives container rebuilds. +_APP_HOME = Path(os.path.expanduser("~")) / ".fireform" +_APP_HOME.mkdir(parents=True, exist_ok=True) +DB_PATH = Path(os.getenv("FIREFORM_DB_PATH", _APP_HOME / "fireform.db")) +DATABASE_URL = f"sqlite:///{DB_PATH}" +DB_ECHO = os.getenv("FIREFORM_DB_ECHO", "true").lower() == "true" + +# --- External services ---------------------------------------------------- +OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") +OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") +WHISPER_HOST = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") + +# --- CORS ----------------------------------------------------------------- +_DEFAULT_ORIGINS = "http://127.0.0.1:5173,http://localhost:5173" +ALLOWED_ORIGINS = [ + origin.strip() + for origin in os.getenv("FRONTEND_ORIGINS", _DEFAULT_ORIGINS).split(",") + if origin.strip() +] diff --git a/api/schemas/__init__.py b/app/core/errors/__init__.py similarity index 100% rename from api/schemas/__init__.py rename to app/core/errors/__init__.py diff --git a/api/errors/base.py b/app/core/errors/base.py similarity index 100% rename from api/errors/base.py rename to app/core/errors/base.py diff --git a/api/errors/handlers.py b/app/core/errors/handlers.py similarity index 88% rename from api/errors/handlers.py rename to app/core/errors/handlers.py index 903e744..0285ddb 100644 --- a/api/errors/handlers.py +++ b/app/core/errors/handlers.py @@ -1,6 +1,6 @@ from fastapi import Request from fastapi.responses import JSONResponse -from api.errors.base import AppError +from app.core.errors.base import AppError def register_exception_handlers(app): @app.exception_handler(AppError) diff --git a/app/core/lifespan.py b/app/core/lifespan.py new file mode 100644 index 0000000..7f84273 --- /dev/null +++ b/app/core/lifespan.py @@ -0,0 +1,17 @@ +"""Application lifespan: startup and shutdown hooks.""" + +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from app.db.init_db import init_db + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Initializing database...") + init_db() + yield diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..48d28a8 --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,14 @@ +"""Logging setup. Call setup_logging() once at app startup.""" + +import logging + + +def setup_logging(level: str = "INFO") -> None: + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)-8s %(name)s: %(message)s", + ) + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) diff --git a/src/__init__.py b/app/db/__init__.py similarity index 100% rename from src/__init__.py rename to app/db/__init__.py diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 0000000..0437afd --- /dev/null +++ b/app/db/database.py @@ -0,0 +1,14 @@ +from sqlmodel import Session, create_engine + +from app.core.config import DATABASE_URL, DB_ECHO + +engine = create_engine( + DATABASE_URL, + echo=DB_ECHO, + connect_args={"check_same_thread": False}, +) + + +def get_session(): + with Session(engine) as session: + yield session diff --git a/api/db/init_db.py b/app/db/init_db.py similarity index 65% rename from api/db/init_db.py rename to app/db/init_db.py index ea77cb6..fc17147 100644 --- a/api/db/init_db.py +++ b/app/db/init_db.py @@ -1,9 +1,15 @@ -import json import datetime -from sqlmodel import SQLModel, Session, select -from api.db.database import engine -from api.db import models -from api.db.models import Template +import logging + +from sqlmodel import Session, SQLModel, select + +from app.core.config import DEFAULT_TEMPLATE_DIR +from app.db.database import engine + +from app.models import FormSubmission, Template # noqa: F401 + +logger = logging.getLogger(__name__) + def seed_db(): with Session(engine) as session: @@ -14,9 +20,9 @@ def seed_db(): except Exception: # Table might not exist yet if called at a weird time results = None - + if not results: - print("Seeding database with default template...") + logger.info("Seeding database with default template...") fields = { "Employee's name": "string", "Employee's job title": "string", @@ -24,24 +30,26 @@ def seed_db(): "Employee's phone number": "string", "Employee's email": "string", "Signature": "string", - "Date": "string" + "Date": "string", } - + # Using ID 2 as agreed to avoid any ID 1 corruption default_template = Template( id=2, name="Manual Test Template", fields=fields, - pdf_path="src/inputs/file_template_manual.pdf", - created_at=datetime.datetime.now() + pdf_path=f"{DEFAULT_TEMPLATE_DIR}/file_template_manual.pdf", + created_at=datetime.datetime.now(), ) session.add(default_template) session.commit() - print("Database seeded successfully.") + logger.info("Database seeded successfully.") + def init_db(): SQLModel.metadata.create_all(engine) seed_db() + if __name__ == "__main__": - init_db() \ No newline at end of file + init_db() diff --git a/api/db/repositories.py b/app/db/repositories.py similarity index 93% rename from api/db/repositories.py rename to app/db/repositories.py index a9ac3cf..ebb80de 100644 --- a/api/db/repositories.py +++ b/app/db/repositories.py @@ -1,5 +1,5 @@ from sqlmodel import Session, select -from api.db.models import Template, FormSubmission +from app.models import Template, FormSubmission # Templates def create_template(session: Session, template: Template) -> Template: diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3d3bdad --- /dev/null +++ b/app/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.router import api_router +from app.core import config +from app.core.errors.handlers import register_exception_handlers +from app.core.lifespan import lifespan +from app.core.logging import setup_logging + + +def create_app() -> FastAPI: + setup_logging() + + app = FastAPI( + title=config.APP_TITLE, + version=config.APP_VERSION, + lifespan=lifespan, + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=config.ALLOWED_ORIGINS, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + + register_exception_handlers(app) + app.include_router(api_router) + + return app + + +app = create_app() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..f46e17d --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +"""ORM models. Import from here: `from app.models import Template`.""" + +from app.models.models import FormSubmission, Template + +__all__ = ["Template", "FormSubmission"] diff --git a/api/db/models.py b/app/models/models.py similarity index 100% rename from api/db/models.py rename to app/models/models.py diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controller.py b/app/services/controller.py similarity index 87% rename from src/controller.py rename to app/services/controller.py index 0e19290..49c10e4 100644 --- a/src/controller.py +++ b/app/services/controller.py @@ -1,4 +1,4 @@ -from src.file_manipulator import FileManipulator +from app.services.file_manipulator import FileManipulator class Controller: def __init__(self): diff --git a/src/file_manipulator.py b/app/services/file_manipulator.py similarity index 96% rename from src/file_manipulator.py rename to app/services/file_manipulator.py index 8d1f3a0..357356e 100644 --- a/src/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -1,6 +1,6 @@ import os -from src.filler import Filler -from src.llm import LLM +from app.services.filler import Filler +from app.services.llm import LLM class FileManipulator: diff --git a/src/filler.py b/app/services/filler.py similarity index 97% rename from src/filler.py rename to app/services/filler.py index 7f738c2..aec9756 100644 --- a/src/filler.py +++ b/app/services/filler.py @@ -1,5 +1,5 @@ from pdfrw import PdfReader, PdfWriter -from src.llm import LLM +from app.services.llm import LLM from datetime import datetime diff --git a/src/inputs/input.txt b/app/services/inputs/input.txt similarity index 100% rename from src/inputs/input.txt rename to app/services/inputs/input.txt diff --git a/src/llm.py b/app/services/llm.py similarity index 94% rename from src/llm.py rename to app/services/llm.py index 053b883..2998318 100644 --- a/src/llm.py +++ b/app/services/llm.py @@ -3,6 +3,8 @@ import requests from requests.exceptions import Timeout, RequestException +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL + class LLM: def __init__(self, transcript_text: str=None, target_fields: list=None, json_dict: dict=None, model: str=None): @@ -31,9 +33,8 @@ def main_loop(self): total_fields = len(self._target_fields) for i, (field, field_type) in enumerate(self._target_fields.items(), 1): prompt = self.build_prompt(field, field_type if isinstance(field_type, str) else "string") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") - ollama_url = f"{ollama_host}/api/generate" - ollama_model = self._model or os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") + ollama_url = f"{OLLAMA_HOST}/api/generate" + ollama_model = self._model or OLLAMA_MODEL payload = { "model": ollama_model, diff --git a/src/outputs/test_output_1.json b/app/services/outputs/test_output_1.json similarity index 100% rename from src/outputs/test_output_1.json rename to app/services/outputs/test_output_1.json diff --git a/src/prompt.txt b/app/services/prompt.txt similarity index 100% rename from src/prompt.txt rename to app/services/prompt.txt diff --git a/src/test/example_template.json b/app/services/test/example_template.json similarity index 100% rename from src/test/example_template.json rename to app/services/test/example_template.json diff --git a/src/test/test_output_1.json b/app/services/test/test_output_1.json similarity index 100% rename from src/test/test_output_1.json rename to app/services/test/test_output_1.json diff --git a/examples/pipeline_demo.py b/examples/pipeline_demo.py new file mode 100644 index 0000000..dbad305 --- /dev/null +++ b/examples/pipeline_demo.py @@ -0,0 +1,27 @@ +"""Manual, end-to-end demo of the fill pipeline (not part of the package). + +Run from the repo root with a real PDF path. This is a developer convenience +for exercising the services layer without the API; it is not imported anywhere. +""" + +from commonforms import prepare_form +from pypdf import PdfReader + +from app.services.controller import Controller + +if __name__ == "__main__": + file = "./data/inputs/file.pdf" # update to a real input PDF + user_input = ( + "Hi. The employee's name is John Doe. His job title is managing director. " + "His department supervisor is Jane Doe. His phone number is 123456. " + "His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" + ) + prepared_pdf = "temp_outfile.pdf" + prepare_form(file, prepared_pdf) + + reader = PdfReader(prepared_pdf) + fields = reader.get_fields() + num_fields = len(fields) if fields else 0 + + controller = Controller() + controller.fill_form(user_input, fields, file) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 630d262..0000000 --- a/src/main.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -# Monkey patch rfdetr to force CPU usage on Mac Silicon / Docker -try: - import rfdetr.detr - original_ensure = rfdetr.detr._ensure_model_on_device - def patched_ensure(model_ctx): - model_ctx.device = "cpu" - original_ensure(model_ctx) - rfdetr.detr._ensure_model_on_device = patched_ensure -except ImportError: - pass - -from commonforms import prepare_form -from pypdf import PdfReader -from controller import Controller - -if __name__ == "__main__": - file = "./src/inputs/file.pdf" - user_input = "Hi. The employee's name is John Doe. His job title is managing director. His department supervisor is Jane Doe. His phone number is 123456. His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" - fields = [ - "Employee's name", - "Employee's job title", - "Employee's department supervisor", - "Employee's phone number", - "Employee's email", - "Signature", - "Date", - ] - prepared_pdf = "temp_outfile.pdf" - prepare_form(file, prepared_pdf) - - reader = PdfReader(prepared_pdf) - fields = reader.get_fields() - if fields: - num_fields = len(fields) - else: - num_fields = 0 - - controller = Controller() - controller.fill_form(user_input, fields, file) diff --git a/tests/conftest.py b/tests/conftest.py index 5f1eb3f..56dea60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,9 @@ from sqlalchemy.pool import StaticPool from sqlmodel import SQLModel, Session, create_engine -from api.main import app -from api.deps import get_db -from api.db.models import Template, FormSubmission # noqa: F401 — registers tables +from app.main import app +from app.api.deps import get_db +from app.models import Template, FormSubmission # noqa: F401 — registers tables # --------------------------------------------------------------------------- # In-memory database @@ -85,8 +85,8 @@ def pdf_upload(pdf_bytes): @pytest.fixture def mock_controller(): """Patch Controller so create_template / fill_form don't touch the FS or LLM.""" - with patch("api.routes.templates.Controller") as tpl_cls, \ - patch("api.routes.forms.Controller") as form_cls: + with patch("app.api.routes.templates.Controller") as tpl_cls, \ + patch("app.api.routes.forms.Controller") as form_cls: tpl_instance = MagicMock() tpl_instance.create_template.return_value = "src/inputs/test_template.pdf" tpl_cls.return_value = tpl_instance diff --git a/tests/test_api.py b/tests/test_api.py index 89a9b25..66696b9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,7 +7,7 @@ import pytest from sqlmodel import select -from api.db.models import Template, FormSubmission +from app.models import Template, FormSubmission # ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/test/test_model.py b/tests/test_model.py similarity index 100% rename from src/test/test_model.py rename to tests/test_model.py From f4ee6fe909c6a0c45b9e17f9938cdf6a611b169f Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 30 May 2026 23:34:39 +0530 Subject: [PATCH 02/18] Updated the change of code structure layer in docker, makefile and workflows --- .github/workflows/release.yml | 2 +- Dockerfile | 4 ++-- Makefile | 4 +--- docker-compose.yml | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6d1f83..9ddeaf3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: working-directory: . shell: bash run: | - pyinstaller --name api-backend --onefile api/main.py + pyinstaller --name api-backend --onefile app/main.py mkdir -p frontend/bin cp dist/api-backend* frontend/bin/ diff --git a/Dockerfile b/Dockerfile index 282eb25..b7b00ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,11 +18,11 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . -# All imports use api.*, src.* which require the root to be on the path +# All imports use the app.* package, which requires the root on the path ENV PYTHONPATH=/app # Expose FastAPI port EXPOSE 8000 # Start the FastAPI server (not tail -f /dev/null which does nothing) -CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Makefile b/Makefile index 026a721..3f15533 100644 --- a/Makefile +++ b/Makefile @@ -67,10 +67,8 @@ shell: # Start the FastAPI server inside the running container run: - docker compose exec app uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload + docker compose exec app uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -exec: - docker compose exec app python3 src/main.py pull-model: docker compose exec ollama ollama pull $(OLLAMA_MODEL) diff --git a/docker-compose.yml b/docker-compose.yml index a9e8e6e..45ad034 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,7 @@ services: condition: service_healthy whisper: condition: service_started - command: /bin/sh -c "python3 -m api.db.init_db && python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000" + command: /bin/sh -c "python3 -m app.db.init_db && python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000" volumes: - .:/app # Persist the SQLite DB (~/.fireform) across container rebuilds so created From e260df9e86dc2d1502c8872830e8515a47cbf07d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 2 Jun 2026 23:59:24 +0530 Subject: [PATCH 03/18] fix: lint target src/ -> app/ and remove stale electron release workflow --- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 59 ----------------------------------- 2 files changed, 1 insertion(+), 60 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93d41fb..1652a2b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,4 +26,4 @@ jobs: - name: Run linter run: | - ruff check src/ --output-format=github \ No newline at end of file + ruff check app/ --output-format=github \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 9ddeaf3..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release Electron App - -on: - push: - tags: ["v*"] - -jobs: - build: - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - defaults: - run: - working-directory: frontend - - permissions: - contents: write - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install Python dependencies - working-directory: . - shell: bash - run: | - python -m pip install --upgrade pip - if [ "$RUNNER_OS" == "macOS" ]; then - pip install pyinstaller - pip install -r requirements.txt - else - pip install torch --index-url https://download.pytorch.org/whl/cpu - pip install pyinstaller - pip install -r requirements.txt - fi - - - name: Build Python backend with PyInstaller - working-directory: . - shell: bash - run: | - pyinstaller --name api-backend --onefile app/main.py - mkdir -p frontend/bin - cp dist/api-backend* frontend/bin/ - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: npm ci - - - name: Build & publish - run: npx electron-builder --publish always - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From fa7188f2d048f97fc30e2f8d41919887e5440d3a Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 3 Jun 2026 00:05:22 +0530 Subject: [PATCH 04/18] fix: explicit re-export of templates and forms in routes __init__. Fixes lint errors --- app/api/routes/__init__.py | 2 ++ app/api/schemas/common.py | 2 +- tests/test_api.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index e8fe8f4..2264aee 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1 +1,3 @@ from . import templates, forms + +__all__ = ["templates", "forms"] diff --git a/app/api/schemas/common.py b/app/api/schemas/common.py index 578cd2c..8d20a24 100644 --- a/app/api/schemas/common.py +++ b/app/api/schemas/common.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Any, Optional +from typing import Any class SuccessResponse(BaseModel): success: bool = True diff --git a/tests/test_api.py b/tests/test_api.py index 66696b9..33b2c62 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,7 +4,6 @@ All heavy dependencies (LLM, commonforms, filesystem) are mocked via conftest. """ -import pytest from sqlmodel import select from app.models import Template, FormSubmission From 93a5b3ac1630e1cc66664ae9eee58886bd438e69 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 3 Jun 2026 00:16:29 +0530 Subject: [PATCH 05/18] chore: remove frontend directory, as it's migrated to fireform-frontend repo --- frontend/app.js | 1362 ------------ frontend/electron.js | 64 - frontend/index.html | 165 -- frontend/package-lock.json | 4025 ------------------------------------ frontend/package.json | 44 - frontend/preload.js | 3 - frontend/styles.css | 658 ------ 7 files changed, 6321 deletions(-) delete mode 100644 frontend/app.js delete mode 100644 frontend/electron.js delete mode 100644 frontend/index.html delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/preload.js delete mode 100644 frontend/styles.css diff --git a/frontend/app.js b/frontend/app.js deleted file mode 100644 index 4fd15ad..0000000 --- a/frontend/app.js +++ /dev/null @@ -1,1362 +0,0 @@ -const STORAGE_TEMPLATES_KEY = "fireform.templates.v1"; -const STORAGE_LAST_OUTPUT_KEY = "fireform.lastOutputPath.v1"; -// Where uploaded template PDFs are copied. Fixed for now; longer term this -// should be user-configurable behind a Settings button (see note below). -const DEFAULT_TEMPLATE_DIRECTORY = "src/inputs"; -const API_BASE_URL = "http://127.0.0.1:8000"; - -// UI label <-> stored type-string mapping. The stored values stay backward -// compatible with the existing default "string" type. -const FIELD_TYPES = [ - { label: "Text", value: "string" }, - { label: "Long Text", value: "long_text" }, - { label: "Number", value: "number" }, - { label: "Date", value: "date" }, - { label: "Time", value: "time" }, - { label: "Email", value: "email" }, - { label: "Phone", value: "phone" }, - { label: "Signature", value: "signature" }, - { label: "Checkbox", value: "checkbox" }, - { label: "List", value: "list" }, -]; -const TYPE_VALUE_TO_LABEL = Object.fromEntries(FIELD_TYPES.map((t) => [t.value, t.label])); -const DEFAULT_FIELD_ROWS = [{ name: "", type: "string" }]; - -const elements = { - tabs: Array.from(document.querySelectorAll(".tab")), - panels: Array.from(document.querySelectorAll(".panel")), - templateForm: document.getElementById("templateForm"), - templateName: document.getElementById("templateName"), - templatePdfFile: document.getElementById("templatePdfFile"), - pdfDropZone: document.getElementById("pdfDropZone"), - selectedFileMeta: document.getElementById("selectedFileMeta"), - changePdfBtn: document.getElementById("changePdfBtn"), - makeFillableBtn: document.getElementById("makeFillableBtn"), - makeFillableHelpBtn: document.getElementById("makeFillableHelpBtn"), - makeFillableHelp: document.getElementById("makeFillableHelp"), - fieldsBuilder: document.getElementById("fieldsBuilder"), - fieldCountBadge: document.getElementById("fieldCountBadge"), - addFieldBtn: document.getElementById("addFieldBtn"), - templateFormMessage: document.getElementById("templateFormMessage"), - templateFormResponse: document.getElementById("templateFormResponse"), - fillForm: document.getElementById("fillForm"), - fillModel: document.getElementById("fillModel"), - fillTemplateTiles: document.getElementById("fillTemplateTiles"), - fillSelectionHint: document.getElementById("fillSelectionHint"), - fillSubmitBtn: document.getElementById("fillSubmitBtn"), - inputText: document.getElementById("inputText"), - sttControls: document.getElementById("sttControls"), - sttRecordBtn: document.getElementById("sttRecordBtn"), - sttPauseBtn: document.getElementById("sttPauseBtn"), - sttStopBtn: document.getElementById("sttStopBtn"), - sttStatus: document.getElementById("sttStatus"), - fillFormMessage: document.getElementById("fillFormMessage"), - fillFormResponse: document.getElementById("fillFormResponse"), - templatesEmpty: document.getElementById("templatesEmpty"), - templatesList: document.getElementById("templatesList"), - localPdfFile: document.getElementById("localPdfFile"), - serverPdfPath: document.getElementById("serverPdfPath"), - previewPathBtn: document.getElementById("previewPathBtn"), - previewStatus: document.getElementById("previewStatus"), - pdfFrame: document.getElementById("pdfFrame"), -}; - -let templates = loadTemplates(); -let activeObjectUrl = null; -let selectedTemplateFile = null; -// Field rows are scratch state for building one template — they start empty -// each session and are not persisted. -let fieldRows = DEFAULT_FIELD_ROWS.map((row) => ({ ...row })); -let dragSourceIndex = null; -let uploadedPath = null; -let uploadedFieldCount = null; -// Template ids currently selected in the Fill Form tab (multi-select). -let selectedFillIds = new Set(); - -// Speech-to-text recording state. The MediaRecorder captures compressed audio -// in the renderer; on stop we POST it straight to /forms/transcribe (the local -// Whisper service handles decoding). -let mediaRecorder = null; -let recordedChunks = []; -let recordingStream = null; - -waitForBackend().then(initialize); - -async function waitForBackend() { - const loadingScreen = document.getElementById("loadingScreen"); - let isReady = false; - - while (!isReady) { - try { - const response = await fetch(`${API_BASE_URL}/templates`); - if (response.ok) { - isReady = true; - } - } catch (e) { - // Ignore error and try again - } - - if (!isReady) { - await new Promise(r => setTimeout(r, 500)); - } - } - - if (loadingScreen) { - loadingScreen.classList.add("hidden"); - } -} - -async function initialize() { - bindEvents(); - renderFieldRows(); - renderTemplates(); - renderFillTemplates(); - restorePreviewState(); - updateSelectedFileMeta(); - loadModels(); - await refreshTemplatesFromApi(); -} - -function bindEvents() { - elements.tabs.forEach((tab) => { - tab.addEventListener("click", () => activateSection(tab.dataset.target)); - }); - - elements.templateForm.addEventListener("submit", handleTemplateSubmit); - elements.templatePdfFile.addEventListener("change", handleTemplateFileInput); - elements.pdfDropZone.addEventListener("click", () => elements.templatePdfFile.click()); - elements.pdfDropZone.addEventListener("keydown", handleDropZoneKeyDown); - elements.changePdfBtn.addEventListener("click", () => elements.templatePdfFile.click()); - elements.addFieldBtn.addEventListener("click", handleAddFieldClick); - elements.makeFillableBtn.addEventListener("click", handleMakeFillableClick); - elements.makeFillableHelpBtn.addEventListener("click", toggleMakeFillableHelp); - bindDropZoneDragEvents(); - elements.fillForm.addEventListener("submit", handleFillSubmit); - elements.fillTemplateTiles.addEventListener("click", handleTileClick); - elements.fillTemplateTiles.addEventListener("keydown", handleTileKeydown); - elements.sttRecordBtn.addEventListener("click", startRecording); - elements.sttPauseBtn.addEventListener("click", togglePauseRecording); - elements.sttStopBtn.addEventListener("click", stopRecording); - elements.templatesList.addEventListener("click", handleTemplateActionClick); - elements.localPdfFile.addEventListener("change", handleLocalFilePreview); - elements.previewPathBtn.addEventListener("click", () => - previewFromPath(elements.serverPdfPath.value, { switchToPreview: true }) - ); -} - -function activateSection(targetId) { - switchSection(targetId); -} - -async function refreshTemplatesFromApi() { - try { - const response = await fetch(`${API_BASE_URL}/templates`); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - if (Array.isArray(body)) { - templates = body.map((template) => ({ - id: template.id, - name: template.name || "", - pdf_path: template.pdf_path || "", - fields: template.fields || {}, - })); - saveTemplates(); - // Drop selections for templates that no longer exist. - const liveIds = new Set(templates.map((t) => Number(t.id))); - selectedFillIds.forEach((id) => { if (!liveIds.has(id)) selectedFillIds.delete(id); }); - renderTemplates(); - renderFillTemplates(); - } - } catch (error) { - setStatus( - elements.templateFormMessage, - `Could not refresh templates from API: ${error.message}`, - "error" - ); - } -} - -function bindDropZoneDragEvents() { - ["dragenter", "dragover"].forEach((eventName) => { - elements.pdfDropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - event.stopPropagation(); - elements.pdfDropZone.classList.add("active"); - }); - }); - - ["dragleave", "dragend", "drop"].forEach((eventName) => { - elements.pdfDropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - event.stopPropagation(); - elements.pdfDropZone.classList.remove("active"); - }); - }); - - elements.pdfDropZone.addEventListener("drop", (event) => { - const file = event.dataTransfer?.files?.[0]; - setSelectedTemplateFile(file); - }); -} - -function handleDropZoneKeyDown(event) { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - elements.templatePdfFile.click(); - } -} - -function handleTemplateFileInput(event) { - const file = event.target.files && event.target.files[0]; - setSelectedTemplateFile(file); -} - -function setSelectedTemplateFile(file) { - if (!file) { - return; - } - - if (!isPdfFile(file)) { - selectedTemplateFile = null; - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - setStatus(elements.templateFormMessage, "Please select a PDF file.", "error"); - updateSelectedFileMeta(); - return; - } - - selectedTemplateFile = file; - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - clearJson(elements.templateFormResponse); - setStatus(elements.templateFormMessage, ""); - updateSelectedFileMeta(); - // Eager upload so the user gets a live field-count comparison while building rows. - uploadSelectedFileSilently(); -} - -async function uploadSelectedFileSilently() { - if (!selectedTemplateFile) return; - const directory = DEFAULT_TEMPLATE_DIRECTORY; - - const fileAtUploadStart = selectedTemplateFile; - try { - const upload = await uploadTemplatePdf(fileAtUploadStart, directory); - // Guard against the user picking a different file mid-upload. - if (fileAtUploadStart !== selectedTemplateFile) return; - uploadedPath = upload.pdf_path; - uploadedFieldCount = - typeof upload.field_count === "number" ? upload.field_count : null; - maybeSeedFieldRows(upload.fields); - renderFieldCountBadge(); - } catch (_error) { - // Silent failure — the explicit Create / Make Fillable paths surface errors. - } -} - -// Auto-add a row per field the PDF already defines — same as clicking "+ Add -// Field" for each — filling in its description and type so the user can edit. -// If the list already has rows the user typed, warn before replacing them. -function maybeSeedFieldRows(fields) { - if (!Array.isArray(fields) || !fields.length) return; - syncFieldRowsFromDom(); - - if (fieldRows.some((row) => row.name.trim())) { - const replace = window.confirm( - `This PDF has ${fields.length} fillable field${fields.length === 1 ? "" : "s"}.\n\n` + - "Replace your current form fields with them? Your existing entries will be lost." - ); - if (!replace) { - setStatus(elements.templateFormMessage, "Kept your existing form fields.", "info"); - return; - } - } - - fieldRows = fields.map((f) => ({ - name: f.description || f.name || "", - type: normalizeFieldType(f.type), - })); - renderFieldRows(); - setStatus( - elements.templateFormMessage, - `Loaded ${fieldRows.length} field${fieldRows.length === 1 ? "" : "s"} from the PDF — edit the descriptions as needed.`, - "info" - ); -} - -function setMakeFillableButtonState() { - if (!elements.makeFillableBtn) return; - elements.makeFillableBtn.disabled = !selectedTemplateFile; - elements.makeFillableBtn.textContent = "Make this PDF fillable"; -} - -function renderFieldCountBadge() { - const badge = elements.fieldCountBadge; - if (!badge) return; - - if (!selectedTemplateFile || uploadedFieldCount === null) { - badge.classList.add("hidden"); - badge.classList.remove("match", "mismatch"); - badge.textContent = ""; - return; - } - - const expected = uploadedFieldCount; - const actual = fieldRows.length; - const noun = (n) => `${n} fillable field${n === 1 ? "" : "s"}`; - const rowNoun = (n) => `${n} row${n === 1 ? "" : "s"}`; - - badge.classList.remove("hidden", "match", "mismatch"); - if (expected === actual) { - badge.classList.add("match"); - badge.textContent = `PDF has ${noun(expected)} — your ${rowNoun(actual)} match.`; - } else { - badge.classList.add("mismatch"); - badge.textContent = `PDF has ${noun(expected)} — you have ${rowNoun(actual)}.`; - } -} - -function isPdfFile(file) { - const name = String(file?.name || "").toLowerCase(); - return name.endsWith(".pdf"); -} - -function updateSelectedFileMeta() { - // Once a file is chosen, swap the drop zone for a compact "change" control. - const hasFile = !!selectedTemplateFile; - elements.pdfDropZone.classList.toggle("hidden", hasFile); - elements.changePdfBtn.classList.toggle("hidden", !hasFile); - - if (!hasFile) { - elements.selectedFileMeta.textContent = "No PDF selected."; - return; - } - - const destinationPath = `${DEFAULT_TEMPLATE_DIRECTORY}/${selectedTemplateFile.name}`; - - elements.selectedFileMeta.textContent = `Selected: ${selectedTemplateFile.name} (${formatBytes( - selectedTemplateFile.size - )}) - destination: ${destinationPath}`; -} - -function formatBytes(bytes) { - if (!Number.isFinite(bytes) || bytes <= 0) { - return "0 B"; - } - - const units = ["B", "KB", "MB", "GB"]; - let value = bytes; - let unitIndex = 0; - - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex += 1; - } - - return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; -} - -function switchSection(targetId) { - elements.panels.forEach((panel) => { - panel.classList.toggle("hidden", panel.id !== targetId); - }); - elements.tabs.forEach((tab) => { - tab.classList.toggle("active", tab.dataset.target === targetId); - }); -} - -function setStatus(target, message, type = "info") { - target.textContent = message || ""; - target.className = "status"; - if (type) { - target.classList.add(type); - } -} - -function showJson(preElement, payload) { - preElement.textContent = JSON.stringify(payload, null, 2); - preElement.classList.remove("hidden"); -} - -function clearJson(preElement) { - preElement.textContent = ""; - preElement.classList.add("hidden"); -} - -function collectFieldRows() { - syncFieldRowsFromDom(); - - if (fieldRows.length === 0) { - return { error: "Add at least one field before creating the template." }; - } - - const dict = {}; - const seen = new Set(); - for (const row of fieldRows) { - const name = row.name.trim(); - if (!name) { - return { error: "Every field needs a name." }; - } - const key = name.toLowerCase(); - if (seen.has(key)) { - return { error: `Field names must be unique ("${name}" appears more than once).` }; - } - seen.add(key); - dict[name] = row.type || "string"; - } - return { value: dict }; -} - -async function handleTemplateSubmit(event) { - event.preventDefault(); - clearJson(elements.templateFormResponse); - setStatus(elements.templateFormMessage, ""); - - const name = elements.templateName.value.trim(); - const templateDirectory = DEFAULT_TEMPLATE_DIRECTORY; - const collected = collectFieldRows(); - - if (!name || !selectedTemplateFile) { - setStatus( - elements.templateFormMessage, - "Name and PDF file are required.", - "error" - ); - return; - } - - if (collected.error) { - setStatus(elements.templateFormMessage, collected.error, "error"); - return; - } - - try { - let activePdfPath = uploadedPath; - if (!activePdfPath) { - setStatus(elements.templateFormMessage, "Copying PDF into project directory...", "info"); - const upload = await uploadTemplatePdf(selectedTemplateFile, templateDirectory); - activePdfPath = upload.pdf_path; - uploadedPath = upload.pdf_path; - } - - const payload = { - name, - pdf_path: activePdfPath, - fields: collected.value, - }; - - setStatus(elements.templateFormMessage, "Creating template...", "info"); - const response = await fetch(`${API_BASE_URL}/templates/create`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - upsertTemplate(body); - if (body.id != null) { - selectedFillIds.add(Number(body.id)); - } - await refreshTemplatesFromApi(); - elements.serverPdfPath.value = body.pdf_path || ""; - - const expected = body.field_count; - const actual = Object.keys(collected.value).length; - let mismatchNote = ""; - let statusLevel = "success"; - if (typeof expected === "number" && expected !== actual) { - mismatchNote = ` Heads up — the PDF has ${expected} fillable field${expected === 1 ? "" : "s"}, but you added ${actual} row${actual === 1 ? "" : "s"}. Fills may be incomplete or misaligned.`; - statusLevel = "error"; - } - - setStatus( - elements.templateFormMessage, - `Template created (id: ${body.id}). PDF saved at ${activePdfPath}.${mismatchNote}`, - statusLevel - ); - showJson(elements.templateFormResponse, body); - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - } catch (error) { - setStatus(elements.templateFormMessage, error.message, "error"); - } -} - -async function uploadTemplatePdf(file, directory) { - const formData = new FormData(); - formData.append("file", file, file.name); - formData.append("directory", directory); - - const response = await fetch(`${API_BASE_URL}/templates/upload`, { - method: "POST", - body: formData, - }); - - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - return body; -} - -// ───────────────────────── Fill Form: model + template tiles ────────────── - -// "1 field" / "3 forms" — keeps the count-and-label logic in one place. -function pluralize(count, noun) { - return `${count} ${noun}${count === 1 ? "" : "s"}`; -} - -// Look up a template by id (ids may arrive as strings from dataset attributes). -function findTemplate(id) { - return templates.find((template) => Number(template.id) === Number(id)); -} - -// Populate the model picker from the local Ollama models the API reports. -async function loadModels() { - const select = elements.fillModel; - try { - const response = await fetch(`${API_BASE_URL}/forms/models`); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - select.innerHTML = ""; - const models = body.models || []; - models.forEach((name) => { - const isDefault = name === body.default; - const option = document.createElement("option"); - option.value = name; - option.textContent = isDefault ? `${name} (default)` : name; - option.selected = isDefault; - select.append(option); - }); - } catch (_error) { - // Ollama unreachable — leave one placeholder so the picker isn't empty. - if (!select.options.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "(default model)"; - select.append(option); - } - } -} - -// Build one selectable tile. Whether it's selected is shown purely through the -// tile's highlighted styling (.selected) — there's no separate checkbox. -function createTemplateTile(template) { - const id = Number(template.id); - const selected = selectedFillIds.has(id); - - const tile = document.createElement("div"); - tile.className = selected ? "template-tile selected" : "template-tile"; - tile.dataset.templateId = String(id); - // Behaves like a toggle button for keyboard and screen-reader users. - tile.setAttribute("role", "button"); - tile.setAttribute("tabindex", "0"); - tile.setAttribute("aria-pressed", String(selected)); - - const title = document.createElement("span"); - title.className = "tile-title"; - title.textContent = template.name || "Untitled"; - - const fieldCount = template.fields ? Object.keys(template.fields).length : 0; - const meta = document.createElement("span"); - meta.className = "tile-meta"; - meta.textContent = pluralize(fieldCount, "field"); - - const body = document.createElement("div"); - body.className = "tile-body"; - body.append(title, meta); - - // Preview must not toggle selection, so it carries its own id and the click - // handler stops the event from bubbling up to the tile. - const previewButton = document.createElement("button"); - previewButton.type = "button"; - previewButton.className = "tile-preview-btn"; - previewButton.dataset.previewId = String(id); - previewButton.textContent = "Preview"; - - tile.append(body, previewButton); - return tile; -} - -function renderFillTemplates() { - const container = elements.fillTemplateTiles; - container.innerHTML = ""; - - if (!templates.length) { - const empty = document.createElement("p"); - empty.className = "empty-state"; - empty.textContent = "No templates yet — create one in the Create Template tab."; - container.append(empty); - updateFillButtonState(); - return; - } - - templates.forEach((template) => container.append(createTemplateTile(template))); - updateFillButtonState(); -} - -function handleTileClick(event) { - // A click on the Preview button previews the PDF without toggling selection. - const previewButton = event.target.closest(".tile-preview-btn"); - if (previewButton) { - event.stopPropagation(); - const template = findTemplate(previewButton.dataset.previewId); - if (template) { - elements.serverPdfPath.value = template.pdf_path || ""; - previewFromPath(template.pdf_path || "", { switchToPreview: true }); - } - return; - } - - // A click anywhere else on the tile toggles it on/off for filling. - const tile = event.target.closest(".template-tile"); - if (tile) { - toggleFillSelection(Number(tile.dataset.templateId)); - } -} - -function handleTileKeydown(event) { - // Enter/Space activate the focused tile, matching its role="button". - if (event.key !== "Enter" && event.key !== " ") { - return; - } - const tile = event.target.closest(".template-tile"); - if (tile) { - event.preventDefault(); - toggleFillSelection(Number(tile.dataset.templateId)); - } -} - -function toggleFillSelection(id) { - if (selectedFillIds.has(id)) { - selectedFillIds.delete(id); - } else { - selectedFillIds.add(id); - } - renderFillTemplates(); -} - -function updateFillButtonState() { - const count = selectedFillIds.size; - const nothingSelected = count === 0; - - // Greyed out (but still clickable) until at least one form is chosen. - elements.fillSubmitBtn.classList.toggle("is-disabled", nothingSelected); - elements.fillSubmitBtn.textContent = count > 1 ? `Fill ${count} Forms` : "Fill Form"; - - elements.fillSelectionHint.classList.remove("error"); - elements.fillSelectionHint.textContent = nothingSelected - ? "Select one or more forms to fill." - : `${pluralize(count, "form")} selected.`; -} - -// A human-readable label for a template, used in the success/error summary. -function templateLabel(id) { - const template = findTemplate(id); - return template && template.name ? template.name : `id ${id}`; -} - -// Fill a single template and return its submission. Throws on failure so the -// caller can note which form failed and still continue with the others. -async function fillOneTemplate(id, inputText, model) { - const response = await fetch(`${API_BASE_URL}/forms/fill`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ template_id: id, input_text: inputText, model }), - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - return body; -} - -// Summarize "N filled, M failed" into the status line, choosing the right tone: -// all-good = success, some failed but some worked = info, nothing worked = error. -function reportFillOutcome(results, errors) { - const parts = []; - if (results.length) parts.push(`${results.length} filled`); - if (errors.length) parts.push(`${errors.length} failed`); - - let level = "success"; - if (errors.length) { - level = results.length ? "info" : "error"; - } - - const detail = errors.length ? ` ${errors.join("; ")}` : ""; - setStatus(elements.fillFormMessage, `${parts.join(", ")}.${detail}`, level); -} - -async function handleFillSubmit(event) { - event.preventDefault(); - clearJson(elements.fillFormResponse); - setStatus(elements.fillFormMessage, ""); - - const ids = Array.from(selectedFillIds); - if (!ids.length) { - // The button looks disabled but stays clickable, so prompt the user here. - elements.fillSelectionHint.classList.add("error"); - elements.fillSelectionHint.textContent = "Select at least one form to fill."; - setStatus(elements.fillFormMessage, "Select at least one form to fill.", "error"); - return; - } - - const inputText = elements.inputText.value.trim(); - if (!inputText) { - setStatus(elements.fillFormMessage, "Input text is required.", "error"); - return; - } - - // An empty picker value means "let the server use its default model". - const model = elements.fillModel.value || undefined; - setStatus(elements.fillFormMessage, `Filling ${pluralize(ids.length, "form")}…`, "info"); - - // Fill each selected form independently so one failure doesn't stop the rest. - const results = []; - const errors = []; - for (const id of ids) { - try { - results.push(await fillOneTemplate(id, inputText, model)); - } catch (error) { - errors.push(`${templateLabel(id)}: ${error.message}`); - } - } - - const lastResult = results[results.length - 1]; - if (lastResult) { - showJson(elements.fillFormResponse, results.length === 1 ? lastResult : results); - if (lastResult.output_pdf_path) { - localStorage.setItem(STORAGE_LAST_OUTPUT_KEY, lastResult.output_pdf_path); - elements.serverPdfPath.value = lastResult.output_pdf_path; - } - } - - reportFillOutcome(results, errors); - - // Preview the most recently filled PDF. - if (lastResult && lastResult.output_pdf_path) { - await previewFromPath(lastResult.output_pdf_path, { switchToPreview: true }); - } -} - -// ───────────────────────── Speech-to-text (local Whisper) ───────────────── - -function setSttStatus(message) { - if (elements.sttStatus) { - elements.sttStatus.textContent = message || ""; - } -} - -async function startRecording() { - if (mediaRecorder) { - return; - } - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - setSttStatus("Microphone capture is not available in this environment."); - return; - } - - try { - recordingStream = await navigator.mediaDevices.getUserMedia({ audio: true }); - } catch (error) { - setSttStatus("Microphone permission denied."); - return; - } - - recordedChunks = []; - mediaRecorder = new MediaRecorder(recordingStream); - mediaRecorder.addEventListener("dataavailable", (event) => { - if (event.data && event.data.size > 0) { - recordedChunks.push(event.data); - } - }); - mediaRecorder.addEventListener("stop", handleRecordingStop); - mediaRecorder.start(); - - elements.sttControls.classList.add("is-recording"); - elements.sttControls.classList.remove("is-paused"); - elements.sttRecordBtn.disabled = true; - elements.sttPauseBtn.disabled = false; - elements.sttStopBtn.disabled = false; - elements.sttPauseBtn.textContent = "Pause"; - setSttStatus("Recording…"); -} - -function togglePauseRecording() { - if (!mediaRecorder) { - return; - } - if (mediaRecorder.state === "recording") { - mediaRecorder.pause(); - elements.sttControls.classList.add("is-paused"); - elements.sttControls.classList.remove("is-recording"); - elements.sttPauseBtn.textContent = "Resume"; - setSttStatus("Paused."); - } else if (mediaRecorder.state === "paused") { - mediaRecorder.resume(); - elements.sttControls.classList.add("is-recording"); - elements.sttControls.classList.remove("is-paused"); - elements.sttPauseBtn.textContent = "Pause"; - setSttStatus("Recording…"); - } -} - -function stopRecording() { - if (!mediaRecorder) { - return; - } - // Lock the controls while we finalize capture and transcribe. - elements.sttPauseBtn.disabled = true; - elements.sttStopBtn.disabled = true; - setSttStatus("Finishing capture…"); - mediaRecorder.stop(); -} - -async function handleRecordingStop() { - elements.sttControls.classList.remove("is-recording", "is-paused"); - stopRecordingStream(); - - const chunks = recordedChunks; - const recorder = mediaRecorder; - recordedChunks = []; - mediaRecorder = null; - - const blob = new Blob(chunks, { type: (recorder && recorder.mimeType) || "audio/webm" }); - if (!blob.size) { - resetSttControls(); - setSttStatus("Nothing was recorded."); - return; - } - - try { - setSttStatus("Transcribing…"); - const text = await transcribeAudio(blob); - appendTranscribedText(text); - setSttStatus(text ? "Transcription added." : "No speech detected."); - } catch (error) { - setSttStatus(`Transcription failed: ${error.message}`); - } finally { - resetSttControls(); - } -} - -function resetSttControls() { - elements.sttRecordBtn.disabled = false; - elements.sttPauseBtn.disabled = true; - elements.sttStopBtn.disabled = true; - elements.sttPauseBtn.textContent = "Pause"; - elements.sttControls.classList.remove("is-recording", "is-paused"); -} - -function stopRecordingStream() { - if (recordingStream) { - recordingStream.getTracks().forEach((track) => track.stop()); - recordingStream = null; - } -} - -function appendTranscribedText(text) { - if (!text) { - return; - } - const existing = elements.inputText.value.trim(); - elements.inputText.value = existing ? `${existing} ${text}` : text; - // Let any listeners (and the required-field check) see the new value. - elements.inputText.dispatchEvent(new Event("input")); -} - -// "audio/webm;codecs=opus" -> "webm". Just gives the upload a sensible filename; -// the server decodes by content, not extension. -function audioExtension(mimeType) { - const subtype = (mimeType || "").split("/")[1] || ""; - const withoutCodecs = subtype.split(";")[0].trim(); - return withoutCodecs || "webm"; -} - -// The Whisper ASR service decodes audio with ffmpeg, so we post the recording -// as-is (typically webm/opus) — no client-side transcoding needed. -async function transcribeAudio(blob) { - const formData = new FormData(); - formData.append("audio", blob, `recording.${audioExtension(blob.type)}`); - - const response = await fetch(`${API_BASE_URL}/forms/transcribe`, { - method: "POST", - body: formData, - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - return (body.text || "").trim(); -} - -function handleTemplateActionClick(event) { - const button = event.target.closest("button[data-action]"); - if (!button) { - return; - } - - const id = Number(button.dataset.templateId); - const template = templates.find((item) => Number(item.id) === id); - if (!template) { - return; - } - - if (button.dataset.action === "preview") { - elements.serverPdfPath.value = template.pdf_path || ""; - previewFromPath(template.pdf_path || "", { switchToPreview: true }); - return; - } - - if (button.dataset.action === "use-fill") { - selectedFillIds.add(Number(template.id)); - renderFillTemplates(); - activateSection("fillFormSection"); - setStatus( - elements.fillFormMessage, - `"${template.name || "Template"}" selected for filling.`, - "info" - ); - } -} - -function handleLocalFilePreview(event) { - const file = event.target.files && event.target.files[0]; - if (!file) { - return; - } - - if (activeObjectUrl) { - URL.revokeObjectURL(activeObjectUrl); - } - - activeObjectUrl = URL.createObjectURL(file); - elements.pdfFrame.src = activeObjectUrl; - switchSection("pdfPreviewerSection"); - setStatus(elements.previewStatus, `Previewing local file: ${file.name}`, "success"); -} - -function resolvePreviewCandidates(pathInput) { - const raw = String(pathInput || "").trim(); - if (!raw) { - return []; - } - - if (/^https?:\/\//i.test(raw)) { - return [raw]; - } - - return [`${API_BASE_URL}/templates/preview?path=${encodeURIComponent(raw)}`]; -} - -async function previewFromPath(pathInput, options = {}) { - if (options.switchToPreview) { - switchSection("pdfPreviewerSection"); - } - - const raw = String(pathInput || "").trim(); - if (!raw) { - setStatus(elements.previewStatus, "Enter a PDF path or URL first.", "error"); - return false; - } - - const candidates = resolvePreviewCandidates(raw); - if (!candidates.length) { - setStatus(elements.previewStatus, "Unable to parse preview path.", "error"); - return false; - } - - setStatus(elements.previewStatus, "Attempting to preview path...", "info"); - let lastReason = "unknown error"; - - for (const candidate of candidates) { - try { - const response = await fetch(candidate, { method: "HEAD" }); - if (response.ok || response.status === 405) { - elements.pdfFrame.src = candidate; - setStatus(elements.previewStatus, `Previewing path: ${candidate}`, "success"); - return true; - } - lastReason = `${response.status} ${response.statusText}`.trim(); - } catch (error) { - lastReason = error.message; - } - } - - const likelyServerLocal = - !/^https?:\/\//i.test(raw) && !raw.startsWith("/"); - - if (likelyServerLocal) { - setStatus( - elements.previewStatus, - `Could not preview "${raw}". It looks like a server-local path and may not be web-accessible.`, - "error" - ); - } else { - setStatus( - elements.previewStatus, - `Could not preview path. Last error: ${lastReason}`, - "error" - ); - } - - return false; -} - -function renderTemplates() { - elements.templatesList.innerHTML = ""; - - if (!templates.length) { - elements.templatesEmpty.classList.remove("hidden"); - return; - } - - elements.templatesEmpty.classList.add("hidden"); - templates.forEach((template) => { - const card = document.createElement("article"); - card.className = "template-card"; - - const title = document.createElement("h3"); - title.textContent = `${template.name || "Untitled"} (id: ${template.id ?? "n/a"})`; - - const path = document.createElement("p"); - path.className = "template-meta"; - path.textContent = `pdf_path: ${template.pdf_path || ""}`; - - const fields = buildFieldsTable(template.fields || {}); - - const actions = document.createElement("div"); - actions.className = "card-actions"; - - const previewButton = document.createElement("button"); - previewButton.type = "button"; - previewButton.dataset.action = "preview"; - previewButton.dataset.templateId = String(template.id); - previewButton.textContent = "Preview This Template"; - - const useFillButton = document.createElement("button"); - useFillButton.type = "button"; - useFillButton.dataset.action = "use-fill"; - useFillButton.dataset.templateId = String(template.id); - useFillButton.textContent = "Use in Fill Form"; - - actions.append(previewButton, useFillButton); - card.append(title, path, fields, actions); - elements.templatesList.append(card); - }); -} - -function buildFieldsTable(fieldsDict) { - const table = document.createElement("table"); - table.className = "fields-table"; - - const thead = document.createElement("thead"); - thead.innerHTML = "FieldType"; - table.appendChild(thead); - - const tbody = document.createElement("tbody"); - const entries = Object.entries(fieldsDict || {}); - if (!entries.length) { - const row = document.createElement("tr"); - const cell = document.createElement("td"); - cell.colSpan = 2; - cell.textContent = "No fields."; - row.appendChild(cell); - tbody.appendChild(row); - } else { - for (const [name, type] of entries) { - const row = document.createElement("tr"); - const nameCell = document.createElement("td"); - nameCell.textContent = name; - const typeCell = document.createElement("td"); - typeCell.textContent = TYPE_VALUE_TO_LABEL[type] || "Text"; - row.append(nameCell, typeCell); - tbody.appendChild(row); - } - } - table.appendChild(tbody); - return table; -} - -function normalizeFieldType(value) { - return TYPE_VALUE_TO_LABEL[value] ? value : "string"; -} - -function syncFieldRowsFromDom() { - const rowEls = Array.from(elements.fieldsBuilder.querySelectorAll(".field-row")); - fieldRows = rowEls.map((rowEl) => ({ - name: rowEl.querySelector(".field-name").value, - type: rowEl.querySelector(".field-type").value, - })); -} - -function renderFieldRows() { - const fragment = document.createDocumentFragment(); - fieldRows.forEach((row, index) => { - fragment.appendChild(buildFieldRow(row, index)); - }); - elements.fieldsBuilder.innerHTML = ""; - elements.fieldsBuilder.appendChild(fragment); - renderFieldCountBadge(); -} - -function buildFieldRow(row, index) { - const rowEl = document.createElement("div"); - rowEl.className = "field-row"; - rowEl.draggable = true; - rowEl.dataset.index = String(index); - - const handle = document.createElement("span"); - handle.className = "field-drag-handle"; - handle.setAttribute("aria-hidden", "true"); - handle.textContent = "⋮⋮"; // two-column dots — reads as a grip handle - - const nameInput = document.createElement("input"); - nameInput.type = "text"; - nameInput.className = "field-name"; - nameInput.placeholder = "Give description here"; - nameInput.value = row.name || ""; - nameInput.addEventListener("input", () => { - syncFieldRowsFromDom(); - }); - - const typeSelect = document.createElement("select"); - typeSelect.className = "field-type"; - FIELD_TYPES.forEach((t) => { - const opt = document.createElement("option"); - opt.value = t.value; - opt.textContent = t.label; - typeSelect.appendChild(opt); - }); - typeSelect.value = normalizeFieldType(row.type); - typeSelect.addEventListener("change", () => { - syncFieldRowsFromDom(); - }); - - const deleteBtn = document.createElement("button"); - deleteBtn.type = "button"; - deleteBtn.className = "field-delete-btn"; - deleteBtn.setAttribute("aria-label", "Remove field"); - deleteBtn.textContent = "✕"; // ✕ - deleteBtn.addEventListener("click", () => { - syncFieldRowsFromDom(); - const rowIndex = Number(rowEl.dataset.index); - fieldRows.splice(rowIndex, 1); - renderFieldRows(); - }); - - rowEl.addEventListener("dragstart", handleRowDragStart); - rowEl.addEventListener("dragover", handleRowDragOver); - rowEl.addEventListener("dragleave", handleRowDragLeave); - rowEl.addEventListener("drop", handleRowDrop); - rowEl.addEventListener("dragend", handleRowDragEnd); - - rowEl.append(handle, nameInput, typeSelect, deleteBtn); - return rowEl; -} - -function toggleMakeFillableHelp() { - const willShow = elements.makeFillableHelp.classList.contains("hidden"); - elements.makeFillableHelp.classList.toggle("hidden", !willShow); - elements.makeFillableHelpBtn.setAttribute("aria-expanded", String(willShow)); -} - -async function handleMakeFillableClick() { - if (!selectedTemplateFile) { - setStatus(elements.templateFormMessage, "Select a PDF first.", "error"); - return; - } - - const templateDirectory = DEFAULT_TEMPLATE_DIRECTORY; - - elements.makeFillableBtn.disabled = true; - const previousLabel = elements.makeFillableBtn.textContent; - elements.makeFillableBtn.textContent = "Working..."; - setStatus( - elements.templateFormMessage, - "Uploading PDF and running fillable-field detection (this can take a minute)...", - "info" - ); - - try { - if (!uploadedPath) { - const upload = await uploadTemplatePdf(selectedTemplateFile, templateDirectory); - uploadedPath = upload.pdf_path; - } - - const response = await fetch(`${API_BASE_URL}/templates/make-fillable`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ pdf_path: uploadedPath }), - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - uploadedPath = body.pdf_path; - const count = typeof body.field_count === "number" ? body.field_count : null; - uploadedFieldCount = count; - renderFieldCountBadge(); - setStatus( - elements.templateFormMessage, - count !== null - ? `Fillable PDF created — ${count} field${count === 1 ? "" : "s"} detected.` - : "Fillable PDF created.", - "success" - ); - elements.makeFillableBtn.textContent = "Re-detect fields"; - elements.makeFillableBtn.disabled = false; - } catch (error) { - setStatus(elements.templateFormMessage, error.message, "error"); - elements.makeFillableBtn.textContent = previousLabel; - elements.makeFillableBtn.disabled = false; - } -} - -function handleAddFieldClick() { - syncFieldRowsFromDom(); - fieldRows.push({ name: "", type: "string" }); - renderFieldRows(); - const rows = elements.fieldsBuilder.querySelectorAll(".field-row .field-name"); - if (rows.length) { - rows[rows.length - 1].focus(); - } -} - -function handleRowDragStart(event) { - const rowEl = event.currentTarget; - dragSourceIndex = Number(rowEl.dataset.index); - rowEl.classList.add("is-dragging"); - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", String(dragSourceIndex)); - } -} - -function handleRowDragOver(event) { - event.preventDefault(); - if (event.dataTransfer) { - event.dataTransfer.dropEffect = "move"; - } - event.currentTarget.classList.add("drag-over"); -} - -function handleRowDragLeave(event) { - event.currentTarget.classList.remove("drag-over"); -} - -function handleRowDrop(event) { - event.preventDefault(); - const rowEl = event.currentTarget; - rowEl.classList.remove("drag-over"); - const targetIndex = Number(rowEl.dataset.index); - if (dragSourceIndex === null || dragSourceIndex === targetIndex) { - return; - } - syncFieldRowsFromDom(); - const [moved] = fieldRows.splice(dragSourceIndex, 1); - fieldRows.splice(targetIndex, 0, moved); - dragSourceIndex = null; - renderFieldRows(); -} - -function handleRowDragEnd(event) { - event.currentTarget.classList.remove("is-dragging"); - elements.fieldsBuilder - .querySelectorAll(".field-row.drag-over") - .forEach((el) => el.classList.remove("drag-over")); - dragSourceIndex = null; -} - -function loadTemplates() { - try { - const raw = localStorage.getItem(STORAGE_TEMPLATES_KEY); - if (!raw) { - return []; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch (_error) { - return []; - } -} - -function saveTemplates() { - localStorage.setItem(STORAGE_TEMPLATES_KEY, JSON.stringify(templates)); -} - -function upsertTemplate(template) { - const normalized = { - id: template.id, - name: template.name || "", - pdf_path: template.pdf_path || "", - fields: template.fields || {}, - }; - - const index = templates.findIndex((item) => Number(item.id) === Number(template.id)); - if (index >= 0) { - templates[index] = normalized; - } else { - templates.unshift(normalized); - } - - saveTemplates(); -} - -function restorePreviewState() { - const lastPath = localStorage.getItem(STORAGE_LAST_OUTPUT_KEY); - if (lastPath) { - elements.serverPdfPath.value = lastPath; - } -} - -async function parseJsonResponse(response) { - const text = await response.text(); - if (!text) { - return {}; - } - try { - return JSON.parse(text); - } catch (_error) { - return { raw: text }; - } -} - -function extractErrorMessage(responseBody, statusCode) { - if (responseBody && typeof responseBody === "object") { - if (typeof responseBody.error === "string") { - return responseBody.error; - } - if (Array.isArray(responseBody.detail)) { - const first = responseBody.detail[0]; - if (first && typeof first.msg === "string") { - return first.msg; - } - } - if (typeof responseBody.detail === "string") { - return responseBody.detail; - } - if (typeof responseBody.raw === "string") { - return responseBody.raw; - } - } - return `Request failed with status ${statusCode}.`; -} diff --git a/frontend/electron.js b/frontend/electron.js deleted file mode 100644 index 9fc7e19..0000000 --- a/frontend/electron.js +++ /dev/null @@ -1,64 +0,0 @@ -const { app, BrowserWindow } = require("electron"); -const path = require("path"); -const { spawn } = require("child_process"); - -let backendProcess = null; - -function startBackend() { - if (app.isPackaged) { - const ext = process.platform === 'win32' ? '.exe' : ''; - const backendPath = path.join(process.resourcesPath, "bin", `api-backend${ext}`); - - backendProcess = spawn(backendPath, [], { - stdio: 'ignore' - }); - - backendProcess.on('error', (err) => { - console.error('Failed to start backend process.', err); - }); - } else { - console.log("Running in development mode. Assuming backend is running via Docker Desktop."); - } -} - -function createWindow() { - const win = new BrowserWindow({ - width: 1100, - height: 800, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - }, - }); - - // Allow microphone capture for the local speech-to-text recorder. Audio is - // only ever sent to the local Whisper service — nothing leaves the machine. - win.webContents.session.setPermissionRequestHandler( - (webContents, permission, callback) => { - callback(permission === "media"); - } - ); - - win.loadFile("index.html"); - if (!app.isPackaged) { - win.webContents.openDevTools(); - } -} - -app.whenReady().then(() => { - startBackend(); - createWindow(); -}); - -app.on("will-quit", () => { - if (backendProcess) { - backendProcess.kill(); - } -}); - -app.on("window-all-closed", () => { - if (process.platform !== "darwin") app.quit(); -}); - -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); -}); diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index d525015..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - FireForm - - - -
-
-

Starting up...

-

Initializing FireForm backend

-
-
-
-

FireForm

-

- Create templates, fill forms, and preview PDFs from one place. -

-
- - - -
-

Create Template

-
- - - - - -
- Drag and drop a PDF here - or click to select a file -
-

No PDF selected.

- - -
- - -
- - - -

- What information should be filled in? Add one row per field. Fields are filled - into your PDF in the order shown — drag to reorder. -

- -
- - - -
-

- -
- - - - - - -
- - - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 86ed680..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,4025 +0,0 @@ -{ - "name": "fireform", - "version": "1.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fireform", - "version": "1.1.0", - "devDependencies": { - "electron": "^35.0.0", - "electron-builder": "^26.0.0" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" - } - }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/fuses/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/fuses/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.8.1", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.3", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.7", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "which": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.8.1", - "electron-builder-squirrel-windows": "26.8.1" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "35.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", - "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.8.1", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.29.0.tgz", - "integrity": "sha512-bGc7hHz6lrdpMqH3XqfiHc5PKzEhjgUj6OLpTXynkLi9JZKyMByI/tdpm4Liu6O2BjtE1lakBWXjOQS1EnSQLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 468e34c..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "fireform", - "version": "1.1.0", - "description": "FireForm — report once, file everywhere", - "repository": "fireform-core/FireForm", - "main": "electron.js", - "scripts": { - "start": "electron .", - "dist": "electron-builder --publish never" - }, - "devDependencies": { - "electron": "^35.0.0", - "electron-builder": "^26.0.0" - }, - "build": { - "appId": "com.fireform.app", - "productName": "FireForm", - "files": [ - "index.html", - "app.js", - "styles.css", - "electron.js", - "preload.js" - ], - "extraResources": [ - { - "from": "bin", - "to": "bin", - "filter": [ - "**/*" - ] - } - ], - "mac": { - "target": "dmg" - }, - "win": { - "target": "nsis" - }, - "linux": { - "target": "AppImage" - } - } -} diff --git a/frontend/preload.js b/frontend/preload.js deleted file mode 100644 index 4e99198..0000000 --- a/frontend/preload.js +++ /dev/null @@ -1,3 +0,0 @@ -// Preload script — placeholder for future Node.js bridge APIs. -// Use contextBridge.exposeInMainWorld() here to safely expose -// Node functionality to the renderer process when needed. diff --git a/frontend/styles.css b/frontend/styles.css deleted file mode 100644 index c5b4a69..0000000 --- a/frontend/styles.css +++ /dev/null @@ -1,658 +0,0 @@ -:root { - --bg: linear-gradient(145deg, #f7f4ec 0%, #eef4f7 100%); - --panel: #ffffff; - --panel-border: #d7dde3; - --text: #1f2a36; - --muted: #576779; - --primary: #125a86; - --primary-strong: #0b4568; - --success: #1a7f4b; - --error: #a43434; - --radius: 14px; - --shadow: 0 10px 28px rgba(16, 44, 70, 0.09); -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100vh; - font-family: "Avenir Next", "Trebuchet MS", "Segoe UI", sans-serif; - color: var(--text); - background: var(--bg); -} - -.app-shell { - max-width: 980px; - margin: 0 auto; - padding: 24px 18px 48px; -} - -.app-header { - margin-bottom: 16px; -} - -.eyebrow { - margin: 0 0 8px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); - font-size: 0.78rem; -} - -h1 { - margin: 0; - font-size: clamp(1.8rem, 3vw, 2.5rem); - line-height: 1.15; -} - -h2 { - margin: 0 0 14px; - font-size: 1.25rem; -} - -.subtitle { - margin: 8px 0 0; - color: var(--muted); -} - -.card { - background: var(--panel); - border: 1px solid var(--panel-border); - border-radius: var(--radius); - box-shadow: var(--shadow); - padding: 18px; -} - -.api-config { - display: grid; - gap: 8px; - margin-bottom: 14px; -} - -.tabs { - display: flex; - gap: 8px; - margin-bottom: 14px; - flex-wrap: wrap; -} - -.tab { - border: 1px solid var(--panel-border); - background: #f9fbfc; - border-radius: 999px; - padding: 8px 14px; - cursor: pointer; - font-weight: 600; - color: var(--text); -} - -.tab.active { - border-color: var(--primary); - color: #fff; - background: var(--primary); -} - -.panel { - display: grid; - gap: 14px; -} - -.stacked-form { - display: grid; - gap: 10px; -} - -.dropzone { - border: 2px dashed #9ab1c7; - border-radius: 12px; - padding: 20px 14px; - display: grid; - gap: 4px; - text-align: center; - cursor: pointer; - background: #f7fbff; - color: #2f4a63; - transition: border-color 0.2s ease, background-color 0.2s ease; -} - -.dropzone strong { - font-size: 1.02rem; - color: #1f3a53; -} - -.dropzone span { - font-size: 0.92rem; - color: #4e657b; -} - -.dropzone:hover, -.dropzone:focus-visible, -.dropzone.active { - border-color: var(--primary); - background: #eef7ff; - outline: none; -} - -label { - font-weight: 600; -} - -input, -textarea, -select, -button { - font: inherit; -} - -input, -textarea, -select { - width: 100%; - border: 1px solid #bcc8d6; - border-radius: 10px; - padding: 10px 12px; - background: #fff; - color: var(--text); -} - -select { - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 12px center; - padding-right: 32px; -} - -input:focus, -textarea:focus, -select:focus { - outline: 2px solid var(--primary); - outline-offset: 1px; - border-color: var(--primary); -} - -textarea { - resize: vertical; -} - -button { - border: 0; - border-radius: 10px; - background: var(--primary); - color: #fff; - cursor: pointer; - padding: 10px 14px; - font-weight: 700; -} - -button:hover { - background: var(--primary-strong); -} - -.helper { - margin: 0; - color: var(--muted); - font-size: 0.95rem; -} - -.status { - margin: 0; - min-height: 1.4em; - color: var(--muted); -} - -.status.success { - color: var(--success); -} - -.status.error { - color: var(--error); -} - -.json-output { - margin: 0; - white-space: pre-wrap; - word-break: break-word; - border-radius: 10px; - border: 1px solid #d7dde3; - background: #f7fafc; - padding: 10px 12px; - max-height: 280px; - overflow: auto; - font-family: "IBM Plex Mono", "Menlo", "Consolas", monospace; - font-size: 0.88rem; -} - -.hidden { - display: none; -} - -.secondary-btn { - background: #f1f5f9; - color: var(--primary-strong); - border: 1px solid var(--panel-border); - font-weight: 600; - padding: 8px 14px; - justify-self: start; -} - -.secondary-btn:hover { - background: #e5ecf3; -} - -.stt-controls { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.stt-btn { - display: inline-flex; - align-items: center; - gap: 7px; - padding: 8px 14px; -} - -.stt-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.stt-dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: #c2ccd6; - flex-shrink: 0; -} - -.stt-controls.is-recording .stt-dot { - background: var(--error); - animation: stt-pulse 1.1s ease-in-out infinite; -} - -.stt-controls.is-paused .stt-dot { - background: var(--primary); -} - -@keyframes stt-pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.35; transform: scale(0.78); } -} - -.stt-status { - color: var(--muted); - font-size: 0.92rem; - min-height: 1.2em; -} - -/* Fill Form: model picker, template tiles, view toggle */ -.helper.error { - color: var(--error); - font-weight: 600; -} - -.template-tiles { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 14px; -} - -.template-tile { - position: relative; - display: flex; - flex-direction: column; - gap: 6px; - min-height: 112px; - border: 1px solid var(--panel-border); - border-radius: 12px; - background: #fbfcfe; - padding: 16px; - cursor: pointer; - transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; -} - -.template-tile:hover { - border-color: #9ab1c7; -} - -.template-tile:focus-visible { - outline: 2px solid var(--primary); - outline-offset: 1px; -} - -.template-tile.selected { - border-color: var(--primary); - background: #eef7ff; - box-shadow: inset 0 0 0 1px var(--primary); -} - -.tile-body { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; -} - -.tile-title { - font-weight: 700; - color: var(--text); - /* Uniform tiles: clamp long titles to two lines, then ellipsis. */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; -} - -.tile-meta { - font-size: 0.85rem; - color: var(--muted); -} - -.tile-preview-btn { - margin-top: auto; /* pin to the bottom so every tile's button lines up */ - align-self: flex-start; - background: transparent; - color: var(--primary-strong); - border: 1px solid var(--panel-border); - border-radius: 8px; - padding: 5px 10px; - font-size: 0.82rem; - font-weight: 600; -} - -.tile-preview-btn:hover { - background: #e5ecf3; -} - -/* Greyed-but-clickable: looks disabled, still fires click to prompt the user. */ -button.is-disabled, -button.is-disabled:hover { - background: #c2ccd6; - cursor: not-allowed; -} - -.help-trigger { - background: #e5ecf3; - color: var(--primary-strong); - border: 1px solid #c8d2dd; - border-radius: 50%; - width: 24px; - height: 24px; - padding: 0; - font-weight: 700; - font-size: 0.9rem; - line-height: 1; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - flex-shrink: 0; -} - -.help-trigger:hover { - background: #d8e2eb; -} - -.help-trigger[aria-expanded="true"] { - background: var(--primary); - color: #fff; - border-color: var(--primary); -} - -.field-count-badge { - margin: 0; - padding: 6px 10px; - border-radius: 8px; - background: #f1f5f9; - border: 1px solid var(--panel-border); - color: var(--muted); - font-size: 0.92rem; - display: inline-block; - justify-self: start; -} - -.field-count-badge.hidden { - display: none; -} - -.field-count-badge.match { - background: #e8f3ec; - border-color: #b9d8c4; - color: var(--success); -} - -.field-count-badge.mismatch { - background: #fbe9e9; - border-color: #f3d3d3; - color: var(--error); -} - -.fields-builder { - display: grid; - gap: 8px; -} - -.field-row { - display: grid; - grid-template-columns: 24px 1fr 170px 36px; - gap: 10px; - align-items: center; - padding: 8px 10px; - border: 1px solid var(--panel-border); - border-radius: 12px; - background: #fbfcfe; -} - -.field-row.is-dragging { - opacity: 0.4; -} - -.field-row.drag-over { - border-color: var(--primary); - background: #eef7ff; -} - -.field-drag-handle { - cursor: grab; - color: var(--muted); - text-align: center; - user-select: none; - font-size: 1rem; - line-height: 1; -} - -.field-drag-handle:active { - cursor: grabbing; -} - -.field-delete-btn { - background: transparent; - color: var(--muted); - border: 1px solid transparent; - padding: 0; - font-size: 1rem; - font-weight: 600; - line-height: 1; - border-radius: 8px; - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.field-delete-btn:hover { - background: #fbe9e9; - color: var(--error); - border-color: #f3d3d3; -} - -.fields-table { - width: 100%; - border-collapse: collapse; - border: 1px solid var(--panel-border); - border-radius: 10px; - overflow: hidden; - background: #fff; - font-size: 0.92rem; -} - -.fields-table th, -.fields-table td { - text-align: left; - padding: 8px 10px; - border-bottom: 1px solid var(--panel-border); -} - -.fields-table tr:last-child td { - border-bottom: 0; -} - -.fields-table th { - background: #f1f5f9; - font-weight: 600; - color: var(--muted); -} - -@media (max-width: 540px) { - .field-row { - grid-template-columns: 22px 1fr 36px; - grid-template-areas: - "handle name delete" - ". select select"; - } - - .field-drag-handle { grid-area: handle; } - .field-row input[type="text"] { grid-area: name; } - .field-row select { grid-area: select; } - .field-delete-btn { grid-area: delete; } -} - -.divider { - width: 100%; - border: 0; - border-top: 1px solid #d7dde3; - margin: 4px 0; -} - -.template-list { - display: grid; - gap: 12px; -} - -.template-card { - border: 1px solid #d7dde3; - border-radius: 12px; - padding: 14px; - background: #fbfcfe; -} - -.template-meta { - margin: 0; -} - -.card-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.card-actions button { - padding: 8px 12px; - font-weight: 600; -} - -.empty-state { - padding: 14px; - border-radius: 10px; - border: 1px dashed #bcc8d6; - color: var(--muted); - background: #f8fafb; -} - -.preview-controls { - display: grid; - gap: 8px; -} - -.inline-actions { - display: grid; - grid-template-columns: 1fr auto; - gap: 8px; - align-items: center; -} - -#pdfFrame { - width: 100%; - min-height: 560px; - border: 1px solid #d7dde3; - border-radius: 10px; - background: #fff; -} - -@media (max-width: 720px) { - .app-shell { - padding: 16px 12px 28px; - } - - .card { - padding: 14px; - } - - .inline-actions { - grid-template-columns: 1fr; - } - - #pdfFrame { - min-height: 420px; - } -} - -.loading-screen { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: var(--bg); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 9999; - transition: opacity 0.5s ease, visibility 0.5s ease; -} - -.loading-screen.hidden { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - -.loading-screen h2 { - margin-top: 24px; - color: var(--primary); -} - -.spinner { - width: 50px; - height: 50px; - border: 5px solid rgba(18, 90, 134, 0.2); - border-top-color: var(--primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} From e26a0fff8ab4a981ab5f0d0440cb9cb54411d70c Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 3 Jun 2026 00:37:22 +0530 Subject: [PATCH 06/18] chore: move scripts to scripts/ directory, fixes #517 --- container-init.sh => scripts/container-init.sh | 0 setup-dockers-env.sh => scripts/setup-dockers-env.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename container-init.sh => scripts/container-init.sh (100%) rename setup-dockers-env.sh => scripts/setup-dockers-env.sh (100%) diff --git a/container-init.sh b/scripts/container-init.sh similarity index 100% rename from container-init.sh rename to scripts/container-init.sh diff --git a/setup-dockers-env.sh b/scripts/setup-dockers-env.sh similarity index 100% rename from setup-dockers-env.sh rename to scripts/setup-dockers-env.sh From b6527b7ea109b6360a10bbb11bc7f022f5c9db2c Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 4 Jun 2026 00:57:31 +0530 Subject: [PATCH 07/18] chore: created docker file and compose for development envirnment --- docker/dev/Dockerfile | 26 +++++++++++++ docker/dev/compose.yml | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docker/dev/Dockerfile create mode 100644 docker/dev/compose.yml diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 0000000..ad4bfd4 --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 +FROM python:3.11-slim + +WORKDIR /app + +# Use apt cache mount to speed up system package installation across builds +RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y \ + curl \ + libgl1 \ + libglib2.0-0 \ + libxcb1 + +COPY requirements.txt . + +# Use pip cache mount so it remembers downloaded wheels +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install -r requirements.txt + +ENV PYTHONPATH=/app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml new file mode 100644 index 0000000..5b7c2f0 --- /dev/null +++ b/docker/dev/compose.yml @@ -0,0 +1,83 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: fireform-ollama + ports: + - "127.0.0.1:11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "ollama list || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + whisper: + image: onerahmet/openai-whisper-asr-webservice:latest + container_name: fireform-whisper + environment: + - ASR_ENGINE=faster_whisper + - ASR_MODEL=${WHISPER_MODEL:-small.en} + - ASR_MODEL_PATH=/data/whisper + volumes: + - whisper_models:/data/whisper + ports: + - "127.0.0.1:9000:9000" + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + + app: + build: + context: ../.. + dockerfile: docker/dev/Dockerfile + container_name: fireform-app + depends_on: + ollama: + condition: service_healthy + whisper: + condition: service_started + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + volumes: + - ../..:/app + - fireform_db:/data/db + - fireform_uploads:/data/uploads + ports: + - "${APP_PORT:-8000}:8000" + environment: + - PYTHONUNBUFFERED=1 + - CUDA_VISIBLE_DEVICES= + - PYTHONPATH=/app + - OLLAMA_HOST=${OLLAMA_HOST:-http://ollama:11434} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-300} + - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b} + - WHISPER_HOST=${WHISPER_HOST:-http://whisper:9000} + - FIREFORM_DB_PATH=/data/db/fireform.db + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - FIREFORM_DB_ECHO=${FIREFORM_DB_ECHO:-true} + - FRONTEND_ORIGINS=${FRONTEND_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173} + networks: + - fireform-network + +volumes: + ollama_data: + driver: local + whisper_models: + driver: local + fireform_db: + driver: local + fireform_uploads: + driver: local + +networks: + fireform-network: + driver: bridge From 79a1ce05ef82009893de5e3a30e059354db6502d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 4 Jun 2026 01:10:06 +0530 Subject: [PATCH 08/18] chore: docker file and docker compose for prod envirnment --- docker/prod/Dockerfile | 37 +++++++++++++++++ docker/prod/compose.yml | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docker/prod/Dockerfile create mode 100644 docker/prod/compose.yml diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile new file mode 100644 index 0000000..8ad8dea --- /dev/null +++ b/docker/prod/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.11-slim AS builder + +WORKDIR /build + +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + + +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + curl \ + libgl1 \ + libglib2.0-0 \ + libxcb1 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /install /usr/local + +# Copy only app code, not data/ temp/ tests/ docs/ etc. +COPY app/ ./app/ +COPY requirements.txt . + +ENV PYTHONPATH=/app + +# Data dirs created here; actual storage comes from mounted volumes at runtime. +RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh + +EXPOSE 8000 + +CMD ["gunicorn", "app.main:app", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--access-logfile", "-", \ + "--error-logfile", "-"] diff --git a/docker/prod/compose.yml b/docker/prod/compose.yml new file mode 100644 index 0000000..edd2fa4 --- /dev/null +++ b/docker/prod/compose.yml @@ -0,0 +1,90 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: fireform-ollama + ports: + - "127.0.0.1:11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - fireform-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "ollama list || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + whisper: + image: onerahmet/openai-whisper-asr-webservice:latest + container_name: fireform-whisper + environment: + - ASR_ENGINE=faster_whisper + - ASR_MODEL=${WHISPER_MODEL} + - ASR_MODEL_PATH=/data/whisper + volumes: + - whisper_models:/data/whisper + ports: + - "127.0.0.1:9000:9000" + networks: + - fireform-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + + app: + build: + context: ../.. + dockerfile: docker/prod/Dockerfile + container_name: fireform-app + depends_on: + ollama: + condition: service_healthy + whisper: + condition: service_started + command: ["gunicorn", "app.main:app", + "--worker-class", "uvicorn.workers.UvicornWorker", + "--bind", "0.0.0.0:8000", + "--access-logfile", "-", + "--error-logfile", "-"] + volumes: + - fireform_db:/data/db + - fireform_uploads:/data/uploads + ports: + - "${APP_PORT}:8000" + environment: + - PYTHONUNBUFFERED=1 + - CUDA_VISIBLE_DEVICES= + - PYTHONPATH=/app + - OLLAMA_HOST=${OLLAMA_HOST} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT} + - OLLAMA_MODEL=${OLLAMA_MODEL} + - WHISPER_HOST=${WHISPER_HOST} + - FIREFORM_DB_PATH=/data/db/fireform.db + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - FIREFORM_DB_ECHO=false + - FRONTEND_ORIGINS=${FRONTEND_ORIGINS} + - WEB_CONCURRENCY=${GUNICORN_WORKERS} + networks: + - fireform-network + restart: unless-stopped + +volumes: + ollama_data: + driver: local + whisper_models: + driver: local + fireform_db: + driver: local + fireform_uploads: + driver: local + +networks: + fireform-network: + driver: bridge From 0bc991a5cade6d7cdd085d7211b2728049057209 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 4 Jun 2026 15:43:36 +0530 Subject: [PATCH 09/18] Updated Makefile: added make init command to run a project initilization setup. added init-env, select ollama model scripts. Updated docker compose path proposed in #526 and #527 --- Makefile | 102 +++++++++++++++++++++----------------- scripts/check-deps.sh | 51 +++++++++++++++++++ scripts/init-env.sh | 35 +++++++++++++ scripts/select-model.sh | 107 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 44 deletions(-) create mode 100755 scripts/check-deps.sh create mode 100755 scripts/init-env.sh create mode 100755 scripts/select-model.sh diff --git a/Makefile b/Makefile index 3f15533..13ea366 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -.PHONY: help build up down logs shell exec pull-model test clean fireform logs-app logs-ollama logs-frontend super-clean +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean -# The extraction model pulled into Ollama and used by src/llm.py. Override with -# `make pull-model OLLAMA_MODEL=...`. A 1.5B model keeps per-field fills fast. -OLLAMA_MODEL ?= qwen2.5:1.5b +COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev +ENV_DEV = docker/.env.dev + +# Read OLLAMA_MODEL from .env.dev at runtime; fall back to default if file absent. +OLLAMA_MODEL = $(shell grep -E '^OLLAMA_MODEL=' $(ENV_DEV) 2>/dev/null | cut -d= -f2 | tr -d '[:space:]' || echo qwen2.5:1.5b) help: @printf '%s\n' \ @@ -13,72 +15,84 @@ help: '/_/ /_//_/ \___/ /_/ \____/_/ /_/ /_/ /_/ ' \ '' @echo "" - @echo "Fireform Development Commands" + @echo "FireForm Development Commands" @echo "==============================" - @echo "make fireform - Build and start containers, then open a shell" + @echo "make init - First-time setup: check deps, create .env.dev, pick model" + @echo "make fireform - Build images, start containers, pull Ollama model" @echo "make build - Build Docker images" - @echo "make up - Start all containers" + @echo "make up - Start all containers (detached)" @echo "make down - Stop all containers" - @echo "make logs - View container logs" - @echo "make logs-app - View API container logs" - @echo "make logs-frontend - View frontend container logs" - @echo "make logs-ollama - View Ollama container logs" - @echo "make shell - Open Python shell in app container" - @echo "make exec - Execute Python script in container" - @echo "make pull-model - Pull the extraction model ($(OLLAMA_MODEL)) into Ollama" - @echo "make test - Run tests" - @echo "make clean - Remove containers" - @echo "make super-clean - [CAUTION] Use carefully. Cleans up ALL stopped containers, networks, build cache..." - -# Fix #382 — pull-model is now part of the main setup flow -# The extraction model is pulled automatically before you need it -fireform: build up pull-model + @echo "make logs - Stream all container logs" + @echo "make logs-app - Stream app container logs" + @echo "make logs-ollama - Stream Ollama container logs" + @echo "make shell - Open shell in running app container" + @echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))" + @echo "make test - Run test suite" + @echo "make clean - Stop containers (preserves volumes)" + @echo "make super-clean - [CAUTION] Stop containers, delete volumes, prune Docker" + +init: + @chmod +x scripts/check-deps.sh scripts/init-env.sh scripts/select-model.sh + @sh scripts/check-deps.sh + @sh scripts/init-env.sh + @sh scripts/select-model.sh + @printf "Build containers and pull model now? [y/N] "; \ + read answer; \ + case "$$answer" in \ + [yY]*) $(MAKE) fireform ;; \ + *) echo "Run 'make fireform' when ready." ;; \ + esac + +fireform: build up + @printf "Waiting for Ollama to be ready..." + @until $(COMPOSE) exec -T ollama ollama list > /dev/null 2>&1; do \ + printf '.'; sleep 2; \ + done + @echo " ready." + @if $(COMPOSE) exec -T ollama ollama list 2>/dev/null | grep -q "^$(OLLAMA_MODEL)"; then \ + echo " Model $(OLLAMA_MODEL) already pulled."; \ + else \ + echo " Pulling $(OLLAMA_MODEL)..."; \ + $(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL); \ + fi @echo "" - @echo "✅ FireForm is ready!" - @echo " Frontend: http://localhost:5173" + @echo "FireForm is ready!" @echo " API: http://localhost:8000" @echo " API Docs: http://localhost:8000/docs" @echo "" @echo "Run 'make logs' to view live logs, 'make down' to stop." build: - docker compose build + @$(COMPOSE) build --progress=quiet up: - docker compose up -d + @$(COMPOSE) up -d down: - docker compose down + @$(COMPOSE) down --remove-orphans logs: - docker compose logs -f + @$(COMPOSE) logs -f logs-app: - docker compose logs -f app + @$(COMPOSE) logs -f app logs-ollama: - docker compose logs -f ollama - -logs-frontend: - docker compose logs -f frontend + @$(COMPOSE) logs -f ollama shell: - docker compose exec app /bin/bash - -# Start the FastAPI server inside the running container -run: - docker compose exec app uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - + @$(COMPOSE) exec app /bin/sh pull-model: - docker compose exec ollama ollama pull $(OLLAMA_MODEL) + @$(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL) -# Fix — correct test directory (was src/test/ which doesn't exist) test: - docker compose exec app python3 -m pytest tests/ -v + @$(COMPOSE) exec -T app python3 -m pytest tests/ -v clean: - docker compose down -v + @$(COMPOSE) down + super-clean: - docker compose down -v - docker system prune + @echo "WARNING: this will delete all volumes (database, uploads, model weights)." + @$(COMPOSE) down -v + @docker system prune -f diff --git a/scripts/check-deps.sh b/scripts/check-deps.sh new file mode 100755 index 0000000..3c4484e --- /dev/null +++ b/scripts/check-deps.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -e + +PASS=0 +FAIL=1 +errors=0 + +check() { + label="$1" + shift + if "$@" > /dev/null 2>&1; then + echo " [ok] $label" + else + echo " [!!] $label" + errors=$((errors + 1)) + fi +} + +echo "" +echo "Checking dependencies..." +echo "========================" + +# Docker daemon running +check "Docker daemon is running" docker info + +# docker compose v2 (plugin form, not legacy docker-compose) +check "docker compose v2 available" docker compose version + +# Minimum Docker version: 24 (BuildKit cache mounts stable) +DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1) +if [ -n "$DOCKER_VERSION" ] && [ "$DOCKER_VERSION" -ge 24 ] 2>/dev/null; then + echo " [ok] Docker version >= 24 (found $(docker version --format '{{.Server.Version}}' 2>/dev/null))" +else + echo " [!!] Docker version >= 24 required (found $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo 'unknown'))" + echo " BuildKit cache mounts in docker/dev/Dockerfile require Docker 24+." + errors=$((errors + 1)) +fi + +echo "" + +if [ "$errors" -gt 0 ]; then + echo "$errors check(s) failed. Fix the above before continuing." + echo "" + echo " Install Docker: https://docs.docker.com/get-docker/" + echo " Upgrade Docker Desktop: https://docs.docker.com/desktop/release-notes/" + echo "" + exit 1 +fi + +echo "All checks passed." +echo "" diff --git a/scripts/init-env.sh b/scripts/init-env.sh new file mode 100755 index 0000000..c2c0804 --- /dev/null +++ b/scripts/init-env.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +ENV_EXAMPLE="docker/.env.example" +ENV_DEV="docker/.env.dev" + +echo "" +echo "Setting up environment..." +echo "=========================" + +if [ ! -f "$ENV_EXAMPLE" ]; then + echo "Error: $ENV_EXAMPLE not found. Are you running from the repo root?" + exit 1 +fi + +if [ -f "$ENV_DEV" ]; then + printf " %s already exists. Overwrite? [y/N] " "$ENV_DEV" + read -r answer + case "$answer" in + [yY]*) + cp "$ENV_EXAMPLE" "$ENV_DEV" + echo " Overwritten." + ;; + *) + echo " Kept existing $ENV_DEV." + ;; + esac +else + cp "$ENV_EXAMPLE" "$ENV_DEV" + echo " Created $ENV_DEV from $ENV_EXAMPLE." +fi + +echo "" +echo " Review docker/.env.dev and adjust values if needed before running 'make fireform'." +echo "" diff --git a/scripts/select-model.sh b/scripts/select-model.sh new file mode 100755 index 0000000..4b3c9d3 --- /dev/null +++ b/scripts/select-model.sh @@ -0,0 +1,107 @@ +#!/bin/sh +set -e + +ENV_DEV="docker/.env.dev" +COMPOSE="docker compose -f docker/dev/compose.yml --env-file $ENV_DEV" + +# model name | approx size +MODELS="qwen2.5:1.5b|~1GB qwen2.5:3b|~2GB qwen2.5:7b|~4GB llama3.2:3b|~2GB mistral:7b|~4GB" + +current_model="" +if [ -f "$ENV_DEV" ]; then + current_model=$(grep -E '^OLLAMA_MODEL=' "$ENV_DEV" | cut -d= -f2 | tr -d '[:space:]') +fi + +echo "" +echo "Select Ollama model" +echo "===================" +[ -n "$current_model" ] && echo " Current: $current_model" +echo "" + +i=1 +for entry in $MODELS; do + name=$(echo "$entry" | cut -d'|' -f1) + size=$(echo "$entry" | cut -d'|' -f2) + if [ "$name" = "${current_model}" ]; then + echo " $i) $name $size [current]" + else + echo " $i) $name $size" + fi + i=$((i + 1)) +done +echo " $i) Enter custom model name" +i=$((i + 1)) +echo " $i) Keep current" +echo "" +printf "Choice [1-$i]: " +read -r choice + +total=$(echo "$MODELS" | wc -w | tr -d '[:space:]') +keep_choice=$((total + 2)) +custom_choice=$((total + 1)) + +if [ "$choice" = "$keep_choice" ] || [ -z "$choice" ]; then + echo " Keeping current model: ${current_model:-qwen2.5:1.5b}" + echo "" + exit 0 +fi + +if [ "$choice" = "$custom_choice" ]; then + printf " Enter model name (e.g. phi3:mini): " + read -r selected + if [ -z "$selected" ]; then + echo " No model entered. Keeping current." + echo "" + exit 0 + fi + selected_size="unknown size" +else + # Validate numeric choice in range + if ! echo "$choice" | grep -qE '^[0-9]+$' || [ "$choice" -lt 1 ] || [ "$choice" -gt "$total" ]; then + echo " Invalid choice. Keeping current model." + echo "" + exit 0 + fi + i=1 + for entry in $MODELS; do + if [ "$i" = "$choice" ]; then + selected=$(echo "$entry" | cut -d'|' -f1) + selected_size=$(echo "$entry" | cut -d'|' -f2) + fi + i=$((i + 1)) + done +fi + +# Patch OLLAMA_MODEL in .env.dev +tmp=$(mktemp) +sed "s|^OLLAMA_MODEL=.*|OLLAMA_MODEL=$selected|" "$ENV_DEV" > "$tmp" +mv "$tmp" "$ENV_DEV" +echo "" +echo " OLLAMA_MODEL set to: $selected" + +# If container is running, check and optionally pull immediately. +# If not running, the caller (make init) handles the pull prompt. +if $COMPOSE ps ollama 2>/dev/null | grep -q "running"; then + if $COMPOSE exec -T ollama ollama list 2>/dev/null | grep -q "^$selected"; then + echo " Model already pulled. Nothing to download." + echo "" + exit 0 + fi + + echo "" + printf " Model not yet downloaded ($selected_size). Pull now? [y/N] " + read -r pull_answer + case "$pull_answer" in + [yY]*) + echo "" + $COMPOSE exec -T ollama ollama pull "$selected" + echo "" + echo " Model ready." + ;; + *) + echo " Skipped. Run 'make pull-model' when ready." + ;; + esac +fi + +echo "" From e2e066265130f51d93eec4e8491b9bf1b9f48257 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 6 Jun 2026 03:44:31 +0530 Subject: [PATCH 10/18] remove unwanted html files under docs/ which is already migrated to fireform-core/fireform-website --- docs/dpg.html | 203 ------------------- docs/index.html | 176 ----------------- docs/privacy.html | 97 --------- docs/styles.css | 488 ---------------------------------------------- docs/terms.html | 152 --------------- 5 files changed, 1116 deletions(-) delete mode 100644 docs/dpg.html delete mode 100644 docs/index.html delete mode 100644 docs/privacy.html delete mode 100644 docs/styles.css delete mode 100644 docs/terms.html diff --git a/docs/dpg.html b/docs/dpg.html deleted file mode 100644 index 47371bc..0000000 --- a/docs/dpg.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - FireForm - Digital Public Good & SDG Relevance - - - - - - - -
-
-
-
- - - -
-
-
-
Digital Public Good
-

SDG Relevance

-

- FireForm is proud to be an open-source Digital Public Good, dedicated to advancing the United Nations' Sustainable Development Goals (SDGs). By drastically reducing the administrative burden on first responders, we enable emergency services to operate more efficiently, safely, and transparently. -

-
-
-
- -
-
-

Relevant Sustainable Development Goals

- -
- -
-
🏢
-

SDG 16: Peace, Justice and Strong Institutions

-

Target 16.6: Develop effective, accountable and transparent institutions at all levels.

-

How FireForm contributes: By unifying and digitizing the reporting structure for firefighters and other emergency responders, FireForm builds stronger, more accountable local institutions. Emergency services can seamlessly share critical incident data across county lines, sheriff departments, and EMS, ensuring transparency and highly effective public service administration without the overhead of repetitive paperwork.

-
- -
-
🏙️
-

SDG 11: Sustainable Cities and Communities

-

Target 11.b: By 2020, substantially increase the number of cities and human settlements adopting and implementing integrated policies and plans towards inclusion, resource efficiency, mitigation and adaptation to climate change, resilience to disasters...

-

How FireForm contributes: FireForm directly enhances a city's resilience to disasters by giving first responders hours of their time back. Instead of doing administrative work, firefighters can focus on training, disaster mitigation, and responding to emergencies, ultimately creating safer and more resilient communities.

-
- -
-
💡
-

SDG 9: Industry, Innovation and Infrastructure

-

Target 9.4: By 2030, upgrade infrastructure and retrofit industries to make them sustainable, with increased resource-use efficiency and greater adoption of clean and environmentally sound technologies and industrial processes...

-

How FireForm contributes: FireForm modernizes the outdated infrastructure of first responder reporting. By leveraging open-source AI locally, it serves as an innovative digital infrastructure that makes emergency management more resource-efficient and environmentally sound (eliminating physical paperwork and reducing digital redundancies).

-
- -
- -
-

Clear Ownership & Accountability

-
-

- In accordance with the Digital Public Goods Alliance requirements for Open Software, the ownership and accountability of the FireForm project and all its produced assets are clearly defined. -

-

- Accountable Entity: The intellectual property and copyright of the software code and content are owned by the original core creators: Juan Álvarez Sánchez, Manuel Carriedo Garrido, Vincent Harkins, Marc Vergés, and Jan Sans. -

-

- This ownership is publicly documented and verifiable across our project's assets: -

-
    -
  • Software License: Detailed in our public MIT License.
  • -
  • Source Repository: Explicitly stated in our GitHub README page.
  • -
  • Public Website: Documented here on our official project website.
  • -
-
-
- -
-

Platform Independence

-
-

- In accordance with the Digital Public Goods Alliance requirements for Open Software and Open AI Systems, FireForm guarantees platform independence. We do not rely on mandatory proprietary dependencies or closed components that would restrict our MIT License. -

-

- Core Dependencies: -

-
    -
  • Frontend: React, Electron, Node.js. (Declared in frontend/package.json)
  • -
  • Backend: Python, FastAPI, SQLite. (Declared in requirements.txt)
  • -
  • AI System (Optional): Ollama running open-weight models like Mistral. All inference runs locally. Note: The AI features are optional and not core to the main functionality of FireForm (which operates as a digital form and template manager). The AI extraction can be disabled in the application settings. Furthermore, this local Ollama dependency can be swapped with any other LLM service.
  • -
-

- Dependency Graph (SBOM): All components and their versions in our software supply chain are automatically tracked by our source repository. You can view our full dependency graph and SBOM directly on our GitHub repository. If any proprietary components are ever integrated, they will be strictly optional and replaceable with open alternatives without modifying the core solution. -

-
-
- -
-

Mechanism for Extracting Data

-
-

- Digital public goods must ensure that data and content can be extracted or imported in a non-proprietary format. FireForm embraces this requirement at the core of its architecture: -

-
    -
  • Standard Format (JSON): All data extracted by the LLM—whether non-PII or PII—is formatted and exported natively as a non-proprietary JSON object. This makes it instantly compatible with any modern software or data pipeline without vendor lock-in.
  • -
  • Open Storage (SQLite): Metadata, templates, and local configuration are stored using SQLite, an open, serverless database engine. The data is saved locally in a fireform.db file, which can be easily extracted, backed up, and read by hundreds of open-source tools.
  • -
  • API Access: Our local Python FastAPI backend exposes these JSON objects and database entries, allowing automated systems to securely extract the data.
  • -
-

- By standardizing on JSON and SQLite, FireForm guarantees that emergency departments retain full ownership and accessibility of their data at all times. -

-
-
- -
-

Privacy & Applicable Laws

-
-

- FireForm is designed from the ground up with a privacy-first, local-only architecture. Because emergency incident reports frequently contain sensitive Personally Identifiable Information (PII), we guarantee that no data ever leaves the user's device. -

-

- By eliminating cloud servers and third-party APIs, FireForm enables first responder organizations to easily comply with the world's most stringent data protection laws, including: -

-
    -
  • HIPAA (Health Insurance Portability and Accountability Act): Ensures EMS medical data remains entirely offline and secure.
  • -
  • CCPA (California Consumer Privacy Act): Ensures no consumer data is shared or sold.
  • -
  • GDPR (General Data Protection Regulation): Enforces strict data minimization and localizes data subjects' rights.
  • -
-

- For full details on our consent management procedures, data minimization practices, and how the solution was designed to comply with HIPAA, CCPA, and GDPR, please read our official Privacy Policy. -

-
-
- -
-

Do No Harm by Design

-
-

- FireForm is committed to anticipating and preventing harm by design, strictly adhering to the DPGA's framework: -

-
    -
  • 9A. Data Privacy & Security: Because our solution handles sensitive incident data (PII), we mitigate risk by strictly operating offline. Data is never exposed to public networks, avoiding data breaches. (See our Privacy Policy).
  • -
  • 9B. Inappropriate & Illegal Content: FireForm is an enterprise productivity tool for emergency responders, not a social platform. It does not host public user-generated content, completely mitigating the risk of distributing illegal or misleading public content.
  • -
  • 9C. Protection from Harassment: There is no public user-to-user interaction within the app. For our open-source contributor community, we strictly enforce our Code of Conduct to protect against harassment and abuse.
  • -
-
-
- -
-

Standards & Best Practices

-
-

- FireForm strictly aligns with globally recognized standards and best practices curated by the DPGA to ensure high-quality, interoperable, and sustainable software: -

-

Adhered Standards:

-
    -
  • JSON & UTF-8 (Data Interchange): All incident data extracted by our AI system is structured purely as JSON with UTF-8 encoding.
  • -
  • OpenAPI & REST (Data Exchange): Our Python backend is built with FastAPI, which automatically generates interactive OpenAPI specifications and follows RESTful architectural patterns.
  • -
-

Adhered Best Practices:

-
    -
  • Community: We maintain a welcoming environment through our Code of Conduct and clear Contribution Guidelines.
  • -
  • Lifecycle Management: All codebase modifications use Git for Change Management. We track progress with Tagged Releases and strictly adhere to Semantic Versioning.
  • -
  • Interoperability & Architecture: We prioritize Open Standards and File Formats, modularized Programmatic APIs, and strict Dependency Management.
  • -
-
-
- -
-

Public Communications & Mission

-

- Our mission is to build software that protects the people who protect us. FireForm was originally conceived and won 1st place at the Reboot the Earth hackathon, hosted by the UN and UC Santa Cruz, specifically targeting solutions for a better, more sustainable future. -

-

- We believe that modern technology like Local LLMs should be leveraged as public goods, accessible to any department regardless of budget, to help them better serve their communities. -

-
-
-
- -
-
-

FireForm is a Digital Public Good. Licensed under MIT.

-
-
- - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index d01e95e..0000000 --- a/docs/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - FireForm - Report Once, File Everywhere - - - - - - - -
-
-
-
- - - -
-
-
-
Reboot the Earth Hackathon Winner
-

Report Once,
File Everywhere.

-

- An open-source, AI-powered system built to solve administrative overhead for first responders. Save hundreds of hours by eliminating redundant paperwork. -

- - -
- -
-

Digital Public Good

-
-
-

FireForm is proudly recognized as a Digital Public Good (DPG). We are committed to advancing the United Nations' Sustainable Development Goals (SDGs).

-
- Learn about our SDG Relevance → -
-
- -
-

Current Features

-
-
-
📝
-

Smart PDF Conversion

-

Automatically turn any non-fillable PDF into a fillable, standardized template.

-
-
-
💾
-

Local Template Database

-

Store your templates in a local DB. Recover past templates and quickly generate new forms.

-
-
-
🎙️
-

Voice or Text Input

-

Provide input via text or voice transcription. A single input drives everything.

-
-
-
🤖
-

Automated LLM Filling

-

Our local LLM extracts relevant information and automatically populates the documents.

-
-
-
- -
-

Future Roadmap

-
-
-
-
-
🌍
-

External Data APIs

-

Connect to APIs to automatically fetch climate, location, and other external data.

-
-
-
-
-
-
📊
-

Insightful Dashboard

-

Visualize your reports with comprehensive graphics and statistics.

-
-
-
-
-
-
🧠
-

ML Field Detection

-

Machine learning models for advanced, automatic form field detection.

-
-
-
-
-
-
📑
-

Advanced PDF Features

-

More powerful PDF manipulation, merging, and management tools.

-
-
-
-
- -
-

About the Project

-
-
-

FireForm was born at a Silicon Valley hackathon organized by the United Nations, University of California, and CalFire.

-

After winning first place, the project was awarded a 6-month mentorship by Salesforce. We are also proud to be part of Google Summer of Code, welcoming new collaborators to scale our impact.

-
- -
-
-
-
- - - - diff --git a/docs/privacy.html b/docs/privacy.html deleted file mode 100644 index 461a370..0000000 --- a/docs/privacy.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - FireForm - Privacy Policy - - - - - - - -
-
-
-
- - - -
-
-
-

Privacy Policy

-

- FireForm is built on the principle of privacy by design. Our architecture guarantees that your data, including Personally Identifiable Information (PII), never leaves your device. -

-
-
-
- -
-
- -
-

1. Local-First Architecture

-

- FireForm is a completely offline, local-first application. All processing, including voice transcription and AI-based information extraction, is performed on your local hardware using local Large Language Models (LLMs) via Ollama. -

-

- We do not use cloud APIs, we do not have central servers that collect your data, and we do not track telemetry. Therefore, we do not collect, process, or share any PII with third parties. -

-
- -
-

2. Data Collection and Usage

-

- The only data collected by FireForm is the information you explicitly input (voice memos, text, and PDF templates) to generate incident reports. This data is stored locally on your device in a local SQLite database and standard JSON files. You maintain full ownership and control over this data, and can delete it or export it at any time. -

-

- Because all data remains on your device, consent management and data subject requests are fully within the control of the user or the deploying organization (e.g., the fire department). -

-
- -
-

3. Compliance with Applicable Laws

-

- By guaranteeing that data never leaves the host machine, FireForm enables deploying organizations to easily comply with the strictest data protection and privacy laws worldwide. We specifically support compliance with: -

-
    -
  • Health Insurance Portability and Accountability Act (HIPAA): By ensuring EMS and healthcare-related incident data remains entirely offline and secure on the local device.
  • -
  • California Consumer Privacy Act (CCPA) / CPRA: No consumer data is shared or sold. The local architecture defaults to maximum privacy.
  • -
  • General Data Protection Regulation (GDPR): Complete data minimization and localized processing ensures that no unauthorized cross-border data transfers occur, and data subjects' rights are easily managed locally.
  • -
-
- -
-

4. Contact Us

-

- If you have any questions about this Privacy Policy or how FireForm handles data, please contact the core maintainers via our GitHub Repository. -

-
- -
-
- - - - diff --git a/docs/styles.css b/docs/styles.css deleted file mode 100644 index 4f3dae7..0000000 --- a/docs/styles.css +++ /dev/null @@ -1,488 +0,0 @@ -:root { - --bg-dark: #0f0a0a; - --bg-card: rgba(20, 10, 10, 0.6); - --bg-card-hover: rgba(40, 15, 15, 0.8); - --text-primary: #ffffff; - --text-secondary: #ffb8b8; - --accent-red: #ff3b30; - --accent-orange: #ff9500; - --border-color: rgba(255, 59, 48, 0.2); - --glow-color: rgba(255, 59, 48, 0.4); - --font-main: 'Inter', system-ui, -apple-system, sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - background-color: var(--bg-dark); - color: var(--text-primary); - font-family: var(--font-main); - line-height: 1.6; - overflow-x: hidden; - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.container { - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 0 2rem; -} - -/* Background Effects */ -.background-effects { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: -1; - overflow: hidden; - pointer-events: none; -} - -.glow { - position: absolute; - width: 60vw; - height: 60vw; - border-radius: 50%; - filter: blur(100px); - opacity: 0.15; - animation: pulse 10s ease-in-out infinite alternate; -} - -.red-glow { - top: -20%; - right: -10%; - background: var(--accent-red); -} - -.orange-glow { - bottom: -20%; - left: -10%; - background: var(--accent-orange); - animation-delay: -5s; -} - -@keyframes pulse { - 0% { transform: scale(1) translate(0, 0); opacity: 0.15; } - 100% { transform: scale(1.1) translate(-5%, 5%); opacity: 0.25; } -} - -/* Navigation */ -.navbar { - padding: 1.5rem 0; - border-bottom: 1px solid var(--border-color); - background: rgba(15, 10, 10, 0.8); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - position: sticky; - top: 0; - z-index: 100; -} - -.navbar .container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo { - font-size: 1.5rem; - font-weight: 800; - letter-spacing: -0.5px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.fire-icon { - font-size: 1.8rem; - filter: drop-shadow(0 0 8px var(--accent-orange)); -} - -.github-link { - color: var(--text-primary); - text-decoration: none; - font-weight: 600; - padding: 0.5rem 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - transition: all 0.3s ease; -} - -.github-link:hover { - background: var(--border-color); - box-shadow: 0 0 15px var(--glow-color); -} - -/* Hero Section */ -.hero { - flex: 1; - display: flex; - align-items: center; - padding: 6rem 0; -} - -.hero-content { - text-align: center; - max-width: 800px; - margin: 0 auto; -} - -.badge { - display: inline-block; - padding: 0.4rem 1rem; - background: rgba(255, 149, 0, 0.1); - border: 1px solid var(--accent-orange); - color: var(--accent-orange); - border-radius: 20px; - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 1.5rem; - box-shadow: 0 0 15px rgba(255, 149, 0, 0.2); -} - -.hero-title { - font-size: 4rem; - font-weight: 800; - line-height: 1.1; - margin-bottom: 1.5rem; - letter-spacing: -1px; -} - -.gradient-text { - background: linear-gradient(135deg, var(--accent-red), var(--accent-orange)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.hero-subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - margin-bottom: 3rem; - font-weight: 300; -} - -/* Download Section */ -.download-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 3rem 2rem; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - box-shadow: 0 20px 40px rgba(0,0,0,0.4), inset 0 0 0 1px rgba(255,255,255,0.05); -} - -.download-heading { - font-size: 1.5rem; - margin-bottom: 2rem; - font-weight: 600; -} - -.button-group { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.btn { - display: flex; - align-items: center; - gap: 1rem; - padding: 1.25rem; - border-radius: 16px; - text-decoration: none; - transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); - background: rgba(25, 15, 15, 0.8); - border: 1px solid var(--border-color); - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(135deg, rgba(255,59,48,0.1), rgba(255,149,0,0.1)); - opacity: 0; - transition: opacity 0.3s ease; -} - -.btn:hover { - transform: translateY(-5px) scale(1.02); - border-color: var(--accent-orange); - box-shadow: 0 15px 30px rgba(255, 59, 48, 0.2); -} - -.btn:hover::before { - opacity: 1; -} - -.os-icon { - font-size: 2rem; - z-index: 1; -} - -.btn-text { - display: flex; - flex-direction: column; - align-items: flex-start; - z-index: 1; -} - -.os-name { - color: var(--text-primary); - font-weight: 600; - font-size: 1.1rem; -} - -.file-type { - color: var(--text-secondary); - font-size: 0.85rem; -} - -.view-all-link { - color: var(--accent-orange); - text-decoration: none; - font-size: 0.95rem; - font-weight: 600; - transition: color 0.2s ease; -} - -.view-all-link:hover { - color: var(--text-primary); -} - -/* Features Sections */ -.features-section { - margin-top: 5rem; -} - -.section-heading { - text-align: center; - font-size: 2.2rem; - margin-bottom: 2.5rem; - color: var(--text-primary); - font-weight: 800; -} - -.section-heading::after { - content: ''; - display: block; - width: 60px; - height: 4px; - background: linear-gradient(135deg, var(--accent-red), var(--accent-orange)); - margin: 1rem auto 0; - border-radius: 2px; -} - -.features-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 2rem; -} - -/* Timeline Styles */ -.timeline { - position: relative; - max-width: 800px; - margin: 0 auto; - padding: 2rem 0; -} -.timeline::before { - content: ''; - position: absolute; - left: 50%; - top: 0; - bottom: 0; - width: 2px; - background: linear-gradient(to bottom, var(--accent-red), var(--accent-orange)); - transform: translateX(-50%); -} -.timeline-item { - position: relative; - width: 50%; - padding: 1.5rem 3rem; -} -.timeline-item:nth-child(odd) { - left: 0; - text-align: right; -} -.timeline-item:nth-child(even) { - left: 50%; - text-align: left; -} -.timeline-dot { - position: absolute; - top: 50%; - width: 20px; - height: 20px; - background: var(--bg-dark); - border: 4px solid var(--accent-orange); - border-radius: 50%; - transform: translateY(-50%); - box-shadow: 0 0 10px var(--accent-orange); - z-index: 2; -} -.timeline-item:nth-child(odd) .timeline-dot { - right: -10px; -} -.timeline-item:nth-child(even) .timeline-dot { - left: -10px; -} -.timeline-content { - background: rgba(15, 10, 10, 0.4); - border: 1px dashed rgba(255, 149, 0, 0.3); - padding: 1.5rem; - border-radius: 16px; - transition: all 0.3s ease; -} -.timeline-item:hover .timeline-content { - background: rgba(255, 149, 0, 0.05); - border-style: solid; - border-color: var(--accent-orange); - transform: translateY(-5px); -} - -.feature-card { - background: rgba(15, 10, 10, 0.4); - border: 1px solid rgba(255, 255, 255, 0.05); - padding: 2rem; - border-radius: 16px; - text-align: left; - transition: transform 0.3s ease, border-color 0.3s ease; -} - -.feature-card:hover { - transform: translateY(-5px); - border-color: var(--border-color); -} - -.feature-icon { - font-size: 2.5rem; - margin-bottom: 1rem; -} - -.feature-card h3 { - margin-bottom: 0.5rem; - font-size: 1.2rem; -} - -.feature-card p { - color: var(--text-secondary); - font-size: 0.95rem; -} - -/* About Section Styles */ -.about-section { - margin-top: 5rem; - padding-bottom: 3rem; -} -.about-card { - background: linear-gradient(135deg, rgba(20, 10, 10, 0.8), rgba(40, 15, 15, 0.6)); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 3rem; - display: flex; - flex-direction: column; - gap: 2rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,59,48,0.1); -} -.about-content { - font-size: 1.1rem; - color: var(--text-secondary); -} -.about-content p { - margin-bottom: 1rem; -} -.about-content strong { - color: var(--text-primary); -} -.contact-links { - margin-top: 1rem; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); -} -.contact-links h3 { - margin-bottom: 1rem; - font-size: 1.2rem; -} -.social-badges { - display: flex; - gap: 1rem; - flex-wrap: wrap; -} -.badge-link { - display: inline-flex; - align-items: center; - padding: 0.5rem 1rem; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: var(--text-primary); - text-decoration: none; - border-radius: 8px; - font-size: 0.95rem; - transition: all 0.2s ease; -} -.badge-link:hover { - background: rgba(255, 59, 48, 0.1); - border-color: var(--accent-red); - transform: translateY(-2px); -} - -/* Footer */ -footer { - text-align: center; - padding: 2rem 0; - border-top: 1px solid rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.4); - font-size: 0.9rem; -} - -/* Responsive */ -@media (max-width: 768px) { - .hero-title { - font-size: 2.8rem; - } - - .hero { - padding: 3rem 0; - } - - .download-section { - padding: 2rem 1.5rem; - } - - .timeline::before { - left: 20px; - } - .timeline-item { - width: 100%; - padding-left: 50px; - padding-right: 0; - } - .timeline-item:nth-child(odd), .timeline-item:nth-child(even) { - left: 0; - text-align: left; - } - .timeline-item:nth-child(odd) .timeline-dot, .timeline-item:nth-child(even) .timeline-dot { - left: 10px; - right: auto; - } - .about-card { - padding: 2rem 1.5rem; - } -} diff --git a/docs/terms.html b/docs/terms.html deleted file mode 100644 index 557c127..0000000 --- a/docs/terms.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - FireForm - Terms of Service - - - - - - - -
-
-
-
- - - -
-
-
-

Terms of Service

-

- By downloading, installing, or using FireForm you agree to the following terms. Please read them carefully. If you do not agree, do not use the software. -

-

- Last updated: May 2026 -

-
-
-
- -
-
- -
-

1. Acceptance of Terms

-

- These Terms of Service ("Terms") govern your access to and use of the FireForm software application, source code, documentation, and related materials (collectively, the "Software"). By using the Software you confirm that you are at least 18 years old (or the age of majority in your jurisdiction) and have the legal authority to accept these Terms on behalf of yourself or the organization you represent. -

-
- -
-

2. Open-Source License

-

- FireForm is released under the MIT License. You are free to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software subject to the conditions stated in the LICENSE file included with every distribution and available in our GitHub repository. -

-

- Nothing in these Terms restricts rights granted to you by the MIT License; they are intended solely to clarify responsibilities and limitations not addressed by that license. -

-
- -
-

3. Permitted Use

-

You may use FireForm for any lawful purpose, including:

-
    -
  • Internal use by emergency services, public safety agencies, and fire departments.
  • -
  • Research, academic, and non-commercial projects.
  • -
  • Commercial deployments, provided you comply with the MIT License attribution requirements.
  • -
  • Developing derivative works or integrations, subject to the MIT License terms.
  • -
-
- -
-

4. Prohibited Use

-

You must not use FireForm to:

-
    -
  • Violate any applicable law or regulation, including but not limited to laws governing data protection, privacy, and public safety reporting.
  • -
  • Intentionally generate false, fraudulent, or misleading incident reports.
  • -
  • Infringe the intellectual property rights of any third party.
  • -
  • Introduce malware, exploits, or other harmful code into the Software or its dependencies.
  • -
  • Use the Software in a manner that endangers the health or safety of any person.
  • -
-
- -
-

5. No Warranty

-

- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THE FIREFORM CONTRIBUTORS DO NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE, UNINTERRUPTED, SECURE, OR THAT ANY DEFECTS WILL BE CORRECTED. USE OF THE SOFTWARE FOR CRITICAL PUBLIC-SAFETY OPERATIONS IS ENTIRELY AT YOUR OWN RISK AND YOUR ORGANIZATION'S DISCRETION. -

-
- -
-

6. Limitation of Liability

-

- TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE FIREFORM CONTRIBUTORS, MAINTAINERS, SPONSORS, OR AFFILIATED ORGANIZATIONS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES — INCLUDING LOSS OF DATA, REVENUE, GOODWILL, OR LIFE — ARISING OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN JURISDICTIONS THAT DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR LIMITATIONS OF LIABILITY, OUR LIABILITY IS LIMITED TO THE FULLEST EXTENT PERMITTED BY LAW. -

-
- -
-

7. User-Generated Content and Data

-

- FireForm processes data exclusively on your local device. You retain full ownership of all incident reports, templates, voice recordings, and any other data you create or import. We do not access, store, or transmit your data. -

-

- You are solely responsible for the accuracy and legality of the data you enter into FireForm and for complying with any reporting obligations imposed by your jurisdiction or regulatory authority. -

-
- -
-

8. Third-Party Components

-

- FireForm integrates with third-party software, including Ollama for local LLM inference and Whisper for voice transcription. Your use of those components is governed by their respective licenses and terms. We make no representations or warranties regarding third-party software and are not responsible for any issues arising from their use. -

-
- -
-

9. Contributions

-

- Contributions to FireForm (pull requests, issues, documentation, etc.) are subject to our Contributing Guidelines and Code of Conduct. By submitting a contribution you confirm that you have the right to license it under the MIT License and that you grant the FireForm project a perpetual, worldwide, royalty-free license to use, reproduce, and distribute your contribution. -

-
- -
-

10. Changes to These Terms

-

- We may update these Terms from time to time. When we do, we will revise the "Last updated" date at the top of this page and, for material changes, post a notice in our GitHub repository. Continued use of FireForm after changes are posted constitutes your acceptance of the revised Terms. -

-
- -
-

11. Contact

-

- If you have questions about these Terms, please reach out to the core maintainers via our GitHub Repository. We welcome feedback from deploying organizations and community members. -

-
- -
-
- - - - From a318ff976ab668e8cc0b6792c35fc4e2e93aafb5 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 7 Jun 2026 20:33:01 +0530 Subject: [PATCH 11/18] added entrypoint, readme and updated docker file and compose --- .gitignore | 3 ++- docker/.env.example | 27 +++++++++++++++++++++++++++ docker/README.md | 31 +++++++++++++++++++++++++++++++ docker/dev/compose.yml | 1 + docker/entrypoint.sh | 10 ++++++++++ docker/prod/Dockerfile | 2 ++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 docker/.env.example create mode 100644 docker/README.md create mode 100755 docker/entrypoint.sh diff --git a/.gitignore b/.gitignore index 268a0ff..1515bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ frontend/release/ # Local Claude Code instructions CLAUDE.md -*temp/ \ No newline at end of file +*temp/ +*.env* \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..c4321ac --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,27 @@ +# Copy this file to .env.dev or .env.prod and fill in values. +# Never commit .env.dev or .env.prod — they are gitignored. + +# --- App ------------------------------------------------------------------ +APP_PORT=8000 + +# --- Ollama --------------------------------------------------------------- +# Ollama runs in Docker with port mapped to host. App runs on host. +# Override to point at an external Ollama instance (e.g. a GPU server). +OLLAMA_HOST=http://localhost:11434 +OLLAMA_MODEL=qwen2.5:1.5b +OLLAMA_TIMEOUT=300 + +# --- Whisper -------------------------------------------------------------- +# Whisper runs in Docker with port mapped to host. App runs on host. +# Override to point at an external Whisper endpoint. +WHISPER_HOST=http://localhost:9000 +WHISPER_MODEL=small.en + +# --- Gunicorn (prod only) ------------------------------------------------- +GUNICORN_WORKERS=2 + +# --- CORS ----------------------------------------------------------------- +# Comma-separated list of allowed frontend origins. +# Dev: http://localhost:5173,http://127.0.0.1:5173 +# Prod: https://your-domain.com +FRONTEND_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..e238c64 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,31 @@ +# Docker + +``` +docker/ + dev/ + Dockerfile # uvicorn --reload, source-mounted for hot reload + compose.yml + prod/ + Dockerfile # multi-stage build, gunicorn, no source mount + compose.yml + .env.example # template — copy to .env.dev or .env.prod + .env.dev # gitignored, dev values + .env.prod # gitignored, prod values + entrypoint.sh # runs DB migrations then exec's the server command + README.md +``` + +## Env vars + +See `.env.example` for the full list with descriptions. + +## Volumes + +`docker compose down` never removes volumes. Use `docker compose down -v` only to intentionally wipe all data. + +| Volume | Path in container | Purpose | +| ------------------ | ---------------------- | ----------------------------------- | +| `fireform_db` | `/data/db/fireform.db` | SQLite database | +| `fireform_uploads` | `/data/uploads` | Uploaded templates + generated PDFs | +| `ollama_data` | `/root/.ollama` | Ollama model weights | +| `whisper_models` | `/data/whisper` | Whisper model cache | diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 5b7c2f0..cdb94cc 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -45,6 +45,7 @@ services: condition: service_healthy whisper: condition: service_started + entrypoint: ["sh", "docker/entrypoint.sh"] command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] volumes: - ../..:/app diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..84a8142 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# Ensure data directories exist (volumes may be empty on first run) +mkdir -p /data/db /data/uploads + +# Run DB migrations / init before starting the server +python3 -m app.db.init_db + +exec "$@" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 8ad8dea..f97d6dd 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -22,6 +22,7 @@ COPY --from=builder /install /usr/local # Copy only app code, not data/ temp/ tests/ docs/ etc. COPY app/ ./app/ COPY requirements.txt . +COPY docker/entrypoint.sh /entrypoint.sh ENV PYTHONPATH=/app @@ -30,6 +31,7 @@ RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh EXPOSE 8000 +ENTRYPOINT ["/entrypoint.sh"] CMD ["gunicorn", "app.main:app", \ "--worker-class", "uvicorn.workers.UvicornWorker", \ "--bind", "0.0.0.0:8000", \ From eb20a9570b70559ad6dd92f665378ab53a8b4ad2 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 7 Jun 2026 20:44:30 +0530 Subject: [PATCH 12/18] fixed tests, replaced api -> app.api following our current code structure --- tests/test_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 33b2c62..e98d16f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -125,7 +125,7 @@ def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch): # Point the upload directory inside tmp_path (which is inside the project # for the path-safety check — we monkeypatch the check). monkeypatch.setattr( - "api.routes.templates.PROJECT_ROOT", + "app.api.routes.templates.PROJECT_ROOT", tmp_path, ) resp = client.post( @@ -234,7 +234,7 @@ def fake_post(url, params=None, files=None, timeout=None): captured["files"] = files return fake_response - monkeypatch.setattr("api.routes.forms.requests.post", fake_post) + monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"RIFFfake"), "audio/wav")) resp = client.post("/forms/transcribe", files=[audio]) @@ -252,7 +252,7 @@ def test_list_models(self, client, monkeypatch): fake_response = MagicMock() fake_response.json.return_value = {"models": [{"name": "qwen2.5:3b"}, {"name": "mistral:latest"}]} fake_response.raise_for_status.return_value = None - monkeypatch.setattr("api.routes.forms.requests.get", lambda *a, **k: fake_response) + monkeypatch.setattr("app.api.routes.forms.requests.get", lambda *a, **k: fake_response) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") resp = client.get("/forms/models") @@ -269,7 +269,7 @@ def test_list_models_ollama_down(self, client, monkeypatch): def boom(*a, **k): raise requests.exceptions.ConnectionError("down") - monkeypatch.setattr("api.routes.forms.requests.get", boom) + monkeypatch.setattr("app.api.routes.forms.requests.get", boom) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") resp = client.get("/forms/models") @@ -296,7 +296,7 @@ def test_transcribe_service_unavailable(self, client, monkeypatch): def fake_post(*args, **kwargs): raise requests.exceptions.ConnectionError("no service") - monkeypatch.setattr("api.routes.forms.requests.post", fake_post) + monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"data"), "audio/wav")) resp = client.post("/forms/transcribe", files=[audio]) @@ -315,7 +315,7 @@ class TestE2EPipeline: def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypatch, db): # -- Step 1: Upload a PDF -- - monkeypatch.setattr("api.routes.templates.PROJECT_ROOT", tmp_path) + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) upload_resp = client.post( "/templates/upload", files=[pdf_upload], From b4f6c709f3b3e044b37a041a4ac24ef1356849b5 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 9 Jun 2026 03:46:45 +0530 Subject: [PATCH 13/18] added API contracts --- contracts/openapi.yaml | 98 +++ contracts/path/extraction.yaml | 313 ++++++++ contracts/path/forms.yaml | 348 ++++++++ contracts/path/incidents.yaml | 313 ++++++++ contracts/path/input.yaml | 247 ++++++ contracts/path/jobs.yaml | 79 ++ contracts/path/reporting.yaml | 191 +++++ contracts/path/system.yaml | 152 ++++ contracts/path/templates.yaml | 267 +++++++ contracts/schemas/canonical-incident.yaml | 930 ++++++++++++++++++++++ contracts/schemas/common.yaml | 129 +++ contracts/schemas/enums.yaml | 130 +++ contracts/schemas/extraction-record.yaml | 134 ++++ contracts/schemas/form-record.yaml | 198 +++++ contracts/schemas/incident-record.yaml | 150 ++++ contracts/schemas/input-record.yaml | 138 ++++ contracts/schemas/reporting.yaml | 112 +++ contracts/schemas/system.yaml | 101 +++ contracts/schemas/template.yaml | 144 ++++ 19 files changed, 4174 insertions(+) create mode 100644 contracts/openapi.yaml create mode 100644 contracts/path/extraction.yaml create mode 100644 contracts/path/forms.yaml create mode 100644 contracts/path/incidents.yaml create mode 100644 contracts/path/input.yaml create mode 100644 contracts/path/jobs.yaml create mode 100644 contracts/path/reporting.yaml create mode 100644 contracts/path/system.yaml create mode 100644 contracts/path/templates.yaml create mode 100644 contracts/schemas/canonical-incident.yaml create mode 100644 contracts/schemas/common.yaml create mode 100644 contracts/schemas/enums.yaml create mode 100644 contracts/schemas/extraction-record.yaml create mode 100644 contracts/schemas/form-record.yaml create mode 100644 contracts/schemas/incident-record.yaml create mode 100644 contracts/schemas/input-record.yaml create mode 100644 contracts/schemas/reporting.yaml create mode 100644 contracts/schemas/system.yaml create mode 100644 contracts/schemas/template.yaml diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml new file mode 100644 index 0000000..3b19ea3 --- /dev/null +++ b/contracts/openapi.yaml @@ -0,0 +1,98 @@ +openapi: 3.0.0 + +info: + title: FireForm API + version: 1.0.0 + description: | + Proposed API contract for FireForm as GSoC project 2026 by [chetanr25.in](https://chetanr25.in) + contact: + name: Chetan R + url: https://github.com/chetanr25 + email: chetan250204@gmail.com + +servers: + - url: http://localhost:8080 + description: Local FireForm instance (default) + +tags: + - name: input + description: Submit voice or text incident narratives + - name: extraction + description: AI-powered data extraction from narratives into canonical JSON + - name: forms + description: Generate agency-specific PDF forms from extracted data + - name: incidents + description: Manage incident records linking inputs, extractions, and forms + - name: reporting + description: Aggregate statistics and periodic report generation + - name: templates + description: Form template configuration and management + - name: system + description: Health checks, schema introspection, and system status + - name: jobs + description: Cross-cutting async job status polling + +paths: + # ── Layer 1: Input ────────────────────────────────────────────── + /api/v1/input/voice: + $ref: "path/input.yaml#/voice" + /api/v1/input/text: + $ref: "path/input.yaml#/text" + /api/v1/input/{input_id}: + $ref: "path/input.yaml#/input_by_id" + + # ── Layer 2: AI Extraction ───────────────────────────────────── + /api/v1/extract/{input_id}: + $ref: "path/extraction.yaml#/extract_by_input" + /api/v1/extract/{extract_id}: + $ref: "path/extraction.yaml#/extract_by_id" + /api/v1/extract/{extract_id}/validate: + $ref: "path/extraction.yaml#/validate" + + # ── Layer 3: Form Generation ─────────────────────────────────── + /api/v1/forms/generate/all: + $ref: "path/forms.yaml#/generate_all" + /api/v1/forms/generate/{form_type}: + $ref: "path/forms.yaml#/generate_single" + /api/v1/forms/{form_id}: + $ref: "path/forms.yaml#/form_by_id" + /api/v1/forms/{form_id}/pdf: + $ref: "path/forms.yaml#/form_pdf" + /api/v1/forms/{form_id}/json: + $ref: "path/forms.yaml#/form_json" + /api/v1/forms/batch/{batch_id}: + $ref: "path/forms.yaml#/batch_by_id" + + # ── Layer 4: Incident Management ─────────────────────────────── + /api/v1/incidents: + $ref: "path/incidents.yaml#/incidents" + /api/v1/incidents/{incident_id}: + $ref: "path/incidents.yaml#/incident_by_id" + + # ── Layer 5: Reporting & Analytics ───────────────────────────── + /api/v1/reports/summary: + $ref: "path/reporting.yaml#/summary" + /api/v1/reports/generate: + $ref: "path/reporting.yaml#/generate" + /api/v1/reports/{report_id}: + $ref: "path/reporting.yaml#/report_by_id" + + # ── Layer 6: Templates & Configuration ───────────────────────── + /api/v1/templates: + $ref: "path/templates.yaml#/templates" + /api/v1/templates/{template_id}: + $ref: "path/templates.yaml#/template_by_id" + /api/v1/templates/{template_id}/fields: + $ref: "path/templates.yaml#/template_fields" + + # ── Layer 7: System ──────────────────────────────────────────── + /api/v1/health: + $ref: "path/system.yaml#/health" + /api/v1/schema/incident: + $ref: "path/system.yaml#/schema_incident" + /api/v1/schema/incident/versions: + $ref: "path/system.yaml#/schema_versions" + + # ── Layer 8: Async Jobs ──────────────────────────────────────── + /api/v1/jobs/{job_id}: + $ref: "path/jobs.yaml#/job_by_id" diff --git a/contracts/path/extraction.yaml b/contracts/path/extraction.yaml new file mode 100644 index 0000000..0e11309 --- /dev/null +++ b/contracts/path/extraction.yaml @@ -0,0 +1,313 @@ +# Layer 2 AI Extraction Endpoints +# POST /api/v1/extract/{input_id} +# GET /api/v1/extract/{extract_id} +# PATCH /api/v1/extract/{extract_id} +# POST /api/v1/extract/{extract_id}/validate + +extract_by_input: + post: + operationId: createExtraction + summary: Start AI extraction from input narrative + description: | + Sends the narrative (from a previously submitted input) to the local Ollama + LLM with a structured prompt to extract all incident fields into the canonical + FireForm JSON schema. This is an asynchronous operation the LLM may take + 30–120 seconds. Returns an extract_id and job_id for polling. + tags: + - extraction + parameters: + - name: input_id + in: path + required: true + description: ID of the input record to extract from + schema: + type: string + format: uuid + requestBody: + required: false + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ExtractionRequest" + example: + model_override: "llama3:8b" + extraction_hints: + incident_type: "wildland_fire" + state: "CA" + responses: + "202": + description: Extraction job queued successfully + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/AsyncJobResponse" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "processing" + input_id: "550e8400-e29b-41d4-a716-446655440001" + queued_at: "2024-07-15T14:30:00Z" + estimated_seconds: 60 + poll_url: "/api/v1/extract/550e8400-e29b-41d4-a716-446655440020" + "404": + description: Input ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INPUT_NOT_FOUND" + message: "Input with ID 550e8400-e29b-41d4-a716-446655440099 not found" + "409": + description: Conflict input not ready or extraction already exists + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + not_ready: + summary: Input still transcribing + value: + error_code: "INPUT_NOT_READY" + message: "Input is in 'transcribing' state. Wait until status is 'ready'." + detail: + current_status: "transcribing" + already_exists: + summary: Extraction already initiated for this input + value: + error_code: "EXTRACTION_EXISTS" + message: "An extraction already exists for this input" + detail: + existing_extract_id: "550e8400-e29b-41d4-a716-446655440020" + "503": + description: Ollama LLM service unavailable + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "LLM_UNAVAILABLE" + message: "Ollama LLM service is not available" + retry_after_seconds: 30 + detail: + ollama_status: "connection_refused" + +extract_by_id: + get: + operationId: getExtraction + summary: Get extraction result by ID + description: | + Returns the full canonical FireForm JSON when extraction is complete, or + the current job status while still processing. When status is "completed", + the response body contains the entire canonical incident schema. When still + processing, includes a retry_after_seconds hint for polling. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction + schema: + type: string + format: uuid + responses: + "200": + description: Extraction record (completed or in-progress) + content: + application/json: + schema: + oneOf: + - $ref: "../schemas/extraction-record.yaml#/ExtractionCompleted" + - $ref: "../schemas/extraction-record.yaml#/ExtractionProcessing" + examples: + completed: + summary: Extraction completed with canonical JSON + value: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "completed" + completed_at: "2024-07-15T14:31:05Z" + canonical_incident: + schema_version: "1.1.0" + schema_name: "fireform_canonical_incident" + extraction_metadata: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + confidence_score: 0.91 + incident: + name: "Bear Creek Wildfire" + processing: + summary: Extraction still in progress + value: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "processing" + started_at: "2024-07-15T14:30:00Z" + retry_after_seconds: 5 + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_NOT_FOUND" + message: "Extraction with ID 550e8400-e29b-41d4-a716-446655440099 not found" + + patch: + operationId: updateExtraction + summary: Manually correct extracted fields + description: | + Allows a responder to correct any field in the canonical JSON after LLM + extraction. Uses JSON Merge Patch (RFC 7396) only send the fields that + changed. The server records an audit trail of all changes vs the original + LLM output and recalculates completeness scores and applicable_forms. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction to update + schema: + type: string + format: uuid + requestBody: + required: true + description: JSON Merge Patch (RFC 7396) only include changed fields + content: + application/merge-patch+json: + schema: + $ref: "../schemas/canonical-incident.yaml#/CanonicalIncident" + example: + fire: + estimated_damage_usd: 250000 + casualties: + total_responder_injuries: 2 + responses: + "200": + description: Extraction updated returns full updated canonical JSON + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ExtractionCompleted" + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Extraction is locked (already submitted) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_LOCKED" + message: "Cannot modify extraction incident report has been submitted" + detail: + report_status: "submitted" + submitted_at: "2024-07-15T18:00:00Z" + "422": + description: Invalid field path or value + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Invalid field path or value in patch" + validation_errors: + - field: "fire.cause_certainty" + issue: "Must be one of: confirmed, probable, suspected, undetermined" + value: "maybe" + +validate: + post: + operationId: validateExtraction + summary: Validate extraction against a form's requirements + description: | + Validates the canonical JSON against a specific form type's field requirements. + Returns whether the extraction has all required fields, which recommended + fields are missing, and any warnings. Useful for checking "can I generate + a NERIS report with what I have?" before triggering form generation. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction to validate + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - form_type + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + example: + form_type: "neris" + responses: + "200": + description: Validation result + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ValidationResult" + example: + valid: true + form_type: "neris" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + missing_required: [] + missing_recommended: + - "fire.detector_present" + - "fire.detector_operated" + warnings: + - "fire.estimated_damage_usd is null NERIS recommends providing damage estimates" + field_coverage_percent: 94 + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Unknown form type + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "UNKNOWN_FORM_TYPE" + message: "Form type 'xyz' is not recognized" + detail: + valid_form_types: + - neris + - nemsis_epcr + - nibrs + - nfirs_basic + - nfirs_fire + - nfirs_structure + - nfirs_wildland + - nfirs_ems + - nfirs_hazmat + - nfirs_apparatus + - nfirs_personnel + - nfirs_arson + - nfirs_casualty_civilian + - nfirs_casualty_responder + - cal_fire_ics209 + - osha_301 + - un_ssirs + - state_georgia + - state_california + - state_new_york diff --git a/contracts/path/forms.yaml b/contracts/path/forms.yaml new file mode 100644 index 0000000..b920b41 --- /dev/null +++ b/contracts/path/forms.yaml @@ -0,0 +1,348 @@ +# Layer 3 Form Generation Endpoints +# POST /api/v1/forms/generate/all +# POST /api/v1/forms/generate/{form_type} +# GET /api/v1/forms/{form_id} +# GET /api/v1/forms/{form_id}/pdf +# GET /api/v1/forms/{form_id}/json +# GET /api/v1/forms/batch/{batch_id} + +generate_all: + post: + operationId: generateAllForms + summary: Generate all applicable forms from an extraction + description: | + Triggers batch generation of ALL forms listed in the extraction's + extraction_metadata.applicable_forms. This is an async batch job. + Use skip_incomplete to skip forms that fail validation, or force_partial + to generate forms with blank fields where data is missing. + tags: + - forms + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/GenerateAllRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + options: + skip_incomplete: true + force_partial: false + responses: + "202": + description: Batch form generation job accepted + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/BatchGenerateResponse" + example: + batch_id: "550e8400-e29b-41d4-a716-446655440030" + status: "processing" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + forms_queued: + - "neris" + - "nfirs_basic" + - "nfirs_wildland" + forms_skipped: + - form_type: "nemsis_epcr" + reason: "Missing required fields: ems.patients[0].date_of_birth" + estimated_seconds: 45 + poll_url: "/api/v1/forms/batch/550e8400-e29b-41d4-a716-446655440030" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Extraction not yet completed + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_NOT_COMPLETED" + message: "Extraction is still processing. Wait until status is 'completed'." + detail: + current_status: "processing" + "422": + description: No applicable forms found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "NO_APPLICABLE_FORMS" + message: "No forms are applicable for this extraction's incident type" + +generate_single: + post: + operationId: generateSingleForm + summary: Generate one specific form type + description: | + Generates a single agency-specific form from the canonical extraction data. + The form_type path parameter specifies which form to generate. If the extraction + is missing required fields for this form, returns 422 unless force_partial is true. + tags: + - forms + parameters: + - name: form_type + in: path + required: true + description: Type of form to generate + schema: + $ref: "../schemas/enums.yaml#/FormType" + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/GenerateSingleRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + options: + output_format: "pdf" + force_partial: false + responses: + "202": + description: Form generation job accepted + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormGenerateResponse" + example: + form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "processing" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + job_id: "550e8400-e29b-41d4-a716-446655440097" + estimated_seconds: 15 + poll_url: "/api/v1/forms/550e8400-e29b-41d4-a716-446655440040" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Validation failure or form not in applicable list + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "FORM_VALIDATION_FAILED" + message: "Extraction is missing required fields for NERIS form" + detail: + form_type: "neris" + missing_fields: + - "incident.types[0].neris_code" + - "location.coordinates" + validation_errors: + - field: "incident.types[0].neris_code" + issue: "Required field is null" + +form_by_id: + get: + operationId: getForm + summary: Get form metadata and status + description: | + Returns the form record including generation status, associated extract and + incident IDs, and a field_mapping_summary showing how canonical fields were + mapped to the form's agency-specific fields (for audit/transparency). + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the generated form + schema: + type: string + format: uuid + responses: + "200": + description: Form record found + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormRecord" + example: + form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_id: "550e8400-e29b-41d4-a716-446655440050" + created_at: "2024-07-15T14:32:00Z" + completed_at: "2024-07-15T14:32:12Z" + pdf_ready: true + json_ready: true + field_mapping_summary: + total_form_fields: 85 + fields_filled: 72 + fields_blank: 13 + coverage_percent: 84.7 + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +form_pdf: + get: + operationId: downloadFormPdf + summary: Download filled PDF form + description: | + Returns the filled PDF binary file for the specified form. If the form + generation is still in progress, returns 202 Accepted with a retry_after + hint. The Content-Disposition header is set for browser download. + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the form to download + schema: + type: string + format: uuid + responses: + "200": + description: Filled PDF file + content: + application/pdf: + schema: + type: string + format: binary + headers: + Content-Disposition: + description: Attachment filename for download + schema: + type: string + example: 'attachment; filename="NERIS_FF-2024-CA-0157.pdf"' + "202": + description: Form generation still in progress + content: + application/json: + schema: + type: object + properties: + message: + type: string + status: + type: string + retry_after_seconds: + type: integer + example: + message: "Form generation is still in progress" + status: "processing" + retry_after_seconds: 5 + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "500": + description: PDF generation failed + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "PDF_GENERATION_FAILED" + message: "Failed to generate PDF for form" + detail: + reason: "Template file corrupted or missing" + +form_json: + get: + operationId: getFormJson + summary: Get form-specific field-mapped JSON + description: | + Returns the form-specific JSON with fields mapped to the agency's expected + format not the canonical FireForm schema, but the actual field names and + structure that the target agency system expects. Useful for future direct + API submission to agency systems. + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the form + schema: + type: string + format: uuid + responses: + "200": + description: Form-specific mapped JSON + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormMappedJson" + example: + form_type: "neris" + form_version: "2.0" + form_id: "550e8400-e29b-41d4-a716-446655440040" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + agency_fields: + incident_type: "wildland-fire" + incident_date: "2024-07-10" + alarm_time: "13:52" + acres_burned: 1247 + cause: "natural" + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +batch_by_id: + get: + operationId: getBatchStatus + summary: Get batch form generation status + description: | + Returns the status of a batch form generation job including progress + for each individual form. Poll this endpoint after POST /forms/generate/all. + tags: + - forms + parameters: + - name: batch_id + in: path + required: true + description: Unique identifier of the batch job + schema: + type: string + format: uuid + responses: + "200": + description: Batch job status + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/BatchStatus" + example: + batch_id: "550e8400-e29b-41d4-a716-446655440030" + status: "completed" + total: 3 + completed: 3 + failed: 0 + forms: + - form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + - form_id: "550e8400-e29b-41d4-a716-446655440041" + form_type: "nfirs_basic" + status: "completed" + - form_id: "550e8400-e29b-41d4-a716-446655440042" + form_type: "nfirs_wildland" + status: "completed" + "404": + description: Batch job not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/incidents.yaml b/contracts/path/incidents.yaml new file mode 100644 index 0000000..810a311 --- /dev/null +++ b/contracts/path/incidents.yaml @@ -0,0 +1,313 @@ +# Layer 4 Incident Management Endpoints +# POST /api/v1/incidents +# GET /api/v1/incidents +# GET /api/v1/incidents/{incident_id} +# PATCH /api/v1/incidents/{incident_id} +# DELETE /api/v1/incidents/{incident_id} + +incidents: + post: + operationId: createIncident + summary: Create a full incident record + description: | + Creates a permanent incident record that links input, extraction, and + generated forms into a single coherent unit. Assigns a permanent incident_id + and stores the complete incident for future retrieval and reporting. + tags: + - incidents + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/CreateIncidentRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_number: "CA-SQF-2024-0421" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + responses: + "201": + description: Incident record created + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecord" + example: + incident_id: "550e8400-e29b-41d4-a716-446655440050" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_number: "CA-SQF-2024-0421" + status: "draft" + forms_generated: + - form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + created_at: "2024-07-15T14:35:00Z" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Duplicate incident number + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "DUPLICATE_INCIDENT_NUMBER" + message: "Incident number CA-SQF-2024-0421 already exists" + detail: + existing_incident_id: "550e8400-e29b-41d4-a716-446655440049" + + get: + operationId: listIncidents + summary: List incidents with filtering + description: | + Returns a paginated list of incident records with optional filtering by + date range, incident type, and status. Supports sorting by date. + Maximum 100 results per page. + tags: + - incidents + parameters: + - name: date_from + in: query + description: Start date filter (inclusive, ISO 8601) + schema: + type: string + format: date + - name: date_to + in: query + description: End date filter (inclusive, ISO 8601) + schema: + type: string + format: date + - name: incident_type + in: query + description: Filter by incident category + schema: + $ref: "../schemas/enums.yaml#/IncidentCategory" + - name: status + in: query + description: Filter by report status + schema: + $ref: "../schemas/enums.yaml#/ReportStatus" + - name: page + in: query + description: Page number (1-based) + schema: + type: integer + minimum: 1 + default: 1 + - name: per_page + in: query + description: Items per page (max 100) + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + description: Sort order + schema: + type: string + enum: + - date_asc + - date_desc + default: date_desc + responses: + "200": + description: Paginated list of incidents + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentListResponse" + example: + data: + - incident_id: "550e8400-e29b-41d4-a716-446655440050" + incident_number: "CA-SQF-2024-0421" + status: "draft" + incident_name: "Bear Creek Wildfire" + incident_type: "fire" + incident_date: "2024-07-10" + forms_count: 3 + created_at: "2024-07-15T14:35:00Z" + pagination: + total: 42 + page: 1 + per_page: 20 + total_pages: 3 + has_next: true + has_prev: false + "422": + description: Invalid query parameter format + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Invalid date format for date_from" + validation_errors: + - field: "date_from" + issue: "Must be ISO 8601 date format (YYYY-MM-DD)" + value: "15/07/2024" + +incident_by_id: + get: + operationId: getIncident + summary: Get full incident record + description: | + Returns the complete incident record including the linked canonical + extraction, all generated forms and their statuses, submission log, + and audit trail. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident + schema: + type: string + format: uuid + responses: + "200": + description: Full incident record + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecordFull" + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + + patch: + operationId: updateIncident + summary: Update incident metadata + description: | + Updates incident metadata such as status, tags, incident number, and notes. + Does NOT allow changing the underlying extraction data use PATCH /extract + for that. Submitted incidents cannot be modified. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident to update + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/UpdateIncidentRequest" + example: + status: "approved" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + - "reviewed" + notes: "Reviewed by BC Wilson. Ready for submission." + responses: + "200": + description: Incident updated + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecord" + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Cannot modify a submitted incident + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INCIDENT_SUBMITTED" + message: "Cannot modify an incident that has been submitted" + detail: + submitted_at: "2024-07-15T18:00:00Z" + + delete: + operationId: deleteIncident + summary: Soft-delete an incident + description: | + Performs a soft delete sets a deleted_at timestamp but never removes data. + Submitted incidents cannot be deleted. Soft-deleted incidents are excluded + from list queries by default but can be recovered. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident to delete + schema: + type: string + format: uuid + responses: + "200": + description: Incident soft-deleted + content: + application/json: + schema: + type: object + properties: + incident_id: + type: string + format: uuid + deleted_at: + type: string + format: date-time + recoverable: + type: boolean + example: + incident_id: "550e8400-e29b-41d4-a716-446655440050" + deleted_at: "2024-07-16T10:00:00Z" + recoverable: true + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Cannot delete already deleted or submitted + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + already_deleted: + summary: Incident already soft-deleted + value: + error_code: "ALREADY_DELETED" + message: "Incident has already been deleted" + detail: + deleted_at: "2024-07-16T09:00:00Z" + submitted: + summary: Submitted incidents cannot be deleted + value: + error_code: "INCIDENT_SUBMITTED" + message: "Submitted incidents cannot be deleted" diff --git a/contracts/path/input.yaml b/contracts/path/input.yaml new file mode 100644 index 0000000..a2d6a57 --- /dev/null +++ b/contracts/path/input.yaml @@ -0,0 +1,247 @@ +# Layer 1 Input Endpoints +# POST /api/v1/input/voice, POST /api/v1/input/text, GET /api/v1/input/{input_id} + +voice: + post: + operationId: submitVoiceInput + summary: Submit a voice recording for transcription + description: | + Accepts an audio file (voice memo) from a first responder along with optional + incident metadata. The audio is saved and queued for transcription via Whisper + running on the local Ollama instance. Returns an input_id immediately the + transcription runs asynchronously. Poll GET /api/v1/input/{input_id} for status. + tags: + - input + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - audio_file + properties: + audio_file: + type: string + format: binary + description: Audio file in wav, mp3, m4a, ogg, or webm format. Max 500MB. + station_id: + type: string + description: Station identifier (e.g. "STA-045") + responder_badge: + type: string + description: Badge number of the responder submitting the report + incident_date_hint: + type: string + format: date + description: Approximate date of the incident to aid extraction + example: + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-15" + responses: + "201": + description: Voice input accepted and queued for transcription + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/VoiceInputResponse" + example: + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "queued" + input_type: "voice" + estimated_processing_seconds: 30 + created_at: "2024-07-15T14:25:00Z" + job_id: "550e8400-e29b-41d4-a716-446655440099" + poll_url: "/api/v1/input/550e8400-e29b-41d4-a716-446655440001" + "400": + description: Malformed request (missing audio file or invalid form data) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: Audio file exceeds 500MB limit + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "FILE_TOO_LARGE" + message: "Audio file exceeds maximum size of 500MB" + detail: + max_size_bytes: 524288000 + received_size_bytes: 600000000 + "415": + description: Unsupported audio format + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "UNSUPPORTED_FORMAT" + message: "Audio format not supported. Accepted formats: wav, mp3, m4a, ogg, webm" + detail: + accepted_formats: + - wav + - mp3 + - m4a + - ogg + - webm + "503": + description: Ollama/Whisper service unavailable + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "LLM_UNAVAILABLE" + message: "Ollama transcription service is not available" + retry_after_seconds: 30 + +text: + post: + operationId: submitTextInput + summary: Submit a text narrative for processing + description: | + Accepts a free-text incident narrative from a first responder. This is + synchronous the text is stored immediately and the input is marked as + "ready" for extraction. No transcription step needed. + tags: + - input + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/TextInputRequest" + example: + narrative: > + Responded to a wildfire at Bear Creek Trailhead around 1:45 PM on July 10th. + Lightning strike ignited timber litter in mixed conifer and chaparral. Fire + spread rapidly northeast driven by 15-25 mph SW winds. We deployed 18 engines, + 6 water tenders, and 3 helicopters. 247 personnel total. One firefighter + treated for heat exhaustion, returned to duty July 12. 1247 acres burned across + federal, state, and private land. 47 structures threatened, 3 damaged, none + destroyed. Fire contained July 14, controlled July 15. + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-10" + responses: + "201": + description: Text input accepted and ready for extraction + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/TextInputResponse" + example: + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "ready" + input_type: "text" + character_count: 612 + word_count: 98 + created_at: "2024-07-15T14:25:00Z" + "400": + description: Malformed JSON request body + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: Narrative exceeds 50,000 character limit + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "NARRATIVE_TOO_LONG" + message: "Narrative exceeds maximum length of 50,000 characters" + detail: + max_characters: 50000 + received_characters: 52000 + "422": + description: Validation error (empty or too-short narrative) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Narrative is too short to extract meaningful incident data" + validation_errors: + - field: "narrative" + issue: "Must contain at least 10 words" + value: "Fire happened" + +input_by_id: + get: + operationId: getInput + summary: Get input record by ID + description: | + Returns the full input record including the original text or transcript, + processing status for voice inputs, and metadata. For voice inputs, poll + this endpoint until status transitions from "queued"/"transcribing" to "ready". + tags: + - input + parameters: + - name: input_id + in: path + required: true + description: Unique identifier of the input record + schema: + type: string + format: uuid + responses: + "200": + description: Input record found + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/InputRecord" + examples: + voice_ready: + summary: Voice input with completed transcription + value: + input_id: "550e8400-e29b-41d4-a716-446655440001" + input_type: "voice" + status: "ready" + transcript: "Responded to a wildfire at Bear Creek..." + original_filename: "incident_memo.m4a" + audio_duration_seconds: 180 + character_count: 612 + word_count: 98 + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-10" + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:30Z" + voice_processing: + summary: Voice input still transcribing + value: + input_id: "550e8400-e29b-41d4-a716-446655440001" + input_type: "voice" + status: "transcribing" + transcript: null + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:10Z" + retry_after_seconds: 5 + text_ready: + summary: Text input ready for extraction + value: + input_id: "550e8400-e29b-41d4-a716-446655440002" + input_type: "text" + status: "ready" + transcript: "Responded to a wildfire at Bear Creek..." + character_count: 612 + word_count: 98 + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:00Z" + "404": + description: Input record not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INPUT_NOT_FOUND" + message: "Input with ID 550e8400-e29b-41d4-a716-446655440099 not found" diff --git a/contracts/path/jobs.yaml b/contracts/path/jobs.yaml new file mode 100644 index 0000000..2ac042f --- /dev/null +++ b/contracts/path/jobs.yaml @@ -0,0 +1,79 @@ +# Layer 8 Async Job Status (Cross-cutting) +# GET /api/v1/jobs/{job_id} + +job_by_id: + get: + operationId: getJobStatus + summary: Get async job status + description: | + Universal job status endpoint for any asynchronous operation in FireForm — + transcription, LLM extraction, form generation, and report generation. + Returns the current status, progress percentage, and a result URL when + the job completes. Poll this endpoint for long-running operations. + tags: + - jobs + parameters: + - name: job_id + in: path + required: true + description: Unique identifier of the async job + schema: + type: string + format: uuid + responses: + "200": + description: Job status + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/Job" + examples: + queued: + summary: Job waiting in queue + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "queued" + progress_percent: 0 + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:30:00Z" + processing: + summary: Job currently processing + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "processing" + progress_percent: 45 + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:30:30Z" + completed: + summary: Job completed successfully + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "completed" + progress_percent: 100 + result_url: "/api/v1/extract/550e8400-e29b-41d4-a716-446655440020" + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:31:05Z" + failed: + summary: Job failed with error + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "failed" + progress_percent: 23 + error: + error_code: "LLM_TIMEOUT" + message: "Ollama did not respond within 120 seconds" + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:32:00Z" + "404": + description: Job not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "JOB_NOT_FOUND" + message: "Job with ID 550e8400-e29b-41d4-a716-446655440099 not found" diff --git a/contracts/path/reporting.yaml b/contracts/path/reporting.yaml new file mode 100644 index 0000000..ad10172 --- /dev/null +++ b/contracts/path/reporting.yaml @@ -0,0 +1,191 @@ +# Layer 5 Reporting & Analytics Endpoints +# GET /api/v1/reports/summary +# POST /api/v1/reports/generate +# GET /api/v1/reports/{report_id} + +summary: + get: + operationId: getReportSummary + summary: Get aggregate incident statistics + description: | + Returns aggregate statistics for a dashboard view total incidents, + breakdown by type and status, forms generated, average completeness + scores, and average processing times. Supports filtering by date range + and grouping by time period or incident type. + tags: + - reporting + parameters: + - name: date_from + in: query + description: Start date (inclusive, ISO 8601) + schema: + type: string + format: date + - name: date_to + in: query + description: End date (inclusive, ISO 8601) + schema: + type: string + format: date + - name: group_by + in: query + description: Group results by time period or incident type + schema: + type: string + enum: + - day + - week + - month + - incident_type + default: month + responses: + "200": + description: Aggregate statistics + content: + application/json: + schema: + $ref: "../schemas/reporting.yaml#/ReportSummary" + example: + date_from: "2024-01-01" + date_to: "2024-07-15" + total_incidents: 157 + by_type: + fire: 42 + ems: 68 + rescue: 12 + hazardous_conditions: 8 + service_call: 15 + good_intent: 7 + false_alarm: 5 + by_status: + draft: 3 + under_review: 2 + approved: 12 + submitted: 140 + forms_generated: 489 + avg_completeness_score: 88.3 + avg_processing_time_seconds: 47.2 + groups: + - period: "2024-07" + incident_count: 23 + forms_generated: 71 + +generate: + post: + operationId: generateReport + summary: Generate a periodic report + description: | + Generates a periodic (monthly, quarterly, or annual) aggregate report + as PDF or JSON. This includes statistics, incident summaries, and + compliance metrics required by agencies like USFA/NERIS. This is an + async operation returns a report_id for polling. + + Note: USFA encourages monthly submission but requires quarterly at minimum. + This endpoint supports generating reports aligned with those cadences. + tags: + - reporting + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/reporting.yaml#/GenerateReportRequest" + example: + period_type: "monthly" + year: 2024 + month: 7 + format: "pdf" + responses: + "202": + description: Report generation job accepted + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/AsyncJobResponse" + example: + job_id: "550e8400-e29b-41d4-a716-446655440096" + job_type: "report_generation" + status: "processing" + estimated_seconds: 30 + poll_url: "/api/v1/reports/550e8400-e29b-41d4-a716-446655440060" + report_id: "550e8400-e29b-41d4-a716-446655440060" + "422": + description: Invalid period or future date + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + future_period: + summary: Future period requested + value: + error_code: "FUTURE_PERIOD" + message: "Cannot generate a report for a future period" + invalid_quarter: + summary: Invalid quarter value + value: + error_code: "VALIDATION_ERROR" + message: "Invalid quarter value" + validation_errors: + - field: "quarter" + issue: "Must be 1, 2, 3, or 4" + value: 5 + +report_by_id: + get: + operationId: getReport + summary: Retrieve a generated report + description: | + Returns a previously generated periodic report. If the report is still + being generated, returns 202 Accepted with a retry hint. When complete, + returns the report in the originally requested format (PDF binary or JSON). + tags: + - reporting + parameters: + - name: report_id + in: path + required: true + description: Unique identifier of the generated report + schema: + type: string + format: uuid + responses: + "200": + description: Generated report (PDF or JSON depending on original request) + content: + application/pdf: + schema: + type: string + format: binary + application/json: + schema: + $ref: "../schemas/reporting.yaml#/PeriodicReport" + headers: + Content-Disposition: + description: Attachment filename for PDF downloads + schema: + type: string + example: 'attachment; filename="FireForm_Monthly_2024-07.pdf"' + "202": + description: Report still being generated + content: + application/json: + schema: + type: object + properties: + message: + type: string + status: + type: string + retry_after_seconds: + type: integer + example: + message: "Report generation is still in progress" + status: "processing" + retry_after_seconds: 10 + "404": + description: Report not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/system.yaml b/contracts/path/system.yaml new file mode 100644 index 0000000..cd00901 --- /dev/null +++ b/contracts/path/system.yaml @@ -0,0 +1,152 @@ +# Layer 7 System Endpoints +# GET /api/v1/health +# GET /api/v1/schema/incident +# GET /api/v1/schema/incident/versions + +health: + get: + operationId: getHealth + summary: System health check + description: | + Returns the health status of all FireForm components — database, Ollama LLM + (including loaded models, GPU status, and current load), Whisper transcription, + and file storage. Returns 200 even when degraded (so load balancers don't kill + it), 503 only if the system is truly unhealthy and cannot serve any requests. + tags: + - system + responses: + "200": + description: System is healthy or degraded + content: + application/json: + schema: + $ref: "../schemas/system.yaml#/HealthStatus" + examples: + healthy: + summary: All systems operational + value: + status: "healthy" + version: "1.0.0" + uptime_seconds: 86400 + components: + database: + status: "healthy" + response_time_ms: 2 + ollama: + status: "healthy" + response_time_ms: 15 + model_loaded: "llama3:8b" + ollama_version: "0.3.0" + models_available: + - name: "llama3:8b" + size_gb: 4.7 + quantization: "Q4_K_M" + loaded: true + - name: "mistral:7b" + size_gb: 4.1 + quantization: "Q4_0" + loaded: false + current_load: + active_requests: 1 + queued_requests: 0 + whisper: + status: "healthy" + response_time_ms: 10 + storage: + status: "healthy" + disk_free_gb: 120.5 + degraded: + summary: Some components have issues + value: + status: "degraded" + version: "1.0.0" + uptime_seconds: 3600 + components: + database: + status: "healthy" + response_time_ms: 2 + ollama: + status: "degraded" + response_time_ms: 5000 + detail: "High latency model may be loading" + current_load: + active_requests: 3 + queued_requests: 2 + whisper: + status: "unhealthy" + detail: "Whisper model not loaded" + storage: + status: "healthy" + disk_free_gb: 120.5 + "503": + description: System is unhealthy cannot serve requests + content: + application/json: + schema: + $ref: "../schemas/system.yaml#/HealthStatus" + example: + status: "unhealthy" + version: "1.0.0" + uptime_seconds: 10 + components: + database: + status: "unhealthy" + detail: "Connection refused" + ollama: + status: "unhealthy" + detail: "Connection refused" + +schema_incident: + get: + operationId: getIncidentSchema + summary: Get the canonical FireForm JSON Schema + description: | + Returns the full canonical FireForm incident JSON Schema (JSON Schema + draft-07). Clients can use this to validate incident data locally before + submitting. This is a schema-as-API pattern for interoperability. + tags: + - system + responses: + "200": + description: Full JSON Schema document + content: + application/json: + schema: + type: object + description: JSON Schema draft-07 document for the canonical incident model + example: + $schema: "http://json-schema.org/draft-07/schema#" + title: "FireForm Canonical Incident" + type: "object" + properties: + schema_version: + type: "string" + +schema_versions: + get: + operationId: getSchemaVersions + summary: Get schema version history + description: | + Returns the version history of the canonical FireForm incident schema, + including changelogs and whether each version introduced breaking changes. + Useful for clients tracking schema evolution across FireForm updates. + tags: + - system + responses: + "200": + description: Schema version history + content: + application/json: + schema: + type: array + items: + $ref: "../schemas/system.yaml#/SchemaVersion" + example: + - version: "1.1.0" + released_at: "2026-02-01T00:00:00Z" + changelog: "Added NERIS fields replacing legacy NFIRS. Added wildland.aerial_operations. Updated incident.types to include neris_code." + breaking_changes: true + - version: "1.0.0" + released_at: "2024-01-01T00:00:00Z" + changelog: "Initial canonical schema with NFIRS support." + breaking_changes: false diff --git a/contracts/path/templates.yaml b/contracts/path/templates.yaml new file mode 100644 index 0000000..dab9dca --- /dev/null +++ b/contracts/path/templates.yaml @@ -0,0 +1,267 @@ +# Layer 6 Template & Configuration Endpoints +# GET /api/v1/templates +# GET /api/v1/templates/{template_id} +# POST /api/v1/templates +# PUT /api/v1/templates/{template_id} +# GET /api/v1/templates/{template_id}/fields + +templates: + get: + operationId: listTemplates + summary: List all available form templates + description: | + Returns all registered form templates including built-in standard templates + (NERIS, NEMSIS, NIBRS, NFIRS modules, OSHA, etc.) and any custom templates + added for specific jurisdictions. Each template defines the fields, validation + rules, and mapping from the canonical FireForm schema. + tags: + - templates + responses: + "200": + description: List of all templates + content: + application/json: + schema: + type: array + items: + $ref: "../schemas/template.yaml#/TemplateSummary" + example: + - template_id: "550e8400-e29b-41d4-a716-446655440070" + form_type: "neris" + display_name: "NERIS Incident Report" + jurisdiction: "US-Federal" + agency_type: "fire_department" + version: "2.0" + last_updated: "2026-02-01" + field_count: 85 + status: "active" + - template_id: "550e8400-e29b-41d4-a716-446655440071" + form_type: "nemsis_epcr" + display_name: "NEMSIS Electronic Patient Care Report" + jurisdiction: "US-Federal" + agency_type: "ems" + version: "3.5" + last_updated: "2024-01-15" + field_count: 142 + status: "active" + - template_id: "550e8400-e29b-41d4-a716-446655440072" + form_type: "nfirs_basic" + display_name: "NFIRS Basic Module (Legacy)" + jurisdiction: "US-Federal" + agency_type: "fire_department" + version: "5.0" + last_updated: "2015-01-01" + field_count: 65 + status: "legacy" + + post: + operationId: createTemplate + summary: Register a new custom form template + description: | + Registers a new form template for a jurisdiction or agency not yet supported. + This is how FireForm extends to new states, countries, or custom agency forms + without code changes. The template defines all fields, their types, validation + rules, and how each maps from the canonical FireForm schema. + tags: + - templates + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/CreateTemplateRequest" + example: + form_type: "state_texas" + display_name: "Texas State Fire Marshal Incident Report" + jurisdiction: "US-TX" + agency_type: "fire_department" + fields: + - field_name: "incident_number" + field_type: "string" + required: true + max_length: 20 + description: "State-assigned incident number" + canonical_mapping: "report_metadata.incident_number" + - field_name: "fire_cause" + field_type: "enum" + required: true + allowed_values: + - "accidental" + - "natural" + - "intentional" + - "undetermined" + canonical_mapping: "fire.cause_category" + field_mappings_from_canonical: + "report_metadata.incident_number": "incident_number" + "fire.cause_category": "fire_cause" + responses: + "201": + description: Template created + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "409": + description: Template with this form_type already exists + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Invalid template definition + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +template_by_id: + get: + operationId: getTemplate + summary: Get full template schema + description: | + Returns the complete template definition including all fields, their types, + required/optional status, validation rules, and the mapping from canonical + FireForm schema fields. Includes the source standard reference where applicable. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template + schema: + type: string + format: uuid + responses: + "200": + description: Full template definition + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + + put: + operationId: updateTemplate + summary: Update an existing template + description: | + Replaces an existing template definition. Used when an upstream standard + changes (e.g., NERIS schema update). Templates used by submitted incidents + cannot be modified create a new version instead. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template to update + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/CreateTemplateRequest" + responses: + "200": + description: Template updated + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Template in use by submitted incidents cannot modify + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "TEMPLATE_IN_USE" + message: "Template is referenced by submitted incidents. Create a new version instead." + detail: + submitted_incident_count: 15 + recommendation: "POST /api/v1/templates to create a new version" + +template_fields: + get: + operationId: getTemplateFields + summary: Get template field definitions + description: | + Returns just the fields list for a template with type, required/optional + status, validation rules, and canonical schema mapping. Useful for building + validation checklists and for the validate endpoint to determine requirements. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template + schema: + type: string + format: uuid + - name: required_only + in: query + description: If true, return only required fields + schema: + type: boolean + default: false + responses: + "200": + description: List of template fields + content: + application/json: + schema: + type: object + properties: + template_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + total_fields: + type: integer + required_fields: + type: integer + optional_fields: + type: integer + fields: + type: array + items: + $ref: "../schemas/template.yaml#/TemplateField" + example: + template_id: "550e8400-e29b-41d4-a716-446655440070" + form_type: "neris" + total_fields: 85 + required_fields: 32 + optional_fields: 53 + fields: + - field_name: "incident_type" + field_type: "enum" + required: true + description: "Primary incident type code" + canonical_mapping: "incident.types[0].neris_code" + - field_name: "incident_date" + field_type: "date" + required: true + description: "Date of incident" + canonical_mapping: "incident.start_datetime" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/schemas/canonical-incident.yaml b/contracts/schemas/canonical-incident.yaml new file mode 100644 index 0000000..a11d1b9 --- /dev/null +++ b/contracts/schemas/canonical-incident.yaml @@ -0,0 +1,930 @@ +# Canonical FireForm Incident Schema +# This is the master superset schema single source of truth for all downstream forms + +CanonicalIncident: + type: object + description: | + The canonical FireForm incident data model. This is the superset schema containing + every field any downstream form could need. Form-specific mappers select only the + relevant fields for each agency template. + properties: + schema_version: + type: string + description: Schema version identifier + example: "1.1.0" + schema_name: + type: string + description: Schema name identifier + enum: + - fireform_canonical_incident + + extraction_metadata: + $ref: "#/ExtractionMetadata" + report_metadata: + $ref: "#/ReportMetadata" + incident: + $ref: "#/Incident" + location: + $ref: "#/Location" + fire: + $ref: "#/Fire" + wildland: + $ref: "#/Wildland" + structure: + $ref: "#/Structure" + casualties: + $ref: "#/Casualties" + ems: + $ref: "#/EMS" + hazmat: + $ref: "#/Hazmat" + arson: + $ref: "#/Arson" + responding_agencies: + $ref: "#/RespondingAgencies" + resources_deployed: + $ref: "#/ResourcesDeployed" + weather: + $ref: "#/Weather" + environmental_impact: + $ref: "#/EnvironmentalImpact" + infrastructure_impact: + $ref: "#/InfrastructureImpact" + near_miss_and_safety: + $ref: "#/NearMissAndSafety" + lessons_learned: + $ref: "#/LessonsLearned" + follow_up: + $ref: "#/FollowUp" + periodic_reporting: + $ref: "#/PeriodicReporting" + attachments: + $ref: "#/Attachments" + +# --- Sub-schemas --- + +ExtractionMetadata: + type: object + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + input_type: + type: string + enum: [voice, text] + extracted_at: + type: string + format: date-time + llm_model: + type: string + example: "llama3:8b" + confidence_score: + type: number + minimum: 0 + maximum: 1 + description: Overall confidence score from the LLM extraction (0.0–1.0) + completeness: + $ref: "#/Completeness" + applicable_forms: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + +Completeness: + type: object + description: | + Tells the system which forms can be fully auto-generated vs which need + manual review. Recalculated server-side after every PATCH /extract. + properties: + overall_percent: + type: integer + minimum: 0 + maximum: 100 + missing_fields: + type: array + items: + type: string + description: JSON paths of fields that have no value + low_confidence_fields: + type: array + items: + type: string + description: JSON paths of fields where LLM confidence is low + inferred_fields: + type: array + items: + type: string + description: JSON paths of fields that were inferred (not explicitly stated) + forms_fully_generatable: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_needing_review: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_missing_data: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + +ReportMetadata: + type: object + properties: + report_id: + type: string + example: "FF-2024-CA-0157" + incident_number: + type: string + example: "CA-SQF-2024-0421" + report_date: + type: string + format: date + report_time: + type: string + format: time + report_status: + $ref: "../schemas/enums.yaml#/ReportStatus" + reporting_unit: + $ref: "#/ReportingUnit" + prepared_by: + type: array + items: + $ref: "#/Personnel" + reviewed_by: + type: array + items: + $ref: "#/Reviewer" + submission_log: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + submitted_at: + type: string + format: date-time + submitted_to: + type: string + +ReportingUnit: + type: object + properties: + station_name: + type: string + station_id: + type: string + agency_name: + type: string + agency_id: + type: string + format: uuid + agency_type: + type: string + +Personnel: + type: object + properties: + name: + type: string + badge_number: + type: string + rank: + type: string + role: + type: string + contact_number: + type: string + signature_captured: + type: boolean + +Reviewer: + type: object + properties: + name: + type: string + badge_number: + type: string + rank: + type: string + role: + type: string + reviewed_at: + type: string + format: date-time + approved: + type: boolean + +Incident: + type: object + properties: + name: + type: string + description: Human-readable incident name + types: + type: array + items: + $ref: "#/IncidentType" + start_datetime: + type: string + format: date-time + alarm_datetime: + type: string + format: date-time + first_arrival_datetime: + type: string + format: date-time + containment_datetime: + type: string + format: date-time + nullable: true + controlled_datetime: + type: string + format: date-time + nullable: true + cleared_datetime: + type: string + format: date-time + nullable: true + total_duration_hours: + type: number + nullable: true + narrative: + type: string + description: Free text summary of the incident + raw_transcript: + type: string + description: Original voice or text input verbatim + +IncidentType: + type: object + properties: + primary: + type: boolean + category: + $ref: "../schemas/enums.yaml#/IncidentCategory" + subcategory: + type: string + neris_code: + type: string + description: NERIS incident type code (replaces NFIRS as of Feb 2026) + nfirs_code: + type: string + description: Legacy NFIRS incident type code + +Location: + type: object + properties: + address: + type: string + nullable: true + nearest_landmark: + type: string + nullable: true + nearest_town: + type: string + nullable: true + county: + type: string + nullable: true + state: + type: string + nullable: true + country: + type: string + nullable: true + postal_code: + type: string + nullable: true + coordinates: + $ref: "#/Coordinates" + ignition_point_coordinates: + $ref: "#/CoordinatesBasic" + elevation_range_ft: + type: string + nullable: true + legal_description: + type: string + nullable: true + jurisdiction: + type: object + properties: + federal: + type: boolean + state: + type: boolean + private: + type: boolean + tribal: + type: boolean + property_type: + type: string + nullable: true + property_use: + type: string + nullable: true + dispatch_center: + type: string + nullable: true + +Coordinates: + type: object + properties: + latitude: + type: number + longitude: + type: number + accuracy_meters: + type: number + +CoordinatesBasic: + type: object + properties: + latitude: + type: number + longitude: + type: number + +Fire: + type: object + properties: + cause_category: + type: string + nullable: true + cause_specific: + type: string + nullable: true + cause_certainty: + $ref: "../schemas/enums.yaml#/CauseCertainty" + arson_suspected: + type: boolean + material_first_ignited: + type: string + nullable: true + fuel_types: + type: array + items: + type: string + fire_spread_directions: + type: array + items: + type: string + rate_of_spread: + $ref: "../schemas/enums.yaml#/RateOfSpread" + flame_lengths_ft: + type: string + nullable: true + spotting_distance_miles: + type: number + nullable: true + unusual_behaviors: + type: array + items: + type: string + detector_present: + type: boolean + nullable: true + detector_operated: + type: boolean + nullable: true + suppression_system_present: + type: boolean + nullable: true + suppression_system_operated: + type: boolean + nullable: true + estimated_damage_usd: + type: number + nullable: true + contents_loss_usd: + type: number + nullable: true + +Wildland: + type: object + properties: + is_wildland_incident: + type: boolean + total_acres_burned: + type: number + nullable: true + land_ownership_breakdown: + type: object + properties: + federal_acres: + type: number + state_acres: + type: number + private_acres: + type: number + tribal_acres: + type: number + percent_contained: + type: integer + minimum: 0 + maximum: 100 + fire_lines: + type: object + properties: + primary_line_miles: + type: number + secondary_line_miles: + type: number + dozer_line_miles: + type: number + hand_line_miles: + type: number + aerial_operations: + type: object + properties: + water_drops_gallons: + type: integer + retardant_drops_gallons: + type: integer + total_flight_hours: + type: number + containment_strategies: + type: array + items: + type: string + +Structure: + type: object + properties: + is_structure_involved: + type: boolean + structures_threatened: + type: integer + nullable: true + structures_damaged: + type: integer + nullable: true + structures_destroyed: + type: integer + nullable: true + structures_protected: + type: integer + nullable: true + construction_type: + type: string + nullable: true + stories: + type: integer + nullable: true + area_sqft: + type: number + nullable: true + occupancy_at_time: + type: integer + nullable: true + +Casualties: + type: object + properties: + civilian: + type: array + items: + $ref: "#/CivilianCasualty" + responder: + type: array + items: + $ref: "#/ResponderCasualty" + total_civilian_injuries: + type: integer + total_civilian_fatalities: + type: integer + total_responder_injuries: + type: integer + total_responder_fatalities: + type: integer + +CivilianCasualty: + type: object + properties: + age: + type: integer + nullable: true + sex: + type: string + nullable: true + injury_type: + type: string + severity: + $ref: "../schemas/enums.yaml#/InjurySeverity" + cause: + type: string + nullable: true + location_at_time: + type: string + nullable: true + transported: + type: boolean + hospital: + type: string + nullable: true + +ResponderCasualty: + type: object + properties: + personnel_id: + type: string + agency: + type: string + role: + type: string + injury_type: + type: string + severity: + $ref: "../schemas/enums.yaml#/InjurySeverity" + treatment: + type: string + nullable: true + transported: + type: boolean + hospital: + type: string + nullable: true + return_to_duty_date: + type: string + format: date + nullable: true + osha_recordable: + type: boolean + nfirs_5_required: + type: boolean + +EMS: + type: object + properties: + ems_response_required: + type: boolean + patients: + type: array + items: + $ref: "#/EMSPatient" + total_patients: + type: integer + ems_agency_responded: + type: string + nullable: true + nemsis_report_required: + type: boolean + nemsis_report_ids: + type: array + items: + type: string + +EMSPatient: + type: object + properties: + patient_ref_id: + type: string + age_approx: + type: integer + nullable: true + sex: + type: string + nullable: true + chief_complaint: + type: string + nullable: true + disposition: + type: string + nullable: true + transported: + type: boolean + date_of_birth: + type: string + format: date + nullable: true + nemsis_data_captured: + type: boolean + +Hazmat: + type: object + properties: + involved: + type: boolean + materials: + type: array + items: + type: object + properties: + name: + type: string + un_number: + type: string + nullable: true + quantity: + type: string + nullable: true + epa_reportable_quantity_exceeded: + type: boolean + spill_size_gallons: + type: number + nullable: true + +Arson: + type: object + properties: + suspected: + type: boolean + confirmed: + type: boolean + law_enforcement_notified: + type: boolean + investigation_required: + type: boolean + investigation_agency: + type: string + nullable: true + evidence_collected: + type: boolean + nibrs_report_required: + type: boolean + notes: + type: string + nullable: true + +RespondingAgencies: + type: object + properties: + primary_agency: + type: string + all_agencies: + type: array + items: + $ref: "#/RespondingAgency" + mutual_aid_activated: + type: boolean + mutual_aid_agencies: + type: array + items: + type: string + unified_command: + type: boolean + +RespondingAgency: + type: object + properties: + agency_name: + type: string + agency_type: + type: string + role: + type: string + personnel_count: + type: integer + +ResourcesDeployed: + type: object + properties: + total_personnel: + type: integer + personnel_breakdown: + type: object + properties: + firefighters: + type: integer + crew_supervisors: + type: integer + engineers: + type: integer + incident_command: + type: integer + support_staff: + type: integer + apparatus: + type: array + items: + $ref: "#/Apparatus" + crew_types: + type: array + items: + type: string + +Apparatus: + type: object + properties: + type: + type: string + count: + type: integer + +Weather: + type: object + properties: + on_arrival: + $ref: "#/WeatherReading" + worst_conditions: + $ref: "#/WeatherReadingExtended" + factors_influencing_fire: + type: array + items: + type: string + +WeatherReading: + type: object + properties: + datetime: + type: string + format: date-time + temperature_f: + type: number + relative_humidity_percent: + type: number + wind_speed_mph: + type: number + wind_direction: + type: string + haines_index: + type: integer + nullable: true + +WeatherReadingExtended: + type: object + properties: + datetime: + type: string + format: date-time + temperature_f: + type: number + relative_humidity_percent: + type: number + wind_speed_mph: + type: number + wind_gusts_mph: + type: number + nullable: true + +EnvironmentalImpact: + type: object + properties: + wildlife_habitat_affected_acres: + type: number + nullable: true + watershed_impact: + type: string + nullable: true + soil_erosion_risk: + type: string + nullable: true + sensitive_species_affected: + type: array + items: + type: string + air_quality_impact: + type: string + nullable: true + water_body_affected: + type: boolean + nullable: true + +InfrastructureImpact: + type: object + properties: + items: + type: array + items: + $ref: "#/InfrastructureItem" + +InfrastructureItem: + type: object + properties: + type: + type: string + unit: + type: string + quantity: + type: number + severity: + type: string + +NearMissAndSafety: + type: object + properties: + near_miss_events: + type: array + items: + type: object + properties: + description: + type: string + date: + type: string + format: date + contributing_factors: + type: array + items: + type: string + lessons_learned: + type: string + nullable: true + corrective_action: + type: string + nullable: true + safety_breaches: + type: integer + weather_related_risks: + type: array + items: + type: string + +LessonsLearned: + type: object + properties: + successful_tactics: + type: array + items: + type: string + areas_for_improvement: + type: array + items: + type: string + recommendations: + type: array + items: + type: string + +FollowUp: + type: object + properties: + mop_up: + type: object + properties: + percent_complete: + type: integer + estimated_completion_date: + type: string + format: date + personnel_assigned: + type: integer + rehabilitation: + type: object + properties: + erosion_control_acres: + type: number + reseeding_acres: + type: number + hazard_tree_removal_required: + type: boolean + next_inspection_date: + type: string + format: date + nullable: true + investigation_ongoing: + type: boolean + +PeriodicReporting: + type: object + properties: + contributes_to_monthly_report: + type: boolean + contributes_to_quarterly_report: + type: boolean + contributes_to_annual_report: + type: boolean + neris_submitted: + type: boolean + neris_submitted_at: + type: string + format: date-time + nullable: true + state_submitted: + type: boolean + state_submitted_at: + type: string + format: date-time + nullable: true + +Attachments: + type: object + properties: + maps: + type: boolean + photos_count: + type: integer + weather_charts: + type: boolean + resource_tracking_logs: + type: boolean + incident_action_plans: + type: boolean + attachment_refs: + type: array + items: + type: object + properties: + ref_id: + type: string + format: uuid + filename: + type: string + content_type: + type: string + size_bytes: + type: integer diff --git a/contracts/schemas/common.yaml b/contracts/schemas/common.yaml new file mode 100644 index 0000000..0798f85 --- /dev/null +++ b/contracts/schemas/common.yaml @@ -0,0 +1,129 @@ +# Common schemas used across multiple endpoints + +ErrorResponse: + type: object + required: + - error_code + - message + properties: + error_code: + type: string + description: Machine-readable error code + example: "EXTRACT_NOT_FOUND" + message: + type: string + description: Human-readable error message + example: "Extraction with ID xyz not found" + detail: + type: object + description: Additional context specific to the error type + additionalProperties: true + retry_after_seconds: + type: integer + description: Present on 503 errors seconds to wait before retrying + validation_errors: + type: array + description: Present on 422 errors list of field-level validation issues + items: + $ref: "#/ValidationError" + +ValidationError: + type: object + properties: + field: + type: string + description: JSON path of the invalid field + example: "fire.cause_certainty" + issue: + type: string + description: Description of the validation issue + example: "Must be one of: confirmed, probable, suspected, undetermined" + value: + description: The invalid value that was provided + +Pagination: + type: object + properties: + total: + type: integer + description: Total number of items across all pages + page: + type: integer + description: Current page number (1-based) + per_page: + type: integer + description: Items per page + total_pages: + type: integer + description: Total number of pages + has_next: + type: boolean + description: Whether there is a next page + has_prev: + type: boolean + description: Whether there is a previous page + +AsyncJobResponse: + type: object + required: + - job_id + - status + properties: + job_id: + type: string + format: uuid + description: Unique job identifier for polling + job_type: + type: string + description: Type of async operation + enum: + - transcription + - extraction + - form_generation + - batch_form_generation + - report_generation + status: + $ref: "enums.yaml#/JobStatus" + estimated_seconds: + type: integer + description: Estimated time to completion in seconds + poll_url: + type: string + description: Full URL to poll for job status + +Job: + type: object + required: + - job_id + - job_type + - status + properties: + job_id: + type: string + format: uuid + job_type: + type: string + enum: + - transcription + - extraction + - form_generation + - batch_form_generation + - report_generation + status: + $ref: "enums.yaml#/JobStatus" + progress_percent: + type: integer + minimum: 0 + maximum: 100 + description: Progress percentage (0–100) + result_url: + type: string + description: URL of the completed result (present when status is "completed") + error: + $ref: "#/ErrorResponse" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time diff --git a/contracts/schemas/enums.yaml b/contracts/schemas/enums.yaml new file mode 100644 index 0000000..b79beda --- /dev/null +++ b/contracts/schemas/enums.yaml @@ -0,0 +1,130 @@ +# Shared enums used across the FireForm API + +InputStatus: + type: string + enum: + - queued + - transcribing + - ready + - failed + description: Status of an input record + +ExtractionStatus: + type: string + enum: + - processing + - completed + - failed + - needs_review + description: Status of an AI extraction job + +FormStatus: + type: string + enum: + - queued + - generating + - completed + - failed + description: Status of a form generation job + +ReportStatus: + type: string + enum: + - draft + - under_review + - approved + - submitted + description: Status of an incident report in the approval workflow + +JobStatus: + type: string + enum: + - queued + - processing + - completed + - failed + description: Status of any async job + +FormType: + type: string + enum: + - neris + - nemsis_epcr + - nibrs + - nfirs_basic + - nfirs_fire + - nfirs_structure + - nfirs_wildland + - nfirs_ems + - nfirs_hazmat + - nfirs_apparatus + - nfirs_personnel + - nfirs_arson + - nfirs_casualty_civilian + - nfirs_casualty_responder + - cal_fire_ics209 + - osha_301 + - un_ssirs + - state_georgia + - state_california + - state_new_york + description: | + Stable string identifier for a form type. Includes the new NERIS standard + (replacing NFIRS as of Feb 2026), legacy NFIRS modules, NEMSIS, NIBRS, + OSHA, state-specific, and international (UN SSIRS) forms. + +IncidentCategory: + type: string + enum: + - fire + - ems + - rescue + - hazardous_conditions + - service_call + - good_intent + - false_alarm + - law_enforcement + description: High-level incident category + +CauseCertainty: + type: string + enum: + - confirmed + - probable + - suspected + - undetermined + description: Certainty level of fire cause determination + +InjurySeverity: + type: string + enum: + - minor + - moderate + - severe + - fatal + description: Severity classification for injuries + +RateOfSpread: + type: string + enum: + - slow + - moderate + - rapid + - extreme + description: Rate of fire spread classification + +PeriodType: + type: string + enum: + - monthly + - quarterly + - annual + description: Reporting period type + +OutputFormat: + type: string + enum: + - pdf + - json + - both + description: Output format for generated forms diff --git a/contracts/schemas/extraction-record.yaml b/contracts/schemas/extraction-record.yaml new file mode 100644 index 0000000..aef7442 --- /dev/null +++ b/contracts/schemas/extraction-record.yaml @@ -0,0 +1,134 @@ +# Extraction-related schemas + +ExtractionRequest: + type: object + properties: + model_override: + type: string + description: Override the default LLM model (e.g. "llama3:70b") + extraction_hints: + type: object + description: Optional hints to improve extraction accuracy + properties: + incident_type: + type: string + description: Hint about the incident type (e.g. "wildland_fire", "structure_fire") + state: + type: string + description: US state code to apply state-specific extraction rules + agency_type: + type: string + description: Agency type hint for form selection + additionalProperties: true + +ExtractionCompleted: + type: object + required: + - extract_id + - input_id + - status + - canonical_incident + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + status: + type: string + enum: + - completed + completed_at: + type: string + format: date-time + model_used: + type: string + description: LLM model that performed the extraction + processing_time_seconds: + type: number + canonical_incident: + $ref: "canonical-incident.yaml#/CanonicalIncident" + corrections: + type: array + description: Audit trail of manual corrections applied via PATCH + items: + type: object + properties: + field_path: + type: string + original_value: {} + corrected_value: {} + corrected_at: + type: string + format: date-time + corrected_by: + type: string + +ExtractionProcessing: + type: object + required: + - extract_id + - input_id + - status + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + status: + type: string + enum: + - processing + - failed + started_at: + type: string + format: date-time + retry_after_seconds: + type: integer + description: Polling hint for clients + error_type: + type: string + nullable: true + description: Present when status is "failed" + error_detail: + type: string + nullable: true + partial_result: + $ref: "canonical-incident.yaml#/CanonicalIncident" + +ValidationResult: + type: object + required: + - valid + - form_type + - extract_id + properties: + valid: + type: boolean + description: Whether all required fields for this form type are present + form_type: + $ref: "enums.yaml#/FormType" + extract_id: + type: string + format: uuid + missing_required: + type: array + items: + type: string + description: JSON paths of required fields that are missing + missing_recommended: + type: array + items: + type: string + description: JSON paths of recommended fields that are missing + warnings: + type: array + items: + type: string + description: Human-readable warnings about data quality + field_coverage_percent: + type: number + description: Percentage of form fields that have values diff --git a/contracts/schemas/form-record.yaml b/contracts/schemas/form-record.yaml new file mode 100644 index 0000000..a83ec41 --- /dev/null +++ b/contracts/schemas/form-record.yaml @@ -0,0 +1,198 @@ +# Form generation related schemas + +GenerateAllRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + options: + type: object + properties: + skip_incomplete: + type: boolean + default: true + description: Skip forms that fail validation + force_partial: + type: boolean + default: false + description: Generate forms even with missing fields (leaving blanks) + +GenerateSingleRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + options: + type: object + properties: + output_format: + $ref: "../schemas/enums.yaml#/OutputFormat" + force_partial: + type: boolean + default: false + force: + type: boolean + default: false + description: Allow generation even if form_type is not in applicable_forms + +BatchGenerateResponse: + type: object + required: + - batch_id + - status + - extract_id + properties: + batch_id: + type: string + format: uuid + status: + type: string + enum: [processing] + extract_id: + type: string + format: uuid + forms_queued: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_skipped: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + reason: + type: string + estimated_seconds: + type: integer + poll_url: + type: string + +FormGenerateResponse: + type: object + required: + - form_id + - form_type + - status + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + type: string + enum: [processing, completed] + extract_id: + type: string + format: uuid + job_id: + type: string + format: uuid + estimated_seconds: + type: integer + poll_url: + type: string + +FormRecord: + type: object + required: + - form_id + - form_type + - status + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" + extract_id: + type: string + format: uuid + incident_id: + type: string + format: uuid + nullable: true + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + nullable: true + pdf_ready: + type: boolean + json_ready: + type: boolean + field_mapping_summary: + type: object + properties: + total_form_fields: + type: integer + fields_filled: + type: integer + fields_blank: + type: integer + coverage_percent: + type: number + +FormMappedJson: + type: object + required: + - form_type + - form_id + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + form_version: + type: string + form_id: + type: string + format: uuid + extract_id: + type: string + format: uuid + agency_fields: + type: object + additionalProperties: true + description: Agency-specific field names and values as the target system expects + +BatchStatus: + type: object + required: + - batch_id + - status + properties: + batch_id: + type: string + format: uuid + status: + type: string + enum: [processing, completed, failed] + total: + type: integer + completed: + type: integer + failed: + type: integer + forms: + type: array + items: + type: object + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" diff --git a/contracts/schemas/incident-record.yaml b/contracts/schemas/incident-record.yaml new file mode 100644 index 0000000..932425c --- /dev/null +++ b/contracts/schemas/incident-record.yaml @@ -0,0 +1,150 @@ +# Incident management schemas + +CreateIncidentRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + incident_number: + type: string + description: Optional org-assigned incident number + tags: + type: array + items: + type: string + +UpdateIncidentRequest: + type: object + properties: + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + tags: + type: array + items: + type: string + incident_number: + type: string + notes: + type: string + +IncidentRecord: + type: object + required: + - incident_id + - extract_id + - status + properties: + incident_id: + type: string + format: uuid + extract_id: + type: string + format: uuid + incident_number: + type: string + nullable: true + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + incident_name: + type: string + nullable: true + incident_type: + type: string + nullable: true + incident_date: + type: string + format: date + nullable: true + forms_generated: + type: array + items: + type: object + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" + tags: + type: array + items: + type: string + notes: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + nullable: true + +IncidentRecordFull: + description: Full incident record with linked extraction and forms + allOf: + - $ref: "#/IncidentRecord" + - type: object + properties: + canonical_incident: + $ref: "canonical-incident.yaml#/CanonicalIncident" + forms: + type: array + items: + $ref: "form-record.yaml#/FormRecord" + submission_log: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + submitted_at: + type: string + format: date-time + submitted_to: + type: string + status: + type: string + +IncidentListResponse: + type: object + properties: + data: + type: array + items: + type: object + properties: + incident_id: + type: string + format: uuid + incident_number: + type: string + nullable: true + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + incident_name: + type: string + nullable: true + incident_type: + type: string + nullable: true + incident_date: + type: string + format: date + nullable: true + forms_count: + type: integer + created_at: + type: string + format: date-time + pagination: + $ref: "common.yaml#/Pagination" diff --git a/contracts/schemas/input-record.yaml b/contracts/schemas/input-record.yaml new file mode 100644 index 0000000..65dd205 --- /dev/null +++ b/contracts/schemas/input-record.yaml @@ -0,0 +1,138 @@ +# Input-related schemas + +TextInputRequest: + type: object + required: + - narrative + properties: + narrative: + type: string + description: Free-text incident narrative (10+ words, max 50,000 characters) + minLength: 20 + maxLength: 50000 + station_id: + type: string + description: Station identifier + responder_badge: + type: string + description: Badge number of the reporting responder + incident_date_hint: + type: string + format: date + description: Approximate date of the incident + +VoiceInputResponse: + type: object + required: + - input_id + - status + - input_type + properties: + input_id: + type: string + format: uuid + status: + type: string + enum: + - queued + input_type: + type: string + enum: + - voice + estimated_processing_seconds: + type: integer + created_at: + type: string + format: date-time + job_id: + type: string + format: uuid + description: Job ID for polling transcription status + poll_url: + type: string + description: URL to poll for input status + +TextInputResponse: + type: object + required: + - input_id + - status + - input_type + properties: + input_id: + type: string + format: uuid + status: + type: string + enum: + - ready + input_type: + type: string + enum: + - text + character_count: + type: integer + word_count: + type: integer + created_at: + type: string + format: date-time + +InputRecord: + type: object + required: + - input_id + - input_type + - status + properties: + input_id: + type: string + format: uuid + input_type: + type: string + enum: + - voice + - text + status: + $ref: "enums.yaml#/InputStatus" + transcript: + type: string + nullable: true + description: Transcribed text (for voice) or original narrative (for text). Null while transcribing. + original_filename: + type: string + nullable: true + description: Original audio filename (voice inputs only) + audio_duration_seconds: + type: number + nullable: true + description: Duration of audio in seconds (voice inputs only) + character_count: + type: integer + nullable: true + word_count: + type: integer + nullable: true + station_id: + type: string + nullable: true + responder_badge: + type: string + nullable: true + incident_date_hint: + type: string + format: date + nullable: true + error_detail: + type: string + nullable: true + description: Error message if transcription failed + retry_after_seconds: + type: integer + description: Polling hint present when status is queued or transcribing + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time diff --git a/contracts/schemas/reporting.yaml b/contracts/schemas/reporting.yaml new file mode 100644 index 0000000..f352e70 --- /dev/null +++ b/contracts/schemas/reporting.yaml @@ -0,0 +1,112 @@ +# Reporting & Analytics schemas + +ReportSummary: + type: object + properties: + date_from: + type: string + format: date + date_to: + type: string + format: date + total_incidents: + type: integer + by_type: + type: object + additionalProperties: + type: integer + description: Incident counts grouped by category + by_status: + type: object + additionalProperties: + type: integer + description: Incident counts grouped by report status + forms_generated: + type: integer + avg_completeness_score: + type: number + avg_processing_time_seconds: + type: number + groups: + type: array + items: + type: object + properties: + period: + type: string + description: Period label (e.g. "2024-07" for monthly, "fire" for by type) + incident_count: + type: integer + forms_generated: + type: integer + +GenerateReportRequest: + type: object + required: + - period_type + - year + properties: + period_type: + $ref: "../schemas/enums.yaml#/PeriodType" + year: + type: integer + minimum: 2000 + maximum: 2100 + month: + type: integer + minimum: 1 + maximum: 12 + description: Required when period_type is "monthly" + quarter: + type: integer + minimum: 1 + maximum: 4 + description: Required when period_type is "quarterly" + format: + $ref: "../schemas/enums.yaml#/OutputFormat" + +PeriodicReport: + type: object + properties: + report_id: + type: string + format: uuid + period_type: + $ref: "../schemas/enums.yaml#/PeriodType" + period_label: + type: string + description: Human-readable period (e.g. "July 2024", "Q3 2024", "2024") + generated_at: + type: string + format: date-time + summary: + $ref: "#/ReportSummary" + incidents: + type: array + items: + type: object + properties: + incident_id: + type: string + format: uuid + incident_number: + type: string + incident_date: + type: string + format: date + incident_type: + type: string + status: + type: string + forms_generated: + type: integer + compliance: + type: object + properties: + neris_submission_rate: + type: number + description: Percentage of incidents with NERIS submitted + average_submission_delay_days: + type: number + overdue_incidents: + type: integer diff --git a/contracts/schemas/system.yaml b/contracts/schemas/system.yaml new file mode 100644 index 0000000..8c25693 --- /dev/null +++ b/contracts/schemas/system.yaml @@ -0,0 +1,101 @@ +# System & health check schemas + +HealthStatus: + type: object + required: + - status + - version + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + version: + type: string + description: FireForm API version + uptime_seconds: + type: integer + components: + type: object + properties: + database: + $ref: "#/ComponentHealth" + ollama: + $ref: "#/ComponentHealth" + whisper: + $ref: "#/ComponentHealth" + storage: + $ref: "#/ComponentHealth" + +ComponentHealth: + type: object + required: + - status + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + response_time_ms: + type: integer + nullable: true + detail: + type: string + nullable: true + model_loaded: + type: string + nullable: true + description: Currently loaded model (ollama component) + disk_free_gb: + type: number + nullable: true + description: Free disk space (storage component) + ollama_version: + type: string + nullable: true + description: Ollama server version (ollama component) + models_available: + type: array + nullable: true + description: All models pulled and available on the Ollama server (ollama component) + items: + type: object + properties: + name: + type: string + size_gb: + type: number + quantization: + type: string + nullable: true + loaded: + type: boolean + current_load: + type: object + nullable: true + description: Current processing load (ollama component) + properties: + active_requests: + type: integer + queued_requests: + type: integer + +SchemaVersion: + type: object + required: + - version + - released_at + properties: + version: + type: string + released_at: + type: string + format: date-time + changelog: + type: string + breaking_changes: + type: boolean diff --git a/contracts/schemas/template.yaml b/contracts/schemas/template.yaml new file mode 100644 index 0000000..45c71f7 --- /dev/null +++ b/contracts/schemas/template.yaml @@ -0,0 +1,144 @@ +# Template & Configuration schemas + +TemplateSummary: + type: object + properties: + template_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + display_name: + type: string + jurisdiction: + type: string + description: Jurisdiction code (e.g. "US-Federal", "US-CA", "US-GA") + agency_type: + type: string + version: + type: string + last_updated: + type: string + format: date + field_count: + type: integer + status: + type: string + enum: + - active + - legacy + - draft + +CreateTemplateRequest: + type: object + required: + - form_type + - display_name + - jurisdiction + - fields + - field_mappings_from_canonical + properties: + form_type: + type: string + description: Unique form type identifier + display_name: + type: string + jurisdiction: + type: string + agency_type: + type: string + fields: + type: array + items: + $ref: "#/TemplateField" + field_mappings_from_canonical: + type: object + additionalProperties: + type: string + description: | + Mapping from canonical FireForm JSON paths to this template's field names. + Key = canonical path (e.g. "fire.cause_category"), value = template field name. + source_standard: + type: string + nullable: true + description: Reference to the source standard (e.g. "NERIS v2.0", "NFIRS 5.0") + pdf_template_ref: + type: string + nullable: true + description: Reference to the PDF template file + +Template: + allOf: + - type: object + properties: + template_id: + type: string + format: uuid + version: + type: string + last_updated: + type: string + format: date + field_count: + type: integer + status: + type: string + enum: [active, legacy, draft] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + - $ref: "#/CreateTemplateRequest" + +TemplateField: + type: object + required: + - field_name + - field_type + - required + properties: + field_name: + type: string + description: Field identifier within this template + field_type: + type: string + enum: + - string + - integer + - number + - boolean + - date + - datetime + - time + - enum + - text + - array + description: Data type of the field + required: + type: boolean + description: + type: string + nullable: true + max_length: + type: integer + nullable: true + min_value: + type: number + nullable: true + max_value: + type: number + nullable: true + allowed_values: + type: array + items: + type: string + nullable: true + description: Valid values for enum fields + canonical_mapping: + type: string + description: JSON path in canonical FireForm schema this field maps from + default_value: + nullable: true + description: Default value if canonical field is null From 38253221f5b4795856b410624bb0f93478df71a6 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 9 Jun 2026 11:41:46 +0530 Subject: [PATCH 14/18] renamed incident-contract --- .../schemas/{canonical-incident.yaml => incident-contract.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/schemas/{canonical-incident.yaml => incident-contract.yaml} (100%) diff --git a/contracts/schemas/canonical-incident.yaml b/contracts/schemas/incident-contract.yaml similarity index 100% rename from contracts/schemas/canonical-incident.yaml rename to contracts/schemas/incident-contract.yaml From 7b354ea39ea790fefa39adbb6cce84de3d5c69f8 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 9 Jun 2026 11:54:45 +0530 Subject: [PATCH 15/18] updated incident-contracts in all references --- contracts/path/extraction.yaml | 6 +++--- contracts/schemas/extraction-record.yaml | 8 ++++---- contracts/schemas/incident-contract.yaml | 4 ++-- contracts/schemas/incident-record.yaml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/path/extraction.yaml b/contracts/path/extraction.yaml index 0e11309..865e20c 100644 --- a/contracts/path/extraction.yaml +++ b/contracts/path/extraction.yaml @@ -129,9 +129,9 @@ extract_by_id: input_id: "550e8400-e29b-41d4-a716-446655440001" status: "completed" completed_at: "2024-07-15T14:31:05Z" - canonical_incident: + incident_contract: schema_version: "1.1.0" - schema_name: "fireform_canonical_incident" + schema_name: "fireform_incident_contract" extraction_metadata: extract_id: "550e8400-e29b-41d4-a716-446655440020" confidence_score: 0.91 @@ -179,7 +179,7 @@ extract_by_id: content: application/merge-patch+json: schema: - $ref: "../schemas/canonical-incident.yaml#/CanonicalIncident" + $ref: "../schemas/incident-contract.yaml#/IncidentContract" example: fire: estimated_damage_usd: 250000 diff --git a/contracts/schemas/extraction-record.yaml b/contracts/schemas/extraction-record.yaml index aef7442..ae0fd48 100644 --- a/contracts/schemas/extraction-record.yaml +++ b/contracts/schemas/extraction-record.yaml @@ -27,7 +27,7 @@ ExtractionCompleted: - extract_id - input_id - status - - canonical_incident + - incident_contract properties: extract_id: type: string @@ -47,8 +47,8 @@ ExtractionCompleted: description: LLM model that performed the extraction processing_time_seconds: type: number - canonical_incident: - $ref: "canonical-incident.yaml#/CanonicalIncident" + incident_contract: + $ref: "incident-contract.yaml#/IncidentContract" corrections: type: array description: Audit trail of manual corrections applied via PATCH @@ -97,7 +97,7 @@ ExtractionProcessing: type: string nullable: true partial_result: - $ref: "canonical-incident.yaml#/CanonicalIncident" + $ref: "incident-contract.yaml#/IncidentContract" ValidationResult: type: object diff --git a/contracts/schemas/incident-contract.yaml b/contracts/schemas/incident-contract.yaml index a11d1b9..1a129df 100644 --- a/contracts/schemas/incident-contract.yaml +++ b/contracts/schemas/incident-contract.yaml @@ -1,7 +1,7 @@ # Canonical FireForm Incident Schema # This is the master superset schema single source of truth for all downstream forms -CanonicalIncident: +IncidentContract: type: object description: | The canonical FireForm incident data model. This is the superset schema containing @@ -16,7 +16,7 @@ CanonicalIncident: type: string description: Schema name identifier enum: - - fireform_canonical_incident + - fireform_incident_contract extraction_metadata: $ref: "#/ExtractionMetadata" diff --git a/contracts/schemas/incident-record.yaml b/contracts/schemas/incident-record.yaml index 932425c..686e7ce 100644 --- a/contracts/schemas/incident-record.yaml +++ b/contracts/schemas/incident-record.yaml @@ -94,8 +94,8 @@ IncidentRecordFull: - $ref: "#/IncidentRecord" - type: object properties: - canonical_incident: - $ref: "canonical-incident.yaml#/CanonicalIncident" + incident_contract: + $ref: "incident-contract.yaml#/IncidentContract" forms: type: array items: From 0529cfd90f7abc39b6a9006674091375af1b13cf Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 11 Jun 2026 23:43:17 +0530 Subject: [PATCH 16/18] added setup docs, updated contributing.md --- CONTRIBUTING.md | 23 +++--------- docs/SETUP.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 docs/SETUP.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 154daf8..7d297fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,25 +39,12 @@ If you have a great idea for FireForm, we'd love to hear it! Please open an issu 4. Make sure your code lints. 5. Issue that pull request! -## 🛠️ Local Development Setup - -FireForm uses Docker and Docker Compose for the backend to ensure a consistent environment. - -### Prerequisites +**Issues are not formally assigned.** You are free to pick any open issue, work on it, and raise a PR directly. If multiple PRs address the same issue, the first one that actually fixes it generally gets preference. To avoid duplicating someone else's work, coordinate with other contributors on our [Discord](https://discord.gg/nBv5b6kF68) before starting. -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/install/) -- `make` (optional, but recommended) -- [Node.js](https://nodejs.org/) 20+ (only needed for the desktop app) +## 💬 Community -### Desktop App Development +Join our Discord server to ask questions, discuss issues, and coordinate work with other contributors: https://discord.gg/nBv5b6kF68 -The frontend is a vanilla HTML/CSS/JS app wrapped in Electron. To run it locally: - -```bash -cd frontend -npm install # one-time setup -npm start # launches the Electron desktop window -``` +## 🛠️ Local Development Setup -The backend (API + Ollama) must be running separately via Docker — see `make fireform`. +See the [Setup Guide](docs/SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..216bc24 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,98 @@ +# Setup Guide + +This guide gets the FireForm backend running locally with Docker. It assumes you are comfortable with git and a terminal, but new to this project. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) **24 or newer**, with the Docker daemon running +- Docker Compose v2 (bundled with Docker Desktop; verify with `docker compose version`) +- `make` +- ~3 GB of free disk space (Docker images + LLM model weights) + +## Setup + +### 1. Clone the repository + +```bash +git clone https://github.com/fireform-core/FireForm.git +cd FireForm +``` + +### 2. Run first-time setup + +```bash +make init +``` + +This will: + +1. Check that Docker meets the requirements above +2. Create `docker/.env.dev` from `docker/.env.example` (gitignored; defaults work out of the box) +3. Prompt you to pick an Ollama model (the default, `qwen2.5:1.5b`, is the smallest and fine for development) +4. Offer to build and start everything answer `y`, or run `make fireform` later + +### 3. Build and start (if you skipped it in step 2) + +```bash +make fireform +``` + +This builds the Docker images, starts the containers, waits for Ollama, and pulls the LLM model. The first run takes several minutes (image build + model download); later runs are fast. + +When it finishes you'll see: + +``` +FireForm is ready! + API: http://localhost:8000 + API Docs: http://localhost:8000/docs +``` + +### 4. Verify it works + +Open **http://localhost:8000/docs** in your browser. This is the interactive Swagger UI you can explore and test every API endpoint directly from there (expand an endpoint, click _Try it out_, then _Execute_). + +## Day-to-day commands + +| Command | What it does | +| ------------ | ------------------------------------------------------------------------ | +| `make up` | Start containers | +| `make down` | Stop containers (data is preserved) | +| `make logs` | Stream all container logs (`make logs-app` / `make logs-ollama` for one) | +| `make shell` | Open a shell inside the app container | +| `make test` | Run the test suite | +| `make help` | List all commands | + +The dev container mounts the source code, so code changes reload automatically — no rebuild needed. Rebuild (`make build`) only when dependencies in `requirements.txt` or the Dockerfile change. + +## Frontend (optional) + +The desktop/web frontend lives in a separate repository: + +```bash +git clone https://github.com/fireform-core/fireform-frontend.git +``` + +Follow the README in that repository to run it. The backend from this guide must be running for the frontend to work. + +## Troubleshooting + +**`make init` fails dependency checks** +Docker isn't running or is too old. Start Docker Desktop (or the Docker daemon) and confirm `docker version` reports 24+. + +**Port 8000 already in use** +Another process is bound to the port. Either stop it, or change `APP_PORT` in `docker/.env.dev` and run `make down && make up`. + +**Model pull is slow or times out** +The first `make fireform` downloads the LLM weights (~1 GB for the default model). On a slow connection just wait, or re-run `make pull-model` it resumes safely. + +**Containers start but the API doesn't respond** +Check `make logs-app` for the actual error. The entrypoint runs database migrations on startup, so the API takes a few seconds after the container starts. + +**Want a clean slate** +`make super-clean` stops everything and **deletes all volumes** database, uploads, and downloaded model weights. Only use it when you intend to wipe all local data. + +## Where to go next + +- **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors +- [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute +- [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail From c131ee14d479a806c8037d9c8ab5e20d0cb5d795 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 12 Jun 2026 03:13:11 +0530 Subject: [PATCH 17/18] added project structure --- CONTRIBUTING.md | 2 + docs/{SETUP.md => 1. SETUP.md} | 1 + docs/2. PROJECT_STRUCTURE.md | 87 ++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) rename docs/{SETUP.md => 1. SETUP.md} (97%) create mode 100644 docs/2. PROJECT_STRUCTURE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d297fb..6cf0be7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,3 +48,5 @@ Join our Discord server to ask questions, discuss issues, and coordinate work wi ## 🛠️ Local Development Setup See the [Setup Guide](docs/SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. + +Before writing code, read the [Project Structure](docs/PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. diff --git a/docs/SETUP.md b/docs/1. SETUP.md similarity index 97% rename from docs/SETUP.md rename to docs/1. SETUP.md index 216bc24..5b4de34 100644 --- a/docs/SETUP.md +++ b/docs/1. SETUP.md @@ -95,4 +95,5 @@ Check `make logs-app` for the actual error. The entrypoint runs database migrati - **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors - [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute +- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes - [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail diff --git a/docs/2. PROJECT_STRUCTURE.md b/docs/2. PROJECT_STRUCTURE.md new file mode 100644 index 0000000..cee9c40 --- /dev/null +++ b/docs/2. PROJECT_STRUCTURE.md @@ -0,0 +1,87 @@ +# Project Structure + +This document explains how the FireForm repository is organized and, when you add new code, where it should go. + +## Top-level layout + +``` +FireForm/ +├── app/ # Backend source code (FastAPI application) +├── tests/ # Test suite (pytest) +├── docker/ # Dockerfiles, compose files, env templates +├── scripts/ # Shell scripts +├── docs/ # Project documentation +├── examples/ # Standalone demo scripts +├── data/ # Local runtime data (gitignored contents) +├── Makefile # Entry point for all dev commands (`make help`) +└── requirements.txt # Python dependencies +``` + +| You want to… | Go to | +| -------------------------------- | ------------------------------------------------------- | +| Change backend behavior | `app/` | +| Add or fix tests | `tests/` | +| Change container setup, env vars | `docker/` (see [docker/README.md](../docker/README.md)) | +| Change setup/bootstrap scripts | `scripts/` | +| Add documentation | `docs/` | +| Add dev commands | `Makefile` | + +## Inside `app/` + +The backend follows a layered structure: **routes → services → repositories → database**. Requests enter through the API layer, business logic lives in services, and all database access goes through repositories. + +``` +app/ +├── main.py # App factory: creates FastAPI app, wires middleware and routers +├── api/ # HTTP layer - request/response handling only, no business logic +│ ├── router.py # Aggregates all route modules into one router; main.py mounts this +│ ├── deps.py # Shared FastAPI dependencies (e.g. DB session injection) +│ ├── routes/ # One module per feature (forms.py, templates.py, …) +│ └── schemas/ # Pydantic request/response models, one module per feature +├── core/ # App-wide infrastructure +│ ├── config.py # All settings and env var reading nothing else reads os.environ +│ ├── lifespan.py # Startup/shutdown logic (DB init, etc.) +│ ├── logging.py # Logging configuration +│ └── errors/ # Custom exception classes (base.py) and handlers (handlers.py) +├── services/ # Business logic — LLM extraction, PDF filling, orchestration +├── db/ # Database engine/session (database.py) and repositories.py +├── models/ # SQLAlchemy ORM models +└── utils/ # Small generic helpers with no business logic +``` + +### Where does my new code go? + +**Adding a new API endpoint:** + +1. `app/api/schemas/<...>.py` - Pydantic models for the request and response bodies +2. `app/api/routes/<...>.py` - the route handlers; keep them thin, delegate to a service +3. `app/api/router.py` - register the new router (one `include_router` line) +4. Business logic goes in `app/services/`, not in the route handler + +**Adding business logic:** `app/services/`. A service should not know about HTTP (no FastAPI imports) it takes plain data in and returns plain data, so it can be tested and reused independently. + +**Adding a database table:** define the ORM model in `app/models/models.py`, and add its query/persistence functions to `app/db/repositories.py`. Services call repositories; routes never touch the database directly. + +**Adding a setting or env var:** declare it in `app/core/config.py` and document it in `docker/.env.example`. Code elsewhere imports from `config`, never reads `os.environ` itself. + +**Adding a custom error:** subclass in `app/core/errors/base.py`; map it to an HTTP response in `app/core/errors/handlers.py`. + +## Tests + +Tests live in `tests/`, run with `make test` (pytest inside the app container). `conftest.py` holds shared fixtures. Name files `test_.py` and mirror what you're testing: API endpoint tests alongside `test_api.py`, model/DB tests alongside `test_model.py`. +Note: Tests will be undergoing many changes hence the docs can be outdated, Please raise issue if tests or docs are outdated. + +## Docker & scripts + +- `docker/dev/` - development image (hot reload, source mounted) and its compose file. This is what `make up` runs. +- `docker/prod/` - production image (multi-stage build, gunicorn, no source mount). +- `docker/.env.example` - template for env vars; copied to `.env.dev` by `make init`. +- `scripts/` - shell scripts invoked by `make init` (dependency checks, env file creation, model selection) and by container startup. Not meant to be run directly. + +Full details: [docker/README.md](../docker/README.md). + +## Everything else + +- `examples/` - runnable demo scripts showing the pipeline end to end; not imported by the app. +- `data/` - runtime working data on your machine; contents are not part of the codebase. +- `src/`, `temp/` - scratch/legacy directories slated for cleanup; don't add new code here. From 6ea00393f865393b2a3aee3aa5c93942a17e5fc0 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 12 Jun 2026 19:17:47 +0530 Subject: [PATCH 18/18] Updated renamed file name --- CONTRIBUTING.md | 4 ++-- docs/1. SETUP.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cf0be7..d30e9b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,6 @@ Join our Discord server to ask questions, discuss issues, and coordinate work wi ## 🛠️ Local Development Setup -See the [Setup Guide](docs/SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. +See the [Setup Guide](docs/1.%20SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. -Before writing code, read the [Project Structure](docs/PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. +Before writing code, read the [Project Structure](docs/2.%20PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. diff --git a/docs/1. SETUP.md b/docs/1. SETUP.md index 5b4de34..30d2aee 100644 --- a/docs/1. SETUP.md +++ b/docs/1. SETUP.md @@ -95,5 +95,5 @@ Check `make logs-app` for the actual error. The entrypoint runs database migrati - **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors - [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute -- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes +- [PROJECT_STRUCTURE.md](2.%20PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes - [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail