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 <c.schaad@pog.ch>
This commit is contained in:
Copilot 2025-08-28 18:57:15 +02:00 committed by GitHub
parent 1f7df973c1
commit 497379f62b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 160 additions and 1 deletions

4
.gitignore vendored
View file

@ -59,3 +59,7 @@ nunit-*.xml
# mac files
.DS_Store
# Configuration files (uncomment to avoid committing development configs)
config/
**/config/

64
DOCKER_CONFIG.md Normal file
View file

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

View file

@ -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<Shift>? LoadShiftsFromFile()
{
if (!File.Exists(_shiftsFilePath))
return null;
try
{
var json = File.ReadAllText(_shiftsFilePath);
return JsonSerializer.Deserialize<List<Shift>>(json);
}
catch
{
return null;
}
}
private TransportConfiguration? LoadTransportFromFile()
{
if (!File.Exists(_transportFilePath))
return null;
try
{
var json = File.ReadAllText(_transportFilePath);
return JsonSerializer.Deserialize<TransportConfiguration>(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
}
}
}
}