Release v1.5.10

This commit is contained in:
2026-07-26 12:32:01 +01:00
parent b5fbf79af7
commit 07ec17ad91
24 changed files with 1113 additions and 352 deletions
@@ -33,7 +33,7 @@
<div id="background-tasks-task-card" class="card card-outline card-primary mb-4" data-pagination-card="tasks" data-table-search-container>
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0">Task queue</h3>
<h3 class="card-title mb-0">Task Queue</h3>
<div class="ms-auto d-flex flex-nowrap align-items-center gap-2 background-tasks-task-tools">
<div class="input-group input-group-sm background-tasks-task-search" style="width: min(12rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
@@ -59,7 +59,6 @@
<th>Started</th>
<th>Finished</th>
<th>Source</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@@ -120,27 +119,11 @@
<span class="empty">-</span>
{{/if}}
</td>
<td data-label="Actions">
<div class="d-flex flex-wrap gap-2">
{{#if canManage}}
{{#if (eq status 'queued')}}
<form method="post" action="/settings/background-tasks/{{id}}/cancel">
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm-message="Cancel this queued task?">Cancel</button>
</form>
{{/if}}
{{#if (eq status 'failed')}}
<form method="post" action="/settings/background-tasks/{{id}}/retry">
<button type="submit" class="btn btn-outline-primary btn-sm">Retry</button>
</form>
{{/if}}
{{/if}}
</div>
</td>
</tr>
{{/each}}
{{else}}
<tr data-table-search-empty-default>
<td colspan="8" class="empty">No background tasks are queued right now.</td>
<td colspan="7" class="empty">No background tasks are queued right now.</td>
</tr>
{{/if}}
</tbody>
@@ -169,94 +152,4 @@
</div>
{{/if}}
</div>
<div id="background-tasks-recurring-card" class="card card-outline card-secondary" data-pagination-card="recurring" data-table-search-container>
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0">Scheduled refreshes</h3>
<div class="ms-auto d-flex flex-nowrap align-items-center gap-2 background-tasks-recurring-tools">
<div class="input-group input-group-sm background-tasks-recurring-search" style="width: min(12rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search scheduled" aria-label="Search scheduled refreshes" data-table-search />
</div>
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 align-middle" data-table-searchable>
<thead>
<tr>
<th>Task</th>
<th>Interval</th>
<th>Next run</th>
<th>Last run</th>
<th>Last result</th>
<th>Source</th>
</tr>
</thead>
<tbody>
{{#if recurringTasks.length}}
{{#each recurringTasks}}
<tr data-table-search-row>
<td data-label="Task">
<div class="fw-semibold">{{title}}</div>
<div class="text-muted small text-break">{{key}}</div>
</td>
<td data-label="Interval">{{intervalLabel}}</td>
<td data-label="Next run">
{{#if nextRunAt}}
<time data-local-datetime datetime="{{nextRunAt}}">{{nextRunAt}}</time>
{{else}}-{{/if}}
</td>
<td data-label="Last run">
{{#if lastRunAt}}
<time data-local-datetime datetime="{{lastRunAt}}">{{lastRunAt}}</time>
{{else}}-{{/if}}
</td>
<td data-label="Last result">
{{#if lastStatus}}
<span class="badge {{#if (eq lastStatus 'failed')}}text-bg-danger{{else if (eq lastStatus 'completed')}}text-bg-success{{else}}text-bg-secondary{{/if}}">{{lastStatus}}</span>
{{#if lastError}}<div class="text-danger small mt-1 text-break">{{lastError}}</div>{{/if}}
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
<td data-label="Source">
{{#if metadata.sourceName}}
<div>{{metadata.sourceName}}</div>
<div class="text-muted small">{{metadata.sourceType}} #{{metadata.sourceId}}</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr data-table-search-empty-default>
<td colspan="6" class="empty">No scheduled refreshes are registered yet.</td>
</tr>
{{/if}}
</tbody>
</table>
</div>
{{#if recurringPagination.hasMultiplePages}}
<div class="card-footer d-flex flex-wrap align-items-center justify-content-between gap-3">
<div class="text-muted small">
Showing {{recurringPagination.startItem}}-{{recurringPagination.endItem}} of {{recurringPagination.totalItems}} scheduled refreshes
</div>
<nav aria-label="Scheduled refresh pages">
<ul class="pagination pagination-sm mb-0">
<li class="page-item {{#unless recurringPagination.hasPrevious}}disabled{{/unless}}">
<a class="page-link" href="{{#if recurringPagination.hasPrevious}}/settings/background-tasks{{recurringPagination.previousUrl}}{{else}}#{{/if}}" aria-label="Previous page">Previous</a>
</li>
{{#each recurringPagination.pages}}
<li class="page-item {{#if active}}active{{/if}}">
<a class="page-link" href="/settings/background-tasks{{url}}">{{number}}</a>
</li>
{{/each}}
<li class="page-item {{#unless recurringPagination.hasNext}}disabled{{/unless}}">
<a class="page-link" href="{{#if recurringPagination.hasNext}}/settings/background-tasks{{recurringPagination.nextUrl}}{{else}}#{{/if}}" aria-label="Next page">Next</a>
</li>
</ul>
</nav>
</div>
{{/if}}
</div>
@@ -0,0 +1,87 @@
<div id="background-tasks-state" data-state-version="{{stateVersion}}" hidden></div>
<div class="page-header">
<div>
<h2>Scheduled tasks</h2>
<p>Use this page to view and manage recurring tasks.</p>
</div>
</div>
<div id="background-tasks-recurring-card" class="card card-outline card-secondary" data-pagination-card="recurring" data-table-search-container>
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0">Scheduled Tasks</h3>
<div class="ms-auto d-flex flex-nowrap align-items-center background-tasks-recurring-tools">
<div class="input-group input-group-sm background-tasks-recurring-search" style="width: min(12rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search scheduled" aria-label="Search scheduled refreshes" data-table-search />
</div>
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 align-middle" data-table-searchable>
<thead>
<tr>
<th>Task</th>
<th>Interval</th>
<th>Next run</th>
<th>Last run</th>
<th>Last result</th>
<th>Source</th>
{{#if canManage}}<th>Actions</th>{{/if}}
</tr>
</thead>
<tbody>
{{#if recurringTasks.length}}
{{#each recurringTasks}}
<tr data-table-search-row>
<td data-label="Task">
<div class="fw-semibold">{{title}}</div>
<div class="text-muted small text-break">{{key}}</div>
</td>
<td data-label="Interval">{{intervalLabel}}</td>
<td data-label="Next run">
{{#if nextRunAt}}
<time data-local-datetime datetime="{{nextRunAt}}">{{nextRunAt}}</time>
{{else}}-{{/if}}
</td>
<td data-label="Last run">
{{#if lastRunAt}}
<time data-local-datetime datetime="{{lastRunAt}}">{{lastRunAt}}</time>
{{else}}-{{/if}}
</td>
<td data-label="Last result">
{{#if lastStatus}}
<span class="badge text-capitalize {{#if (eq lastStatus 'failed')}}text-bg-danger{{else if (eq lastStatus 'completed')}}text-bg-success{{else}}text-bg-secondary{{/if}}">{{lastStatus}}</span>
{{#if lastError}}<div class="text-danger small mt-1 text-break">{{lastError}}</div>{{/if}}
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
<td data-label="Source">
{{#if metadata.sourceName}}
<div>{{metadata.sourceName}}</div>
<div class="text-muted small">{{metadata.sourceType}} #{{metadata.sourceId}}</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
{{#if ../canManage}}
<td data-label="Actions">
<div class="d-flex flex-wrap gap-2">
<form method="post" action="/settings/background-tasks/recurring/{{key}}/run">
<button type="submit" class="btn btn-outline-primary btn-sm">Run now</button>
</form>
</div>
</td>
{{/if}}
</tr>
{{/each}}
{{else}}
<tr data-table-search-empty-default>
<td colspan="{{#if canManage}}7{{else}}6{{/if}}" class="empty">No scheduled refreshes are registered yet.</td>
</tr>
{{/if}}
</tbody>
</table>
</div>
</div>
+27 -6
View File
@@ -213,7 +213,7 @@
</li>
{{/if}}
{{/if}}
{{#if (anyPermission currentUser 'background-tasks.read' 'users.read' 'rbac.read')}}
{{#if (anyPermission currentUser 'background-tasks.read' 'scheduled-tasks.read' 'users.read' 'rbac.read')}}
<li class="nav-header">SETTINGS</li>
{{#if (hasPermission currentUser 'users.read')}}
<li class="nav-item">
@@ -231,12 +231,33 @@
</a>
</li>
{{/if}}
{{#if (anyPermission currentUser 'background-tasks.read')}}
<li class="nav-item">
<a class="nav-link {{#if (eq active 'background-tasks')}}active{{/if}}" href="/settings/background-tasks">
<i class="nav-icon bi bi-list-task"></i>
<p>Background tasks</p>
{{#if (anyPermission currentUser 'background-tasks.read' 'scheduled-tasks.read')}}
<li class="nav-item has-treeview{{#if backgroundTasksMenuOpen}} menu-open{{/if}}">
<a class="nav-link{{#if backgroundTasksMenuOpen}} active{{/if}}" href="#">
<i class="nav-icon bi bi-pen"></i>
<p>
Tasks
<i class="nav-arrow bi bi-chevron-right"></i>
</p>
</a>
{{#if (anyPermission currentUser 'background-tasks.read')}}
<ul class="nav nav-treeview">
<li class="nav-item">
<a class="nav-link {{#if (eq active 'background-tasks')}}active{{/if}}" href="/settings/background-tasks">
<i class="nav-icon bi bi-list-ol"></i>
<p>Task queue</p>
</a>
</li>
{{/if}}
{{#if (hasPermission currentUser 'scheduled-tasks.read')}}
<li class="nav-item">
<a class="nav-link {{#if (eq active 'background-tasks-scheduled')}}active{{/if}}" href="/settings/background-tasks/scheduled">
<i class="nav-icon bi bi-calendar-event"></i>
<p>Scheduled tasks</p>
</a>
</li>
{{/if}}
</ul>
</li>
{{/if}}
{{/if}}
+5 -170
View File
@@ -48,7 +48,7 @@
</div>
<aside class="card card-outline card-secondary admin-form-card slide-preview-card">
<div class="card-header d-flex align-items-center justify-content-between gap-2">
<div class="card-header d-flex align-items-center justify-content-between">
<div class="slide-preview-header">
<h3>Preview</h3>
</div>
@@ -57,7 +57,7 @@
<div class="card-body slide-preview-scroll">
<div class="slide-preview-stage" id="slide-preview-stage">
<div class="slide-preview-canvas" id="slide-preview-canvas">
<img id="slide-preview-background" class="slide-preview-background" alt="" style="display:none;" />
<img id="slide-preview-background" class="slide-preview-background d-none" alt="" />
<div class="slide-preview-empty" id="slide-preview-empty">Select a template to preview the slide.</div>
<div class="slide-preview-overlay" id="slide-preview-overlay"></div>
</div>
@@ -76,7 +76,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-grid gap-3">
<div class="slide-image-cropper-frame" style="width:100%;height:560px;max-height:70vh;overflow:hidden;">
<div class="slide-image-cropper-frame">
<img id="slide-image-cropper-image" alt="Selected image to crop" />
</div>
<div class="btn-toolbar flex-wrap gap-2 slide-image-cropper-toolbar" role="toolbar" aria-label="Image editing controls">
@@ -107,177 +107,12 @@
</div>
{{/modal-shell}}
<style>
.slide-image-cropper-frame {
position: relative;
width: 100%;
height: 560px;
min-height: 360px;
max-height: 70vh;
overflow: hidden;
border: 0;
border-radius: 0;
background: var(--bs-body-bg);
}
.slide-image-cropper-frame img {
display: block;
max-width: 100%;
max-height: 65vh;
}
.slide-image-cropper-frame > .cropper-container {
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 100% !important;
display: block;
}
.slide-image-cropper-frame > .cropper-container img {
max-width: none !important;
max-height: none !important;
}
.slide-image-cropper-frame .cropper-container,
.slide-image-cropper-frame .cropper-canvas,
.slide-image-cropper-frame .cropper-wrap-box,
.slide-image-cropper-frame .cropper-crop-box,
.slide-image-cropper-frame .cropper-view-box,
.slide-image-cropper-frame .cropper-face {
border-radius: 0 !important;
}
.slide-image-cropper-frame .cropper-wrap-box,
.slide-image-cropper-frame .cropper-crop-box {
border: 0 !important;
}
.slide-image-cropper-toolbar .btn {
white-space: nowrap;
}
.slide-image-region-preview-box {
display: grid;
gap: 0;
justify-items: end;
}
.slide-image-region-preview-shell {
position: relative;
display: block;
width: 100%;
max-width: 288px;
height: 176px;
cursor: pointer;
}
.slide-image-region-preview-shell:focus-visible,
.slide-image-region-preview-shell:hover .slide-image-region-preview-remove,
.slide-image-region-preview-shell:focus-visible .slide-image-region-preview-remove {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.slide-image-region-preview-remove {
position: absolute;
top: 50%;
left: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 999px;
background: rgba(33, 37, 41, 0.82);
color: #fff;
opacity: 0;
transform: translate(-50%, -50%) scale(0.92);
transition: opacity 0.15s ease, transform 0.15s ease, background-color 0.15s ease;
pointer-events: none;
}
.slide-image-region-preview-shell:hover .slide-image-region-preview-remove,
.slide-image-region-preview-shell:focus-visible .slide-image-region-preview-remove {
background: rgba(var(--bs-danger-rgb), 0.92);
}
.slide-image-region-preview-remove i {
font-size: 1.5rem;
line-height: 1;
}
.slide-image-region-upload-zone {
flex: 1 1 auto;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 0.9rem;
min-height: 176px;
padding: 1rem 1.1rem;
border: 1px dashed var(--bs-border-color);
border-radius: 0;
background: var(--bs-tertiary-bg);
color: var(--bs-body-color);
cursor: pointer;
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}
.slide-image-region-upload-zone:hover,
.slide-image-region-upload-zone:focus-within,
.slide-image-region-upload-zone.is-dragover {
border-color: var(--bs-primary);
background: var(--bs-secondary-bg);
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.12);
}
.slide-image-region-upload-zone-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
font-size: 1.25rem;
color: var(--bs-primary);
}
.slide-image-region-upload-zone-copy {
display: grid;
gap: 0.2rem;
}
.slide-image-region-upload-zone-copy span {
color: var(--bs-secondary-color);
font-size: 0.875rem;
}
.slide-image-region-preview {
display: block;
width: 100%;
max-width: 288px;
height: 176px;
object-fit: contain;
border-radius: 0;
background: var(--bs-tertiary-bg);
border: 0;
}
.slide-image-region-preview-empty {
display: flex;
align-items: center;
justify-content: center;
border: 0;
color: var(--bs-secondary-color);
font-size: 0.8rem;
}
</style>
{{#if isEdit}}
<form id="delete-slide-form" method="post" action="/slides/{{slide.id}}/delete" data-confirm-message="Delete this slide?"></form>
{{/if}}
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
<script src="/assets/vendor/cropperjs/cropper.min.js"></script>
<script type="module" src="/assets/js/slides/slide-form.js?v=20260725.7"></script>
<script type="module" src="/assets/js/slides/slide-image-cropper.js?v=20260725.4"></script>
<script type="module" src="/assets/js/slides/slide-form.js?v=20260726.4"></script>
<script type="module" src="/assets/js/slides/slide-image-cropper.js?v=20260726.4"></script>