From 497379f62b9fb243910f8d51b97150a152ff4302 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 Aug 2025 18:57:15 +0200 Subject: [PATCH] Save shifts and transport configurations in external files for Docker persistence (#31) * Initial plan * Implement external configuration file persistence for Docker containers Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Add Docker documentation and finalize external config implementation Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> * Delete the files * Adapt gitignore --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com> Co-authored-by: Claudio Schaad --- .gitignore | 4 ++ DOCKER_CONFIG.md | 64 ++++++++++++++++++++++ Services/ConfigurationService.cs | 93 +++++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 DOCKER_CONFIG.md diff --git a/.gitignore b/.gitignore index c629877..a0ae5fc 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ nunit-*.xml # mac files .DS_Store + +# Configuration files (uncomment to avoid committing development configs) +config/ +**/config/ diff --git a/DOCKER_CONFIG.md b/DOCKER_CONFIG.md new file mode 100644 index 0000000..967b82f --- /dev/null +++ b/DOCKER_CONFIG.md @@ -0,0 +1,64 @@ +# Configuration Persistence for Docker + +ShiftScheduler now supports persisting configuration changes in external files, making it suitable for Docker deployments where configuration should survive container rebuilds. + +## How it Works + +Configuration is automatically saved to and loaded from external JSON files in the `config/` directory: + +- `config/shifts.json` - Contains all shift definitions (name, icon, times) +- `config/transport.json` - Contains transport API settings and parameters + +## Behavior + +1. **Startup**: Service reads from external files if they exist, otherwise uses `appsettings.json` defaults +2. **Initial Creation**: If external files don't exist, they're created with current configuration +3. **Updates**: Any configuration changes via the UI are automatically saved to external files +4. **Persistence**: External files persist across application restarts + +## Docker Usage + +### Docker Compose Example + +```yaml +version: '3.8' +services: + shiftscheduler: + build: . + ports: + - "5000:5000" + volumes: + - ./config:/app/config # Mount config directory + environment: + - ASPNETCORE_ENVIRONMENT=Production +``` + +### Folder Structure + +``` +project/ +├── docker-compose.yml +├── config/ # This directory will be created automatically +│ ├── shifts.json # Persisted shift configurations +│ └── transport.json # Persisted transport settings +└── ... +``` + +## Benefits + +- **Persistence**: Configuration survives container rebuilds +- **Backup**: Easy to backup/restore configuration by copying JSON files +- **Version Control**: Configuration files can be committed to source control +- **Multiple Environments**: Different config files for different deployments +- **No Data Loss**: UI changes are automatically preserved + +## Migration + +No migration needed - existing installations will automatically: +1. Create the `config/` directory on next startup +2. Export current configuration to external files +3. Continue using existing settings + +## File Format + +Configuration files use standard JSON format and can be edited directly if needed. Changes take effect after application restart. \ No newline at end of file diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs index 7903f61..7aac9d8 100644 --- a/Services/ConfigurationService.cs +++ b/Services/ConfigurationService.cs @@ -17,10 +17,27 @@ namespace ShiftScheduler.Services { private ApplicationConfiguration _configuration; private readonly object _lock = new object(); + private readonly string _configDirectory; + private readonly string _shiftsFilePath; + private readonly string _transportFilePath; public ConfigurationService(ApplicationConfiguration initialConfiguration) { - _configuration = initialConfiguration; + _configDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config"); + _shiftsFilePath = Path.Combine(_configDirectory, "shifts.json"); + _transportFilePath = Path.Combine(_configDirectory, "transport.json"); + + // Ensure config directory exists + Directory.CreateDirectory(_configDirectory); + + // Load configuration from external files if they exist, otherwise use provided configuration + _configuration = LoadConfigurationFromFiles(initialConfiguration); + + // If no external files existed, save the initial configuration to create them + if (!File.Exists(_shiftsFilePath) || !File.Exists(_transportFilePath)) + { + SaveConfigurationToFiles(_configuration); + } } public ApplicationConfiguration GetConfiguration() @@ -76,6 +93,9 @@ namespace ShiftScheduler.Services AfternoonTime = s.AfternoonTime }).ToList() }; + + // Persist changes to external files + SaveConfigurationToFiles(_configuration); } } @@ -107,5 +127,76 @@ namespace ShiftScheduler.Services { return GetConfiguration().Transport; } + + private ApplicationConfiguration LoadConfigurationFromFiles(ApplicationConfiguration fallbackConfiguration) + { + try + { + var shifts = LoadShiftsFromFile() ?? fallbackConfiguration.Shifts; + var transport = LoadTransportFromFile() ?? fallbackConfiguration.Transport; + + return new ApplicationConfiguration + { + Transport = transport, + Shifts = shifts + }; + } + catch + { + // If any error occurs loading from files, use fallback configuration + return fallbackConfiguration; + } + } + + private List? LoadShiftsFromFile() + { + if (!File.Exists(_shiftsFilePath)) + return null; + + try + { + var json = File.ReadAllText(_shiftsFilePath); + return JsonSerializer.Deserialize>(json); + } + catch + { + return null; + } + } + + private TransportConfiguration? LoadTransportFromFile() + { + if (!File.Exists(_transportFilePath)) + return null; + + try + { + var json = File.ReadAllText(_transportFilePath); + return JsonSerializer.Deserialize(json); + } + catch + { + return null; + } + } + + private void SaveConfigurationToFiles(ApplicationConfiguration configuration) + { + try + { + var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + + var shiftsJson = JsonSerializer.Serialize(configuration.Shifts, jsonOptions); + File.WriteAllText(_shiftsFilePath, shiftsJson); + + var transportJson = JsonSerializer.Serialize(configuration.Transport, jsonOptions); + File.WriteAllText(_transportFilePath, transportJson); + } + catch + { + // Log error if needed, but don't throw to avoid breaking the application + // In a production app, you'd want proper logging here + } + } } } \ No newline at end of file