Browse Source

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 !
Lauri Ojansivu 3 days ago
parent
commit
da68b01502

+ 3 - 0
client/00-startup.js

@@ -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';

+ 806 - 0
client/components/settings/cronSettings.css

@@ -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 - 0
client/components/settings/cronSettings.jade

@@ -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 - 0
client/components/settings/cronSettings.js

@@ -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);
+};

+ 6 - 0
client/components/settings/settingBody.jade

@@ -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

+ 2 - 0
client/components/settings/settingBody.js

@@ -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);
     }
   },

+ 9 - 9
client/lib/migrationManager.js

@@ -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;
         }

+ 49 - 1
imports/i18n/en.i18n.json

@@ -96,5 +96,53 @@
   "overall-progress": "Overall Progress",
   "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-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"
 }

+ 3 - 0
server/00checkStartup.js

@@ -27,3 +27,6 @@ if (errors.length > 0) {
 
 // Import migration runner for on-demand migrations
 import './migrationRunner';
+
+// Import cron migration manager for cron-based migrations
+import './cronMigrationManager';

+ 982 - 0
server/cronMigrationManager.js

@@ -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();
+  }
+});