Removed vite

Removed vite, and consolidated files to frontend folder
This commit is contained in:
sassanix
2025-11-13 09:22:59 -04:00
parent 62a04e2993
commit 23028fe696
56 changed files with 1954 additions and 2743 deletions

View File

@@ -2,55 +2,65 @@
## 1.0.3 - 2025-11-07
### Added
- **Vite build tool (multi-page app) for the frontend**
- Initialized npm in `frontend/` with `dev`, `build`, and `preview` scripts.
- Introduced `vite.config.js` with `root: 'src'`, `outDir: '../dist'`, `publicDir: 'public'`, a development `/api` proxy, and explicit multi-page HTML inputs.
- Restructured frontend: moved sources to `frontend/src/` and static/non-module assets to `frontend/public/`.
- _Files: `frontend/package.json`, `frontend/vite.config.js`, `frontend/src/**/*`, `frontend/public/**/*`_
- **Modular Entry Orchestrator and Feature Modules**
- Added a lean ES module entry orchestrator `index.js` that initializes auth and feature modules after `authStateReady`.
- Created feature-specific modules to prepare deconstruction of the monolith while preserving identical behavior:
- `warrantyListController.js` (list loading/filtering/rendering controller surface)
- `tagManager.js` (tag management: load/render/create/update/delete)
- `addWarrantyForm.js` (multi-step add warranty wizard surface)
- Wired pages to load the new module entry with `<script type="module" src="./js/index.js"></script>` alongside legacy `script.js` for zero-regression rollout.
- _Files: `frontend/js/index.js`, `frontend/js/controllers/warrantyListController.js`, `frontend/js/components/tagManager.js`, `frontend/js/components/addWarrantyForm.js`, `frontend/index.html`, `frontend/about.html`, `frontend/status.html`, `frontend/register.html`, `frontend/reset-password.html`, `frontend/reset-password-request.html`_
- **Centralized Authentication Service**
- New module `authService.js` exposes `initAuth`, `login`, `logout`, `getToken`, `getCurrentUser`, `isAuthenticated`.
- Backwards compatibility: attaches a `window.auth` facade so existing code continues to work.
- Handles header UI state, user menu/settings dropdown toggling, and dispatches a unified `authStateReady` event.
- Robustly hides “Create Account” UI when registration is disabled (with a MutationObserver to keep it hidden after other scripts mutate the DOM).
- _Files: `frontend/src/js/services/authService.js`, `frontend/src/*.html` (module includes)_
- _Files: `frontend/js/services/authService.js`, `frontend/*.html` (module includes)_
- **Dedicated API Service**
- New `apiService.js` provides a `baseRequest` wrapper that injects the `Authorization` header and normalizes error handling.
- Exposes named helpers (e.g., `getWarranties`, `updateWarranty`, `deleteWarranty`, `getStatistics`, `savePreferences`) and a `window.api` shim for non-module scripts.
- Safe global `fetch` shim adds the Authorization header for `/api/...` calls when missing.
- _Files: `frontend/src/js/services/apiService.js`, `frontend/src/*.html` (module includes)_
- _Files: `frontend/js/services/apiService.js`, `frontend/*.html` (module includes)_
- **Central State Management Store**
- Introduced a `store.js` module to act as a single source of truth for application data (warranties, filters, loading states), eliminating scattered global variables.
- UI updates are now driven by custom events, creating a predictable data flow.
- _Files: `frontend/src/js/store.js`_
- _Files: `frontend/js/store.js`_
- **Component-Based UI Modules**
- Created a suite of new, reusable UI component modules to replace HTML string concatenation. Each is responsible for building a specific part of the UI via safe DOM manipulation.
- Modules include: `warrantyCard.js`, `tag.js`, `modals.js`, `editModal.js`, `claims.js`, `notes.js`, `paperless.js`, and `ui.js`.
- _Files: `frontend/src/js/components/*`_
- _Files: `frontend/js/components/*`_
- **HTML `<template>` for Warranty Cards**
- Added a `<template id="warranty-card-template">` to `index.html` to define the warranty card structure, separating markup from rendering logic for improved performance and maintainability.
- _Files: `frontend/src/index.html`_
- _Files: `frontend/index.html`_
### Enhanced
- **Dockerfile: multi-stage frontend build + runtime serving**
- Added `frontend-build` stage using `node:20-alpine` to run `npm ci && npm run build`.
- Final stage copies `frontend/dist/` to `/var/www/html` and includes `frontend/public/` assets in the build.
- Unpinned Node digest to resolve `not found` errors on some hosts.
- **Dockerfile: simplified static file serving**
- Removed build stage; frontend files are now copied directly to `/var/www/html/` for static serving.
- No build step required; all frontend assets are served as-is.
- _Files: `Dockerfile`_
- **HTML and asset loading**
- Switched all page `<script>` and `<link>` paths to root-relative (e.g., `/script.js`).
- Switched all page `<script>` and `<link>` paths to relative paths (e.g., `./script.js`) for static file serving.
- Ensured `script.js` is loaded with `defer` across pages to avoid early DOM access issues.
- Moved non-module assets (e.g., `i18n.js`, `i18next*.js`, `script.js`, `theme-loader.js`, etc.) to `frontend/public/` so they are served verbatim in production.
- _Files: `frontend/src/*.html`, `frontend/public/**/*`_
- All assets are served directly from the `frontend/` directory without a build step.
- _Files: `frontend/*.html`, `frontend/**/*`_
- **Build stability**
- Moved `settings-styles.css` to `public/` to bypass a PostCSS parse error in production builds (no functional/visual changes).
- _Files: `frontend/public/settings-styles.css`_
- **Frontend structure**
- Consolidated all frontend files into a single `frontend/` directory for simplified static serving.
- _Files: `frontend/settings-styles.css`_
- **Global State Management Refactor**
- Migrated `frontend/script.js` to read/write state exclusively via the centralized `window.store` API.
- Replaced direct usages of `warranties`, `currentFilters`, and `allTags` with `store.getWarranties/setWarranties`, `store.getFilters/updateFilter`, and `store.getAllTags/setAllTags`.
- Updated filter event listeners, export flow, tag management (create/update/delete), and the filtering/rendering pipeline (`applyFilters`, `renderWarranties`) to consume store-backed data.
- `loadTags()` now persists tags to the store and dispatches events to refresh dependent UIs.
- Replaced remaining direct reads of removed globals with store getters in `script.js` (e.g., card action handlers, `filterWarranties()`, `openClaimsModal()`, and notes modal handlers). App behavior unchanged; all reads/writes now flow through `window.store`.
- _Files: `frontend/script.js`_
- **Major UI Refactor for Maintainability & Security**
- Replaced nearly all `innerHTML` assignments with a robust, component-based rendering approach. This makes the UI easier to debug, maintain, and more secure against XSS vulnerabilities.
@@ -58,39 +68,113 @@
- **Edit Modal:** The complex dynamic sections (serial numbers, current document displays for invoices/manuals/photos, and tag selection) are now built with the `editModal.js` component.
- **Claims Modal:** The entire claims view, including the header, loading/error states, and claims list, is now rendered by the `claims.js` component.
- **Other Modals:** Refactored the rendering logic for the Tag Management, Notes, and Paperless-ngx document browser modals to use their respective new components.
- _Files: `frontend/src/script.js`, `frontend/src/js/components/*`_
- _Files: `frontend/script.js`, `frontend/js/components/*`_
- **Begin Extraction of UI Event Listeners**
- Moved the global UI event initialization (`setupUIEventListeners`) into `warrantyListController.initEventListeners()` as part of the modularization.
- Legacy `frontend/script.js` delegates to the controller to attach listeners, maintaining identical behavior during the staged rollout.
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
- **Controller-based Filter Pipeline**
- Migrated `applyFilters` logic into `warrantyListController.js` to centralize filtering while preserving identical behavior.
- Legacy `applyFilters` in `frontend/script.js` now delegates to the controller to avoid duplication.
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
- **Filter Dropdowns and Preference Persistence Extraction**
- Moved filter dropdown population functions (`populateTagFilter`, `populateVendorFilter`, `populateWarrantyTypeFilter`) and preference persistence (`saveFilterPreferences`, `loadFilterAndSortPreferences`) into `warrantyListController.js`.
- Legacy functions in `frontend/script.js` now delegate to the controller, ensuring a single source of truth.
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
- **Read-only State Bridges for Module Access**
- Exposed read-only getters on `window` for `currentView`, `lastLoadedArchived`, and `lastLoadedIncludesArchived` to allow module code to read legacy flags safely without changing their source of truth.
- _Files: `frontend/script.js`_
- **Delegated List Actions Fallback in Controller**
- Added robust delegated handlers in `warrantyListController.initEventListeners()` for `#warrantiesList` (claims, edit, delete, notes, archive/unarchive) that activate only when the legacy initializer is not present, preventing double bindings.
- _Files: `frontend/js/controllers/warrantyListController.js`_
- **Controller-based Data Loader (`loadWarranties`)**
- Migrated the full `loadWarranties` flow into `warrantyListController.js` while preserving behavior: authentication check, scope selection (Personal/Global), Archived vs All handling with archived merge, processing via `processWarrantyData`, filter dropdown repopulation, and final render through `applyFilters()`.
- Legacy `frontend/script.js` now delegates `loadWarranties` to the controller.
- _Files: `frontend/js/controllers/warrantyListController.js`, `frontend/script.js`_
- **Warranty List Rendering (component-based refactor, public bundle)**
- Rewrote `renderWarranties` in `frontend/script.js` to stop concatenating HTML strings and instead delegate card creation to `window.components.createWarrantyCard(warranty, options)`.
- Builds `actionsHtml`, `contentHtml`, `statusText/statusClass`, and view-specific root class via helpers; appends tags using `window.components.appendTags`; preserves document links row placement per view.
- Uses `DocumentFragment` to batch DOM insertions for performance and `window.components.ui.renderEmptyState` for empty states.
- Centralized card action handling with a single delegated listener on `#warrantiesList` for edit, delete, archive/unarchive, claims, and notes.
- Updated all call sites to the no-arg `renderWarranties()`; visual output and behavior remain identical across Grid, List, and Table views.
- _Files: `frontend/script.js`_
- **Finalized Legacy Script Delegation**
- Completed migration of UI event listeners from `frontend/script.js` (`setupUIEventListeners`) into `warrantyListController.initEventListeners()`:
- Filters: `statusFilter`, `tagFilter`, `vendorFilter`, `warrantyTypeFilter`
- Sort: `sortBy`
- View switchers: `gridViewBtn`, `listViewBtn`, `tableViewBtn`
- Actions: `refreshBtn`, `exportBtn`, `importBtn`
- Replaced the legacy initializer call site and removed `setupUIEventListeners` from `script.js` (now delegates to the controller).
- Delegated modal openings to component layer: `openDeleteModal`/`openArchiveModal` now call `window.components.modals.openDeleteModal/openArchiveModal` which owns DOM updates and visibility.
- Moved Edit modal save flow out of legacy script: `saveWarranty` now lives in `frontend/js/components/editModal.js`, uses `apiService.updateWarranty`, and is exposed as `window.components.editModal.saveWarranty`.
- Updated Save button wiring: controller binds Save to `editModal.saveWarranty`, preserving `setupSaveWarrantyObserver` wrapping when present.
- Consolidated card action delegation: the single `#warrantiesList` delegated listener resides exclusively in the controller; removed the overlapping legacy delegation from `script.js`.
- `script.js` is now a thin delegator with identical external behavior.
- _Files: `frontend/script.js`, `frontend/js/controllers/warrantyListController.js`, `frontend/js/components/modals.js`, `frontend/js/components/editModal.js`_
- **Page-Specific Script Modularization (`status.js`)**
- Converted `frontend/status.js` from a legacy script into a self-contained ES Module.
- Replaced all `fetch` calls with methods from the imported `apiService.js` and all authentication logic with `authService.js`, removing dependencies on the global scope.
- The module now initializes itself via a `DOMContentLoaded` listener, making it independent of the legacy `script.js`.
- Updated `frontend/status.html` to load `status.js` with `type="module"`, ensuring modern script handling.
- _Files: `frontend/status.js`, `frontend/status.html`_
- **API Service Extension**
- Added a generic `apiService.request()` method to handle various API calls, which was utilized in the refactoring of `status.js`.
- _Files: `frontend/js/services/apiService.js`_
### Fixed
- 404s for non-module assets in production by serving them from Vite `public/`.
- 404s for non-module assets in production by ensuring all assets are properly referenced with relative paths.
- User menu not opening: restored click/close listeners within `authService.js`.
- Login page “Create Account” visible while registration disabled: force-hidden with strong CSS and observer.
- Status page edit actions blocked by early `document.body.appendChild(...)`: added `defer` to `script.js` includes.
- **Thumbnail sizing on initial load (Grid/List/Table)**
- Removed inline image sizing in the renderer so thumbnail sizes are now driven by CSS view classes, ensuring correct sizes apply immediately without hard reload or manual view switch.
- Applied view preference before first render during data load to guarantee correct view class on first paint.
- Tuned responsive CSS for very small screens (≤360px): List view 60×60, Table view 20×20; preserved existing ≤768px sizes.
- _Files: `frontend/script.js`, `frontend/style.css`, `frontend/js/controllers/warrantyListController.js`_
- **Security:** Mitigated potential cross-site scripting (XSS) vulnerabilities by removing reliance on building the UI with `innerHTML` from dynamic data.
- **Performance:** Improved rendering performance by using efficient DOM creation and appending (`<template>` clones, `document.createElement`) instead of causing the browser to re-parse large HTML strings on every update.
- **Code Quality:** Drastically reduced code complexity and repetition in `script.js` by delegating UI creation to specialized component modules.
- Eliminated 404s for removed stylesheet by deleting the `/styles.css` link in `frontend/src/about.html` and removing it from the service worker pre-cache (`frontend/public/sw.js`).
- _Files: `frontend/src/about.html`, `frontend/public/sw.js`_
- Eliminated 404s for removed stylesheet by deleting the `/styles.css` link in `frontend/about.html` and removing it from the service worker pre-cache (`frontend/sw.js`).
- _Files: `frontend/about.html`, `frontend/sw.js`_
- Resolved transient `ReferenceError` messages (e.g., `allTags`/`warranties`/`currentFilters` not defined) by completing the store-backed migration in `frontend/script.js`.
### Removed
- Legacy, scattered auth scripts now replaced by `authService.js`:
- `frontend/src/auth.js`, `frontend/src/auth-new.js`, `frontend/src/include-auth-new.js`, `frontend/src/fix-auth-buttons.js`, `frontend/src/fix-auth-buttons-loader.js`, `frontend/src/registration-status.js`.
- `frontend/auth.js`, `frontend/auth-new.js`, `frontend/include-auth-new.js`, `frontend/fix-auth-buttons.js`, `frontend/fix-auth-buttons-loader.js`, `frontend/registration-status.js`.
- **Global State Variables**
- Eliminated global variables like `let warranties = []` and `let currentFilters = {}` from `script.js`, centralizing all state management within `store.js`.
- **Hardcoded HTML Strings**
- Removed large, multi-line HTML string templates from JavaScript files, which were fragile and difficult to maintain.
- Frontend duplicate/unused assets removed in favor of `frontend/public` copies:
- `frontend/src/js/i18n.js`, `frontend/src/js/i18n-debug.js`, and `frontend/src/js/lib/*` (kept `frontend/public/js` versions)
- Root-level duplicates in `frontend/src/`: `chart.js`, `file-utils.js`, `footer-content.js`, `footer-fix.js`, `mobile-menu.js`, `script.js`, `settings-new.js`, `theme-loader.js`, `version-checker.js`, `auth-redirect.js`, `sw.js`
- Removed unused stylesheet: `frontend/src/styles.css`
- Removed debug files: `frontend/public/js/i18n-debug.js`, `frontend/src/temp-toast-debug.js`
- Frontend duplicate/unused assets removed:
- Removed unused stylesheet: `frontend/styles.css`
- Removed debug files: `frontend/js/i18n-debug.js`, `frontend/temp-toast-debug.js`
- Backend cleanup: removed unused `backend/fix_notification_columns.py`.
### House Cleaning
- Deleted generated artifacts not needed in repo: `frontend/dist`, `frontend/node_modules` (Docker builds frontend in-image)
- Deleted generated artifacts not needed in repo: `frontend/dist`, `frontend/node_modules` (frontend is now served statically without build)
- Purged Python caches: `backend/**/__pycache__`, stray `.pyc`
- Removed old backup: `backend/fix_permissions.sql.bak`
- **Code Refinement in `settings-new.js`**
- **Settings page refactor — ES Module & event wiring**
- Converted `frontend/settings-new.js` into a self-contained ES Module and updated `settings-new.html` to load it with a `<script type="module" src="./settings-new.js"></script>` tag. The module imports `authService` and `apiService` rather than relying on global functions.
- Removed inline `onclick` handlers from the Settings HTML (notably the Delete User modal) and rewired UI interactions using `addEventListener` inside the module so handlers are module-scoped instead of being attached to `window`.
- Extended `frontend/js/services/apiService.js` with settings/admin endpoints consumed by the module (for example: `getUser`, `getUsers`, `updateUser`, `deleteUser`, `getSiteSettings`, `saveSiteSettings`, `getAuditTrail`, `triggerNotifications`) and preserved the `window.api` shim for backward compatibility.
- Fixed a porting-induced syntax/parsing issue (unbalanced brace) and removed a leftover global (`window.currentDeleteUserId`) in favor of a module-scoped `currentDeleteUserId` to avoid leaking state.
- The refactored module parses without syntax errors. Runtime smoke tests are planned/encouraged (see next steps) and final decommission of the legacy `script.js` will follow once runtime verification is complete.
- _Files: `frontend/settings-new.html`, `frontend/settings-new.js`, `frontend/js/services/apiService.js`_
## 1.0.2 - 2025-10-30

View File

@@ -54,17 +54,6 @@ COPY backend/requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
FROM node:20-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
COPY frontend/vite.config.js ./
COPY frontend/public ./public
COPY frontend/src ./src
RUN npm ci --no-audit --no-fund && npm run build
FROM python:3.13-slim-trixie@sha256:079601253d5d25ae095110937ea8cfd7403917b53b077870bccd8b026dc7c42f AS runtime
ARG NGINX_VERSION
@@ -123,8 +112,8 @@ COPY --chown=warracker:warracker backend/migrations/ ./migrations/
COPY --chown=warracker:warracker locales/ ./locales/
COPY --chown=warracker:warracker locales/ /var/www/html/locales/
# 4. Frontend (built with Vite)
COPY --chown=warracker:warracker --from=frontend-build /app/frontend/dist/ /var/www/html/
# 4. Frontend (static files, no build step)
COPY --chown=warracker:warracker frontend/ /var/www/html/
# 5. Backend (bundled in one instruction)
COPY --chown=warracker:warracker backend/ ./backend/

View File

@@ -7,32 +7,33 @@
<title data-i18n="about.title">About - Warracker</title>
<!-- 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="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
<link rel="manifest" href="/manifest.json">
<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?v=20250119004"> <!-- Main stylesheet -->
<link rel="stylesheet" href="./style.css?v=20250119004"> <!-- Main stylesheet -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"> <!-- Font Awesome -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001"> <!-- Header specific styles -->
<link rel="stylesheet" href="./header-fix.css?v=20250119001"> <!-- Header specific styles -->
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- Scripts loaded in head -->
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<script src="/script.js?v=20250119001" defer></script>
<script src="./script.js?v=20250119001" defer></script>
<script type="module" src="./js/index.js"></script>
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n initialization script -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<style>
.about-hero {
@@ -414,13 +415,13 @@
<!-- Version Checker -->
<script src="/version-checker.js?v=20251030001" defer></script> <!-- Version checker script -->
<script src="./version-checker.js?v=20251030001" defer></script> <!-- Version checker script -->
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -4,10 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authenticating...</title>
<link rel="stylesheet" href="/style.css?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script>
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<link rel="stylesheet" href="./style.css?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script>
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<style>
body {
display: flex;
@@ -161,20 +161,20 @@
</script>
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n Configuration -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<script src="/auth-redirect.js?v=20250119001"></script>
<script src="./auth-redirect.js?v=20250119001"></script>
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -106,14 +106,14 @@
</div>
<!-- i18next Local Scripts -->
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n Configuration -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<script>
let warranties = [];

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -2,56 +2,56 @@
<html>
<head>
<!-- Authentication redirect script -->
<script src="/auth-redirect.js?v=20250119001" data-protected="true"></script>
<script src="./auth-redirect.js?v=20250119001" data-protected="true"></script>
<!-- Include authentication script first to handle login state immediately -->
<!-- File utilities script for secure file handling -->
<script src="/file-utils.js?v=20250119001"></script>
<script src="./file-utils.js?v=20250119001"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warracker - Warranty Tracker</title>
<!-- Add standard favicon.ico link -->
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
<!-- Replace the old favicon link -->
<!-- <link rel="icon" type="image/png" href="img/favicon.png"> -->
<!-- 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?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="/js/store.js"></script>
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<script type="module" src="/js/components/warrantyCard.js"></script>
<script type="module" src="/js/components/tag.js"></script>
<script type="module" src="/js/components/editModal.js"></script>
<script type="module" src="/js/components/modals.js"></script>
<script type="module" src="/js/components/claims.js"></script>
<script type="module" src="/js/components/notes.js"></script>
<script type="module" src="/js/components/paperless.js"></script>
<script type="module" src="/js/components/ui.js"></script>
<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?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="./js/store.js"></script>
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<script type="module" src="./js/components/warrantyCard.js"></script>
<script type="module" src="./js/components/tag.js"></script>
<script type="module" src="./js/components/editModal.js"></script>
<script type="module" src="./js/components/modals.js"></script>
<script type="module" src="./js/components/claims.js"></script>
<script type="module" src="./js/components/notes.js"></script>
<script type="module" src="./js/components/paperless.js"></script>
<script type="module" src="./js/components/ui.js"></script>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
<!-- Load fix for auth buttons -->
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n Configuration -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<!-- Immediate authentication check script -->
<script>
@@ -1337,7 +1337,8 @@
</div>
<script src="/script.js?v=20250119002" defer></script>
<script src="./script.js?v=20250119002" defer></script>
<script type="module" src="./js/index.js"></script>
<script>
// Lightweight UI logic for Filter/Sort popovers (no change to core logic)
document.addEventListener('DOMContentLoaded', function () {
@@ -1646,10 +1647,10 @@
</script>
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -0,0 +1,83 @@
// Add Warranty Form Wizard (compat layer)
// Delegates to existing global implementations to preserve identical behavior.
function callIfExists(fn, ...args) {
if (typeof fn === 'function') {
return fn(...args);
}
}
export function initFormTabs() {
return callIfExists(window.initFormTabs);
}
export function switchToTab(index) {
return callIfExists(window.switchToTab, index);
}
export function updateNavigationButtons() {
return callIfExists(window.updateNavigationButtons);
}
export function updateCompletedTabs() {
return callIfExists(window.updateCompletedTabs);
}
export function validateTab(tabIndex) {
return callIfExists(window.validateTab, tabIndex);
}
export function showValidationErrors(tabIndex) {
return callIfExists(window.showValidationErrors, tabIndex);
}
export function updateSummary() {
return callIfExists(window.updateSummary);
}
export function handleFormSubmit(event) {
return callIfExists(window.handleFormSubmit, event);
}
export function resetAddWarrantyWizard() {
return callIfExists(window.resetAddWarrantyWizard);
}
export function handleLifetimeChange(event) {
return callIfExists(window.handleLifetimeChange, event);
}
export function handleWarrantyMethodChange() {
return callIfExists(window.handleWarrantyMethodChange);
}
export function init() {
// Ensure tab structure initialized if present
initFormTabs();
// Wire form submit if form exists
const form = document.getElementById('addWarrantyForm');
if (form && !form._awfBound) {
form.addEventListener('submit', (e) => handleFormSubmit(e));
form._awfBound = true;
}
}
// Backward compatibility on window if needed
if (typeof window !== 'undefined') {
window.addWarrantyForm = {
init,
initFormTabs,
switchToTab,
updateNavigationButtons,
updateCompletedTabs,
validateTab,
showValidationErrors,
updateSummary,
handleFormSubmit,
resetAddWarrantyWizard,
handleLifetimeChange,
handleWarrantyMethodChange
};
}

View File

@@ -1,4 +1,5 @@
// Edit modal helper components (DOM-based, no innerHTML templates)
import { updateWarranty } from '../services/apiService.js';
function createElement(tag, className, text) {
const el = document.createElement(tag);
@@ -245,7 +246,213 @@ if (typeof window !== 'undefined') {
renderCurrentManual,
renderCurrentPhoto,
renderCurrentOther,
saveWarranty,
};
}
// Submit handler for the Edit Warranty modal
export async function saveWarranty() {
const currentId = typeof window !== 'undefined' ? window.currentWarrantyId : null;
if (!currentId) {
if (window && window.showToast) window.showToast(window.t ? window.t('messages.no_warranty_selected_for_update') : 'No warranty selected', 'error');
return;
}
// Gather inputs
const productName = (document.getElementById('editProductName') || {}).value?.trim() || '';
const purchaseDate = (document.getElementById('editPurchaseDate') || {}).value || '';
const isLifetime = !!(document.getElementById('editIsLifetime') || {}).checked;
const editDurationMethodRadioLocal = document.getElementById('editDurationMethod');
const isDurationMethod = !!(editDurationMethodRadioLocal && editDurationMethodRadioLocal.checked);
const years = parseInt((document.getElementById('editWarrantyDurationYears') || {}).value || 0);
const months = parseInt((document.getElementById('editWarrantyDurationMonths') || {}).value || 0);
const days = parseInt((document.getElementById('editWarrantyDurationDays') || {}).value || 0);
const editExactExpirationDateInputLocal = document.getElementById('editExactExpirationDate');
const exactDate = editExactExpirationDateInputLocal ? editExactExpirationDateInputLocal.value : '';
const editWarrantyDurationFieldsLocal = document.getElementById('editWarrantyDurationFields');
// Basic validation
if (!productName) {
if (window && window.showToast) window.showToast(window.t ? window.t('messages.product_name_required') : 'Product name is required', 'error');
return;
}
if (!purchaseDate) {
if (window && window.showToast) window.showToast(window.t ? window.t('messages.purchase_date_required') : 'Purchase date is required', 'error');
return;
}
// Validation for lifetime/duration/exact date
if (!isLifetime) {
if (isDurationMethod) {
if (years === 0 && months === 0 && days === 0) {
if (window && window.showToast) window.showToast(window.t ? window.t('messages.warranty_duration_required') : 'Warranty duration is required', 'error');
const yearsInput = document.getElementById('editWarrantyDurationYears');
if (yearsInput) {
yearsInput.focus();
if (editWarrantyDurationFieldsLocal) editWarrantyDurationFieldsLocal.classList.add('invalid-duration');
}
return;
}
} else {
if (!exactDate) {
if (window && window.showToast) window.showToast(window.t ? window.t('messages.exact_expiration_date_required') : 'Exact expiration date is required', 'error');
if (editExactExpirationDateInputLocal) editExactExpirationDateInputLocal.focus();
return;
}
if (purchaseDate && exactDate <= purchaseDate) {
if (window && window.showToast) window.showToast(window.t ? window.t('messages.expiration_date_after_purchase_date') : 'Expiration date must be after purchase date', 'error');
if (editExactExpirationDateInputLocal) editExactExpirationDateInputLocal.focus();
return;
}
}
}
if (editWarrantyDurationFieldsLocal) editWarrantyDurationFieldsLocal.classList.remove('invalid-duration');
// Build FormData
const formData = new FormData();
formData.append('product_name', productName);
formData.append('purchase_date', purchaseDate);
let productUrl = (document.getElementById('editProductUrl') || {}).value?.trim() || '';
if (productUrl) {
if (!productUrl.startsWith('http://') && !productUrl.startsWith('https://')) {
productUrl = 'https://' + productUrl;
}
formData.append('product_url', productUrl);
}
const purchasePrice = (document.getElementById('editPurchasePrice') || {}).value;
const currency = (document.getElementById('editCurrency') || {}).value;
if (purchasePrice) formData.append('purchase_price', purchasePrice);
if (currency) formData.append('currency', currency);
// Serial numbers
const serialInputs = document.querySelectorAll('#editSerialNumbersContainer input[name="serial_numbers[]"]');
formData.delete('serial_numbers[]');
serialInputs.forEach(input => {
if (input.value && input.value.trim()) formData.append('serial_numbers[]', input.value.trim());
});
// Tags
const editSelectedTags = (typeof window !== 'undefined' && window.editSelectedTags) ? window.editSelectedTags : [];
formData.append('tag_ids', JSON.stringify(Array.isArray(editSelectedTags) ? editSelectedTags.map(t => t.id) : []));
// Document URLs
formData.append('invoice_url', (document.getElementById('editInvoiceUrl') || {}).value || '');
formData.append('manual_url', (document.getElementById('editManualUrl') || {}).value || '');
formData.append('other_document_url', (document.getElementById('editOtherDocumentUrl') || {}).value || '');
// Files
const invoiceFile = (document.getElementById('editInvoice') || {}).files ? document.getElementById('editInvoice').files[0] : null;
const manualFile = (document.getElementById('editManual') || {}).files ? document.getElementById('editManual').files[0] : null;
const otherDocumentFile = (document.getElementById('editOtherDocument') || {}).files ? document.getElementById('editOtherDocument').files[0] : null;
const productPhotoFile = (document.getElementById('editProductPhoto') || {}).files ? document.getElementById('editProductPhoto').files[0] : null;
if (invoiceFile) formData.append('invoice', invoiceFile);
if (manualFile) formData.append('manual', manualFile);
if (otherDocumentFile) formData.append('other_document', otherDocumentFile);
if (productPhotoFile) formData.append('product_photo', productPhotoFile);
// Deletion flags
const deleteInvoiceBtn = document.getElementById('deleteInvoiceBtn');
if (deleteInvoiceBtn && deleteInvoiceBtn.dataset.delete === 'true') formData.append('delete_invoice', 'true');
const deleteManualBtn = document.getElementById('deleteManualBtn');
if (deleteManualBtn && deleteManualBtn.dataset.delete === 'true') formData.append('delete_manual', 'true');
const deleteOtherDocumentBtn = document.getElementById('deleteOtherDocumentBtn');
if (deleteOtherDocumentBtn && deleteOtherDocumentBtn.dataset.delete === 'true') formData.append('delete_other_document', 'true');
const deleteProductPhotoBtn = document.getElementById('deleteProductPhotoBtn');
if (deleteProductPhotoBtn && deleteProductPhotoBtn.dataset.delete === 'true') formData.append('delete_product_photo', 'true');
// Lifetime/duration
formData.append('is_lifetime', String(isLifetime));
if (!isLifetime) {
if (isDurationMethod) {
formData.append('warranty_duration_years', years);
formData.append('warranty_duration_months', months);
formData.append('warranty_duration_days', days);
} else {
formData.append('exact_expiration_date', exactDate);
formData.append('warranty_duration_years', 0);
formData.append('warranty_duration_months', 0);
formData.append('warranty_duration_days', 0);
}
} else {
formData.append('warranty_duration_years', 0);
formData.append('warranty_duration_months', 0);
formData.append('warranty_duration_days', 0);
}
// Notes and misc fields
const notes = (document.getElementById('editNotes') || {}).value || '';
formData.append('notes', notes.trim() !== '' ? notes : '');
const editModelNumber = document.getElementById('editModelNumber');
formData.append('model_number', editModelNumber && editModelNumber.value.trim() ? editModelNumber.value.trim() : '');
const editVendorInput = document.getElementById('editVendor');
formData.append('vendor', editVendorInput ? (editVendorInput.value || '').trim() : '');
const editWarrantyTypeInput = document.getElementById('editWarrantyType');
const editWarrantyTypeCustomInput = document.getElementById('editWarrantyTypeCustom');
let warrantyTypeValue = '';
if (editWarrantyTypeInput) {
warrantyTypeValue = (editWarrantyTypeInput.value === 'other' && editWarrantyTypeCustomInput && editWarrantyTypeCustomInput.value.trim())
? editWarrantyTypeCustomInput.value.trim()
: editWarrantyTypeInput.value.trim();
}
formData.append('warranty_type', warrantyTypeValue);
// Debug logs preserved
console.log('[DEBUG saveWarranty] isLifetime:', isLifetime);
console.log('[DEBUG saveWarranty] isDurationMethod:', isDurationMethod);
console.log('[DEBUG saveWarranty] exactDate:', exactDate);
console.log('[DEBUG saveWarranty] years/months/days:', years, months, days);
for (let [key, value] of formData.entries()) {
console.log(`[DEBUG saveWarranty] FormData: ${key} = ${value}`);
}
if (window && window.showLoadingSpinner) window.showLoadingSpinner();
try {
// Paperless uploads pipeline (delegated to legacy/global)
const uploads = (window && typeof window.processEditPaperlessNgxUploads === 'function')
? await window.processEditPaperlessNgxUploads(formData)
: {};
Object.keys(uploads || {}).forEach(k => formData.append(k, uploads[k]));
// Submit update via API service (auto-injects Authorization header)
await updateWarranty(currentId, formData);
if (window && window.hideLoadingSpinner) window.hideLoadingSpinner();
if (window && window.showToast) window.showToast('Warranty updated successfully', 'success');
if (window && typeof window.closeModals === 'function') window.closeModals();
// Reload warranties to pick up processed assets/paths
if (window && window.warrantyListController && typeof window.warrantyListController.loadWarranties === 'function') {
try {
await window.warrantyListController.loadWarranties(true);
if (window && typeof window.applyFilters === 'function') window.applyFilters();
setTimeout(() => {
if (window && typeof window.loadSecureImages === 'function') {
window.loadSecureImages();
}
}, 200);
const notesModal = document.getElementById('notesModal');
if (notesModal && notesModal.style.display === 'block') notesModal.style.display = 'none';
// Auto-link paperless docs post-update
if ((invoiceFile || manualFile || otherDocumentFile) && currentId) {
setTimeout(() => {
if (typeof window.autoLinkRecentDocuments === 'function') {
const fileInfo = {};
if (invoiceFile) fileInfo.invoice = invoiceFile.name;
if (manualFile) fileInfo.manual = manualFile.name;
if (otherDocumentFile) fileInfo.other = otherDocumentFile.name;
window.autoLinkRecentDocuments(currentId, ['invoice', 'manual', 'other'], 10, 10000, fileInfo);
}
}, 3000);
}
} catch (e) {
console.error('Error reloading warranties after edit:', e);
}
}
} catch (error) {
if (window && window.hideLoadingSpinner) window.hideLoadingSpinner();
console.error('Error updating warranty:', error);
if (window && window.showToast) window.showToast(error?.message || 'Failed to update warranty', 'error');
}
}

View File

@@ -0,0 +1,95 @@
// Tag Manager (compat layer)
// Encapsulates tag management by delegating to existing global implementations to keep behavior identical.
function callIfExists(fn, ...args) {
if (typeof fn === 'function') {
return fn(...args);
}
}
export function loadTags(force = false) {
return callIfExists(window.loadTags, force);
}
export function initTagFunctionality() {
return callIfExists(window.initTagFunctionality);
}
export function renderTagsList(searchTerm = '') {
return callIfExists(window.renderTagsList, searchTerm);
}
export function renderEditTagsList(searchTerm = '') {
return callIfExists(window.renderEditTagsList, searchTerm);
}
export function renderSelectedTags() {
return callIfExists(window.renderSelectedTags);
}
export function renderEditSelectedTags() {
return callIfExists(window.renderEditSelectedTags);
}
export function createTag(name) {
return callIfExists(window.createTag, name);
}
export function updateTag(id, name, color) {
return callIfExists(window.updateTag, id, name, color);
}
export function deleteTag(id) {
return callIfExists(window.deleteTag, id);
}
export function openTagManagementModal() {
return callIfExists(window.openTagManagementModal);
}
export function renderExistingTags() {
return callIfExists(window.renderExistingTags);
}
export function addTagManagementEventListeners() {
return callIfExists(window.addTagManagementEventListeners);
}
export function init() {
// Ensure tag functionality on main form if present
initTagFunctionality();
// Attach global Manage Tags button if present
const btn = document.getElementById('globalManageTagsBtn');
if (btn && !btn._tmBound) {
btn.addEventListener('click', async (e) => {
e.preventDefault();
try {
await loadTags();
} finally {
openTagManagementModal();
}
});
btn._tmBound = true;
}
}
// Backward compatibility on window
if (typeof window !== 'undefined') {
window.tagManager = {
init,
loadTags,
initTagFunctionality,
renderTagsList,
renderEditTagsList,
renderSelectedTags,
renderEditSelectedTags,
createTag,
updateTag,
deleteTag,
openTagManagementModal,
renderExistingTags,
addTagManagementEventListeners
};
}

View File

@@ -0,0 +1,622 @@
// Warranty List Controller
// Incremental extraction of main list responsibilities with strict backward compatibility.
function callIfExists(fn, ...args) {
if (typeof fn === 'function') return fn(...args);
}
// Public API (delegations kept for now until full extraction of implementations)
export async function loadWarranties(isAuthenticated) {
// Show loading (tolerant to whichever loader exists)
callIfExists(window.showLoading) || callIfExists(window.showLoadingSpinner);
try {
// Ensure view preference is applied BEFORE first render so thumbnails use correct sizes
callIfExists(window.loadViewPreference);
const __filters = (window.store && window.store.getFilters && window.store.getFilters()) || {};
// Check auth state (match legacy behavior)
if (!isAuthenticated) {
if (typeof window.i18next !== 'undefined') {
callIfExists(window.renderEmptyState, window.i18next.t('messages.login_to_view_warranties'));
} else {
callIfExists(window.renderEmptyState, 'Please log in to view warranties.');
}
return;
}
const token = window.auth && window.auth.getToken ? window.auth.getToken() : null;
if (!token) {
if (typeof window.i18next !== 'undefined') {
callIfExists(window.renderEmptyState, window.i18next.t('messages.authentication_error_login_again'));
} else {
callIfExists(window.renderEmptyState, 'Authentication error. Please log in again.');
}
return;
}
// Determine scope and endpoint (mirrors legacy logic)
const savedScope = callIfExists(window.loadViewScopePreference) || 'personal';
const shouldUseGlobalView = savedScope === 'global';
const baseUrl = window.location.origin;
const isArchivedView = __filters && __filters.status === 'archived';
const apiUrl = isArchivedView
? (shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`)
: (shouldUseGlobalView ? `${baseUrl}/api/warranties/global` : `${baseUrl}/api/warranties`);
// Fetch warranties
const response = await fetch(apiUrl, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
});
if (!response.ok) {
let msg = `HTTP ${response.status}`;
try {
const j = await response.json();
if (j && j.message) msg = j.message;
} catch {}
throw new Error(msg);
}
let data = await response.json();
if (!Array.isArray(data)) data = [];
// Keep isGlobalView in sync with loaded scope
try { if (typeof window.isGlobalView !== 'undefined') window.isGlobalView = shouldUseGlobalView; } catch {}
// Merge archived when Status=All (mirrors legacy behavior for both scopes)
let combinedData = Array.isArray(data) ? data : [];
try {
// Reset includesArchived flag
try { if (typeof window.lastLoadedIncludesArchived !== 'undefined') window.lastLoadedIncludesArchived = false; } catch {}
if (!isArchivedView && __filters && __filters.status === 'all') {
const archivedUrl = shouldUseGlobalView ? `${baseUrl}/api/warranties/global/archived` : `${baseUrl}/api/warranties/archived`;
const archivedResp = await fetch(archivedUrl, { headers: { 'Authorization': `Bearer ${token}` } });
if (archivedResp.ok) {
const archivedData = await archivedResp.json();
const archivedMarked = Array.isArray(archivedData) ? archivedData.map(w => ({ ...w, __isArchived: true })) : [];
combinedData = combinedData.concat(archivedMarked);
try { if (typeof window.lastLoadedIncludesArchived !== 'undefined') window.lastLoadedIncludesArchived = true; } catch {}
}
}
} catch {}
// Process warranties using legacy processor if available
const processed = Array.isArray(combinedData)
? combinedData.map(w => {
const p = callIfExists(window.processWarrantyData, w);
return p !== undefined ? p : w;
})
: [];
if (window.store && window.store.setWarranties) window.store.setWarranties(processed);
// Update archived-load flag
try { if (typeof window.lastLoadedArchived !== 'undefined') window.lastLoadedArchived = !!isArchivedView; } catch {}
const loaded = (window.store && window.store.getWarranties && window.store.getWarranties()) || [];
if (loaded.length === 0) {
if (typeof window.i18next !== 'undefined') {
callIfExists(window.renderEmptyState, window.i18next.t('messages.no_warranties_found_add_first'));
} else {
callIfExists(window.renderEmptyState, 'No warranties yet. Add your first warranty to get started.');
}
return;
}
// Populate filters and render
try { populateTagFilter(); } catch {}
try { populateVendorFilter(); } catch {}
try { populateWarrantyTypeFilter(); } catch {}
callIfExists(window.applyFilters) || applyFilters();
} catch (error) {
console.error('[controller.loadWarranties] Error:', error);
if (typeof window.i18next !== 'undefined') {
callIfExists(window.renderEmptyState, window.i18next.t('messages.error_loading_warranties_try_again'));
} else {
callIfExists(window.renderEmptyState, 'Error loading warranties. Please try again.');
}
} finally {
callIfExists(window.hideLoading) || callIfExists(window.hideLoadingSpinner);
}
}
export function applyFilters() {
// Mirrors legacy script.js logic to preserve identical behavior
const __filters = (window.store && window.store.getFilters && window.store.getFilters()) || {};
const __warranties = (window.store && window.store.getWarranties && window.store.getWarranties()) || [];
// Ensure data source correctness around archived/all toggles
const wasArchivedLoaded = !!(window.lastLoadedArchived);
const includesArchivedLoaded = !!(window.lastLoadedIncludesArchived);
if (__filters.status === 'archived') {
if (!wasArchivedLoaded) {
loadWarranties(true);
return;
}
} else if (wasArchivedLoaded) {
loadWarranties(true);
return;
} else if (__filters.status === 'all' && !includesArchivedLoaded) {
loadWarranties(true);
return;
}
// Filter warranties based on current filters
const filtered = __warranties.filter(warranty => {
// Exclude archived in specific status views (only show in 'all' or 'archived')
if (warranty.is_archived && __filters.status !== 'all' && __filters.status !== 'archived') return false;
// Status filter: allow archived items to pass in All view
if (__filters.status !== 'all' && __filters.status !== 'archived' && warranty.status !== __filters.status) return false;
// Tag filter
if (__filters.tag !== 'all') {
const tagId = parseInt(__filters.tag);
const hasTag = warranty.tags && Array.isArray(warranty.tags) && warranty.tags.some(tag => tag.id === tagId);
if (!hasTag) return false;
}
// Vendor filter
if (__filters.vendor !== 'all' && (warranty.vendor || '').toLowerCase() !== (__filters.vendor || '').toLowerCase()) return false;
// Warranty type filter
if (__filters.warranty_type !== 'all' && (warranty.warranty_type || '').toLowerCase() !== (__filters.warranty_type || '').toLowerCase()) return false;
// Search filter
if (__filters.search) {
const searchTerm = (__filters.search || '').toLowerCase();
const productNameMatch = (warranty.product_name || '').toLowerCase().includes(searchTerm);
const tagMatch = warranty.tags && Array.isArray(warranty.tags) && warranty.tags.some(tag => (tag.name || '').toLowerCase().includes(searchTerm));
const notesMatch = (warranty.notes || '').toLowerCase().includes(searchTerm);
const vendorMatch = (warranty.vendor || '').toLowerCase().includes(searchTerm);
const modelNumberMatch = (warranty.model_number || '').toLowerCase().includes(searchTerm);
const serialNumberMatch = warranty.serial_numbers && Array.isArray(warranty.serial_numbers) && warranty.serial_numbers.some(sn => sn && String(sn).toLowerCase().includes(searchTerm));
if (!productNameMatch && !tagMatch && !notesMatch && !vendorMatch && !modelNumberMatch && !serialNumberMatch) return false;
}
return true;
});
// Preserve legacy behavior: just render (renderWarranties re-applies filters during render)
renderWarranties();
}
export function renderWarranties() { return callIfExists(window.renderWarranties); }
export function filterWarranties() { return callIfExists(window.filterWarranties); }
// Extracted implementations: dropdown population
export function populateTagFilter() {
const tagFilter = document.getElementById('tagFilter');
if (!tagFilter) return;
while (tagFilter.options.length > 1) tagFilter.remove(1);
const uniqueTags = new Set();
const all = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []);
all.forEach(warranty => {
if (warranty.tags && Array.isArray(warranty.tags)) {
warranty.tags.forEach(tag => uniqueTags.add(JSON.stringify({ id: tag.id, name: tag.name, color: tag.color })));
}
});
const sortedTags = Array.from(uniqueTags).map(j => JSON.parse(j)).sort((a, b) => a.name.localeCompare(b.name));
sortedTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag.id;
option.textContent = tag.name;
tagFilter.appendChild(option);
});
}
export function populateVendorFilter() {
const vendorFilterElement = document.getElementById('vendorFilter');
if (!vendorFilterElement) return;
while (vendorFilterElement.options.length > 1) vendorFilterElement.remove(1);
const uniqueVendors = new Set();
const all = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []);
all.forEach(warranty => {
if (warranty.vendor && warranty.vendor.trim() !== '') uniqueVendors.add(warranty.vendor.trim().toLowerCase());
});
const sortedVendors = Array.from(uniqueVendors).sort((a, b) => a.localeCompare(b));
sortedVendors.forEach(vendor => {
const option = document.createElement('option');
option.value = vendor;
option.textContent = vendor.charAt(0).toUpperCase() + vendor.slice(1);
vendorFilterElement.appendChild(option);
});
}
export function populateWarrantyTypeFilter() {
const warrantyTypeFilterElement = document.getElementById('warrantyTypeFilter');
if (!warrantyTypeFilterElement) return;
while (warrantyTypeFilterElement.options.length > 1) warrantyTypeFilterElement.remove(1);
const uniqueTypes = new Set();
const all = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []);
all.forEach(warranty => {
if (warranty.warranty_type && warranty.warranty_type.trim() !== '') uniqueTypes.add(warranty.warranty_type.trim().toLowerCase());
});
const sorted = Array.from(uniqueTypes).sort((a, b) => a.localeCompare(b));
sorted.forEach(type => {
const option = document.createElement('option');
option.value = type;
option.textContent = type.charAt(0).toUpperCase() + type.slice(1);
warrantyTypeFilterElement.appendChild(option);
});
}
// Extracted implementations: persist and restore filters/sort
export async function saveFilterPreferences(saveToApi = true) {
try {
const prefix = (typeof window.getPreferenceKeyPrefix === 'function') ? window.getPreferenceKeyPrefix() : '';
const filters = (window.store && window.store.getFilters && window.store.getFilters()) || {};
const filtersToSave = {
status: filters.status || 'all',
tag: filters.tag || 'all',
vendor: filters.vendor || 'all',
warranty_type: filters.warranty_type || 'all',
search: filters.search || '',
sortBy: filters.sortBy || 'expiration'
};
localStorage.setItem(`${prefix}warrantyFilters`, JSON.stringify(filtersToSave));
if (saveToApi && window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
const token = window.auth.getToken && window.auth.getToken();
if (token) {
try {
await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ saved_filters: filtersToSave })
});
} catch (e) {
console.error('[Filters] Error saving filter preferences to API:', e);
}
}
}
} catch (e) {
console.warn('Failed to save filter preferences', e);
}
}
export function loadFilterAndSortPreferences() {
try {
const prefix = (typeof window.getPreferenceKeyPrefix === 'function') ? window.getPreferenceKeyPrefix() : '';
const savedFiltersRaw = localStorage.getItem(`${prefix}warrantyFilters`);
if (!savedFiltersRaw) return;
const savedFilters = JSON.parse(savedFiltersRaw);
if (!savedFilters || typeof savedFilters !== 'object') return;
const existing = (window.store && window.store.getFilters && window.store.getFilters()) || {};
if (window.store && window.store.updateFilter) {
if (savedFilters.status !== undefined) window.store.updateFilter('status', savedFilters.status || existing.status);
if (savedFilters.tag !== undefined) window.store.updateFilter('tag', savedFilters.tag || existing.tag);
if (savedFilters.vendor !== undefined) window.store.updateFilter('vendor', savedFilters.vendor || existing.vendor);
if (savedFilters.warranty_type !== undefined) window.store.updateFilter('warranty_type', savedFilters.warranty_type || existing.warranty_type);
if (savedFilters.search !== undefined) window.store.updateFilter('search', savedFilters.search || '');
if (savedFilters.sortBy !== undefined) window.store.updateFilter('sortBy', savedFilters.sortBy || existing.sortBy);
}
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
if (searchInput && savedFilters.search) {
searchInput.value = savedFilters.search;
if (clearSearchBtn) clearSearchBtn.style.display = 'flex';
if (searchInput.parentElement) searchInput.parentElement.classList.add('active-search');
}
} catch (e) {
console.warn('Failed to load filter/sort preferences', e);
}
}
// Extracted: setupUIEventListeners -> initEventListeners
export function initEventListeners() {
// --- Global Manage Tags Button ---
const globalManageTagsBtn = document.getElementById('globalManageTagsBtn');
if (globalManageTagsBtn && !globalManageTagsBtn._wlcBound) {
globalManageTagsBtn.addEventListener('click', async () => {
const __tags = (window.store && window.store.getAllTags && window.store.getAllTags()) || [];
if (!__tags || __tags.length === 0) {
callIfExists(window.showLoadingSpinner);
try {
await callIfExists(window.loadTags);
} catch (error) {
console.error('Failed to load tags before opening modal:', error);
callIfExists(window.showToast, window.t ? window.t('messages.could_not_load_tags') : 'Could not load tags', 'error');
callIfExists(window.hideLoadingSpinner);
return;
}
callIfExists(window.hideLoadingSpinner);
}
callIfExists(window.openTagManagementModal);
});
globalManageTagsBtn._wlcBound = true;
}
// Initialize edit tabs if available
callIfExists(window.initEditTabs);
// Close modals on backdrop or dismiss buttons
document.querySelectorAll('.modal-backdrop, [data-dismiss="modal"]').forEach(element => {
if (element._wlcBound) return;
element.addEventListener('click', (e) => {
if (e.target === element || e.target.matches('[data-dismiss="modal"]')) {
const modalToClose = e.target.closest('.modal-backdrop');
if (modalToClose) {
if ((modalToClose.id === 'addWarrantyModal' || modalToClose.id === 'editModal' || modalToClose.id === 'claimsModal' || modalToClose.id === 'claimFormModal') && e.target === modalToClose) {
return;
}
modalToClose.classList.remove('active');
if (modalToClose.id === 'editModal') {
// optional reset area
} else if (modalToClose.id === 'addWarrantyModal') {
callIfExists(window.resetAddWarrantyWizard);
}
}
}
});
element._wlcBound = true;
});
// Prevent modal content clicks from closing the modal
document.querySelectorAll('.modal').forEach(modal => {
if (modal._wlcBound) return;
modal.addEventListener('click', (e) => e.stopPropagation());
modal._wlcBound = true;
});
// Filter controls
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
const statusFilter = document.getElementById('statusFilter');
const tagFilter = document.getElementById('tagFilter');
const sortBySelect = document.getElementById('sortBy');
const vendorFilter = document.getElementById('vendorFilter');
const warrantyTypeFilter = document.getElementById('warrantyTypeFilter');
if (searchInput && !searchInput._wlcBound) {
let searchDebounceTimeout;
searchInput.addEventListener('input', () => {
if (window.store && window.store.updateFilter) window.store.updateFilter('search', searchInput.value.toLowerCase());
if (clearSearchBtn) clearSearchBtn.style.display = searchInput.value ? 'flex' : 'none';
if (searchInput.value) {
searchInput.parentElement.classList.add('active-search');
} else {
searchInput.parentElement.classList.remove('active-search');
}
if (searchDebounceTimeout) clearTimeout(searchDebounceTimeout);
searchDebounceTimeout = setTimeout(() => {
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
}, 300);
});
searchInput._wlcBound = true;
}
if (clearSearchBtn && !clearSearchBtn._wlcBound) {
clearSearchBtn.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
if (window.store && window.store.updateFilter) window.store.updateFilter('search', '');
clearSearchBtn.style.display = 'none';
searchInput.parentElement.classList.remove('active-search');
searchInput.focus();
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
}
});
clearSearchBtn._wlcBound = true;
}
if (statusFilter && !statusFilter._wlcBound) {
statusFilter.addEventListener('change', () => {
if (window.store && window.store.updateFilter) window.store.updateFilter('status', statusFilter.value);
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
});
statusFilter._wlcBound = true;
}
if (tagFilter && !tagFilter._wlcBound) {
tagFilter.addEventListener('change', () => {
if (window.store && window.store.updateFilter) window.store.updateFilter('tag', tagFilter.value);
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
});
tagFilter._wlcBound = true;
}
if (vendorFilter && !vendorFilter._wlcBound) {
vendorFilter.addEventListener('change', () => {
if (window.store && window.store.updateFilter) window.store.updateFilter('vendor', vendorFilter.value);
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
});
vendorFilter._wlcBound = true;
}
if (warrantyTypeFilter && !warrantyTypeFilter._wlcBound) {
warrantyTypeFilter.addEventListener('change', () => {
if (window.store && window.store.updateFilter) window.store.updateFilter('warranty_type', warrantyTypeFilter.value);
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
});
warrantyTypeFilter._wlcBound = true;
}
if (sortBySelect && !sortBySelect._wlcBound) {
sortBySelect.addEventListener('change', () => {
if (window.store && window.store.updateFilter) window.store.updateFilter('sortBy', sortBySelect.value);
callIfExists(window.applyFilters);
callIfExists(window.saveFilterPreferences);
});
sortBySelect._wlcBound = true;
}
// View switchers
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
const tableViewBtn = document.getElementById('tableViewBtn');
if (gridViewBtn && !gridViewBtn._wlcBound) { gridViewBtn.addEventListener('click', () => callIfExists(window.switchView, 'grid')); gridViewBtn._wlcBound = true; }
if (listViewBtn && !listViewBtn._wlcBound) { listViewBtn.addEventListener('click', () => callIfExists(window.switchView, 'list')); listViewBtn._wlcBound = true; }
if (tableViewBtn && !tableViewBtn._wlcBound) { tableViewBtn.addEventListener('click', () => callIfExists(window.switchView, 'table')); tableViewBtn._wlcBound = true; }
// Export / Import
const exportBtn = document.getElementById('exportBtn');
if (exportBtn && !exportBtn._wlcBound) { exportBtn.addEventListener('click', () => callIfExists(window.exportWarranties)); exportBtn._wlcBound = true; }
const importBtn = document.getElementById('importBtn');
const csvFileInput = document.getElementById('csvFileInput');
if (importBtn && csvFileInput && !importBtn._wlcBound) {
importBtn.addEventListener('click', () => csvFileInput.click());
csvFileInput.addEventListener('change', (event) => {
if (event.target.files && event.target.files.length > 0) {
callIfExists(window.handleImport, event.target.files[0]);
}
});
importBtn._wlcBound = true;
}
// Refresh
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn && !refreshBtn._wlcBound) {
refreshBtn.addEventListener('click', () => loadWarranties(true));
refreshBtn._wlcBound = true;
}
// Warranty Type custom input handling
const warrantyTypeInput = document.getElementById('warrantyType');
const warrantyTypeCustomInput = document.getElementById('warrantyTypeCustom');
if (warrantyTypeInput && warrantyTypeCustomInput && !warrantyTypeInput._wlcBound) {
warrantyTypeInput.addEventListener('change', () => {
if (warrantyTypeInput.value === 'other') {
warrantyTypeCustomInput.style.display = 'block';
warrantyTypeCustomInput.focus();
} else {
warrantyTypeCustomInput.style.display = 'none';
warrantyTypeCustomInput.value = '';
}
callIfExists(window.updateSummary);
});
warrantyTypeCustomInput.addEventListener('input', () => callIfExists(window.updateSummary));
warrantyTypeInput._wlcBound = true;
}
const editWarrantyTypeInput = document.getElementById('editWarrantyType');
const editWarrantyTypeCustomInput = document.getElementById('editWarrantyTypeCustom');
if (editWarrantyTypeInput && editWarrantyTypeCustomInput && !editWarrantyTypeInput._wlcBound) {
editWarrantyTypeInput.addEventListener('change', () => {
if (editWarrantyTypeInput.value === 'other') {
editWarrantyTypeCustomInput.style.display = 'block';
editWarrantyTypeCustomInput.focus();
} else {
editWarrantyTypeCustomInput.style.display = 'none';
editWarrantyTypeCustomInput.value = '';
}
});
editWarrantyTypeInput._wlcBound = true;
}
// Save warranty changes
const saveWarrantyBtn = document.getElementById('saveWarrantyBtn');
if (saveWarrantyBtn && !saveWarrantyBtn._wlcBound) {
let baseSave =
(window.components && window.components.editModal && window.components.editModal.saveWarranty)
? window.components.editModal.saveWarranty
: window.saveWarranty;
let functionToAttachOnClick = baseSave;
if (typeof window.setupSaveWarrantyObserver === 'function') {
try {
functionToAttachOnClick = window.setupSaveWarrantyObserver(baseSave);
window.saveWarrantyObserverAttachedByScriptJS = true;
} catch (e) {
console.error('[warrantyListController] Failed to wrap saveWarranty:', e);
}
}
saveWarrantyBtn.addEventListener('click', () => {
if (typeof functionToAttachOnClick === 'function') functionToAttachOnClick();
});
saveWarrantyBtn._wlcBound = true;
}
// Confirm delete/archive
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn && !confirmDeleteBtn._wlcBound) { confirmDeleteBtn.addEventListener('click', () => callIfExists(window.deleteWarranty)); confirmDeleteBtn._wlcBound = true; }
const confirmArchiveBtn = document.getElementById('confirmArchiveBtn');
if (confirmArchiveBtn && !confirmArchiveBtn._wlcBound) { confirmArchiveBtn.addEventListener('click', () => callIfExists(window.confirmArchive)); confirmArchiveBtn._wlcBound = true; }
// List delegated events (claims/edit/delete/notes/archive/unarchive) - controller owns this exclusively
const list = document.getElementById('warrantiesList');
if (list && !list._wlcDelegated) {
list.addEventListener('click', (e) => {
if (e.target.closest('.claims-link')) {
e.preventDefault();
const id = parseInt(e.target.closest('.claims-link')?.dataset?.id);
if (typeof window.openClaimsModal === 'function' && id) window.openClaimsModal(id);
return;
}
if (e.target.closest('.edit-btn')) {
e.preventDefault();
const el = e.target.closest('.edit-btn');
if (el?.disabled) return;
const id = parseInt(el?.dataset?.id);
const warranty = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []).find(w => w.id === id);
if (warranty && typeof window.openEditModal === 'function') window.openEditModal(warranty);
return;
}
if (e.target.closest('.delete-btn')) {
e.preventDefault();
const el = e.target.closest('.delete-btn');
const id = parseInt(el?.dataset?.id);
const card = el?.closest('.warranty-card');
const titleEl = card ? card.querySelector('.product-name-header .warranty-title') : null;
const productName = titleEl ? titleEl.textContent.trim() : '';
if (typeof window.openDeleteModal === 'function') window.openDeleteModal(id, productName);
return;
}
if (e.target.closest('.notes-link')) {
e.preventDefault();
const el = e.target.closest('.notes-link');
const id = parseInt(el?.dataset?.id);
const warranty = ((window.store && window.store.getWarranties && window.store.getWarranties()) || []).find(w => w.id === id);
if (warranty && typeof window.showNotesModal === 'function') window.showNotesModal(warranty.notes, warranty);
return;
}
if (e.target.closest('.archive-btn')) {
e.preventDefault();
const el = e.target.closest('.archive-btn');
const id = parseInt(el?.dataset?.id);
const card = el?.closest('.warranty-card');
const titleEl = card ? card.querySelector('.product-name-header .warranty-title') : null;
const productName = titleEl ? titleEl.textContent.trim() : '';
if (typeof window.openArchiveModal === 'function') window.openArchiveModal(id, productName);
return;
}
if (e.target.closest('.unarchive-btn')) {
e.preventDefault();
const id = parseInt(e.target.closest('.unarchive-btn')?.dataset?.id);
if (typeof window.toggleArchiveStatus === 'function' && id) window.toggleArchiveStatus(id, false);
return;
}
});
list._wlcDelegated = true;
}
}
// Backward compatibility for legacy callers
if (typeof window !== 'undefined') {
window.warrantyListController = {
loadWarranties,
applyFilters,
renderWarranties,
filterWarranties,
populateTagFilter,
populateVendorFilter,
populateWarrantyTypeFilter,
saveFilterPreferences,
loadFilterAndSortPreferences,
initEventListeners
};
// Legacy globals to avoid breaking existing references during staged extraction
window.populateTagFilter = populateTagFilter;
window.populateVendorFilter = populateVendorFilter;
window.populateWarrantyTypeFilter = populateWarrantyTypeFilter;
window.saveFilterPreferences = saveFilterPreferences;
window.loadFilterAndSortPreferences = loadFilterAndSortPreferences;
}
export const init = initEventListeners;

25
frontend/js/index.js Normal file
View File

@@ -0,0 +1,25 @@
// Main Application Entry Point
import authService from './services/authService.js';
import { init as initListController, loadWarranties } from './controllers/warrantyListController.js';
import { init as initTagManager } from './components/tagManager.js';
import { init as initAddForm } from './components/addWarrantyForm.js';
// Initialize auth first (idempotent; authService also auto-inits on DOMContentLoaded)
document.addEventListener('DOMContentLoaded', () => {
authService.initAuth();
});
// Wait for authentication to be confirmed before loading protected data and initializing auth-dependent modules
window.addEventListener('authStateReady', (event) => {
if (event.detail && event.detail.isAuthenticated) {
// Initialize all feature modules
initListController();
initTagManager();
initAddForm();
// Trigger the initial data load for the application
loadWarranties(true);
}
});

View File

@@ -1,4 +1,4 @@
import authService from '/js/services/authService.js';
import authService from './authService.js';
async function baseRequest(path, options = {}) {
const headers = new Headers(options.headers || {});
@@ -48,6 +48,60 @@ export async function getStatistics() {
return res.json();
}
// --- Settings & Admin endpoints ---
export async function getUser() {
const res = await baseRequest('/api/auth/user');
return res.json();
}
export async function getSiteSettings() {
const res = await baseRequest('/api/admin/settings');
return res.json();
}
export async function saveSiteSettings(settings) {
const res = await baseRequest('/api/admin/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
});
return res.json();
}
export async function getUsers() {
const res = await baseRequest('/api/admin/users');
return res.json();
}
export async function updateUser(userId, data) {
const res = await baseRequest(`/api/admin/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
}
export async function deleteUser(userId) {
await baseRequest(`/api/admin/users/${userId}`, { method: 'DELETE' });
return true;
}
export async function getAuditTrail() {
const res = await baseRequest('/api/admin/audit-trail');
return res.json();
}
export async function triggerNotifications() {
const res = await baseRequest('/api/admin/send-notifications', { method: 'POST' });
return res.json();
}
// Generic request helper for arbitrary endpoints
export async function request(path, options = {}) {
return baseRequest(path, options);
}
export async function savePreferences(prefs) {
const res = await baseRequest('/api/auth/preferences', {
method: 'POST',
@@ -81,7 +135,17 @@ if (typeof window !== 'undefined') {
updateWarranty,
deleteWarranty,
getStatistics,
request,
savePreferences,
// settings & admin
getUser,
getSiteSettings,
saveSiteSettings,
getUsers,
updateUser,
deleteUser,
getAuditTrail,
triggerNotifications,
};
}
@@ -90,7 +154,17 @@ export default {
updateWarranty,
deleteWarranty,
getStatistics,
request,
savePreferences,
// settings & admin
getUser,
getSiteSettings,
saveSiteSettings,
getUsers,
updateUser,
deleteUser,
getAuditTrail,
triggerNotifications,
};

View File

@@ -0,0 +1,20 @@
// Small adapter to provide a stable i18n interface for modules.
// It prefers a global `window.i18next` (loaded via non-module scripts),
// but keeps a safe fallback API so callers can import this module.
const i18nAdapter = {
t: (...args) => {
if (typeof window !== 'undefined' && window.i18next && typeof window.i18next.t === 'function') return window.i18next.t(...args);
// fallback: return default or key
return args[1] || args[0] || '';
},
changeLanguage: (lang) => {
if (typeof window !== 'undefined' && window.i18next && typeof window.i18next.changeLanguage === 'function') return window.i18next.changeLanguage(lang);
return Promise.resolve();
},
getCurrentLanguage: () => {
if (typeof window !== 'undefined' && window.i18next && window.i18next.language) return window.i18next.language;
return 'en';
}
};
export default i18nAdapter;

View File

@@ -1,19 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<script src="/auth-redirect.js?v=20250119001" data-protected="false"></script>
<script src="./auth-redirect.js?v=20250119001" data-protected="false"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="auth.login_title">Login - Warracker</title>
<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?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script>
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<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?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script>
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<script>
// If user is authenticated and has a theme in user_info (from previous sessions), reflect it early
try {
@@ -24,10 +24,10 @@
} catch (_) {}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
</head>
@@ -84,7 +84,7 @@
</form>
<div id="ssoSection" style="display: none;">
<div class="separator"><span>OR</span></div>
<a href="/api/oidc/login" id="oidcLoginButton" class="btn btn-secondary btn-block btn-sso" style="text-decoration: none;">
<a href="./api/oidc/login" id="oidcLoginButton" class="btn btn-secondary btn-block btn-sso" style="text-decoration: none;">
<i class="fab fa-openid" style="margin-right: 8px;"></i> <span>Login with SSO Provider</span>
</a>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "auth-new.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"vite": "^7.1.12"
}
}

View File

@@ -2,36 +2,37 @@
<html>
<head>
<!-- Authentication redirect script -->
<script src="/auth-redirect.js?v=20250119001" data-protected="false"></script>
<script src="./auth-redirect.js?v=20250119001" data-protected="false"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="auth.register">Register - Warracker</title>
<!-- 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="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/style.css?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<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?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n initialization script -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="/script.js?v=20250119001" defer></script>
<script src="./script.js?v=20250119001" defer></script>
<script type="module" src="./js/index.js"></script>
<style>
.auth-container {
max-width: 500px;
@@ -621,10 +622,10 @@
</script>
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -5,30 +5,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="auth.reset_password_title">Warracker - Reset Password</title>
<!-- 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="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/style.css?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<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?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n initialization script -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="/script.js?v=20250119001" defer></script>
<script src="./script.js?v=20250119001" defer></script>
<script type="module" src="./js/index.js"></script>
<style>
.auth-container {
max-width: 400px;
@@ -264,10 +265,10 @@
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -5,30 +5,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="auth.set_new_password_title">Warracker - Set New Password</title>
<!-- 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="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/style.css?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<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?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n initialization script -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="/script.js?v=20250119001" defer></script>
<script src="./script.js?v=20250119001" defer></script>
<script type="module" src="./js/index.js"></script>
<style>
.auth-container {
max-width: 400px;
@@ -431,10 +432,10 @@
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<html>
<head>
<!-- Authentication redirect script -->
<script src="/auth-redirect.js?v=20250119001" data-protected="true"></script>
<script src="./auth-redirect.js?v=20250119001" data-protected="true"></script>
<!-- Include authentication script first to handle login state immediately -->
@@ -11,38 +11,38 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings - Warracker</title>
<!-- 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="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
<link rel="manifest" href="/manifest.json">
<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?v=20250119004">
<link rel="stylesheet" href="./style.css?v=20250119004">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- Then load settings-specific styles -->
<link rel="stylesheet" href="/settings-styles.css?v=20250119001">
<link rel="stylesheet" href="./settings-styles.css?v=20250119001">
<!-- Apply theme immediately -->
<script src="/theme-loader.js?v=20250119001"></script>
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<script src="./theme-loader.js?v=20250119001"></script>
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<!-- Load header fix styles last to override any conflicting styles -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- Load fix for auth buttons -->
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n Configuration -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<!-- Mobile menu logic isolated to avoid collisions on settings -->
<script src="/mobile-menu.js?v=20250119002" defer></script>
<script src="./mobile-menu.js?v=20250119002" defer></script>
<!-- Immediate authentication check script -->
<script>
@@ -1076,52 +1076,17 @@
<p style="font-size: 0.8em; color: #666;">User ID: <span id="displayUserId"></span></p>
<div class="modal-footer">
<button type="button" class="btn btn-secondary close-modal">Cancel</button>
<button type="button" id="confirmDeleteUserBtn" class="btn btn-danger" onclick="deleteUser()">Delete</button>
<button type="button" id="confirmDeleteUserBtn" class="btn btn-danger">Delete</button>
</div>
<div style="margin-top: 20px; border-top: 1px solid #ddd; padding-top: 10px;">
<p><small>If the delete button doesn't work, try these alternatives:</small></p>
<ul style="font-size: 0.8em;">
<li><a href="#" id="directDeleteLink" style="color: #dc3545;" onclick="deleteUser(); return false;">Alternative 1: Direct function call</a></li>
<li><a href="#" id="directAPILink" style="color: #dc3545;" onclick="directDeleteUserAPI(document.getElementById('deleteUserId').value); return false;">Alternative 2: Direct API call</a></li>
<li><a href="#" id="directDeleteLink" style="color: #dc3545;">Alternative 1: Direct function call</a></li>
<li><a href="#" id="directAPILink" style="color: #dc3545;">Alternative 2: Direct API call</a></li>
<li><button type="submit" class="btn btn-sm btn-danger">Alternative 3: Form Submit</button></li>
</ul>
<div style="margin-top: 10px;">
<button type="button" id="emergencyDeleteBtn" class="btn btn-sm btn-danger"
onclick="(function() {
const userId = document.getElementById('deleteUserId').value || document.getElementById('displayUserId').textContent;
if (!userId) {
alert('Error: User ID is missing');
return;
}
const token = localStorage.getItem('auth_token');
if (!token) {
alert('Error: Authentication token is missing');
return;
}
alert('Emergency delete for user ID: ' + userId);
fetch('/api/admin/users/' + userId, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
})
.then(response => {
alert('Response status: ' + response.status);
if (response.status >= 200 && response.status < 300) {
alert('User deleted successfully');
document.querySelectorAll('.modal-backdrop').forEach(m => m.style.display = 'none');
location.reload();
} else {
alert('Failed to delete user: ' + response.status);
}
})
.catch(error => {
alert('Error: ' + error.message);
});
})(); return false;">
Emergency Delete (Inline JS)
</button>
<button type="button" id="emergencyDeleteBtn" class="btn btn-sm btn-danger">Emergency Delete</button>
</div>
</div>
</form>
@@ -1172,13 +1137,13 @@
<!-- Scripts -->
<script src="/settings-new.js?v=20250119001"></script>
<script type="module" src="./settings-new.js"></script>
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -2,46 +2,47 @@
<html>
<head>
<!-- Authentication redirect script -->
<script src="/auth-redirect.js?v=20250119001" data-protected="true"></script>
<script src="./auth-redirect.js?v=20250119001" data-protected="true"></script>
<!-- Include authentication script first to handle login state immediately -->
<!-- File utilities script for secure file handling -->
<script src="/file-utils.js?v=20250119001"></script>
<script src="./file-utils.js?v=20250119001"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="status.title">Status - Warracker</title>
<!-- 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="apple-touch-icon" sizes="180x180" href="/img/favicon-512x512.png">
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/style.css?v=20250119004">
<script src="/theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="/js/store.js"></script>
<script type="module" src="/js/services/authService.js"></script>
<script type="module" src="/js/services/apiService.js"></script>
<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?v=20250119004">
<script src="./theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script type="module" src="./js/store.js"></script>
<script type="module" src="./js/services/authService.js"></script>
<script type="module" src="./js/services/apiService.js"></script>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="/header-fix.css?v=20250119001">
<link rel="stylesheet" href="./header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="/mobile-header.css?v=20250119002">
<link rel="stylesheet" href="./mobile-header.css?v=20250119002">
<!-- Chart.js for visualizations -->
<script src="/chart.js?v=20250119001"></script>
<script src="./chart.js?v=20250119001"></script>
<!-- i18next Local Scripts -->
<script src="/js/lib/i18next.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="/js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="./js/lib/i18next.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="./js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<!-- i18n initialization script -->
<script src="/js/i18n.js?v=20250119001"></script>
<script src="./js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="/script.js?v=20250119002" defer></script> <!-- Added script.js -->
<script type="module" src="/status.js?v=20250119001"></script> <!-- Status page specific functionality -->
<script src="./script.js?v=20250119002" defer></script> <!-- Added script.js -->
<script type="module" src="./js/index.js"></script>
<script type="module" src="./status.js?v=20250119001"></script> <!-- Status page specific functionality -->
<style>
.user-menu {
position: relative;
@@ -826,13 +827,13 @@
</div>
<script type="module" src="/status.js?v=20250119001"></script>
<script type="module" src="./status.js?v=20250119001"></script>
<!-- Footer Width Fix -->
<script src="/footer-fix.js?v=20251024001"></script>
<script src="./footer-fix.js?v=20251024001"></script>
<!-- Footer Content Manager -->
<script src="/footer-content.js?v=20250119001"></script>
<script src="./footer-content.js?v=20250119001"></script>
<!-- Powered by Warracker Footer -->
<footer class="warracker-footer" id="warrackerFooter">

View File

@@ -1,3 +1,5 @@
import apiService from './js/services/apiService.js';
import authService from './js/services/authService.js';
(function() {
// DOM Elements
const loadingIndicator = document.getElementById('loadingIndicator');
@@ -101,17 +103,13 @@
async function fetchStatistics() {
try {
console.log('Checking authentication status... (status.js IIFE)');
if (!window.auth || !window.auth.isAuthenticated()) {
if (!authService || !authService.isAuthenticated()) {
throw new Error('Authentication required.');
}
const token = window.auth.getToken();
const token = authService.getToken();
if (!token) {
throw new Error('Authentication token not available.');
}
const options = {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json'}
};
// Check saved view scope preference to determine which API endpoint to use
const savedScope = loadViewScopePreference();
@@ -121,17 +119,15 @@
const apiUrl = shouldUseGlobalView ? GLOBAL_STATISTICS_API_URL : STATISTICS_API_URL;
console.log('Fetching statistics from:', apiUrl, '(Global view preference:', savedScope, ')');
const response = await fetch(apiUrl, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch statistics: ${response.status} ${errorText}`);
}
const response = shouldUseGlobalView
? await apiService.request('/api/statistics/global')
: await apiService.request('/api/statistics');
const json = await response.json();
// Update isGlobalView to match the loaded data
isGlobalView = shouldUseGlobalView;
console.log(`[DEBUG] Set isGlobalView to: ${isGlobalView}`);
return await response.json();
return json;
} catch (error) {
console.error('Error fetching statistics (status.js IIFE):', error);
throw error;
@@ -158,13 +154,10 @@
try {
// Check if global view is enabled for this user
const token = window.auth.getToken();
const token = authService.getToken();
if (!token) return;
const response = await fetch('/api/settings/global-view-status', {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
const response = await apiService.request('/api/settings/global-view-status', { method: 'GET' });
if (response.ok) {
const result = await response.json();
@@ -343,11 +336,8 @@
try {
// Check if global view is still available
const token = window.auth.getToken();
const response = await fetch('/api/settings/global-view-status', {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
const token = authService.getToken();
const response = await apiService.request('/api/settings/global-view-status', { method: 'GET' });
if (response.ok) {
const result = await response.json();
@@ -944,14 +934,9 @@
let allWarrantiesForTimeline = [];
try {
// Try to fetch all warranties for a complete timeline
const token = window.auth && window.auth.getToken ? window.auth.getToken() : null;
const token = authService && authService.getToken ? authService.getToken() : null;
if (token) {
const allWarrantiesResponse = await fetch('/api/warranties', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const allWarrantiesResponse = await apiService.request('/api/warranties', { method: 'GET' });
if (allWarrantiesResponse.ok) {
allWarrantiesForTimeline = await allWarrantiesResponse.json();
} else {
@@ -1032,9 +1017,8 @@
async function loadUserPreferences() {
try {
if (!window.auth || !window.auth.getToken()) { console.warn('Auth or token not available for prefs in status.js'); return; }
const token = window.auth.getToken();
const response = await fetch('/api/auth/preferences', { headers: { 'Authorization': `Bearer ${token}` } });
if (!authService || !authService.getToken()) { console.warn('Auth or token not available for prefs in status.js'); return; }
const response = await apiService.request('/api/auth/preferences', { method: 'GET' });
if (response.ok) {
const prefs = await response.json();
if (prefs && prefs.expiring_soon_days !== undefined) { // Check for undefined specifically
@@ -1088,13 +1072,10 @@
// ALWAYS fetch fresh details when editing from the status page to ensure modal has latest data.
console.log(`[DEBUG status.js] Edit from status page for warranty ${warrantyId}. ALWAYS fetching fresh details from /api/debug/warranty/:id.`);
showLoadingSpinner(); // Show spinner for this specific fetch
const token = localStorage.getItem('auth_token') || (window.auth && window.auth.getToken());
const token = authService.getToken();
if (!token) throw new Error('Authentication token not available for fetching fresh details.');
const response = await fetch(`/api/debug/warranty/${warrantyId}`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
});
const response = await apiService.request(`/api/debug/warranty/${warrantyId}`, { method: 'GET' });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));

View File

@@ -4827,18 +4827,18 @@ html.dark-mode .warracker-footer a:hover {
}
}
@media (max-width: 768px) {
@media (max-width: 360px) {
.grid-view .product-photo-thumbnail img {
width: 30px;
height: 30px;
width: 30px;
height: 30px;
}
.list-view .product-photo-thumbnail img {
width: 25px;
height: 25px;
width: 60px;
height: 60px;
}
.table-view .product-photo-thumbnail img {
width: 20px;
height: 20px;
width: 20px;
height: 20px;
}
.product-photo-thumbnail {
margin-right: 5px;

View File

@@ -1,35 +0,0 @@
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
root: 'src',
publicDir: resolve(__dirname, 'public'),
build: {
outDir: '../dist',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/index.html'),
login: resolve(__dirname, 'src/login.html'),
register: resolve(__dirname, 'src/register.html'),
status: resolve(__dirname, 'src/status.html'),
settings: resolve(__dirname, 'src/settings-new.html'),
resetPassword: resolve(__dirname, 'src/reset-password.html'),
resetPasswordRequest: resolve(__dirname, 'src/reset-password-request.html'),
authRedirect: resolve(__dirname, 'src/auth-redirect.html'),
about: resolve(__dirname, 'src/about.html'),
debugExport: resolve(__dirname, 'src/debug-export.html'),
},
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
secure: false,
},
},
},
});