Initial commit: Inspection reporting app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0a4f870f67
22 changed files with 654 additions and 0 deletions
1
.ralph/current-events
Normal file
1
.ralph/current-events
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.ralph/events-20260321-235351.jsonl
|
||||||
1
.ralph/current-loop-id
Normal file
1
.ralph/current-loop-id
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
primary-20260321-235351
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[2m2026-03-21T23:53:51.596629Z[0m [33m WARN[0m [2mralph::loop_runner[0m[2m:[0m Interactive mode requested but stdout is not a TTY, falling back to autonomous
|
||||||
|
[2m2026-03-21T23:53:51.596942Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Memory injection check: enabled=true, inject=Auto, workspace_root="/home/jimmy/inspection_app_n/prototoolagain"
|
||||||
|
[2m2026-03-21T23:53:51.596956Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Looking for memories at: "/home/jimmy/inspection_app_n/prototoolagain/.ralph/agent/memories.md" (exists: false)
|
||||||
|
[2m2026-03-21T23:53:51.596960Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Successfully loaded 0 memories from store
|
||||||
|
[2m2026-03-21T23:53:51.596961Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Memory store is empty - no memories to inject
|
||||||
|
[2m2026-03-21T23:59:04.846327Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Memory injection check: enabled=true, inject=Auto, workspace_root="/home/jimmy/inspection_app_n/prototoolagain"
|
||||||
|
[2m2026-03-21T23:59:04.846344Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Looking for memories at: "/home/jimmy/inspection_app_n/prototoolagain/.ralph/agent/memories.md" (exists: false)
|
||||||
|
[2m2026-03-21T23:59:04.846349Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Successfully loaded 0 memories from store
|
||||||
|
[2m2026-03-21T23:59:04.846352Z[0m [32m INFO[0m [2mralph_core::event_loop[0m[2m:[0m Memory store is empty - no memories to inject
|
||||||
1
.ralph/events-20260321-235351.jsonl
Normal file
1
.ralph/events-20260321-235351.jsonl
Normal file
|
|
@ -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]"}
|
||||||
1
.ralph/history.jsonl
Normal file
1
.ralph/history.jsonl
Normal file
File diff suppressed because one or more lines are too long
0
.ralph/history.jsonl.lock
Normal file
0
.ralph/history.jsonl.lock
Normal file
5
.ralph/loop.lock
Normal file
5
.ralph/loop.lock
Normal file
|
|
@ -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..."
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -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.
|
||||||
237
PROMPT.md
Normal file
237
PROMPT.md
Normal file
|
|
@ -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/<id>/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/<id>)
|
||||||
|
- 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/<id>/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_<ref>_v<version>.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
|
||||||
|
|
||||||
|
|
||||||
47
README.md
Normal file
47
README.md
Normal file
|
|
@ -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/<id>/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`.
|
||||||
36
app/__init__.py
Normal file
36
app/__init__.py
Normal file
|
|
@ -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
|
||||||
17
app/forms.py
Normal file
17
app/forms.py
Normal file
|
|
@ -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")
|
||||||
89
app/models.py
Normal file
89
app/models.py
Normal file
|
|
@ -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)
|
||||||
29
app/routes/auth.py
Normal file
29
app/routes/auth.py
Normal file
|
|
@ -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'))
|
||||||
44
app/templates/base.html
Normal file
44
app/templates/base.html
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Inspection Reporting{% endblock %}</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100">
|
||||||
|
<nav class="bg-blue-800 text-white p-4">
|
||||||
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
|
<div class="text-white font-bold">Inspection App</div>
|
||||||
|
<div>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<span class="mr-4">Welcome, {{ current_user.full_name }}</span>
|
||||||
|
<a href="{{ url_for('auth.logout') }}" class="text-white hover:underline">Logout</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('auth.login') }}" class="text-white hover:underline">Login</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container mx-auto py-6">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="p-3 mb-2 rounded {% if category == 'danger' %}bg-red-100 text-red-800{% elif category == 'success' %}bg-green-100 text-green-800{% elif category == 'info' %}bg-blue-100 text-blue-800{% else %}bg-gray-100 text-gray-800{% endif %}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Place for any custom JS
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
app/templates/dashboard.html
Normal file
11
app/templates/dashboard.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h1 class="text-3xl font-bold mb-4">Dashboard</h1>
|
||||||
|
<a href="{{ url_for('admin.users') }}" class="bg-green-600 text-white px-4 py-2 rounded mr-4 hover:bg-green-700">Admin Panel</a>
|
||||||
|
<!-- Content will be added here later -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
29
app/templates/login.html
Normal file
29
app/templates/login.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-md mx-auto bg-white p-8 rounded shadow-md">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Login</h1>
|
||||||
|
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ 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 %}
|
||||||
|
<p class="text-red-600 text-sm">{{ form.username.errors[0] }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ 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 %}
|
||||||
|
<p class="text-red-600 text-sm">{{ form.password.errors[0] }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ form.submit(class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700") }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
15
config.py
Normal file
15
config.py
Normal file
|
|
@ -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'}
|
||||||
13
hats.yml
Normal file
13
hats.yml
Normal file
|
|
@ -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.
|
||||||
34
ralph.yml
Normal file
34
ralph.yml
Normal file
|
|
@ -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
|
||||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Flask>=2.3
|
||||||
|
Flask-Login
|
||||||
|
Flask-WTF
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
WeasyPrint
|
||||||
|
trustme
|
||||||
|
python-dotenv
|
||||||
|
Flask-Bcrypt
|
||||||
|
Pillow
|
||||||
|
uuid
|
||||||
Loading…
Reference in a new issue