Fix global warranties view, add Model Number field, and enhance modal tab responsiveness

* **Fixed:**

  * Global view on Index page now correctly shows warranties from all users, including archived ones.
  * Added `GET /api/warranties/global/archived` and unified global queries with correlated subqueries to avoid missing or collapsed rows.
  * Updated frontend logic to merge archived warranties from the new endpoint when Global scope and Status = “All.”
  * Bumped `script.js` and service worker cache to ensure clients receive updated logic.
  * Updated files: `backend/warranties_routes.py`, `frontend/script.js`, `frontend/sw.js`, `frontend/index.html`, `frontend/status.html`.

* **Added:**

  * Introduced **Model Number** field to warranties.
  * Backend: Added `model_number` column, integrated into GET/POST/PUT routes.
  * Frontend: Added Model Number input in New/Edit modals and display on warranty cards.
  * Updated files: `backend/migrations/047_add_model_number_to_warranties.sql`, `backend/warranties_routes.py`, `frontend/index.html`, `frontend/status.html`, `frontend/script.js`, `locales/en/translation.json`.

* **Enhanced:**

  * Improved **Add Warranty modal** tab alignment for responsive layouts (≤740px).
  * Adjusted tab label size and spacing to prevent wrapping while keeping icons and labels visible.
  * Ensured consistent five-step progress indicator across all breakpoints.
  * Updated file: `frontend/style.css`.
This commit is contained in:
sassanix
2025-10-09 15:04:13 -03:00
parent 9007c9c23a
commit 96f2859975
12 changed files with 308 additions and 68 deletions
+22
View File
@@ -1,4 +1,26 @@
# Changelog
## 0.10.1.15 - 2025-10-09
### Fixed
- Global view on Index page now shows warranties from all users as expected:
- Backend: Added `GET /api/warranties/global/archived` and unified global queries to return complete sets with correlated subqueries for claim status; no accidental row collapse.
- Frontend: When scope is Global and Status is "All", archived warranties are merged from the new global archived endpoint into the main list. Archived view in Global scope now uses the global archived endpoint.
- Cache bust: Bumped `script.js` and service worker cache to ensure clients receive the updated logic.
- Files: `backend/warranties_routes.py`, `frontend/script.js`, `frontend/sw.js`, `frontend/index.html`, `frontend/status.html`
### Added
- Model Number field added to warranties:
- Backend: Added `model_number` column and wired through GET/POST/PUT.
- Frontend: New and Edit modals now include a Model Number input.
- Cards: When present, Model Number displays on warranty cards (all views).
- Files: `backend/migrations/047_add_model_number_to_warranties.sql`, `backend/warranties_routes.py`, `frontend/index.html`, `frontend/status.html`, `frontend/script.js`, `locales/en/translation.json`
### Enhanced
- Add Warranty modal tabs responsive alignment:
- Shrink tab label text and spacing at ≤740px to prevent wrapping and keep all five tabs aligned (Product, Warranty, Documents, Tags, Summary).
- Maintain icons with labels; no text hiding. Uses five-step progress indicator consistent across breakpoints.
- Files: `frontend/style.css`
## 0.10.1.14 - 2025-10-06
+52 -10
View File
@@ -17,14 +17,40 @@ DB_USER = os.environ.get('DB_USER', 'warranty_user')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'warranty_password')
connection_pool = None # Global connection pool for this module
# Track the PID that created the current pool to detect post-fork reuse
pool_pid: Optional[int] = None
def _close_stale_pool_if_forked(current_pid: int) -> None:
"""Close any existing pool if it was created in a different process.
Gunicorn with preload_app=True forks workers after the app (and pool) may be
initialized. Psycopg2 connections/pools are not fork-safe. If we detect that
the pool was created in a different PID, we proactively close it in this
process so a fresh, per-process pool can be created.
"""
global connection_pool, pool_pid
if connection_pool is not None and pool_pid is not None and pool_pid != current_pid:
logger.warning(f"[DB_HANDLER] Detected PID change (pool pid {pool_pid} -> current pid {current_pid}). Closing stale pool and reinitializing...")
try:
# Close all connections owned by this (forked) process copy of the pool
connection_pool.closeall()
except Exception as close_err:
logger.warning(f"[DB_HANDLER] Error while closing stale pool in forked process: {close_err}")
finally:
connection_pool = None
pool_pid = None
def init_db_pool(max_retries=5, retry_delay=5):
global connection_pool # Ensure we're modifying the global variable in this module
global connection_pool, pool_pid # Ensure we're modifying the global variable in this module
attempt = 0
last_exception = None
if connection_pool is not None:
logger.info("[DB_HANDLER] Database connection pool already initialized.")
current_pid = os.getpid()
# If a pool exists but was created in a different PID, ensure we drop it first
_close_stale_pool_if_forked(current_pid)
if connection_pool is not None and pool_pid == current_pid:
logger.info("[DB_HANDLER] Database connection pool already initialized for this process.")
return connection_pool
while attempt < max_retries:
@@ -43,6 +69,7 @@ def init_db_pool(max_retries=5, retry_delay=5):
application_name='warracker_optimized' # Identify connections
)
logger.info("[DB_HANDLER] Database connection pool initialized successfully.")
pool_pid = current_pid
return connection_pool # Return the pool for external check if needed
except Exception as e:
last_exception = e
@@ -58,18 +85,33 @@ def init_db_pool(max_retries=5, retry_delay=5):
raise Exception("Unknown error creating database pool")
def get_db_connection():
global connection_pool
if connection_pool is None:
logger.error("[DB_HANDLER] Database connection pool is None. Attempting to re-initialize.")
init_db_pool() # Attempt to initialize it
if connection_pool is None: # If still None after attempt
logger.critical("[DB_HANDLER] CRITICAL: Database pool re-initialization failed.")
global connection_pool, pool_pid
current_pid = os.getpid()
# Detect and clean up any forked/stale pool
_close_stale_pool_if_forked(current_pid)
if connection_pool is None or pool_pid != current_pid:
if connection_pool is None:
logger.info("[DB_HANDLER] Database connection pool not initialized in this process. Initializing now...")
else:
logger.warning("[DB_HANDLER] Pool PID mismatch detected. Reinitializing pool for current process...")
init_db_pool() # Attempt to initialize it for this PID
if connection_pool is None or pool_pid != current_pid: # If still invalid after attempt
logger.critical("[DB_HANDLER] CRITICAL: Database pool initialization failed for current process.")
raise Exception("Database connection pool is not initialized and could not be re-initialized.")
try:
return connection_pool.getconn()
except Exception as e:
logger.error(f"[DB_HANDLER] Error getting connection from pool: {e}")
raise
# As a last resort, try reinitializing once in case the pool was invalidated
try:
_close_stale_pool_if_forked(os.getpid())
init_db_pool()
return connection_pool.getconn()
except Exception:
# Re-raise original to preserve context
raise
def release_db_connection(conn):
global connection_pool
@@ -0,0 +1,22 @@
-- Migration: Add model_number column to warranties table
-- Date: 2025-10-09
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'warranties'
AND column_name = 'model_number'
) THEN
ALTER TABLE warranties ADD COLUMN model_number VARCHAR(255) DEFAULT NULL;
RAISE NOTICE 'Added model_number column to warranties table';
ELSE
RAISE NOTICE 'model_number column already exists in warranties table';
END IF;
END $$;
-- Optional index to speed up searches/filtering by model number
CREATE INDEX IF NOT EXISTS idx_warranties_model_number ON warranties(model_number);
+132 -11
View File
@@ -65,7 +65,7 @@ def get_warranties():
w.purchase_price, w.user_id, w.created_at, w.updated_at, w.is_lifetime, w.vendor, w.warranty_type,
w.warranty_duration_years, w.warranty_duration_months, w.warranty_duration_days, w.product_photo_path, w.currency,
w.paperless_invoice_id, w.paperless_manual_id, w.paperless_photo_id, w.paperless_other_id,
w.invoice_url, w.manual_url, w.other_document_url,
w.invoice_url, w.manual_url, w.other_document_url, w.model_number,
CASE
WHEN COUNT(c.id) = 0 THEN 'NO_CLAIMS'
WHEN BOOL_OR(c.status IN ('Submitted', 'In Progress')) THEN 'OPEN'
@@ -134,7 +134,7 @@ def get_archived_warranties():
w.purchase_price, w.user_id, w.created_at, w.updated_at, w.is_lifetime, w.vendor, w.warranty_type,
w.warranty_duration_years, w.warranty_duration_months, w.warranty_duration_days, w.product_photo_path, w.currency,
w.paperless_invoice_id, w.paperless_manual_id, w.paperless_photo_id, w.paperless_other_id,
w.invoice_url, w.manual_url, w.other_document_url,
w.invoice_url, w.manual_url, w.other_document_url, w.model_number,
CASE
WHEN COUNT(c.id) = 0 THEN 'NO_CLAIMS'
WHEN BOOL_OR(c.status IN ('Submitted', 'In Progress')) THEN 'OPEN'
@@ -249,6 +249,7 @@ def add_warranty():
notes = request.form.get('notes', '')
vendor = request.form.get('vendor', None)
warranty_type = request.form.get('warranty_type', None)
model_number = request.form.get('model_number', None)
# Get URL fields for documents
invoice_url = request.form.get('invoice_url', None)
@@ -440,16 +441,16 @@ def add_warranty():
invoice_path, manual_path, other_document_path, product_url, purchase_price, user_id, is_lifetime, notes, vendor, warranty_type,
warranty_duration_years, warranty_duration_months, warranty_duration_days, product_photo_path, currency,
paperless_invoice_id, paperless_manual_id, paperless_photo_id, paperless_other_id,
invoice_url, manual_url, other_document_url
invoice_url, manual_url, other_document_url, model_number
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
''', (
product_name, purchase_date, expiration_date,
db_invoice_path, db_manual_path, db_other_document_path, product_url, purchase_price, user_id, is_lifetime, notes, vendor, warranty_type,
warranty_duration_years, warranty_duration_months, warranty_duration_days, db_product_photo_path, currency,
paperless_invoice_id, paperless_manual_id, paperless_photo_id, paperless_other_id,
invoice_url, manual_url, other_document_url
invoice_url, manual_url, other_document_url, model_number
))
warranty_id = cur.fetchone()[0]
@@ -711,6 +712,7 @@ def update_warranty(warranty_id):
notes = request.form.get('notes', None)
vendor = request.form.get('vendor', None)
warranty_type = request.form.get('warranty_type', None)
model_number = request.form.get('model_number', None)
# Get URL fields for documents
invoice_url = request.form.get('invoice_url', None)
@@ -1003,7 +1005,8 @@ def update_warranty(warranty_id):
'purchase_price': purchase_price,
'vendor': vendor,
'warranty_type': warranty_type,
'currency': currency
'currency': currency,
'model_number': model_number
}
sql_fields = []
sql_values = []
@@ -1537,6 +1540,7 @@ def get_global_warranties():
conn = get_db_connection()
with conn.cursor() as cur:
# Get all warranties from all users with user information (exclude archived for default view)
# Use correlated subqueries for claim status to avoid GROUP BY collapsing or miscounting
cur.execute('''
SELECT
w.id, w.product_name, w.purchase_date, w.expiration_date, w.invoice_path, w.manual_path, w.other_document_path,
@@ -1546,15 +1550,19 @@ def get_global_warranties():
w.invoice_url, w.manual_url, w.other_document_url,
u.username, u.email, u.first_name, u.last_name,
CASE
WHEN COUNT(c.id) = 0 THEN 'NO_CLAIMS'
WHEN BOOL_OR(c.status IN ('Submitted', 'In Progress')) THEN 'OPEN'
ELSE 'FINISHED'
WHEN EXISTS (
SELECT 1 FROM warranty_claims c
WHERE c.warranty_id = w.id AND c.status IN ('Submitted', 'In Progress')
) THEN 'OPEN'
WHEN EXISTS (
SELECT 1 FROM warranty_claims c
WHERE c.warranty_id = w.id
) THEN 'FINISHED'
ELSE 'NO_CLAIMS'
END AS claim_status_summary
FROM warranties w
JOIN users u ON w.user_id = u.id
LEFT JOIN warranty_claims c ON w.id = c.warranty_id
WHERE w.archived_at IS NULL
GROUP BY w.id, u.id
ORDER BY u.username, CASE WHEN w.is_lifetime THEN 1 ELSE 0 END, w.expiration_date NULLS LAST, w.product_name
''')
@@ -1614,6 +1622,119 @@ def get_global_warranties():
release_db_connection(conn)
@warranties_bp.route('/warranties/global/archived', methods=['GET'])
@token_required
def get_global_warranties_archived():
"""Get archived warranties from all users (public view for all authenticated users)"""
conn = None
try:
# Check if global view is enabled for this user
user_is_admin = request.user.get('is_admin', False)
conn = get_db_connection()
with conn.cursor() as cur:
# Get both global view settings
cur.execute("SELECT key, value FROM site_settings WHERE key IN ('global_view_enabled', 'global_view_admin_only')")
settings = {row[0]: row[1] for row in cur.fetchall()}
# Check if global view is enabled at all
global_view_enabled = settings.get('global_view_enabled', 'true').lower() == 'true'
if not global_view_enabled:
return jsonify({"error": "Global view is disabled by administrator"}), 403
# Check if global view is restricted to admins only
admin_only = settings.get('global_view_admin_only', 'false').lower() == 'true'
if admin_only and not user_is_admin:
return jsonify({"error": "Global view is restricted to administrators only"}), 403
# Release the connection since we'll get a new one below
release_db_connection(conn)
conn = None
conn = get_db_connection()
with conn.cursor() as cur:
# Get archived warranties from all users with user information
# Use correlated subqueries for claim status to avoid GROUP BY collapsing or miscounting
cur.execute('''
SELECT
w.id, w.product_name, w.purchase_date, w.expiration_date, w.invoice_path, w.manual_path, w.other_document_path,
w.product_url, w.notes, w.purchase_price, w.user_id, w.created_at, w.updated_at, w.is_lifetime,
w.vendor, w.warranty_type, w.warranty_duration_years, w.warranty_duration_months, w.warranty_duration_days, w.product_photo_path, w.currency,
w.paperless_invoice_id, w.paperless_manual_id, w.paperless_photo_id, w.paperless_other_id,
w.invoice_url, w.manual_url, w.other_document_url,
u.username, u.email, u.first_name, u.last_name,
CASE
WHEN EXISTS (
SELECT 1 FROM warranty_claims c
WHERE c.warranty_id = w.id AND c.status IN ('Submitted', 'In Progress')
) THEN 'OPEN'
WHEN EXISTS (
SELECT 1 FROM warranty_claims c
WHERE c.warranty_id = w.id
) THEN 'FINISHED'
ELSE 'NO_CLAIMS'
END AS claim_status_summary
FROM warranties w
JOIN users u ON w.user_id = u.id
WHERE w.archived_at IS NOT NULL
ORDER BY w.archived_at DESC NULLS LAST, w.updated_at DESC NULLS LAST, w.product_name
''')
warranties = cur.fetchall()
columns = [desc[0] for desc in cur.description]
warranties_list = []
for row in warranties:
warranty_dict = dict(zip(columns, row))
# Convert date objects to ISO format strings for JSON serialization
for key, value in warranty_dict.items():
if isinstance(value, (datetime, date)):
warranty_dict[key] = value.isoformat()
# Convert Decimal objects to float for JSON serialization
elif isinstance(value, Decimal):
warranty_dict[key] = float(value)
# Get serial numbers for this warranty
warranty_id = warranty_dict['id']
cur.execute('SELECT serial_number FROM serial_numbers WHERE warranty_id = %s', (warranty_id,))
serial_numbers = [row[0] for row in cur.fetchall()]
warranty_dict['serial_numbers'] = serial_numbers
# Get tags for this warranty
cur.execute('''
SELECT t.id, t.name, t.color
FROM tags t
JOIN warranty_tags wt ON t.id = wt.tag_id
WHERE wt.warranty_id = %s
ORDER BY t.name
''', (warranty_id,))
tags = [{'id': t[0], 'name': t[1], 'color': t[2]} for t in cur.fetchall()]
warranty_dict['tags'] = tags
# Add user display name for better UI
first_name = warranty_dict.get('first_name', '').strip() if warranty_dict.get('first_name') else ''
last_name = warranty_dict.get('last_name', '').strip() if warranty_dict.get('last_name') else ''
username = warranty_dict.get('username', '').strip() if warranty_dict.get('username') else ''
if first_name and last_name:
warranty_dict['user_display_name'] = f"{first_name} {last_name}"
elif first_name:
warranty_dict['user_display_name'] = first_name
elif username:
warranty_dict['user_display_name'] = username
else:
warranty_dict['user_display_name'] = 'Unknown User'
warranties_list.append(warranty_dict)
return jsonify(warranties_list)
except Exception as e:
logger.error(f"Error retrieving archived global warranties: {e}")
return jsonify({"error": "Failed to retrieve archived global warranties"}), 500
finally:
if conn:
release_db_connection(conn)
@warranties_bp.route('/currencies', methods=['GET'])
@token_required
def get_currencies():
+2 -2
View File
@@ -323,7 +323,7 @@
<!-- Hero Section -->
<div class="about-hero">
<h1><i class="fas fa-shield-alt"></i> Warracker</h1>
<div class="version" id="versionDisplay" data-i18n="about.version">Version v0.10.1.14</div>
<div class="version" id="versionDisplay" data-i18n="about.version">Version v0.10.1.15</div>
<p class="description" data-i18n="about.description">
A comprehensive warranty management system designed to help you track, organize, and manage all your product warranties in one secure, user-friendly platform.
</p>
@@ -421,7 +421,7 @@
// Update version display dynamically
const versionDisplay = document.getElementById('versionDisplay');
if (versionDisplay && window.i18next) {
const currentVersion = '0.10.1.14'; // This should match version-checker.js
const currentVersion = '0.10.1.15'; // This should match version-checker.js
versionDisplay.textContent = window.i18next.t('about.version') + ' v' + currentVersion;
}
+9 -1
View File
@@ -415,6 +415,10 @@
<input type="text" name="serial_numbers[]" class="form-control" style="margin-bottom: 8px;" data-i18n-placeholder="warranties.enter_serial_number" placeholder="Enter serial number">
</div>
</div>
<div class="form-group">
<label for="modelNumber" data-i18n="warranties.model_number_optional">Model Number (Optional)</label>
<input type="text" id="modelNumber" name="model_number" class="form-control" data-i18n-placeholder="warranties.model_number_placeholder" placeholder="e.g. SM-G991U1">
</div>
<div class="form-group">
<label for="vendor" data-i18n="warranties.vendor_optional">Vendor (Optional)</label>
<input type="text" id="vendor" name="vendor" class="form-control" data-i18n-placeholder="warranties.vendor_placeholder" placeholder="e.g. Amazon, Best Buy, etc.">
@@ -819,6 +823,10 @@
<!-- Serial number inputs will be added dynamically -->
</div>
</div>
<div class="form-group">
<label for="editModelNumber" data-i18n="warranties.model_number_optional">Model Number (Optional)</label>
<input type="text" id="editModelNumber" name="model_number" class="form-control" data-i18n-placeholder="warranties.model_number_placeholder" placeholder="e.g. SM-G991U1">
</div>
<div class="form-group">
<label for="editVendor" data-i18n="warranties.vendor_optional">Vendor (Optional)</label>
<input type="text" id="editVendor" name="vendor" class="form-control" data-i18n-placeholder="warranties.vendor_placeholder" placeholder="e.g. Amazon, Best Buy, etc.">
@@ -1318,7 +1326,7 @@
</div>
<script src="auth.js?v=20250119001"></script>
<script src="script.js?v=20250119001"></script>
<script src="script.js?v=20250119002"></script>
<script>
// Lightweight UI logic for Filter/Sort popovers (no change to core logic)
document.addEventListener('DOMContentLoaded', function () {
+33 -4
View File
@@ -936,6 +936,9 @@ let formTabs = []; // Changed from const to let, initialized as empty
if (notesModalWarrantyObj.warranty_type) {
formData.append('warranty_type', notesModalWarrantyObj.warranty_type);
}
if (typeof notesModalWarrantyObj.model_number !== 'undefined' && notesModalWarrantyObj.model_number !== null) {
formData.append('model_number', notesModalWarrantyObj.model_number);
}
if (notesModalWarrantyObj.serial_numbers && Array.isArray(notesModalWarrantyObj.serial_numbers)) {
notesModalWarrantyObj.serial_numbers.forEach(sn => {
if (sn && sn.trim() !== '') {
@@ -953,6 +956,11 @@ let formTabs = []; // Changed from const to let, initialized as empty
formData.append('tag_ids', JSON.stringify([]));
}
formData.append('notes', notesValue);
// Also include model number value from edit input if present on DOM
const editModelNumberInput = document.getElementById('editModelNumber');
if (editModelNumberInput && editModelNumberInput.value.trim() !== '') {
formData.set('model_number', editModelNumberInput.value.trim());
}
const response = await fetch(`/api/warranties/${warrantyId}`, {
method: 'PUT',
headers: {
@@ -2182,10 +2190,10 @@ async function loadWarranties(isAuthenticated) { // Added isAuthenticated parame
// Use the appropriate API endpoint based on saved preference
const baseUrl = window.location.origin;
// If status is 'archived', use archived endpoint (user-scoped only for now)
// If status is 'archived', use archived endpoint (support global vs personal)
const isArchivedView = currentFilters && currentFilters.status === 'archived';
const apiUrl = isArchivedView
? `${baseUrl}/api/warranties/archived`
? (shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`)
: (shouldUseGlobalView ? `${baseUrl}/api/warranties/global` : `${baseUrl}/api/warranties`);
console.log(`[DEBUG] Using API endpoint based on saved preference '${savedScope}', archivedView=${isArchivedView}: ${apiUrl}`);
@@ -2235,9 +2243,9 @@ async function loadWarranties(isAuthenticated) { // Added isAuthenticated parame
// Optionally merge archived items into the "All" view (only in personal scope)
let combinedData = Array.isArray(data) ? data : [];
lastLoadedIncludesArchived = false;
if (!shouldUseGlobalView && !isArchivedView && currentFilters && currentFilters.status === 'all') {
if (!isArchivedView && currentFilters && currentFilters.status === 'all') {
try {
const archivedUrl = `${baseUrl}/api/warranties/archived`;
const archivedUrl = shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`;
const archivedResp = await fetch(archivedUrl, options);
if (archivedResp.ok) {
const archivedData = await archivedResp.json();
@@ -2709,6 +2717,7 @@ async function renderWarranties(warrantiesToRender) {
</div>
` : ''}
` : ''}
${warranty.model_number ? `<div><i class="fas fa-tag"></i> ${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: <span>${warranty.model_number}</span></div>` : ''}
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
</div>
@@ -2771,6 +2780,7 @@ async function renderWarranties(warrantiesToRender) {
</div>
` : ''}
` : ''}
${warranty.model_number ? `<div><i class=\"fas fa-tag\"></i> ${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: <span>${warranty.model_number}</span></div>` : ''}
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
</div>
@@ -2833,6 +2843,7 @@ async function renderWarranties(warrantiesToRender) {
</div>
` : ''}
` : ''}
${warranty.model_number ? `<div><i class=\"fas fa-tag\"></i> ${window.i18next ? window.i18next.t('warranties.model_number') : 'Model Number'}: <span>${warranty.model_number}</span></div>` : ''}
${warranty.vendor ? `<div><i class="fas fa-store"></i> ${window.i18next ? window.i18next.t('warranties.vendor') : 'Vendor'}: <span>${warranty.vendor}</span></div>` : ''}
${warranty.warranty_type ? `<div><i class="fas fa-shield-alt"></i> ${window.i18next ? window.i18next.t('warranties.type') : 'Type'}: <span>${warranty.warranty_type}</span></div>` : ''}
</div>
@@ -3205,6 +3216,10 @@ async function openEditModal(warranty) {
// Populate form fields
document.getElementById('editProductName').value = warranty.product_name;
document.getElementById('editProductUrl').value = warranty.product_url || '';
const editModelNumberInput = document.getElementById('editModelNumber');
if (editModelNumberInput) {
editModelNumberInput.value = warranty.model_number || '';
}
document.getElementById('editPurchaseDate').value = warranty.purchase_date.split('T')[0];
// Populate new duration fields
document.getElementById('editWarrantyDurationYears').value = warranty.warranty_duration_years || 0;
@@ -3890,6 +3905,11 @@ async function handleFormSubmit(event) { // Made async to properly await paperle
// Create form data object
const formData = new FormData(warrantyForm);
// Ensure model_number is included if present
const modelNumberInput = document.getElementById('modelNumber');
if (modelNumberInput && modelNumberInput.value.trim() !== '') {
formData.set('model_number', modelNumberInput.value.trim());
}
// Handle warranty type - use custom value if "other" is selected
const warrantyTypeSelect = document.getElementById('warrantyType');
@@ -6359,6 +6379,15 @@ function saveWarranty() {
formData.append('notes', '');
}
// Add model number to form data (optional)
const editModelNumber = document.getElementById('editModelNumber');
if (editModelNumber && editModelNumber.value.trim() !== '') {
formData.append('model_number', editModelNumber.value.trim());
} else {
// Explicitly clear if empty
formData.append('model_number', '');
}
// Add vendor/retailer to form data
const editVendorInput = document.getElementById('editVendor'); // Use the correct ID
formData.append('vendor', editVendorInput ? editVendorInput.value.trim() : ''); // Use the correct variable
+5 -1
View File
@@ -37,7 +37,7 @@
<script src="js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="fix-auth-buttons-loader.js?v=20250119001"></script>
<script src="script.js?v=20250119001" defer></script> <!-- Added script.js -->
<script src="script.js?v=20250119002" defer></script> <!-- Added script.js -->
<script src="status.js?v=20250119001" defer></script> <!-- Status page specific functionality -->
<style>
.user-menu {
@@ -551,6 +551,10 @@
<!-- Serial number inputs will be added dynamically -->
</div>
</div>
<div class="form-group">
<label for="editModelNumber" data-i18n="warranties.model_number_optional">Model Number (Optional)</label>
<input type="text" id="editModelNumber" name="model_number" class="form-control" data-i18n-placeholder="warranties.model_number_placeholder" placeholder="e.g. SM-G991U1">
</div>
<div class="form-group">
<label for="editVendor" data-i18n="warranties.vendor_optional">Vendor (Optional)</label>
<input type="text" id="editVendor" name="vendor" class="form-control" data-i18n-placeholder="warranties.vendor_placeholder" placeholder="e.g. Amazon, Best Buy, etc.">
+24 -35
View File
@@ -304,35 +304,16 @@ header .container {
display: none;
}
.form-tabs::before {
content: '';
position: absolute;
bottom: -2px;
height: 2px;
background-color: var(--secondary-color);
transition: all 0.3s ease;
width: 25%;
z-index: 1;
}
.form-tabs::before { display: none; }
.form-tabs[data-step="0"]::before {
left: 0;
}
.form-tabs[data-step="1"]::before {
left: 25%;
}
.form-tabs[data-step="2"]::before {
left: 50%;
}
.form-tabs[data-step="3"]::before {
left: 75%;
}
/* Progress handled via ::after below */
.form-tab {
flex: 1;
flex: 1 1 20%;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
text-align: center;
padding: 15px 10px;
cursor: pointer;
@@ -340,7 +321,9 @@ header .container {
color: var(--dark-gray);
transition: color 0.3s ease;
box-sizing: border-box;
width: 25%; /* Ensure each tab is exactly 25% width */
width: 20%; /* Five tabs fit on one row */
min-width: 0; /* Allow shrinking without wrapping inner content */
white-space: nowrap; /* Prevent label wrapping */
}
.form-tab:hover {
@@ -430,16 +413,22 @@ input.invalid {
transition: width 0.3s ease;
}
.form-tabs[data-step="0"]::after {
width: calc(100% / 3);
.form-tabs[data-step="0"]::after { width: calc(100% / 5); }
.form-tabs[data-step="1"]::after { width: calc(200% / 5); }
.form-tabs[data-step="2"]::after { width: calc(300% / 5); }
.form-tabs[data-step="3"]::after { width: calc(400% / 5); }
.form-tabs[data-step="4"]::after { width: 100%; }
@media (max-width: 788px) {
.form-tab { padding: 12px 6px; }
.form-tab i { margin-right: 6px; }
}
.form-tabs[data-step="1"]::after {
width: calc(200% / 3);
}
.form-tabs[data-step="2"]::after {
width: 100%;
/* On narrow screens, shrink labels to keep tabs uniform (keep text visible) */
@media (max-width: 740px) {
.form-tab { padding: 10px 4px; gap: 4px; }
.form-tab i { margin-right: 4px; font-size: 0.95rem; }
.form-tab span { display: inline; font-size: 0.85rem; }
}
@keyframes fade-in {
+2 -2
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'warracker-cache-v20250119005';
const CACHE_NAME = 'warracker-cache-v20250119006';
const urlsToCache = [
'./',
'./index.html',
@@ -8,7 +8,7 @@ const urlsToCache = [
'./settings-styles.css?v=20250119001',
'./header-fix.css?v=20250119001',
'./mobile-header.css?v=20250119002',
'./script.js?v=20250119001',
'./script.js?v=20250119002',
'./auth.js?v=20250119001',
'./settings-new.js?v=20250119001',
'./status.js?v=20250119001',
+1 -1
View File
@@ -1,6 +1,6 @@
// Version checker for Warracker
document.addEventListener('DOMContentLoaded', () => {
const currentVersion = '0.10.1.14'; // Current version of the application
const currentVersion = '0.10.1.15'; // Current version of the application
const updateStatus = document.getElementById('updateStatus');
const updateLink = document.getElementById('updateLink');
const versionDisplay = document.getElementById('versionDisplay');
+4 -1
View File
@@ -108,6 +108,7 @@
"warranty_ends": "Warranty Ends",
"price": "Price",
"serial_number": "Serial Number",
"model_number": "Model Number",
"vendor": "Vendor",
"type": "Type",
"owner": "Owner",
@@ -211,7 +212,9 @@
"months_placeholder": "Months",
"days_placeholder": "Days",
"add_any_notes_placeholder": "Add any notes about this warranty...",
"add_serial_number": "Add another serial number"
"add_serial_number": "Add another serial number",
"model_number_optional": "Model Number (Optional)",
"model_number_placeholder": "e.g. SM-G991U1"
},
"settings": {
"title": "Warracker - Settings",