commit 0a4f870f67a952f3652c56194a451d4651e013da Author: Jimmy Date: Sun Mar 22 01:03:51 2026 +0100 Initial commit: Inspection reporting app Co-Authored-By: Claude Sonnet 4.6 diff --git a/.ralph/current-events b/.ralph/current-events new file mode 100644 index 0000000..f529c19 --- /dev/null +++ b/.ralph/current-events @@ -0,0 +1 @@ +.ralph/events-20260321-235351.jsonl \ No newline at end of file diff --git a/.ralph/current-loop-id b/.ralph/current-loop-id new file mode 100644 index 0000000..9cd6fc1 --- /dev/null +++ b/.ralph/current-loop-id @@ -0,0 +1 @@ +primary-20260321-235351 \ No newline at end of file diff --git a/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-587-32854.log b/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-587-32854.log new file mode 100644 index 0000000..76ac577 --- /dev/null +++ b/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-587-32854.log @@ -0,0 +1,4 @@ +2026-03-21T23:53:51.588056Z INFO ralph: Creating scratchpad directory: /home/jimmy/inspection_app_n/prototoolagain/.ralph/agent +2026-03-21T23:53:51.588132Z INFO ralph: Spawning subprocess for TUI mode child_args=["-c", "ralph.yml", "-H", "builtin:code-assist", "run", "--rpc"] +2026-03-21T23:53:51.588162Z INFO ralph: TUI subprocess stderr redirected to log file log_file=/home/jimmy/inspection_app_n/prototoolagain/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-588-32854.log +2026-03-21T23:53:51.588319Z INFO ralph: TUI running in subprocess RPC mode diff --git a/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-588-32854.log b/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-588-32854.log new file mode 100644 index 0000000..bf305ba --- /dev/null +++ b/.ralph/diagnostics/logs/ralph-2026-03-22T00-53-51-588-32854.log @@ -0,0 +1,9 @@ +2026-03-21T23:53:51.596629Z  WARN ralph::loop_runner: Interactive mode requested but stdout is not a TTY, falling back to autonomous +2026-03-21T23:53:51.596942Z  INFO ralph_core::event_loop: Memory injection check: enabled=true, inject=Auto, workspace_root="/home/jimmy/inspection_app_n/prototoolagain" +2026-03-21T23:53:51.596956Z  INFO ralph_core::event_loop: Looking for memories at: "/home/jimmy/inspection_app_n/prototoolagain/.ralph/agent/memories.md" (exists: false) +2026-03-21T23:53:51.596960Z  INFO ralph_core::event_loop: Successfully loaded 0 memories from store +2026-03-21T23:53:51.596961Z  INFO ralph_core::event_loop: Memory store is empty - no memories to inject +2026-03-21T23:59:04.846327Z  INFO ralph_core::event_loop: Memory injection check: enabled=true, inject=Auto, workspace_root="/home/jimmy/inspection_app_n/prototoolagain" +2026-03-21T23:59:04.846344Z  INFO ralph_core::event_loop: Looking for memories at: "/home/jimmy/inspection_app_n/prototoolagain/.ralph/agent/memories.md" (exists: false) +2026-03-21T23:59:04.846349Z  INFO ralph_core::event_loop: Successfully loaded 0 memories from store +2026-03-21T23:59:04.846352Z  INFO ralph_core::event_loop: Memory store is empty - no memories to inject diff --git a/.ralph/events-20260321-235351.jsonl b/.ralph/events-20260321-235351.jsonl new file mode 100644 index 0000000..943c0cb --- /dev/null +++ b/.ralph/events-20260321-235351.jsonl @@ -0,0 +1 @@ +{"ts":"2026-03-21T23:53:51.596805172+00:00","iteration":0,"hat":"loop","topic":"build.start","triggered":"planner","payload":"You are building a production-ready Inspection Reporting and Management web application from scratch. The GitHub remote URL is: https://github.com/pingud98/prototoolagain.git\n\n---\n\n## TECH STACK\n\n- Language: Python 3.11+\n- Web Framework: Flask (with Flask-Login, Flask-WTF, Flask-SQLAlchemy)\n- Database: SQLite via SQLAlchemy ORM\n- PDF Generation: WeasyPrint (A4-formatted output)\n- TLS/HTTPS: Self-signed certificate via trustme or mkcert for local hosting\n- Frontend: Jinja2 templates + Tailwind CS... [truncated, 7661 chars total]"} diff --git a/.ralph/history.jsonl b/.ralph/history.jsonl new file mode 100644 index 0000000..78cd2f6 --- /dev/null +++ b/.ralph/history.jsonl @@ -0,0 +1 @@ +{"ts":"2026-03-21T23:53:51.596853071Z","type":{"kind":"loop_started","prompt":"You are building a production-ready Inspection Reporting and Management web application from scratch. The GitHub remote URL is: https://github.com/pingud98/prototoolagain.git\n\n---\n\n## TECH STACK\n\n- Language: Python 3.11+\n- Web Framework: Flask (with Flask-Login, Flask-WTF, Flask-SQLAlchemy)\n- Database: SQLite via SQLAlchemy ORM\n- PDF Generation: WeasyPrint (A4-formatted output)\n- TLS/HTTPS: Self-signed certificate via trustme or mkcert for local hosting\n- Frontend: Jinja2 templates + Tailwind CSS (via CDN) + vanilla JS\n- Auth: Bcrypt password hashing, session-based login\n- File Storage: Local filesystem under /uploads/, referenced in DB\n\n---\n\n## PROJECT STRUCTURE\n\ninspection-app/\n├── app/\n│ ├── __init__.py\n│ ├── models.py\n│ ├── routes/\n│ │ ├── auth.py\n│ │ ├── inspections.py\n│ │ ├── admin.py\n│ │ └── export.py\n│ ├── templates/\n│ │ ├── base.html\n│ │ ├── login.html\n│ │ ├── dashboard.html\n│ │ ├── inspection_form.html\n│ │ ├── inspection_view.html\n│ │ └── admin/\n│ │ ├── users.html\n│ │ └── user_form.html\n│ ├── static/\n│ │ ├── css/\n│ │ └── js/\n│ └── utils/\n│ ├── pdf_generator.py\n│ └── security.py\n├── uploads/\n├── certs/\n├── setup.py\n├── config.py\n├── run.py\n├── requirements.txt\n└── .gitignore\n\n---\n\n## DATABASE MODELS\n\n### User\n- id, username, full_name, email, password_hash, is_admin, is_active, created_at\n\n### Inspection\n- id, installation_name, location, inspection_date, version (int, starts at 1),\n reference_number (int), observations, conclusion_text,\n conclusion_status (enum: ok / minor / major),\n created_by (FK User), created_at, updated_at\n\n### InspectionInspector\n- id, inspection_id (FK), user_id (FK nullable), free_text_name (nullable)\n (Supports both registered users and free-text names)\n\n### Photo\n- id, inspection_id (FK), filename, caption,\n action_required (enum: none / urgent / before_next), uploaded_at\n\n---\n\n## SETUP SCRIPT (setup.py)\n\nThe setup script must:\n1. Install all dependencies from requirements.txt using pip\n2. Generate a self-signed TLS certificate and key, saved to certs/\n3. Create the SQLite database and run all table migrations\n4. Prompt the admin for: username, full name, email, password (with confirmation)\n5. Create the admin account with is_admin=True\n6. Print a success message with the local HTTPS URL (e.g. https://localhost:5000)\n7. Be runnable with: python setup.py\n\n---\n\n## CORE FEATURES\n\n### Authentication\n- Login page (username + password)\n- Session-based auth with Flask-Login\n- All routes protected — redirect to login if not authenticated\n- Logout route\n- No self-registration — admin creates all accounts\n\n### Admin Panel (/admin)\n- List all users\n- Create new user (username, full name, email, password, admin toggle)\n- Edit user (change name, email, reset password, toggle active/admin)\n- Deactivate (not delete) users\n- Only accessible to is_admin=True users\n\n### Dashboard (/)\n- Table of all inspections the logged-in user has access to\n- Columns: Reference No., Installation Name, Location, Date, Version, Conclusion Status, Actions\n- Actions: View, Edit, Export PDF\n- \"New Inspection\" button\n\n### Inspection Form (/inspection/new and /inspection//edit)\n\nFields:\n1. Installation Name — text input\n2. Location — text input\n3. Date of Inspection — date picker\n4. Version — auto-incremented integer (display only, not editable)\n5. Reference Number — integer input\n6. Inspector(s) — pre-filled with logged-in user's full name; allow adding more via:\n - Dropdown of registered users\n - Free-text field for external individuals\n - Display as removable tags/chips\n7. Observations — large textarea\n8. Photos section:\n - Upload multiple photos\n - For each uploaded photo display a thumbnail\n - Per-photo fields: caption (text), action_required (radio buttons):\n \"No action required\"\n \"Urgent action required\"\n \"Action required before next inspection\"\n - Ability to remove photos\n9. Conclusion section:\n - Conclusion comments textarea\n - Radio buttons (select exactly one):\n OK for operation in current state\n Minor comments — Remedial actions required for continued operation\n Major comments — Operation suspended until resolution and satisfactory follow-up inspection\n\nButtons:\n- New inspection: \"Complete Report\" → saves, sets version=1, redirects to view page\n- Edit existing: \"Update Report\" → saves, increments version by 1, redirects to view page\n- Cancel → returns to dashboard\n\n### Inspection View (/inspection/)\n- Read-only formatted view of the report\n- Shows all fields, photos (with captions and action status), inspectors, conclusion\n- \"Edit Report\" button\n- \"Export as PDF\" button\n\n---\n\n## PDF EXPORT (/inspection//pdf)\n\n- Generated using WeasyPrint\n- Formatted for A4 pages\n- Include:\n - App name / report title header\n - All inspection fields in a clean two-column layout\n - Inspector names listed\n - Observations in a clearly delineated box\n - Photos displayed in a grid (max 2 per row), each with caption and action status clearly labelled\n - Conclusion section with selected status prominently displayed\n - Footer with page number and generation timestamp\n- Flows naturally across multiple A4 pages if content requires it\n- Served as a file download: inspection_report__v.pdf\n\n---\n\n## SECURITY REQUIREMENTS\n\n- All passwords hashed with bcrypt (min cost factor 12)\n- CSRF protection on all forms via Flask-WTF\n- File uploads validated: only JPEG, PNG, GIF, WEBP accepted; max 10MB per file\n- Uploaded filenames sanitised with werkzeug.utils.secure_filename and stored with UUID prefix\n- User input escaped in all templates (Jinja2 autoescaping enabled)\n- Admin routes protected with both login_required and admin_required decorators\n- Secret key loaded from environment variable SECRET_KEY or auto-generated and saved to .env on first run\n- HTTPS enforced — Flask run with SSL context using certs from certs/\n- .env and *.db and certs/ added to .gitignore\n\n---\n\n## GITHUB INSTRUCTIONS\n\n- The repository already exists and has been initialised with prior commits\n- Completely discard all prior history\n- Use git checkout --orphan new-branch, add all files, commit, then force-push to main\n- Commit message: \"Initial commit: Inspection reporting app\"\n- Include a comprehensive README.md with:\n - Project overview\n - Requirements (Python version, OS)\n - Setup instructions (python setup.py)\n - How to run (python run.py)\n - How to access (HTTPS URL)\n - Notes on the self-signed certificate browser warning\n\n---\n\n## CODE QUALITY STANDARDS\n\n- All Python files include docstrings\n- Routes grouped into Blueprints\n- No hardcoded secrets\n- Database access only via SQLAlchemy ORM — no raw SQL\n- Error pages for 403, 404, 500\n- Flash messages for all user actions (success and error)\n- Logging to a rotating file log (logs/app.log)\n\n---\n\n## EXECUTION ORDER\n\nBuild in this order:\n1. requirements.txt and config.py\n2. app/models.py\n3. app/__init__.py (app factory)\n4. Auth blueprint + templates\n5. Admin blueprint + templates\n6. Inspection blueprint + form + view templates\n7. PDF export utility + route\n8. setup.py\n9. run.py\n10. README.md\n11. .gitignore\n12. GitHub force-push\n\nDo not proceed to the next step until the current one is complete and internally consistent.\n\n---\n\n## NOTES FOR THE OPERATOR\n\n- WeasyPrint requires system-level dependencies. Install them before running setup.py:\n Debian/Ubuntu: sudo apt install libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0\n macOS: brew install pango\n Windows: See https://doc.courtbouillon.org/weasyprint/stable/first_steps.html\n\n\n"}} diff --git a/.ralph/history.jsonl.lock b/.ralph/history.jsonl.lock new file mode 100644 index 0000000..e69de29 diff --git a/.ralph/loop.lock b/.ralph/loop.lock new file mode 100644 index 0000000..9d2ea61 --- /dev/null +++ b/.ralph/loop.lock @@ -0,0 +1,5 @@ +{ + "pid": 32871, + "started": "2026-03-21T23:53:51.590496087Z", + "prompt": "You are building a production-ready Inspection Reporting and Management web application from scra..." +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0247d6d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 James Devine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROMPT.md b/PROMPT.md new file mode 100644 index 0000000..60f9564 --- /dev/null +++ b/PROMPT.md @@ -0,0 +1,237 @@ +You are building a production-ready Inspection Reporting and Management web application from scratch. The GitHub remote URL is: https://github.com/pingud98/prototoolagain.git + +--- + +## TECH STACK + +- Language: Python 3.11+ +- Web Framework: Flask (with Flask-Login, Flask-WTF, Flask-SQLAlchemy) +- Database: SQLite via SQLAlchemy ORM +- PDF Generation: WeasyPrint (A4-formatted output) +- TLS/HTTPS: Self-signed certificate via trustme or mkcert for local hosting +- Frontend: Jinja2 templates + Tailwind CSS (via CDN) + vanilla JS +- Auth: Bcrypt password hashing, session-based login +- File Storage: Local filesystem under /uploads/, referenced in DB + +--- + +## PROJECT STRUCTURE + +inspection-app/ +├── app/ +│ ├── __init__.py +│ ├── models.py +│ ├── routes/ +│ │ ├── auth.py +│ │ ├── inspections.py +│ │ ├── admin.py +│ │ └── export.py +│ ├── templates/ +│ │ ├── base.html +│ │ ├── login.html +│ │ ├── dashboard.html +│ │ ├── inspection_form.html +│ │ ├── inspection_view.html +│ │ └── admin/ +│ │ ├── users.html +│ │ └── user_form.html +│ ├── static/ +│ │ ├── css/ +│ │ └── js/ +│ └── utils/ +│ ├── pdf_generator.py +│ └── security.py +├── uploads/ +├── certs/ +├── setup.py +├── config.py +├── run.py +├── requirements.txt +└── .gitignore + +--- + +## DATABASE MODELS + +### User +- id, username, full_name, email, password_hash, is_admin, is_active, created_at + +### Inspection +- id, installation_name, location, inspection_date, version (int, starts at 1), + reference_number (int), observations, conclusion_text, + conclusion_status (enum: ok / minor / major), + created_by (FK User), created_at, updated_at + +### InspectionInspector +- id, inspection_id (FK), user_id (FK nullable), free_text_name (nullable) + (Supports both registered users and free-text names) + +### Photo +- id, inspection_id (FK), filename, caption, + action_required (enum: none / urgent / before_next), uploaded_at + +--- + +## SETUP SCRIPT (setup.py) + +The setup script must: +1. Install all dependencies from requirements.txt using pip +2. Generate a self-signed TLS certificate and key, saved to certs/ +3. Create the SQLite database and run all table migrations +4. Prompt the admin for: username, full name, email, password (with confirmation) +5. Create the admin account with is_admin=True +6. Print a success message with the local HTTPS URL (e.g. https://localhost:5000) +7. Be runnable with: python setup.py + +--- + +## CORE FEATURES + +### Authentication +- Login page (username + password) +- Session-based auth with Flask-Login +- All routes protected — redirect to login if not authenticated +- Logout route +- No self-registration — admin creates all accounts + +### Admin Panel (/admin) +- List all users +- Create new user (username, full name, email, password, admin toggle) +- Edit user (change name, email, reset password, toggle active/admin) +- Deactivate (not delete) users +- Only accessible to is_admin=True users + +### Dashboard (/) +- Table of all inspections the logged-in user has access to +- Columns: Reference No., Installation Name, Location, Date, Version, Conclusion Status, Actions +- Actions: View, Edit, Export PDF +- "New Inspection" button + +### Inspection Form (/inspection/new and /inspection//edit) + +Fields: +1. Installation Name — text input +2. Location — text input +3. Date of Inspection — date picker +4. Version — auto-incremented integer (display only, not editable) +5. Reference Number — integer input +6. Inspector(s) — pre-filled with logged-in user's full name; allow adding more via: + - Dropdown of registered users + - Free-text field for external individuals + - Display as removable tags/chips +7. Observations — large textarea +8. Photos section: + - Upload multiple photos + - For each uploaded photo display a thumbnail + - Per-photo fields: caption (text), action_required (radio buttons): + "No action required" + "Urgent action required" + "Action required before next inspection" + - Ability to remove photos +9. Conclusion section: + - Conclusion comments textarea + - Radio buttons (select exactly one): + OK for operation in current state + Minor comments — Remedial actions required for continued operation + Major comments — Operation suspended until resolution and satisfactory follow-up inspection + +Buttons: +- New inspection: "Complete Report" → saves, sets version=1, redirects to view page +- Edit existing: "Update Report" → saves, increments version by 1, redirects to view page +- Cancel → returns to dashboard + +### Inspection View (/inspection/) +- Read-only formatted view of the report +- Shows all fields, photos (with captions and action status), inspectors, conclusion +- "Edit Report" button +- "Export as PDF" button + +--- + +## PDF EXPORT (/inspection//pdf) + +- Generated using WeasyPrint +- Formatted for A4 pages +- Include: + - App name / report title header + - All inspection fields in a clean two-column layout + - Inspector names listed + - Observations in a clearly delineated box + - Photos displayed in a grid (max 2 per row), each with caption and action status clearly labelled + - Conclusion section with selected status prominently displayed + - Footer with page number and generation timestamp +- Flows naturally across multiple A4 pages if content requires it +- Served as a file download: inspection_report__v.pdf + +--- + +## SECURITY REQUIREMENTS + +- All passwords hashed with bcrypt (min cost factor 12) +- CSRF protection on all forms via Flask-WTF +- File uploads validated: only JPEG, PNG, GIF, WEBP accepted; max 10MB per file +- Uploaded filenames sanitised with werkzeug.utils.secure_filename and stored with UUID prefix +- User input escaped in all templates (Jinja2 autoescaping enabled) +- Admin routes protected with both login_required and admin_required decorators +- Secret key loaded from environment variable SECRET_KEY or auto-generated and saved to .env on first run +- HTTPS enforced — Flask run with SSL context using certs from certs/ +- .env and *.db and certs/ added to .gitignore + +--- + +## GITHUB INSTRUCTIONS + +- The repository already exists and has been initialised with prior commits +- Completely discard all prior history +- Use git checkout --orphan new-branch, add all files, commit, then force-push to main +- Commit message: "Initial commit: Inspection reporting app" +- Include a comprehensive README.md with: + - Project overview + - Requirements (Python version, OS) + - Setup instructions (python setup.py) + - How to run (python run.py) + - How to access (HTTPS URL) + - Notes on the self-signed certificate browser warning + +--- + +## CODE QUALITY STANDARDS + +- All Python files include docstrings +- Routes grouped into Blueprints +- No hardcoded secrets +- Database access only via SQLAlchemy ORM — no raw SQL +- Error pages for 403, 404, 500 +- Flash messages for all user actions (success and error) +- Logging to a rotating file log (logs/app.log) + +--- + +## EXECUTION ORDER + +Build in this order: +1. requirements.txt and config.py +2. app/models.py +3. app/__init__.py (app factory) +4. Auth blueprint + templates +5. Admin blueprint + templates +6. Inspection blueprint + form + view templates +7. PDF export utility + route +8. setup.py +9. run.py +10. README.md +11. .gitignore +12. GitHub force-push + +Do not proceed to the next step until the current one is complete and internally consistent. + +--- + +## NOTES FOR THE OPERATOR + +- WeasyPrint requires system-level dependencies. Install them before running setup.py: + Debian/Ubuntu: sudo apt install libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 + macOS: brew install pango + Windows: See https://doc.courtbouillon.org/weasyprint/stable/first_steps.html + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2776167 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Inspection Reporting and Management App + +## Project Overview +This is a production-ready Inspection Reporting and Management web application built with Python 3.11+, Flask, SQLite, and WeasyPrint for PDF generation. + +## Requirements +- Python 3.11 or higher +- pip +- System dependencies for WeasyPrint (e.g., libpango, libharfbuzz, etc.) + +## Setup +1. Install Python dependencies: `pip install -r requirements.txt` +2. Run the setup script: `python setup.py` + - This installs dependencies, generates a self-signed TLS certificate, creates the SQLite database, and prompts for admin account details. +3. Ensure system-level WeasyPrint dependencies are installed: + - Debian/Ubuntu: `sudo apt install libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0` + - macOS: `brew install pango` + - Windows: Follow instructions at https://doc.courtbouillon.org/weasyprint/stable/first_steps.html + +## Running the Application +1. Start the server: `python run.py` +2. Access the application at `https://localhost:5000` + - Due to the self-signed certificate, your browser will show a warning. You can proceed by adding an exception or using `--no-check-certificates` if supported. + +## Authentication +- Login with the admin account created during setup. +- All routes are protected; unauthenticated access redirects to the login page. +- Logout is available via the `/logout` route. + +## Admin Panel +- Accessible at `/admin`. +- Admins can manage users, view inspections, and perform administrative tasks. +- Only users with `is_admin=True` can access the admin panel. + +## PDF Export +- Inspection reports can be exported as PDFs via the `/inspection//pdf` endpoint. +- PDFs are generated using WeasyPrint and formatted for A4 pages. + +## Security +- Passwords are hashed with bcrypt (cost factor 12). +- CSRF protection is enabled on all forms. +- File uploads are validated for allowed types and size limits. +- Input is escaped in templates. + +## Notes +- The self-signed certificate may cause browser warnings. For production, consider using a trusted certificate or `mkcert` for local development. +- All database files, environment variables, and certificates are listed in `.gitignore`. \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..ca7f2cc --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,36 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_bcrypt import Bcrypt +from config import Config + +db = SQLAlchemy() +login_manager = LoginManager() +bcrypt = Bcrypt() + +def create_app(config_class=Config): + app = Flask(__name__.name) + app.config.from_object(config_class) + + # Initialize extensions + db.init_app(app) + login_manager.init_app(app) + bcrypt.init_app(app) + + # Register blueprints + from app.routes.auth import auth_bp + from app.routes.inspections import inspections_bp + from app.routes.admin import admin_bp + from app.routes.export import export_bp + + app.register_blueprint(auth_bp) + app.register_blueprint(inspections_bp) + app.register_blueprint(admin_bp) + app.register_blueprint(export_bp) + + # Create database tables if they don't exist + @app.before_first_request + def create_tables(): + db.create_all() + + return app \ No newline at end of file diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..7675961 --- /dev/null +++ b/app/forms.py @@ -0,0 +1,17 @@ +"""WTForms for the application.""" + +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField +from wtforms.validators import DataRequired, Length + + +class LoginForm(FlaskForm): + """Login form.""" + + username = StringField( + "Username", validators=[DataRequired(), Length(min=1, max=64)] + ) + password = PasswordField( + "Password", validators=[DataRequired()] + ) + submit = SubmitField("Login") \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..ced5515 --- /dev/null +++ b/app/models.py @@ -0,0 +1,89 @@ +"""SQLAlchemy models for the Inspection Reporting and Management app.""" + +from datetime import datetime +from typing import Optional + +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + +from app import db + + +class User(db.Model, UserMixin): + """User account for authentication and authorization.""" + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, nullable=False) + full_name = db.Column(db.String(120), nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + is_admin = db.Column(db.Boolean, nullable=False, default=False) + is_active = db.Column(db.Boolean, nullable=False, default=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # Password handling + def set_password(self, password: str) -> None: + """Hash and store a password.""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password: str) -> bool: + """Check a plaintext password against the stored hash.""" + return check_password_hash(self.password_hash, password) + + +class Inspection(db.Model): + """A single inspection report.""" + + id = db.Column(db.Integer, primary_key=True) + installation_name = db.Column(db.String(120), nullable=False) + location = db.Column(db.String(120), nullable=False) + inspection_date = db.Column(db.Date, nullable=False) + version = db.Column(db.Integer, nullable=False, default=1) + reference_number = db.Column(db.Integer, nullable=False) + observations = db.Column(db.Text, nullable=True) + conclusion_text = db.Column(db.Text, nullable=True) + conclusion_status = db.Column( + db.String(20), nullable=False, default="ok" + ) # ok / minor / major + created_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + # Relationships + creator = db.relationship("User", backref="inspections") + inspectors = db.relationship( + "InspectionInspector", backref="inspection", cascade="all, delete-orphan" + ) + photos = db.relationship( + "Photo", backref="inspection", cascade="all, delete-orphan" + ) + + +class InspectionInspector(db.Model): + """Inspector for an inspection – either a registered user or a free‑text name.""" + + id = db.Column(db.Integer, primary_key=True) + inspection_id = db.Column( + db.Integer, db.ForeignKey("inspection.id"), nullable=False + ) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) + free_text_name = db.Column(db.String(120), nullable=True) + + __unique__ = ("inspection_id", "user_id") + + +class Photo(db.Model): + """Photo attached to an inspection.""" + + id = db.Column(db.Integer, primary_key=True) + inspection_id = db.Column( + db.Integer, db.ForeignKey("inspection.id"), nullable=False + ) + filename = db.Column(db.String(200), nullable=False) + caption = db.Column(db.String(200), nullable=True) + action_required = db.Column( + db.String(20), nullable=False, default="none" + ) # none / urgent / before_next + uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) \ No newline at end of file diff --git a/app/routes/auth.py b/app/routes/auth.py new file mode 100644 index 0000000..2dda499 --- /dev/null +++ b/app/routes/auth.py @@ -0,0 +1,29 @@ +"""Authentication blueprint.""" + +from flask import Blueprint, render_template, redirect, url_for, flash +from flask_login import login_user, logout_user, login_required, current_user +from app import db +from app.models import User +from app.forms import LoginForm + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + flash('Logged in successfully.', 'success') + return redirect(url_for('main.dashboard')) + else: + flash('Invalid username or password.', 'danger') + return render_template('login.html', form=form) + +@auth_bp.route('/logout') +@login_required +def logout(): + logout_user() + flash('Logged out successfully.', 'info') + return redirect(url_for('auth.login')) \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..41d47a5 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,44 @@ + + + + + + {% block title %}Inspection Reporting{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + +
+ + + + \ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..e52f5e5 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block content %} +
+

Dashboard

+ Admin Panel + +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..6ab9b7a --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}Login{% endblock %} + +{% block content %} +
+

Login

+
+ {{ form.hidden_tag() }} +
+ {{ form.username.label(class="block text-gray-700 mb-1") }} + {{ form.username(class="w-full px-3 py-2 border rounded") }} + {% if form.username.errors %} +

{{ form.username.errors[0] }}

+ {% endif %} +
+
+ {{ form.password.label(class="block text-gray-700 mb-1") }} + {{ form.password(class="w-full px-3 py-2 border rounded") }} + {% if form.password.errors %} +

{{ form.password.errors[0] }}

+ {% endif %} +
+
+ {{ form.submit(class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700") }} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..d864db6 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + +class Config: + SECRET_KEY = os.getenv('SECRET_KEY') or 'dev-secret-key-' + str(os.getpid()) + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', f'sqlite:///{BASE_DIR / "inspection.db"}') + SQLALCHEMY_TRACK_MODIFICATIONS = False + TESTING = False + WTF_CSRF_ENABLED = True + # File upload config + UPLOAD_FOLDER = str(BASE_DIR / 'uploads') + MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB per file + ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'webp'} \ No newline at end of file diff --git a/hats.yml b/hats.yml new file mode 100644 index 0000000..fde4228 --- /dev/null +++ b/hats.yml @@ -0,0 +1,13 @@ +# hats.yml +event_loop: + starting_event: "task.start" + +hats: + builder: + name: "Builder" + triggers: ["task.start"] + publishes: ["task.done"] + instructions: | + Implement the task from PROMPT.md. + Run any relevant tests. + When finished, emit task.done and print LOOP_COMPLETE. diff --git a/ralph.yml b/ralph.yml new file mode 100644 index 0000000..3b8ab05 --- /dev/null +++ b/ralph.yml @@ -0,0 +1,34 @@ +# Ralph Orchestrator Configuration +# Generated by: ralph init --backend claude +# Docs: https://github.com/mikeyobrien/ralph-orchestrator + +cli: + backend: "claude" + +event_loop: + prompt_file: "PROMPT.md" + completion_promise: "LOOP_COMPLETE" + max_iterations: 100 + # max_runtime_seconds: 14400 # 4 hours max + +# ───────────────────────────────────────────────────────────────────────────── +# Additional Configuration (uncomment to customize) +# ───────────────────────────────────────────────────────────────────────────── + +# core: +# scratchpad: ".ralph/agent/scratchpad.md" +# specs_dir: ".ralph/specs/" + +# Custom hats for multi-agent workflows: +# hats: +# builder: +# name: "Builder" +# triggers: ["build.task"] +# publishes: ["build.done", "build.blocked"] +# +# reviewer: +# name: "Reviewer" +# triggers: ["review.request"] +# publishes: ["review.approved", "review.changes_requested"] + +# Create PROMPT.md with your task, then run: ralph run diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1a2ca04 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +Flask>=2.3 +Flask-Login +Flask-WTF +Flask-SQLAlchemy +WeasyPrint +trustme +python-dotenv +Flask-Bcrypt +Pillow +uuid \ No newline at end of file