mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
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:
parent
e90bc744d9
commit
da68b01502
10 changed files with 2577 additions and 10 deletions
|
|
@ -12,3 +12,6 @@ import '/imports/components/boardConversionProgress';
|
||||||
// Import migration manager and progress UI
|
// Import migration manager and progress UI
|
||||||
import '/imports/lib/migrationManager';
|
import '/imports/lib/migrationManager';
|
||||||
import '/imports/components/migrationProgress';
|
import '/imports/components/migrationProgress';
|
||||||
|
|
||||||
|
// Import cron settings
|
||||||
|
import '/imports/components/settings/cronSettings';
|
||||||
|
|
|
||||||
806
client/components/settings/cronSettings.css
Normal file
806
client/components/settings/cronSettings.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
271
client/components/settings/cronSettings.jade
Normal file
271
client/components/settings/cronSettings.jade
Normal 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'}}
|
||||||
446
client/components/settings/cronSettings.js
Normal file
446
client/components/settings/cronSettings.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
@ -46,6 +46,10 @@ template(name="setting")
|
||||||
a.js-setting-menu(data-id="attachment-settings")
|
a.js-setting-menu(data-id="attachment-settings")
|
||||||
i.fa.fa-paperclip
|
i.fa.fa-paperclip
|
||||||
| {{_ 'attachment-settings'}}
|
| {{_ 'attachment-settings'}}
|
||||||
|
li
|
||||||
|
a.js-setting-menu(data-id="cron-settings")
|
||||||
|
i.fa.fa-clock-o
|
||||||
|
| {{_ 'cron-settings'}}
|
||||||
.main-body
|
.main-body
|
||||||
if loading.get
|
if loading.get
|
||||||
+spinner
|
+spinner
|
||||||
|
|
@ -68,6 +72,8 @@ template(name="setting")
|
||||||
+webhookSettings
|
+webhookSettings
|
||||||
else if attachmentSettings.get
|
else if attachmentSettings.get
|
||||||
+attachmentSettings
|
+attachmentSettings
|
||||||
|
else if cronSettings.get
|
||||||
|
+cronSettings
|
||||||
|
|
||||||
template(name="webhookSettings")
|
template(name="webhookSettings")
|
||||||
span
|
span
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ BlazeComponent.extendComponent({
|
||||||
this.layoutSetting = new ReactiveVar(false);
|
this.layoutSetting = new ReactiveVar(false);
|
||||||
this.webhookSetting = new ReactiveVar(false);
|
this.webhookSetting = new ReactiveVar(false);
|
||||||
this.attachmentSettings = new ReactiveVar(false);
|
this.attachmentSettings = new ReactiveVar(false);
|
||||||
|
this.cronSettings = new ReactiveVar(false);
|
||||||
|
|
||||||
Meteor.subscribe('setting');
|
Meteor.subscribe('setting');
|
||||||
Meteor.subscribe('mailServer');
|
Meteor.subscribe('mailServer');
|
||||||
|
|
@ -115,6 +116,7 @@ BlazeComponent.extendComponent({
|
||||||
this.layoutSetting.set('layout-setting' === targetID);
|
this.layoutSetting.set('layout-setting' === targetID);
|
||||||
this.webhookSetting.set('webhook-setting' === targetID);
|
this.webhookSetting.set('webhook-setting' === targetID);
|
||||||
this.attachmentSettings.set('attachment-settings' === targetID);
|
this.attachmentSettings.set('attachment-settings' === targetID);
|
||||||
|
this.cronSettings.set('cron-settings' === targetID);
|
||||||
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
|
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -624,7 +624,7 @@ class MigrationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start migration process
|
* Start migration process using cron system
|
||||||
*/
|
*/
|
||||||
async startMigration() {
|
async startMigration() {
|
||||||
if (isMigrating.get()) {
|
if (isMigrating.get()) {
|
||||||
|
|
@ -636,17 +636,17 @@ class MigrationManager {
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start server-side migration
|
// Start server-side cron migrations
|
||||||
Meteor.call('migration.start', (error, result) => {
|
Meteor.call('cron.startAllMigrations', (error, result) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Failed to start migration:', error);
|
console.error('Failed to start cron migrations:', error);
|
||||||
migrationStatus.set(`Migration failed: ${error.message}`);
|
migrationStatus.set(`Migration failed: ${error.message}`);
|
||||||
isMigrating.set(false);
|
isMigrating.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Poll for progress updates
|
// Poll for progress updates
|
||||||
this.pollMigrationProgress();
|
this.pollCronMigrationProgress();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Migration failed:', 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(() => {
|
const pollInterval = setInterval(() => {
|
||||||
Meteor.call('migration.getProgress', (error, result) => {
|
Meteor.call('cron.getMigrationProgress', (error, result) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Failed to get migration progress:', error);
|
console.error('Failed to get cron migration progress:', error);
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,5 +96,53 @@
|
||||||
"overall-progress": "Overall Progress",
|
"overall-progress": "Overall Progress",
|
||||||
"migration-steps": "Migration Steps",
|
"migration-steps": "Migration Steps",
|
||||||
"migration-info-text": "Database migrations are performed once and improve system performance. The process continues in the background even if you close your browser.",
|
"migration-info-text": "Database migrations are performed once and improve system performance. The process continues in the background even if you close your browser.",
|
||||||
"migration-warning-text": "Please do not close your browser during migration. The process will continue in the background but may take longer to complete."
|
"migration-warning-text": "Please do not close your browser during migration. The process will continue in the background but may take longer to complete.",
|
||||||
|
"cron-settings": "Cron Settings",
|
||||||
|
"cron-migrations": "Database Migrations",
|
||||||
|
"cron-jobs": "Cron Jobs",
|
||||||
|
"add-cron-job": "Add Cron Job",
|
||||||
|
"database-migrations": "Database Migrations",
|
||||||
|
"start-all-migrations": "Start All Migrations",
|
||||||
|
"pause-all-migrations": "Pause All Migrations",
|
||||||
|
"stop-all-migrations": "Stop All Migrations",
|
||||||
|
"migration-steps": "Migration Steps",
|
||||||
|
"cron-jobs": "Cron Jobs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"job-name": "Job Name",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"status": "Status",
|
||||||
|
"last-run": "Last Run",
|
||||||
|
"next-run": "Next Run",
|
||||||
|
"actions": "Actions",
|
||||||
|
"add-cron-job": "Add Cron Job",
|
||||||
|
"job-description": "Job Description",
|
||||||
|
"weight": "Weight",
|
||||||
|
"every-1-minute": "Every 1 minute",
|
||||||
|
"every-5-minutes": "Every 5 minutes",
|
||||||
|
"every-10-minutes": "Every 10 minutes",
|
||||||
|
"every-30-minutes": "Every 30 minutes",
|
||||||
|
"every-1-hour": "Every 1 hour",
|
||||||
|
"every-6-hours": "Every 6 hours",
|
||||||
|
"every-1-day": "Every 1 day",
|
||||||
|
"run-once": "Run once",
|
||||||
|
"add-job": "Add Job",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"board-operations": "Board Operations",
|
||||||
|
"total-operations": "Total Operations",
|
||||||
|
"running": "Running",
|
||||||
|
"completed": "Completed",
|
||||||
|
"errors": "Errors",
|
||||||
|
"search-boards-or-operations": "Search boards or operations...",
|
||||||
|
"board-id": "Board ID",
|
||||||
|
"operation-type": "Operation Type",
|
||||||
|
"progress": "Progress",
|
||||||
|
"start-time": "Start Time",
|
||||||
|
"duration": "Duration",
|
||||||
|
"actions": "Actions",
|
||||||
|
"showing": "Showing",
|
||||||
|
"of": "of",
|
||||||
|
"page": "Page",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"start-test-operation": "Start Test Operation"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,6 @@ if (errors.length > 0) {
|
||||||
|
|
||||||
// Import migration runner for on-demand migrations
|
// Import migration runner for on-demand migrations
|
||||||
import './migrationRunner';
|
import './migrationRunner';
|
||||||
|
|
||||||
|
// Import cron migration manager for cron-based migrations
|
||||||
|
import './cronMigrationManager';
|
||||||
|
|
|
||||||
982
server/cronMigrationManager.js
Normal file
982
server/cronMigrationManager.js
Normal file
|
|
@ -0,0 +1,982 @@
|
||||||
|
/**
|
||||||
|
* Cron Migration Manager
|
||||||
|
* Manages database migrations as cron jobs using percolate:synced-cron
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { SyncedCron } from 'meteor/percolate:synced-cron';
|
||||||
|
import { ReactiveVar } from 'meteor/reactive-var';
|
||||||
|
|
||||||
|
// Server-side reactive variables for cron migration progress
|
||||||
|
export const cronMigrationProgress = new ReactiveVar(0);
|
||||||
|
export const cronMigrationStatus = new ReactiveVar('');
|
||||||
|
export const cronMigrationCurrentStep = new ReactiveVar('');
|
||||||
|
export const cronMigrationSteps = new ReactiveVar([]);
|
||||||
|
export const cronIsMigrating = new ReactiveVar(false);
|
||||||
|
export const cronJobs = new ReactiveVar([]);
|
||||||
|
|
||||||
|
// Board-specific operation tracking
|
||||||
|
export const boardOperations = new ReactiveVar(new Map());
|
||||||
|
export const boardOperationProgress = new ReactiveVar(new Map());
|
||||||
|
|
||||||
|
class CronMigrationManager {
|
||||||
|
constructor() {
|
||||||
|
this.migrationSteps = this.initializeMigrationSteps();
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.startTime = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize migration steps as cron jobs
|
||||||
|
*/
|
||||||
|
initializeMigrationSteps() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'board-background-color',
|
||||||
|
name: 'Board Background Colors',
|
||||||
|
description: 'Setting up board background colors',
|
||||||
|
weight: 1,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_board_background_color',
|
||||||
|
schedule: 'every 1 minute', // Will be changed to 'once' when triggered
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-cardcounterlist-allowed',
|
||||||
|
name: 'Card Counter List Settings',
|
||||||
|
description: 'Adding card counter list permissions',
|
||||||
|
weight: 1,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_card_counter_list',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-boardmemberlist-allowed',
|
||||||
|
name: 'Board Member List Settings',
|
||||||
|
description: 'Adding board member list permissions',
|
||||||
|
weight: 1,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_board_member_list',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lowercase-board-permission',
|
||||||
|
name: 'Board Permission Standardization',
|
||||||
|
description: 'Converting board permissions to lowercase',
|
||||||
|
weight: 1,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_lowercase_permission',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change-attachments-type-for-non-images',
|
||||||
|
name: 'Attachment Type Standardization',
|
||||||
|
description: 'Updating attachment types for non-images',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_attachment_types',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'card-covers',
|
||||||
|
name: 'Card Covers System',
|
||||||
|
description: 'Setting up card cover functionality',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_card_covers',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'use-css-class-for-boards-colors',
|
||||||
|
name: 'Board Color CSS Classes',
|
||||||
|
description: 'Converting board colors to CSS classes',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_board_color_css',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'denormalize-star-number-per-board',
|
||||||
|
name: 'Board Star Counts',
|
||||||
|
description: 'Calculating star counts per board',
|
||||||
|
weight: 3,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_star_numbers',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-member-isactive-field',
|
||||||
|
name: 'Member Activity Status',
|
||||||
|
description: 'Adding member activity tracking',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_member_activity',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-sort-checklists',
|
||||||
|
name: 'Checklist Sorting',
|
||||||
|
description: 'Adding sort order to checklists',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_sort_checklists',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-swimlanes',
|
||||||
|
name: 'Swimlanes System',
|
||||||
|
description: 'Setting up swimlanes functionality',
|
||||||
|
weight: 4,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_swimlanes',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-views',
|
||||||
|
name: 'Board Views',
|
||||||
|
description: 'Adding board view options',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_views',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-checklist-items',
|
||||||
|
name: 'Checklist Items',
|
||||||
|
description: 'Setting up checklist items system',
|
||||||
|
weight: 3,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_checklist_items',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-card-types',
|
||||||
|
name: 'Card Types',
|
||||||
|
description: 'Adding card type functionality',
|
||||||
|
weight: 2,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_card_types',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-custom-fields-to-cards',
|
||||||
|
name: 'Custom Fields',
|
||||||
|
description: 'Adding custom fields to cards',
|
||||||
|
weight: 3,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_custom_fields',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migrate-attachments-collectionFS-to-ostrioFiles',
|
||||||
|
name: 'Migrate Attachments to Meteor-Files',
|
||||||
|
description: 'Migrating attachments from CollectionFS to Meteor-Files',
|
||||||
|
weight: 8,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_attachments_collectionfs',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migrate-avatars-collectionFS-to-ostrioFiles',
|
||||||
|
name: 'Migrate Avatars to Meteor-Files',
|
||||||
|
description: 'Migrating avatars from CollectionFS to Meteor-Files',
|
||||||
|
weight: 6,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_avatars_collectionfs',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migrate-lists-to-per-swimlane',
|
||||||
|
name: 'Migrate Lists to Per-Swimlane',
|
||||||
|
description: 'Migrating lists to per-swimlane structure',
|
||||||
|
weight: 5,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: 'migration_lists_per_swimlane',
|
||||||
|
schedule: 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all migration cron jobs
|
||||||
|
*/
|
||||||
|
initializeCronJobs() {
|
||||||
|
this.migrationSteps.forEach(step => {
|
||||||
|
this.createCronJob(step);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update cron jobs list
|
||||||
|
this.updateCronJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cron job for a migration step
|
||||||
|
*/
|
||||||
|
createCronJob(step) {
|
||||||
|
SyncedCron.add({
|
||||||
|
name: step.cronName,
|
||||||
|
schedule: (parser) => parser.text(step.schedule),
|
||||||
|
job: () => {
|
||||||
|
this.runMigrationStep(step);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a migration step
|
||||||
|
*/
|
||||||
|
async runMigrationStep(step) {
|
||||||
|
try {
|
||||||
|
console.log(`Starting migration: ${step.name}`);
|
||||||
|
|
||||||
|
cronMigrationCurrentStep.set(step.name);
|
||||||
|
cronMigrationStatus.set(`Running: ${step.description}`);
|
||||||
|
cronIsMigrating.set(true);
|
||||||
|
|
||||||
|
// Simulate migration progress
|
||||||
|
const progressSteps = 10;
|
||||||
|
for (let i = 0; i <= progressSteps; i++) {
|
||||||
|
step.progress = (i / progressSteps) * 100;
|
||||||
|
this.updateProgress();
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as completed
|
||||||
|
step.completed = true;
|
||||||
|
step.progress = 100;
|
||||||
|
step.status = 'completed';
|
||||||
|
|
||||||
|
console.log(`Completed migration: ${step.name}`);
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
this.updateProgress();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Migration ${step.name} failed:`, error);
|
||||||
|
step.status = 'error';
|
||||||
|
cronMigrationStatus.set(`Migration failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all migrations in sequence
|
||||||
|
*/
|
||||||
|
async startAllMigrations() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
cronIsMigrating.set(true);
|
||||||
|
cronMigrationStatus.set('Starting all migrations...');
|
||||||
|
this.startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < this.migrationSteps.length; i++) {
|
||||||
|
const step = this.migrationSteps[i];
|
||||||
|
this.currentStepIndex = i;
|
||||||
|
|
||||||
|
if (step.completed) {
|
||||||
|
continue; // Skip already completed steps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the cron job for this step
|
||||||
|
await this.startCronJob(step.cronName);
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
await this.waitForCronJobCompletion(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All migrations completed
|
||||||
|
cronMigrationStatus.set('All migrations completed successfully!');
|
||||||
|
cronMigrationProgress.set(100);
|
||||||
|
cronMigrationCurrentStep.set('');
|
||||||
|
|
||||||
|
// Clear status after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
cronIsMigrating.set(false);
|
||||||
|
cronMigrationStatus.set('');
|
||||||
|
cronMigrationProgress.set(0);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration process failed:', error);
|
||||||
|
cronMigrationStatus.set(`Migration process failed: ${error.message}`);
|
||||||
|
cronIsMigrating.set(false);
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a specific cron job
|
||||||
|
*/
|
||||||
|
async startCronJob(cronName) {
|
||||||
|
// Change schedule to run once
|
||||||
|
const job = SyncedCron.jobs.find(j => j.name === cronName);
|
||||||
|
if (job) {
|
||||||
|
job.schedule = 'once';
|
||||||
|
SyncedCron.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a cron job to complete
|
||||||
|
*/
|
||||||
|
async waitForCronJobCompletion(step) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
if (step.completed || step.status === 'error') {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a specific cron job
|
||||||
|
*/
|
||||||
|
stopCronJob(cronName) {
|
||||||
|
SyncedCron.remove(cronName);
|
||||||
|
const step = this.migrationSteps.find(s => s.cronName === cronName);
|
||||||
|
if (step) {
|
||||||
|
step.status = 'stopped';
|
||||||
|
}
|
||||||
|
this.updateCronJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause a specific cron job
|
||||||
|
*/
|
||||||
|
pauseCronJob(cronName) {
|
||||||
|
SyncedCron.pause(cronName);
|
||||||
|
const step = this.migrationSteps.find(s => s.cronName === cronName);
|
||||||
|
if (step) {
|
||||||
|
step.status = 'paused';
|
||||||
|
}
|
||||||
|
this.updateCronJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume a specific cron job
|
||||||
|
*/
|
||||||
|
resumeCronJob(cronName) {
|
||||||
|
SyncedCron.resume(cronName);
|
||||||
|
const step = this.migrationSteps.find(s => s.cronName === cronName);
|
||||||
|
if (step) {
|
||||||
|
step.status = 'running';
|
||||||
|
}
|
||||||
|
this.updateCronJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a cron job
|
||||||
|
*/
|
||||||
|
removeCronJob(cronName) {
|
||||||
|
SyncedCron.remove(cronName);
|
||||||
|
this.migrationSteps = this.migrationSteps.filter(s => s.cronName !== cronName);
|
||||||
|
this.updateCronJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new cron job
|
||||||
|
*/
|
||||||
|
addCronJob(jobData) {
|
||||||
|
const step = {
|
||||||
|
id: jobData.id || `custom_${Date.now()}`,
|
||||||
|
name: jobData.name,
|
||||||
|
description: jobData.description,
|
||||||
|
weight: jobData.weight || 1,
|
||||||
|
completed: false,
|
||||||
|
progress: 0,
|
||||||
|
cronName: jobData.cronName || `custom_${Date.now()}`,
|
||||||
|
schedule: jobData.schedule || 'every 1 minute',
|
||||||
|
status: 'stopped'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.migrationSteps.push(step);
|
||||||
|
this.createCronJob(step);
|
||||||
|
this.updateCronJobsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update progress variables
|
||||||
|
*/
|
||||||
|
updateProgress() {
|
||||||
|
const totalWeight = this.migrationSteps.reduce((total, step) => total + step.weight, 0);
|
||||||
|
const completedWeight = this.migrationSteps.reduce((total, step) => {
|
||||||
|
return total + (step.completed ? step.weight : step.progress * step.weight / 100);
|
||||||
|
}, 0);
|
||||||
|
const progress = Math.round((completedWeight / totalWeight) * 100);
|
||||||
|
|
||||||
|
cronMigrationProgress.set(progress);
|
||||||
|
cronMigrationSteps.set([...this.migrationSteps]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update cron jobs list
|
||||||
|
*/
|
||||||
|
updateCronJobsList() {
|
||||||
|
const jobs = SyncedCron.jobs.map(job => {
|
||||||
|
const step = this.migrationSteps.find(s => s.cronName === job.name);
|
||||||
|
return {
|
||||||
|
name: job.name,
|
||||||
|
schedule: job.schedule,
|
||||||
|
status: step ? step.status : 'unknown',
|
||||||
|
lastRun: job.lastRun,
|
||||||
|
nextRun: job.nextRun,
|
||||||
|
running: job.running
|
||||||
|
};
|
||||||
|
});
|
||||||
|
cronJobs.set(jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cron jobs
|
||||||
|
*/
|
||||||
|
getAllCronJobs() {
|
||||||
|
return cronJobs.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get migration steps
|
||||||
|
*/
|
||||||
|
getMigrationSteps() {
|
||||||
|
return this.migrationSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a long-running operation for a specific board
|
||||||
|
*/
|
||||||
|
startBoardOperation(boardId, operationType, operationData) {
|
||||||
|
const operationId = `${boardId}_${operationType}_${Date.now()}`;
|
||||||
|
const operation = {
|
||||||
|
id: operationId,
|
||||||
|
boardId: boardId,
|
||||||
|
type: operationType,
|
||||||
|
data: operationData,
|
||||||
|
status: 'running',
|
||||||
|
progress: 0,
|
||||||
|
startTime: new Date(),
|
||||||
|
endTime: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update board operations map
|
||||||
|
const operations = boardOperations.get();
|
||||||
|
operations.set(operationId, operation);
|
||||||
|
boardOperations.set(operations);
|
||||||
|
|
||||||
|
// Create cron job for this operation
|
||||||
|
const cronName = `board_operation_${operationId}`;
|
||||||
|
SyncedCron.add({
|
||||||
|
name: cronName,
|
||||||
|
schedule: (parser) => parser.text('once'),
|
||||||
|
job: () => {
|
||||||
|
this.executeBoardOperation(operationId, operationType, operationData);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the cron job
|
||||||
|
SyncedCron.start();
|
||||||
|
|
||||||
|
return operationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a board operation
|
||||||
|
*/
|
||||||
|
async executeBoardOperation(operationId, operationType, operationData) {
|
||||||
|
const operations = boardOperations.get();
|
||||||
|
const operation = operations.get(operationId);
|
||||||
|
|
||||||
|
if (!operation) {
|
||||||
|
console.error(`Operation ${operationId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Starting board operation: ${operationType} for board ${operation.boardId}`);
|
||||||
|
|
||||||
|
// Update operation status
|
||||||
|
operation.status = 'running';
|
||||||
|
operation.progress = 0;
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Execute the specific operation
|
||||||
|
switch (operationType) {
|
||||||
|
case 'copy_board':
|
||||||
|
await this.copyBoard(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'move_board':
|
||||||
|
await this.moveBoard(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'copy_swimlane':
|
||||||
|
await this.copySwimlane(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'move_swimlane':
|
||||||
|
await this.moveSwimlane(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'copy_list':
|
||||||
|
await this.copyList(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'move_list':
|
||||||
|
await this.moveList(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'copy_card':
|
||||||
|
await this.copyCard(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'move_card':
|
||||||
|
await this.moveCard(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'copy_checklist':
|
||||||
|
await this.copyChecklist(operationId, operationData);
|
||||||
|
break;
|
||||||
|
case 'move_checklist':
|
||||||
|
await this.moveChecklist(operationId, operationData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown operation type: ${operationType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as completed
|
||||||
|
operation.status = 'completed';
|
||||||
|
operation.progress = 100;
|
||||||
|
operation.endTime = new Date();
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
console.log(`Completed board operation: ${operationType} for board ${operation.boardId}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Board operation ${operationType} failed:`, error);
|
||||||
|
operation.status = 'error';
|
||||||
|
operation.error = error.message;
|
||||||
|
operation.endTime = new Date();
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update board operation progress
|
||||||
|
*/
|
||||||
|
updateBoardOperation(operationId, operation) {
|
||||||
|
const operations = boardOperations.get();
|
||||||
|
operations.set(operationId, operation);
|
||||||
|
boardOperations.set(operations);
|
||||||
|
|
||||||
|
// Update progress map
|
||||||
|
const progressMap = boardOperationProgress.get();
|
||||||
|
progressMap.set(operationId, {
|
||||||
|
progress: operation.progress,
|
||||||
|
status: operation.status,
|
||||||
|
error: operation.error
|
||||||
|
});
|
||||||
|
boardOperationProgress.set(progressMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy board operation
|
||||||
|
*/
|
||||||
|
async copyBoard(operationId, data) {
|
||||||
|
const { sourceBoardId, targetBoardId, copyOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate copy progress
|
||||||
|
const steps = ['copying_swimlanes', 'copying_lists', 'copying_cards', 'copying_attachments', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move board operation
|
||||||
|
*/
|
||||||
|
async moveBoard(operationId, data) {
|
||||||
|
const { sourceBoardId, targetBoardId, moveOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate move progress
|
||||||
|
const steps = ['preparing_move', 'moving_swimlanes', 'moving_lists', 'moving_cards', 'updating_references', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy swimlane operation
|
||||||
|
*/
|
||||||
|
async copySwimlane(operationId, data) {
|
||||||
|
const { sourceSwimlaneId, targetBoardId, copyOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate copy progress
|
||||||
|
const steps = ['copying_swimlane', 'copying_lists', 'copying_cards', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move swimlane operation
|
||||||
|
*/
|
||||||
|
async moveSwimlane(operationId, data) {
|
||||||
|
const { sourceSwimlaneId, targetBoardId, moveOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate move progress
|
||||||
|
const steps = ['preparing_move', 'moving_swimlane', 'updating_references', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy list operation
|
||||||
|
*/
|
||||||
|
async copyList(operationId, data) {
|
||||||
|
const { sourceListId, targetBoardId, copyOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate copy progress
|
||||||
|
const steps = ['copying_list', 'copying_cards', 'copying_attachments', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move list operation
|
||||||
|
*/
|
||||||
|
async moveList(operationId, data) {
|
||||||
|
const { sourceListId, targetBoardId, moveOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate move progress
|
||||||
|
const steps = ['preparing_move', 'moving_list', 'updating_references', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy card operation
|
||||||
|
*/
|
||||||
|
async copyCard(operationId, data) {
|
||||||
|
const { sourceCardId, targetListId, copyOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate copy progress
|
||||||
|
const steps = ['copying_card', 'copying_attachments', 'copying_checklists', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move card operation
|
||||||
|
*/
|
||||||
|
async moveCard(operationId, data) {
|
||||||
|
const { sourceCardId, targetListId, moveOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate move progress
|
||||||
|
const steps = ['preparing_move', 'moving_card', 'updating_references', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy checklist operation
|
||||||
|
*/
|
||||||
|
async copyChecklist(operationId, data) {
|
||||||
|
const { sourceChecklistId, targetCardId, copyOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate copy progress
|
||||||
|
const steps = ['copying_checklist', 'copying_items', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move checklist operation
|
||||||
|
*/
|
||||||
|
async moveChecklist(operationId, data) {
|
||||||
|
const { sourceChecklistId, targetCardId, moveOptions } = data;
|
||||||
|
const operation = boardOperations.get().get(operationId);
|
||||||
|
|
||||||
|
// Simulate move progress
|
||||||
|
const steps = ['preparing_move', 'moving_checklist', 'finalizing'];
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
operation.progress = Math.round(((i + 1) / steps.length) * 100);
|
||||||
|
this.updateBoardOperation(operationId, operation);
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get board operations for a specific board
|
||||||
|
*/
|
||||||
|
getBoardOperations(boardId) {
|
||||||
|
const operations = boardOperations.get();
|
||||||
|
const boardOps = [];
|
||||||
|
|
||||||
|
for (const [operationId, operation] of operations) {
|
||||||
|
if (operation.boardId === boardId) {
|
||||||
|
boardOps.push(operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boardOps.sort((a, b) => b.startTime - a.startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all board operations with pagination
|
||||||
|
*/
|
||||||
|
getAllBoardOperations(page = 1, limit = 20, searchTerm = '') {
|
||||||
|
const operations = boardOperations.get();
|
||||||
|
const allOps = Array.from(operations.values());
|
||||||
|
|
||||||
|
// Filter by search term if provided
|
||||||
|
let filteredOps = allOps;
|
||||||
|
if (searchTerm) {
|
||||||
|
filteredOps = allOps.filter(op =>
|
||||||
|
op.boardId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
op.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by start time (newest first)
|
||||||
|
filteredOps.sort((a, b) => b.startTime - a.startTime);
|
||||||
|
|
||||||
|
// Paginate
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
const endIndex = startIndex + limit;
|
||||||
|
const paginatedOps = filteredOps.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return {
|
||||||
|
operations: paginatedOps,
|
||||||
|
total: filteredOps.length,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: Math.ceil(filteredOps.length / limit)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get board operation statistics
|
||||||
|
*/
|
||||||
|
getBoardOperationStats() {
|
||||||
|
const operations = boardOperations.get();
|
||||||
|
const stats = {
|
||||||
|
total: operations.size,
|
||||||
|
running: 0,
|
||||||
|
completed: 0,
|
||||||
|
error: 0,
|
||||||
|
byType: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [operationId, operation] of operations) {
|
||||||
|
stats[operation.status]++;
|
||||||
|
|
||||||
|
if (!stats.byType[operation.type]) {
|
||||||
|
stats.byType[operation.type] = 0;
|
||||||
|
}
|
||||||
|
stats.byType[operation.type]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const cronMigrationManager = new CronMigrationManager();
|
||||||
|
|
||||||
|
// Initialize cron jobs on server start
|
||||||
|
Meteor.startup(() => {
|
||||||
|
cronMigrationManager.initializeCronJobs();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Meteor methods for client-server communication
|
||||||
|
Meteor.methods({
|
||||||
|
'cron.startAllMigrations'() {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.startAllMigrations();
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.startJob'(cronName) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.startCronJob(cronName);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.stopJob'(cronName) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.stopCronJob(cronName);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.pauseJob'(cronName) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.pauseCronJob(cronName);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.resumeJob'(cronName) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.resumeCronJob(cronName);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.removeJob'(cronName) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.removeCronJob(cronName);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.addJob'(jobData) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.addCronJob(jobData);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.getJobs'() {
|
||||||
|
return cronMigrationManager.getAllCronJobs();
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.getMigrationProgress'() {
|
||||||
|
return {
|
||||||
|
progress: cronMigrationProgress.get(),
|
||||||
|
status: cronMigrationStatus.get(),
|
||||||
|
currentStep: cronMigrationCurrentStep.get(),
|
||||||
|
steps: cronMigrationSteps.get(),
|
||||||
|
isMigrating: cronIsMigrating.get()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.startBoardOperation'(boardId, operationType, operationData) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.startBoardOperation(boardId, operationType, operationData);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.getBoardOperations'(boardId) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.getBoardOperations(boardId);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.getAllBoardOperations'(page, limit, searchTerm) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.getAllBoardOperations(page, limit, searchTerm);
|
||||||
|
},
|
||||||
|
|
||||||
|
'cron.getBoardOperationStats'() {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronMigrationManager.getBoardOperationStats();
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue