05/06 Phase 2: fixed issues 3rd

This commit is contained in:
2026-05-07 11:44:05 -04:00
parent 50347e95c6
commit 33010fd71b
19 changed files with 81 additions and 49 deletions
+37 -1
View File
@@ -886,6 +886,42 @@ MySQL backup credentials stored in `/etc/mysql/backup.cnf` (mode 600, owned by `
---
## Development Rules
The following rules are mandatory for all development on this codebase. Violations have caused production 500 errors.
**Rule 1 — Never assume, always read first.**
Before fixing any error or touching any file, read the actual file content on disk. Do not assume the file matches what was previously written — the server may have a different version. Use `cat`, `grep`, or `sed -n` to read the exact content before making any changes.
**Rule 2 — No temporary fixes.**
All fixes must address the root cause. Workarounds that mask a problem without solving it are not permitted. If the root cause is unclear, investigate further before writing any code.
**Rule 3 — Never remove or change existing functionality unless explicitly instructed.**
All changes must be additive or corrective. Existing routes, function names, variable names, and model fields must be preserved unless a change is explicitly requested.
**Rule 4 — Always audit the full extends chain in every template you create or modify.**
Every `{% extends "..." %}` path must resolve relative to the Flask app's `template_folder` root. Before delivering any template, verify the parent template exists at that exact path. Correct convention for this project: `admin/layouts/base.html` for admin templates, `tenant/layouts/base.html` for tenant templates.
**Rule 5 — Never leave `url_for()` calls pointing at unregistered or stub-only endpoints.**
If a blueprint is registered as a stub (no routes yet), all `url_for()` references to its endpoints in templates must be replaced with `'#'` or guarded with `{% if %}` until the route is implemented. A `BuildError` from an unregistered endpoint is a 500 error in production.
**Rule 6 — Always add logging for create, edit, and delete actions.**
Every route that creates, edits, deletes, or changes the status of a record must call `logger.info()` or `logger.warning()` with the action, relevant IDs, and actor context.
**Rule 7 — All audit log values must be JSON-serialisable.**
When passing data to `AuditLog.log()` or any JSON column, always convert `datetime`/`date` to ISO-8601 strings via `.isoformat()` and `Decimal` to `float`. Use `model_to_dict()` from `app/admin/utils.py` — never pass raw ORM objects.
**Rule 8 — Use absolute paths for `template_folder` and `static_folder` in app factories.**
Never use relative paths (`"../templates"`) in Flask app factories. Flask resolves relative paths against the package directory, not the project root, which differs depending on where Gunicorn is started. Always use `os.path.dirname(os.path.abspath(__file__))` as the anchor.
**Rule 9 — Test all fixes before delivering them.**
Every fix must be validated by a programmatic test (import test, render test, or unit test) before being packaged for delivery. Do not ship code that has not been executed in the container.
**Rule 10 — Pack files when more than 5 files are changed.**
When a fix touches more than 5 files, compress them into a ZIP for delivery. Always include only the changed files — not the entire project.
---
## Phase 1 — Implementation Notes
The following decisions and resolutions were made during Phase 1 scaffold implementation.
@@ -952,4 +988,4 @@ All Phase 2+ blueprints are registered as stubs (blueprint object only, no route
| 24 | Admin login URL | Admin auth blueprint uses `url_prefix=""`. Login page is at `posadmin.ngodanguyen.tech/login`. Root `/` redirects to `/login` (unauthenticated) or `/dashboard` (authenticated). |
| 25 | JWT blocklist table location | `jwt_blocklist` defined in `platform.py` (not `salon.py`) — it is a platform-level concern shared across all tenants. DB-persisted (not in-memory) to survive Gunicorn worker restarts. |
| 26 | Template path convention | `template_folder` points to project-root `templates/`. All `render_template()` calls and `{% extends %}` use full paths: `"admin/auth/login.html"`, `"admin/layouts/base.html"`, `"tenant/auth/login.html"`, etc. |
| 27 | Production domains | Admin portal: `posadmin.ngodanguyen.tech`. Tenant portal: `pos.ngodanguyen.tech`. Updated in `.env`, `nginx.conf`, and all documentation. |
| 27 | Production domains | Admin portal: `posadmin.ngodanguyen.tech`. Tenant portal: `pos.ngodanguyen.tech`. Updated in `.env`, `nginx.conf`, and all documentation. |
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Account Suspended{% endblock %}
{% block content %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "tenant/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Account Cancelled{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
+2 -4
View File
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Sign In{% endblock %}
{% block content %}
@@ -44,9 +44,7 @@
<a href="{{ url_for('tenant_auth.password_reset_request') }}" class="text-muted small d-block mt-2">
Forgot password?
</a>
<a href="{{ url_for('tenant_auth.demo_login') }}" class="text-muted small d-block mt-1">
View demo account
</a>
</div>
</div>
</div>
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Set New Password{% endblock %}
{% block content %}
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Reset Password{% endblock %}
{% block content %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "tenant/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Account Suspended{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "tenant/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h4 class="mb-4">Dashboard
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Feature Unavailable{% endblock %}
{% block content %}
+13 -13
View File
@@ -37,7 +37,7 @@
{% for loc in locations %}
<li>
<a class="dropdown-item {% if g.location and g.location.id == loc.id %}active{% endif %}"
href="{{ url_for('locations.switch', location_id=loc.id) }}">
href="{{ '#' }}">
{{ loc.name }}
{% if loc.is_primary %}<span class="badge bg-secondary ms-1">Primary</span>{% endif %}
</a>
@@ -86,74 +86,74 @@
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'appointments' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('appointments.index') }}">
href="{{ '#' }}">
<i class="bi bi-calendar3 me-2"></i>Appointments
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'pos' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('pos.checkout') }}">
href="{{ '#' }}">
<i class="bi bi-cash-register me-2"></i>POS / Checkout
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'customers' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('customers.index') }}">
href="{{ '#' }}">
<i class="bi bi-people me-2"></i>Customers
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'staff' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('staff.index') }}">
href="{{ '#' }}">
<i class="bi bi-person-badge me-2"></i>Staff
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'services' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('services.index') }}">
href="{{ '#' }}">
<i class="bi bi-card-list me-2"></i>Services & Products
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'inventory' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('inventory.index') }}">
href="{{ '#' }}">
<i class="bi bi-box-seam me-2"></i>Inventory
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'gift_cards' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('gift_cards.index') }}">
href="{{ '#' }}">
<i class="bi bi-gift me-2"></i>Gift Cards
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'reconciliation' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('reconciliation.index') }}">
href="{{ '#' }}">
<i class="bi bi-calculator me-2"></i>Reconciliation
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'reports' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('reports.index') }}">
href="{{ '#' }}">
<i class="bi bi-bar-chart-line me-2"></i>Reports
</a>
</li>
{% if current_user.role == 'tenant_admin' %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'marketing' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('marketing.index') }}">
href="{{ '#' }}">
<i class="bi bi-megaphone me-2"></i>Marketing
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'locations' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('locations.index') }}">
href="{{ '#' }}">
<i class="bi bi-geo-alt me-2"></i>Locations
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'settings' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('settings.index') }}">
href="{{ '#' }}">
<i class="bi bi-gear me-2"></i>Settings
</a>
</li>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "tenant/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Staff Login{% endblock %}
{% block content %}
<div class="row justify-content-center">
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Staff Login{% endblock %}
{% block content %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Account Suspended{% endblock %}
{% block content %}
+2 -4
View File
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Sign In{% endblock %}
{% block content %}
@@ -44,9 +44,7 @@
<a href="{{ url_for('tenant_auth.password_reset_request') }}" class="text-muted small d-block mt-2">
Forgot password?
</a>
<a href="{{ url_for('tenant_auth.demo_login') }}" class="text-muted small d-block mt-1">
View demo account
</a>
</div>
</div>
</div>
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Set New Password{% endblock %}
{% block content %}
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Reset Password{% endblock %}
{% block content %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Feature Unavailable{% endblock %}
{% block content %}
+13 -13
View File
@@ -37,7 +37,7 @@
{% for loc in locations %}
<li>
<a class="dropdown-item {% if g.location and g.location.id == loc.id %}active{% endif %}"
href="{{ url_for('locations.switch', location_id=loc.id) }}">
href="{{ '#' }}">
{{ loc.name }}
{% if loc.is_primary %}<span class="badge bg-secondary ms-1">Primary</span>{% endif %}
</a>
@@ -86,74 +86,74 @@
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'appointments' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('appointments.index') }}">
href="{{ '#' }}">
<i class="bi bi-calendar3 me-2"></i>Appointments
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'pos' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('pos.checkout') }}">
href="{{ '#' }}">
<i class="bi bi-cash-register me-2"></i>POS / Checkout
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'customers' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('customers.index') }}">
href="{{ '#' }}">
<i class="bi bi-people me-2"></i>Customers
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'staff' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('staff.index') }}">
href="{{ '#' }}">
<i class="bi bi-person-badge me-2"></i>Staff
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'services' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('services.index') }}">
href="{{ '#' }}">
<i class="bi bi-card-list me-2"></i>Services & Products
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'inventory' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('inventory.index') }}">
href="{{ '#' }}">
<i class="bi bi-box-seam me-2"></i>Inventory
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'gift_cards' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('gift_cards.index') }}">
href="{{ '#' }}">
<i class="bi bi-gift me-2"></i>Gift Cards
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'reconciliation' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('reconciliation.index') }}">
href="{{ '#' }}">
<i class="bi bi-calculator me-2"></i>Reconciliation
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'reports' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('reports.index') }}">
href="{{ '#' }}">
<i class="bi bi-bar-chart-line me-2"></i>Reports
</a>
</li>
{% if current_user.role == 'tenant_admin' %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'marketing' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('marketing.index') }}">
href="{{ '#' }}">
<i class="bi bi-megaphone me-2"></i>Marketing
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'locations' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('locations.index') }}">
href="{{ '#' }}">
<i class="bi bi-geo-alt me-2"></i>Locations
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'settings' in request.endpoint %}active fw-bold{% endif %}"
href="{{ url_for('settings.index') }}">
href="{{ '#' }}">
<i class="bi bi-gear me-2"></i>Settings
</a>
</li>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "layouts/base.html" %}
{% extends "tenant/layouts/base.html" %}
{% block title %}Staff Login{% endblock %}
{% block content %}