Added Cron Manager to Admin Panel for long running jobs, like running migrations when opening board, copying or moving boards swimlanes lists cards etc.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2025-10-11 19:41:09 +03:00
parent e90bc744d9
commit da68b01502
10 changed files with 2577 additions and 10 deletions

View file

@ -12,3 +12,6 @@ import '/imports/components/boardConversionProgress';
// Import migration manager and progress UI
import '/imports/lib/migrationManager';
import '/imports/components/migrationProgress';
// Import cron settings
import '/imports/components/settings/cronSettings';

View file

@ -0,0 +1,806 @@
/* Cron Settings Styles */
.cron-settings-content {
min-height: 600px;
}
.cron-migrations {
padding: 20px;
}
.migration-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.migration-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.migration-header h2 i {
margin-right: 10px;
color: #667eea;
}
.migration-controls {
display: flex;
gap: 10px;
}
.migration-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.migration-controls .btn-primary {
background-color: #28a745;
color: white;
}
.migration-controls .btn-primary:hover {
background-color: #218838;
}
.migration-controls .btn-warning {
background-color: #ffc107;
color: #212529;
}
.migration-controls .btn-warning:hover {
background-color: #e0a800;
}
.migration-controls .btn-danger {
background-color: #dc3545;
color: white;
}
.migration-controls .btn-danger:hover {
background-color: #c82333;
}
.migration-progress {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border-left: 4px solid #667eea;
}
.progress-overview {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 6px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-text {
text-align: center;
font-weight: 700;
color: #667eea;
font-size: 18px;
}
.progress-label {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 4px;
}
.current-step {
text-align: center;
color: #333;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
.current-step i {
margin-right: 8px;
color: #667eea;
}
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #bbdefb;
}
.migration-status i {
margin-right: 8px;
color: #2196f3;
}
.migration-steps {
margin-top: 30px;
}
.migration-steps h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
.steps-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.migration-step {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.migration-step:last-child {
border-bottom: none;
}
.migration-step.completed {
background-color: #d4edda;
border-left: 4px solid #28a745;
}
.migration-step.current {
background-color: #cce7ff;
border-left: 4px solid #667eea;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
.step-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.step-icon {
margin-right: 12px;
font-size: 18px;
width: 24px;
text-align: center;
}
.step-icon i.fa-check-circle {
color: #28a745;
}
.step-icon i.fa-cog.fa-spin {
color: #667eea;
}
.step-icon i.fa-circle-o {
color: #ccc;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.step-description {
color: #666;
font-size: 12px;
line-height: 1.3;
}
.step-progress {
text-align: right;
min-width: 40px;
}
.step-progress .progress-text {
font-size: 12px;
font-weight: 600;
}
.step-progress-bar {
width: 100%;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.step-progress-bar .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
transition: width 0.3s ease;
}
/* Cron Jobs Styles */
.cron-jobs {
padding: 20px;
}
.jobs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.jobs-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.jobs-header h2 i {
margin-right: 10px;
color: #667eea;
}
.jobs-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.jobs-controls .btn-success {
background-color: #28a745;
color: white;
}
.jobs-controls .btn-success:hover {
background-color: #218838;
}
.jobs-list {
margin-top: 20px;
}
.table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.table thead {
background-color: #f8f9fa;
}
.table th,
.table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.table th {
font-weight: 600;
color: #333;
font-size: 14px;
}
.table td {
font-size: 14px;
color: #666;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.status-running {
background-color: #d4edda;
color: #155724;
}
.status-badge.status-stopped {
background-color: #f8d7da;
color: #721c24;
}
.status-badge.status-paused {
background-color: #fff3cd;
color: #856404;
}
.status-badge.status-completed {
background-color: #d1ecf1;
color: #0c5460;
}
.status-badge.status-error {
background-color: #f8d7da;
color: #721c24;
}
.btn-group {
display: flex;
gap: 4px;
}
.btn-group .btn {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-group .btn-success {
background-color: #28a745;
color: white;
}
.btn-group .btn-success:hover {
background-color: #218838;
}
.btn-group .btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-group .btn-warning:hover {
background-color: #e0a800;
}
.btn-group .btn-danger {
background-color: #dc3545;
color: white;
}
.btn-group .btn-danger:hover {
background-color: #c82333;
}
/* Add Job Form Styles */
.cron-add-job {
padding: 20px;
}
.add-job-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.add-job-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.add-job-header h2 i {
margin-right: 10px;
color: #667eea;
}
.add-job-form {
max-width: 600px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.form-control[type="number"] {
width: 100px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 30px;
}
.form-actions .btn {
padding: 10px 20px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.form-actions .btn-primary {
background-color: #667eea;
color: white;
}
.form-actions .btn-primary:hover {
background-color: #5a6fd8;
}
.form-actions .btn-default {
background-color: #6c757d;
color: white;
}
.form-actions .btn-default:hover {
background-color: #5a6268;
}
/* Board Operations Styles */
.cron-board-operations {
padding: 20px;
}
.board-operations-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.board-operations-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.board-operations-header h2 i {
margin-right: 10px;
color: #667eea;
}
.board-operations-controls {
display: flex;
gap: 10px;
}
.board-operations-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.board-operations-controls .btn-success {
background-color: #28a745;
color: white;
}
.board-operations-controls .btn-success:hover {
background-color: #218838;
}
.board-operations-controls .btn-primary {
background-color: #667eea;
color: white;
}
.board-operations-controls .btn-primary:hover {
background-color: #5a6fd8;
}
.board-operations-stats {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border-left: 4px solid #667eea;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #667eea;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.board-operations-search {
margin-bottom: 30px;
}
.search-box {
position: relative;
max-width: 400px;
}
.search-box .form-control {
padding-right: 40px;
}
.search-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 16px;
}
.board-operations-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.operations-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.operations-header h3 {
margin: 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.pagination-info {
color: #666;
font-size: 14px;
}
.operations-table {
overflow-x: auto;
}
.operations-table .table {
margin: 0;
border: none;
}
.operations-table .table th {
background-color: #f8f9fa;
border-bottom: 2px solid #e0e0e0;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.operations-table .table td {
vertical-align: middle;
border-bottom: 1px solid #f0f0f0;
}
.board-id {
font-family: monospace;
font-size: 12px;
color: #666;
background: #f8f9fa;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.operation-type {
font-weight: 500;
color: #333;
text-transform: capitalize;
}
.progress-container {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
}
.progress-container .progress-bar {
flex: 1;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-container .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-container .progress-text {
font-size: 12px;
font-weight: 600;
color: #667eea;
min-width: 35px;
text-align: right;
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8f9fa;
border-top: 1px solid #e0e0e0;
}
.pagination .btn {
padding: 6px 12px;
font-size: 12px;
border-radius: 4px;
border: 1px solid #ddd;
background: white;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
}
.pagination .btn:hover {
background: #f8f9fa;
border-color: #667eea;
}
.pagination .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
color: #666;
font-size: 14px;
}
/* Responsive design */
@media (max-width: 768px) {
.migration-header,
.jobs-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.migration-controls,
.jobs-controls {
width: 100%;
justify-content: center;
}
.table {
font-size: 12px;
}
.table th,
.table td {
padding: 8px 12px;
}
.btn-group {
flex-direction: column;
}
.add-job-form {
max-width: 100%;
}
}

View file

@ -0,0 +1,271 @@
template(name="cronSettings")
.setting-content.cron-settings-content
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-body
.side-menu
ul
li
a.js-cron-migrations(data-id="cron-migrations")
i.fa.fa-database
| {{_ 'cron-migrations'}}
li
a.js-cron-board-operations(data-id="cron-board-operations")
i.fa.fa-tasks
| {{_ 'board-operations'}}
li
a.js-cron-jobs(data-id="cron-jobs")
i.fa.fa-clock-o
| {{_ 'cron-jobs'}}
li
a.js-cron-add(data-id="cron-add")
i.fa.fa-plus
| {{_ 'add-cron-job'}}
.main-body
if loading.get
+spinner
else if showMigrations.get
+cronMigrations
else if showBoardOperations.get
+cronBoardOperations
else if showJobs.get
+cronJobs
else if showAddJob.get
+cronAddJob
template(name="cronMigrations")
.cron-migrations
.migration-header
h2
i.fa.fa-database
| {{_ 'database-migrations'}}
.migration-controls
button.btn.btn-primary.js-start-all-migrations
i.fa.fa-play
| {{_ 'start-all-migrations'}}
button.btn.btn-warning.js-pause-all-migrations
i.fa.fa-pause
| {{_ 'pause-all-migrations'}}
button.btn.btn-danger.js-stop-all-migrations
i.fa.fa-stop
| {{_ 'stop-all-migrations'}}
.migration-progress
.progress-overview
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}
.current-step
i.fa.fa-cog.fa-spin
| {{migrationCurrentStep}}
.migration-status
i.fa.fa-info-circle
| {{migrationStatus}}
.migration-steps
h3 {{_ 'migration-steps'}}
.steps-list
each migrationSteps
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
.step-header
.step-icon
if completed
i.fa.fa-check-circle
else if isCurrentStep
i.fa.fa-cog.fa-spin
else
i.fa.fa-circle-o
.step-info
.step-name {{name}}
.step-description {{description}}
.step-progress
if completed
.progress-text 100%
else if isCurrentStep
.progress-text {{progress}}%
else
.progress-text 0%
if isCurrentStep
.step-progress-bar
.progress-fill(style="width: {{progress}}%")
template(name="cronBoardOperations")
.cron-board-operations
.board-operations-header
h2
i.fa.fa-tasks
| {{_ 'board-operations'}}
.board-operations-controls
button.btn.btn-success.js-refresh-board-operations
i.fa.fa-refresh
| {{_ 'refresh'}}
button.btn.btn-primary.js-start-test-operation
i.fa.fa-play
| {{_ 'start-test-operation'}}
.board-operations-stats
.stats-grid
.stat-item
.stat-value {{operationStats.total}}
.stat-label {{_ 'total-operations'}}
.stat-item
.stat-value {{operationStats.running}}
.stat-label {{_ 'running'}}
.stat-item
.stat-value {{operationStats.completed}}
.stat-label {{_ 'completed'}}
.stat-item
.stat-value {{operationStats.error}}
.stat-label {{_ 'errors'}}
.board-operations-search
.search-box
input.form-control.js-search-board-operations(type="text" placeholder="{{_ 'search-boards-or-operations'}}")
i.fa.fa-search.search-icon
.board-operations-list
.operations-header
h3 {{_ 'board-operations'}} ({{pagination.total}})
.pagination-info
| {{_ 'showing'}} {{pagination.start}} - {{pagination.end}} {{_ 'of'}} {{pagination.total}}
.operations-table
table.table.table-striped
thead
tr
th {{_ 'board-id'}}
th {{_ 'operation-type'}}
th {{_ 'status'}}
th {{_ 'progress'}}
th {{_ 'start-time'}}
th {{_ 'duration'}}
th {{_ 'actions'}}
tbody
each boardOperations
tr
td
.board-id {{boardId}}
td
.operation-type {{operationType}}
td
span.status-badge(class="status-{{status}}") {{status}}
td
.progress-container
.progress-bar
.progress-fill(style="width: {{progress}}%")
.progress-text {{progress}}%
td {{formatDateTime startTime}}
td {{formatDuration startTime endTime}}
td
.btn-group
if isRunning
button.btn.btn-sm.btn-warning.js-pause-operation(data-operation="{{id}}")
i.fa.fa-pause
else
button.btn.btn-sm.btn-success.js-resume-operation(data-operation="{{id}}")
i.fa.fa-play
button.btn.btn-sm.btn-danger.js-stop-operation(data-operation="{{id}}")
i.fa.fa-stop
button.btn.btn-sm.btn-info.js-view-details(data-operation="{{id}}")
i.fa.fa-info-circle
.pagination
if pagination.hasPrev
button.btn.btn-sm.btn-default.js-prev-page
i.fa.fa-chevron-left
| {{_ 'previous'}}
.page-info
| {{_ 'page'}} {{pagination.page}} {{_ 'of'}} {{pagination.totalPages}}
if pagination.hasNext
button.btn.btn-sm.btn-default.js-next-page
| {{_ 'next'}}
i.fa.fa-chevron-right
template(name="cronJobs")
.cron-jobs
.jobs-header
h2
i.fa.fa-clock-o
| {{_ 'cron-jobs'}}
.jobs-controls
button.btn.btn-success.js-refresh-jobs
i.fa.fa-refresh
| {{_ 'refresh'}}
.jobs-list
table.table.table-striped
thead
tr
th {{_ 'job-name'}}
th {{_ 'schedule'}}
th {{_ 'status'}}
th {{_ 'last-run'}}
th {{_ 'next-run'}}
th {{_ 'actions'}}
tbody
each cronJobs
tr
td {{name}}
td {{schedule}}
td
span.status-badge(class="status-{{status}}") {{status}}
td {{formatDate lastRun}}
td {{formatDate nextRun}}
td
.btn-group
if isRunning
button.btn.btn-sm.btn-warning.js-pause-job(data-job="{{name}}")
i.fa.fa-pause
else
button.btn.btn-sm.btn-success.js-start-job(data-job="{{name}}")
i.fa.fa-play
button.btn.btn-sm.btn-danger.js-stop-job(data-job="{{name}}")
i.fa.fa-stop
button.btn.btn-sm.btn-danger.js-remove-job(data-job="{{name}}")
i.fa.fa-trash
template(name="cronAddJob")
.cron-add-job
.add-job-header
h2
i.fa.fa-plus
| {{_ 'add-cron-job'}}
.add-job-form
form.js-add-cron-job-form
.form-group
label(for="job-name") {{_ 'job-name'}}
input.form-control#job-name(type="text" name="name" required)
.form-group
label(for="job-description") {{_ 'job-description'}}
textarea.form-control#job-description(name="description" rows="3")
.form-group
label(for="job-schedule") {{_ 'schedule'}}
select.form-control#job-schedule(name="schedule")
option(value="every 1 minute") {{_ 'every-1-minute'}}
option(value="every 5 minutes") {{_ 'every-5-minutes'}}
option(value="every 10 minutes") {{_ 'every-10-minutes'}}
option(value="every 30 minutes") {{_ 'every-30-minutes'}}
option(value="every 1 hour") {{_ 'every-1-hour'}}
option(value="every 6 hours") {{_ 'every-6-hours'}}
option(value="every 1 day") {{_ 'every-1-day'}}
option(value="once") {{_ 'run-once'}}
.form-group
label(for="job-weight") {{_ 'weight'}}
input.form-control#job-weight(type="number" name="weight" value="1" min="1" max="10")
.form-actions
button.btn.btn-primary(type="submit")
i.fa.fa-plus
| {{_ 'add-job'}}
button.btn.btn-default.js-cancel-add-job
i.fa.fa-times
| {{_ 'cancel'}}

View file

@ -0,0 +1,446 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from '/imports/i18n';
// Reactive variables for cron settings
const migrationProgress = new ReactiveVar(0);
const migrationStatus = new ReactiveVar('');
const migrationCurrentStep = new ReactiveVar('');
const migrationSteps = new ReactiveVar([]);
const isMigrating = new ReactiveVar(false);
const cronJobs = new ReactiveVar([]);
Template.cronSettings.onCreated(function() {
this.loading = new ReactiveVar(true);
this.showMigrations = new ReactiveVar(true);
this.showBoardOperations = new ReactiveVar(false);
this.showJobs = new ReactiveVar(false);
this.showAddJob = new ReactiveVar(false);
// Board operations pagination
this.currentPage = new ReactiveVar(1);
this.pageSize = new ReactiveVar(20);
this.searchTerm = new ReactiveVar('');
this.boardOperations = new ReactiveVar([]);
this.operationStats = new ReactiveVar({});
this.pagination = new ReactiveVar({});
// Load initial data
this.loadCronData();
});
Template.cronSettings.helpers({
loading() {
return Template.instance().loading.get();
},
showMigrations() {
return Template.instance().showMigrations.get();
},
showBoardOperations() {
return Template.instance().showBoardOperations.get();
},
showJobs() {
return Template.instance().showJobs.get();
},
showAddJob() {
return Template.instance().showAddJob.get();
},
migrationProgress() {
return migrationProgress.get();
},
migrationStatus() {
return migrationStatus.get();
},
migrationCurrentStep() {
return migrationCurrentStep.get();
},
migrationSteps() {
const steps = migrationSteps.get();
const currentStep = migrationCurrentStep.get();
return steps.map(step => ({
...step,
isCurrentStep: step.name === currentStep
}));
},
cronJobs() {
return cronJobs.get();
},
formatDate(date) {
if (!date) return '-';
return new Date(date).toLocaleString();
},
boardOperations() {
return Template.instance().boardOperations.get();
},
operationStats() {
return Template.instance().operationStats.get();
},
pagination() {
return Template.instance().pagination.get();
},
formatDateTime(date) {
if (!date) return '-';
return new Date(date).toLocaleString();
},
formatDuration(startTime, endTime) {
if (!startTime) return '-';
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : new Date();
const diffMs = end - start;
const diffMins = Math.floor(diffMs / 60000);
const diffSecs = Math.floor((diffMs % 60000) / 1000);
if (diffMins > 0) {
return `${diffMins}m ${diffSecs}s`;
} else {
return `${diffSecs}s`;
}
}
});
Template.cronSettings.events({
'click .js-cron-migrations'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(true);
instance.showJobs.set(false);
instance.showAddJob.set(false);
},
'click .js-cron-board-operations'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(false);
instance.showBoardOperations.set(true);
instance.showJobs.set(false);
instance.showAddJob.set(false);
instance.loadBoardOperations();
},
'click .js-cron-jobs'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(false);
instance.showBoardOperations.set(false);
instance.showJobs.set(true);
instance.showAddJob.set(false);
instance.loadCronJobs();
},
'click .js-cron-add'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(false);
instance.showJobs.set(false);
instance.showAddJob.set(true);
},
'click .js-start-all-migrations'(event) {
event.preventDefault();
Meteor.call('cron.startAllMigrations', (error, result) => {
if (error) {
console.error('Failed to start migrations:', error);
alert('Failed to start migrations: ' + error.message);
} else {
console.log('Migrations started successfully');
Template.instance().pollMigrationProgress();
}
});
},
'click .js-pause-all-migrations'(event) {
event.preventDefault();
// Pause all migration cron jobs
const jobs = cronJobs.get();
jobs.forEach(job => {
if (job.name.startsWith('migration_')) {
Meteor.call('cron.pauseJob', job.name);
}
});
},
'click .js-stop-all-migrations'(event) {
event.preventDefault();
// Stop all migration cron jobs
const jobs = cronJobs.get();
jobs.forEach(job => {
if (job.name.startsWith('migration_')) {
Meteor.call('cron.stopJob', job.name);
}
});
},
'click .js-refresh-jobs'(event) {
event.preventDefault();
Template.instance().loadCronJobs();
},
'click .js-start-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
Meteor.call('cron.startJob', jobName, (error, result) => {
if (error) {
console.error('Failed to start job:', error);
alert('Failed to start job: ' + error.message);
} else {
console.log('Job started successfully');
Template.instance().loadCronJobs();
}
});
},
'click .js-pause-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
Meteor.call('cron.pauseJob', jobName, (error, result) => {
if (error) {
console.error('Failed to pause job:', error);
alert('Failed to pause job: ' + error.message);
} else {
console.log('Job paused successfully');
Template.instance().loadCronJobs();
}
});
},
'click .js-stop-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
Meteor.call('cron.stopJob', jobName, (error, result) => {
if (error) {
console.error('Failed to stop job:', error);
alert('Failed to stop job: ' + error.message);
} else {
console.log('Job stopped successfully');
Template.instance().loadCronJobs();
}
});
},
'click .js-remove-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
if (confirm('Are you sure you want to remove this job?')) {
Meteor.call('cron.removeJob', jobName, (error, result) => {
if (error) {
console.error('Failed to remove job:', error);
alert('Failed to remove job: ' + error.message);
} else {
console.log('Job removed successfully');
Template.instance().loadCronJobs();
}
});
}
},
'submit .js-add-cron-job-form'(event) {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const jobData = {
name: formData.get('name'),
description: formData.get('description'),
schedule: formData.get('schedule'),
weight: parseInt(formData.get('weight'))
};
Meteor.call('cron.addJob', jobData, (error, result) => {
if (error) {
console.error('Failed to add job:', error);
alert('Failed to add job: ' + error.message);
} else {
console.log('Job added successfully');
form.reset();
Template.instance().showJobs.set(true);
Template.instance().showAddJob.set(false);
Template.instance().loadCronJobs();
}
});
},
'click .js-cancel-add-job'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showJobs.set(true);
instance.showAddJob.set(false);
},
'click .js-refresh-board-operations'(event) {
event.preventDefault();
Template.instance().loadBoardOperations();
},
'click .js-start-test-operation'(event) {
event.preventDefault();
const testBoardId = 'test-board-' + Date.now();
const operationData = {
sourceBoardId: 'source-board',
targetBoardId: 'target-board',
copyOptions: { includeCards: true, includeAttachments: true }
};
Meteor.call('cron.startBoardOperation', testBoardId, 'copy_board', operationData, (error, result) => {
if (error) {
console.error('Failed to start test operation:', error);
alert('Failed to start test operation: ' + error.message);
} else {
console.log('Test operation started:', result);
Template.instance().loadBoardOperations();
}
});
},
'input .js-search-board-operations'(event) {
const searchTerm = $(event.currentTarget).val();
const instance = Template.instance();
instance.searchTerm.set(searchTerm);
instance.currentPage.set(1);
instance.loadBoardOperations();
},
'click .js-prev-page'(event) {
event.preventDefault();
const instance = Template.instance();
const currentPage = instance.currentPage.get();
if (currentPage > 1) {
instance.currentPage.set(currentPage - 1);
instance.loadBoardOperations();
}
},
'click .js-next-page'(event) {
event.preventDefault();
const instance = Template.instance();
const currentPage = instance.currentPage.get();
const pagination = instance.pagination.get();
if (currentPage < pagination.totalPages) {
instance.currentPage.set(currentPage + 1);
instance.loadBoardOperations();
}
},
'click .js-pause-operation'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
// Implementation for pausing operation
console.log('Pause operation:', operationId);
},
'click .js-resume-operation'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
// Implementation for resuming operation
console.log('Resume operation:', operationId);
},
'click .js-stop-operation'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
if (confirm('Are you sure you want to stop this operation?')) {
// Implementation for stopping operation
console.log('Stop operation:', operationId);
}
},
'click .js-view-details'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
// Implementation for viewing operation details
console.log('View details for operation:', operationId);
}
});
Template.cronSettings.prototype.loadCronData = function() {
this.loading.set(true);
// Load migration progress
Meteor.call('cron.getMigrationProgress', (error, result) => {
if (result) {
migrationProgress.set(result.progress);
migrationStatus.set(result.status);
migrationCurrentStep.set(result.currentStep);
migrationSteps.set(result.steps);
isMigrating.set(result.isMigrating);
}
});
// Load cron jobs
this.loadCronJobs();
this.loading.set(false);
};
Template.cronSettings.prototype.loadCronJobs = function() {
Meteor.call('cron.getJobs', (error, result) => {
if (result) {
cronJobs.set(result);
}
});
};
Template.cronSettings.prototype.loadBoardOperations = function() {
const instance = this;
const page = instance.currentPage.get();
const limit = instance.pageSize.get();
const searchTerm = instance.searchTerm.get();
Meteor.call('cron.getAllBoardOperations', page, limit, searchTerm, (error, result) => {
if (result) {
instance.boardOperations.set(result.operations);
instance.pagination.set({
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
start: ((result.page - 1) * result.limit) + 1,
end: Math.min(result.page * result.limit, result.total),
hasPrev: result.page > 1,
hasNext: result.page < result.totalPages
});
}
});
// Load operation stats
Meteor.call('cron.getBoardOperationStats', (error, result) => {
if (result) {
instance.operationStats.set(result);
}
});
};
Template.cronSettings.prototype.pollMigrationProgress = function() {
const pollInterval = setInterval(() => {
Meteor.call('cron.getMigrationProgress', (error, result) => {
if (result) {
migrationProgress.set(result.progress);
migrationStatus.set(result.status);
migrationCurrentStep.set(result.currentStep);
migrationSteps.set(result.steps);
isMigrating.set(result.isMigrating);
// Stop polling if migration is complete
if (!result.isMigrating && result.progress === 100) {
clearInterval(pollInterval);
}
}
});
}, 1000);
};

View file

@ -46,6 +46,10 @@ template(name="setting")
a.js-setting-menu(data-id="attachment-settings")
i.fa.fa-paperclip
| {{_ 'attachment-settings'}}
li
a.js-setting-menu(data-id="cron-settings")
i.fa.fa-clock-o
| {{_ 'cron-settings'}}
.main-body
if loading.get
+spinner
@ -68,6 +72,8 @@ template(name="setting")
+webhookSettings
else if attachmentSettings.get
+attachmentSettings
else if cronSettings.get
+cronSettings
template(name="webhookSettings")
span

View file

@ -17,6 +17,7 @@ BlazeComponent.extendComponent({
this.layoutSetting = new ReactiveVar(false);
this.webhookSetting = new ReactiveVar(false);
this.attachmentSettings = new ReactiveVar(false);
this.cronSettings = new ReactiveVar(false);
Meteor.subscribe('setting');
Meteor.subscribe('mailServer');
@ -115,6 +116,7 @@ BlazeComponent.extendComponent({
this.layoutSetting.set('layout-setting' === targetID);
this.webhookSetting.set('webhook-setting' === targetID);
this.attachmentSettings.set('attachment-settings' === targetID);
this.cronSettings.set('cron-settings' === targetID);
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
}
},

View file

@ -624,7 +624,7 @@ class MigrationManager {
}
/**
* Start migration process
* Start migration process using cron system
*/
async startMigration() {
if (isMigrating.get()) {
@ -636,17 +636,17 @@ class MigrationManager {
this.startTime = Date.now();
try {
// Start server-side migration
Meteor.call('migration.start', (error, result) => {
// Start server-side cron migrations
Meteor.call('cron.startAllMigrations', (error, result) => {
if (error) {
console.error('Failed to start migration:', error);
console.error('Failed to start cron migrations:', error);
migrationStatus.set(`Migration failed: ${error.message}`);
isMigrating.set(false);
}
});
// Poll for progress updates
this.pollMigrationProgress();
this.pollCronMigrationProgress();
} catch (error) {
console.error('Migration failed:', error);
@ -656,13 +656,13 @@ class MigrationManager {
}
/**
* Poll for migration progress updates
* Poll for cron migration progress updates
*/
pollMigrationProgress() {
pollCronMigrationProgress() {
const pollInterval = setInterval(() => {
Meteor.call('migration.getProgress', (error, result) => {
Meteor.call('cron.getMigrationProgress', (error, result) => {
if (error) {
console.error('Failed to get migration progress:', error);
console.error('Failed to get cron migration progress:', error);
clearInterval(pollInterval);
return;
}