mirror of
https://github.com/unraid/api.git
synced 2026-01-04 07:29:48 -06:00
feat: setup initial backup stats
This commit is contained in:
8
.bivvy/abcd-climb.md
Normal file
8
.bivvy/abcd-climb.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
id: abcd
|
||||
type: feature
|
||||
description: This is an example Climb
|
||||
---
|
||||
## Example PRD
|
||||
|
||||
TODO
|
||||
21
.bivvy/abcd-moves.json
Normal file
21
.bivvy/abcd-moves.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"climb": "0000",
|
||||
"moves": [
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "install the dependencies",
|
||||
"details": "install the deps listed as New Dependencies"
|
||||
}, {
|
||||
"status": "skip",
|
||||
"description": "Write tests"
|
||||
}, {
|
||||
"status": "climbing",
|
||||
"description": "Build the first part of the feature",
|
||||
"rest": "true"
|
||||
}, {
|
||||
"status": "todo",
|
||||
"description": "Build the last part of the feature",
|
||||
"details": "After this, you'd ask the user if they want to return to write tests"
|
||||
}
|
||||
]
|
||||
}
|
||||
139
.bivvy/k8P2-climb.md
Normal file
139
.bivvy/k8P2-climb.md
Normal file
@@ -0,0 +1,139 @@
|
||||
**STARTFILE k8P2-climb.md**
|
||||
<Climb>
|
||||
<header>
|
||||
<id>k8P2</id>
|
||||
<type>bug</type>
|
||||
<description>Fix RClone backup jobs not appearing in jobs list and missing status data</description>
|
||||
</header>
|
||||
<newDependencies>None - this is a bug fix for existing functionality</newDependencies>
|
||||
<prerequisitChanges>None - working with existing backup service implementation</prerequisitChanges>
|
||||
<relevantFiles>
|
||||
- api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts (main RClone API service)
|
||||
- api/src/unraid-api/graph/resolvers/backup/backup-mutations.resolver.ts (backup mutations)
|
||||
- web/components/Backup/BackupOverview.vue (frontend backup overview)
|
||||
- web/components/Backup/backup-jobs.query.ts (GraphQL query for jobs)
|
||||
- api/src/unraid-api/graph/resolvers/backup/backup-queries.resolver.ts (backup queries resolver)
|
||||
</relevantFiles>
|
||||
<everythingElse>
|
||||
## Problem Statement
|
||||
|
||||
The newly implemented backup service has two critical issues:
|
||||
1. **Jobs not appearing in non-system jobs list**: When users trigger backup jobs via the "Run Now" button in BackupOverview.vue, these jobs are not showing up in the jobs list query, even when `showSystemJobs: false`
|
||||
2. **Missing job status data**: Jobs that are started don't return proper status information, making it impossible to track backup progress
|
||||
|
||||
## Background
|
||||
|
||||
This issue emerged immediately after implementing the new backup service. The backup functionality uses:
|
||||
- RClone RC daemon for job execution via Unix socket
|
||||
- GraphQL mutations for triggering backups (`triggerJob`, `initiateBackup`)
|
||||
- Job grouping system with groups like `backup/manual` and `backup/${id}`
|
||||
- Vue.js frontend with real-time job status monitoring
|
||||
|
||||
## Root Cause Analysis Areas
|
||||
|
||||
### 1. Job Group Classification
|
||||
The current implementation sets job groups as:
|
||||
- `backup/manual` for manual backups
|
||||
- `backup/${id}` for configured job backups
|
||||
|
||||
**Potential Issue**: The jobs query may be filtering these groups incorrectly, classifying user-initiated backups as "system jobs"
|
||||
|
||||
### 2. RClone API Response Handling
|
||||
**Potential Issue**: The `startBackup` method may not be properly handling or returning job metadata from RClone RC API responses
|
||||
|
||||
### 3. Job Status Synchronization
|
||||
**Potential Issue**: There may be a disconnect between job initiation and the jobs listing/status APIs
|
||||
|
||||
### 4. Logging Deficiency
|
||||
**Current Gap**: Insufficient logging around RClone API responses makes debugging difficult
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Enhanced Logging
|
||||
- Add comprehensive debug logging for all RClone API calls and responses
|
||||
- Log job initiation parameters and returned job metadata
|
||||
- Log job listing and filtering logic
|
||||
- Add structured logging for job group classification
|
||||
|
||||
### Job Classification Fix
|
||||
- Ensure user-initiated backup jobs are properly classified as non-system jobs
|
||||
- Review and fix job group filtering logic in the jobs query resolver
|
||||
- Validate that job groups `backup/manual` and `backup/${id}` are treated as non-system
|
||||
|
||||
### Status Data Flow
|
||||
- Verify job ID propagation from RClone startBackup response
|
||||
- Ensure job status API correctly retrieves and formats status data
|
||||
- Fix any data transformation issues between RClone API and GraphQL responses
|
||||
|
||||
### Data Model Consistency
|
||||
- Ensure BackupJob GraphQL type includes all necessary fields (note: current linter error shows missing 'type' field)
|
||||
- Verify job data structure consistency between API and frontend
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Primary Fixes
|
||||
1. **Jobs Visibility**: User-triggered backup jobs appear in the jobs list when `showSystemJobs: false`
|
||||
2. **Status Data**: Job status data (progress, speed, ETA, etc.) is properly retrieved and displayed
|
||||
3. **Job ID Tracking**: Job IDs are properly returned and can be used for status queries
|
||||
|
||||
### Secondary Improvements
|
||||
4. **Enhanced Logging**: Comprehensive logging for debugging RClone interactions
|
||||
5. **Type Safety**: Fix TypeScript/linting errors in BackupOverview.vue
|
||||
6. **System Jobs Investigation**: Document findings about excessive system jobs
|
||||
|
||||
## Testing Approach
|
||||
|
||||
### Manual Testing
|
||||
1. Trigger backup via "Run Now" button in BackupOverview.vue
|
||||
2. Verify job appears in running jobs list (with showSystemJobs: false)
|
||||
3. Confirm job status data displays correctly (progress, speed, etc.)
|
||||
4. Test both `triggerJob` (configured jobs) and `initiateBackup` (manual jobs) flows
|
||||
|
||||
### API Testing
|
||||
1. Verify RClone API responses contain expected job metadata
|
||||
2. Test job listing API with various group filters
|
||||
3. Validate job status API returns complete data
|
||||
|
||||
### Edge Cases
|
||||
1. Test behavior when RClone daemon is restarted
|
||||
2. Test concurrent backup jobs
|
||||
3. Test backup job cancellation/completion scenarios
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Debugging & Logging
|
||||
- Add comprehensive logging to RClone API service
|
||||
- Log all API responses and job metadata
|
||||
- Add logging to job filtering logic
|
||||
|
||||
### Phase 2: Job Classification Fix
|
||||
- Fix job group filtering in backup queries resolver
|
||||
- Ensure proper non-system job classification
|
||||
- Test job visibility in frontend
|
||||
|
||||
### Phase 3: Status Data Fix
|
||||
- Fix job status data retrieval and formatting
|
||||
- Ensure complete job metadata is available
|
||||
- Fix TypeScript/GraphQL type issues
|
||||
|
||||
### Phase 4: Validation & Testing
|
||||
- Comprehensive testing of backup job lifecycle
|
||||
- Validate all acceptance criteria
|
||||
- Document system jobs investigation findings
|
||||
|
||||
## Security Considerations
|
||||
- Ensure logging doesn't expose sensitive backup configuration data
|
||||
- Maintain proper authentication/authorization for backup operations
|
||||
- Validate that job status queries don't leak information between users
|
||||
|
||||
## Performance Considerations
|
||||
- Ensure logging doesn't significantly impact performance
|
||||
- Optimize job listing queries if necessary
|
||||
- Consider caching strategies for frequently accessed job data
|
||||
|
||||
## Known Constraints
|
||||
- Must work with existing RClone RC daemon setup
|
||||
- Cannot break existing backup functionality during fixes
|
||||
- Must maintain backward compatibility with existing backup configurations
|
||||
</Climb>
|
||||
**ENDFILE**
|
||||
53
.bivvy/k8P2-moves.json
Normal file
53
.bivvy/k8P2-moves.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"Climb": "k8P2",
|
||||
"moves": [
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "Investigate current backup jobs query resolver implementation",
|
||||
"details": "Find and examine the backup-queries.resolver.ts to understand how jobs are currently filtered and what determines system vs non-system jobs"
|
||||
},
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "Add enhanced logging to RClone API service",
|
||||
"details": "Add comprehensive debug logging to startBackup, listRunningJobs, and getJobStatus methods in rclone-api.service.ts to capture API responses and job metadata"
|
||||
},
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "Add logging to job filtering logic",
|
||||
"details": "Add logging to the backup jobs query resolver to understand how jobs are being classified and filtered",
|
||||
"rest": true
|
||||
},
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "Fix job group classification in backup queries resolver",
|
||||
"details": "Ensure that jobs with groups 'backup/manual' and 'backup/{id}' are properly classified as non-system jobs"
|
||||
},
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "Verify job ID propagation from RClone responses",
|
||||
"details": "Ensure that job IDs returned from RClone startBackup are properly captured and returned in GraphQL mutations"
|
||||
},
|
||||
{
|
||||
"status": "todo",
|
||||
"description": "Fix job status data retrieval and formatting",
|
||||
"details": "Ensure getJobStatus properly retrieves and formats all status data (progress, speed, ETA, etc.) for display in the frontend",
|
||||
"rest": true
|
||||
},
|
||||
{
|
||||
"status": "todo",
|
||||
"description": "Fix TypeScript errors in BackupOverview.vue",
|
||||
"details": "Add missing 'type' field to BackupJob GraphQL type and fix any other type inconsistencies"
|
||||
},
|
||||
{
|
||||
"status": "todo",
|
||||
"description": "Test job visibility and status data end-to-end",
|
||||
"details": "Manually test triggering backup jobs via 'Run Now' button and verify they appear in jobs list with proper status data",
|
||||
"rest": true
|
||||
},
|
||||
{
|
||||
"status": "todo",
|
||||
"description": "Document system jobs investigation findings",
|
||||
"details": "Investigate why there are many system jobs running and document findings for potential future work"
|
||||
}
|
||||
]
|
||||
}
|
||||
184
.bivvy/x7K9-climb.md
Normal file
184
.bivvy/x7K9-climb.md
Normal file
@@ -0,0 +1,184 @@
|
||||
**STARTFILE x7K9-climb.md**
|
||||
<Climb>
|
||||
<header>
|
||||
<id>x7K9</id>
|
||||
<type>feature</type>
|
||||
<description>Enhanced Backup Job Management System with disable/enable controls, manual triggering, and real-time progress monitoring</description>
|
||||
</header>
|
||||
<newDependencies>No new external dependencies expected - leveraging existing GraphQL subscriptions infrastructure</newDependencies>
|
||||
<prerequisiteChanges>None - building on existing backup system architecture</prerequisiteChanges>
|
||||
<relevantFiles>
|
||||
- web/components/Backup/BackupJobConfig.vue (main UI component)
|
||||
- web/components/Backup/backup-jobs.query.ts (GraphQL queries/mutations)
|
||||
- api/src/unraid-api/graph/resolvers/backup/backup.resolver.ts (GraphQL resolver)
|
||||
- api/src/unraid-api/graph/resolvers/backup/backup-config.service.ts (business logic)
|
||||
- api/src/unraid-api/graph/resolvers/backup/backup.model.ts (GraphQL schema types)
|
||||
</relevantFiles>
|
||||
|
||||
## Feature Overview
|
||||
Enhance the existing backup job management system to provide better control and monitoring capabilities for users managing their backup operations.
|
||||
|
||||
## Purpose Statement
|
||||
Users need granular control over their backup jobs with the ability to enable/disable individual jobs, manually trigger scheduled jobs on-demand, and monitor real-time progress of running backup operations.
|
||||
|
||||
## Problem Being Solved
|
||||
- Users cannot easily disable/enable individual backup jobs without deleting them
|
||||
- No way to manually trigger a scheduled backup job outside its schedule
|
||||
- No real-time visibility into backup job progress once initiated
|
||||
- Limited feedback on current backup operation status
|
||||
|
||||
## Success Metrics
|
||||
- Users can toggle backup jobs on/off without losing configuration
|
||||
- Users can manually trigger any configured backup job
|
||||
- Real-time progress updates for active backup operations
|
||||
- Improved user experience with immediate feedback
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### Job Control
|
||||
- Toggle individual backup jobs enabled/disabled state
|
||||
- Manual trigger functionality for any configured backup job
|
||||
- Preserve all job configuration when disabling
|
||||
- Visual indicators for job state (enabled/disabled/running)
|
||||
|
||||
### Progress Monitoring
|
||||
- Real-time subscription for backup job progress
|
||||
- Display progress percentage, speed, ETA, and transferred data
|
||||
- Show currently running jobs in the UI
|
||||
- Update job status in real-time without page refresh
|
||||
|
||||
### UI Enhancements
|
||||
- Add enable/disable toggle controls to job cards
|
||||
- Add "Run Now" button for manual triggering
|
||||
- Progress indicators and status updates
|
||||
- Better visual feedback for job states
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### GraphQL API
|
||||
- Add mutation for enabling/disabling backup job configs
|
||||
- Add mutation for manually triggering backup jobs by config ID
|
||||
- Add subscription for real-time backup job progress updates
|
||||
- Extend existing BackupJob type with progress fields
|
||||
|
||||
### Backend Services
|
||||
- Enhance BackupConfigService with enable/disable functionality
|
||||
- Add manual trigger capability that uses existing job configs
|
||||
- Implement subscription resolver for real-time updates
|
||||
- Ensure proper error handling and status reporting
|
||||
|
||||
### Frontend Implementation
|
||||
- Add toggle controls to BackupJobConfig.vue
|
||||
- Implement manual trigger buttons
|
||||
- Subscribe to progress updates and display in UI
|
||||
- Handle loading states and error conditions
|
||||
|
||||
## User Flow
|
||||
|
||||
### Disabling a Job
|
||||
1. User views backup job list
|
||||
2. User clicks toggle to disable a job
|
||||
3. Job status updates immediately
|
||||
4. Scheduled execution stops, configuration preserved
|
||||
|
||||
### Manual Triggering
|
||||
1. User clicks "Run Now" on any configured job
|
||||
2. System validates job configuration
|
||||
3. Backup initiates immediately
|
||||
4. User sees real-time progress updates
|
||||
|
||||
### Progress Monitoring
|
||||
1. User initiates backup (scheduled or manual)
|
||||
2. Progress subscription automatically activates
|
||||
3. Real-time updates show in UI
|
||||
4. Completion status updates when job finishes
|
||||
|
||||
## API Specifications
|
||||
|
||||
### New Mutations (Nested Pattern)
|
||||
Following the established pattern from ArrayMutations, create BackupMutations:
|
||||
```graphql
|
||||
type BackupMutations {
|
||||
toggleJobConfig(id: String!, enabled: Boolean!): BackupJobConfig
|
||||
triggerJob(configId: String!): BackupStatus
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Structure
|
||||
- Create `BackupMutationsResolver` class similar to `ArrayMutationsResolver`
|
||||
- Use `@ResolveField()` decorators instead of `@Mutation()`
|
||||
- Add appropriate `@UsePermissions()` decorators
|
||||
- Group all backup-related mutations under `BackupMutations` type
|
||||
|
||||
### New Subscription
|
||||
```graphql
|
||||
backupJobProgress(jobId: String): BackupJob
|
||||
```
|
||||
|
||||
### Enhanced Types
|
||||
- Extend BackupJob with progress percentage
|
||||
- Add jobConfigId reference to running jobs
|
||||
- Include more detailed status information
|
||||
|
||||
### Frontend GraphQL Usage
|
||||
```graphql
|
||||
mutation ToggleBackupJob($id: String!, $enabled: Boolean!) {
|
||||
backup {
|
||||
toggleJobConfig(id: $id, enabled: $enabled) {
|
||||
id
|
||||
enabled
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation TriggerBackupJob($configId: String!) {
|
||||
backup {
|
||||
triggerJob(configId: $configId) {
|
||||
status
|
||||
jobId
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Real-time Updates
|
||||
- Use existing GraphQL subscription infrastructure
|
||||
- Efficient polling of rclone API for progress data
|
||||
- Proper cleanup of subscriptions when jobs complete
|
||||
|
||||
### State Management
|
||||
- Update job configs atomically
|
||||
- Handle concurrent operations gracefully
|
||||
- Maintain consistency between scheduled and manual executions
|
||||
|
||||
### Error Handling
|
||||
- Validate job configs before manual triggering
|
||||
- Graceful degradation if progress updates fail
|
||||
- Clear error messages for failed operations
|
||||
|
||||
## Testing Approach
|
||||
|
||||
### Test Cases
|
||||
- Toggle job enabled/disabled state
|
||||
- Manual trigger of backup jobs
|
||||
- Real-time progress subscription functionality
|
||||
- Error handling for invalid operations
|
||||
- Concurrent job execution scenarios
|
||||
|
||||
### Acceptance Criteria
|
||||
- Jobs can be disabled/enabled without data loss
|
||||
- Manual triggers work for all valid job configurations
|
||||
- Progress updates are accurate and timely
|
||||
- UI responds appropriately to all state changes
|
||||
- No memory leaks from subscription management
|
||||
|
||||
## Future Considerations
|
||||
- Job scheduling modification (change cron without recreate)
|
||||
- Backup job templates and bulk operations
|
||||
- Advanced progress details (file-level progress)
|
||||
- Job history and logging improvements
|
||||
</Climb>
|
||||
**ENDFILE**
|
||||
63
.bivvy/x7K9-moves.json
Normal file
63
.bivvy/x7K9-moves.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"Climb": "x7K9",
|
||||
"moves": [
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Create BackupMutations GraphQL type and resolver structure",
|
||||
"details": "Add BackupMutations type to backup.model.ts, create backup-mutations.resolver.ts file, and move existing mutations (createBackupJobConfig, updateBackupJobConfig, deleteBackupJobConfig, initiateBackup) from BackupResolver to the new BackupMutationsResolver following the ArrayMutationsResolver pattern"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Implement toggleJobConfig mutation",
|
||||
"details": "Add toggleJobConfig resolver method with proper permissions and update BackupConfigService to handle enable/disable functionality"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Implement triggerJob mutation",
|
||||
"details": "Add triggerJob resolver method that manually triggers a backup job using existing config, with validation and error handling"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Add backupJobProgress subscription",
|
||||
"details": "Create GraphQL subscription resolver for real-time backup job progress updates using existing rclone API polling",
|
||||
"rest": true
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Enhance BackupJob type with progress fields",
|
||||
"details": "Add progress percentage, configId reference, and detailed status fields to BackupJob model"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Update frontend GraphQL queries and mutations",
|
||||
"details": "Add new mutations and subscription to backup-jobs.query.ts following the nested mutation pattern"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Add toggle controls to BackupJobConfig.vue",
|
||||
"details": "Add enable/disable toggle switches to each job card with proper state management and error handling"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Add manual trigger buttons to BackupJobConfig.vue",
|
||||
"details": "Add 'Run Now' buttons with loading states and trigger the new mutation",
|
||||
"rest": true
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Implement progress monitoring in the UI",
|
||||
"details": "Subscribe to backup job progress and display real-time updates in the job cards with progress bars and status"
|
||||
},
|
||||
{
|
||||
"status": "done",
|
||||
"description": "Add visual indicators for job states",
|
||||
"details": "Enhance job cards with better status indicators for enabled/disabled/running states and improve overall UX"
|
||||
},
|
||||
{
|
||||
"status": "todo",
|
||||
"description": "Test integration and error handling",
|
||||
"details": "Test all functionality including edge cases, error scenarios, and subscription cleanup",
|
||||
"rest": true
|
||||
}
|
||||
]
|
||||
}
|
||||
181
.cursor/rules/bivvy.mdc
Normal file
181
.cursor/rules/bivvy.mdc
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
description: Creating, working on, or closing a pitch(feature, bug, task, or exploration).
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
## Description
|
||||
When a user requests to start a new Climb(feature, bug, task, or exploration), you will start a 2-phase process: Step 1 is to define a Product Requirements Document (PRD) via conversation with the user; Step 2 is to build a task list (a.k.a. "Moves") from that PRD.
|
||||
|
||||
Once alerted, you will then begin working on the Climb, move-by-move, soliciting user feedback and approval. Moves can have "rest: true" on it, at which point you WILL ALWAYS stop after completion to check with the user on status. They can also have the status "skip" on them, in which case you will skip it and ASK the user at the end if they want to return to it. Once you reach the end of the Moves list and they've opted out of skipping, you will mark the Climb as complete in both the PRD and the Moves list, and move both files to the .bivvy/complete/ directory.
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. YOU MUST STOP AND GET USER APPROVAL after:
|
||||
- Creating the initial PRD draft
|
||||
- Making any significant changes to the PRD
|
||||
- Completing any move marked with "rest: true"
|
||||
2. NEVER work on tasks marked as "skip" unless explicitly requested by the user
|
||||
3. NEVER work ahead of the current move in the task list
|
||||
4. ALWAYS follow the moves in order, one at a time
|
||||
|
||||
## File locations
|
||||
- Active PRDs are found in .bivvy/[id]-climb.md (PRD) and .bivvy/[id]-moves.json (i.e. the task list).
|
||||
- Completed PRDs are moved to .bivvy/complete/[id]-climb.md and .bivvy/complete/[id]-moves.json
|
||||
- The [id] is a 4-character string where each character can be [A-z0-9], e.g. "02b7" or "xK4p" (enforce randomness here, also check that the id doesn't already exist in the .bivvy/complete/ directory)
|
||||
|
||||
## Climb
|
||||
IMPORTANT: When collecting information, we need to know if this is a feature, bug, task, or exploration. The top of the PRD should always be:
|
||||
**STARTFILE [id]-climb.md**
|
||||
<Climb>
|
||||
<header>
|
||||
<id>[id]</id>
|
||||
<type>[feature|bug|task|exploration]</type>
|
||||
<description>(description)</description>
|
||||
<newDependencies>(Make sure to ask the user if there will be any new dependencies)</newDependencies>
|
||||
<prerequisitChanges>(You should think through this carefully)</prerequisitChanges>
|
||||
<relevantFiles>(Please do an initial grep based on the information you gather / ask the user for relevant files that might not be obvious)</relevantFiles>
|
||||
<everythingElse>(See below, there is a lot that could go here)</everythingElse>
|
||||
</Climb>
|
||||
**ENDFILE**
|
||||
|
||||
Note: no tasks / moves...just everything needed to carry them out
|
||||
|
||||
The PRD will differ with every Climb, but here are some guidelines:
|
||||
Key Components to Include
|
||||
Feature Overview
|
||||
|
||||
Feature Name and ID: Clear, unique identifier for the feature
|
||||
Purpose Statement: Concise explanation of what the feature is and why it's valuable
|
||||
Problem Being Solved: Specific user pain points or business needs addressed
|
||||
Success Metrics: Measurable outcomes that indicate feature success
|
||||
|
||||
Requirements
|
||||
|
||||
Functional Requirements: Specific capabilities the feature must provide
|
||||
Technical Requirements: Performance, security, and reliability expectations
|
||||
User Requirements: How the feature should work from the user's perspective
|
||||
Constraints: Technical limitations, business rules, or regulatory considerations
|
||||
|
||||
Design and Implementation
|
||||
|
||||
User Flow: Step-by-step journey through the feature
|
||||
Architecture Overview: How this feature integrates with existing systems
|
||||
Dependent Components: Other systems or features this feature relies on
|
||||
API Specifications: Required endpoints, payloads, and responses
|
||||
Data Models: Key data structures and relationships
|
||||
|
||||
Development Details
|
||||
|
||||
Relevant Files: Specific files or components that will be affected
|
||||
Implementation Considerations: Technical approach and potential challenges
|
||||
Dependencies: External services, libraries, or APIs required
|
||||
Security Considerations: Authentication, authorization, data protection needs
|
||||
|
||||
Testing Approach
|
||||
|
||||
Test Cases: Critical scenarios to validate
|
||||
Acceptance Criteria: Conditions that must be met for feature approval
|
||||
Edge Cases: Unusual or boundary conditions that need special handling
|
||||
Performance Requirements: Specific benchmarks for speed and reliability
|
||||
|
||||
Design Assets
|
||||
|
||||
Mockups/Wireframes: Visual representations of the UI (references or links)
|
||||
User Interface Guidelines: Styling, interaction patterns, and accessibility requirements
|
||||
Content Guidelines: Copy samples, terminology standards, messaging approach
|
||||
|
||||
Future Considerations
|
||||
|
||||
Scalability Plans: How the feature should evolve as usage grows
|
||||
Enhancement Ideas: Potential future improvements outside current scope
|
||||
Known Limitations: Acknowledged constraints in the current implementation
|
||||
|
||||
Formatting Best Practices
|
||||
|
||||
Be Specific: Avoid vague language; use precise descriptions
|
||||
Use Clear Structure: Organize with consistent headers and formatting
|
||||
Include Examples: Provide concrete examples when explaining complex functionality
|
||||
Prioritize Requirements: Indicate which requirements are essential vs. nice-to-have
|
||||
Link to Resources: Reference existing documentation, designs, or research
|
||||
Keep It Concise: Focus on what's necessary; avoid unnecessary detail
|
||||
Use Visual Aids: Include diagrams, flowcharts, or mockups when helpful
|
||||
Define Technical Terms: Include a glossary if specialized terminology is used
|
||||
|
||||
What to Avoid
|
||||
|
||||
Prescribing Implementation Details: Focus on what, not how (unless necessary)
|
||||
Including Task Lists: Leave specific tasks for project management tools
|
||||
Rigid Timelines: PRDs describe requirements, not project schedules
|
||||
Vague Goals: Ensure all success metrics are measurable
|
||||
Overspecification: Allow room for engineering creativity in solutions
|
||||
Ignoring Constraints: Acknowledge technical and business limitations
|
||||
Excessive Jargon: Write for clarity across different team roles
|
||||
|
||||
By following this framework, you'll create feature PRDs that provide clear direction while maintaining flexibility for implementation approaches, ultimately leading to better features and more efficient development.
|
||||
|
||||
## Moves
|
||||
Once the Climb is generated and approved by the user, generate the Moves list.
|
||||
You should carefully consider the ORDER in which these tasks need to be completed.
|
||||
The size of every move should be something an AI agent can carry out in 2-3 code changes.
|
||||
Make sure to add reasonable {rest: true} along the way.
|
||||
Moves have the statuses: todo|climbing|skip|complete
|
||||
|
||||
Here is a sample moves file:
|
||||
**STARTFILE [id]-moves.json**
|
||||
{
|
||||
"Climb": "abcd",
|
||||
"moves": [
|
||||
{
|
||||
"status": "complete",
|
||||
"description": "install the dependencies",
|
||||
"details": "install the deps listed as New Dependencies"
|
||||
}, {
|
||||
"status": "skip",
|
||||
"description": "Write tests"
|
||||
}, {
|
||||
"status": "climbing",
|
||||
"description": "Build the first part of the feature",
|
||||
"rest": "true"
|
||||
}, {
|
||||
"status": "todo",
|
||||
"description": "Build the last part of the feature",
|
||||
"details": "After this, you'd ask the user if they want to return to write tests"
|
||||
}
|
||||
]
|
||||
}
|
||||
**ENDFILE**
|
||||
|
||||
## Running
|
||||
Creating the PRD:
|
||||
- THIS NEEDS TO BE ITERATIVE!
|
||||
- If you need to, ask the user for clarifying questions before starting
|
||||
- CRITICAL: YOU MUST STOP after your first draft of the PRD and wait for user approval
|
||||
- CRITICAL: YOU MUST STOP after any significant changes to the PRD
|
||||
- The PRD must be approved before moving on to creating the moves list
|
||||
|
||||
Creating and Managing Moves:
|
||||
- After PRD approval, create the moves list
|
||||
- CRITICAL: YOU MUST STOP after creating the initial moves list for user approval
|
||||
- Moves marked as "skip" MUST NOT be worked on unless explicitly requested by the user
|
||||
- NEVER work ahead or complete tasks out of order
|
||||
- Each move should be completed and approved before moving to the next one
|
||||
- If a move depends on a skipped move, YOU MUST ASK the user if they want to return to the skipped move first
|
||||
|
||||
Updating [id]-moves.json:
|
||||
- After EVERY code approval, you should update the moves.json file
|
||||
- Move through the moves array until you hit a todo item, then move on to it
|
||||
- NEVER work on moves marked as "skip"
|
||||
- NEVER work ahead of the current move
|
||||
- It is okay to check with the user if they want to move forward, but trust the process
|
||||
- Make sure to update the statuses within the moves.json file
|
||||
|
||||
Keeping track of the Climb
|
||||
- IMPORTANT: EVERY TIME YOU USE THIS RULE, THE LAST LINE OF YOUR OUPUT SHOULD BE: "/|\ Bivvy Climb [id]"
|
||||
- CRITICAL: Unless they are closing the Climb (see below) then do NOT keep track of the Climb.
|
||||
|
||||
## Closing (or canceling) a Climb
|
||||
- If the user asks to close a Climb, ask them if they want to "delete" it or "complete" it
|
||||
- They can also do either without asking to "close" first
|
||||
- If they delete a Climb, delete both the Climb and moves file by id
|
||||
- If they close a Climb, move it to complete
|
||||
- CRITICAL: STOP ADDING THE Climb-TRACKING TEXT TO RESPONSES
|
||||
- CRITICAL: STOP USING THIS RULE UNTIL A NEW Climb IS STARTED
|
||||
@@ -15,6 +15,7 @@ PATHS_ACTIVATION_BASE=./dev/activation
|
||||
PATHS_PASSWD=./dev/passwd
|
||||
PATHS_RCLONE_SOCKET=./dev/rclone-socket
|
||||
PATHS_LOG_BASE=./dev/log # Where we store logs
|
||||
PATHS_BACKUP_JOBS=./dev/api/backup
|
||||
ENVIRONMENT="development"
|
||||
NODE_ENV="development"
|
||||
PORT="3001"
|
||||
@@ -26,4 +27,4 @@ BYPASS_PERMISSION_CHECKS=false
|
||||
BYPASS_CORS_CHECKS=true
|
||||
CHOKIDAR_USEPOLLING=true
|
||||
LOG_TRANSPORT=console
|
||||
LOG_LEVEL=trace
|
||||
LOG_LEVEL=debug # Change to trace for extremely noisy logging
|
||||
|
||||
14
api/dev/api/backup/backup-jobs.json
Normal file
14
api/dev/api/backup/backup-jobs.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "1ad3f9a9-f438-43b2-bdd5-976c7ca4b2f5",
|
||||
"name": "test",
|
||||
"sourcePath": "/Users/elibosley/Downloads",
|
||||
"remoteName": "FlashBackup",
|
||||
"destinationPath": "backup",
|
||||
"schedule": "0 2 * * *",
|
||||
"enabled": true,
|
||||
"rcloneOptions": {},
|
||||
"createdAt": "2025-05-24T12:19:29.150Z",
|
||||
"updatedAt": "2025-05-24T12:19:29.150Z"
|
||||
}
|
||||
]
|
||||
@@ -758,6 +758,7 @@ enum Resource {
|
||||
ACTIVATION_CODE
|
||||
API_KEY
|
||||
ARRAY
|
||||
BACKUP
|
||||
CLOUD
|
||||
CONFIG
|
||||
CONNECT
|
||||
@@ -883,6 +884,63 @@ type VmMutations {
|
||||
reset(id: PrefixedID!): Boolean!
|
||||
}
|
||||
|
||||
"""Backup related mutations"""
|
||||
type BackupMutations {
|
||||
"""Create a new backup job configuration"""
|
||||
createBackupJobConfig(input: CreateBackupJobConfigInput!): BackupJobConfig!
|
||||
|
||||
"""Update a backup job configuration"""
|
||||
updateBackupJobConfig(id: String!, input: UpdateBackupJobConfigInput!): BackupJobConfig
|
||||
|
||||
"""Delete a backup job configuration"""
|
||||
deleteBackupJobConfig(id: String!): Boolean!
|
||||
|
||||
"""Initiates a backup using a configured remote."""
|
||||
initiateBackup(input: InitiateBackupInput!): BackupStatus!
|
||||
|
||||
"""Toggle a backup job configuration enabled/disabled"""
|
||||
toggleJobConfig(id: String!): BackupJobConfig
|
||||
|
||||
"""Manually trigger a backup job using existing configuration"""
|
||||
triggerJob(id: PrefixedID!): BackupStatus!
|
||||
}
|
||||
|
||||
input CreateBackupJobConfigInput {
|
||||
name: String!
|
||||
sourcePath: String!
|
||||
remoteName: String!
|
||||
destinationPath: String!
|
||||
schedule: String!
|
||||
enabled: Boolean! = true
|
||||
rcloneOptions: JSON
|
||||
}
|
||||
|
||||
input UpdateBackupJobConfigInput {
|
||||
name: String
|
||||
sourcePath: String
|
||||
remoteName: String
|
||||
destinationPath: String
|
||||
schedule: String
|
||||
enabled: Boolean
|
||||
rcloneOptions: JSON
|
||||
}
|
||||
|
||||
input InitiateBackupInput {
|
||||
"""The name of the remote configuration to use for the backup."""
|
||||
remoteName: String!
|
||||
|
||||
"""Source path to backup."""
|
||||
sourcePath: String!
|
||||
|
||||
"""Destination path on the remote."""
|
||||
destinationPath: String!
|
||||
|
||||
"""
|
||||
Additional options for the backup operation, such as --dry-run or --transfers.
|
||||
"""
|
||||
options: JSON
|
||||
}
|
||||
|
||||
"""API Key related mutations"""
|
||||
type ApiKeyMutations {
|
||||
"""Create an API key"""
|
||||
@@ -1036,7 +1094,7 @@ type RCloneRemote {
|
||||
|
||||
type Backup implements Node {
|
||||
id: PrefixedID!
|
||||
jobs: [BackupJob!]!
|
||||
jobs(showSystemJobs: Boolean = false): [BackupJob!]!
|
||||
configs: [BackupJobConfig!]!
|
||||
|
||||
"""Get the status for the backup service"""
|
||||
@@ -1053,10 +1111,10 @@ type BackupStatus {
|
||||
|
||||
type BackupJob {
|
||||
"""Job ID"""
|
||||
id: String!
|
||||
id: PrefixedID!
|
||||
|
||||
"""Job type (e.g., sync/copy)"""
|
||||
type: String!
|
||||
"""RClone group for the job"""
|
||||
group: String
|
||||
|
||||
"""Job status and statistics"""
|
||||
stats: JSON!
|
||||
@@ -1072,9 +1130,18 @@ type BackupJob {
|
||||
|
||||
"""Formatted ETA"""
|
||||
formattedEta: String
|
||||
|
||||
"""Progress percentage (0-100)"""
|
||||
progressPercentage: Float
|
||||
|
||||
"""Configuration ID that triggered this job"""
|
||||
configId: PrefixedID
|
||||
|
||||
"""Detailed status of the job"""
|
||||
detailedStatus: String
|
||||
}
|
||||
|
||||
type BackupJobConfig {
|
||||
type BackupJobConfig implements Node {
|
||||
id: PrefixedID!
|
||||
|
||||
"""Human-readable name for this backup job"""
|
||||
@@ -1112,7 +1179,7 @@ type BackupJobConfig {
|
||||
}
|
||||
|
||||
type BackupJobConfigForm {
|
||||
id: ID!
|
||||
id: PrefixedID!
|
||||
dataSchema: JSON!
|
||||
uiSchema: JSON!
|
||||
}
|
||||
@@ -1674,7 +1741,7 @@ type Query {
|
||||
backupJobConfig(id: String!): BackupJobConfig
|
||||
|
||||
"""Get status of a specific backup job"""
|
||||
backupJob(jobId: String!): BackupJob
|
||||
backupJob(jobId: PrefixedID!): BackupJob
|
||||
|
||||
"""Get the JSON schema for backup job configuration form"""
|
||||
backupJobConfigForm(input: BackupJobConfigFormInput): BackupJobConfigForm!
|
||||
@@ -1719,21 +1786,10 @@ type Mutation {
|
||||
array: ArrayMutations!
|
||||
docker: DockerMutations!
|
||||
vm: VmMutations!
|
||||
backup: BackupMutations!
|
||||
parityCheck: ParityCheckMutations!
|
||||
apiKey: ApiKeyMutations!
|
||||
rclone: RCloneMutations!
|
||||
|
||||
"""Create a new backup job configuration"""
|
||||
createBackupJobConfig(input: CreateBackupJobConfigInput!): BackupJobConfig!
|
||||
|
||||
"""Update a backup job configuration"""
|
||||
updateBackupJobConfig(id: String!, input: UpdateBackupJobConfigInput!): BackupJobConfig
|
||||
|
||||
"""Delete a backup job configuration"""
|
||||
deleteBackupJobConfig(id: String!): Boolean!
|
||||
|
||||
"""Initiates a backup using a configured remote."""
|
||||
initiateBackup(input: InitiateBackupInput!): BackupStatus!
|
||||
updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues!
|
||||
connectSignIn(input: ConnectSignInInput!): Boolean!
|
||||
connectSignOut: Boolean!
|
||||
@@ -1751,42 +1807,6 @@ input NotificationData {
|
||||
link: String
|
||||
}
|
||||
|
||||
input CreateBackupJobConfigInput {
|
||||
name: String!
|
||||
sourcePath: String!
|
||||
remoteName: String!
|
||||
destinationPath: String!
|
||||
schedule: String!
|
||||
enabled: Boolean! = true
|
||||
rcloneOptions: JSON
|
||||
}
|
||||
|
||||
input UpdateBackupJobConfigInput {
|
||||
name: String
|
||||
sourcePath: String
|
||||
remoteName: String
|
||||
destinationPath: String
|
||||
schedule: String
|
||||
enabled: Boolean
|
||||
rcloneOptions: JSON
|
||||
}
|
||||
|
||||
input InitiateBackupInput {
|
||||
"""The name of the remote configuration to use for the backup."""
|
||||
remoteName: String!
|
||||
|
||||
"""Source path to backup."""
|
||||
sourcePath: String!
|
||||
|
||||
"""Destination path on the remote."""
|
||||
destinationPath: String!
|
||||
|
||||
"""
|
||||
Additional options for the backup operation, such as --dry-run or --transfers.
|
||||
"""
|
||||
options: JSON
|
||||
}
|
||||
|
||||
input ApiSettingsInput {
|
||||
"""
|
||||
If true, the GraphQL sandbox will be enabled and available at /graphql. If false, the GraphQL sandbox will be disabled and only the production API will be available.
|
||||
@@ -1883,6 +1903,9 @@ type Subscription {
|
||||
serversSubscription: Server!
|
||||
parityHistorySubscription: ParityCheck!
|
||||
arraySubscription: UnraidArray!
|
||||
|
||||
"""Subscribe to real-time backup job progress updates"""
|
||||
backupJobProgress(jobId: PrefixedID!): BackupJob
|
||||
}
|
||||
|
||||
"""Available authentication action verbs"""
|
||||
|
||||
@@ -8,6 +8,7 @@ eventEmitter.setMaxListeners(30);
|
||||
|
||||
export enum PUBSUB_CHANNEL {
|
||||
ARRAY = 'ARRAY',
|
||||
BACKUP_JOB_PROGRESS = 'BACKUP_JOB_PROGRESS',
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
DISPLAY = 'DISPLAY',
|
||||
INFO = 'INFO',
|
||||
|
||||
@@ -71,6 +71,7 @@ const initialState = {
|
||||
),
|
||||
webGuiBase: '/usr/local/emhttp/webGui' as const,
|
||||
identConfig: resolvePath(process.env.PATHS_IDENT_CONFIG ?? ('/boot/config/ident.cfg' as const)),
|
||||
backupBase: resolvePath(process.env.PATHS_BACKUP_JOBS ?? ('/boot/config/api/backup/' as const)),
|
||||
};
|
||||
|
||||
// Derive asset paths from base paths
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { existsSync } from 'fs';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { CronJob } from 'cron';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getters } from '@app/store/index.js';
|
||||
import {
|
||||
BackupJobConfig,
|
||||
CreateBackupJobConfigInput,
|
||||
@@ -31,13 +33,15 @@ interface BackupJobConfigData {
|
||||
@Injectable()
|
||||
export class BackupConfigService {
|
||||
private readonly logger = new Logger(BackupConfigService.name);
|
||||
private readonly configPath = '/boot/config/backup-jobs.json';
|
||||
private readonly configPath: string;
|
||||
private configs: Map<string, BackupJobConfigData> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly rcloneService: RCloneService,
|
||||
private readonly schedulerRegistry: SchedulerRegistry
|
||||
) {
|
||||
const paths = getters.paths();
|
||||
this.configPath = join(paths.backupBase, 'backup-jobs.json');
|
||||
this.loadConfigs();
|
||||
}
|
||||
|
||||
@@ -112,15 +116,19 @@ export class BackupConfigService {
|
||||
const result = await this.rcloneService['rcloneApiService'].startBackup({
|
||||
srcPath: config.sourcePath,
|
||||
dstPath: `${config.remoteName}:${config.destinationPath}`,
|
||||
options: config.rcloneOptions,
|
||||
async: true,
|
||||
group: `backup/${config.id}`,
|
||||
options: config.rcloneOptions || {},
|
||||
});
|
||||
|
||||
const jobId = result.jobId || result.jobid;
|
||||
|
||||
config.lastRunAt = new Date().toISOString();
|
||||
config.lastRunStatus = `Started with job ID: ${result.jobId}`;
|
||||
config.lastRunStatus = `Started with job ID: ${jobId}`;
|
||||
this.configs.set(config.id, config);
|
||||
await this.saveConfigs();
|
||||
|
||||
this.logger.log(`Backup job ${config.name} started successfully: ${result.jobId}`);
|
||||
this.logger.log(`Backup job ${config.name} started successfully: ${jobId}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
config.lastRunAt = new Date().toISOString();
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
AuthActionVerb,
|
||||
AuthPossession,
|
||||
UsePermissions,
|
||||
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
|
||||
import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js';
|
||||
import {
|
||||
BackupJobConfig,
|
||||
BackupStatus,
|
||||
CreateBackupJobConfigInput,
|
||||
InitiateBackupInput,
|
||||
UpdateBackupJobConfigInput,
|
||||
} from '@app/unraid-api/graph/resolvers/backup/backup.model.js';
|
||||
import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { BackupMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
|
||||
import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js';
|
||||
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
|
||||
|
||||
@Resolver(() => BackupMutations)
|
||||
export class BackupMutationsResolver {
|
||||
private readonly logger = new Logger(BackupMutationsResolver.name);
|
||||
|
||||
constructor(
|
||||
private readonly backupConfigService: BackupConfigService,
|
||||
private readonly rcloneService: RCloneService
|
||||
) {}
|
||||
|
||||
private async executeBackup(
|
||||
sourcePath: string,
|
||||
remoteName: string,
|
||||
destinationPath: string,
|
||||
options: Record<string, any> = {},
|
||||
group: string
|
||||
): Promise<BackupStatus> {
|
||||
try {
|
||||
this.logger.log(
|
||||
`Executing backup: ${sourcePath} -> ${remoteName}:${destinationPath} (group: ${group})`
|
||||
);
|
||||
|
||||
const result = await this.rcloneService['rcloneApiService'].startBackup({
|
||||
srcPath: sourcePath,
|
||||
dstPath: `${remoteName}:${destinationPath}`,
|
||||
async: true,
|
||||
group: group,
|
||||
options: options,
|
||||
});
|
||||
|
||||
this.logger.debug(`RClone startBackup result: ${JSON.stringify(result)}`);
|
||||
|
||||
const jobId = result.jobid || result.jobId;
|
||||
this.logger.log(`Backup job initiated successfully with ID: ${jobId}`);
|
||||
|
||||
return {
|
||||
status: 'Backup initiated successfully',
|
||||
jobId: jobId,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(
|
||||
`Failed to execute backup: ${errorMessage}`,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
|
||||
return {
|
||||
status: `Failed to initiate backup: ${errorMessage}`,
|
||||
jobId: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupJobConfig, {
|
||||
description: 'Create a new backup job configuration',
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.CREATE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async createBackupJobConfig(
|
||||
@Args('input') input: CreateBackupJobConfigInput
|
||||
): Promise<BackupJobConfig> {
|
||||
return this.backupConfigService.createBackupJobConfig(input);
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupJobConfig, {
|
||||
description: 'Update a backup job configuration',
|
||||
nullable: true,
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async updateBackupJobConfig(
|
||||
@Args('id') id: string,
|
||||
@Args('input') input: UpdateBackupJobConfigInput
|
||||
): Promise<BackupJobConfig | null> {
|
||||
return this.backupConfigService.updateBackupJobConfig(id, input);
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean, {
|
||||
description: 'Delete a backup job configuration',
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.DELETE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async deleteBackupJobConfig(@Args('id') id: string): Promise<boolean> {
|
||||
return this.backupConfigService.deleteBackupJobConfig(id);
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupStatus, {
|
||||
description: 'Initiates a backup using a configured remote.',
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.CREATE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async initiateBackup(@Args('input') input: InitiateBackupInput): Promise<BackupStatus> {
|
||||
return this.executeBackup(
|
||||
input.sourcePath,
|
||||
input.remoteName,
|
||||
input.destinationPath,
|
||||
input.options || {},
|
||||
'backup/manual'
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupJobConfig, {
|
||||
description: 'Toggle a backup job configuration enabled/disabled',
|
||||
nullable: true,
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async toggleJobConfig(@Args('id') id: string): Promise<BackupJobConfig | null> {
|
||||
const existing = await this.backupConfigService.getBackupJobConfig(id);
|
||||
if (!existing) return null;
|
||||
|
||||
return this.backupConfigService.updateBackupJobConfig(id, {
|
||||
enabled: !existing.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupStatus, {
|
||||
description: 'Manually trigger a backup job using existing configuration',
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.CREATE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async triggerJob(@Args('id', { type: () => PrefixedID }) id: string): Promise<BackupStatus> {
|
||||
const config = await this.backupConfigService.getBackupJobConfig(id);
|
||||
if (!config) {
|
||||
return {
|
||||
status: 'Failed to trigger backup: Configuration not found',
|
||||
jobId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return this.executeBackup(
|
||||
config.sourcePath,
|
||||
config.remoteName,
|
||||
config.destinationPath,
|
||||
config.rcloneOptions || {},
|
||||
`backup/${id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Field, ID, InputType, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, InputType, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { type Layout } from '@jsonforms/core';
|
||||
import { IsBoolean, IsNotEmpty, IsObject, IsOptional, IsString, Matches } from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-scalars';
|
||||
|
||||
import { Node } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { RCloneJob } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
|
||||
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
|
||||
|
||||
@ObjectType({
|
||||
implements: () => Node,
|
||||
})
|
||||
export class Backup extends Node {
|
||||
@Field(() => [BackupJob])
|
||||
jobs!: BackupJob[];
|
||||
@Field(() => [RCloneJob])
|
||||
jobs!: RCloneJob[];
|
||||
|
||||
@Field(() => [BackupJobConfig])
|
||||
configs!: BackupJobConfig[];
|
||||
@@ -58,37 +60,15 @@ export class BackupStatus {
|
||||
jobId?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class BackupJob {
|
||||
@Field(() => String, { description: 'Job ID' })
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, { description: 'Job type (e.g., sync/copy)' })
|
||||
type!: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { description: 'Job status and statistics' })
|
||||
stats!: Record<string, unknown>;
|
||||
|
||||
@Field(() => String, { description: 'Formatted bytes transferred', nullable: true })
|
||||
formattedBytes?: string;
|
||||
|
||||
@Field(() => String, { description: 'Formatted transfer speed', nullable: true })
|
||||
formattedSpeed?: string;
|
||||
|
||||
@Field(() => String, { description: 'Formatted elapsed time', nullable: true })
|
||||
formattedElapsedTime?: string;
|
||||
|
||||
@Field(() => String, { description: 'Formatted ETA', nullable: true })
|
||||
formattedEta?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class RCloneWebGuiInfo {
|
||||
@Field()
|
||||
url!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType({
|
||||
implements: () => Node,
|
||||
})
|
||||
export class BackupJobConfig extends Node {
|
||||
@Field(() => String, { description: 'Human-readable name for this backup job' })
|
||||
name!: string;
|
||||
@@ -223,7 +203,7 @@ export class UpdateBackupJobConfigInput {
|
||||
|
||||
@ObjectType()
|
||||
export class BackupJobConfigForm {
|
||||
@Field(() => ID)
|
||||
@Field(() => PrefixedID)
|
||||
id!: string;
|
||||
|
||||
@Field(() => GraphQLJSON)
|
||||
|
||||
@@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js';
|
||||
import { BackupMutationsResolver } from '@app/unraid-api/graph/resolvers/backup/backup-mutations.resolver.js';
|
||||
import { BackupResolver } from '@app/unraid-api/graph/resolvers/backup/backup.resolver.js';
|
||||
import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js';
|
||||
import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [RCloneModule, ScheduleModule.forRoot()],
|
||||
providers: [BackupResolver, BackupConfigService, FormatService],
|
||||
providers: [BackupResolver, BackupMutationsResolver, BackupConfigService, FormatService],
|
||||
exports: [],
|
||||
})
|
||||
export class BackupModule {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { Args, Mutation, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { pubsub } from '@app/core/pubsub.js';
|
||||
import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js';
|
||||
import {
|
||||
Backup,
|
||||
@@ -9,13 +10,12 @@ import {
|
||||
BackupJobConfigForm,
|
||||
BackupJobConfigFormInput,
|
||||
BackupStatus,
|
||||
CreateBackupJobConfigInput,
|
||||
InitiateBackupInput,
|
||||
UpdateBackupJobConfigInput,
|
||||
} from '@app/unraid-api/graph/resolvers/backup/backup.model.js';
|
||||
import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js';
|
||||
import { buildBackupJobConfigSchema } from '@app/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.js';
|
||||
import { RCloneJob } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js';
|
||||
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
|
||||
|
||||
@Resolver(() => Backup)
|
||||
export class BackupResolver {
|
||||
@@ -41,8 +41,11 @@ export class BackupResolver {
|
||||
@ResolveField(() => [BackupJob], {
|
||||
description: 'Get all running backup jobs',
|
||||
})
|
||||
async jobs(): Promise<BackupJob[]> {
|
||||
return this.backupJobs();
|
||||
async jobs(
|
||||
@Args('showSystemJobs', { type: () => Boolean, nullable: true, defaultValue: false })
|
||||
showSystemJobs?: boolean
|
||||
): Promise<BackupJob[]> {
|
||||
return this.backupJobs(showSystemJobs);
|
||||
}
|
||||
|
||||
@ResolveField(() => [BackupJobConfig], {
|
||||
@@ -60,81 +63,19 @@ export class BackupResolver {
|
||||
return this.backupConfigService.getBackupJobConfig(id);
|
||||
}
|
||||
|
||||
@Mutation(() => BackupJobConfig, {
|
||||
description: 'Create a new backup job configuration',
|
||||
})
|
||||
async createBackupJobConfig(
|
||||
@Args('input') input: CreateBackupJobConfigInput
|
||||
): Promise<BackupJobConfig> {
|
||||
return this.backupConfigService.createBackupJobConfig(input);
|
||||
}
|
||||
|
||||
@Mutation(() => BackupJobConfig, {
|
||||
description: 'Update a backup job configuration',
|
||||
nullable: true,
|
||||
})
|
||||
async updateBackupJobConfig(
|
||||
@Args('id') id: string,
|
||||
@Args('input') input: UpdateBackupJobConfigInput
|
||||
): Promise<BackupJobConfig | null> {
|
||||
return this.backupConfigService.updateBackupJobConfig(id, input);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete a backup job configuration',
|
||||
})
|
||||
async deleteBackupJobConfig(@Args('id') id: string): Promise<boolean> {
|
||||
return this.backupConfigService.deleteBackupJobConfig(id);
|
||||
}
|
||||
|
||||
private async backupJobs(): Promise<BackupJob[]> {
|
||||
try {
|
||||
const jobs = await this.rcloneService['rcloneApiService'].listRunningJobs();
|
||||
return (
|
||||
jobs.jobids?.map((jobId: string, index: number) => {
|
||||
const stats = jobs.stats?.[index] || {};
|
||||
return {
|
||||
id: jobId,
|
||||
type: 'backup',
|
||||
stats,
|
||||
formattedBytes: stats.bytes
|
||||
? this.formatService.formatBytes(stats.bytes)
|
||||
: undefined,
|
||||
formattedSpeed: stats.speed
|
||||
? this.formatService.formatBytes(stats.speed)
|
||||
: undefined,
|
||||
formattedElapsedTime: stats.elapsedTime
|
||||
? this.formatService.formatDuration(stats.elapsedTime)
|
||||
: undefined,
|
||||
formattedEta: stats.eta
|
||||
? this.formatService.formatDuration(stats.eta)
|
||||
: undefined,
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch backup jobs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => BackupJob, {
|
||||
description: 'Get status of a specific backup job',
|
||||
nullable: true,
|
||||
})
|
||||
async backupJob(@Args('jobId') jobId: string): Promise<BackupJob | null> {
|
||||
async backupJob(
|
||||
@Args('jobId', { type: () => PrefixedID }) jobId: string
|
||||
): Promise<BackupJob | null> {
|
||||
try {
|
||||
const status = await this.rcloneService['rcloneApiService'].getJobStatus({ jobId });
|
||||
return {
|
||||
id: jobId,
|
||||
type: status.group || 'backup',
|
||||
group: status.group || '',
|
||||
stats: status,
|
||||
formattedBytes: status.bytes ? this.formatService.formatBytes(status.bytes) : undefined,
|
||||
formattedSpeed: status.speed ? this.formatService.formatBytes(status.speed) : undefined,
|
||||
formattedElapsedTime: status.elapsedTime
|
||||
? this.formatService.formatDuration(status.elapsedTime)
|
||||
: undefined,
|
||||
formattedEta: status.eta ? this.formatService.formatDuration(status.eta) : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch backup job ${jobId}:`, error);
|
||||
@@ -142,32 +83,6 @@ export class BackupResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => BackupStatus, {
|
||||
description: 'Initiates a backup using a configured remote.',
|
||||
})
|
||||
async initiateBackup(@Args('input') input: InitiateBackupInput): Promise<BackupStatus> {
|
||||
try {
|
||||
const result = await this.rcloneService['rcloneApiService'].startBackup({
|
||||
srcPath: input.sourcePath,
|
||||
dstPath: `${input.remoteName}:${input.destinationPath}`,
|
||||
options: input.options,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'Backup initiated successfully',
|
||||
jobId: result.jobid || result.jobId,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error('Failed to initiate backup:', error);
|
||||
|
||||
return {
|
||||
status: `Failed to initiate backup: ${errorMessage}`,
|
||||
jobId: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupStatus, {
|
||||
description: 'Get the status for the backup service',
|
||||
})
|
||||
@@ -198,4 +113,77 @@ export class BackupResolver {
|
||||
uiSchema,
|
||||
};
|
||||
}
|
||||
|
||||
@Subscription(() => BackupJob, {
|
||||
description: 'Subscribe to real-time backup job progress updates',
|
||||
nullable: true,
|
||||
})
|
||||
async backupJobProgress(@Args('jobId', { type: () => PrefixedID }) jobId: string) {
|
||||
return pubsub.asyncIterableIterator(`BACKUP_JOB_PROGRESS:${jobId}`);
|
||||
}
|
||||
|
||||
private async backupJobs(showSystemJobs: boolean = false): Promise<RCloneJob[]> {
|
||||
try {
|
||||
this.logger.debug(`backupJobs called with showSystemJobs: ${showSystemJobs}`);
|
||||
|
||||
let jobs;
|
||||
if (showSystemJobs) {
|
||||
// Get all jobs when showing system jobs
|
||||
jobs = await this.rcloneService['rcloneApiService'].getAllJobsWithStats();
|
||||
this.logger.debug(`All jobs with stats: ${JSON.stringify(jobs)}`);
|
||||
} else {
|
||||
// Get only backup jobs with enhanced stats when not showing system jobs
|
||||
jobs = await this.rcloneService['rcloneApiService'].getBackupJobsWithStats();
|
||||
this.logger.debug(`Backup jobs with enhanced stats: ${JSON.stringify(jobs)}`);
|
||||
}
|
||||
|
||||
// Filter and map jobs
|
||||
const allJobs =
|
||||
jobs.jobids?.map((jobId: string | number, index: number) => {
|
||||
const stats = jobs.stats?.[index] || {};
|
||||
const group = stats.group || '';
|
||||
|
||||
this.logger.debug(
|
||||
`Processing job ${jobId}: group="${group}", stats keys: [${Object.keys(stats).join(', ')}]`
|
||||
);
|
||||
|
||||
return {
|
||||
id: String(jobId),
|
||||
group: group,
|
||||
stats,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
this.logger.debug(`Mapped ${allJobs.length} jobs total`);
|
||||
|
||||
// Log all job groups for analysis
|
||||
const jobGroupSummary = allJobs.map((job) => ({ id: job.id, group: job.group }));
|
||||
this.logger.debug(`All job groups: ${JSON.stringify(jobGroupSummary)}`);
|
||||
|
||||
// Filter based on showSystemJobs flag
|
||||
if (showSystemJobs) {
|
||||
this.logger.debug(`Returning all ${allJobs.length} jobs (showSystemJobs=true)`);
|
||||
return allJobs;
|
||||
} else {
|
||||
// When not showing system jobs, we already filtered to backup jobs in getBackupJobsWithStats
|
||||
// But let's double-check the filtering for safety
|
||||
const filteredJobs = allJobs.filter((job) => job.group.startsWith('backup/'));
|
||||
this.logger.debug(
|
||||
`Filtered to ${filteredJobs.length} backup jobs (group starts with 'backup/')`
|
||||
);
|
||||
|
||||
const nonBackupJobs = allJobs.filter((job) => !job.group.startsWith('backup/'));
|
||||
if (nonBackupJobs.length > 0) {
|
||||
this.logger.debug(
|
||||
`Excluded ${nonBackupJobs.length} non-backup jobs: ${JSON.stringify(nonBackupJobs.map((j) => ({ id: j.id, group: j.group })))}`
|
||||
);
|
||||
}
|
||||
|
||||
return filteredJobs;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch backup jobs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum Resource {
|
||||
ACTIVATION_CODE = 'ACTIVATION_CODE',
|
||||
API_KEY = 'API_KEY',
|
||||
ARRAY = 'ARRAY',
|
||||
BACKUP = 'BACKUP',
|
||||
CLOUD = 'CLOUD',
|
||||
CONFIG = 'CONFIG',
|
||||
CONNECT = 'CONNECT',
|
||||
|
||||
@@ -17,6 +17,11 @@ export class DockerMutations {}
|
||||
@ObjectType()
|
||||
export class VmMutations {}
|
||||
|
||||
@ObjectType({
|
||||
description: 'Backup related mutations',
|
||||
})
|
||||
export class BackupMutations {}
|
||||
|
||||
@ObjectType({
|
||||
description: 'API Key related mutations',
|
||||
})
|
||||
@@ -43,6 +48,9 @@ export class RootMutations {
|
||||
@Field(() => VmMutations, { description: 'VM related mutations' })
|
||||
vm: VmMutations = new VmMutations();
|
||||
|
||||
@Field(() => BackupMutations, { description: 'Backup related mutations' })
|
||||
backup: BackupMutations = new BackupMutations();
|
||||
|
||||
@Field(() => ApiKeyMutations, { description: 'API Key related mutations' })
|
||||
apiKey: ApiKeyMutations = new ApiKeyMutations();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Mutation, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
ApiKeyMutations,
|
||||
ArrayMutations,
|
||||
BackupMutations,
|
||||
DockerMutations,
|
||||
ParityCheckMutations,
|
||||
RCloneMutations,
|
||||
@@ -27,6 +28,11 @@ export class RootMutationsResolver {
|
||||
return new VmMutations();
|
||||
}
|
||||
|
||||
@Mutation(() => BackupMutations, { name: 'backup' })
|
||||
backup(): BackupMutations {
|
||||
return new BackupMutations();
|
||||
}
|
||||
|
||||
@Mutation(() => ParityCheckMutations, { name: 'parityCheck' })
|
||||
parityCheck(): ParityCheckMutations {
|
||||
return new ParityCheckMutations();
|
||||
|
||||
2626
api/src/unraid-api/graph/resolvers/rclone/Remote Control _ API.html
Normal file
2626
api/src/unraid-api/graph/resolvers/rclone/Remote Control _ API.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,18 @@ import got, { HTTPError } from 'got';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
import { sanitizeParams } from '@app/core/log.js';
|
||||
import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js';
|
||||
import {
|
||||
CreateRCloneRemoteDto,
|
||||
DeleteRCloneRemoteDto,
|
||||
GetRCloneJobStatusDto,
|
||||
GetRCloneRemoteConfigDto,
|
||||
GetRCloneRemoteDetailsDto,
|
||||
RCloneJobListResponse,
|
||||
RCloneJobStats,
|
||||
RCloneJobStatusResponse,
|
||||
RCloneJobsWithStatsResponse,
|
||||
RCloneJobWithStats,
|
||||
RCloneProviderOptionResponse,
|
||||
RCloneProviderResponse,
|
||||
RCloneRemoteConfig,
|
||||
@@ -35,21 +41,18 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
process.env.RCLONE_USERNAME || crypto.randomBytes(12).toString('base64');
|
||||
private readonly rclonePassword: string =
|
||||
process.env.RCLONE_PASSWORD || crypto.randomBytes(24).toString('base64');
|
||||
constructor() {}
|
||||
constructor(private readonly formatService: FormatService) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
try {
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
// Check if Rclone Socket is running, if not, start it.
|
||||
this.rcloneSocketPath = getters.paths()['rclone-socket'];
|
||||
const logFilePath = join(getters.paths()['log-base'], 'rclone-unraid-api.log');
|
||||
this.logger.log(`RClone socket path: ${this.rcloneSocketPath}`);
|
||||
this.logger.log(`RClone log file path: ${logFilePath}`);
|
||||
|
||||
// Format the base URL for Unix socket
|
||||
this.rcloneBaseUrl = `http://unix:${this.rcloneSocketPath}:`;
|
||||
|
||||
// Check if the RClone socket exists, if not, create it.
|
||||
const socketExists = await this.checkRcloneSocketExists(this.rcloneSocketPath);
|
||||
|
||||
if (socketExists) {
|
||||
@@ -83,19 +86,14 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
this.logger.log('RCloneApiService module destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the RClone RC daemon on the specified socket path
|
||||
*/
|
||||
private async startRcloneSocket(socketPath: string, logFilePath: string): Promise<boolean> {
|
||||
try {
|
||||
// Make log file exists
|
||||
if (!existsSync(logFilePath)) {
|
||||
this.logger.debug(`Creating log file: ${logFilePath}`);
|
||||
await mkdir(dirname(logFilePath), { recursive: true });
|
||||
await writeFile(logFilePath, '', 'utf-8');
|
||||
}
|
||||
this.logger.log(`Starting RClone RC daemon on socket: ${socketPath}`);
|
||||
// Start the process but don't wait for it to finish
|
||||
|
||||
this.rcloneProcess = execa(
|
||||
'rclone',
|
||||
[
|
||||
@@ -109,17 +107,15 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
...(this.rcloneUsername ? ['--rc-user', this.rcloneUsername] : []),
|
||||
...(this.rclonePassword ? ['--rc-pass', this.rclonePassword] : []),
|
||||
],
|
||||
{ detached: false } // Keep attached to manage lifecycle
|
||||
{ detached: false }
|
||||
);
|
||||
|
||||
// Handle potential errors during process spawning (e.g., command not found)
|
||||
this.rcloneProcess.on('error', (error: Error) => {
|
||||
this.logger.error(`RClone process failed to start: ${error.message}`);
|
||||
this.rcloneProcess = null; // Clear the handle on error
|
||||
this.rcloneProcess = null;
|
||||
this.isInitialized = false;
|
||||
});
|
||||
|
||||
// Handle unexpected exit
|
||||
this.rcloneProcess.on('exit', (code, signal) => {
|
||||
this.logger.warn(
|
||||
`RClone process exited unexpectedly with code: ${code}, signal: ${signal}`
|
||||
@@ -128,14 +124,13 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
this.isInitialized = false;
|
||||
});
|
||||
|
||||
// Wait for socket to be ready using p-retry with exponential backoff
|
||||
await pRetry(
|
||||
async () => {
|
||||
const isRunning = await this.checkRcloneSocketRunning();
|
||||
if (!isRunning) throw new Error('Rclone socket not ready');
|
||||
},
|
||||
{
|
||||
retries: 6, // 7 attempts total
|
||||
retries: 6,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 5000,
|
||||
factor: 2,
|
||||
@@ -146,7 +141,7 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error starting RClone RC daemon: ${error}`);
|
||||
this.rcloneProcess?.kill(); // Attempt to kill if started but failed later
|
||||
this.rcloneProcess?.kill();
|
||||
this.rcloneProcess = null;
|
||||
return false;
|
||||
}
|
||||
@@ -156,22 +151,21 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
if (this.rcloneProcess && !this.rcloneProcess.killed) {
|
||||
this.logger.log(`Stopping RClone RC daemon process (PID: ${this.rcloneProcess.pid})...`);
|
||||
try {
|
||||
const killed = this.rcloneProcess.kill('SIGTERM'); // Send SIGTERM first
|
||||
const killed = this.rcloneProcess.kill('SIGTERM');
|
||||
if (!killed) {
|
||||
this.logger.warn('Failed to kill RClone process with SIGTERM, trying SIGKILL.');
|
||||
this.rcloneProcess.kill('SIGKILL'); // Force kill if SIGTERM failed
|
||||
this.rcloneProcess.kill('SIGKILL');
|
||||
}
|
||||
this.logger.log('RClone process stopped.');
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error stopping RClone process: ${error}`);
|
||||
} finally {
|
||||
this.rcloneProcess = null; // Clear the handle
|
||||
this.rcloneProcess = null;
|
||||
}
|
||||
} else {
|
||||
this.logger.log('RClone process not running or already stopped.');
|
||||
}
|
||||
|
||||
// Clean up the socket file if it exists
|
||||
if (this.rcloneSocketPath && existsSync(this.rcloneSocketPath)) {
|
||||
this.logger.log(`Removing RClone socket file: ${this.rcloneSocketPath}`);
|
||||
try {
|
||||
@@ -182,9 +176,6 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the RClone socket exists
|
||||
*/
|
||||
private async checkRcloneSocketExists(socketPath: string): Promise<boolean> {
|
||||
const socketExists = existsSync(socketPath);
|
||||
if (!socketExists) {
|
||||
@@ -194,27 +185,15 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the RClone socket is running
|
||||
*/
|
||||
private async checkRcloneSocketRunning(): Promise<boolean> {
|
||||
// Use the API check instead of execa('rclone', ['about']) as rclone might not be in PATH
|
||||
// or configured correctly for the execa environment vs the rcd environment.
|
||||
try {
|
||||
// A simple API call to check if the daemon is responsive
|
||||
await this.callRcloneApi('core/pid');
|
||||
this.logger.debug('RClone socket is running and responsive.');
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
// Log less verbosely during checks
|
||||
// this.logger.error(`Error checking RClone socket: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get providers supported by RClone
|
||||
*/
|
||||
async getProviders(): Promise<RCloneProviderResponse[]> {
|
||||
const response = (await this.callRcloneApi('config/providers')) as {
|
||||
providers: RCloneProviderResponse[];
|
||||
@@ -222,34 +201,22 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
return response?.providers || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* List all remotes configured in rclone
|
||||
*/
|
||||
async listRemotes(): Promise<string[]> {
|
||||
const response = (await this.callRcloneApi('config/listremotes')) as { remotes: string[] };
|
||||
return response?.remotes || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete remote details
|
||||
*/
|
||||
async getRemoteDetails(input: GetRCloneRemoteDetailsDto): Promise<RCloneRemoteConfig> {
|
||||
await validateObject(GetRCloneRemoteDetailsDto, input);
|
||||
const config = (await this.getRemoteConfig({ name: input.name })) || {};
|
||||
return config as RCloneRemoteConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration of a remote
|
||||
*/
|
||||
async getRemoteConfig(input: GetRCloneRemoteConfigDto): Promise<RCloneRemoteConfig> {
|
||||
await validateObject(GetRCloneRemoteConfigDto, input);
|
||||
return this.callRcloneApi('config/get', { name: input.name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new remote configuration
|
||||
*/
|
||||
async createRemote(input: CreateRCloneRemoteDto): Promise<any> {
|
||||
await validateObject(CreateRCloneRemoteDto, input);
|
||||
this.logger.log(`Creating new remote: ${input.name} of type: ${input.type}`);
|
||||
@@ -263,9 +230,6 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing remote configuration
|
||||
*/
|
||||
async updateRemote(input: UpdateRCloneRemoteDto): Promise<any> {
|
||||
await validateObject(UpdateRCloneRemoteDto, input);
|
||||
this.logger.log(`Updating remote: ${input.name}`);
|
||||
@@ -276,55 +240,224 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.callRcloneApi('config/update', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a remote configuration
|
||||
*/
|
||||
async deleteRemote(input: DeleteRCloneRemoteDto): Promise<any> {
|
||||
await validateObject(DeleteRCloneRemoteDto, input);
|
||||
this.logger.log(`Deleting remote: ${input.name}`);
|
||||
return this.callRcloneApi('config/delete', { name: input.name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a backup operation using sync/copy
|
||||
* This copies a directory from source to destination
|
||||
*/
|
||||
async startBackup(input: RCloneStartBackupInput): Promise<any> {
|
||||
await validateObject(RCloneStartBackupInput, input);
|
||||
this.logger.log(`Starting backup from ${input.srcPath} to ${input.dstPath}`);
|
||||
this.logger.log(
|
||||
`Starting backup from ${input.srcPath} to ${input.dstPath} with group: ${input.group}`
|
||||
);
|
||||
const params = {
|
||||
srcFs: input.srcPath,
|
||||
dstFs: input.dstPath,
|
||||
...(input.async && { _async: input.async }),
|
||||
...(input.group && { _group: input.group }),
|
||||
...(input.options || {}),
|
||||
};
|
||||
return this.callRcloneApi('sync/copy', params);
|
||||
|
||||
const result = await this.callRcloneApi('sync/copy', params);
|
||||
|
||||
this.logger.log(
|
||||
`Backup job created with ID: ${result.jobid || result.jobId || 'unknown'}, group: ${input.group}`
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of a running job
|
||||
*/
|
||||
async getJobStatus(input: GetRCloneJobStatusDto): Promise<any> {
|
||||
async getJobStatus(input: GetRCloneJobStatusDto): Promise<RCloneJobStatusResponse> {
|
||||
await validateObject(GetRCloneJobStatusDto, input);
|
||||
return this.callRcloneApi('job/status', { jobid: input.jobId });
|
||||
|
||||
const result = await this.callRcloneApi('job/status', { jobid: input.jobId });
|
||||
|
||||
if (result.error) {
|
||||
this.logger.warn(`Job ${input.jobId} has error: ${result.error}`);
|
||||
}
|
||||
|
||||
if (!result.stats && result.group) {
|
||||
try {
|
||||
const groupStats = await this.getGroupStats(result.group);
|
||||
if (groupStats && typeof groupStats === 'object') {
|
||||
result.stats = { ...groupStats };
|
||||
}
|
||||
} catch (groupError) {
|
||||
this.logger.warn(`Failed to get group stats for job ${input.jobId}: ${groupError}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.stats) {
|
||||
result.stats = this.enhanceStatsWithFormattedFields(result.stats);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all running jobs
|
||||
*/
|
||||
async listRunningJobs(): Promise<any> {
|
||||
return this.callRcloneApi('job/list');
|
||||
async listRunningJobs(): Promise<RCloneJobListResponse> {
|
||||
const result = await this.callRcloneApi('job/list');
|
||||
return result;
|
||||
}
|
||||
|
||||
async getGroupStats(group: string): Promise<any> {
|
||||
const result = await this.callRcloneApi('core/stats', { group });
|
||||
return result;
|
||||
}
|
||||
|
||||
async getBackupJobsWithStats(): Promise<RCloneJobsWithStatsResponse> {
|
||||
const jobList = await this.listRunningJobs();
|
||||
|
||||
if (!jobList.jobids || jobList.jobids.length === 0) {
|
||||
this.logger.log('No active jobs found in RClone');
|
||||
return { jobids: [], stats: [] };
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${jobList.jobids.length} active jobs in RClone, processing all jobs with stats`
|
||||
);
|
||||
|
||||
const allJobs: RCloneJobWithStats[] = [];
|
||||
let successfulJobQueries = 0;
|
||||
|
||||
for (const jobId of jobList.jobids) {
|
||||
try {
|
||||
const jobStatus = await this.getJobStatus({ jobId: String(jobId) });
|
||||
const group = jobStatus.group || '';
|
||||
|
||||
let detailedStats = {};
|
||||
if (group) {
|
||||
try {
|
||||
const groupStats = await this.getGroupStats(group);
|
||||
if (groupStats && typeof groupStats === 'object') {
|
||||
detailedStats = { ...groupStats };
|
||||
}
|
||||
} catch (groupError) {
|
||||
this.logger.warn(
|
||||
`Failed to get core/stats for job ${jobId}, group ${group}: ${groupError}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhancedStats = {
|
||||
...jobStatus.stats,
|
||||
...detailedStats,
|
||||
};
|
||||
|
||||
const finalStats = this.enhanceStatsWithFormattedFields(enhancedStats);
|
||||
|
||||
allJobs.push({
|
||||
jobId,
|
||||
stats: finalStats,
|
||||
});
|
||||
|
||||
successfulJobQueries++;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get status for job ${jobId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully queried ${successfulJobQueries} jobs from ${jobList.jobids.length} total jobs`
|
||||
);
|
||||
|
||||
const result: RCloneJobsWithStatsResponse = {
|
||||
jobids: allJobs.map((job) => job.jobId),
|
||||
stats: allJobs.map((job) => job.stats),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAllJobsWithStats(): Promise<RCloneJobsWithStatsResponse> {
|
||||
const jobList = await this.listRunningJobs();
|
||||
|
||||
if (!jobList.jobids || jobList.jobids.length === 0) {
|
||||
this.logger.log('No active jobs found in RClone');
|
||||
return { jobids: [], stats: [] };
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${jobList.jobids.length} active jobs in RClone: [${jobList.jobids.join(', ')}]`
|
||||
);
|
||||
|
||||
const allJobs: RCloneJobWithStats[] = [];
|
||||
let successfulJobQueries = 0;
|
||||
|
||||
for (const jobId of jobList.jobids) {
|
||||
try {
|
||||
const jobStatus = await this.getJobStatus({ jobId: String(jobId) });
|
||||
const group = jobStatus.group || '';
|
||||
|
||||
let detailedStats = {};
|
||||
if (group) {
|
||||
try {
|
||||
const groupStats = await this.getGroupStats(group);
|
||||
if (groupStats && typeof groupStats === 'object') {
|
||||
detailedStats = { ...groupStats };
|
||||
}
|
||||
} catch (groupError) {
|
||||
this.logger.warn(
|
||||
`Failed to get core/stats for job ${jobId}, group ${group}: ${groupError}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhancedStats = {
|
||||
...jobStatus.stats,
|
||||
...detailedStats,
|
||||
};
|
||||
|
||||
const finalStats = this.enhanceStatsWithFormattedFields(enhancedStats);
|
||||
|
||||
allJobs.push({
|
||||
jobId,
|
||||
stats: finalStats,
|
||||
});
|
||||
|
||||
successfulJobQueries++;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get status for job ${jobId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully queried ${successfulJobQueries}/${jobList.jobids.length} jobs for detailed stats`
|
||||
);
|
||||
|
||||
const result: RCloneJobsWithStatsResponse = {
|
||||
jobids: allJobs.map((job) => job.jobId),
|
||||
stats: allJobs.map((job) => job.stats),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private enhanceStatsWithFormattedFields(stats: RCloneJobStats): RCloneJobStats {
|
||||
const enhancedStats = { ...stats };
|
||||
|
||||
if (stats.bytes !== undefined && stats.bytes !== null) {
|
||||
enhancedStats.formattedBytes = this.formatService.formatBytes(stats.bytes);
|
||||
}
|
||||
|
||||
if (stats.speed !== undefined && stats.speed !== null && stats.speed > 0) {
|
||||
enhancedStats.formattedSpeed = this.formatService.formatBytes(stats.speed);
|
||||
}
|
||||
|
||||
if (stats.elapsedTime !== undefined && stats.elapsedTime !== null) {
|
||||
enhancedStats.formattedElapsedTime = this.formatService.formatDuration(stats.elapsedTime);
|
||||
}
|
||||
|
||||
if (stats.eta !== undefined && stats.eta !== null && stats.eta > 0) {
|
||||
enhancedStats.formattedEta = this.formatService.formatDuration(stats.eta);
|
||||
}
|
||||
|
||||
return enhancedStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to call the RClone RC API
|
||||
*/
|
||||
private async callRcloneApi(endpoint: string, params: Record<string, any> = {}): Promise<any> {
|
||||
const url = `${this.rcloneBaseUrl}/${endpoint}`;
|
||||
try {
|
||||
this.logger.debug(
|
||||
`Calling RClone API: ${url} with params: ${JSON.stringify(sanitizeParams(params))}`
|
||||
);
|
||||
|
||||
const response = await got.post(url, {
|
||||
json: params,
|
||||
responseType: 'json',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type Layout } from '@jsonforms/core';
|
||||
import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-scalars';
|
||||
|
||||
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
|
||||
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
|
||||
|
||||
@ObjectType()
|
||||
@@ -147,6 +148,16 @@ export class RCloneStartBackupInput {
|
||||
@IsString()
|
||||
dstPath!: string;
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
async?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
group?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@@ -206,3 +217,145 @@ export class GetRCloneJobStatusDto {
|
||||
@IsString()
|
||||
jobId!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class RCloneJobStats {
|
||||
@Field(() => Number, { description: 'Bytes transferred', nullable: true })
|
||||
bytes?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Transfer speed in bytes/sec', nullable: true })
|
||||
speed?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Estimated time to completion in seconds', nullable: true })
|
||||
eta?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Elapsed time in seconds', nullable: true })
|
||||
elapsedTime?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Progress percentage (0-100)', nullable: true })
|
||||
percentage?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Number of checks completed', nullable: true })
|
||||
checks?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Number of deletes completed', nullable: true })
|
||||
deletes?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Number of errors encountered', nullable: true })
|
||||
errors?: number;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether a fatal error occurred', nullable: true })
|
||||
fatalError?: boolean;
|
||||
|
||||
@Field(() => String, { description: 'Last error message', nullable: true })
|
||||
lastError?: string;
|
||||
|
||||
@Field(() => Number, { description: 'Number of renames completed', nullable: true })
|
||||
renames?: number;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether there is a retry error', nullable: true })
|
||||
retryError?: boolean;
|
||||
|
||||
@Field(() => Number, { description: 'Number of server-side copies', nullable: true })
|
||||
serverSideCopies?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Bytes in server-side copies', nullable: true })
|
||||
serverSideCopyBytes?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Number of server-side moves', nullable: true })
|
||||
serverSideMoves?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Bytes in server-side moves', nullable: true })
|
||||
serverSideMoveBytes?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Total bytes to transfer', nullable: true })
|
||||
totalBytes?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Total checks to perform', nullable: true })
|
||||
totalChecks?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Total transfers to perform', nullable: true })
|
||||
totalTransfers?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Time spent transferring in seconds', nullable: true })
|
||||
transferTime?: number;
|
||||
|
||||
@Field(() => Number, { description: 'Number of transfers completed', nullable: true })
|
||||
transfers?: number;
|
||||
|
||||
@Field(() => GraphQLJSON, { description: 'Currently transferring files', nullable: true })
|
||||
transferring?: any[];
|
||||
|
||||
@Field(() => GraphQLJSON, { description: 'Currently checking files', nullable: true })
|
||||
checking?: any[];
|
||||
|
||||
// Formatted fields
|
||||
@Field(() => String, { description: 'Human-readable bytes transferred', nullable: true })
|
||||
formattedBytes?: string;
|
||||
|
||||
@Field(() => String, { description: 'Human-readable transfer speed', nullable: true })
|
||||
formattedSpeed?: string;
|
||||
|
||||
@Field(() => String, { description: 'Human-readable elapsed time', nullable: true })
|
||||
formattedElapsedTime?: string;
|
||||
|
||||
@Field(() => String, { description: 'Human-readable ETA', nullable: true })
|
||||
formattedEta?: string;
|
||||
|
||||
// Allow additional fields
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class RCloneJob {
|
||||
@Field(() => PrefixedID, { description: 'Job ID' })
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, { description: 'RClone group for the job', nullable: true })
|
||||
group?: string;
|
||||
|
||||
@Field(() => RCloneJobStats, { description: 'Job status and statistics', nullable: true })
|
||||
stats?: RCloneJobStats;
|
||||
|
||||
@Field(() => Number, { description: 'Progress percentage (0-100)', nullable: true })
|
||||
progressPercentage?: number;
|
||||
|
||||
@Field(() => PrefixedID, { description: 'Configuration ID that triggered this job', nullable: true })
|
||||
configId?: string;
|
||||
|
||||
@Field(() => String, { description: 'Detailed status of the job', nullable: true })
|
||||
detailedStatus?: string;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether the job is finished', nullable: true })
|
||||
finished?: boolean;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether the job was successful', nullable: true })
|
||||
success?: boolean;
|
||||
|
||||
@Field(() => String, { description: 'Error message if job failed', nullable: true })
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// API Response Types (for internal use)
|
||||
export interface RCloneJobListResponse {
|
||||
jobids: (string | number)[];
|
||||
}
|
||||
|
||||
export interface RCloneJobStatusResponse {
|
||||
group?: string;
|
||||
finished?: boolean;
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
stats?: RCloneJobStats;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface RCloneJobWithStats {
|
||||
jobId: string | number;
|
||||
stats: RCloneJobStats;
|
||||
}
|
||||
|
||||
export interface RCloneJobsWithStatsResponse {
|
||||
jobids: (string | number)[];
|
||||
stats: RCloneJobStats[];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export * from '@/components/common/loading';
|
||||
export * from '@/components/form/input';
|
||||
export * from '@/components/form/label';
|
||||
export * from '@/components/form/number';
|
||||
export * from '@/components/form/lightswitch';
|
||||
export * from '@/components/form/select';
|
||||
export * from '@/components/form/switch';
|
||||
export * from '@/components/common/scroll-area';
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @todo complete this component
|
||||
*/
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
label: string;
|
||||
}>();
|
||||
|
||||
const checked = ref(false);
|
||||
</script>
|
||||
<template>
|
||||
<SwitchGroup as="div">
|
||||
<div class="flex flex-shrink-0 items-center gap-16px">
|
||||
<Switch
|
||||
v-model="checked"
|
||||
:class="[
|
||||
checked ? 'bg-green-500' : 'bg-gray-200',
|
||||
'relative inline-flex h-24px w-[44px] flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
checked ? 'translate-x-20px' : 'translate-x-0',
|
||||
'pointer-events-none relative inline-block h-20px w-20px transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
checked ? 'opacity-0 duration-100 ease-out' : 'opacity-100 duration-200 ease-in',
|
||||
'absolute inset-0 flex h-full w-full items-center justify-center transition-opacity',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-12px w-12px text-gray-400" fill="none" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
checked ? 'opacity-100 duration-200 ease-in' : 'opacity-0 duration-100 ease-out',
|
||||
'absolute inset-0 flex h-full w-full items-center justify-center transition-opacity',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-12px w-12px text-green-500" fill="currentColor" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</Switch>
|
||||
<SwitchLabel class="text-14px">
|
||||
{{ label }}
|
||||
</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</template>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Lightswitch } from './Lightswitch.vue';
|
||||
@@ -1,23 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { useQuery, useMutation, useSubscription } from '@vue/apollo-composable';
|
||||
import {
|
||||
Switch,
|
||||
Button,
|
||||
Badge,
|
||||
Spinner,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTitle
|
||||
} from '@unraid/ui';
|
||||
|
||||
import {
|
||||
BACKUP_JOB_CONFIGS_QUERY,
|
||||
TOGGLE_BACKUP_JOB_CONFIG_MUTATION,
|
||||
TRIGGER_BACKUP_JOB_MUTATION,
|
||||
BACKUP_JOB_PROGRESS_SUBSCRIPTION
|
||||
} from '~/components/Backup/backup-jobs.query';
|
||||
import BackupJobConfigForm from '~/components/Backup/BackupJobConfigForm.vue';
|
||||
|
||||
const showConfigModal = ref(false);
|
||||
const togglingJobs = ref<Set<string>>(new Set<string>());
|
||||
const triggeringJobs = ref<Set<string>>(new Set<string>());
|
||||
const activeJobId = ref<string | null>(null);
|
||||
|
||||
const { result, loading, error, refetch } = useQuery(BACKUP_JOB_CONFIGS_QUERY);
|
||||
|
||||
const { mutate: toggleJobConfig } = useMutation(TOGGLE_BACKUP_JOB_CONFIG_MUTATION);
|
||||
const { mutate: triggerJob } = useMutation(TRIGGER_BACKUP_JOB_MUTATION);
|
||||
|
||||
const { result: progressResult } = useSubscription(
|
||||
BACKUP_JOB_PROGRESS_SUBSCRIPTION,
|
||||
{ jobId: activeJobId.value || '' },
|
||||
{ enabled: computed(() => !!activeJobId.value) }
|
||||
);
|
||||
|
||||
const backupConfigs = computed(() => result.value?.backup?.configs || []);
|
||||
|
||||
const currentJobProgress = computed(() => {
|
||||
if (!progressResult.value?.backupJobProgress) return null;
|
||||
|
||||
const job = progressResult.value.backupJobProgress;
|
||||
const percentage = job.stats?.percentage || 0;
|
||||
|
||||
return {
|
||||
jobId: job.id,
|
||||
percentage: Math.round(percentage),
|
||||
transferredBytes: job.formattedBytes || '0 B',
|
||||
speed: job.formattedSpeed || '0 B/s',
|
||||
elapsedTime: job.formattedElapsedTime || '0s',
|
||||
eta: job.formattedEta || 'Unknown'
|
||||
};
|
||||
});
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString();
|
||||
}
|
||||
|
||||
function onConfigComplete() {
|
||||
showConfigModal.value = false;
|
||||
refetch();
|
||||
}
|
||||
|
||||
async function handleToggleJob(jobId: string) {
|
||||
if (togglingJobs.value.has(jobId)) return;
|
||||
|
||||
togglingJobs.value.add(jobId);
|
||||
|
||||
try {
|
||||
await toggleJobConfig({ id: jobId });
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle job:', error);
|
||||
} finally {
|
||||
togglingJobs.value.delete(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTriggerJob(jobId: string) {
|
||||
if (triggeringJobs.value.has(jobId)) return;
|
||||
|
||||
triggeringJobs.value.add(jobId);
|
||||
|
||||
try {
|
||||
const result = await triggerJob({ id: jobId });
|
||||
if (result?.data?.backup?.triggerJob?.jobId) {
|
||||
const backupJobId = result.data.backup.triggerJob.jobId;
|
||||
activeJobId.value = backupJobId;
|
||||
console.log('Backup job triggered:', result.data.backup.triggerJob);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to trigger backup job:', error);
|
||||
} finally {
|
||||
triggeringJobs.value.delete(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
function stopProgressMonitoring() {
|
||||
activeJobId.value = null;
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
activeJobId.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="backup-config">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Scheduled Backup Jobs
|
||||
</h2>
|
||||
<button
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Scheduled Backup Jobs</h2>
|
||||
<Button
|
||||
variant="primary"
|
||||
@click="showConfigModal = true"
|
||||
>
|
||||
Add Backup Job
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Progress monitoring banner -->
|
||||
<div
|
||||
v-if="currentJobProgress"
|
||||
class="mb-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Backup in Progress
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="stopProgressMonitoring"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between text-sm text-blue-700 dark:text-blue-300 mb-1">
|
||||
<span>{{ currentJobProgress.percentage }}% complete</span>
|
||||
<span>{{ currentJobProgress.speed }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-blue-200 dark:bg-blue-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${currentJobProgress.percentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 text-sm text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<span class="font-medium">Transferred:</span> {{ currentJobProgress.transferredBytes }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Elapsed:</span> {{ currentJobProgress.elapsedTime }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">ETA:</span> {{ currentJobProgress.eta }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !result" class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600 mx-auto"></div>
|
||||
<Spinner class="mx-auto" />
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading backup configurations...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
@@ -34,25 +189,23 @@
|
||||
<div class="text-gray-400 dark:text-gray-600 mb-4">
|
||||
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3a4 4 0 118 0v4m-4 8l-4-4 4-4m0 8h8a2 2 0 002-2V5a2 2 0 00-2-2H4a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3a4 4 0 118 0v4m-4 8l-4-4 4-4m0 8h8a2 2 0 002-2V5a2 2 0 00-2-2H4a2 2 0 002 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
No backup jobs configured
|
||||
</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No backup jobs configured</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Create your first scheduled backup job to automatically protect your data.
|
||||
</p>
|
||||
<button
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
<Button
|
||||
variant="primary"
|
||||
@click="showConfigModal = true"
|
||||
>
|
||||
Create First Backup Job
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
@@ -64,59 +217,79 @@
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
<div
|
||||
:class="[
|
||||
'w-3 h-3 rounded-full',
|
||||
config.enabled ? 'bg-green-400' : 'bg-gray-400'
|
||||
'w-3 h-3 rounded-full',
|
||||
config.enabled ? 'bg-green-400' : 'bg-gray-400',
|
||||
currentJobProgress?.jobId === activeJobId ? 'animate-pulse' : ''
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ config.name }}
|
||||
<span v-if="currentJobProgress?.jobId === activeJobId" class="text-sm text-blue-600 dark:text-blue-400 ml-2">
|
||||
(Running)
|
||||
</span>
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ config.sourcePath }} → {{ config.remoteName }}:{{ config.destinationPath }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||
config.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400'
|
||||
]"
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Toggle Switch -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ config.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
<Switch
|
||||
:checked="config.enabled"
|
||||
:disabled="togglingJobs.has(config.id)"
|
||||
@update:checked="() => handleToggleJob(config.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Run Now Button -->
|
||||
<Button
|
||||
:disabled="!config.enabled || triggeringJobs.has(config.id)"
|
||||
:variant="config.enabled && !triggeringJobs.has(config.id) ? 'primary' : 'outline'"
|
||||
size="sm"
|
||||
@click="handleTriggerJob(config.id)"
|
||||
>
|
||||
{{ config.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
<span v-if="triggeringJobs.has(config.id)" class="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin mr-1"></span>
|
||||
<svg v-else class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6-4h8a2 2 0 012 2v8a2 2 0 01-2 2H8a2 2 0 01-2-2V8a2 2 0 012-2z" />
|
||||
</svg>
|
||||
{{ triggeringJobs.has(config.id) ? 'Starting...' : 'Run Now' }}
|
||||
</Button>
|
||||
|
||||
<Badge
|
||||
:variant="config.enabled ? 'green' : 'gray'"
|
||||
size="sm"
|
||||
>
|
||||
{{ config.enabled ? 'Active' : 'Inactive' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Schedule
|
||||
</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Schedule</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ config.schedule }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Last Run
|
||||
</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Run</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ config.lastRunAt ? formatDate(config.lastRunAt) : 'Never' }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Status
|
||||
</dt>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Status</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ config.lastRunStatus || 'Not run yet' }}
|
||||
</dd>
|
||||
@@ -126,58 +299,21 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal for adding new backup job -->
|
||||
<div
|
||||
v-if="showConfigModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-auto">
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h2 id="modal-title" class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Add New Backup Job
|
||||
</h2>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label="Close dialog"
|
||||
@click="showConfigModal = false"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<Sheet v-model:open="showConfigModal">
|
||||
<SheetContent class="w-full max-w-4xl max-h-[90vh] overflow-auto">
|
||||
<SheetTitle class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Add New Backup Job
|
||||
</SheetTitle>
|
||||
<div class="p-6">
|
||||
<BackupJobConfigForm @complete="onConfigComplete" @cancel="showConfigModal = false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { BACKUP_JOB_CONFIGS_QUERY } from './backup-jobs.query'
|
||||
import BackupJobConfigForm from './BackupJobConfigForm.vue'
|
||||
|
||||
const showConfigModal = ref(false)
|
||||
|
||||
const { result, loading, error, refetch } = useQuery(BACKUP_JOB_CONFIGS_QUERY)
|
||||
|
||||
const backupConfigs = computed(() => result.value?.backup?.configs || [])
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
function onConfigComplete() {
|
||||
showConfigModal.value = false
|
||||
refetch()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backup-config {
|
||||
@apply max-w-7xl mx-auto p-6;
|
||||
@apply mx-auto max-w-7xl p-6;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useQuery } from '@vue/apollo-composable';
|
||||
|
||||
import { BACKUP_JOBS_QUERY } from './backup-jobs.query';
|
||||
import BackupJobConfig from './BackupJobConfig.vue';
|
||||
|
||||
const showSystemJobs = ref(false);
|
||||
|
||||
const { result, loading, error, refetch } = useQuery(
|
||||
BACKUP_JOBS_QUERY,
|
||||
() => ({ showSystemJobs: showSystemJobs.value }),
|
||||
);
|
||||
|
||||
const backupJobs = computed(() => result.value?.backup?.jobs || []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="backup-overview">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Backup Management
|
||||
</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Backup Management</h1>
|
||||
<button
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
:disabled="loading"
|
||||
@@ -18,21 +33,33 @@
|
||||
|
||||
<!-- Running Backup Jobs Section -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Running Backup Jobs
|
||||
</h2>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Running Backup Jobs</h2>
|
||||
<div class="flex items-center space-x-3">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input v-model="showSystemJobs" type="checkbox" class="sr-only peer" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
|
||||
></div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
Show system jobs
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !result" class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading backup jobs...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Error loading backup jobs
|
||||
</h3>
|
||||
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Error loading backup jobs</h3>
|
||||
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
{{ error.message }}
|
||||
</div>
|
||||
@@ -44,16 +71,15 @@
|
||||
<div class="text-gray-400 dark:text-gray-600 mb-4">
|
||||
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
No backup jobs running
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
There are currently no active backup operations.
|
||||
</p>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No backup jobs running</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">There are currently no active backup operations.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
@@ -71,75 +97,59 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ job.type || 'Backup Job' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Job ID: {{ job.id }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Job ID: {{ job.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div v-if="job.formattedBytes" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Bytes Transferred
|
||||
</dt>
|
||||
<div v-if="job.stats?.formattedBytes" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Bytes Transferred</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.formattedBytes }}
|
||||
{{ job.stats.formattedBytes }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats.transfers" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Files Transferred
|
||||
</dt>
|
||||
<div v-if="job.stats?.transfers" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Files Transferred</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.stats.transfers }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.formattedSpeed" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Transfer Speed
|
||||
</dt>
|
||||
<div v-if="job.stats?.formattedSpeed" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Transfer Speed</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ job.stats.formattedSpeed }}/s</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.formattedElapsedTime" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Elapsed Time</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.formattedSpeed }}/s
|
||||
{{ job.stats.formattedElapsedTime }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.formattedElapsedTime" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Elapsed Time
|
||||
</dt>
|
||||
<div v-if="job.stats?.formattedEta" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">ETA</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.formattedElapsedTime }}
|
||||
{{ job.stats.formattedEta }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.formattedEta" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
ETA
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.formattedEta }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats.percentage" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Progress
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.stats.percentage }}%
|
||||
</dd>
|
||||
<div v-if="job.stats?.percentage" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Progress</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ job.stats.percentage }}%</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.percentage" class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
||||
<div
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${job.stats.percentage}%` }"
|
||||
></div>
|
||||
@@ -151,20 +161,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { BACKUP_JOBS_QUERY } from './backup-jobs.query'
|
||||
import BackupJobConfig from './BackupJobConfig.vue'
|
||||
|
||||
const { result, loading, error, refetch } = useQuery(BACKUP_JOBS_QUERY, {}, {
|
||||
pollInterval: 5000, // Refresh every 5 seconds
|
||||
})
|
||||
|
||||
const backupJobs = computed(() => result.value?.backup?.jobs || [])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backup-overview {
|
||||
@apply max-w-7xl mx-auto p-6;
|
||||
@apply mx-auto max-w-7xl p-6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +1,65 @@
|
||||
import { graphql } from '~/composables/gql/gql';
|
||||
|
||||
|
||||
export const BACKUP_STATS_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment BackupStats on BackupJobStats {
|
||||
bytes
|
||||
speed
|
||||
eta
|
||||
elapsedTime
|
||||
percentage
|
||||
checks
|
||||
deletes
|
||||
errors
|
||||
fatalError
|
||||
lastError
|
||||
renames
|
||||
retryError
|
||||
serverSideCopies
|
||||
serverSideCopyBytes
|
||||
serverSideMoves
|
||||
serverSideMoveBytes
|
||||
totalBytes
|
||||
totalChecks
|
||||
totalTransfers
|
||||
transferTime
|
||||
transfers
|
||||
transferring
|
||||
checking
|
||||
formattedBytes
|
||||
formattedSpeed
|
||||
formattedElapsedTime
|
||||
formattedEta
|
||||
group
|
||||
finished
|
||||
success
|
||||
error
|
||||
}
|
||||
`);
|
||||
|
||||
export const BACKUP_JOBS_QUERY = graphql(/* GraphQL */ `
|
||||
query BackupJobs {
|
||||
query BackupJobs($showSystemJobs: Boolean) {
|
||||
backup {
|
||||
id
|
||||
jobs {
|
||||
jobs(showSystemJobs: $showSystemJobs) {
|
||||
id
|
||||
type
|
||||
stats
|
||||
formattedBytes
|
||||
formattedSpeed
|
||||
formattedElapsedTime
|
||||
formattedEta
|
||||
group
|
||||
stats {
|
||||
...BackupStats
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const BACKUP_JOB_QUERY = graphql(/* GraphQL */ `
|
||||
query BackupJob($jobId: String!) {
|
||||
query BackupJob($jobId: PrefixedID!) {
|
||||
backupJob(jobId: $jobId) {
|
||||
id
|
||||
type
|
||||
stats
|
||||
group
|
||||
stats {
|
||||
...BackupStats
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -60,16 +97,100 @@ export const BACKUP_JOB_CONFIG_FORM_QUERY = graphql(/* GraphQL */ `
|
||||
|
||||
export const CREATE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {
|
||||
createBackupJobConfig(input: $input) {
|
||||
id
|
||||
name
|
||||
sourcePath
|
||||
remoteName
|
||||
destinationPath
|
||||
schedule
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
backup {
|
||||
createBackupJobConfig(input: $input) {
|
||||
id
|
||||
name
|
||||
sourcePath
|
||||
remoteName
|
||||
destinationPath
|
||||
schedule
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const UPDATE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {
|
||||
backup {
|
||||
updateBackupJobConfig(id: $id, input: $input) {
|
||||
id
|
||||
name
|
||||
sourcePath
|
||||
remoteName
|
||||
destinationPath
|
||||
schedule
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
lastRunAt
|
||||
lastRunStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const DELETE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation DeleteBackupJobConfig($id: String!) {
|
||||
backup {
|
||||
deleteBackupJobConfig(id: $id)
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const TOGGLE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation ToggleBackupJobConfig($id: String!) {
|
||||
backup {
|
||||
toggleJobConfig(id: $id) {
|
||||
id
|
||||
name
|
||||
sourcePath
|
||||
remoteName
|
||||
destinationPath
|
||||
schedule
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
lastRunAt
|
||||
lastRunStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const TRIGGER_BACKUP_JOB_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation TriggerBackupJob($id: PrefixedID!) {
|
||||
backup {
|
||||
triggerJob(id: $id) {
|
||||
status
|
||||
jobId
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const INITIATE_BACKUP_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation InitiateBackup($input: InitiateBackupInput!) {
|
||||
backup {
|
||||
initiateBackup(input: $input) {
|
||||
status
|
||||
jobId
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const BACKUP_JOB_PROGRESS_SUBSCRIPTION = graphql(/* GraphQL */ `
|
||||
subscription BackupJobProgress($jobId: PrefixedID!) {
|
||||
backupJobProgress(jobId: $jobId) {
|
||||
id
|
||||
type
|
||||
stats {
|
||||
...BackupStats
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -20,11 +20,17 @@ type Documents = {
|
||||
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": typeof types.CreateApiKeyDocument,
|
||||
"\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": typeof types.DeleteApiKeyDocument,
|
||||
"\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": typeof types.ApiKeyMetaDocument,
|
||||
"\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": typeof types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n": typeof types.BackupJobDocument,
|
||||
"\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": typeof types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n": typeof types.BackupJobDocument,
|
||||
"\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.BackupJobConfigsDocument,
|
||||
"\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n": typeof types.BackupJobConfigFormDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n": typeof types.CreateBackupJobConfigDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n": typeof types.CreateBackupJobConfigDocument,
|
||||
"\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.UpdateBackupJobConfigDocument,
|
||||
"\n mutation DeleteBackupJobConfig($id: String!) {\n backup {\n deleteBackupJobConfig(id: $id)\n }\n }\n": typeof types.DeleteBackupJobConfigDocument,
|
||||
"\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.ToggleBackupJobConfigDocument,
|
||||
"\n mutation TriggerBackupJob($id: PrefixedID!) {\n backup {\n triggerJob(id: $id) {\n status\n jobId\n }\n }\n }\n": typeof types.TriggerBackupJobDocument,
|
||||
"\n mutation InitiateBackup($input: InitiateBackupInput!) {\n backup {\n initiateBackup(input: $input) {\n status\n jobId\n }\n }\n }\n": typeof types.InitiateBackupDocument,
|
||||
"\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n": typeof types.BackupJobProgressDocument,
|
||||
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": typeof types.GetConnectSettingsFormDocument,
|
||||
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": typeof types.UpdateConnectSettingsDocument,
|
||||
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument,
|
||||
@@ -62,11 +68,17 @@ const documents: Documents = {
|
||||
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": types.CreateApiKeyDocument,
|
||||
"\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": types.DeleteApiKeyDocument,
|
||||
"\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": types.ApiKeyMetaDocument,
|
||||
"\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n": types.BackupJobDocument,
|
||||
"\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n": types.BackupJobDocument,
|
||||
"\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.BackupJobConfigsDocument,
|
||||
"\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n": types.BackupJobConfigFormDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n": types.CreateBackupJobConfigDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n": types.CreateBackupJobConfigDocument,
|
||||
"\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.UpdateBackupJobConfigDocument,
|
||||
"\n mutation DeleteBackupJobConfig($id: String!) {\n backup {\n deleteBackupJobConfig(id: $id)\n }\n }\n": types.DeleteBackupJobConfigDocument,
|
||||
"\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.ToggleBackupJobConfigDocument,
|
||||
"\n mutation TriggerBackupJob($id: PrefixedID!) {\n backup {\n triggerJob(id: $id) {\n status\n jobId\n }\n }\n }\n": types.TriggerBackupJobDocument,
|
||||
"\n mutation InitiateBackup($input: InitiateBackupInput!) {\n backup {\n initiateBackup(input: $input) {\n status\n jobId\n }\n }\n }\n": types.InitiateBackupDocument,
|
||||
"\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n": types.BackupJobProgressDocument,
|
||||
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": types.GetConnectSettingsFormDocument,
|
||||
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": types.UpdateConnectSettingsDocument,
|
||||
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument,
|
||||
@@ -139,11 +151,11 @@ export function graphql(source: "\n query ApiKeyMeta {\n apiKeyPossibleRoles
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"): (typeof documents)["\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"): (typeof documents)["\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n"): (typeof documents)["\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n"];
|
||||
export function graphql(source: "\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n"): (typeof documents)["\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -155,7 +167,31 @@ export function graphql(source: "\n query BackupJobConfigForm($input: BackupJob
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n"): (typeof documents)["\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n"): (typeof documents)["\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation DeleteBackupJobConfig($id: String!) {\n backup {\n deleteBackupJobConfig(id: $id)\n }\n }\n"): (typeof documents)["\n mutation DeleteBackupJobConfig($id: String!) {\n backup {\n deleteBackupJobConfig(id: $id)\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"): (typeof documents)["\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation TriggerBackupJob($id: PrefixedID!) {\n backup {\n triggerJob(id: $id) {\n status\n jobId\n }\n }\n }\n"): (typeof documents)["\n mutation TriggerBackupJob($id: PrefixedID!) {\n backup {\n triggerJob(id: $id) {\n status\n jobId\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation InitiateBackup($input: InitiateBackupInput!) {\n backup {\n initiateBackup(input: $input) {\n status\n jobId\n }\n }\n }\n"): (typeof documents)["\n mutation InitiateBackup($input: InitiateBackupInput!) {\n backup {\n initiateBackup(input: $input) {\n status\n jobId\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n"): (typeof documents)["\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -382,8 +382,17 @@ export type Backup = Node & {
|
||||
status: BackupStatus;
|
||||
};
|
||||
|
||||
|
||||
export type BackupJobsArgs = {
|
||||
showSystemJobs?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
export type BackupJob = {
|
||||
__typename?: 'BackupJob';
|
||||
/** Configuration ID that triggered this job */
|
||||
configId?: Maybe<Scalars['PrefixedID']['output']>;
|
||||
/** Detailed status of the job */
|
||||
detailedStatus?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted bytes transferred */
|
||||
formattedBytes?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted elapsed time */
|
||||
@@ -392,15 +401,19 @@ export type BackupJob = {
|
||||
formattedEta?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted transfer speed */
|
||||
formattedSpeed?: Maybe<Scalars['String']['output']>;
|
||||
/** RClone group for the job */
|
||||
group?: Maybe<Scalars['String']['output']>;
|
||||
/** Job ID */
|
||||
id: Scalars['String']['output'];
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
/** Progress percentage (0-100) */
|
||||
progressPercentage?: Maybe<Scalars['Float']['output']>;
|
||||
/** Job status and statistics */
|
||||
stats: Scalars['JSON']['output'];
|
||||
/** Job type (e.g., sync/copy) */
|
||||
type: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type BackupJobConfig = {
|
||||
export type BackupJobConfig = Node & {
|
||||
__typename?: 'BackupJobConfig';
|
||||
/** When this config was created */
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@@ -430,7 +443,7 @@ export type BackupJobConfig = {
|
||||
export type BackupJobConfigForm = {
|
||||
__typename?: 'BackupJobConfigForm';
|
||||
dataSchema: Scalars['JSON']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
uiSchema: Scalars['JSON']['output'];
|
||||
};
|
||||
|
||||
@@ -438,6 +451,60 @@ export type BackupJobConfigFormInput = {
|
||||
showAdvanced?: Scalars['Boolean']['input'];
|
||||
};
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutations = {
|
||||
__typename?: 'BackupMutations';
|
||||
/** Create a new backup job configuration */
|
||||
createBackupJobConfig: BackupJobConfig;
|
||||
/** Delete a backup job configuration */
|
||||
deleteBackupJobConfig: Scalars['Boolean']['output'];
|
||||
/** Initiates a backup using a configured remote. */
|
||||
initiateBackup: BackupStatus;
|
||||
/** Toggle a backup job configuration enabled/disabled */
|
||||
toggleJobConfig?: Maybe<BackupJobConfig>;
|
||||
/** Manually trigger a backup job using existing configuration */
|
||||
triggerJob: BackupStatus;
|
||||
/** Update a backup job configuration */
|
||||
updateBackupJobConfig?: Maybe<BackupJobConfig>;
|
||||
};
|
||||
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutationsCreateBackupJobConfigArgs = {
|
||||
input: CreateBackupJobConfigInput;
|
||||
};
|
||||
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutationsDeleteBackupJobConfigArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutationsInitiateBackupArgs = {
|
||||
input: InitiateBackupInput;
|
||||
};
|
||||
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutationsToggleJobConfigArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutationsTriggerJobArgs = {
|
||||
id: Scalars['PrefixedID']['input'];
|
||||
};
|
||||
|
||||
|
||||
/** Backup related mutations */
|
||||
export type BackupMutationsUpdateBackupJobConfigArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
input: UpdateBackupJobConfigInput;
|
||||
};
|
||||
|
||||
export type BackupStatus = {
|
||||
__typename?: 'BackupStatus';
|
||||
/** Job ID if available, can be used to check job status. */
|
||||
@@ -999,21 +1066,16 @@ export type Mutation = {
|
||||
archiveNotification: Notification;
|
||||
archiveNotifications: NotificationOverview;
|
||||
array: ArrayMutations;
|
||||
backup: BackupMutations;
|
||||
connectSignIn: Scalars['Boolean']['output'];
|
||||
connectSignOut: Scalars['Boolean']['output'];
|
||||
/** Create a new backup job configuration */
|
||||
createBackupJobConfig: BackupJobConfig;
|
||||
/** Creates a new notification record */
|
||||
createNotification: Notification;
|
||||
/** Deletes all archived notifications on server. */
|
||||
deleteArchivedNotifications: NotificationOverview;
|
||||
/** Delete a backup job configuration */
|
||||
deleteBackupJobConfig: Scalars['Boolean']['output'];
|
||||
deleteNotification: NotificationOverview;
|
||||
docker: DockerMutations;
|
||||
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
|
||||
/** Initiates a backup using a configured remote. */
|
||||
initiateBackup: BackupStatus;
|
||||
parityCheck: ParityCheckMutations;
|
||||
rclone: RCloneMutations;
|
||||
/** Reads each notification to recompute & update the overview. */
|
||||
@@ -1026,8 +1088,6 @@ export type Mutation = {
|
||||
/** Marks a notification as unread. */
|
||||
unreadNotification: Notification;
|
||||
updateApiSettings: ConnectSettingsValues;
|
||||
/** Update a backup job configuration */
|
||||
updateBackupJobConfig?: Maybe<BackupJobConfig>;
|
||||
vm: VmMutations;
|
||||
};
|
||||
|
||||
@@ -1052,21 +1112,11 @@ export type MutationConnectSignInArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateBackupJobConfigArgs = {
|
||||
input: CreateBackupJobConfigInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateNotificationArgs = {
|
||||
input: NotificationData;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteBackupJobConfigArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteNotificationArgs = {
|
||||
id: Scalars['PrefixedID']['input'];
|
||||
type: NotificationType;
|
||||
@@ -1078,11 +1128,6 @@ export type MutationEnableDynamicRemoteAccessArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationInitiateBackupArgs = {
|
||||
input: InitiateBackupInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetAdditionalAllowedOriginsArgs = {
|
||||
input: AllowedOriginInput;
|
||||
};
|
||||
@@ -1112,12 +1157,6 @@ export type MutationUpdateApiSettingsArgs = {
|
||||
input: ApiSettingsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateBackupJobConfigArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
input: UpdateBackupJobConfigInput;
|
||||
};
|
||||
|
||||
export type Network = Node & {
|
||||
__typename?: 'Network';
|
||||
accessUrls?: Maybe<Array<AccessUrl>>;
|
||||
@@ -1358,7 +1397,7 @@ export type QueryApiKeyArgs = {
|
||||
|
||||
|
||||
export type QueryBackupJobArgs = {
|
||||
jobId: Scalars['String']['input'];
|
||||
jobId: Scalars['PrefixedID']['input'];
|
||||
};
|
||||
|
||||
|
||||
@@ -1512,6 +1551,7 @@ export enum Resource {
|
||||
ACTIVATION_CODE = 'ACTIVATION_CODE',
|
||||
API_KEY = 'API_KEY',
|
||||
ARRAY = 'ARRAY',
|
||||
BACKUP = 'BACKUP',
|
||||
CLOUD = 'CLOUD',
|
||||
CONFIG = 'CONFIG',
|
||||
CONNECT = 'CONNECT',
|
||||
@@ -1623,6 +1663,8 @@ export type Share = Node & {
|
||||
export type Subscription = {
|
||||
__typename?: 'Subscription';
|
||||
arraySubscription: UnraidArray;
|
||||
/** Subscribe to real-time backup job progress updates */
|
||||
backupJobProgress?: Maybe<BackupJob>;
|
||||
displaySubscription: Display;
|
||||
infoSubscription: Info;
|
||||
logFile: LogFileContent;
|
||||
@@ -1635,6 +1677,11 @@ export type Subscription = {
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionBackupJobProgressArgs = {
|
||||
jobId: Scalars['PrefixedID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionLogFileArgs = {
|
||||
path: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2080,17 +2127,19 @@ export type ApiKeyMetaQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type ApiKeyMetaQuery = { __typename?: 'Query', apiKeyPossibleRoles: Array<Role>, apiKeyPossiblePermissions: Array<{ __typename?: 'Permission', resource: Resource, actions: Array<string> }> };
|
||||
|
||||
export type BackupJobsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type BackupJobsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, jobs: Array<{ __typename?: 'BackupJob', id: string, type: string, stats: any, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null }> } };
|
||||
|
||||
export type BackupJobQueryVariables = Exact<{
|
||||
jobId: Scalars['String']['input'];
|
||||
export type BackupJobsQueryVariables = Exact<{
|
||||
showSystemJobs?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type BackupJobQuery = { __typename?: 'Query', backupJob?: { __typename?: 'BackupJob', id: string, type: string, stats: any } | null };
|
||||
export type BackupJobsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, jobs: Array<{ __typename?: 'BackupJob', id: string, group?: string | null, stats: any, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null }> } };
|
||||
|
||||
export type BackupJobQueryVariables = Exact<{
|
||||
jobId: Scalars['PrefixedID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BackupJobQuery = { __typename?: 'Query', backupJob?: { __typename?: 'BackupJob', id: string, group?: string | null, stats: any } | null };
|
||||
|
||||
export type BackupJobConfigsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -2109,7 +2158,50 @@ export type CreateBackupJobConfigMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateBackupJobConfigMutation = { __typename?: 'Mutation', createBackupJobConfig: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string } };
|
||||
export type CreateBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', createBackupJobConfig: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string } } };
|
||||
|
||||
export type UpdateBackupJobConfigMutationVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
input: UpdateBackupJobConfigInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', updateBackupJobConfig?: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null } | null } };
|
||||
|
||||
export type DeleteBackupJobConfigMutationVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', deleteBackupJobConfig: boolean } };
|
||||
|
||||
export type ToggleBackupJobConfigMutationVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ToggleBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', toggleJobConfig?: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null } | null } };
|
||||
|
||||
export type TriggerBackupJobMutationVariables = Exact<{
|
||||
id: Scalars['PrefixedID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type TriggerBackupJobMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', triggerJob: { __typename?: 'BackupStatus', status: string, jobId?: string | null } } };
|
||||
|
||||
export type InitiateBackupMutationVariables = Exact<{
|
||||
input: InitiateBackupInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type InitiateBackupMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', initiateBackup: { __typename?: 'BackupStatus', status: string, jobId?: string | null } } };
|
||||
|
||||
export type BackupJobProgressSubscriptionVariables = Exact<{
|
||||
jobId: Scalars['PrefixedID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BackupJobProgressSubscription = { __typename?: 'Subscription', backupJobProgress?: { __typename?: 'BackupJob', id: string, type: string, stats: any, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null } | null };
|
||||
|
||||
export type GetConnectSettingsFormQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -2307,11 +2399,17 @@ export const ApiKeysDocument = {"kind":"Document","definitions":[{"kind":"Operat
|
||||
export const CreateApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateApiKeyMutation, CreateApiKeyMutationVariables>;
|
||||
export const DeleteApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteApiKeyMutation, DeleteApiKeyMutationVariables>;
|
||||
export const ApiKeyMetaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ApiKeyMeta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossibleRoles"}},{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossiblePermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<ApiKeyMetaQuery, ApiKeyMetaQueryVariables>;
|
||||
export const BackupJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"jobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]}}]}}]} as unknown as DocumentNode<BackupJobsQuery, BackupJobsQueryVariables>;
|
||||
export const BackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}}]}}]}}]} as unknown as DocumentNode<BackupJobQuery, BackupJobQueryVariables>;
|
||||
export const BackupJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"showSystemJobs"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"jobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"showSystemJobs"},"value":{"kind":"Variable","name":{"kind":"Name","value":"showSystemJobs"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]}}]}}]} as unknown as DocumentNode<BackupJobsQuery, BackupJobsQueryVariables>;
|
||||
export const BackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}}]}}]}}]} as unknown as DocumentNode<BackupJobQuery, BackupJobQueryVariables>;
|
||||
export const BackupJobConfigsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"configs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode<BackupJobConfigsQuery, BackupJobConfigsQueryVariables>;
|
||||
export const BackupJobConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BackupJobConfigFormInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJobConfigForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]} as unknown as DocumentNode<BackupJobConfigFormQuery, BackupJobConfigFormQueryVariables>;
|
||||
export const CreateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<CreateBackupJobConfigMutation, CreateBackupJobConfigMutationVariables>;
|
||||
export const CreateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode<CreateBackupJobConfigMutation, CreateBackupJobConfigMutationVariables>;
|
||||
export const UpdateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateBackupJobConfigMutation, UpdateBackupJobConfigMutationVariables>;
|
||||
export const DeleteBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteBackupJobConfigMutation, DeleteBackupJobConfigMutationVariables>;
|
||||
export const ToggleBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ToggleBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toggleJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode<ToggleBackupJobConfigMutation, ToggleBackupJobConfigMutationVariables>;
|
||||
export const TriggerBackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TriggerBackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"jobId"}}]}}]}}]}}]} as unknown as DocumentNode<TriggerBackupJobMutation, TriggerBackupJobMutationVariables>;
|
||||
export const InitiateBackupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitiateBackup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InitiateBackupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"initiateBackup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"jobId"}}]}}]}}]}}]} as unknown as DocumentNode<InitiateBackupMutation, InitiateBackupMutationVariables>;
|
||||
export const BackupJobProgressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"BackupJobProgress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJobProgress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]}}]} as unknown as DocumentNode<BackupJobProgressSubscription, BackupJobProgressSubscriptionVariables>;
|
||||
export const GetConnectSettingsFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectSettingsForm"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectSettingsFormQuery, GetConnectSettingsFormQueryVariables>;
|
||||
export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiSettingsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateApiSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]} as unknown as DocumentNode<UpdateConnectSettingsMutation, UpdateConnectSettingsMutationVariables>;
|
||||
export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode<LogFilesQuery, LogFilesQueryVariables>;
|
||||
|
||||
Reference in New Issue
Block a user