Merge pull request #434 from christianlouis/copilot/ensure-accessibility-compliance

feat(ui): WCAG 2.1 AA accessibility compliance with CI enforcement
This commit is contained in:
Christian Krakau-Louis
2026-02-28 21:11:53 +01:00
committed by GitHub
20 changed files with 729 additions and 204 deletions
+51 -6
View File
@@ -86,12 +86,57 @@ These instructions apply to all files in the `frontend/` directory (templates, C
</form>
```
### Accessibility
- Use semantic HTML elements (`nav`, `main`, `article`, `section`)
- Include `alt` text for images
- Use proper heading hierarchy (h1 → h2 → h3)
- Add ARIA labels when needed
- Ensure keyboard navigation works
### Accessibility (WCAG 2.1 Level AA Required)
DocuElevate targets **WCAG 2.1 Level AA** compliance. Every template change **must** follow these rules.
For the full guide with examples, see `docs/AccessibilityGuide.md`.
#### Semantic HTML (WCAG 1.3.1)
- Use semantic elements: `<nav>`, `<main>`, `<article>`, `<section>`, `<header>`, `<footer>`
- Use proper heading hierarchy: one `<h1>` per page, then `<h2>` → `<h3>` (never skip levels)
- Use `<button>` for actions (not `<a>` or `<div>`) and `<a>` for navigation
- Use `<table>` with `<caption>` or `aria-label`, `<thead>`/`<tbody>`, and `scope="col"`/`scope="row"` on headers
#### Images & Icons (WCAG 1.1.1)
- All `<img>` elements **must** have an `alt` attribute — descriptive for content images, `alt=""` for purely decorative ones
- Decorative Font Awesome `<i>` icons **must** have `aria-hidden="true"` when adjacent text already conveys meaning
- Icon-only buttons **must** have `aria-label` describing the action (e.g., `aria-label="Delete file"`)
#### Keyboard Navigation (WCAG 2.1.1, 2.4.1, 2.4.7)
- All interactive elements must be keyboard-reachable (native `<a>`, `<button>`, `<input>`, or add `tabindex="0"` + key handlers)
- `base.html` provides a **skip-to-content** link (`<a href="#main-content" class="skip-link">`) — do not remove it
- Never suppress focus indicators — the global `focus-visible` outline in `styles.css` is required
- Custom interactive widgets (dropdowns, modals) must trap focus correctly
#### ARIA Attributes
- `aria-label` — use on elements whose purpose isn't clear from visible text (icon-only buttons, unlabelled inputs)
- `aria-hidden="true"` — use on purely decorative icons and elements that duplicate adjacent text
- `aria-live="polite"` — add to any container whose content updates dynamically (status messages, search results, upload progress)
- `aria-expanded` — add to buttons that toggle visibility of content (menus, accordions)
- `aria-current="page"` — mark the current page's navigation link
- `aria-sort` — use on sortable table column headers
#### Forms (WCAG 1.3.1, 3.3.2)
- Every `<input>`, `<select>`, and `<textarea>` **must** have an associated `<label>` (via `for`/`id`) or `aria-label`
- Error messages must be linked via `aria-describedby` or announced with `role="alert"`
- Use `role="search"` on search form containers
#### Modals / Dialogs (WCAG 4.1.2)
- Add `role="dialog"`, `aria-modal="true"`, and `aria-labelledby` pointing to the dialog title
- Focus must move into the dialog when opened and return to the trigger when closed
#### Color & Contrast (WCAG 1.4.3, 1.4.1)
- Text must meet 4.5:1 contrast ratio against its background (3:1 for large text)
- Never rely on color alone to convey information — pair color with icons, text labels, or patterns
- Dark-mode overrides in `styles.css` are WCAG AA-verified; maintain this when adding new colors
#### Touch Targets (WCAG 2.5.8)
- All clickable/tappable elements must be at least 44×44 CSS pixels (`min-height:44px; min-width:44px`)
#### Automated Checks
- The CI pipeline runs `djlint` on every PR to catch common accessibility regressions
- Run locally: `djlint frontend/templates/ --lint`
- Configuration is in `pyproject.toml` under `[tool.djlint]`
### Error Handling
- Display user-friendly error messages
+25 -3
View File
@@ -61,6 +61,28 @@ jobs:
# ruff format only supports --check; do not pass --fix here
run: ruff format --check app/ tests/
# ══════════════════════════════════════════════════════════════════════════
# Stage 1b: HTML Accessibility Lint (catches a11y regressions early)
# ══════════════════════════════════════════════════════════════════════════
html-lint:
name: HTML Accessibility Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install djLint
run: pip install djlint>=1.36.0
- name: Lint HTML templates for accessibility
run: djlint frontend/templates/ --lint
# ══════════════════════════════════════════════════════════════════════════
# Stage 2a: Dependency Vulnerability Scan (runs in parallel with lint)
# ══════════════════════════════════════════════════════════════════════════
@@ -93,7 +115,7 @@ jobs:
test:
name: Tests
runs-on: ubuntu-latest
needs: [lint, dependency-scan] # Wait for lint and dependency scan before running tests
needs: [lint, html-lint, dependency-scan] # Wait for lint, HTML a11y lint, and dependency scan before running tests
services:
redis:
image: redis:7
@@ -160,7 +182,7 @@ jobs:
mypy:
name: Mypy
runs-on: ubuntu-latest
needs: [lint, dependency-scan] # Wait for lint and dependency scan before running type checks
needs: [lint, html-lint, dependency-scan] # Wait for lint, HTML a11y lint, and dependency scan before running type checks
steps:
- name: Checkout Code
uses: actions/checkout@v4
@@ -185,7 +207,7 @@ jobs:
build:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: [test, lint, mypy, dependency-scan]
needs: [test, lint, html-lint, mypy, dependency-scan]
if: github.event_name == 'push'
steps:
+353
View File
@@ -0,0 +1,353 @@
# Accessibility Guide
DocuElevate targets **WCAG 2.1 Level AA** compliance across all user-facing pages. This guide documents the accessibility standards, patterns, and practices used in the project.
## Table of Contents
- [Standards Overview](#standards-overview)
- [ARIA Patterns Used](#aria-patterns-used)
- [Keyboard Navigation](#keyboard-navigation)
- [Color and Contrast](#color-and-contrast)
- [Forms and Inputs](#forms-and-inputs)
- [Tables](#tables)
- [Modals and Dialogs](#modals-and-dialogs)
- [Dynamic Content](#dynamic-content)
- [Images and Icons](#images-and-icons)
- [Testing and Validation](#testing-and-validation)
- [Developer Checklist](#developer-checklist)
## Standards Overview
DocuElevate follows [WCAG 2.1 Level AA](https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_overview&levels=aaa) guidelines. The key principles are:
| Principle | Description |
|-----------|-------------|
| **Perceivable** | Information and UI components must be presentable in ways all users can perceive |
| **Operable** | UI components and navigation must be operable via keyboard and assistive technologies |
| **Understandable** | Information and UI operation must be understandable |
| **Robust** | Content must be robust enough for a wide variety of user agents and assistive technologies |
## ARIA Patterns Used
### Landmarks
`base.html` provides the following landmark structure on every page:
```html
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav aria-label="Main navigation">...</nav>
<main id="main-content">{% block content %}{% endblock %}</main>
<footer role="contentinfo">
<nav aria-label="Footer navigation">...</nav>
</footer>
```
### Navigation
- Active page links use `aria-current="page"`
- The admin dropdown uses `aria-haspopup="true"`, `aria-expanded`, `role="menu"`, and `role="menuitem"`
- The mobile menu toggle has `aria-label="Toggle navigation menu"` and `aria-expanded`
### Decorative Icons
Font Awesome icons that appear next to descriptive text must have `aria-hidden="true"`:
```html
<!-- Correct: icon is decorative, text provides meaning -->
<a href="/settings">
<i class="fas fa-cog" aria-hidden="true"></i> Settings
</a>
<!-- Correct: icon-only button needs aria-label -->
<button aria-label="Delete file">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
```
## Keyboard Navigation
### Skip Link
Every page inherits a skip-to-content link from `base.html` that becomes visible on keyboard focus:
```css
.skip-link:focus {
position: fixed;
top: 0;
left: 0;
/* ... visible styles ... */
}
```
### Focus Indicators
Global `focus-visible` styles are defined in `frontend/static/styles.css`:
```css
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
```
> **Warning:** Never remove or suppress focus indicators with `outline: none` unless you provide an equally visible alternative.
### Interactive Custom Elements
When using `<div>` or `<span>` as interactive elements (avoid when possible), ensure:
```html
<div
role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="if(event.key==='Enter'||event.key===' '){handleClick();}"
aria-label="Descriptive action name"
>
```
## Color and Contrast
### Minimum Ratios (WCAG 1.4.3)
| Text Type | Minimum Contrast Ratio |
|-----------|----------------------|
| Normal text (< 18pt) | 4.5:1 |
| Large text (≥ 18pt or ≥ 14pt bold) | 3:1 |
| UI components and graphical objects | 3:1 |
### Dark Mode
Dark mode overrides in `styles.css` are verified for WCAG AA contrast ratios. When adding new color values:
1. Verify light mode contrast at [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
2. Verify dark mode contrast for the corresponding dark override
3. Never rely on color alone — always pair with text, icons, or patterns
### Status Badges
Status indicators use both color **and** text:
```html
<span class="status-badge status-completed">Completed</span>
<span class="status-badge status-failed">Failed</span>
```
## Forms and Inputs
### Labels (WCAG 1.3.1, 3.3.2)
Every form input **must** have an associated label:
```html
<!-- Preferred: explicit label with for/id -->
<label for="username" class="block text-sm font-medium">Username</label>
<input type="text" id="username" name="username" required />
<!-- Hidden inputs: use aria-label -->
<input type="file" class="hidden" aria-label="Select files to upload" />
```
### Error Messages (WCAG 3.3.1)
Error messages must be announced to screen readers:
```html
<div role="alert" class="bg-red-100 text-red-700">
<p>{{ error_message }}</p>
</div>
```
### Search Forms
Use `role="search"` and proper labelling:
```html
<div class="search-box" role="search">
<label for="search-input" class="sr-only">Search documents</label>
<input type="search" id="search-input" placeholder="Search..." />
<button type="button" aria-label="Search documents">
<i class="fas fa-search" aria-hidden="true"></i> Search
</button>
</div>
```
## Tables
### Required Structure (WCAG 1.3.1)
```html
<table aria-label="File records">
<thead>
<tr>
<th scope="col">Filename</th>
<th scope="col">Size</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>document.pdf</td>
<td>1.2 MB</td>
<td>Completed</td>
</tr>
</tbody>
</table>
```
### Sortable Columns
Add `aria-sort` to indicate the current sort state:
```html
<th scope="col" class="sortable" aria-sort="ascending">
Filename <span class="sort-indicator active">▲</span>
</th>
```
## Modals and Dialogs
### Required Attributes (WCAG 4.1.2)
```html
<div
id="deleteModal"
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="deleteModalTitle"
>
<div class="modal-content">
<h2 id="deleteModalTitle">Confirm Deletion</h2>
<p>Are you sure you want to delete this file?</p>
<button>Cancel</button>
<button>Delete</button>
</div>
</div>
```
### Focus Management
When opening a modal:
1. Move focus into the dialog (first focusable element or the dialog itself)
2. Trap focus within the dialog while open
3. Return focus to the trigger element when closed
## Dynamic Content
### Live Regions (WCAG 4.1.3)
Content that updates dynamically must be announced to screen readers:
```html
<!-- Upload progress -->
<div id="uploadProgress" role="status" aria-live="polite">
<!-- Dynamic progress updates appear here -->
</div>
<!-- Status messages -->
<div id="statusMessage" aria-live="polite">
<!-- Success/error messages appear here -->
</div>
<!-- Search results -->
<div id="search-results" aria-live="polite" aria-relevant="additions removals">
<!-- Results injected by JavaScript -->
</div>
```
| Attribute | Use When |
|-----------|----------|
| `aria-live="polite"` | Updates that don't require immediate attention (search results, progress) |
| `aria-live="assertive"` | Critical updates that require immediate attention (errors) |
| `role="status"` | Status information (equivalent to `aria-live="polite"`) |
| `role="alert"` | Important messages that require immediate attention |
## Images and Icons
### Content Images
```html
<img src="/static/images/logo.svg" alt="DocuElevate Logo" class="h-16" />
```
### Decorative Images
```html
<img src="/static/decoration.jpg" alt="" />
```
### Icon-Only Buttons
```html
<button aria-label="View file details" title="View details">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
```
### Icons with Adjacent Text
```html
<a href="/upload">
<i class="fas fa-upload" aria-hidden="true"></i> Upload
</a>
```
## Testing and Validation
### Automated Checks (CI Pipeline)
The CI pipeline runs `djlint` on every pull request:
```bash
# Run locally before committing
djlint frontend/templates/ --lint
```
Configuration is in `pyproject.toml` under `[tool.djlint]`. The linter enforces:
| Rule | Description |
|------|-------------|
| H005 | `<html>` tag must have `lang` attribute |
| H013 | `<img>` tag must have `alt` attribute |
| H016 | Document must have `<title>` tag |
| H025 | Tag should not be an orphan |
| H026 | Empty `id` and `class` attributes should be removed |
### Manual Testing
For each new feature or UI change, verify:
1. **Keyboard navigation**: Tab through all interactive elements; confirm logical focus order
2. **Screen reader**: Test with a screen reader (VoiceOver on macOS, NVDA on Windows, Orca on Linux)
3. **Color contrast**: Check new color combinations with [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
4. **Zoom**: Test at 200% zoom; ensure content remains usable
5. **Responsive**: Test at mobile viewport widths (320px, 375px)
### Browser Testing Tools
- **Chrome DevTools Accessibility panel** — audit ARIA attributes and contrast
- **axe DevTools** browser extension — automated WCAG scanning
- **Lighthouse** — includes accessibility audit in Performance tab
## Developer Checklist
Use this checklist when creating or modifying UI templates:
- [ ] All images have appropriate `alt` attributes
- [ ] All decorative icons have `aria-hidden="true"`
- [ ] All icon-only buttons have `aria-label`
- [ ] All form inputs have associated labels (via `for`/`id` or `aria-label`)
- [ ] All tables have `aria-label` or `<caption>` and `scope` on headers
- [ ] All modals have `role="dialog"`, `aria-modal="true"`, and `aria-labelledby`
- [ ] Dynamic content areas have `aria-live="polite"` or appropriate role
- [ ] Color contrast meets 4.5:1 for normal text, 3:1 for large text
- [ ] All interactive elements are keyboard-accessible
- [ ] Focus indicators are visible on all interactive elements
- [ ] `djlint frontend/templates/ --lint` passes with zero errors
- [ ] Page heading hierarchy is correct (h1 → h2 → h3, no skipped levels)
+71
View File
@@ -1,5 +1,62 @@
/* frontend/static/styles.css */
/* Example overrides or additional styling */
/* =============================================================
ACCESSIBILITY
Skip-to-content link, focus indicators, and screen-reader-only
utility class following WCAG 2.1 Level AA requirements.
============================================================= */
/* Skip-to-content link: visible only on keyboard focus */
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
z-index: 9999;
padding: 0.75rem 1.5rem;
background-color: #1d4ed8;
color: #ffffff;
font-weight: 600;
text-decoration: none;
border-radius: 0 0 0.375rem 0;
}
.skip-link:focus {
position: fixed;
top: 0;
left: 0;
width: auto;
height: auto;
outline: 2px solid #2563eb;
outline-offset: 2px;
}
/* Enhanced focus-visible indicators for keyboard navigation (WCAG 2.4.7) */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
/* Screen-reader-only utility (visually hidden, accessible to AT) */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
body {
/* Your global overrides can go here if needed */
}
@@ -159,6 +216,20 @@ html.dark code { background-color: #111827; color: #d1d5db; }
html.dark #darkModeToggle { color: #fbbf24; }
html.dark #darkModeToggle:hover { background-color: #374151; }
/* ---- Dark-mode skip-link ---- */
html.dark .skip-link { background-color: #2563eb; }
html.dark .skip-link:focus { outline-color: #60a5fa; }
/* ---- Dark-mode focus-visible indicators ---- */
html.dark a:focus-visible,
html.dark button:focus-visible,
html.dark input:focus-visible,
html.dark select:focus-visible,
html.dark textarea:focus-visible,
html.dark [tabindex]:focus-visible {
outline-color: #60a5fa;
}
/* ---- Scrollbar (WebKit browsers) ---- */
html.dark ::-webkit-scrollbar { width: 8px; height: 8px; }
html.dark ::-webkit-scrollbar-track { background: #1f2937; }
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="mx-auto max-w-3xl py-12">
<img
src="/static/explosion.jpg"
alt="Server Explosion"
alt="Illustration of a server error"
class="mx-auto rounded-xl shadow-xl h-[300px] w-full object-cover"
/>
</div>
+11 -11
View File
@@ -14,7 +14,7 @@
</div>
<a href="/settings"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-cog mr-2"></i> Back to Settings
<i class="fas fa-cog mr-2" aria-hidden="true"></i> Back to Settings
</a>
</div>
@@ -29,16 +29,16 @@
{% if entries %}
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<table class="min-w-full divide-y divide-gray-200" aria-label="Settings audit log">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">When</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Changed By</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Setting Key</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Old Value</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">New Value</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rollback</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">When</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Changed By</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Setting Key</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Old Value</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">New Value</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rollback</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@@ -78,7 +78,7 @@
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border border-gray-300 text-gray-700 bg-white hover:bg-yellow-50 hover:border-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-yellow-400 disabled:opacity-50 disabled:cursor-not-allowed"
title="Revert '{{ entry.key }}' to the value in this log entry"
>
<span x-show="rollingBack !== {{ entry.id }}"><i class="fas fa-undo mr-1"></i>Rollback</span>
<span x-show="rollingBack !== {{ entry.id }}"><i class="fas fa-undo mr-1" aria-hidden="true"></i>Rollback</span>
<span x-show="rollingBack === {{ entry.id }}">Working…</span>
</button>
</td>
@@ -89,7 +89,7 @@
</div>
{% else %}
<div class="bg-white shadow rounded-lg p-8 text-center text-gray-500">
<i class="fas fa-history text-4xl mb-4 block text-gray-300"></i>
<i class="fas fa-history text-4xl mb-4 block text-gray-300" aria-hidden="true"></i>
<p class="text-lg">No configuration changes recorded yet.</p>
<p class="text-sm mt-2">Changes you make on the <a href="/settings" class="text-blue-600 hover:underline">Settings page</a> will appear here.</p>
</div>
+33 -24
View File
@@ -34,8 +34,11 @@
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Skip to main content link for keyboard/screen reader users -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Global Nav -->
<nav class="bg-white shadow relative">
<nav class="bg-white shadow relative" aria-label="Main navigation">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between h-16 items-center">
<!-- Brand + Icon -->
@@ -56,10 +59,10 @@
<!-- Menu Items - using x-data for mobile menu toggle -->
<div x-data="{ mobileMenuOpen: false, adminMenuOpen: false }">
<div class="hidden md:flex space-x-4 items-center">
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
<a href="/search" class="text-gray-700 hover:text-gray-900">Search</a>
<a href="/" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/' %}aria-current="page"{% endif %}>Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/upload' %}aria-current="page"{% endif %}>Upload</a>
<a href="/files" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/files' %}aria-current="page"{% endif %}>Files</a>
<a href="/search" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/search' %}aria-current="page"{% endif %}>Search</a>
<!-- Admin dropdown (shown only for admin users via JS) -->
<div id="adminMenuContainer" class="relative hidden">
@@ -69,6 +72,7 @@
type="button"
class="inline-flex items-center text-gray-700 hover:text-gray-900 focus:outline-none"
aria-haspopup="true"
aria-label="Admin menu"
:aria-expanded="adminMenuOpen"
>
<i class="fas fa-shield-alt mr-1 text-red-500"></i>
@@ -84,19 +88,21 @@
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
role="menu"
aria-label="Admin actions"
>
<div class="py-1">
<a href="/settings" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-cog w-4 mr-2 text-gray-500"></i> Settings
<a href="/settings" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-cog w-4 mr-2 text-gray-500" aria-hidden="true"></i> Settings
</a>
<a href="/admin/credentials" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-key w-4 mr-2 text-yellow-500"></i> Credentials
<a href="/admin/credentials" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-key w-4 mr-2 text-yellow-500" aria-hidden="true"></i> Credentials
</a>
<a href="/admin/files" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-folder-open w-4 mr-2 text-gray-500"></i> File Manager
<a href="/admin/files" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-folder-open w-4 mr-2 text-gray-500" aria-hidden="true"></i> File Manager
</a>
<a href="/admin/queue" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-stream w-4 mr-2 text-blue-500"></i> Queue Monitor
<a href="/admin/queue" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-stream w-4 mr-2 text-blue-500" aria-hidden="true"></i> Queue Monitor
</a>
</div>
</div>
@@ -104,10 +110,10 @@
<!-- Status (de-emphasized) -->
<a href="/status" class="text-gray-500 hover:text-gray-700 text-sm" title="System Status">
<i class="fas fa-circle-dot mr-0.5"></i> Status
<i class="fas fa-circle-dot mr-0.5" aria-hidden="true"></i> Status
</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<a href="/about" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>About</a>
<!-- Dark mode toggle (desktop) -->
<button
@@ -131,6 +137,7 @@
type="button"
class="md:hidden inline-flex items-center justify-center p-3 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
style="min-height:44px;min-width:44px;"
aria-label="Toggle navigation menu"
:aria-expanded="mobileMenuOpen"
>
<span class="sr-only">Open main menu</span>
@@ -160,26 +167,26 @@
<div id="mobileAdminSection" class="hidden">
<div class="border-t border-gray-200 mt-1 pt-1">
<p class="px-3 py-1 text-xs font-semibold text-red-600 uppercase tracking-wider flex items-center">
<i class="fas fa-shield-alt mr-1"></i> Admin
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i> Admin
</p>
<a href="/settings" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-cog mr-2 text-gray-400"></i> Settings
<i class="fas fa-cog mr-2 text-gray-400" aria-hidden="true"></i> Settings
</a>
<a href="/admin/credentials" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-key mr-2 text-yellow-400"></i> Credentials
<i class="fas fa-key mr-2 text-yellow-400" aria-hidden="true"></i> Credentials
</a>
<a href="/admin/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-folder-open mr-2 text-gray-400"></i> File Manager
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i> File Manager
</a>
<a href="/admin/queue" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-stream mr-2 text-blue-400"></i> Queue Monitor
<i class="fas fa-stream mr-2 text-blue-400" aria-hidden="true"></i> Queue Monitor
</a>
</div>
</div>
<!-- Status (de-emphasized) in mobile -->
<a href="/status" class="block px-3 py-3 rounded-md text-sm font-medium text-gray-400 hover:text-gray-600 hover:bg-gray-50">
<i class="fas fa-circle-dot mr-1"></i> Status
<i class="fas fa-circle-dot mr-1" aria-hidden="true"></i> Status
</a>
<a href="/about" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">About</a>
@@ -207,20 +214,22 @@
</nav>
<!-- Main Content -->
<main class="flex-grow">
<main id="main-content" class="flex-grow">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-white shadow">
<footer class="bg-white shadow" role="contentinfo">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
DocuElevate 2025 -
<nav aria-label="Footer navigation" class="inline">
<a href="/privacy" class="text-blue-500 hover:underline">Privacy</a> -
<a href="/imprint" class="text-blue-500 hover:underline">Imprint</a> -
<a href="/terms" class="text-blue-500 hover:underline">Terms</a> -
<a href="/cookies" class="text-blue-500 hover:underline">Cookies</a> -
<a href="/license" class="text-blue-500 hover:underline">License</a> -
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a> -
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a>
</nav> -
<span class="text-xs">Version {{ app_version|default(version, true) }}</span>
</div>
</footer>
+14 -13
View File
@@ -7,7 +7,7 @@
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-2">
<i class="fas fa-key text-yellow-500 text-2xl"></i>
<i class="fas fa-key text-yellow-500 text-2xl" aria-hidden="true"></i>
<h1 class="text-3xl font-bold">Credential Audit</h1>
</div>
<p class="text-gray-600">
@@ -20,7 +20,7 @@
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 text-blue-600 text-xl">
<i class="fas fa-list"></i>
<i class="fas fa-list" aria-hidden="true"></i>
</span>
<div>
<div class="text-2xl font-bold text-gray-800">{{ total }}</div>
@@ -29,7 +29,7 @@
</div>
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full bg-green-100 text-green-600 text-xl">
<i class="fas fa-check-circle"></i>
<i class="fas fa-check-circle" aria-hidden="true"></i>
</span>
<div>
<div class="text-2xl font-bold text-green-700">{{ configured_count }}</div>
@@ -38,7 +38,7 @@
</div>
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full bg-gray-100 text-gray-500 text-xl">
<i class="fas fa-circle-question"></i>
<i class="fas fa-circle-question" aria-hidden="true"></i>
</span>
<div>
<div class="text-2xl font-bold text-gray-600">{{ unconfigured_count }}</div>
@@ -49,7 +49,7 @@
<!-- Legend -->
<div class="bg-blue-50 border-l-4 border-blue-500 text-blue-800 p-4 mb-6 rounded-r-md">
<p class="font-semibold mb-2"><i class="fas fa-circle-info mr-1"></i> Legend</p>
<p class="font-semibold mb-2"><i class="fas fa-circle-info mr-1" aria-hidden="true"></i> Legend</p>
<div class="flex flex-wrap gap-4 text-sm">
<span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mr-1">DB</span>
@@ -85,7 +85,7 @@
<!-- Credentials Table -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<table class="min-w-full divide-y divide-gray-200" aria-label="Credentials for {{ category }}">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-56">
@@ -130,11 +130,11 @@
<td class="px-6 py-4 whitespace-nowrap">
{% if cred.configured %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800">
<i class="fas fa-check mr-1"></i> Configured
<i class="fas fa-check mr-1" aria-hidden="true"></i> Configured
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">
<i class="fas fa-circle-exclamation mr-1"></i> Missing
<i class="fas fa-circle-exclamation mr-1" aria-hidden="true"></i> Missing
</span>
{% endif %}
</td>
@@ -143,11 +143,11 @@
<td class="px-6 py-4 whitespace-nowrap">
{% if cred.source == 'db' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="Stored in database (encrypted)">
<i class="fas fa-database mr-1 text-xs"></i> DB
<i class="fas fa-database mr-1 text-xs" aria-hidden="true"></i> DB
</span>
{% elif cred.source == 'env' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800" title="From environment variable">
<i class="fas fa-terminal mr-1 text-xs"></i> ENV
<i class="fas fa-terminal mr-1 text-xs" aria-hidden="true"></i> ENV
</span>
{% else %}
<span class="text-gray-400 text-xs">—</span>
@@ -158,9 +158,10 @@
<td class="px-6 py-4 whitespace-nowrap text-center">
<a href="/settings#{{ cred.key }}"
title="Edit in Settings"
aria-label="Edit {{ cred.key }}"
class="inline-flex items-center justify-center w-7 h-7 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
>
<i class="fas fa-pen-to-square text-sm"></i>
<i class="fas fa-pen-to-square text-sm" aria-hidden="true"></i>
</a>
</td>
@@ -177,13 +178,13 @@
<a href="/settings"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
>
<i class="fas fa-cog"></i> Manage Settings
<i class="fas fa-cog" aria-hidden="true"></i> Manage Settings
</a>
<a href="/api/settings/credentials"
target="_blank"
class="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 text-sm font-medium"
>
<i class="fas fa-code"></i> Raw JSON (API)
<i class="fas fa-code" aria-hidden="true"></i> Raw JSON (API)
</a>
</div>
+25 -21
View File
@@ -958,8 +958,8 @@
{% block content %}
<div class="detail-container">
<a href="/files" class="back-button">
<i class="fas fa-arrow-left"></i>
<a href="/files" class="back-button" aria-label="Back to File List">
<i class="fas fa-arrow-left" aria-hidden="true"></i>
Back to File List
</a>
@@ -979,15 +979,15 @@
{% set main_completed = step_summary.main.success + step_summary.main.skipped %}
{% set uploads_completed = step_summary.uploads.success + step_summary.uploads.skipped %}
{% if file.is_duplicate %}
<i class="fas fa-copy" style="color: #6b7280; margin-right: 0.5rem;"></i>Duplicate
<i class="fas fa-copy" aria-hidden="true" style="color: #6b7280; margin-right: 0.5rem;"></i>Duplicate
{% elif step_summary.main.failure > 0 or step_summary.uploads.failure > 0 %}
<i class="fas fa-times-circle" style="color: #dc2626; margin-right: 0.5rem;"></i>Failed
<i class="fas fa-times-circle" aria-hidden="true" style="color: #dc2626; margin-right: 0.5rem;"></i>Failed
{% elif step_summary.main["in_progress"] > 0 or step_summary.uploads["in_progress"] > 0 %}
<i class="fas fa-circle-notch" style="color: #f59e0b; margin-right: 0.5rem; animation: spin 1s linear infinite;"></i>Processing
<i class="fas fa-circle-notch" aria-hidden="true" style="color: #f59e0b; margin-right: 0.5rem; animation: spin 1s linear infinite;"></i>Processing
{% elif step_summary.total_main_steps > 0 and main_completed == step_summary.total_main_steps and step_summary.main.failure == 0 %}
<i class="fas fa-check-circle" style="color: #059669; margin-right: 0.5rem;"></i>Completed
<i class="fas fa-check-circle" aria-hidden="true" style="color: #059669; margin-right: 0.5rem;"></i>Completed
{% else %}
<i class="fas fa-pause-circle" style="color: #f59e0b; margin-right: 0.5rem;"></i>Pending
<i class="fas fa-pause-circle" aria-hidden="true" style="color: #f59e0b; margin-right: 0.5rem;"></i>Pending
{% endif %}
</div>
</div>
@@ -1066,11 +1066,11 @@
<span class="detail-value">
{% if original_file_exists %}
<span class="file-status-indicator exists">
<i class="fas fa-check-circle"></i> File exists
<i class="fas fa-check-circle" aria-hidden="true"></i> File exists
</span>
{% else %}
<span class="file-status-indicator missing">
<i class="fas fa-times-circle"></i> File not found
<i class="fas fa-times-circle" aria-hidden="true"></i> File not found
</span>
{% endif %}
</span>
@@ -1086,11 +1086,11 @@
<span class="detail-value">
{% if processed_file_exists %}
<span class="file-status-indicator exists">
<i class="fas fa-check-circle"></i> File exists
<i class="fas fa-check-circle" aria-hidden="true"></i> File exists
</span>
{% else %}
<span class="file-status-indicator missing">
<i class="fas fa-times-circle"></i> File not found
<i class="fas fa-times-circle" aria-hidden="true"></i> File not found
</span>
{% endif %}
</span>
@@ -1196,7 +1196,7 @@
</div>
<div class="pdf-canvas-wrapper" id="original-canvas-wrapper">
<div class="pdf-loading">
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 1rem;"></i>
<i class="fas fa-spinner fa-spin" aria-hidden="true" style="font-size: 2rem; margin-bottom: 1rem;"></i>
<p>Loading PDF...</p>
</div>
</div>
@@ -1205,11 +1205,11 @@
onclick="loadAndShowText('original', {{ file.id }})"
style="margin-top: 0.5rem; background-color: #4299e1; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; width: 100%;"
>
<i class="fas fa-file-alt"></i> View Extracted Text
<i class="fas fa-file-alt" aria-hidden="true"></i> View Extracted Text
</button>
{% else %}
<div style="text-align: center; padding: 3rem; background-color: #f7fafc; border-radius: 0.5rem; color: #718096;">
<i class="fas fa-file-pdf" style="font-size: 3rem; opacity: 0.5; margin-bottom: 1rem;"></i>
<i class="fas fa-file-pdf" aria-hidden="true" style="font-size: 3rem; opacity: 0.5; margin-bottom: 1rem;"></i>
<p>Original file not available</p>
</div>
{% endif %}
@@ -1227,7 +1227,7 @@
</div>
<div class="pdf-canvas-wrapper" id="processed-canvas-wrapper">
<div class="pdf-loading">
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 1rem;"></i>
<i class="fas fa-spinner fa-spin" aria-hidden="true" style="font-size: 2rem; margin-bottom: 1rem;"></i>
<p>Loading PDF...</p>
</div>
</div>
@@ -1236,11 +1236,11 @@
onclick="loadAndShowText('processed', {{ file.id }})"
style="margin-top: 0.5rem; background-color: #48bb78; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; width: 100%;"
>
<i class="fas fa-file-alt"></i> View Extracted Text
<i class="fas fa-file-alt" aria-hidden="true"></i> View Extracted Text
</button>
{% else %}
<div style="text-align: center; padding: 3rem; background-color: #f7fafc; border-radius: 0.5rem; color: #718096;">
<i class="fas fa-file-pdf" style="font-size: 3rem; opacity: 0.5; margin-bottom: 1rem;"></i>
<i class="fas fa-file-pdf" aria-hidden="true" style="font-size: 3rem; opacity: 0.5; margin-bottom: 1rem;"></i>
<p>Processed file not available yet</p>
</div>
{% endif %}
@@ -1259,14 +1259,16 @@
id="original-copy-btn"
onclick="copyTextToClipboard('original')"
style="background-color: #4299e1; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
aria-label="Copy extracted text"
>
<i class="fas fa-copy"></i> Copy
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
<button
onclick="toggleTextModal('original-text-modal')"
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
aria-label="Close modal"
>
<i class="fas fa-times"></i> Close
<i class="fas fa-times" aria-hidden="true"></i> Close
</button>
</div>
</div>
@@ -1288,14 +1290,16 @@
id="processed-copy-btn"
onclick="copyTextToClipboard('processed')"
style="background-color: #4299e1; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
aria-label="Copy extracted text"
>
<i class="fas fa-copy"></i> Copy
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
<button
onclick="toggleTextModal('processed-text-modal')"
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
aria-label="Close modal"
>
<i class="fas fa-times"></i> Close
<i class="fas fa-times" aria-hidden="true"></i> Close
</button>
</div>
</div>
+26 -23
View File
@@ -100,8 +100,8 @@
{% elif file %}
<!-- ── Back + header ── -->
<a href="/files" style="display:inline-flex;align-items:center;gap:0.4rem;color:#3b82f6;font-weight:600;text-decoration:none;margin-bottom:1.25rem;">
<i class="fas fa-arrow-left"></i> Back to Files
<a href="/files" style="display:inline-flex;align-items:center;gap:0.4rem;color:#3b82f6;font-weight:600;text-decoration:none;margin-bottom:1.25rem;" aria-label="Back to Files">
<i class="fas fa-arrow-left" aria-hidden="true"></i> Back to Files
</a>
<div class="doc-header">
@@ -121,23 +121,23 @@
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
<!-- Status pill -->
{% if file.is_duplicate %}
<span class="status-pill pill-duplicate"><i class="fas fa-copy"></i> Duplicate</span>
<span class="status-pill pill-duplicate"><i class="fas fa-copy" aria-hidden="true"></i> Duplicate</span>
{% elif step_summary %}
{% set main_completed = step_summary.main.success + step_summary.main.skipped %}
{% if step_summary.main.failure > 0 or step_summary.uploads.failure > 0 %}
<span class="status-pill pill-failed"><i class="fas fa-times-circle"></i> Failed</span>
<span class="status-pill pill-failed"><i class="fas fa-times-circle" aria-hidden="true"></i> Failed</span>
{% elif step_summary.main["in_progress"] > 0 or step_summary.uploads["in_progress"] > 0 %}
<span class="status-pill pill-processing"><i class="fas fa-circle-notch fa-spin"></i> Processing</span>
<span class="status-pill pill-processing"><i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> Processing</span>
{% elif step_summary.total_main_steps > 0 and main_completed == step_summary.total_main_steps and step_summary.main.failure == 0 %}
<span class="status-pill pill-completed"><i class="fas fa-check-circle"></i> Completed</span>
<span class="status-pill pill-completed"><i class="fas fa-check-circle" aria-hidden="true"></i> Completed</span>
{% else %}
<span class="status-pill pill-pending"><i class="fas fa-pause-circle"></i> Pending</span>
<span class="status-pill pill-pending"><i class="fas fa-pause-circle" aria-hidden="true"></i> Pending</span>
{% endif %}
{% endif %}
<!-- Process detail link -->
<a href="/files/{{ file.id }}/detail" class="action-btn btn-process">
<i class="fas fa-cogs"></i> Processing Details
<i class="fas fa-cogs" aria-hidden="true"></i> Processing Details
</a>
</div>
</div>
@@ -149,7 +149,7 @@
<div>
{% if gpt_metadata %}
<div class="doc-card">
<div class="doc-card-title"><i class="fas fa-tags" style="color:#3b82f6;margin-right:0.4rem;"></i>Document Metadata</div>
<div class="doc-card-title"><i class="fas fa-tags" aria-hidden="true" style="color:#3b82f6;margin-right:0.4rem;"></i>Document Metadata</div>
{% if gpt_metadata.document_type %}
<div class="meta-row">
@@ -221,7 +221,7 @@
<!-- File info -->
<div class="doc-card">
<div class="doc-card-title"><i class="fas fa-info-circle" style="color:#6b7280;margin-right:0.4rem;"></i>File Info</div>
<div class="doc-card-title"><i class="fas fa-info-circle" aria-hidden="true" style="color:#6b7280;margin-right:0.4rem;"></i>File Info</div>
<div class="info-row">
<span class="info-key">Uploaded</span>
<span class="info-val">{{ file.created_at.strftime('%Y-%m-%d %H:%M') if file.created_at else 'N/A' }}</span>
@@ -246,25 +246,25 @@
<!-- Actions -->
<div class="doc-card">
<div class="doc-card-title"><i class="fas fa-bolt" style="color:#f59e0b;margin-right:0.4rem;"></i>Actions</div>
<div class="doc-card-title"><i class="fas fa-bolt" aria-hidden="true" style="color:#f59e0b;margin-right:0.4rem;"></i>Actions</div>
<div style="display:flex;flex-wrap:wrap;gap:0.6rem;">
{% if processed_file_exists %}
<a href="/api/files/{{ file.id }}/download?version=processed" class="action-btn btn-primary">
<i class="fas fa-download"></i> Download (processed)
<i class="fas fa-download" aria-hidden="true"></i> Download (processed)
</a>
{% endif %}
{% if original_file_exists %}
<a href="/api/files/{{ file.id }}/download?version=original" class="action-btn btn-secondary">
<i class="fas fa-download"></i> Download (original)
<i class="fas fa-download" aria-hidden="true"></i> Download (original)
</a>
{% endif %}
{% if processed_file_exists %}
<a href="/api/files/{{ file.id }}/preview?version=processed" target="_blank" class="action-btn btn-secondary">
<i class="fas fa-external-link-alt"></i> Open processed
<i class="fas fa-external-link-alt" aria-hidden="true"></i> Open processed
</a>
{% elif original_file_exists %}
<a href="/api/files/{{ file.id }}/preview?version=original" target="_blank" class="action-btn btn-secondary">
<i class="fas fa-external-link-alt"></i> Open original
<i class="fas fa-external-link-alt" aria-hidden="true"></i> Open original
</a>
{% endif %}
</div>
@@ -274,7 +274,7 @@
<!-- RIGHT: PDF preview -->
<div>
<div class="doc-card" style="padding:1rem;">
<div class="doc-card-title"><i class="fas fa-file-pdf" style="color:#ef4444;margin-right:0.4rem;"></i>Preview</div>
<div class="doc-card-title"><i class="fas fa-file-pdf" aria-hidden="true" style="color:#ef4444;margin-right:0.4rem;"></i>Preview</div>
{% if processed_file_exists %}
<iframe
src="/api/files/{{ file.id }}/preview?version=processed"
@@ -303,14 +303,14 @@
{% if file.ocr_text %}
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
<div class="doc-card-title" style="margin:0;"><i class="fas fa-align-left" style="color:#8b5cf6;margin-right:0.4rem;"></i>Extracted Text</div>
<div class="doc-card-title" style="margin:0;"><i class="fas fa-align-left" aria-hidden="true" style="color:#8b5cf6;margin-right:0.4rem;"></i>Extracted Text</div>
<div style="display:flex;gap:0.5rem;">
<button class="text-toggle" onclick="toggleOcrText()">
<i id="ocr-toggle-icon" class="fas fa-chevron-down"></i>
<button class="text-toggle" onclick="toggleOcrText()" aria-expanded="false" aria-controls="ocr-text-block">
<i id="ocr-toggle-icon" class="fas fa-chevron-down" aria-hidden="true"></i>
<span id="ocr-toggle-label">Show text</span>
</button>
<button class="text-toggle" onclick="copyOcrText()" id="ocr-copy-btn" style="color:#10b981;">
<i class="fas fa-copy"></i> Copy
<button class="text-toggle" onclick="copyOcrText()" id="ocr-copy-btn" style="color:#10b981;" aria-label="Copy extracted text">
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
</div>
</div>
@@ -321,9 +321,9 @@
{% elif processed_file_exists or original_file_exists %}
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div class="doc-card-title" style="margin:0;"><i class="fas fa-align-left" style="color:#8b5cf6;margin-right:0.4rem;"></i>Extracted Text</div>
<div class="doc-card-title" style="margin:0;"><i class="fas fa-align-left" aria-hidden="true" style="color:#8b5cf6;margin-right:0.4rem;"></i>Extracted Text</div>
<button class="action-btn btn-secondary" onclick="loadText({{ file.id }})">
<i class="fas fa-file-alt"></i> Extract &amp; show text
<i class="fas fa-file-alt" aria-hidden="true"></i> Extract &amp; show text
</button>
</div>
<div id="text-load-area" style="margin-top:0.75rem;"></div>
@@ -343,14 +343,17 @@
var block = document.getElementById('ocr-text-block');
var icon = document.getElementById('ocr-toggle-icon');
var label = document.getElementById('ocr-toggle-label');
var btn = icon.closest('button');
if (block.style.display === 'none') {
block.style.display = 'block';
icon.className = 'fas fa-chevron-up';
label.textContent = 'Hide text';
if (btn) btn.setAttribute('aria-expanded', 'true');
} else {
block.style.display = 'none';
icon.className = 'fas fa-chevron-down';
label.textContent = 'Show text';
if (btn) btn.setAttribute('aria-expanded', 'false');
}
}
+41 -41
View File
@@ -345,25 +345,25 @@
{% block content %}
<!-- Drop overlay -->
<div id="dropOverlay" class="drop-overlay">
<div id="dropOverlay" class="drop-overlay" aria-hidden="true">
<div class="drop-message">
<i class="fas fa-cloud-upload-alt"></i>
<i class="fas fa-cloud-upload-alt" aria-hidden="true"></i>
<p>Drop files or folders anywhere to upload</p>
<div class="drop-hint">Supports PDF, Office docs, images, HTML, Markdown, and more – folders are processed recursively</div>
</div>
</div>
<!-- Upload progress modal -->
<div id="uploadModal" class="upload-modal">
<div id="uploadModal" class="upload-modal" role="dialog" aria-modal="true" aria-label="Upload progress">
<div class="upload-modal-header">
<span><i class="fas fa-upload"></i> Uploading Files</span>
<button class="close-modal-btn" onclick="closeUploadModal()">
<i class="fas fa-times"></i>
<span><i class="fas fa-upload" aria-hidden="true"></i> Uploading Files</span>
<button class="close-modal-btn" onclick="closeUploadModal()" aria-label="Close upload progress">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="upload-modal-body">
<div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2"></div>
<div id="uploadProgressContainer" class="space-y-2"></div>
<div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2" aria-live="polite"></div>
<div id="uploadProgressContainer" class="space-y-2" role="status" aria-live="polite"></div>
</div>
</div>
@@ -374,7 +374,7 @@
<div id="queueBanner" class="hidden bg-blue-50 border-l-4 border-blue-400 text-blue-800 p-4 mb-6 rounded-r-lg" role="status">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-spinner fa-spin mr-3 text-blue-500"></i>
<i class="fas fa-spinner fa-spin mr-3 text-blue-500" aria-hidden="true"></i>
<span>
<strong id="queueBannerCount">0</strong> item(s) are currently queued or being processed.
Files will appear here once processing completes.
@@ -382,7 +382,7 @@
</div>
<a href="/admin/queue" class="text-blue-600 hover:text-blue-800 text-sm font-medium whitespace-nowrap ml-4"
id="queueBannerLink" style="display:none;">
<i class="fas fa-external-link-alt mr-1"></i>View Queue
<i class="fas fa-external-link-alt mr-1" aria-hidden="true"></i>View Queue
</a>
</div>
</div>
@@ -445,7 +445,7 @@
<div class="filters-section" style="margin-top: 0.75rem;">
<div style="width: 100%;">
<label for="fulltext-search" style="font-weight: 600; display: block; margin-bottom: 0.4rem;">
<i class="fas fa-search"></i> Full-Text Search (OCR text, metadata, tags)
<i class="fas fa-search" aria-hidden="true"></i> Full-Text Search (OCR text, metadata, tags)
</label>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input
@@ -490,10 +490,10 @@
</div>
<div class="flex flex-wrap gap-2">
<button type="button" onclick="bulkReprocess()" style="padding: 0.5rem 1rem; background-color: #3182ce; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-sync"></i> Reprocess Selected
<i class="fas fa-sync" aria-hidden="true"></i> Reprocess Selected
</button>
<button type="button" onclick="bulkDelete()" style="padding: 0.5rem 1rem; background-color: #e53e3e; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-trash"></i> Delete Selected
<i class="fas fa-trash" aria-hidden="true"></i> Delete Selected
</button>
<button type="button" onclick="clearSelection()" style="padding: 0.5rem 1rem; background-color: #718096; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
Clear Selection
@@ -504,13 +504,13 @@
<!-- File table (desktop) -->
<div class="hidden md:block overflow-x-auto">
<table class="file-table" id="fileTable">
<table class="file-table" id="fileTable" aria-label="File records">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page">
<th scope="col" style="width: 40px;">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page" aria-label="Select all files on this page">
</th>
<th class="sortable" onclick="sortTable('id')">
<th scope="col" class="sortable" onclick="sortTable('id')" role="columnheader" aria-sort="{% if sort_by == 'id' %}{% if sort_order == 'asc' %}ascending{% else %}descending{% endif %}{% else %}none{% endif %}">
ID
<span class="sort-indicator {% if sort_by == 'id' %}active{% endif %}">
{% if sort_by == 'id' %}
@@ -520,7 +520,7 @@
{% endif %}
</span>
</th>
<th class="sortable" onclick="sortTable('original_filename')">
<th scope="col" class="sortable" onclick="sortTable('original_filename')">
Original Filename
<span class="sort-indicator {% if sort_by == 'original_filename' %}active{% endif %}">
{% if sort_by == 'original_filename' %}
@@ -530,7 +530,7 @@
{% endif %}
</span>
</th>
<th class="sortable" onclick="sortTable('file_size')">
<th scope="col" class="sortable" onclick="sortTable('file_size')">
File Size
<span class="sort-indicator {% if sort_by == 'file_size' %}active{% endif %}">
{% if sort_by == 'file_size' %}
@@ -540,7 +540,7 @@
{% endif %}
</span>
</th>
<th class="sortable" onclick="sortTable('mime_type')">
<th scope="col" class="sortable" onclick="sortTable('mime_type')">
MIME Type
<span class="sort-indicator {% if sort_by == 'mime_type' %}active{% endif %}">
{% if sort_by == 'mime_type' %}
@@ -550,8 +550,8 @@
{% endif %}
</span>
</th>
<th>Status</th>
<th class="sortable" onclick="sortTable('created_at')">
<th scope="col">Status</th>
<th scope="col" class="sortable" onclick="sortTable('created_at')">
Created At
<span class="sort-indicator {% if sort_by == 'created_at' %}active{% endif %}">
{% if sort_by == 'created_at' %}
@@ -561,7 +561,7 @@
{% endif %}
</span>
</th>
<th>Actions</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@@ -582,11 +582,11 @@
<td>{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') if file.created_at else 'N/A' }}</td>
<td>
<div style="display: flex; align-items: center;">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle"></i>
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" aria-label="View details for {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash"></i>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" aria-label="Delete {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</div>
</td>
@@ -621,11 +621,11 @@
<div class="flex flex-col items-end gap-2 flex-shrink-0">
<span class="status-badge status-{{ file.processing_status }}">{{ file.processing_status | title }}</span>
<div class="flex gap-1" role="group" aria-label="File actions" onclick="event.stopPropagation();" onkeydown="event.stopPropagation();">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle"></i>
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" aria-label="View details for {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash"></i>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" aria-label="Delete {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -638,36 +638,36 @@
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
<div class="pagination flex-wrap gap-2">
<div class="pagination-info">
<nav class="pagination flex-wrap gap-2" aria-label="File list pagination">
<div class="pagination-info" aria-live="polite">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }}
of {{ pagination.total_items }} files
</div>
<div class="pagination-buttons flex-wrap">
{% if pagination.page > 1 %}
<button class="pagination-button" onclick="goToPage(1)" style="min-height:44px;">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" style="min-height:44px;">Previous</button>
<button class="pagination-button" onclick="goToPage(1)" aria-label="Go to first page" style="min-height:44px;">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="Go to previous page" style="min-height:44px;">Previous</button>
{% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.total_pages + 1, pagination.page + 3)) %}
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" style="min-height:44px;">
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" aria-label="Page {{ p }}" {% if p == pagination.page %}aria-current="page"{% endif %} style="min-height:44px;">
{{ p }}
</button>
{% endfor %}
{% if pagination.page < pagination.total_pages %}
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})" style="min-height:44px;">Last</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" aria-label="Go to next page" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})" aria-label="Go to last page" style="min-height:44px;">Last</button>
{% endif %}
</div>
</div>
</nav>
{% endif %}
<!-- Delete confirmation modal -->
<div id="deleteModal" class="modal">
<div id="deleteModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="deleteModalTitle">
<div class="modal-content">
<div class="modal-title">Confirm Deletion</div>
<div class="modal-title" id="deleteModalTitle">Confirm Deletion</div>
<p>Are you sure you want to delete this file?</p>
<div class="modal-buttons">
<button id="cancelDelete" class="modal-btn modal-btn-cancel" style="min-height:44px;min-width:80px;">Cancel</button>
+17 -17
View File
@@ -13,10 +13,10 @@
</div>
<div class="flex flex-wrap gap-3">
<a href="/upload" class="bg-white text-blue-700 hover:bg-blue-50 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center shadow-sm">
<i class="fas fa-upload mr-2"></i> Upload
<i class="fas fa-upload mr-2" aria-hidden="true"></i> Upload
</a>
<a href="/files" class="bg-transparent border border-white text-white hover:bg-white hover:text-blue-700 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center">
<i class="fas fa-list mr-2"></i> Browse Files
<i class="fas fa-list mr-2" aria-hidden="true"></i> Browse Files
</a>
</div>
</div>
@@ -26,7 +26,7 @@
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl shadow p-6 flex items-center gap-4">
<div class="h-12 w-12 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-file-alt text-blue-600 text-xl"></i>
<i class="fas fa-file-alt text-blue-600 text-xl" aria-hidden="true"></i>
</div>
<div>
<p class="text-3xl font-bold text-blue-600">{{ stats.processed_files | default(0) }}</p>
@@ -35,7 +35,7 @@
</div>
<div class="bg-white rounded-xl shadow p-6 flex items-center gap-4">
<div class="h-12 w-12 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-plug text-green-600 text-xl"></i>
<i class="fas fa-plug text-green-600 text-xl" aria-hidden="true"></i>
</div>
<div>
<p class="text-3xl font-bold text-green-600">{{ stats.active_integrations | default(0) }}</p>
@@ -44,7 +44,7 @@
</div>
<div class="bg-white rounded-xl shadow p-6 flex items-center gap-4">
<div class="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-database text-indigo-600 text-xl"></i>
<i class="fas fa-database text-indigo-600 text-xl" aria-hidden="true"></i>
</div>
<div>
<p class="text-3xl font-bold text-indigo-600">{{ stats.storage_targets | default(0) }}</p>
@@ -59,13 +59,13 @@
<!-- Quick Actions Widget -->
<div class="bg-white rounded-xl shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-bolt text-yellow-500"></i> Quick Actions
<i class="fas fa-bolt text-yellow-500" aria-hidden="true"></i> Quick Actions
</h2>
<ul class="space-y-2">
<li>
<a href="/upload" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0 group-hover:bg-blue-200">
<i class="fas fa-upload text-blue-600 text-sm"></i>
<i class="fas fa-upload text-blue-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">Upload Document</p>
@@ -76,7 +76,7 @@
<li>
<a href="/files" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0 group-hover:bg-indigo-200">
<i class="fas fa-list text-indigo-600 text-sm"></i>
<i class="fas fa-list text-indigo-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">View All Files</p>
@@ -87,7 +87,7 @@
<li>
<a href="/status" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0 group-hover:bg-green-200">
<i class="fas fa-heartbeat text-green-600 text-sm"></i>
<i class="fas fa-heartbeat text-green-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">System Status</p>
@@ -101,27 +101,27 @@
<!-- Capabilities Widget -->
<div class="bg-white rounded-xl shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-cubes text-blue-500"></i> Capabilities
<i class="fas fa-cubes text-blue-500" aria-hidden="true"></i> Capabilities
</h2>
<ul class="space-y-3 text-sm text-gray-600">
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0"></i>
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>OCR &amp; metadata extraction with AI</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0"></i>
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0"></i>
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>Paperless-ngx integration for document management</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0"></i>
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>Email &amp; URL-based document ingestion</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0"></i>
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>Automated classification &amp; routing workflows</span>
</li>
</ul>
@@ -131,7 +131,7 @@
<div class="bg-white rounded-xl shadow p-6 flex flex-col justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-compass text-indigo-500"></i> Getting Started
<i class="fas fa-compass text-indigo-500" aria-hidden="true"></i> Getting Started
</h2>
<ol class="space-y-3 text-sm text-gray-600 list-none">
<li class="flex items-start gap-2">
@@ -149,7 +149,7 @@
</ol>
</div>
<a href="/about" class="mt-6 inline-flex items-center text-sm text-indigo-600 hover:text-indigo-800 font-medium">
Learn more about DocuElevate <i class="fas fa-arrow-right ml-1 text-xs"></i>
Learn more about DocuElevate <i class="fas fa-arrow-right ml-1 text-xs" aria-hidden="true"></i>
</a>
</div>
</div>
+3 -5
View File
@@ -10,7 +10,7 @@
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-gray-100 h-screen flex items-center justify-center">
<div class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full">
<main class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full" role="main">
<div class="flex justify-center mb-6">
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
</div>
@@ -67,7 +67,7 @@
<a href="/oauth-login"
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<span class="sr-only">Sign in with SSO</span>
<i class="fas fa-lock mr-2"></i>
<i class="fas fa-lock mr-2" aria-hidden="true"></i>
{{ oauth_provider_name }}
</a>
</div>
@@ -78,9 +78,7 @@
Return to Home
</a>
</div>
</div>
<!-- Optional footer with version info -->
</main>
<div class="fixed bottom-4 text-center w-full text-xs text-gray-500">
DocuElevate {{ app_version|default('', true) }}
</div>
+12 -12
View File
@@ -11,14 +11,14 @@
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold mb-2">
<i class="fas fa-stream mr-2 text-blue-500"></i>Queue Monitor
<i class="fas fa-stream mr-2 text-blue-500" aria-hidden="true"></i>Queue Monitor
</h1>
<p class="text-gray-600">Real-time view of the document processing pipeline and Celery task queues.</p>
</div>
<!-- Loading state -->
<div id="queueLoading" class="text-center py-12">
<i class="fas fa-spinner fa-spin fa-2x text-blue-400"></i>
<i class="fas fa-spinner fa-spin fa-2x text-blue-400" aria-hidden="true"></i>
<p class="mt-2 text-gray-500">Loading queue statistics&hellip;</p>
</div>
@@ -31,12 +31,12 @@
<div id="queueContent" class="hidden">
<!-- Summary Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8" aria-live="polite">
<!-- Queued -->
<div class="bg-white rounded-lg shadow p-5">
<div class="flex items-center">
<div class="flex-shrink-0 bg-yellow-100 rounded-full p-3">
<i class="fas fa-inbox text-yellow-600 text-xl"></i>
<i class="fas fa-inbox text-yellow-600 text-xl" aria-hidden="true"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Queued Tasks</p>
@@ -48,7 +48,7 @@
<div class="bg-white rounded-lg shadow p-5">
<div class="flex items-center">
<div class="flex-shrink-0 bg-blue-100 rounded-full p-3">
<i class="fas fa-cogs text-blue-600 text-xl"></i>
<i class="fas fa-cogs text-blue-600 text-xl" aria-hidden="true"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Active Tasks</p>
@@ -60,7 +60,7 @@
<div class="bg-white rounded-lg shadow p-5">
<div class="flex items-center">
<div class="flex-shrink-0 bg-indigo-100 rounded-full p-3">
<i class="fas fa-file-alt text-indigo-600 text-xl"></i>
<i class="fas fa-file-alt text-indigo-600 text-xl" aria-hidden="true"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Files Processing</p>
@@ -72,7 +72,7 @@
<div class="bg-white rounded-lg shadow p-5">
<div class="flex items-center">
<div class="flex-shrink-0 bg-green-100 rounded-full p-3">
<i class="fas fa-server text-green-600 text-xl"></i>
<i class="fas fa-server text-green-600 text-xl" aria-hidden="true"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Workers Online</p>
@@ -89,7 +89,7 @@
<div class="bg-white rounded-lg shadow">
<div class="px-5 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">
<i class="fas fa-list-ol mr-2 text-gray-500"></i>Redis Queues
<i class="fas fa-list-ol mr-2 text-gray-500" aria-hidden="true"></i>Redis Queues
</h2>
</div>
<div class="p-5">
@@ -111,7 +111,7 @@
<div class="bg-white rounded-lg shadow">
<div class="px-5 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">
<i class="fas fa-database mr-2 text-gray-500"></i>Processing Pipeline
<i class="fas fa-database mr-2 text-gray-500" aria-hidden="true"></i>Processing Pipeline
</h2>
</div>
<div class="p-5">
@@ -134,7 +134,7 @@
<div class="bg-white rounded-lg shadow mb-8">
<div class="px-5 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">
<i class="fas fa-play-circle mr-2 text-blue-500"></i>Active Tasks
<i class="fas fa-play-circle mr-2 text-blue-500" aria-hidden="true"></i>Active Tasks
<span id="activeCount" class="ml-1 text-sm font-normal text-gray-500">(0)</span>
</h2>
</div>
@@ -158,7 +158,7 @@
<div class="bg-white rounded-lg shadow mb-8">
<div class="px-5 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">
<i class="fas fa-file-medical-alt mr-2 text-indigo-500"></i>Recently Processing Files
<i class="fas fa-file-medical-alt mr-2 text-indigo-500" aria-hidden="true"></i>Recently Processing Files
</h2>
</div>
<div class="p-5 overflow-x-auto">
@@ -179,7 +179,7 @@
<!-- Auto-refresh info -->
<div class="text-center text-gray-400 text-xs mb-4">
<i class="fas fa-sync-alt mr-1"></i>
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>
Auto-refreshes every <span id="refreshInterval">10</span> seconds &mdash;
Last updated: <span id="lastUpdated">—</span>
</div>
+8 -7
View File
@@ -84,27 +84,28 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="search-container">
<h1 class="text-3xl font-bold mb-6"><i class="fas fa-search text-blue-500"></i> Document Search</h1>
<h1 class="text-3xl font-bold mb-6"><i class="fas fa-search text-blue-500" aria-hidden="true"></i> Document Search</h1>
<!-- Search box -->
<div class="search-box">
<div class="search-box" role="search">
<label for="search-input" class="sr-only">Search documents</label>
<input
type="text"
type="search"
id="search-input"
placeholder="Search documents by content, sender, tags, type..."
value="{{ query }}"
autofocus
>
<button type="button" id="search-btn">
<i class="fas fa-search"></i> Search
<button type="button" id="search-btn" aria-label="Search documents">
<i class="fas fa-search" aria-hidden="true"></i> Search
</button>
</div>
<!-- Summary -->
<div id="search-summary" class="search-summary" style="display:none;"></div>
<div id="search-summary" class="search-summary" style="display:none;" aria-live="polite"></div>
<!-- Results -->
<div id="search-results"></div>
<div id="search-results" aria-live="polite" aria-relevant="additions removals"></div>
<!-- Pagination -->
<div id="search-pagination" class="search-pagination" style="display:none;"></div>
+10 -9
View File
@@ -23,31 +23,31 @@
<div class="flex items-center gap-2">
<a href="/setup?step=1"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-magic mr-2"></i> Setup Wizard
<i class="fas fa-magic mr-2" aria-hidden="true"></i> Setup Wizard
</a>
<div class="relative" x-data="{ exportOpen: false }">
<button @click="exportOpen = !exportOpen"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-download mr-2"></i> Export .env
<i class="fas fa-chevron-down ml-1 text-xs"></i>
<i class="fas fa-download mr-2" aria-hidden="true"></i> Export .env
<i class="fas fa-chevron-down ml-1 text-xs" aria-hidden="true"></i>
</button>
<div x-show="exportOpen" @click.outside="exportOpen = false" x-transition
class="absolute right-0 mt-1 w-52 bg-white border border-gray-200 rounded-md shadow-lg z-10">
<a href="/api/settings/export-env?source=db"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
@click="exportOpen = false">
<i class="fas fa-database mr-2 text-green-600"></i>DB settings only
<i class="fas fa-database mr-2 text-green-600" aria-hidden="true"></i>DB settings only
</a>
<a href="/api/settings/export-env?source=effective"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
@click="exportOpen = false">
<i class="fas fa-layer-group mr-2 text-blue-600"></i>Full effective config
<i class="fas fa-layer-group mr-2 text-blue-600" aria-hidden="true"></i>Full effective config
</a>
</div>
</div>
<a href="/admin/settings/audit-log"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-history mr-2"></i> Audit Log
<i class="fas fa-history mr-2" aria-hidden="true"></i> Audit Log
</a>
</div>
</div>
@@ -179,7 +179,7 @@
{% endfor %}
</datalist>
<p class="text-xs text-gray-400 mt-1">
<i class="fas fa-info-circle mr-1"></i>
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
Pick from the list or type any model name supported by your provider.
</p>
</div>
@@ -222,6 +222,7 @@
@click="togglePassword('{{ setting.key }}')"
class="text-gray-400 hover:text-gray-600 focus:outline-none"
title="Show/hide value"
aria-label="Toggle password visibility"
>
<i :class="showPassword['{{ setting.key }}'] ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</button>
@@ -253,7 +254,7 @@
class="px-3 py-1 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
title="Remove DB override and revert to environment variable or default"
>
<span x-show="revertingKey !== '{{ setting.key }}'"><i class="fas fa-trash-alt mr-1"></i>Remove from DB</span>
<span x-show="revertingKey !== '{{ setting.key }}'"><i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>Remove from DB</span>
<span x-show="revertingKey === '{{ setting.key }}'">Reverting…</span>
</button>
<button
@@ -265,7 +266,7 @@
class="px-3 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
title="Save this setting"
>
<span x-show="savingKey !== '{{ setting.key }}'"><i class="fas fa-save mr-1"></i>Save</span>
<span x-show="savingKey !== '{{ setting.key }}'"><i class="fas fa-save mr-1" aria-hidden="true"></i>Save</span>
<span x-show="savingKey === '{{ setting.key }}'">Saving…</span>
</button>
</div>
+6 -6
View File
@@ -31,14 +31,14 @@
</div>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3" aria-live="polite">
{% for name, provider in providers.items() %}
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
<div class="flex items-center">
<span class="inline-flex items-center justify-center h-10 w-10 rounded-md {{ 'bg-green-500' if provider.configured else 'bg-gray-400' }} text-white mr-3">
<!-- Use Font Awesome icon -->
<i class="{{ provider.icon }} fa-lg"></i>
<i class="{{ provider.icon }} fa-lg" aria-hidden="true"></i>
</span>
<h3 class="text-lg leading-6 font-medium text-gray-900">{{ name }}</h3>
</div>
@@ -232,7 +232,7 @@
</div>
<!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="modalTitle">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
<div class="mt-3 text-center">
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
@@ -254,7 +254,7 @@
</div>
<!-- Details Modal -->
<div id="detailsModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div id="detailsModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="detailsModalTitle">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white">
<div class="absolute top-0 right-0 pt-4 pr-4">
<button type="button" id="closeDetailsModal" class="text-gray-400 hover:text-gray-500">
@@ -281,7 +281,7 @@
</div>
<!-- AI Extraction Test Modal -->
<div id="aiExtractionModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div id="aiExtractionModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog" aria-labelledby="aiExtractionModalTitle">
<div class="relative top-10 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-2/3 xl:w-1/2 shadow-lg rounded-md bg-white">
<div class="absolute top-0 right-0 pt-4 pr-4">
<button type="button" id="closeAiExtractionModal" class="text-gray-400 hover:text-gray-500">
@@ -290,7 +290,7 @@
</button>
</div>
<div class="mt-3">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-1">
<h3 id="aiExtractionModalTitle" class="text-lg leading-6 font-medium text-gray-900 mb-1">
<i class="fa-solid fa-flask mr-2 text-indigo-600"></i>AI Extraction Test
</h3>
<p class="text-sm text-gray-500 mb-4">
+10 -5
View File
@@ -12,6 +12,9 @@
<div
id="dropZone"
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full"
role="button"
tabindex="0"
aria-label="File upload area. Drag and drop files here, or press Enter to browse files."
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
>
@@ -28,6 +31,7 @@
onchange="handleFileSelect(event)"
name="files"
multiple
aria-label="Select files to upload"
accept="application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,image/*"
/>
<!-- Browse Files button (all devices) -->
@@ -37,7 +41,7 @@
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 mb-4"
style="min-height:44px;"
>
<i class="fas fa-folder-open mr-2"></i> Browse Files
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> Browse Files
</button>
<div class="text-sm text-gray-500 mt-2">
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
@@ -51,7 +55,7 @@
class="w-full flex items-center justify-center gap-2 bg-green-500 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 cursor-pointer"
style="min-height:44px;"
>
<i class="fas fa-camera"></i> Take Photo / Scan Document
<i class="fas fa-camera" aria-hidden="true"></i> Take Photo / Scan Document
<input
id="cameraInput"
type="file"
@@ -59,6 +63,7 @@
onchange="handleFileSelect(event)"
accept="image/*"
capture="environment"
aria-label="Take photo or scan document"
/>
</label>
</div>
@@ -111,12 +116,12 @@
Download and Process
</button>
</form>
<div id="urlStatusMessage" class="mt-4"></div>
<div id="urlStatusMessage" class="mt-4" aria-live="polite"></div>
</div>
</div>
<div id="statusMessage" class="mt-4 text-gray-700"></div>
<div id="uploadProgress" class="mt-4 w-full max-w-2xl"></div>
<div id="statusMessage" class="mt-4 text-gray-700" aria-live="polite"></div>
<div id="uploadProgress" class="mt-4 w-full max-w-2xl" role="status" aria-live="polite"></div>
</div>
{% endblock %}
+11
View File
@@ -225,3 +225,14 @@ exclude_lines = [
"if TYPE_CHECKING:",
"@abstractmethod",
]
# djLint configuration — HTML template linter for Jinja2 templates
# Enforces accessibility (WCAG 2.1 AA) and HTML best practices in CI.
# Run: djlint frontend/templates/ --profile jinja --lint
[tool.djlint]
profile = "jinja"
# Include only accessibility-critical and correctness rules for CI enforcement
include = "H005,H013,H016,H025,H026"
# Ignore rules that conflict with project patterns or produce excessive noise
ignore = "H006,H021,H023,H030,H031,H029,H036,T003,J018,J004,T028,H014,T001"
max_line_length = 120
+1
View File
@@ -16,6 +16,7 @@ boto3>=1.26.0 # For S3 integration tests
# Code quality
ruff>=0.3.0
mypy>=1.8.0
djlint>=1.36.0 # HTML template linter for accessibility and best practices
# Type stubs for mypy
types-requests>=2.31.0