Vendor, Date Format, Added security

Refer to changelogs
This commit is contained in:
sassanix
2025-05-04 13:02:51 -03:00
parent dee0881c06
commit e045aae208
53 changed files with 1710 additions and 1461 deletions

View File

@@ -1,5 +1,115 @@
# Changelog
## [0.9.9.4] - 2025-05-04
### Fixed
- **Theme Persistence & Consistency:**
- Refactored dark mode logic to use only a single `localStorage` key (`darkMode`) as the source of truth for theme preference across all pages.
- Removed all legacy and user-prefixed theme keys (e.g., `user_darkMode`, `admin_darkMode`, `${prefix}darkMode`).
- Ensured all theme toggles and settings update only the `darkMode` key, and all pages read only this key for theme initialization.
- Verified that `theme-loader.js` is included early in every HTML file to prevent flashes of incorrect theme.
- Cleaned up redundant or conflicting theme logic in all frontend scripts and HTML files.
- Theme preference now persists reliably across logins, logouts, and browser sessions (except in incognito/private mode, where localStorage is temporary by design).
_Files: `frontend/script.js`, `frontend/settings-new.js`, `frontend/status.js`, `frontend/register.html`, `frontend/reset-password.html`, `frontend/reset-password-request.html`, `frontend/theme-loader.js`, all main HTML files._
### Added
- **Admin/User Tag Separation:**
- Implemented distinct tags for Admins and regular Users.
- Tags created by an Admin are only visible to other Admins.
- Tags created by a User are only visible to other Users.
- Added `is_admin_tag` boolean column to the `tags` table via migration (`backend/migrations/009_add_admin_flag_to_tags.sql`).
- Backend API endpoints (`/api/tags`, `/api/warranties`, `/api/warranties/<id>/tags`) updated to filter tags based on the logged-in user's role (`is_admin`).
- Tag creation (`POST /api/tags`) now automatically sets the `is_admin_tag` flag based on the creator's role.
- Tag update (`PUT /api/tags/<id>`) and deletion (`DELETE /api/tags/<id>`) endpoints now prevent users/admins from modifying tags belonging to the other role.
_Files: `backend/app.py`, `backend/migrations/009_add_admin_flag_to_tags.sql`_
- **Version Check:**
- Added a version checker to the About page (`frontend/about.html`) that compares the current version with the latest GitHub release and displays the update status.
_Files: `frontend/about.html`, `frontend/version-checker.js`_
- **Mobile Home Screen Icon Support:**
- Added support for mobile devices to display the app icon when added to the home screen.
- Included `<link rel="apple-touch-icon" sizes="180x180">` for iOS devices, referencing a new 512x512 PNG icon.
- Added a web app manifest (`manifest.json`) referencing the 512x512 icon for Android/Chrome home screen support.
- Updated all main HTML files to include these tags for consistent experience across devices.
_Files: `frontend/index.html`, `frontend/login.html`, `frontend/register.html`, `frontend/about.html`, `frontend/reset-password.html`, `frontend/reset-password-request.html`, `frontend/status.html`, `frontend/settings-new.html`, `frontend/manifest.json`, `frontend/img/favicon-512x512.png`_
- **Optional Vendor Field for Warranties:**
- Users can now specify the vendor (e.g., Amazon, Best Buy) where a product was purchased, as an optional informational field for each warranty.
- The field is available when adding a new warranty and when editing an existing warranty.
- The vendor is displayed on warranty cards and in the summary tab of the add warranty wizard.
- The vendor field is now searchable alongside product name, notes, and tags.
- Backend API and database updated to support this field, including a migration to add the column to the warranties table.
- Editing a warranty now correctly updates the vendor field.
_Files: `backend/app.py`, `backend/migrations/017_add_vendor_to_warranties.sql`, `frontend/index.html`, `frontend/script.js`_
- **Serial Number Search:**
- Enhanced search functionality to include serial numbers.
- Updated search input placeholder text to reflect serial number search capability.
_Files: `frontend/script.js`, `frontend/index.html`_
- **CSV Import Vendor Field:**
- Added support for importing the `Vendor` field via CSV file upload.
- The CSV header should be `Vendor`.
_Files: `backend/app.py`, `frontend/script.js`_
- **Date Format Customization:**
- Users can now choose their preferred date display format in Settings > Preferences.
- Available formats include:
- Month/Day/Year (e.g., 12/31/2024)
- Day/Month/Year (e.g., 31/12/2024)
- Year-Month-Day (e.g., 2024-12-31)
- Mon Day, Year (e.g., Dec 31, 2024)
- Day Mon Year (e.g., 31 Dec 2024)
- Year Mon Day (e.g., 2024 Dec 31)
- The selected format is applied to purchase and expiration dates on warranty cards.
- The setting persists across sessions and is synchronized between open tabs.
_Files: `frontend/settings-new.html`, `frontend/settings-new.js`, `frontend/script.js`_
- **IPv6 Support:** Added `listen [::]:80;` directive to the Nginx configuration (`nginx.conf`) to enable listening on IPv6 interfaces alongside IPv4.
_Files: `nginx.conf`_
- **Cloudflare Compatibility:** Added `<script data-cfasync="false" src="/javascript.js">` to the `<head>` of the status page (`frontend/status.html`) to ensure proper loading when behind Cloudflare.
_Files: `frontend/status.html`_
### Changed
- **Migration System Overhaul:** Refactored the database migration system for improved reliability and consistency.
- **House cleaning**: Removed redundant files such as migrations, .env, and uploads folder.
- **Warranty Listing:** Admins now only see their own warranties on the main warranty list (`/api/warranties`).
- **Warranty Visibility:** Fixed issue where admins could see all users' warranties. Now both admins and regular users only see their own warranties on all pages.
_Files: `backend/app.py`_
**Bug Fixes:**
* **Date Handling:** Fixed issues causing warranty purchase and expiration dates to display incorrectly (off by one day) due to timezone differences:
* **Backend:** Corrected expiration date calculation in `backend/app.py` by removing the inaccurate `timedelta` fallback logic and ensuring the `python-dateutil` library (using `relativedelta`) is consistently used for accurate year addition.
* **Backend:** Added `python-dateutil` to `backend/requirements.txt` dependencies.
* **Frontend:** Updated date parsing in `frontend/script.js` (`processWarrantyData`) to use `Date.UTC()` when creating `Date` objects from `YYYY-MM-DD` strings, preventing local timezone interpretation.
* **Frontend:** Simplified and corrected date formatting in `frontend/script.js` (`formatDate`) to always use UTC date components (`getUTCFullYear`, etc.) for display, bypassing potential `toLocaleDateString` timezone issues.
* **Frontend:** Fixed purchase date display in the "Add Warranty" summary tab (`updateSummary` in `frontend/script.js`) by applying the same UTC-based parsing and formatting used elsewhere, resolving the off-by-one-day error during summary view.
* **Fractional Warranty Years & Date Accuracy:** Corrected the backend expiration date calculation (`backend/app.py`) to accurately handle fractional warranty years (e.g., 1.5 years) and prevent off-by-one-day errors.
* The initial fix for fractional years used an approximation (`timedelta`) which sometimes resulted in dates being a day early.
* The calculation now uses `dateutil.relativedelta` after decomposing the fractional years into integer years, months, and days, ensuring correct handling of leap years and month lengths.
* **UI:** Fixed toasts overlapping header menu due to z-index conflict (`style.css`).
* **API:** Fixed bug preventing updates to user preferences if only the timezone was changed (`app.py`).
* **Settings:** Resolved issue where opening the settings page in multiple tabs could cause both tabs to refresh continuously and potentially log the user out. This involved:
* Preventing `settings-new.js` from unnecessarily updating `user_info` in `localStorage` on load.
* Changing the `storage` event listener in `include-auth-new.js` to update the UI directly instead of reloading the page when auth data changes.
* Removing redundant checks (`setInterval`) and `storage` listener from `fix-auth-buttons.js` to prevent conflicts.
**New Features & Enhancements:**
* **API:** Added `/api/timezones` endpoint to provide a structured list of timezones grouped by region (`app.py`).
### Fixed
- **Tag Visibility:** Fixed an issue where tag visibility was incorrectly reversed between Admins and Users (Users saw Admin tags and vice-versa). Adjusted backend logic to correctly display tags based on role.
_Files: `backend/app.py`_
- Fixed currency symbol not persisting after browser restart/re-login:
- Prevented main warranty list from rendering before user authentication and preferences are fully loaded.
- Ensured preferences (currency, date format, etc.) are fetched from API and saved to localStorage immediately after login.
- Corrected inconsistent user type prefix (`user_` vs `admin_`) determination during initial page load.
## [0.9.9.3] - 2025-04-27
### Added

View File

@@ -1,9 +0,0 @@
# Database configuration
DB_PASSWORD=warranty_password
DB_ADMIN_PASSWORD=change_this_password_in_production
# Email configuration
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USERNAME=youremail@email.com
SMTP_PASSWORD=your_smtp_password

View File

@@ -40,6 +40,7 @@ Warracker is an open-source warranty tracker application designed to help you ef
- **Data Export and Import:** Export warranty data to CSV, or import warranties from CSV files.
- **Email Notifications:** Receive timely email reminders about upcoming expirations — configurable as daily, weekly, or monthly.
- **Customizable Currency Symbols:** Display prices using your preferred currency symbol ($, €, £, ¥, ₹, or a custom symbol).
- **Customizable Dates:** Display dates based on your own region.
- **Tagging:** Organize warranties with flexible, multi-tag support.
- **Password Reset:** Easily recover accounts through a secure, token-based password reset flow.
@@ -189,6 +190,7 @@ To get the docker compose file please go [here](https://github.com/sassanix/Warr
| **PurchasePrice** | Number (`199.99`, `50`) | ❌ No (Optional) | Cannot be negative if provided. |
| **SerialNumber** | Text (`SN123`, `SN123,SN456`) | ❌ No (Optional) | For multiple values, separate with commas. |
| **ProductURL** | Text (URL format) | ❌ No (Optional) | Full URL to product page (optional field). https://producturl.com |
| **Vendor** | Text | ❌ No (Optional) | Name of the vendor or seller where the product was purchased. |
| **Tags** | Text (`tag1,tag2`) | ❌ No (Optional) | Use comma-separated values for multiple tags. |

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
-- backend/init.sql
-- Grant superuser privileges to warranty_user
ALTER ROLE warranty_user WITH SUPERUSER;
-- The rest of the file content (CREATE TABLE, INSERT INTO) should be removed.
-- The migration system (apply_migrations.py) will handle table creation.

View File

@@ -0,0 +1,6 @@
-- Add is_admin_tag column to tags table
ALTER TABLE tags
ADD COLUMN IF NOT EXISTS is_admin_tag BOOLEAN NOT NULL DEFAULT FALSE;
-- Optional: Add an index for faster lookups based on admin status
CREATE INDEX IF NOT EXISTS idx_tags_is_admin_tag ON tags (is_admin_tag);

View File

@@ -1,5 +0,0 @@
-- 013_add_updated_at_to_tags.sql
-- Adds an updated_at column to the tags table for tracking updates
ALTER TABLE tags
ADD COLUMN updated_at TIMESTAMP DEFAULT NOW();

View File

@@ -0,0 +1,5 @@
-- 016_add_updated_at_to_tags.sql
-- Adds an updated_at column to the tags table for tracking updates
ALTER TABLE tags
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();

View File

@@ -0,0 +1,3 @@
-- Migration to add vendor field to warranties table
ALTER TABLE warranties ADD COLUMN IF NOT EXISTS
vendor VARCHAR(255) NULL;

View File

@@ -0,0 +1,18 @@
-- Add date_format column to user_preferences table if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'user_preferences'
AND column_name = 'date_format'
) THEN
-- Add the column with a default value of 'MDY'
ALTER TABLE user_preferences
ADD COLUMN date_format VARCHAR(10) NOT NULL DEFAULT 'MDY';
RAISE NOTICE 'Added date_format column to user_preferences table with default MDY';
ELSE
RAISE NOTICE 'date_format column already exists in user_preferences table';
END IF;
END $$;

View File

@@ -0,0 +1,32 @@
-- Add user_id column to tags table and update constraints
DO $$
BEGIN
-- Add user_id column if it doesn't exist
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'tags' AND column_name = 'user_id'
) THEN
-- First, add the column as nullable
ALTER TABLE tags ADD COLUMN user_id INTEGER;
-- Update existing tags to have user_id = 1 (assuming this is the admin user)
UPDATE tags SET user_id = 1 WHERE user_id IS NULL;
-- Make the column NOT NULL
ALTER TABLE tags ALTER COLUMN user_id SET NOT NULL;
-- Add foreign key constraint
ALTER TABLE tags ADD CONSTRAINT fk_tags_user_id
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- Drop the old unique constraint on name
ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_name_key;
-- Add new unique constraint on name and user_id
ALTER TABLE tags ADD CONSTRAINT tags_name_user_id_key
UNIQUE (name, user_id);
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON tags (user_id);
END IF;
END $$;

View File

@@ -7,4 +7,5 @@ Flask-Login==0.6.2
Flask-Bcrypt==1.0.1
PyJWT==2.6.0
email-validator==1.3.1
APScheduler==3.10.4
APScheduler==3.10.4
python-dateutil

View File

@@ -1,167 +0,0 @@
import unittest
from datetime import date, timedelta
import sys
import os
import json
from unittest.mock import patch, MagicMock
# Add the parent directory to sys.path to allow importing app.py
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# Import the functions from app.py
from app import get_expiring_warranties, format_expiration_email, send_expiration_notifications
class TestWarrantyNotifications(unittest.TestCase):
@patch('app.get_db_connection')
def test_get_expiring_warranties(self, mock_get_db_connection):
"""Test that get_expiring_warranties correctly queries expiring warranties."""
# Set up mock connection and cursor
mock_conn = MagicMock()
mock_cur = MagicMock()
mock_conn.cursor.return_value.__enter__.return_value = mock_cur
mock_get_db_connection.return_value = mock_conn
# Set up mock cursor fetchall response
today = date.today()
mock_cur.fetchall.return_value = [
# Format: email, first_name, product_name, expiration_date, expiring_soon_days
('user1@example.com', 'John', 'Laptop', today + timedelta(days=10), 30),
('user2@example.com', 'Jane', 'Phone', today + timedelta(days=20), 30),
('user3@example.com', None, 'Tablet', today + timedelta(days=5), 30)
]
# Call the function
result = get_expiring_warranties()
# Verify the SQL query is correct (simplified check)
self.assertTrue(mock_cur.execute.called)
args = mock_cur.execute.call_args[0]
self.assertIn('warranties w', args[0]) # Check if the query includes the warranties table
self.assertIn('users u', args[0]) # Check if the query includes the users table
self.assertIn('user_preferences up', args[0]) # Check if the query includes user_preferences
# Verify the result
self.assertEqual(len(result), 3) # Should return 3 warranties
# Check the first warranty
self.assertEqual(result[0]['email'], 'user1@example.com')
self.assertEqual(result[0]['first_name'], 'John')
self.assertEqual(result[0]['product_name'], 'Laptop')
self.assertEqual(result[0]['expiration_date'], (today + timedelta(days=10)).strftime('%Y-%m-%d'))
# Check that null first_name is handled correctly
self.assertEqual(result[2]['first_name'], 'User') # Default value for NULL
# Verify the connection is released
self.assertTrue(mock_conn.close.called)
def test_format_expiration_email(self):
"""Test that format_expiration_email correctly formats the email."""
# Test data
user = {
'first_name': 'John',
'email': 'john@example.com'
}
warranties = [
{
'product_name': 'Laptop',
'expiration_date': '2023-12-31'
},
{
'product_name': 'Phone',
'expiration_date': '2023-11-30'
}
]
# Call the function
email = format_expiration_email(user, warranties)
# Verify the email
self.assertEqual(email['Subject'], 'Warracker: Upcoming Warranty Expirations')
self.assertEqual(email['From'], 'notifications@warracker.com')
self.assertEqual(email['To'], 'john@example.com')
# Check both parts exist (plain text and HTML)
self.assertEqual(len(email.get_payload()), 2)
# Check content of plain text part
plain_text = email.get_payload(0).get_payload()
self.assertIn('Hello John', plain_text)
self.assertIn('Laptop (expires on 2023-12-31)', plain_text)
self.assertIn('Phone (expires on 2023-11-30)', plain_text)
# Check content of HTML part
html = email.get_payload(1).get_payload()
self.assertIn('Hello John', html)
self.assertIn('Laptop', html)
self.assertIn('2023-12-31', html)
self.assertIn('Phone', html)
self.assertIn('2023-11-30', html)
@patch('app.get_expiring_warranties')
@patch('smtplib.SMTP')
def test_send_expiration_notifications(self, mock_smtp, mock_get_expiring_warranties):
"""Test that send_expiration_notifications correctly sends emails."""
# Mock environment variables
with patch.dict(os.environ, {
'SMTP_HOST': 'test.example.com',
'SMTP_PORT': '587',
'SMTP_USERNAME': 'test@example.com',
'SMTP_PASSWORD': 'test_password'
}):
# Set up mock SMTP instance
mock_smtp_instance = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_smtp_instance
# Set up mock for expiring warranties
mock_get_expiring_warranties.return_value = [
{
'email': 'user1@example.com',
'first_name': 'John',
'product_name': 'Laptop',
'expiration_date': '2023-12-31'
},
{
'email': 'user1@example.com',
'first_name': 'John',
'product_name': 'Phone',
'expiration_date': '2023-11-30'
},
{
'email': 'user2@example.com',
'first_name': 'Jane',
'product_name': 'Tablet',
'expiration_date': '2023-10-15'
}
]
# Call the function
send_expiration_notifications()
# Verify SMTP is initialized with correct parameters
mock_smtp.assert_called_once_with('test.example.com', 587)
# Verify starttls and login are called
self.assertTrue(mock_smtp_instance.starttls.called)
mock_smtp_instance.login.assert_called_once_with('test@example.com', 'test_password')
# Verify sendmail is called twice (for two different users)
self.assertEqual(mock_smtp_instance.sendmail.call_count, 2)
# Verify first call to sendmail (for user1 with 2 warranties)
from_email, to_email, msg = mock_smtp_instance.sendmail.call_args_list[0][0]
self.assertEqual(from_email, 'test@example.com')
self.assertEqual(to_email, 'user1@example.com')
self.assertIn('Laptop', msg)
self.assertIn('Phone', msg)
# Verify second call to sendmail (for user2 with 1 warranty)
from_email, to_email, msg = mock_smtp_instance.sendmail.call_args_list[1][0]
self.assertEqual(from_email, 'test@example.com')
self.assertEqual(to_email, 'user2@example.com')
self.assertIn('Tablet', msg)
if __name__ == '__main__':
unittest.main()

View File

@@ -10,6 +10,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<!-- Stylesheets -->
<link rel="stylesheet" href="style.css"> <!-- Main stylesheet -->
@@ -23,6 +25,22 @@
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<script src="include-auth-new.js"></script> <!-- Handles auth state display -->
<script src="fix-auth-buttons-loader.js"></script> <!-- Fixes auth button display timing -->
<script src="auth.js"></script> <!-- Load auth.js early for authentication check -->
<!-- Authentication Check -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// Check if user is authenticated
const authToken = localStorage.getItem('auth_token');
const userInfo = localStorage.getItem('user_info');
if (!authToken || !userInfo) {
// User is not authenticated, redirect to login page
window.location.href = 'login.html';
return;
}
});
</script>
</head>
<body>
@@ -88,7 +106,14 @@
<div class="about-container" style="background-color: var(--card-bg); color: var(--text-color); padding: 20px; border-radius: 8px; margin-top: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); width: 100%; box-sizing: border-box;">
<h1 style="color: var(--primary-color); margin-bottom: 20px;">About Warracker</h1>
<p><strong>Version:</strong> v0.9.9.3</p>
<p><strong>Version:</strong> v0.9.9.4</p>
<div id="versionTracker" style="margin-bottom: 20px;">
<p><strong>Update Status:</strong> <span id="updateStatus">Checking for updates...</span></p>
<a id="updateLink" href="#" target="_blank" rel="noopener noreferrer" style="display: none; color: var(--link-color);">
<i class="fa-solid fa-arrow-up-right-from-square"></i> Release Notes
</a>
</div>
<p>
<strong>GitHub Repository:</strong>
@@ -137,6 +162,7 @@
<!-- Scripts loaded at the end of body -->
<script src="auth.js"></script> <!-- Added for user menu and other auth interactions -->
<script src="script.js"></script> <!-- Include main site script, might contain helpers -->
<script src="version-checker.js"></script> <!-- Version checker script -->
<!-- Explicitly call setupUIEventListeners for this page -->
<script>

View File

@@ -1,95 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Connection Test</title>
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<!-- Favicons -->
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="stylesheet" href="style.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
pre {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
overflow: auto;
max-height: 300px;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="container">
<div class="app-title">
<i class="fas fa-shield-alt"></i>
<h1>Warranty Tracker</h1>
</div>
<div class="nav-links">
<a href="index.html" class="nav-link">
<i class="fas fa-home"></i> Home
</a>
<a href="status.html" class="nav-link">
<i class="fas fa-chart-pie"></i> Status
</a>
</div>
</div>
</header>
<!-- Main Content -->
<div class="container">
<div class="content">
<h2><i class="fas fa-terminal"></i> API Connection Test</h2>
<div class="card">
<div class="card-header">
<h3>Testing API Connection</h3>
</div>
<div class="card-body">
<div id="apiTestResult">
<p>Checking API connection...</p>
</div>
<div class="debug-info">
<h4>Manual API Test</h4>
<p>You can also test the API manually with tools like curl:</p>
<pre>curl -X GET -H "Authorization: Bearer YOUR_AUTH_TOKEN" http://localhost:8005/api/warranties</pre>
</div>
<div class="actions">
<button onclick="location.reload()" class="btn">
<i class="fas fa-sync-alt"></i> Run Test Again
</button>
<a href="index.html" class="btn">
<i class="fas fa-home"></i> Back to Home
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Load test script -->
<script src="test-api.js"></script>
</body>
</html>

View File

@@ -121,6 +121,11 @@ function checkAuthState() {
* Update UI elements for authenticated user
*/
function updateUIForAuthenticatedUser() {
// Fire a global event so other scripts (like script.js) can react to authentication being ready
setTimeout(() => {
console.log('[auth.js] Dispatching authStateReady event');
window.dispatchEvent(new Event('authStateReady'));
}, 0);
console.log('auth.js: Updating UI for authenticated user');
// Log the user data being used

View File

@@ -182,16 +182,4 @@ document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded event triggered, updating auth buttons');
updateAuthButtons();
setupUserMenuDropdown();
// Set up periodic check (every 2 seconds)
setInterval(updateAuthButtons, 2000);
setInterval(setupUserMenuDropdown, 2000);
});
// Update auth buttons when localStorage changes
window.addEventListener('storage', (event) => {
if (event.key === 'auth_token' || event.key === 'user_info') {
console.log('Auth data changed, updating auth buttons');
updateAuthButtons();
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -7,107 +7,87 @@
console.log('include-auth-new.js: Running immediate auth check');
// Immediately check if user is logged in
if (localStorage.getItem('auth_token')) {
console.log('include-auth-new.js: Auth token found, hiding login/register buttons immediately');
// Create and inject CSS to hide auth buttons
var style = document.createElement('style');
style.textContent = `
#authContainer, .auth-buttons, a[href="login.html"], a[href="register.html"],
.login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn {
display: none !important;
visibility: hidden !important;
// Function to update UI based on auth state (extracted for reuse)
function updateAuthUI() {
if (localStorage.getItem('auth_token')) {
console.log('include-auth-new.js: Updating UI for authenticated user');
// Inject CSS to hide auth buttons and show user menu
const styleId = 'auth-ui-style';
let style = document.getElementById(styleId);
if (!style) {
style = document.createElement('style');
style.id = styleId;
document.head.appendChild(style);
}
#userMenu, .user-menu {
display: block !important;
visibility: visible !important;
}
`;
document.head.appendChild(style);
// Set the display style as soon as DOM is ready
window.addEventListener('DOMContentLoaded', function() {
console.log('include-auth-new.js: DOM loaded, ensuring buttons are hidden');
// Hide auth container
var authContainer = document.getElementById('authContainer');
if (authContainer) {
authContainer.style.display = 'none';
authContainer.style.visibility = 'hidden';
}
// Hide all login/register buttons
document.querySelectorAll('a[href="login.html"], a[href="register.html"], .login-btn, .register-btn, .auth-btn').forEach(function(button) {
button.style.display = 'none';
button.style.visibility = 'hidden';
});
// Show user menu
var userMenu = document.getElementById('userMenu');
if (userMenu) {
userMenu.style.display = 'block';
userMenu.style.visibility = 'visible';
}
// Update user information
style.textContent = `
#authContainer, .auth-buttons, a[href="login.html"], a[href="register.html"],
.login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn {
display: none !important;
visibility: hidden !important;
}
#userMenu, .user-menu {
display: block !important;
visibility: visible !important;
}
`;
// Update user info display elements immediately
try {
var userInfoStr = localStorage.getItem('user_info');
if (userInfoStr) {
var userInfo = JSON.parse(userInfoStr);
var displayName = userInfo.username || 'User';
var userDisplayName = document.getElementById('userDisplayName');
if (userDisplayName) {
userDisplayName.textContent = displayName;
}
if (userDisplayName) userDisplayName.textContent = displayName;
var userName = document.getElementById('userName');
if (userName) {
userName.textContent = (userInfo.first_name || '') + ' ' + (userInfo.last_name || '');
if (!userName.textContent.trim()) userName.textContent = userInfo.username || 'User';
}
var userEmail = document.getElementById('userEmail');
if (userEmail && userInfo.email) {
userEmail.textContent = userInfo.email;
}
if (userEmail && userInfo.email) userEmail.textContent = userInfo.email;
}
} catch (e) {
console.error('include-auth-new.js: Error updating user info:', e);
console.error('include-auth-new.js: Error updating user info display:', e);
}
});
} else {
console.log('include-auth-new.js: No auth token found, showing login/register buttons');
// Create and inject CSS to show auth buttons
var style = document.createElement('style');
style.textContent = `
#authContainer, .auth-buttons {
display: flex !important;
visibility: visible !important;
} else {
console.log('include-auth-new.js: Updating UI for logged-out user');
// Inject CSS to show auth buttons and hide user menu
const styleId = 'auth-ui-style';
let style = document.getElementById(styleId);
if (!style) {
style = document.createElement('style');
style.id = styleId;
document.head.appendChild(style);
}
a[href="login.html"], a[href="register.html"],
.login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn {
display: inline-block !important;
visibility: visible !important;
}
#userMenu, .user-menu {
display: none !important;
visibility: hidden !important;
}
`;
document.head.appendChild(style);
style.textContent = `
#authContainer, .auth-buttons {
display: flex !important; /* Use flex for container */
visibility: visible !important;
}
a[href="login.html"], a[href="register.html"],
.login-btn, .register-btn, .auth-btn.login-btn, .auth-btn.register-btn {
display: inline-block !important; /* Use inline-block for buttons */
visibility: visible !important;
}
#userMenu, .user-menu {
display: none !important;
visibility: hidden !important;
}
`;
}
}
// Listen for changes to localStorage
// Immediately check auth state and update UI
updateAuthUI();
// Listen for changes to localStorage and update UI without reloading
window.addEventListener('storage', function(event) {
if (event.key === 'auth_token' || event.key === 'user_info') {
console.log('include-auth-new.js: Auth data changed, reloading page to update UI');
window.location.reload();
console.log(`include-auth-new.js: Storage event detected for ${event.key}. Updating UI.`);
updateAuthUI(); // Update UI instead of reloading
// window.location.reload(); // <-- Keep commented out / Remove permanently
}
});

View File

@@ -20,6 +20,8 @@
<!-- Add new favicon links -->
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css">
<script src="theme-loader.js"></script> <!-- Apply theme early -->
@@ -167,7 +169,7 @@
<div class="search-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchWarranties" placeholder="Find products by name, notes or tag...">
<input type="text" id="searchWarranties" placeholder="Find by name, notes, tag, or serial number...">
<button id="clearSearch" class="clear-search-btn" title="Clear search" style="display: none;">
<i class="fas fa-times"></i>
</button>
@@ -297,6 +299,10 @@
<input type="text" name="serial_numbers[]" class="form-control" style="margin-bottom: 8px;" placeholder="Enter serial number">
</div>
</div>
<div class="form-group">
<label for="vendor">Vendor (Optional)</label>
<input type="text" id="vendor" name="vendor" class="form-control" placeholder="e.g. Amazon, Best Buy, etc.">
</div>
</div>
<!-- Warranty Details Tab -->
<div class="tab-content" id="warranty-details">
@@ -398,6 +404,10 @@
<span class="summary-label">Serial Numbers:</span>
<span id="summary-serial-numbers" class="summary-value">-</span>
</div>
<div class="summary-item">
<span class="summary-label">Vendor:</span>
<span id="summary-vendor" class="summary-value">-</span>
</div>
</div>
<div class="summary-section">
<h4><i class="fas fa-shield-alt"></i> Warranty Details</h4>
@@ -503,6 +513,10 @@
<!-- Serial number inputs will be added dynamically -->
</div>
</div>
<div class="form-group">
<label for="editVendor">Vendor (Optional)</label>
<input type="text" id="editVendor" name="vendor" class="form-control" placeholder="e.g. Amazon, Best Buy, etc.">
</div>
</div>
<!-- Warranty Details Tab -->

View File

@@ -11,6 +11,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css">
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<!-- Font Awesome for icons -->

15
frontend/manifest.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "Warracker",
"short_name": "Warracker",
"icons": [
{
"src": "img/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff"
}

View File

@@ -11,6 +11,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css">
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<!-- Font Awesome for icons -->
@@ -574,16 +576,9 @@
authMessage.className = 'auth-message';
authMessage.classList.add(type);
authMessage.style.display = 'block';
// Scroll to message
authMessage.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Check for dark mode preference
const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
if (darkModeEnabled) {
document.body.classList.add('dark-mode');
}
});
// Theme initialization

View File

@@ -8,6 +8,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css">
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<!-- Font Awesome for icons -->
@@ -210,11 +212,7 @@
authMessage.style.display = 'block';
}
// Check for dark mode preference
const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
if (darkModeEnabled) {
document.body.classList.add('dark-mode');
}
// No longer check for 'enabled' value. Theme is handled by theme-loader.js and below.
});
// Theme initialization

View File

@@ -8,6 +8,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css">
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<!-- Font Awesome for icons -->
@@ -398,11 +400,7 @@
}
}
// Check for dark mode preference
const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
if (darkModeEnabled) {
document.body.classList.add('dark-mode');
}
// No longer check for 'enabled' value. Theme is handled by theme-loader.js and below.
});
// Theme initialization

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<!-- Load the main site styles first -->
<link rel="stylesheet" href="style.css">
<!-- Font Awesome for icons -->
@@ -236,6 +238,23 @@
<input type="number" id="expiringSoonDays" class="form-control" min="1" max="365" value="30">
</div>
</div>
<div class="form-group">
<div class="preference-item">
<div>
<label for="dateFormat">Date Format</label>
<p class="text-muted">Choose how dates are displayed</p>
</div>
<select id="dateFormat" class="form-control">
<option value="MDY">Month/Day/Year (e.g., 12/31/2024)</option>
<option value="DMY">Day/Month/Year (e.g., 31/12/2024)</option>
<option value="YMD">Year-Month-Day (e.g., 2024-12-31)</option>
<option value="MDY_WORDS">Mon Day, Year (e.g., Dec 31, 2024)</option>
<option value="DMY_WORDS">Day Mon Year (e.g., 31 Dec 2024)</option>
<option value="YMD_WORDS">Year Mon Day (e.g., 2024 Dec 31)</option>
</select>
</div>
</div>
<button type="button" id="savePreferencesBtn" class="btn btn-primary">Save Preferences</button>
</form>

View File

@@ -47,6 +47,9 @@ const currencySymbolInput = document.getElementById('currencySymbol');
const currencySymbolSelect = document.getElementById('currencySymbolSelect');
const currencySymbolCustom = document.getElementById('currencySymbolCustom');
// Add dateFormatSelect near other DOM element declarations if not already there
const dateFormatSelect = document.getElementById('dateFormat');
/**
* Set theme (dark/light) - Unified and persistent
* @param {boolean} isDark - Whether to use dark mode
@@ -64,10 +67,10 @@ function setTheme(isDark) {
localStorage.setItem('darkMode', isDark);
// Sync both toggles if present
if (typeof darkModeToggle !== 'undefined' && darkModeToggle) {
darkModeToggle.checked = isDarkMode;
darkModeToggle.checked = isDark;
}
if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) {
darkModeToggleSetting.checked = isDarkMode;
darkModeToggleSetting.checked = isDark;
}
// Also update user_preferences.theme for backward compatibility
try {
@@ -87,23 +90,46 @@ function setTheme(isDark) {
* Initialize dark mode toggle and synchronize state
*/
function initDarkModeToggle() {
// Always check the single source of truth in localStorage
// Always check the single source of truth in localStorage (fallback)
const isDarkMode = localStorage.getItem('darkMode') === 'true';
// Apply theme to DOM if not already set
document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
document.body.classList.toggle('dark-mode', isDarkMode);
// Sync both toggles
// Sync both toggles and add unified handler
const syncToggles = (val) => {
if (typeof darkModeToggle !== 'undefined' && darkModeToggle) darkModeToggle.checked = val;
if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) darkModeToggleSetting.checked = val;
};
syncToggles(isDarkMode);
// Handler to update theme, localStorage, backend
const handleToggle = async function(checked) {
setTheme(checked);
syncToggles(checked);
// Save to backend if authenticated
if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
try {
let prefs = {};
const storedPrefs = localStorage.getItem('user_preferences');
if (storedPrefs) prefs = JSON.parse(storedPrefs);
prefs.theme = checked ? 'dark' : 'light';
await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${window.auth.getToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(prefs)
});
} catch (e) {
console.warn('Failed to save dark mode to backend:', e);
}
}
};
if (typeof darkModeToggle !== 'undefined' && darkModeToggle) {
darkModeToggle.checked = isDarkMode;
darkModeToggle.addEventListener('change', function() {
setTheme(this.checked);
});
darkModeToggle.onchange = function() { handleToggle(this.checked); };
}
if (typeof darkModeToggleSetting !== 'undefined' && darkModeToggleSetting) {
darkModeToggleSetting.checked = isDarkMode;
darkModeToggleSetting.addEventListener('change', function() {
setTheme(this.checked);
});
darkModeToggleSetting.onchange = function() { handleToggle(this.checked); };
}
}
@@ -126,6 +152,12 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize dark mode toggle
initDarkModeToggle();
// Clear dark mode preference on logout for privacy
if (window.auth && window.auth.onLogout) {
window.auth.onLogout(() => {
localStorage.removeItem('darkMode');
});
}
// REMOVED initSettingsMenu() call - Handled by auth.js
@@ -361,22 +393,34 @@ async function loadUserData() {
}
}
// Update localStorage
// Update localStorage ONLY if data has changed
const currentUser = window.auth.getCurrentUser();
let first_name = userData.first_name;
let last_name = userData.last_name;
if (!last_name) first_name = '';
if (!last_name) first_name = ''; // Reset first name if last name is empty
const updatedUser = {
...(currentUser || {}), // Handle case where currentUser might be null
first_name,
last_name,
// Ensure email and username are preserved if they existed
...(currentUser || {}), // Preserve existing fields
first_name, // Update first name
last_name, // Update last name
// Preserve other essential fields from fetched data if currentUser was null
email: currentUser ? currentUser.email : userData.email,
username: currentUser ? currentUser.username : userData.username,
is_admin: currentUser ? currentUser.is_admin : userData.is_admin,
id: currentUser ? currentUser.id : userData.id
};
localStorage.setItem('user_info', JSON.stringify(updatedUser));
// Convert both to JSON strings for reliable comparison
const currentUserString = JSON.stringify(currentUser);
const updatedUserString = JSON.stringify(updatedUser);
if (currentUserString !== updatedUserString) {
console.log('User data changed, updating localStorage.');
localStorage.setItem('user_info', updatedUserString);
} else {
console.log('User data from API matches localStorage, skipping update.');
}
// localStorage.setItem('user_info', JSON.stringify(updatedUser)); // OLD LINE
} else {
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response' }));
console.warn('API error fetching user data:', errorData.message);
@@ -425,198 +469,194 @@ function getPreferenceKeyPrefix() {
/**
* Load user preferences
*/
function loadPreferences() {
async function loadPreferences() {
console.log('Loading preferences...');
showLoading();
// Get the appropriate key prefix based on user type
const prefix = getPreferenceKeyPrefix();
console.log(`Loading preferences with prefix: ${prefix}`);
// First check if we're in dark mode by checking the body class
const isDarkMode = document.body.classList.contains('dark-mode');
// Set the toggle state based on the current theme
if (darkModeToggle) {
darkModeToggle.checked = isDarkMode;
}
if (darkModeToggleSetting) {
darkModeToggleSetting.checked = isDarkMode;
}
// --- BEGIN EDIT: Load Default View with Priority ---
let defaultViewLoaded = false;
if (defaultViewSelect) {
const userSpecificView = localStorage.getItem(`${prefix}defaultView`);
const generalView = localStorage.getItem('viewPreference');
const legacyWarrantyView = localStorage.getItem(`${prefix}warrantyView`); // Check legacy key
console.log('Loading preferences with prefix:', prefix);
if (userSpecificView) {
defaultViewSelect.value = userSpecificView;
defaultViewLoaded = true;
console.log(`Loaded default view from ${prefix}defaultView:`, userSpecificView);
} else if (generalView) {
defaultViewSelect.value = generalView;
defaultViewLoaded = true;
console.log('Loaded default view from viewPreference:', generalView);
} else if (legacyWarrantyView) {
defaultViewSelect.value = legacyWarrantyView;
defaultViewLoaded = true;
console.log(`Loaded default view from legacy ${prefix}warrantyView:`, legacyWarrantyView);
}
}
// --- END EDIT ---
// Load other preferences from localStorage using the appropriate prefix (only if default view not loaded yet)
if (!defaultViewLoaded) {
let darkModeFromAPI = null;
let apiPrefs = null;
// Try to load preferences from backend if authenticated
if (window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
try {
const userPrefs = localStorage.getItem(`${prefix}preferences`);
if (userPrefs) {
const preferences = JSON.parse(userPrefs);
// Default view preference (load only if not loaded above)
if (defaultViewSelect && preferences.default_view && !defaultViewLoaded) {
defaultViewSelect.value = preferences.default_view;
defaultViewLoaded = true; // Mark as loaded
console.log(`Loaded default view from ${prefix}preferences object:`, preferences.default_view);
const response = await fetch('/api/auth/preferences', {
headers: {
'Authorization': `Bearer ${window.auth.getToken()}`
}
// Email notifications preference
if (emailNotificationsToggle && typeof preferences.email_notifications !== 'undefined') { // Check for undefined
emailNotificationsToggle.checked = preferences.email_notifications;
}
// Expiring soon days preference
if (expiringSoonDaysInput && preferences.expiring_soon_days) {
expiringSoonDaysInput.value = preferences.expiring_soon_days;
}
// Notification frequency preference
if (notificationFrequencySelect && preferences.notification_frequency) {
notificationFrequencySelect.value = preferences.notification_frequency;
}
// Notification time preference
if (notificationTimeInput && preferences.notification_time) {
notificationTimeInput.value = preferences.notification_time;
}
// Timezone preference
if (timezoneSelect && preferences.timezone) {
timezoneSelect.value = preferences.timezone;
}
// Currency symbol preference
let symbol = '$';
if (preferences && preferences.currency_symbol) {
symbol = preferences.currency_symbol;
}
if (currencySymbolSelect && currencySymbolCustom) {
// If symbol is in the dropdown, select it; else, select 'other' and show custom
const found = Array.from(currencySymbolSelect.options).some(opt => opt.value === symbol);
if (found) {
currencySymbolSelect.value = symbol;
currencySymbolCustom.style.display = 'none';
currencySymbolCustom.value = '';
} else {
currencySymbolSelect.value = 'other';
currencySymbolCustom.style.display = '';
currencySymbolCustom.value = symbol;
}
});
if (response.ok) {
apiPrefs = await response.json();
if (apiPrefs && apiPrefs.theme) {
darkModeFromAPI = apiPrefs.theme === 'dark';
setTheme(darkModeFromAPI);
// Sync localStorage
localStorage.setItem('darkMode', darkModeFromAPI);
}
}
} catch (e) {
console.error('Error loading preferences from localStorage:', e);
// Fallback to default '$' using the correct elements
if (currencySymbolSelect) currencySymbolSelect.value = '$';
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
console.warn('Failed to load preferences from backend:', e);
}
} else {
// Fallback to default '$' if no localStorage and defaultViewLoaded is true
if (currencySymbolSelect) currencySymbolSelect.value = '$';
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
}
// Fallback: use localStorage if not authenticated or API fails
if (darkModeFromAPI === null) {
const storedDarkMode = localStorage.getItem('darkMode') === 'true';
setTheme(storedDarkMode);
}
// --- Load Date Format --- Add this section
const storedDateFormat = localStorage.getItem('dateFormat');
if (storedDateFormat && dateFormatSelect) {
dateFormatSelect.value = storedDateFormat;
console.log(`Loaded dateFormat from localStorage: ${storedDateFormat}`);
} else if (dateFormatSelect) {
dateFormatSelect.value = 'MDY'; // Default if not found
console.log('dateFormat not found in localStorage, defaulting to MDY');
}
// --- End Date Format Section ---
// Default View
const storedView = localStorage.getItem(`${prefix}defaultView`);
if (storedView && defaultViewSelect) {
defaultViewSelect.value = storedView;
console.log(`Loaded default view from ${prefix}defaultView: ${storedView}`);
} else if (defaultViewSelect) {
defaultViewSelect.value = 'grid'; // Default
console.log(`${prefix}defaultView not found, defaulting view to grid`);
}
// Load preferences from API (API data should override if available, except maybe for default view if already loaded)
fetch('/api/auth/preferences', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to load preferences from API');
}
return response.json();
})
.then(data => {
console.log('Preferences loaded from API:', data);
// API returns preferences directly, not nested under a 'preferences' key
const apiPrefs = data;
// Update UI with API preferences
// Default View: Only update if not already loaded from higher priority localStorage keys
if (defaultViewSelect && apiPrefs.default_view && !defaultViewLoaded) {
defaultViewSelect.value = apiPrefs.default_view;
console.log('Loaded default view from API:', apiPrefs.default_view);
}
// Other preferences always updated from API if available
if (emailNotificationsToggle && typeof apiPrefs.email_notifications !== 'undefined') { // Check for undefined
emailNotificationsToggle.checked = apiPrefs.email_notifications;
}
if (expiringSoonDaysInput && apiPrefs.expiring_soon_days) {
expiringSoonDaysInput.value = apiPrefs.expiring_soon_days;
}
if (notificationFrequencySelect && apiPrefs.notification_frequency) {
notificationFrequencySelect.value = apiPrefs.notification_frequency;
}
if (notificationTimeInput && apiPrefs.notification_time) {
notificationTimeInput.value = apiPrefs.notification_time;
}
if (timezoneSelect && apiPrefs.timezone) {
console.log('API provided timezone:', apiPrefs.timezone);
// Will be populated once timezones are loaded
setTimeout(() => {
if (timezoneSelect.options.length > 1) {
timezoneSelect.value = apiPrefs.timezone;
console.log('Applied timezone from API:', apiPrefs.timezone, 'Current select value:', timezoneSelect.value);
}
}, 500);
}
// Currency symbol from API
let apiSymbol = data.currency_symbol || '$';
if (currencySymbolSelect && currencySymbolCustom) {
const found = Array.from(currencySymbolSelect.options).some(opt => opt.value === apiSymbol);
if (found) {
currencySymbolSelect.value = apiSymbol;
currencySymbolCustom.style.display = 'none';
currencySymbolCustom.value = '';
// Currency Symbol
const storedCurrency = localStorage.getItem(`${prefix}currencySymbol`);
if (storedCurrency) {
if (currencySymbolSelect) {
// Check if the stored symbol is a standard option
const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === storedCurrency);
if (standardOption) {
currencySymbolSelect.value = storedCurrency;
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
} else {
// It's a custom symbol
currencySymbolSelect.value = 'other';
currencySymbolCustom.style.display = '';
currencySymbolCustom.value = apiSymbol;
if (currencySymbolCustom) {
currencySymbolCustom.value = storedCurrency;
currencySymbolCustom.style.display = 'inline-block';
}
}
console.log(`Loaded currency symbol from ${prefix}currencySymbol: ${storedCurrency}`);
}
// Store in localStorage with the appropriate prefix
localStorage.setItem(`${prefix}preferences`, JSON.stringify(apiPrefs));
})
.catch(error => {
console.error('Error loading preferences from API:', error);
})
.finally(() => {
hideLoading();
});
} else {
// Default to '$' if nothing stored
if (currencySymbolSelect) currencySymbolSelect.value = '$';
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
console.log(`${prefix}currencySymbol not found, defaulting to $`);
}
// Expiring Soon Days
const storedExpiringDays = localStorage.getItem(`${prefix}expiringSoonDays`);
if (storedExpiringDays && expiringSoonDaysInput) {
expiringSoonDaysInput.value = storedExpiringDays;
console.log(`Loaded expiring soon days from ${prefix}expiringSoonDays: ${storedExpiringDays}`);
} else if (expiringSoonDaysInput) {
expiringSoonDaysInput.value = 30; // Default
console.log(`${prefix}expiringSoonDays not found, defaulting to 30`);
}
// Now, try fetching preferences from API to override/confirm
if (window.auth && window.auth.isAuthenticated()) {
try {
const token = window.auth.getToken();
const response = await fetch('/api/auth/preferences', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const apiPrefs = await response.json();
console.log('Preferences loaded from API:', apiPrefs);
// Update UI elements with API data where available
if (apiPrefs.default_view && defaultViewSelect) {
// Only update if different from localStorage value (or if localStorage was empty)
const storedView = localStorage.getItem(`${prefix}defaultView`) || 'grid'; // Default if null
if (apiPrefs.default_view !== storedView) {
console.log(`API default_view (${apiPrefs.default_view}) differs from localStorage (${storedView}). Updating UI.`);
defaultViewSelect.value = apiPrefs.default_view;
}
}
// --- MODIFIED CURRENCY SYMBOL HANDLING ---
const storedCurrency = localStorage.getItem(`${prefix}currencySymbol`); // Get localStorage value again for comparison
if (apiPrefs.currency_symbol && currencySymbolSelect) {
// Only update UI from API if the API value is different from what was in localStorage
// Or if localStorage didn't have a value initially
if (!storedCurrency || apiPrefs.currency_symbol !== storedCurrency) {
console.log(`API currency_symbol (${apiPrefs.currency_symbol}) differs from localStorage (${storedCurrency}). Updating UI.`);
// Logic to handle standard vs custom symbol from API
const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === apiPrefs.currency_symbol);
if (standardOption) {
currencySymbolSelect.value = apiPrefs.currency_symbol;
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
} else {
currencySymbolSelect.value = 'other';
if (currencySymbolCustom) {
currencySymbolCustom.value = apiPrefs.currency_symbol;
currencySymbolCustom.style.display = 'inline-block';
}
}
} else {
console.log(`API currency_symbol (${apiPrefs.currency_symbol}) matches localStorage (${storedCurrency}). Skipping UI update.`);
}
}
// --- END MODIFIED CURRENCY SYMBOL HANDLING ---
if (apiPrefs.expiring_soon_days && expiringSoonDaysInput) {
// Only update if different from localStorage value (or if localStorage was empty)
const storedExpiringDays = localStorage.getItem(`${prefix}expiringSoonDays`) || '30'; // Default if null
if (String(apiPrefs.expiring_soon_days) !== storedExpiringDays) {
console.log(`API expiring_soon_days (${apiPrefs.expiring_soon_days}) differs from localStorage (${storedExpiringDays}). Updating UI.`);
expiringSoonDaysInput.value = apiPrefs.expiring_soon_days;
}
}
// --- Update Date Format from API Prefs --- Add this check
const storedDateFormat = localStorage.getItem('dateFormat') || 'MDY'; // Default if null
if (apiPrefs.date_format && dateFormatSelect) {
if (apiPrefs.date_format !== storedDateFormat) {
console.log(`API date_format (${apiPrefs.date_format}) differs from localStorage (${storedDateFormat}). Updating UI.`);
dateFormatSelect.value = apiPrefs.date_format;
}
}
// --- End Date Format Check ---
// Update Email Settings from API
if (emailNotificationsToggle) {
emailNotificationsToggle.checked = apiPrefs.email_notifications !== false; // Default true if null/undefined
}
if (notificationFrequencySelect && apiPrefs.notification_frequency) {
notificationFrequencySelect.value = apiPrefs.notification_frequency;
}
if (notificationTimeInput && apiPrefs.notification_time) {
notificationTimeInput.value = apiPrefs.notification_time.substring(0, 5); // HH:MM format
}
// Load and set timezone from API
if (timezoneSelect && apiPrefs.timezone) {
console.log('API provided timezone:', apiPrefs.timezone);
// Ensure the option exists before setting
if (Array.from(timezoneSelect.options).some(option => option.value === apiPrefs.timezone)) {
timezoneSelect.value = apiPrefs.timezone;
console.log('Applied timezone from API:', timezoneSelect.value, 'Current select value:', timezoneSelect.value);
} else {
console.warn(`Timezone '${apiPrefs.timezone}' from API not found in dropdown.`);
}
} else {
console.log('No timezone preference found in API or timezone select element missing.');
}
} else {
const errorData = await response.json().catch(() => ({}));
console.warn(`Failed to load preferences from API: ${response.status}`, errorData.message || '');
}
} catch (error) {
console.error('Error fetching preferences from API:', error);
}
}
}
/**
@@ -945,109 +985,80 @@ async function saveProfile() {
/**
* Save user preferences
*/
function savePreferences() {
showLoading();
try {
// Get the appropriate key prefix based on user type
const prefix = getPreferenceKeyPrefix();
console.log(`Saving preferences with prefix: ${prefix}`);
// Get values
const isDarkMode = darkModeToggleSetting.checked;
const defaultView = defaultViewSelect.value;
const emailNotifications = emailNotificationsToggle.checked;
const expiringSoonDays = parseInt(expiringSoonDaysInput.value);
const notificationFrequency = notificationFrequencySelect.value;
const notificationTime = notificationTimeInput.value;
const timezone = timezoneSelect.value;
let currencySymbol = '$';
if (currencySymbolSelect && currencySymbolSelect.value === 'other') {
currencySymbol = currencySymbolCustom && currencySymbolCustom.value ? currencySymbolCustom.value : '$';
} else if (currencySymbolSelect) {
async function savePreferences() {
console.log('Saving preferences...');
const prefix = getPreferenceKeyPrefix();
// --- Prepare data to save --- Add dateFormat and dark mode
const preferencesToSave = {
default_view: defaultViewSelect ? defaultViewSelect.value : 'grid',
expiring_soon_days: expiringSoonDaysInput ? parseInt(expiringSoonDaysInput.value) : 30,
date_format: dateFormatSelect ? dateFormatSelect.value : 'MDY',
theme: (localStorage.getItem('darkMode') === 'true') ? 'dark' : 'light',
};
// Handle currency symbol (standard or custom)
let currencySymbol = '$'; // Default
if (currencySymbolSelect) {
if (currencySymbolSelect.value === 'other' && currencySymbolCustom) {
currencySymbol = currencySymbolCustom.value.trim() || '$'; // Use custom or default to $ if empty
} else {
currencySymbol = currencySymbolSelect.value;
}
// Validate inputs
if (isNaN(expiringSoonDays) || expiringSoonDays < 1 || expiringSoonDays > 365) {
showToast('Expiring soon days must be between 1 and 365', 'error');
}
preferencesToSave.currency_symbol = currencySymbol;
// --- End data preparation ---
// +++ ADDED DEBUG LOGGING +++
console.log(`[SavePrefs Debug] Currency Select Value: ${currencySymbolSelect ? currencySymbolSelect.value : 'N/A'}`);
console.log(`[SavePrefs Debug] Custom Input Value: ${currencySymbolCustom ? currencySymbolCustom.value : 'N/A'}`);
console.log(`[SavePrefs Debug] Final currencySymbol value determined: ${currencySymbol}`);
// +++ END DEBUG LOGGING +++
// Save Dark Mode separately (using the single source of truth)
const isDark = darkModeToggleSetting ? darkModeToggleSetting.checked : false;
setTheme(isDark);
console.log(`Saved dark mode: ${isDark}`);
// Save simple preferences to localStorage immediately
localStorage.setItem('dateFormat', preferencesToSave.date_format); // Added
localStorage.setItem(`${prefix}defaultView`, preferencesToSave.default_view);
localStorage.setItem(`${prefix}currencySymbol`, preferencesToSave.currency_symbol);
localStorage.setItem(`${prefix}expiringSoonDays`, preferencesToSave.expiring_soon_days);
console.log('Preferences saved to localStorage (prefix:', prefix, '):', preferencesToSave);
console.log(`Value of dateFormat in localStorage: ${localStorage.getItem('dateFormat')}`);
// Try saving to API
if (window.auth && window.auth.isAuthenticated()) {
try {
showLoading();
const token = window.auth.getToken();
const response = await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(preferencesToSave)
});
hideLoading();
return;
}
if (!timezone) {
showToast('Please select a timezone', 'error');
hideLoading();
return;
}
// Create preferences object
const preferences = {
theme: isDarkMode ? 'dark' : 'light',
default_view: defaultView,
email_notifications: emailNotifications,
expiring_soon_days: expiringSoonDays,
notification_frequency: notificationFrequency,
notification_time: notificationTime,
timezone: timezone,
currency_symbol: currencySymbol
};
// Save to API
fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('auth_token'),
'Content-Type': 'application/json'
},
body: JSON.stringify(preferences)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to save preferences');
if (response.ok) {
showToast('Preferences saved successfully.', 'success');
console.log('Preferences successfully saved to API.');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Failed to save preferences to API: ${response.status}`);
}
return response.json();
})
.then(data => {
// Save to localStorage with the appropriate prefix
localStorage.setItem(`${prefix}preferences`, JSON.stringify(data));
// Also save individual preferences for backward compatibility and general preference
localStorage.setItem(`${prefix}defaultView`, defaultView);
localStorage.setItem(`${prefix}warrantyView`, defaultView); // Keep saving legacy key for now
localStorage.setItem(`${prefix}emailNotifications`, emailNotifications);
localStorage.setItem('viewPreference', defaultView); // --- EDIT: Save general key ---
// Save the dark mode setting for the current user type
localStorage.setItem(`${prefix}darkMode`, isDarkMode);
// Apply theme immediately
document.body.classList.toggle('dark-mode', isDarkMode);
showToast('Preferences saved successfully', 'success');
})
.catch(error => {
console.error('Error saving preferences:', error);
showToast('Error saving preferences', 'error');
// Save to localStorage as fallback with the appropriate prefix
localStorage.setItem(`${prefix}theme`, isDarkMode ? 'dark' : 'light');
localStorage.setItem(`${prefix}defaultView`, defaultView);
localStorage.setItem(`${prefix}warrantyView`, defaultView); // Keep saving legacy key for now
localStorage.setItem(`${prefix}emailNotifications`, emailNotifications);
localStorage.setItem(`${prefix}darkMode`, isDarkMode);
localStorage.setItem('viewPreference', defaultView); // --- EDIT: Save general key ---
// Apply theme even if API save fails
document.body.classList.toggle('dark-mode', isDarkMode);
})
.finally(() => {
} catch (error) {
hideLoading();
});
} catch (error) {
console.error('Error in savePreferences:', error);
showToast('Error saving preferences', 'error');
hideLoading();
console.error('Error saving preferences to API:', error);
showToast(`Preferences saved locally, but failed to sync with server: ${error.message}`, 'warning');
}
} else {
// No auth, just show local save success
showToast('Preferences saved locally.', 'success');
}
}
@@ -2910,31 +2921,80 @@ function saveEmailSettings() {
// --- Add Storage Event Listener for Real-time Sync ---
window.addEventListener('storage', (event) => {
const prefix = getPreferenceKeyPrefix();
const viewKeys = [
`${prefix}defaultView`,
'viewPreference',
`${prefix}warrantyView`,
// Add `${prefix}viewPreference` if still used/relevant
`${prefix}viewPreference`
];
console.log(`[Settings Storage Listener] Event received: key=${event.key}, newValue=${event.newValue}`); // Log all events
if (viewKeys.includes(event.key) && event.newValue) {
console.log(`Storage event detected for view preference (${event.key}) in settings. New value: ${event.newValue}`);
// Ensure the dropdown element exists and the value is different
if (defaultViewSelect && defaultViewSelect.value !== event.newValue) {
// Check if the new value is a valid option in the select
const prefix = getPreferenceKeyPrefix();
const targetKey = `${prefix}defaultView`;
// Only react to changes in the specific default view key for the current user type
if (event.key === targetKey) { // Check key match first
console.log(`[Settings Storage Listener] Matched key: ${targetKey}`);
console.log(`[Settings Storage Listener] defaultViewSelect exists: ${!!defaultViewSelect}`);
if (defaultViewSelect) {
console.log(`[Settings Storage Listener] Current dropdown value: ${defaultViewSelect.value}`);
}
console.log(`[Settings Storage Listener] Event newValue: ${event.newValue}`);
if (event.newValue && defaultViewSelect && defaultViewSelect.value !== event.newValue) {
console.log(`[Settings Storage Listener] Value changed and dropdown exists. Checking options...`);
const optionExists = [...defaultViewSelect.options].some(option => option.value === event.newValue);
console.log(`[Settings Storage Listener] Option ${event.newValue} exists: ${optionExists}`);
if (optionExists) {
defaultViewSelect.value = event.newValue;
console.log('Updated settings default view dropdown.');
console.log(`[Settings Storage Listener] SUCCESS: Updated settings default view dropdown via storage event to ${event.newValue}.`);
} else {
console.warn(`Storage event value (${event.newValue}) not found in dropdown options.`);
console.warn(`[Settings Storage Listener] Storage event value (${event.newValue}) not found in dropdown options.`);
}
} else if (defaultViewSelect) {
console.log('Storage event value matches current dropdown selection, ignoring.');
} else if (!event.newValue) {
console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because newValue is null/empty.`);
} else if (!defaultViewSelect) {
console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because defaultViewSelect element not found.`);
} else {
console.log(`[Settings Storage Listener] Ignoring event for ${targetKey} because value hasn't changed (${defaultViewSelect.value} === ${event.newValue}).`);
}
}
// Add similar checks for other preferences if needed, e.g., dateFormat, currencySymbol
if (event.key === 'dateFormat') { // Simplified log for other keys
console.log(`[Settings Storage Listener] dateFormat changed to ${event.newValue}`);
if (event.newValue && dateFormatSelect && dateFormatSelect.value !== event.newValue) {
const optionExists = [...dateFormatSelect.options].some(option => option.value === event.newValue);
if (optionExists) {
dateFormatSelect.value = event.newValue;
console.log('[Settings Storage Listener] Updated settings date format dropdown via storage event.');
}
}
}
if (event.key === `${prefix}currencySymbol`) { // Simplified log for other keys
console.log(`[Settings Storage Listener] ${prefix}currencySymbol changed to ${event.newValue}`);
if (event.newValue && currencySymbolSelect && currencySymbolSelect.value !== event.newValue) {
// Handle standard vs custom symbol update
const standardOption = Array.from(currencySymbolSelect.options).find(opt => opt.value === event.newValue);
if (standardOption) {
currencySymbolSelect.value = event.newValue;
if (currencySymbolCustom) currencySymbolCustom.style.display = 'none';
console.log('[Settings Storage Listener] Updated settings currency dropdown via storage event.');
} else if (currencySymbolSelect.value !== 'other' || (currencySymbolCustom && currencySymbolCustom.value !== event.newValue)) {
currencySymbolSelect.value = 'other';
if (currencySymbolCustom) {
currencySymbolCustom.value = event.newValue;
currencySymbolCustom.style.display = 'inline-block';
}
console.log('[Settings Storage Listener] Updated settings currency dropdown to custom via storage event.');
}
}
}
// Add check for expiringSoonDays
if (event.key === `${prefix}expiringSoonDays`) { // Simplified log for other keys
console.log(`[Settings Storage Listener] ${prefix}expiringSoonDays changed to ${event.newValue}`);
if (event.newValue && expiringSoonDaysInput && expiringSoonDaysInput.value !== event.newValue) {
expiringSoonDaysInput.value = event.newValue;
console.log('[Settings Storage Listener] Updated settings expiring soon days input via storage event.');
}
}
});
// --- End Storage Event Listener ---

View File

@@ -327,20 +327,16 @@ async function saveProfile() {
* Save user preferences
*/
function savePreferences() {
// Save dark mode preference
const isDark = darkModeToggleSetting.checked;
// Save dark mode
const isDark = darkModeToggle.checked;
setTheme(isDark);
darkModeToggle.checked = isDark;
// Save default view preference
const defaultView = defaultViewSelect.value;
localStorage.setItem('defaultView', defaultView);
localStorage.setItem('warrantyView', defaultView);
// Save email notifications preference
const emailNotifications = emailNotificationsToggle.checked;
localStorage.setItem('emailNotifications', emailNotifications);
// Save default view
localStorage.setItem('defaultView', defaultViewSelect.value);
// Save email notifications
localStorage.setItem('emailNotifications', emailNotificationsToggle.checked);
showToast('Preferences saved successfully', 'success');
}

View File

@@ -17,6 +17,8 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
<link rel="apple-touch-icon" sizes="180x180" href="img/favicon-512x512.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css">
<script src="theme-loader.js"></script> <!-- Apply theme early -->
<!-- Font Awesome for icons -->
@@ -27,6 +29,7 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Load fix for auth buttons -->
<script src="fix-auth-buttons-loader.js"></script>
<script data-cfasync="false" src="/javascript.js"></script> <!-- Cloudflare compatibility -->
<style>
.user-menu {
position: relative;

View File

@@ -1,59 +0,0 @@
// API connection test script
document.addEventListener('DOMContentLoaded', function() {
// Element to display test results
const resultElement = document.getElementById('apiTestResult');
if (resultElement) {
resultElement.innerHTML = '<p>Checking API connection...</p>';
// Check for authentication token
const token = localStorage.getItem('auth_token');
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
// Add auth token if available
if (token) {
options.headers['Authorization'] = `Bearer ${token}`;
}
// Test API connection
fetch('/api/warranties', options)
.then(response => {
if (!response.ok) {
throw new Error('API responded with status: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('API response:', data);
resultElement.innerHTML = `
<div class="alert alert-success">
<h4>✅ API Connection Successful</h4>
<p>The API responded with ${data && Array.isArray(data) ? data.length : 0} warranties.</p>
<pre>${JSON.stringify(data && data.length ? data[0] : {}, null, 2)}</pre>
</div>
`;
})
.catch(error => {
console.error('API connection error:', error);
resultElement.innerHTML = `
<div class="alert alert-danger">
<h4>❌ API Connection Failed</h4>
<p>Error: ${error.message}</p>
<h5>Debug Information:</h5>
<ul>
<li>Browser URL: ${window.location.href}</li>
<li>Target API: /api/warranties</li>
<li>Authentication: ${token ? 'Token provided' : 'No token available'}</li>
<li>Make sure the backend is running and accessible</li>
<li>Check that nginx is properly configured to proxy API requests</li>
</ul>
</div>
`;
});
}
});

View File

@@ -1 +0,0 @@
console.log('Testing authentication status:', localStorage.getItem('auth_token'));

View File

@@ -0,0 +1,53 @@
// Version checker for Warracker
document.addEventListener('DOMContentLoaded', () => {
const currentVersion = '0.9.9.4'; // Current version of the application
const updateStatus = document.getElementById('updateStatus');
const updateLink = document.getElementById('updateLink');
// Function to compare versions
function compareVersions(v1, v2) {
const v1Parts = v1.split('.').map(Number);
const v2Parts = v2.split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
}
// Function to check for updates
async function checkForUpdates() {
try {
const response = await fetch('https://api.github.com/repos/sassanix/Warracker/releases/latest');
if (!response.ok) throw new Error('Failed to fetch release information');
const data = await response.json();
const latestVersion = data.tag_name.replace('v', ''); // Remove 'v' prefix if present
const comparison = compareVersions(latestVersion, currentVersion);
if (comparison > 0) {
// New version available
updateStatus.textContent = `New version ${data.tag_name} available!`;
updateStatus.style.color = 'var(--success-color)';
updateLink.href = data.html_url;
updateLink.style.display = 'inline-block';
} else {
// Up to date
updateStatus.textContent = 'You are using the latest version';
updateStatus.style.color = 'var(--success-color)';
}
} catch (error) {
console.error('Error checking for updates:', error);
updateStatus.textContent = 'Failed to check for updates';
updateStatus.style.color = 'var(--error-color)';
}
}
// Check for updates when the page loads
checkForUpdates();
});

View File

@@ -1,34 +0,0 @@
-- Add notification preferences columns to user_preferences table
-- Check if notification_frequency column exists
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'user_preferences' AND column_name = 'notification_frequency'
) THEN
ALTER TABLE user_preferences ADD COLUMN notification_frequency VARCHAR(10) NOT NULL DEFAULT 'daily';
END IF;
END $$;
-- Check if notification_time column exists
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'user_preferences' AND column_name = 'notification_time'
) THEN
ALTER TABLE user_preferences ADD COLUMN notification_time VARCHAR(5) NOT NULL DEFAULT '09:00';
END IF;
END $$;
-- Add indexes for the new columns
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_preferences' AND indexname = 'idx_user_preferences_notification'
) THEN
CREATE INDEX idx_user_preferences_notification ON user_preferences(notification_frequency, notification_time);
END IF;
END $$;

View File

@@ -1,156 +0,0 @@
#!/usr/bin/env python3
import os
import glob
import psycopg2
import logging
import time
import sys
import importlib.util
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger(__name__)
def get_db_connection(max_attempts=5, attempt_delay=5):
"""Get a connection to the PostgreSQL database with retry logic"""
for attempt in range(1, max_attempts + 1):
try:
logger.info(f"Attempting to connect to database (attempt {attempt}/{max_attempts})")
# Get connection details from environment variables or use defaults
db_host = os.environ.get('DB_HOST', 'localhost')
db_port = os.environ.get('DB_PORT', '5432')
db_name = os.environ.get('DB_NAME', 'warranty_db')
db_user = os.environ.get('DB_USER', 'warranty_user')
db_password = os.environ.get('DB_PASSWORD', 'warranty_password')
conn = psycopg2.connect(
host=db_host,
port=db_port,
dbname=db_name,
user=db_user,
password=db_password
)
# Set autocommit to False for transaction control
conn.autocommit = False
logger.info("Database connection successful")
return conn
except Exception as e:
logger.error(f"Database connection error (attempt {attempt}/{max_attempts}): {e}")
if attempt < max_attempts:
logger.info(f"Retrying in {attempt_delay} seconds...")
time.sleep(attempt_delay)
else:
logger.error("Maximum connection attempts reached. Could not connect to database.")
raise
def load_python_migration(file_path):
"""Load a Python migration module dynamically"""
module_name = os.path.basename(file_path).replace('.py', '')
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def apply_migrations():
"""Apply all SQL and Python migration files in the migrations directory"""
conn = None
try:
conn = get_db_connection()
cur = conn.cursor()
# Create migrations table if it doesn't exist
cur.execute("""
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
# Get list of migration files (both SQL and Python)
migration_dir = os.path.dirname(os.path.abspath(__file__))
sql_files = sorted(glob.glob(os.path.join(migration_dir, '*.sql')))
py_files = sorted(glob.glob(os.path.join(migration_dir, '*.py')))
# Filter out this script itself
py_files = [f for f in py_files if os.path.basename(f) != 'apply_migrations.py']
# Combine and sort all migration files by name
all_migration_files = sorted(sql_files + py_files)
if not all_migration_files:
logger.info("No migration files found.")
return
# Get list of already applied migrations
cur.execute("SELECT filename FROM migrations")
applied_migrations = set([row[0] for row in cur.fetchall()])
# Apply each migration file if not already applied
for migration_file in all_migration_files:
filename = os.path.basename(migration_file)
if filename in applied_migrations:
logger.info(f"Migration {filename} already applied, skipping.")
continue
logger.info(f"Applying migration: {filename}")
try:
if migration_file.endswith('.sql'):
# Apply SQL migration
with open(migration_file, 'r') as f:
sql = f.read()
cur.execute(sql)
elif migration_file.endswith('.py'):
# Apply Python migration
migration_module = load_python_migration(migration_file)
if hasattr(migration_module, 'upgrade'):
migration_module.upgrade(cur)
else:
logger.warning(f"Python migration {filename} does not have an upgrade function, skipping.")
continue
# Record the migration as applied
cur.execute(
"INSERT INTO migrations (filename) VALUES (%s)",
(filename,)
)
conn.commit()
logger.info(f"Migration {filename} applied successfully")
except Exception as e:
conn.rollback()
logger.error(f"Error applying migration {filename}: {e}")
if migration_file.endswith('.sql'):
logger.error(f"\nQUERY: {sql}\n")
raise
except Exception as e:
logger.error(f"Migration error: {e}")
if conn:
conn.rollback()
raise
finally:
if conn:
conn.close()
if __name__ == "__main__":
try:
apply_migrations()
logger.info("Migrations completed successfully")
except Exception as e:
logger.error(f"Migration process failed: {e}")
sys.exit(1)

View File

@@ -1,13 +0,0 @@
#!/bin/bash
# Display script execution
set -x
# Change to the migrations directory
cd /app/migrations
# Run the migration script
python3 apply_migrations.py
# Return the exit code
exit $?

View File

@@ -1,8 +1,9 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
# Point root to the frontend directory where static assets reside
root d:/Project/warracker/Warracker Beta/frontend;
root /d/Project/warracker/Warracker Beta/frontend;
index index.html;
# Enable detailed error logging

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1 +0,0 @@
Test content for debugging

View File

@@ -1 +0,0 @@
test content