Browse Source

Fixed Admin Panel Settings menus Attachments and Cron.

Thanks to xet7 !
Lauri Ojansivu 1 week ago
parent
commit
7bb1e24bda

+ 0 - 3
client/00-startup.js

@@ -15,6 +15,3 @@ import '/client/components/migrationProgress';
 
 // Import cron settings
 import '/client/components/settings/cronSettings';
-
-// Import settings tabs CSS
-import '/client/components/settings/settingsTabs.css';

+ 70 - 25
client/components/settings/attachmentSettings.jade

@@ -1,29 +1,74 @@
 template(name="attachmentSettings")
-  .attachment-settings-content
-    .settings-tabs
-      ul.tab-nav
-        li(class="{{#if showStorageSettings}}active{{/if}}")
-          a.js-attachment-storage-settings(data-id="storage-settings")
-            i.fa.fa-cog
-            | {{_ 'attachment-storage-settings'}}
-        li(class="{{#if showMigration}}active{{/if}}")
-          a.js-attachment-migration(data-id="attachment-migration")
-            i.fa.fa-arrow-right
-            | {{_ 'attachment-migration'}}
-        li(class="{{#if showMonitoring}}active{{/if}}")
-          a.js-attachment-monitoring(data-id="attachment-monitoring")
-            i.fa.fa-chart-line
-            | {{_ 'attachment-monitoring'}}
-    
-    .tab-content
-      if loading
-        +spinner
-      else if showStorageSettings
-        +storageSettings
-      else if showMigration
-        +attachmentMigration
-      else if showMonitoring
-        +attachmentMonitoring
+  ul#attachment-setting.setting-detail
+    li
+      h3 {{_ 'attachment-storage-configuration'}}
+      .form-group
+        label {{_ 'writable-path'}}
+        input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
+        small.form-text.text-muted {{_ 'filesystem-path-description'}}
+      
+      .form-group
+        label {{_ 'attachments-path'}}
+        input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
+        small.form-text.text-muted {{_ 'attachments-path-description'}}
+      
+      .form-group
+        label {{_ 'avatars-path'}}
+        input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
+        small.form-text.text-muted {{_ 'avatars-path-description'}}
+
+    li
+      h3 {{_ 'mongodb-gridfs-storage'}}
+      .form-group
+        label {{_ 'gridfs-enabled'}}
+        input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
+        small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
+
+    li
+      h3 {{_ 's3-minio-storage'}}
+      .form-group
+        label {{_ 's3-enabled'}}
+        input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
+        small.form-text.text-muted {{_ 's3-enabled-description'}}
+      
+      .form-group
+        label {{_ 's3-endpoint'}}
+        input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
+        small.form-text.text-muted {{_ 's3-endpoint-description'}}
+      
+      .form-group
+        label {{_ 's3-bucket'}}
+        input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
+        small.form-text.text-muted {{_ 's3-bucket-description'}}
+      
+      .form-group
+        label {{_ 's3-region'}}
+        input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
+        small.form-text.text-muted {{_ 's3-region-description'}}
+      
+      .form-group
+        label {{_ 's3-access-key'}}
+        input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
+        small.form-text.text-muted {{_ 's3-access-key-description'}}
+      
+      .form-group
+        label {{_ 's3-secret-key'}}
+        input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
+        small.form-text.text-muted {{_ 's3-secret-key-description'}}
+      
+      .form-group
+        label {{_ 's3-ssl-enabled'}}
+        input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
+        small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
+      
+      .form-group
+        label {{_ 's3-port'}}
+        input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
+        small.form-text.text-muted {{_ 's3-port-description'}}
+      
+      .form-group
+        button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
+        button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
 
 template(name="storageSettings")
   .storage-settings

+ 0 - 504
client/components/settings/attachmentSettings.js

@@ -1,504 +0,0 @@
-import { ReactiveCache } from '/imports/reactiveCache';
-import { TAPi18n } from '/imports/i18n';
-
-import { Meteor } from 'meteor/meteor';
-import { Session } from 'meteor/session';
-import { Tracker } from 'meteor/tracker';
-import { ReactiveVar } from 'meteor/reactive-var';
-import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
-import { Chart } from 'chart.js';
-
-// Global reactive variables for attachment settings
-const attachmentSettings = {
-  loading: new ReactiveVar(false),
-  showStorageSettings: new ReactiveVar(false),
-  showMigration: new ReactiveVar(false),
-  showMonitoring: new ReactiveVar(false),
-  
-  // Storage configuration
-  filesystemPath: new ReactiveVar(''),
-  attachmentsPath: new ReactiveVar(''),
-  avatarsPath: new ReactiveVar(''),
-  gridfsEnabled: new ReactiveVar(false),
-  s3Enabled: new ReactiveVar(false),
-  s3Endpoint: new ReactiveVar(''),
-  s3Bucket: new ReactiveVar(''),
-  s3Region: new ReactiveVar(''),
-  s3SslEnabled: new ReactiveVar(false),
-  s3Port: new ReactiveVar(443),
-  
-  // Migration settings
-  migrationBatchSize: new ReactiveVar(10),
-  migrationDelayMs: new ReactiveVar(1000),
-  migrationCpuThreshold: new ReactiveVar(70),
-  migrationProgress: new ReactiveVar(0),
-  migrationStatus: new ReactiveVar('idle'),
-  migrationLog: new ReactiveVar(''),
-  
-  // Monitoring data
-  totalAttachments: new ReactiveVar(0),
-  filesystemAttachments: new ReactiveVar(0),
-  gridfsAttachments: new ReactiveVar(0),
-  s3Attachments: new ReactiveVar(0),
-  totalSize: new ReactiveVar(0),
-  filesystemSize: new ReactiveVar(0),
-  gridfsSize: new ReactiveVar(0),
-  s3Size: new ReactiveVar(0),
-  
-  // Migration state
-  isMigrationRunning: new ReactiveVar(false),
-  isMigrationPaused: new ReactiveVar(false),
-  migrationQueue: new ReactiveVar([]),
-  currentMigration: new ReactiveVar(null)
-};
-
-// Main attachment settings component
-BlazeComponent.extendComponent({
-  onCreated() {
-    this.loading = attachmentSettings.loading;
-    this.showStorageSettings = attachmentSettings.showStorageSettings;
-    this.showMigration = attachmentSettings.showMigration;
-    this.showMonitoring = attachmentSettings.showMonitoring;
-    
-    // Set default sub-menu state
-    this.showStorageSettings.set(true);
-    this.showMigration.set(false);
-    this.showMonitoring.set(false);
-    
-    // Load initial data
-    this.loadStorageConfiguration();
-    this.loadMigrationSettings();
-    this.loadMonitoringData();
-  },
-
-  // Template helpers for this component
-  loading() {
-    return this.loading.get();
-  },
-  showStorageSettings() {
-    return this.showStorageSettings.get();
-  },
-  showMigration() {
-    return this.showMigration.get();
-  },
-  showMonitoring() {
-    return this.showMonitoring.get();
-  },
-
-  events() {
-    return [
-      {
-        'click a.js-attachment-storage-settings': this.switchToStorageSettings,
-        'click a.js-attachment-migration': this.switchToMigration,
-        'click a.js-attachment-monitoring': this.switchToMonitoring,
-      }
-    ];
-  },
-
-  switchToStorageSettings(event) {
-    this.switchMenu(event, 'storage-settings');
-    this.showStorageSettings.set(true);
-    this.showMigration.set(false);
-    this.showMonitoring.set(false);
-  },
-
-  switchToMigration(event) {
-    this.switchMenu(event, 'attachment-migration');
-    this.showStorageSettings.set(false);
-    this.showMigration.set(true);
-    this.showMonitoring.set(false);
-  },
-
-  switchToMonitoring(event) {
-    this.switchMenu(event, 'attachment-monitoring');
-    this.showStorageSettings.set(false);
-    this.showMigration.set(false);
-    this.showMonitoring.set(true);
-  },
-
-  switchMenu(event, targetId) {
-    const target = $(event.target);
-    if (!target.hasClass('active')) {
-      this.loading.set(true);
-      
-      $('.side-menu li.active').removeClass('active');
-      target.parent().addClass('active');
-      
-      // Load data based on target
-      if (targetId === 'storage-settings') {
-        this.loadStorageConfiguration();
-      } else if (targetId === 'attachment-migration') {
-        this.loadMigrationSettings();
-      } else if (targetId === 'attachment-monitoring') {
-        this.loadMonitoringData();
-      }
-      
-      this.loading.set(false);
-    }
-  },
-
-  loadStorageConfiguration() {
-    Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
-      if (!error && result) {
-        attachmentSettings.filesystemPath.set(result.filesystemPath || '');
-        attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
-        attachmentSettings.avatarsPath.set(result.avatarsPath || '');
-        attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
-        attachmentSettings.s3Enabled.set(result.s3Enabled || false);
-        attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
-        attachmentSettings.s3Bucket.set(result.s3Bucket || '');
-        attachmentSettings.s3Region.set(result.s3Region || '');
-        attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
-        attachmentSettings.s3Port.set(result.s3Port || 443);
-      }
-    });
-  },
-
-  loadMigrationSettings() {
-    Meteor.call('getAttachmentMigrationSettings', (error, result) => {
-      if (!error && result) {
-        attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
-        attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
-        attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
-        attachmentSettings.migrationStatus.set(result.status || 'idle');
-        attachmentSettings.migrationProgress.set(result.progress || 0);
-      }
-    });
-  },
-
-  loadMonitoringData() {
-    Meteor.call('getAttachmentMonitoringData', (error, result) => {
-      if (!error && result) {
-        attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
-        attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
-        attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
-        attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
-        attachmentSettings.totalSize.set(result.totalSize || 0);
-        attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
-        attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
-        attachmentSettings.s3Size.set(result.s3Size || 0);
-      }
-    });
-  }
-}).register('attachmentSettings');
-
-// Storage settings component
-BlazeComponent.extendComponent({
-  onCreated() {
-    this.filesystemPath = attachmentSettings.filesystemPath;
-    this.attachmentsPath = attachmentSettings.attachmentsPath;
-    this.avatarsPath = attachmentSettings.avatarsPath;
-    this.gridfsEnabled = attachmentSettings.gridfsEnabled;
-    this.s3Enabled = attachmentSettings.s3Enabled;
-    this.s3Endpoint = attachmentSettings.s3Endpoint;
-    this.s3Bucket = attachmentSettings.s3Bucket;
-    this.s3Region = attachmentSettings.s3Region;
-    this.s3SslEnabled = attachmentSettings.s3SslEnabled;
-    this.s3Port = attachmentSettings.s3Port;
-  },
-
-  events() {
-    return [
-      {
-        'click button.js-test-s3-connection': this.testS3Connection,
-        'click button.js-save-s3-settings': this.saveS3Settings,
-        'change input#s3-secret-key': this.updateS3SecretKey
-      }
-    ];
-  },
-
-  testS3Connection() {
-    const secretKey = $('#s3-secret-key').val();
-    if (!secretKey) {
-      alert(TAPi18n.__('s3-secret-key-required'));
-      return;
-    }
-
-    Meteor.call('testS3Connection', { secretKey }, (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
-      } else {
-        alert(TAPi18n.__('s3-connection-success'));
-      }
-    });
-  },
-
-  saveS3Settings() {
-    const secretKey = $('#s3-secret-key').val();
-    if (!secretKey) {
-      alert(TAPi18n.__('s3-secret-key-required'));
-      return;
-    }
-
-    Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
-      } else {
-        alert(TAPi18n.__('s3-settings-saved'));
-        $('#s3-secret-key').val(''); // Clear the password field
-      }
-    });
-  },
-
-  updateS3SecretKey(event) {
-    // This method can be used to validate the secret key format
-    const secretKey = event.target.value;
-    // Add validation logic here if needed
-  }
-}).register('storageSettings');
-
-// Migration component
-BlazeComponent.extendComponent({
-  onCreated() {
-    this.migrationBatchSize = attachmentSettings.migrationBatchSize;
-    this.migrationDelayMs = attachmentSettings.migrationDelayMs;
-    this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
-    this.migrationProgress = attachmentSettings.migrationProgress;
-    this.migrationStatus = attachmentSettings.migrationStatus;
-    this.migrationLog = attachmentSettings.migrationLog;
-    this.isMigrationRunning = attachmentSettings.isMigrationRunning;
-    this.isMigrationPaused = attachmentSettings.isMigrationPaused;
-    
-    // Subscribe to migration updates
-    this.subscription = Meteor.subscribe('attachmentMigrationStatus');
-    
-    // Set up reactive updates
-    this.autorun(() => {
-      const status = attachmentSettings.migrationStatus.get();
-      if (status === 'running') {
-        this.isMigrationRunning.set(true);
-      } else {
-        this.isMigrationRunning.set(false);
-      }
-    });
-  },
-
-  onDestroyed() {
-    if (this.subscription) {
-      this.subscription.stop();
-    }
-  },
-
-  events() {
-    return [
-      {
-        'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
-        'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
-        'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
-        'click button.js-pause-migration': this.pauseMigration,
-        'click button.js-resume-migration': this.resumeMigration,
-        'click button.js-stop-migration': this.stopMigration,
-        'change input#migration-batch-size': this.updateBatchSize,
-        'change input#migration-delay-ms': this.updateDelayMs,
-        'change input#migration-cpu-threshold': this.updateCpuThreshold
-      }
-    ];
-  },
-
-  startMigration(targetStorage) {
-    const batchSize = parseInt($('#migration-batch-size').val()) || 10;
-    const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
-    const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
-
-    Meteor.call('startAttachmentMigration', {
-      targetStorage,
-      batchSize,
-      delayMs,
-      cpuThreshold
-    }, (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
-      } else {
-        this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
-      }
-    });
-  },
-
-  pauseMigration() {
-    Meteor.call('pauseAttachmentMigration', (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
-      } else {
-        this.addToLog(TAPi18n.__('migration-paused'));
-      }
-    });
-  },
-
-  resumeMigration() {
-    Meteor.call('resumeAttachmentMigration', (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
-      } else {
-        this.addToLog(TAPi18n.__('migration-resumed'));
-      }
-    });
-  },
-
-  stopMigration() {
-    if (confirm(TAPi18n.__('migration-stop-confirm'))) {
-      Meteor.call('stopAttachmentMigration', (error, result) => {
-        if (error) {
-          alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
-        } else {
-          this.addToLog(TAPi18n.__('migration-stopped'));
-        }
-      });
-    }
-  },
-
-  updateBatchSize(event) {
-    const value = parseInt(event.target.value);
-    if (value >= 1 && value <= 100) {
-      attachmentSettings.migrationBatchSize.set(value);
-    }
-  },
-
-  updateDelayMs(event) {
-    const value = parseInt(event.target.value);
-    if (value >= 100 && value <= 10000) {
-      attachmentSettings.migrationDelayMs.set(value);
-    }
-  },
-
-  updateCpuThreshold(event) {
-    const value = parseInt(event.target.value);
-    if (value >= 10 && value <= 90) {
-      attachmentSettings.migrationCpuThreshold.set(value);
-    }
-  },
-
-  addToLog(message) {
-    const timestamp = new Date().toISOString();
-    const currentLog = attachmentSettings.migrationLog.get();
-    const newLog = `[${timestamp}] ${message}\n${currentLog}`;
-    attachmentSettings.migrationLog.set(newLog);
-  }
-}).register('attachmentMigration');
-
-// Monitoring component
-BlazeComponent.extendComponent({
-  onCreated() {
-    this.totalAttachments = attachmentSettings.totalAttachments;
-    this.filesystemAttachments = attachmentSettings.filesystemAttachments;
-    this.gridfsAttachments = attachmentSettings.gridfsAttachments;
-    this.s3Attachments = attachmentSettings.s3Attachments;
-    this.totalSize = attachmentSettings.totalSize;
-    this.filesystemSize = attachmentSettings.filesystemSize;
-    this.gridfsSize = attachmentSettings.gridfsSize;
-    this.s3Size = attachmentSettings.s3Size;
-    
-    // Subscribe to monitoring updates
-    this.subscription = Meteor.subscribe('attachmentMonitoringData');
-    
-    // Set up chart
-    this.autorun(() => {
-      this.updateChart();
-    });
-  },
-
-  onDestroyed() {
-    if (this.subscription) {
-      this.subscription.stop();
-    }
-  },
-
-  events() {
-    return [
-      {
-        'click button.js-refresh-monitoring': this.refreshMonitoring,
-        'click button.js-export-monitoring': this.exportMonitoring
-      }
-    ];
-  },
-
-  refreshMonitoring() {
-    Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
-      }
-    });
-  },
-
-  exportMonitoring() {
-    Meteor.call('exportAttachmentMonitoringData', (error, result) => {
-      if (error) {
-        alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
-      } else {
-        // Download the exported data
-        const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
-        const url = URL.createObjectURL(blob);
-        const a = document.createElement('a');
-        a.href = url;
-        a.download = 'wekan-attachment-monitoring.json';
-        document.body.appendChild(a);
-        a.click();
-        document.body.removeChild(a);
-        URL.revokeObjectURL(url);
-      }
-    });
-  },
-
-  updateChart() {
-    const ctx = document.getElementById('storage-distribution-chart');
-    if (!ctx) return;
-
-    const filesystemCount = this.filesystemAttachments.get();
-    const gridfsCount = this.gridfsAttachments.get();
-    const s3Count = this.s3Attachments.get();
-
-    if (this.chart) {
-      this.chart.destroy();
-    }
-
-    this.chart = new Chart(ctx, {
-      type: 'doughnut',
-      data: {
-        labels: [
-          TAPi18n.__('filesystem-storage'),
-          TAPi18n.__('gridfs-storage'),
-          TAPi18n.__('s3-storage')
-        ],
-        datasets: [{
-          data: [filesystemCount, gridfsCount, s3Count],
-          backgroundColor: [
-            '#28a745',
-            '#007bff',
-            '#ffc107'
-          ]
-        }]
-      },
-      options: {
-        responsive: true,
-        maintainAspectRatio: false,
-        plugins: {
-          legend: {
-            position: 'bottom'
-          }
-        }
-      }
-    });
-  }
-}).register('attachmentMonitoring');
-
-// Template helpers for attachmentSettings
-Template.attachmentSettings.helpers({
-  loading() {
-    const instance = Template.instance();
-    return instance.loading && instance.loading.get();
-  },
-  showStorageSettings() {
-    const instance = Template.instance();
-    return instance.showStorageSettings && instance.showStorageSettings.get();
-  },
-  showMigration() {
-    const instance = Template.instance();
-    return instance.showMigration && instance.showMigration.get();
-  },
-  showMonitoring() {
-    const instance = Template.instance();
-    return instance.showMonitoring && instance.showMonitoring.get();
-  },
-});
-
-// Export the attachment settings for use in other components
-export { attachmentSettings };

+ 43 - 31
client/components/settings/cronSettings.jade

@@ -1,35 +1,47 @@
 template(name="cronSettings")
-  .cron-settings-content
-    .settings-tabs
-      ul.tab-nav
-        li(class="{{#if showMigrations}}active{{/if}}")
-          a.js-cron-migrations(data-id="cron-migrations")
-            i.fa.fa-database
-            | {{_ 'cron-migrations'}}
-        li(class="{{#if showBoardOperations}}active{{/if}}")
-          a.js-cron-board-operations(data-id="cron-board-operations")
-            i.fa.fa-tasks
-            | {{_ 'board-operations'}}
-        li(class="{{#if showJobs}}active{{/if}}")
-          a.js-cron-jobs(data-id="cron-jobs")
-            i.fa.fa-clock-o
-            | {{_ 'cron-jobs'}}
-        li(class="{{#if showAddJob}}active{{/if}}")
-          a.js-cron-add(data-id="cron-add")
-            i.fa.fa-plus
-            | {{_ 'add-cron-job'}}
+  ul#cron-setting.setting-detail
+    li
+      h3 {{_ 'cron-migrations'}}
+      .form-group
+        label {{_ 'migration-status'}}
+        .status-indicator
+          span.status-label {{_ 'status'}}:
+          span.status-value {{migrationStatus}}
+        .progress-section
+          .progress
+            .progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100") 
+              | {{migrationProgress}}%
+          .progress-text
+            | {{migrationProgress}}% {{_ 'complete'}}
+      
+      .form-group
+        button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
+        button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
+        button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
+    
+    li
+      h3 {{_ 'board-operations'}}
+      .form-group
+        label {{_ 'scheduled-board-operations'}}
+        button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
+        button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
+        button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
     
-    .tab-content
-      if loading
-        +spinner
-      else if showMigrations
-        +cronMigrations
-      else if showBoardOperations
-        +cronBoardOperations
-      else if showJobs
-        +cronJobs
-      else if showAddJob
-        +cronAddJob
+    li
+      h3 {{_ 'cron-jobs'}}
+      .form-group
+        label {{_ 'active-cron-jobs'}}
+        each cronJobs
+          .job-item
+            .job-info
+              .job-name {{name}}
+              .job-schedule {{schedule}}
+              .job-description {{description}}
+            .job-actions
+              button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
+              button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
+        .add-job-section
+          button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
 
 template(name="cronMigrations")
   .cron-migrations
@@ -51,7 +63,7 @@ template(name="cronMigrations")
     .migration-progress
       .progress-overview
         .progress-bar
-          .progress-fill(style="width: {{migrationProgress}}%")
+          .progress-fill(style="width: {{migrationProgress}}%") 
         .progress-text {{migrationProgress}}%
         .progress-label {{_ 'overall-progress'}}
       

+ 0 - 552
client/components/settings/cronSettings.js

@@ -1,552 +0,0 @@
-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({});
-  this.queueStats = new ReactiveVar({});
-  this.systemResources = new ReactiveVar({});
-  this.boardMigrationStats = new ReactiveVar({});
-
-  // Load initial data
-  loadCronData(this);
-});
-
-Template.cronSettings.helpers({
-  loading() {
-    const instance = Template.instance();
-    return instance && instance.loading ? instance.loading.get() : true;
-  },
-  
-  showMigrations() {
-    const instance = Template.instance();
-    return instance && instance.showMigrations ? instance.showMigrations.get() : true;
-  },
-  
-  showBoardOperations() {
-    const instance = Template.instance();
-    return instance && instance.showBoardOperations ? instance.showBoardOperations.get() : false;
-  },
-  
-  showJobs() {
-    const instance = Template.instance();
-    return instance && instance.showJobs ? instance.showJobs.get() : false;
-  },
-  
-  showAddJob() {
-    const instance = Template.instance();
-    return instance && instance.showAddJob ? instance.showAddJob.get() : false;
-  },
-  
-  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() {
-    const instance = Template.instance();
-    return instance && instance.boardOperations ? instance.boardOperations.get() : [];
-  },
-
-  operationStats() {
-    const instance = Template.instance();
-    return instance && instance.operationStats ? instance.operationStats.get() : {};
-  },
-
-  pagination() {
-    const instance = Template.instance();
-    return instance && instance.pagination ? instance.pagination.get() : {};
-  },
-
-  queueStats() {
-    const instance = Template.instance();
-    return instance && instance.queueStats ? instance.queueStats.get() : {};
-  },
-
-  systemResources() {
-    const instance = Template.instance();
-    return instance && instance.systemResources ? instance.systemResources.get() : {};
-  },
-
-  boardMigrationStats() {
-    const instance = Template.instance();
-    return instance && instance.boardMigrationStats ? instance.boardMigrationStats.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.switchMenu = function(event, targetID) {
-  const instance = Template.instance();
-  
-  // Reset all tabs
-  instance.showMigrations.set(false);
-  instance.showBoardOperations.set(false);
-  instance.showJobs.set(false);
-  instance.showAddJob.set(false);
-  
-  // Set the selected tab
-  if (targetID === 'cron-migrations') {
-    instance.showMigrations.set(true);
-  } else if (targetID === 'cron-board-operations') {
-    instance.showBoardOperations.set(true);
-  } else if (targetID === 'cron-jobs') {
-    instance.showJobs.set(true);
-  } else if (targetID === 'cron-add') {
-    instance.showAddJob.set(true);
-  }
-};
-
-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);
-    loadBoardOperations(instance);
-  },
-  
-  '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);
-    loadCronJobs(instance);
-  },
-  
-  '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 {
-        // Migrations started successfully
-        pollMigrationProgress(Template.instance());
-      }
-    });
-  },
-  
-  '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();
-    loadCronJobs(Template.instance());
-  },
-  
-  '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');
-        loadCronJobs(Template.instance());
-      }
-    });
-  },
-  
-  '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');
-        loadCronJobs(Template.instance());
-      }
-    });
-  },
-  
-  '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');
-        loadCronJobs(Template.instance());
-      }
-    });
-  },
-  
-  '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');
-          loadCronJobs(Template.instance());
-        }
-      });
-    }
-  },
-  
-  '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);
-        loadCronJobs(Template.instance());
-      }
-    });
-  },
-  
-  '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();
-    loadBoardOperations(Template.instance());
-  },
-
-  '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);
-        loadBoardOperations(Template.instance());
-      }
-    });
-  },
-
-  'input .js-search-board-operations'(event) {
-    const searchTerm = $(event.currentTarget).val();
-    const instance = Template.instance();
-    instance.searchTerm.set(searchTerm);
-    instance.currentPage.set(1);
-    loadBoardOperations(instance);
-  },
-
-  'click .js-prev-page'(event) {
-    event.preventDefault();
-    const instance = Template.instance();
-    const currentPage = instance.currentPage.get();
-    if (currentPage > 1) {
-      instance.currentPage.set(currentPage - 1);
-      loadBoardOperations(instance);
-    }
-  },
-
-  '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);
-      loadBoardOperations(instance);
-    }
-  },
-
-  '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);
-  },
-
-  'click .js-force-board-scan'(event) {
-    event.preventDefault();
-    Meteor.call('cron.forceBoardMigrationScan', (error, result) => {
-      if (error) {
-        console.error('Failed to force board scan:', error);
-        alert('Failed to force board scan: ' + error.message);
-      } else {
-        // Board scan started successfully
-        // Refresh the data
-        loadBoardOperations(Template.instance());
-      }
-    });
-  }
-});
-
-// Helper functions for cron settings
-function loadCronData(instance) {
-  instance.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
-  loadCronJobs(instance);
-  
-  instance.loading.set(false);
-}
-
-function loadCronJobs(instance) {
-  Meteor.call('cron.getJobs', (error, result) => {
-    if (result) {
-      cronJobs.set(result);
-    }
-  });
-}
-
-function loadBoardOperations(instance) {
-  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);
-    }
-  });
-
-  // Load queue stats
-  Meteor.call('cron.getQueueStats', (error, result) => {
-    if (result) {
-      instance.queueStats.set(result);
-    }
-  });
-
-  // Load system resources
-  Meteor.call('cron.getSystemResources', (error, result) => {
-    if (result) {
-      instance.systemResources.set(result);
-    }
-  });
-
-  // Load board migration stats
-  Meteor.call('cron.getBoardMigrationStats', (error, result) => {
-    if (result) {
-      instance.boardMigrationStats.set(result);
-    }
-  });
-}
-
-function pollMigrationProgress(instance) {
-  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);
-}
-
-// Template helpers for cronSettings
-Template.cronSettings.helpers({
-  loading() {
-    const instance = Template.instance();
-    return instance.loading && instance.loading.get();
-  },
-  showMigrations() {
-    const instance = Template.instance();
-    return instance.showMigrations && instance.showMigrations.get();
-  },
-  showBoardOperations() {
-    const instance = Template.instance();
-    return instance.showBoardOperations && instance.showBoardOperations.get();
-  },
-  showJobs() {
-    const instance = Template.instance();
-    return instance.showJobs && instance.showJobs.get();
-  },
-  showAddJob() {
-    const instance = Template.instance();
-    return instance.showAddJob && instance.showAddJob.get();
-  },
-});

+ 144 - 35
client/components/settings/settingBody.jade

@@ -4,115 +4,224 @@ template(name="setting")
       | {{_ 'error-notAuthorized'}}
     else
       .content-title.ext-box
-        if loading.get
-          +spinner
-        else if generalSetting.get
+        if isGeneralSetting
           span
             i.fa.fa-sign-in
             | {{_ 'registration'}}
-        else if emailSetting.get
+        else if isEmailSetting
           span
             i.fa.fa-envelope
             | {{_ 'email'}}
-        else if accountSetting.get
+        else if isAccountSetting
           span
             i.fa.fa-users
             | {{_ 'accounts'}}
-        else if tableVisibilityModeSetting.get
+        else if isTableVisibilityModeSetting
           span
             i.fa.fa-eye
             | {{_ 'tableVisibilityMode'}}
-        else if announcementSetting.get
+        else if isAnnouncementSetting
           span
             i.fa.fa-bullhorn
             | {{_ 'admin-announcement'}}
-        else if accessibilitySetting.get
+        else if isAccessibilitySetting
           span
             i.fa.fa-universal-access
             | {{_ 'accessibility'}}
-        else if layoutSetting.get
+        else if isLayoutSetting
           span
             i.fa.fa-object-group
             | {{_ 'layout'}}
-        else if webhookSetting.get
+        else if isWebhookSetting
           span
             i.fa.fa-globe
             | {{_ 'global-webhook'}}
-        else if attachmentSettings.get
+        else if isAttachmentSettings
           span
             i.fa.fa-paperclip
             | {{_ 'attachments'}}
-        else if cronSettings.get
+        else if isCronSettings
           span
             i.fa.fa-clock-o
             | {{_ 'cron'}}
       .content-body
         .side-menu
           ul
-            li(class="{{#if generalSetting}}active{{/if}}")
+            li(class="{{#if isGeneralSetting}}active{{/if}}")
               a.js-setting-menu(data-id="registration-setting")
                 i.fa.fa-sign-in
                 | {{_ 'registration'}}
             unless isSandstorm
-              li(class="{{#if emailSetting}}active{{/if}}")
+              li(class="{{#if isEmailSetting}}active{{/if}}")
                 a.js-setting-menu(data-id="email-setting")
                   i.fa.fa-envelope
                   | {{_ 'email'}}
-            li(class="{{#if accountSetting}}active{{/if}}")
+            li(class="{{#if isAccountSetting}}active{{/if}}")
               a.js-setting-menu(data-id="account-setting")
                 i.fa.fa-users
                 | {{_ 'accounts'}}
-            li(class="{{#if tableVisibilityModeSetting}}active{{/if}}")
+            li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
               a.js-setting-menu(data-id="tableVisibilityMode-setting")
                 i.fa.fa-eye
                 | {{_ 'tableVisibilityMode'}}
-            li(class="{{#if announcementSetting}}active{{/if}}")
+            li(class="{{#if isAnnouncementSetting}}active{{/if}}")
               a.js-setting-menu(data-id="announcement-setting")
                 i.fa.fa-bullhorn
                 | {{_ 'admin-announcement'}}
-            li(class="{{#if accessibilitySetting}}active{{/if}}")
+            li(class="{{#if isAccessibilitySetting}}active{{/if}}")
               a.js-setting-menu(data-id="accessibility-setting")
                 i.fa.fa-universal-access
                 | {{_ 'accessibility'}}
-            li(class="{{#if layoutSetting}}active{{/if}}")
+            li(class="{{#if isLayoutSetting}}active{{/if}}")
               a.js-setting-menu(data-id="layout-setting")
                 i.fa.fa-object-group
                 | {{_ 'layout'}}
-            li(class="{{#if webhookSetting}}active{{/if}}")
+            li(class="{{#if isWebhookSetting}}active{{/if}}")
               a.js-setting-menu(data-id="webhook-setting")
                 i.fa.fa-globe
                 | {{_ 'global-webhook'}}
-            li(class="{{#if attachmentSettings}}active{{/if}}")
+            li(class="{{#if isAttachmentSettings}}active{{/if}}")
               a.js-setting-menu(data-id="attachment-settings")
                 i.fa.fa-paperclip
                 | {{_ 'attachments'}}
-            li(class="{{#if cronSettings}}active{{/if}}")
+            li(class="{{#if isCronSettings}}active{{/if}}")
               a.js-setting-menu(data-id="cron-settings")
                 i.fa.fa-clock-o
                 | {{_ 'cron'}}
         .main-body
-          if loading.get
+          if isLoading
             +spinner
-          else if attachmentSettings.get
-            +attachmentSettings
-          else if cronSettings.get
-            +cronSettings
-          else if generalSetting.get
+          else if isAttachmentSettings
+            ul#attachment-setting.setting-detail
+              li
+                h3 {{_ 'attachment-storage-configuration'}}
+                .form-group
+                  label {{_ 'writable-path'}}
+                  input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
+                  small.form-text.text-muted {{_ 'filesystem-path-description'}}
+                
+                .form-group
+                  label {{_ 'attachments-path'}}
+                  input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
+                  small.form-text.text-muted {{_ 'attachments-path-description'}}
+                
+                .form-group
+                  label {{_ 'avatars-path'}}
+                  input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
+                  small.form-text.text-muted {{_ 'avatars-path-description'}}
+
+              li
+                h3 {{_ 'mongodb-gridfs-storage'}}
+                .form-group
+                  label {{_ 'gridfs-enabled'}}
+                  input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
+                  small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
+
+              li
+                h3 {{_ 's3-minio-storage'}}
+                .form-group
+                  label {{_ 's3-enabled'}}
+                  input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
+                  small.form-text.text-muted {{_ 's3-enabled-description'}}
+                
+                .form-group
+                  label {{_ 's3-endpoint'}}
+                  input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
+                  small.form-text.text-muted {{_ 's3-endpoint-description'}}
+                
+                .form-group
+                  label {{_ 's3-bucket'}}
+                  input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
+                  small.form-text.text-muted {{_ 's3-bucket-description'}}
+                
+                .form-group
+                  label {{_ 's3-region'}}
+                  input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
+                  small.form-text.text-muted {{_ 's3-region-description'}}
+                
+                .form-group
+                  label {{_ 's3-access-key'}}
+                  input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
+                  small.form-text.text-muted {{_ 's3-access-key-description'}}
+                
+                .form-group
+                  label {{_ 's3-secret-key'}}
+                  input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
+                  small.form-text.text-muted {{_ 's3-secret-key-description'}}
+                
+                .form-group
+                  label {{_ 's3-ssl-enabled'}}
+                  input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
+                  small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
+                
+                .form-group
+                  label {{_ 's3-port'}}
+                  input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
+                  small.form-text.text-muted {{_ 's3-port-description'}}
+                
+                .form-group
+                  button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
+                  button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
+          else if isCronSettings
+            ul#cron-setting.setting-detail
+              li
+                h3 {{_ 'cron-migrations'}}
+                .form-group
+                  label {{_ 'migration-status'}}
+                  .status-indicator
+                    span.status-label {{_ 'status'}}:
+                    span.status-value {{migrationStatus}}
+                  .progress-section
+                    .progress
+                      .progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100") 
+                        | {{migrationProgress}}%
+                    .progress-text
+                      | {{migrationProgress}}% {{_ 'complete'}}
+                
+                .form-group
+                  button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
+                  button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
+                  button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
+              
+              li
+                h3 {{_ 'board-operations'}}
+                .form-group
+                  label {{_ 'scheduled-board-operations'}}
+                  button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
+                  button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
+                  button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
+              
+              li
+                h3 {{_ 'cron-jobs'}}
+                .form-group
+                  label {{_ 'active-cron-jobs'}}
+                  each cronJobs
+                    .job-item
+                      .job-info
+                        .job-name {{name}}
+                        .job-schedule {{schedule}}
+                        .job-description {{description}}
+                      .job-actions
+                        button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
+                        button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
+                  .add-job-section
+                    button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
+          else if isGeneralSetting
             +general
-          else if emailSetting.get
+          else if isEmailSetting
             unless isSandstorm
               +email
-          else if accountSetting.get
+          else if isAccountSetting
             +accountSettings
-          else if tableVisibilityModeSetting.get
+          else if isTableVisibilityModeSetting
             +tableVisibilityModeSettings
-          else if announcementSetting.get
+          else if isAnnouncementSetting
             +announcementSettings
-          else if accessibilitySetting.get
+          else if isAccessibilitySetting
             +accessibilitySettings
-          else if layoutSetting.get
+          else if isLayoutSetting
             +layoutSettings
-          else if webhookSetting.get
+          else if isWebhookSetting
             +webhookSettings
 
 template(name="webhookSettings")

+ 228 - 85
client/components/settings/settingBody.js

@@ -34,11 +34,239 @@ BlazeComponent.extendComponent({
   setError(error) {
     this.error.set(error);
   },
+  
+  // Template helpers moved to BlazeComponent - using different names to avoid conflicts
+  isGeneralSetting() {
+    return this.generalSetting && this.generalSetting.get();
+  },
+  isEmailSetting() {
+    return this.emailSetting && this.emailSetting.get();
+  },
+  isAccountSetting() {
+    return this.accountSetting && this.accountSetting.get();
+  },
+  isTableVisibilityModeSetting() {
+    return this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get();
+  },
+  isAnnouncementSetting() {
+    return this.announcementSetting && this.announcementSetting.get();
+  },
+  isAccessibilitySetting() {
+    return this.accessibilitySetting && this.accessibilitySetting.get();
+  },
+  isLayoutSetting() {
+    return this.layoutSetting && this.layoutSetting.get();
+  },
+  isWebhookSetting() {
+    return this.webhookSetting && this.webhookSetting.get();
+  },
+  isAttachmentSettings() {
+    return this.attachmentSettings && this.attachmentSettings.get();
+  },
+  isCronSettings() {
+    return this.cronSettings && this.cronSettings.get();
+  },
+  isLoading() {
+    return this.loading && this.loading.get();
+  },
+
+  // Attachment settings helpers
+  filesystemPath() {
+    return process.env.WRITABLE_PATH || '/data';
+  },
+  
+  attachmentsPath() {
+    const writablePath = process.env.WRITABLE_PATH || '/data';
+    return `${writablePath}/attachments`;
+  },
+  
+  avatarsPath() {
+    const writablePath = process.env.WRITABLE_PATH || '/data';
+    return `${writablePath}/avatars`;
+  },
+  
+  gridfsEnabled() {
+    return process.env.GRIDFS_ENABLED === 'true';
+  },
+  
+  s3Enabled() {
+    return process.env.S3_ENABLED === 'true';
+  },
+  
+  s3Endpoint() {
+    return process.env.S3_ENDPOINT || '';
+  },
+  
+  s3Bucket() {
+    return process.env.S3_BUCKET || '';
+  },
+  
+  s3Region() {
+    return process.env.S3_REGION || '';
+  },
+  
+  s3SslEnabled() {
+    return process.env.S3_SSL_ENABLED === 'true';
+  },
+  
+  s3Port() {
+    return process.env.S3_PORT || 443;
+  },
+
+  // Cron settings helpers
+  migrationStatus() {
+    return 'idle'; // Placeholder
+  },
+  
+  migrationProgress() {
+    return 0; // Placeholder
+  },
+  
+  cronJobs() {
+    return []; // Placeholder
+  },
 
   setLoading(w) {
     this.loading.set(w);
   },
 
+  // Event handlers for attachment settings
+  'click button.js-test-s3-connection'(event) {
+    event.preventDefault();
+    const secretKey = $('#s3-secret-key').val();
+    if (!secretKey) {
+      alert(TAPi18n.__('s3-secret-key-required'));
+      return;
+    }
+
+    Meteor.call('testS3Connection', { secretKey }, (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('s3-connection-success'));
+      }
+    });
+  },
+
+  'click button.js-save-s3-settings'(event) {
+    event.preventDefault();
+    const secretKey = $('#s3-secret-key').val();
+    if (!secretKey) {
+      alert(TAPi18n.__('s3-secret-key-required'));
+      return;
+    }
+
+    Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('s3-settings-saved'));
+        $('#s3-secret-key').val(''); // Clear the password field
+      }
+    });
+  },
+
+  // Event handlers for cron settings
+  'click button.js-start-all-migrations'(event) {
+    event.preventDefault();
+    Meteor.call('startAllMigrations', (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('migration-started'));
+      }
+    });
+  },
+
+  'click button.js-pause-all-migrations'(event) {
+    event.preventDefault();
+    Meteor.call('pauseAllMigrations', (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('migration-paused'));
+      }
+    });
+  },
+
+  'click button.js-stop-all-migrations'(event) {
+    event.preventDefault();
+    if (confirm(TAPi18n.__('migration-stop-confirm'))) {
+      Meteor.call('stopAllMigrations', (error, result) => {
+        if (error) {
+          alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
+        } else {
+          alert(TAPi18n.__('migration-stopped'));
+        }
+      });
+    }
+  },
+
+  'click button.js-schedule-board-cleanup'(event) {
+    event.preventDefault();
+    Meteor.call('scheduleBoardCleanup', (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('board-cleanup-scheduled'));
+      }
+    });
+  },
+
+  'click button.js-schedule-board-archive'(event) {
+    event.preventDefault();
+    Meteor.call('scheduleBoardArchive', (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('board-archive-scheduled'));
+      }
+    });
+  },
+
+  'click button.js-schedule-board-backup'(event) {
+    event.preventDefault();
+    Meteor.call('scheduleBoardBackup', (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('board-backup-scheduled'));
+      }
+    });
+  },
+
+  'click button.js-pause-job'(event) {
+    event.preventDefault();
+    const jobId = $(event.target).data('job-id');
+    Meteor.call('pauseCronJob', jobId, (error, result) => {
+      if (error) {
+        alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
+      } else {
+        alert(TAPi18n.__('cron-job-paused'));
+      }
+    });
+  },
+
+  'click button.js-delete-job'(event) {
+    event.preventDefault();
+    const jobId = $(event.target).data('job-id');
+    if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
+      Meteor.call('deleteCronJob', jobId, (error, result) => {
+        if (error) {
+          alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
+        } else {
+          alert(TAPi18n.__('cron-job-deleted'));
+        }
+      });
+    }
+  },
+
+  'click button.js-add-cron-job'(event) {
+    event.preventDefault();
+    // Placeholder for adding a new cron job (e.g., open a modal)
+    alert(TAPi18n.__('add-cron-job-placeholder'));
+  },
+
   checkField(selector) {
     const value = $(selector).val();
     if (!value || value.trim() === '') {
@@ -104,37 +332,6 @@ BlazeComponent.extendComponent({
     $('#display-authentication-method').toggleClass('is-checked');
   },
 
-  switchAttachmentTab(event) {
-    event.preventDefault();
-    const target = $(event.target);
-    const targetID = target.data('id');
-    
-    // Update active tab
-    $('.tab-nav li.active').removeClass('active');
-    target.parent().addClass('active');
-    
-    // Call the attachment settings component method if available
-    if (window.attachmentSettings && window.attachmentSettings.switchMenu) {
-      window.attachmentSettings.switchMenu(event, targetID);
-    }
-  },
-
-  switchCronTab(event) {
-    event.preventDefault();
-    const target = $(event.target);
-    const targetID = target.data('id');
-    
-    // Update active tab
-    $('.tab-nav li.active').removeClass('active');
-    target.parent().addClass('active');
-    
-    // Call the cron settings template method if available
-    const cronTemplate = Template.instance();
-    if (cronTemplate && cronTemplate.switchMenu) {
-      cronTemplate.switchMenu(event, targetID);
-    }
-  },
-
   initializeAttachmentSubMenu() {
     // Set default sub-menu state for attachment settings
     // This will be handled by the attachment settings component
@@ -357,13 +554,6 @@ BlazeComponent.extendComponent({
         'click button.js-save-layout': this.saveLayout,
         'click a.js-toggle-display-authentication-method': this
           .toggleDisplayAuthenticationMethod,
-        'click a.js-attachment-storage-settings': this.switchAttachmentTab,
-        'click a.js-attachment-migration': this.switchAttachmentTab,
-        'click a.js-attachment-monitoring': this.switchAttachmentTab,
-        'click a.js-cron-migrations': this.switchCronTab,
-        'click a.js-cron-board-operations': this.switchCronTab,
-        'click a.js-cron-jobs': this.switchCronTab,
-        'click a.js-cron-add': this.switchCronTab,
       },
     ];
   },
@@ -609,50 +799,3 @@ Template.selectSpinnerName.helpers({
   },
 });
 
-// Template helpers for the setting template
-Template.setting.helpers({
-  generalSetting() {
-    const instance = Template.instance();
-    return instance.generalSetting && instance.generalSetting.get();
-  },
-  emailSetting() {
-    const instance = Template.instance();
-    return instance.emailSetting && instance.emailSetting.get();
-  },
-  accountSetting() {
-    const instance = Template.instance();
-    return instance.accountSetting && instance.accountSetting.get();
-  },
-  tableVisibilityModeSetting() {
-    const instance = Template.instance();
-    return instance.tableVisibilityModeSetting && instance.tableVisibilityModeSetting.get();
-  },
-  announcementSetting() {
-    const instance = Template.instance();
-    return instance.announcementSetting && instance.announcementSetting.get();
-  },
-  accessibilitySetting() {
-    const instance = Template.instance();
-    return instance.accessibilitySetting && instance.accessibilitySetting.get();
-  },
-  layoutSetting() {
-    const instance = Template.instance();
-    return instance.layoutSetting && instance.layoutSetting.get();
-  },
-  webhookSetting() {
-    const instance = Template.instance();
-    return instance.webhookSetting && instance.webhookSetting.get();
-  },
-  attachmentSettings() {
-    const instance = Template.instance();
-    return instance.attachmentSettings && instance.attachmentSettings.get();
-  },
-  cronSettings() {
-    const instance = Template.instance();
-    return instance.cronSettings && instance.cronSettings.get();
-  },
-  loading() {
-    const instance = Template.instance();
-    return instance.loading && instance.loading.get();
-  },
-});

+ 0 - 67
client/components/settings/settingsTabs.css

@@ -1,67 +0,0 @@
-/* Settings Tabs Styles */
-.settings-tabs {
-  margin-bottom: 20px;
-  border-bottom: 1px solid #e0e0e0;
-}
-
-.tab-nav {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-  display: flex;
-  flex-wrap: wrap;
-}
-
-.tab-nav li {
-  margin: 0;
-  padding: 0;
-}
-
-.tab-nav li a {
-  display: block;
-  padding: 12px 20px;
-  text-decoration: none;
-  color: #666;
-  border-bottom: 3px solid transparent;
-  transition: all 0.3s ease;
-  font-weight: 500;
-}
-
-.tab-nav li a:hover {
-  color: #333;
-  background-color: #f5f5f5;
-}
-
-.tab-nav li.active a {
-  color: #2196F3;
-  border-bottom-color: #2196F3;
-  background-color: #f8f9fa;
-}
-
-.tab-nav li a i {
-  margin-right: 8px;
-  width: 16px;
-  text-align: center;
-}
-
-.tab-content {
-  padding: 20px 0;
-  min-height: 400px;
-}
-
-/* Responsive design */
-@media (max-width: 768px) {
-  .tab-nav {
-    flex-direction: column;
-  }
-  
-  .tab-nav li a {
-    border-bottom: 1px solid #e0e0e0;
-    border-right: none;
-  }
-  
-  .tab-nav li.active a {
-    border-bottom-color: #2196F3;
-    border-right: 3px solid #2196F3;
-  }
-}