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:
parent
1f7df973c1
commit
497379f62b
3 changed files with 160 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -59,3 +59,7 @@ nunit-*.xml
|
||||||
|
|
||||||
# mac files
|
# mac files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Configuration files (uncomment to avoid committing development configs)
|
||||||
|
config/
|
||||||
|
**/config/
|
||||||
|
|
|
||||||
64
DOCKER_CONFIG.md
Normal file
64
DOCKER_CONFIG.md
Normal 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.
|
||||||
|
|
@ -17,10 +17,27 @@ namespace ShiftScheduler.Services
|
||||||
{
|
{
|
||||||
private ApplicationConfiguration _configuration;
|
private ApplicationConfiguration _configuration;
|
||||||
private readonly object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
|
private readonly string _configDirectory;
|
||||||
|
private readonly string _shiftsFilePath;
|
||||||
|
private readonly string _transportFilePath;
|
||||||
|
|
||||||
public ConfigurationService(ApplicationConfiguration initialConfiguration)
|
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()
|
public ApplicationConfiguration GetConfiguration()
|
||||||
|
|
@ -76,6 +93,9 @@ namespace ShiftScheduler.Services
|
||||||
AfternoonTime = s.AfternoonTime
|
AfternoonTime = s.AfternoonTime
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Persist changes to external files
|
||||||
|
SaveConfigurationToFiles(_configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,5 +127,76 @@ namespace ShiftScheduler.Services
|
||||||
{
|
{
|
||||||
return GetConfiguration().Transport;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue