Skip to content

Conversation

@nfebe
Copy link
Contributor

@nfebe nfebe commented Jan 3, 2026

No description provided.

- Create BackupsTab.vue component for backup management
- Add backup/restore with async job polling (2s interval)
- Add scheduled backup creation UI with cron expression
- Add BackupJob types and API methods to api.ts
- Update DeploymentDetailView to use BackupsTab component
- Include restore, download, delete functionality
@sourceant
Copy link

sourceant bot commented Jan 3, 2026

Code Review Summary

The pull request introduces a new BackupsTab component, its corresponding test file, and integrates it into the DeploymentDetailView. New API types and endpoints related to backups and scheduling are also added. The new component provides functionality for creating, restoring, and deleting manual backups, as well as scheduling automated backups with retention policies. The integration into DeploymentDetailView is seamless, adding a new tab for this functionality.

🚀 Key Improvements

  • Added comprehensive backup and scheduling capabilities to deployments.
  • Integrated job polling to provide real-time status updates for backup/restore operations.
  • Provided clear UI for managing backups and scheduled tasks, including empty and loading states.

💡 Minor Suggestions

  • Improve modal accessibility by allowing closure with the Escape key.
  • Add an empty state message for the scheduled tasks section when no tasks exist.
  • Ensure consistent placeholder values in forms by referencing reactive state where possible.
  • Refactor repetitive API error handling into a helper function for cleaner code and consistency.

🚨 Critical Issues

  • Eliminate setTimeout calls in BackupsTab.test.ts as they lead to flaky and unreliable tests. Replace them with wrapper.vm.$nextTick() or flushPromises() for deterministic asynchronous testing.

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment on lines +27 to +32
const mockGetDeploymentBackups = backupsApi.getDeploymentBackups as ReturnType<typeof vi.fn>;
const mockCreateDeploymentBackup = backupsApi.createDeploymentBackup as ReturnType<typeof vi.fn>;
const mockDeleteBackup = backupsApi.delete as ReturnType<typeof vi.fn>;
const mockRestoreBackup = backupsApi.restore as ReturnType<typeof vi.fn>;
const mockListTasks = schedulerApi.listTasks as ReturnType<typeof vi.fn>;
const mockCreateTask = schedulerApi.createTask as ReturnType<typeof vi.fn>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better type safety and IDE auto-completion with vitest mocks, it's recommended to import MockedFunction and use it instead of ReturnType<typeof vi.fn>. This provides more specific typings for mock properties and methods, making the mocks easier to work with.

Suggested change
const mockGetDeploymentBackups = backupsApi.getDeploymentBackups as ReturnType<typeof vi.fn>;
const mockCreateDeploymentBackup = backupsApi.createDeploymentBackup as ReturnType<typeof vi.fn>;
const mockDeleteBackup = backupsApi.delete as ReturnType<typeof vi.fn>;
const mockRestoreBackup = backupsApi.restore as ReturnType<typeof vi.fn>;
const mockListTasks = schedulerApi.listTasks as ReturnType<typeof vi.fn>;
const mockCreateTask = schedulerApi.createTask as ReturnType<typeof vi.fn>;
import { MockedFunction } from "vitest";
const mockGetDeploymentBackups = backupsApi.getDeploymentBackups as MockedFunction<typeof backupsApi.getDeploymentBackups>;
const mockCreateDeploymentBackup = backupsApi.createDeploymentBackup as MockedFunction<typeof backupsApi.createDeploymentBackup>;
const mockDeleteBackup = backupsApi.delete as MockedFunction<typeof backupsApi.delete>;
const mockRestoreBackup = backupsApi.restore as MockedFunction<typeof backupsApi.restore>;
const mockListTasks = schedulerApi.listTasks as MockedFunction<typeof schedulerApi.listTasks>;
const mockCreateTask = schedulerApi.createTask as MockedFunction<typeof schedulerApi.createTask>;

Comment on lines +215 to +193
const props = defineProps<{
deploymentName: string;
}>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the readonly modifier to props explicitly declares that they should not be modified within the component. This enhances type safety and clearly communicates the intended immutability of the prop, aligning with Vue's best practices.

Suggested change
const props = defineProps<{
deploymentName: string;
}>();
const props = defineProps<{
readonly deploymentName: string;
}>();

Comment on lines 231 to 216
const scheduleForm = ref({
name: "",
cronExpr: "0 2 * * *",
retentionCount: 7,
enabled: true,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracting the initial state of the scheduleForm into a factory function (initialScheduleFormState) improves reusability and makes resetting the form to its default values more concise and robust. This approach centralizes the default form structure.

Suggested change
const scheduleForm = ref({
name: "",
cronExpr: "0 2 * * *",
retentionCount: 7,
enabled: true,
const initialScheduleFormState = () => ({
name: "",
cronExpr: "0 2 * * *",
retentionCount: 7,
enabled: true,
});
const scheduleForm = ref(initialScheduleFormState());

Comment on lines 290 to 311
for (const job of activeJobs.value) {
try {
const response = await backupsApi.getJob(job.id);
const updatedJob = response.data.job;
if (updatedJob.status === "completed") {
if (updatedJob.type === "backup") {
notifications.success("Backup Complete", "Backup has been created successfully");
creatingBackup.value = false;
} else {
notifications.success("Restore Complete", "Backup has been restored successfully");
restoringBackup.value = null;
}
await fetchBackups();
} else if (updatedJob.status === "failed") {
if (updatedJob.type === "backup") {
notifications.error("Backup Failed", updatedJob.error || "Backup creation failed");
creatingBackup.value = false;
} else {
notifications.error("Restore Failed", updatedJob.error || "Backup restore failed");
restoringBackup.value = null;
}
} else {
updatedJobs.push(updatedJob);
}
} catch {
updatedJobs.push(job);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current pollActiveJobs uses a for...of loop with await inside, meaning if one job's API call fails, it could delay or block the processing of subsequent jobs in the same polling cycle. Using Promise.allSettled to fetch all job statuses concurrently makes the polling mechanism more robust, as it allows all promises to settle regardless of individual success or failure, ensuring all jobs are checked efficiently and gracefully handling partial failures.

Suggested change
for (const job of activeJobs.value) {
try {
const response = await backupsApi.getJob(job.id);
const updatedJob = response.data.job;
if (updatedJob.status === "completed") {
if (updatedJob.type === "backup") {
notifications.success("Backup Complete", "Backup has been created successfully");
creatingBackup.value = false;
} else {
notifications.success("Restore Complete", "Backup has been restored successfully");
restoringBackup.value = null;
}
await fetchBackups();
} else if (updatedJob.status === "failed") {
if (updatedJob.type === "backup") {
notifications.error("Backup Failed", updatedJob.error || "Backup creation failed");
creatingBackup.value = false;
} else {
notifications.error("Restore Failed", updatedJob.error || "Backup restore failed");
restoringBackup.value = null;
}
} else {
updatedJobs.push(updatedJob);
}
} catch {
updatedJobs.push(job);
}
}
const jobPromises = activeJobs.value.map(async (job) => {
try {
const response = await backupsApi.getJob(job.id);
const updatedJob = response.data.job;
if (updatedJob.status === "completed") {
if (updatedJob.type === "backup") {
notifications.success("Backup Complete", "Backup has been created successfully");
creatingBackup.value = false;
} else {
notifications.success("Restore Complete", "Backup has been restored successfully");
restoringBackup.value = null;
}
await fetchBackups();
return null; // Job is completed/failed, no longer active
} else if (updatedJob.status === "failed") {
if (updatedJob.type === "backup") {
notifications.error("Backup Failed", updatedJob.error || "Backup creation failed");
creatingBackup.value = false;
} else {
notifications.error("Restore Failed", updatedJob.error || "Backup restore failed");
restoringBackup.value = null;
}
return null; // Job is completed/failed, no longer active
} else {
return updatedJob; // Job is still active
}
} catch (error) {
console.error("Error polling job:", job.id, error);
return job; // Keep job in active list if API call itself fails
}
});
const results = await Promise.allSettled(jobPromises);
activeJobs.value = results
.filter((result) => result.status === 'fulfilled' && result.value !== null)
.map((result) => (result as PromiseFulfilledResult<BackupJob>).value as BackupJob);
if (activeJobs.value.length === 0) {
stopJobPolling();
}

Comment on lines +477 to +472
const date = new Date(dateStr);
return date.toLocaleString();
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toLocaleString() method's output varies depending on the user's browser and locale settings, which can lead to an inconsistent user experience. Using Intl.DateTimeFormat with explicit options allows for specifying a consistent format (e.g., 'en-US') or dynamically adapting it if a global locale is managed. Added a check for an empty date string.

Suggested change
const date = new Date(dateStr);
return date.toLocaleString();
};
const formatDate = (dateStr: string): string => {
if (!dateStr) return "N/A";
const date = new Date(dateStr);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(date);
};

Comment on lines +827 to +828
export const backupsApi = {
list: (deployment?: string, limit?: number) =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly defining the full return type of API methods, including Promise<AxiosResponse<T>>, provides better clarity on the expected output and improves type checking for consumers of these API methods. It helps developers understand what to expect without needing to infer types. Ensure AxiosResponse is imported from axios.

Suggested change
export const backupsApi = {
list: (deployment?: string, limit?: number) =>
create: (deploymentName: string): Promise<AxiosResponse<{ job_id: string; message: string }>> =>
apiClient.post<{ job_id: string; message: string }>("/backups", { deployment_name: deploymentName }),

@nfebe nfebe force-pushed the feat/backup-and-scheduler branch from 2aff62c to cb0577f Compare January 3, 2026 13:47
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 3, 2026

Deploying flatrun-ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7041067
Status: ✅  Deploy successful!
Preview URL: https://0969b105.flatrun-ui.pages.dev
Branch Preview URL: https://feat-backup-and-scheduler.flatrun-ui.pages.dev

View logs

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

mockListTasks.mockResolvedValue({ data: { tasks: [] } });
});

const mountBackupsTab = (options: { backups?: typeof mockBackups; tasks?: typeof mockScheduledTasks } = {}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good practice to use the directly imported types (Backup, ScheduledTask) instead of typeof mockBackups for clarity and consistency, as these types are explicitly defined in @/services/api.

Suggested change
const mountBackupsTab = (options: { backups?: typeof mockBackups; tasks?: typeof mockScheduledTasks } = {}) => {
const mountBackupsTab = (options: { backups?: Backup[]; tasks?: ScheduledTask[] } = {}) => {

const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toFixed(1) method will always return one decimal place, even for whole numbers (e.g., "1.0 KB"). To match the test expectation of "1 KB" for exact kilobyte values, explicitly check if the number has a fractional part before applying toFixed(1) or round to the nearest integer if it is effectively a whole number.

Suggested change
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
return (Math.round(bytes / Math.pow(k, i) * 10) / 10).toString() + " " + sizes[i];

Comment on lines +247 to +250
} catch {
scheduledTasks.value = [];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schedulerApi.listTasks already accepts a deployment parameter, making the client-side filtering by t.deployment_name === props.deploymentName redundant. You only need to filter by type if the API does not support that parameter directly.

Suggested change
} catch {
scheduledTasks.value = [];
}
scheduledTasks.value = (response.data.tasks || []).filter(
(t) => t.type === "backup"
);

@nfebe nfebe force-pushed the feat/backup-and-scheduler branch from cb0577f to 0524b77 Compare January 3, 2026 16:22
Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment on lines +129 to +130
await new Promise((r) => setTimeout(r, 10));
await wrapper.vm.$nextTick();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern await new Promise((r) => setTimeout(r, 10)); await wrapper.vm.$nextTick(); is a common workaround but flushPromises() from @vue/test-utils is generally a more robust and idiomatic way to ensure all pending promises (including microtasks and Vue's reactivity updates) are resolved before assertions. Consider replacing this pattern throughout the test file for better clarity and reliability.

Suggested change
await new Promise((r) => setTimeout(r, 10));
await wrapper.vm.$nextTick();
await flushPromises();

Comment on lines +812 to +813
export type BackupJobType = "backup" | "restore";
export type BackupJobStatus = "pending" | "running" | "completed" | "failed";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type aliases BackupJobType and BackupJobStatus are already defined on lines 809 and 810. Using these aliases here improves type consistency and avoids redundant inline string literal types. It makes the BackupJob interface more readable and maintainable by referring to established types.

Suggested change
export type BackupJobType = "backup" | "restore";
export type BackupJobStatus = "pending" | "running" | "completed" | "failed";
type: BackupJobType;
status: BackupJobStatus;

Add comprehensive tests for:
- Component structure and rendering
- Backup list display and actions
- Create, delete, and restore functionality
- Schedule modal and scheduled tasks
- Job polling behavior

Signed-off-by: nfebe <fenn25.fn@gmail.com>
@nfebe nfebe force-pushed the feat/backup-and-scheduler branch from 0524b77 to 7041067 Compare January 3, 2026 16:33
@sourceant
Copy link

sourceant bot commented Jan 3, 2026

🔍 Code Review

💡 1. **src/components/BackupsTab.test.ts** (Lines 128-130) - BUG

Using setTimeout in tests is generally discouraged as it can lead to flaky tests due to timing issues. For awaiting Vue's DOM updates, await wrapper.vm.$nextTick() should be sufficient. For pending promises (like API calls), await flushPromises() (from @vue/test-utils) is the correct approach. Consider replacing this pattern throughout the test suite for improved reliability and speed.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 2. **src/components/BackupsTab.test.ts** (Lines 138-140) - BUG

Similar to the previous comment, replace setTimeout with wrapper.vm.$nextTick() to avoid flaky tests. If you are waiting for asynchronous operations that don't directly trigger a Vue update cycle, flushPromises() is the appropriate tool.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 3. **src/components/BackupsTab.test.ts** (Lines 178-180) - BUG

Again, avoid setTimeout for waiting on Vue component updates or async operations. wrapper.vm.$nextTick() or flushPromises() are more reliable and precise.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 4. **src/components/BackupsTab.test.ts** (Lines 188-190) - BUG

Remove the setTimeout call. Rely on wrapper.vm.$nextTick() to ensure the DOM is updated after asynchronous operations and reactivity changes.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 5. **src/components/BackupsTab.test.ts** (Lines 197-199) - BUG

This setTimeout can cause tests to be fragile. The proper way to await component updates is await wrapper.vm.$nextTick(). If API calls are involved, use await flushPromises().

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 6. **src/components/BackupsTab.test.ts** (Lines 208-210) - BUG

Replace setTimeout with wrapper.vm.$nextTick() for more deterministic and faster tests.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 7. **src/components/BackupsTab.test.ts** (Lines 217-219) - BUG

The use of setTimeout to await component updates should be replaced with wrapper.vm.$nextTick() to prevent race conditions and improve test reliability.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 8. **src/components/BackupsTab.test.ts** (Lines 228-230) - BUG

Remove setTimeout for better test performance and stability. wrapper.vm.$nextTick() or flushPromises() are the correct utilities.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 9. **src/components/BackupsTab.test.ts** (Lines 286-288) - BUG

Eliminate setTimeout for test stability. Use wrapper.vm.$nextTick() to wait for DOM updates.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 10. **src/components/BackupsTab.test.ts** (Lines 321-323) - BUG

Remove the setTimeout to improve test reliability. The proper way to wait for Vue updates is wrapper.vm.$nextTick().

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 11. **src/components/BackupsTab.test.ts** (Lines 390-392) - BUG

Remove setTimeout. Use wrapper.vm.$nextTick() for reliable waiting on component updates.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 12. **src/components/BackupsTab.test.ts** (Lines 409-411) - BUG

Avoid using setTimeout for synchronization in tests. Utilize wrapper.vm.$nextTick() for Vue reactivity and flushPromises() for asynchronous operations.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 13. **src/components/BackupsTab.test.ts** (Lines 418-420) - BUG

Replace setTimeout with wrapper.vm.$nextTick() or flushPromises() for more robust and faster tests.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 14. **src/components/BackupsTab.test.ts** (Lines 427-429) - BUG

Remove setTimeout. Rely on wrapper.vm.$nextTick() to wait for component updates.

Suggested Code:

      await wrapper.vm.$nextTick();

Current Code:

      await wrapper.vm.$nextTick();
      await new Promise((r) => setTimeout(r, 10));
      await wrapper.vm.$nextTick();
💡 15. **src/components/BackupsTab.vue** (Lines 40-44) - STYLE

The v-if="backup.components?.length" check is redundant here because the v-for directive will not iterate if backup.components is falsy or an empty array. Removing the v-if can slightly simplify the template and has no functional impact.

Suggested Code:

          <div class="backup-components">
            <span v-for="comp in backup.components" :key="comp" class="component-badge">
              {{ comp }}
            </span>
          </div>

Current Code:

          <div v-if="backup.components?.length" class="backup-components">
            <span v-for="comp in backup.components" :key="comp" class="component-badge">
              {{ comp }}
            </span>
          </div>
💡 16. **src/components/BackupsTab.vue** (Lines 65-89) - CLARITY

Consider adding an empty state for scheduled tasks. Currently, if scheduledTasks.length is 0, the entire section is not rendered. Providing a message like "No scheduled backups yet" with an action to schedule one would improve user experience and consistency with the main backups list.

Suggested Code:

    <div v-else-if="scheduledTasks.length === 0" class="scheduled-backups-section">
      <h4>Scheduled Backups</h4>
      <div class="empty-state">
        <i class="pi pi-clock" />
        <h4>No scheduled backups yet</h4>
        <p>Set up recurring backups to automatically protect your deployment.</p>
      </div>
    </div>
    <div v-else class="scheduled-backups-section">
      <h4>Scheduled Backups</h4>
      <div class="scheduled-tasks-list">
        <div v-for="task in scheduledTasks" :key="task.id" class="scheduled-task-item">
          <div class="task-info">
            <span class="task-name">{{ task.name }}</span>
            <span class="task-schedule">{{ task.cron_expr }}</span>
            <span v-if="task.next_run" class="task-next"> Next: {{ formatDate(task.next_run) }} </span>
          </div>
          <div class="task-actions">
            <label class="toggle-switch small">
              <input type="checkbox" :checked="task.enabled" @change="toggleTask(task)" />
              <span class="toggle-slider" />
            </label>
            <button class="btn btn-sm btn-secondary" @click="runTaskNow(task.id)">
              <i class="pi pi-play" />
            </button>
            <button class="btn btn-sm btn-danger" @click="confirmDeleteTask(task.id)">
              <i class="pi pi-trash" />
            </button>
          </div>
        </div>
      </div>
    </div>

Current Code:

    <div v-if="scheduledTasks.length > 0" class="scheduled-backups-section">
      <h4>Scheduled Backups</h4>
      <div class="scheduled-tasks-list">
        <div v-for="task in scheduledTasks" :key="task.id" class="scheduled-task-item">
          <div class="task-info">
            <span class="task-name">{{ task.name }}</span>
            <span class="task-schedule">{{ task.cron_expr }}</span>
            <span v-if="task.next_run" class="task-next"> Next: {{ formatDate(task.next_run) }} </span>
          </div>
          <div class="task-actions">
            <label class="toggle-switch small">
              <input type="checkbox" :checked="task.enabled" @change="toggleTask(task)" />
              <span class="toggle-slider" />
            </label>
            <button class="btn btn-sm btn-secondary" @click="runTaskNow(task.id)">
              <i class="pi pi-play" />
            </button>
            <button class="btn btn-sm btn-danger" @click="confirmDeleteTask(task.id)">
              <i class="pi pi-trash" />
            </button>
          </div>
        </div>
      </div>
    </div>
💡 17. **src/components/BackupsTab.vue** (Line 93) - IMPROVEMENT

For improved accessibility, modals should be closable using the Escape key. Add an @keydown.esc="showScheduleModal = false" listener to the modal-overlay or the modal container.

Suggested Code:

      <div v-if="showScheduleModal" class="modal-overlay" @click.self="showScheduleModal = false" @keydown.esc="showScheduleModal = false">

Current Code:

      <div v-if="showScheduleModal" class="modal-overlay" @click.self="showScheduleModal = false">
💡 18. **src/components/BackupsTab.vue** (Line 113) - STYLE

To maintain consistency and avoid hardcoding, consider referencing the default cronExpr value from initialScheduleFormState for the placeholder. This ensures that if the default value changes, the placeholder automatically updates.

Suggested Code:

              <input v-model="scheduleForm.cronExpr" type="text" :placeholder="initialScheduleFormState().cronExpr" class="form-control" />

Current Code:

              <input v-model="scheduleForm.cronExpr" type="text" placeholder="0 2 * * *" class="form-control" />
💡 19. **src/components/BackupsTab.vue** (Lines 234-238) - REFACTOR

Error handling is a bit repetitive. You could create a helper function, possibly within useNotificationsStore or a local composable, to centralize API error processing. This would make the code cleaner and easier to maintain consistent error messages.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "load backups");
  } finally {

Current Code:

  } catch (err: any) {
    notifications.error("Error", err.response?.data?.error || "Failed to load backups");
  } finally {
💡 20. **src/components/BackupsTab.vue** (Lines 248-249) - REFACTOR

A dedicated error helper function could simplify this error handling as well. For example, handleApiError(err, 'list tasks').

Suggested Code:

  } catch (err: any) {
    handleApiError(err, 'list tasks');

Current Code:

  } catch {
    scheduledTasks.value = [];
💡 21. **src/components/BackupsTab.vue** (Lines 333-335) - REFACTOR

Centralize the error handling logic to reduce repetition. A helper function can abstract away the notifications.error call and err.response?.data?.error check.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "start backup");
    creatingBackup.value = false;

Current Code:

  } catch (err: any) {
    notifications.error("Backup Failed", err.response?.data?.error || "Failed to start backup");
    creatingBackup.value = false;
  }
💡 22. **src/components/BackupsTab.vue** (Lines 350-352) - REFACTOR

This error handling can also be consolidated using a helper function to keep the component script cleaner and more focused on its core logic.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "delete backup");
  } finally {

Current Code:

  } catch (err: any) {
    notifications.error("Delete Failed", err.response?.data?.error || "Failed to delete backup");
  } finally {
💡 23. **src/components/BackupsTab.vue** (Lines 386-388) - REFACTOR

Consolidate this repetitive error handling by using a shared utility function. This improves maintainability and ensures a consistent error message format.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "start restore");
    restoringBackup.value = null;

Current Code:

  } catch (err: any) {
    notifications.error("Restore Failed", err.response?.data?.error || "Failed to start restore");
    restoringBackup.value = null;
  } finally {
💡 24. **src/components/BackupsTab.vue** (Lines 417-419) - REFACTOR

Encapsulate this error handling pattern into a utility function to avoid code duplication and promote consistency across the component's API interactions.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "create schedule");
  } finally {

Current Code:

  } catch (err: any) {
    notifications.error("Error", err.response?.data?.error || "Failed to create schedule");
  } finally {
💡 25. **src/components/BackupsTab.vue** (Lines 428-430) - REFACTOR

Abstract this error handling into a reusable function for consistency and brevity.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "update task");
  }

Current Code:

  } catch (err: any) {
    notifications.error("Error", err.response?.data?.error || "Failed to update task");
  }
💡 26. **src/components/BackupsTab.vue** (Lines 437-439) - REFACTOR

Consolidate this error message logic into a single helper to prevent repetition and ensure uniform error reporting.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "run task");
  }

Current Code:

  } catch (err: any) {
    notifications.error("Error", err.response?.data?.error || "Failed to run task");
  }
💡 27. **src/components/BackupsTab.vue** (Lines 451-453) - REFACTOR

This error handling block can also be streamlined with the common helper function, reducing boilerplate and improving clarity.

Suggested Code:

  } catch (err: any) {
    handleApiError(err, "delete task");
  } finally {

Current Code:

  } catch (err: any) {
    notifications.error("Delete Failed", err.response?.data?.error || "Failed to delete task");
  } finally {
💡 28. **src/components/BackupsTab.vue** (Line 190) - REFACTOR

To streamline error reporting and reduce code duplication, extract the repetitive error notification logic into a helper function. This function can take the error object and a default message, then use the notifications store. This improves maintainability and consistency.

const handleApiError = (err: any, defaultMsg: string) => {
  notifications.error("Error", err.response?.data?.error || `Failed to ${defaultMsg}`);
};

Then, replace repeated notifications.error(...) calls with handleApiError(err, "...").

Suggested Code:

import ConfirmModal from "@/components/ConfirmModal.vue";

const handleApiError = (err: any, defaultMsg: string) => {
  notifications.error("Error", err.response?.data?.error || `Failed to ${defaultMsg}`);
};

Current Code:

import ConfirmModal from "@/components/ConfirmModal.vue";
💡 29. **src/views/DeploymentDetailView.vue** (Lines 1435-1439) - IMPROVEMENT

The action-modal in DeploymentDetailView.vue is missing @click.self="showActionModal = false" on its overlay, which means clicks outside the modal content won't close it, unlike other modals. This can lead to an inconsistent user experience. Additionally, for accessibility, it should also close with the Escape key.

Suggested Code:

    <Teleport to="body">
      <div v-if="showActionModal" class="modal-overlay" @click.self="showActionModal = false" @keydown.esc="showActionModal = false">
        <div class="action-modal modal-container">
          <div class="modal-header">
            <h3>
              <i class="pi pi-bolt" />
              {{ editingActionId ? "Edit Quick Action" : "Add Quick Action" }}
            </h3>
            <button class="close-btn" @click="showActionModal = false">
              <i class="pi pi-times" />
            </button>
          </div>

Current Code:

    <Teleport to="body">
      <div v-if="showActionModal" class="modal-overlay">
        <div class="action-modal modal-container">
          <div class="modal-header">
            <h3>
              <i class="pi pi-bolt" />
              {{ editingActionId ? "Edit Quick Action" : "Add Quick Action" }}
            </h3>
            <button class="close-btn" @click="showActionModal = false">
              <i class="pi pi-times" />
            </button>
          </div>

Verdict: APPROVE

Posted as a comment because posting a review failed.

@nfebe nfebe merged commit 3ea8342 into main Jan 3, 2026
5 checks passed
@nfebe nfebe deleted the feat/backup-and-scheduler branch January 3, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants