22
33namespace App \Livewire \Configuration ;
44
5+ use App \Livewire \Forms \ConfigurationForm ;
56use App \Models \DatabaseServer ;
67use App \Models \Snapshot ;
78use App \Services \FailureNotificationService ;
89use Illuminate \Contracts \View \View ;
9- use Illuminate \Support \Facades \Session ;
10+ use Illuminate \Support \Facades \Log ;
11+ use Livewire \Attributes \Computed ;
1012use Livewire \Attributes \Title ;
1113use Livewire \Component ;
14+ use Lorisleiva \CronTranslator \CronTranslator ;
15+ use Mary \Traits \Toast ;
16+ use Symfony \Component \HttpFoundation \Response ;
17+ use Symfony \Component \Process \Process ;
1218
1319#[Title('Configuration ' )]
1420class Index extends Component
1521{
22+ use Toast;
23+
24+ public ConfigurationForm $ form ;
25+
26+ public function mount (): void
27+ {
28+ $ this ->form ->loadFromConfig ();
29+ }
30+
31+ #[Computed]
32+ public function isAdmin (): bool
33+ {
34+ return auth ()->user ()->isAdmin ();
35+ }
36+
1637 /**
1738 * @return array<int, array{key: string, label: string, class?: string}>
1839 */
@@ -44,94 +65,6 @@ public function getAppConfig(): array
4465 ];
4566 }
4667
47- /**
48- * @return array<int, array{value: mixed, env: string, description: string}>
49- */
50- public function getBackupConfig (): array
51- {
52- return [
53- [
54- 'env ' => 'BACKUP_WORKING_DIRECTORY ' ,
55- 'value ' => config ('backup.working_directory ' ) ?: '- ' ,
56- 'description ' => __ ('Temporary directory for backup and restore operations. ' ),
57- ],
58- [
59- 'env ' => 'BACKUP_COMPRESSION ' ,
60- 'value ' => config ('backup.compression ' ) ?: '- ' ,
61- 'description ' => __ ('Compression algorithm: "gzip", "zstd", or "encrypted". ' ),
62- ],
63- [
64- 'env ' => 'BACKUP_COMPRESSION_LEVEL ' ,
65- 'value ' => config ('backup.compression_level ' ) ?: '- ' ,
66- 'description ' => __ ('Compression level: 1-9 for gzip/encrypted, 1-19 for zstd (default: 6). ' ),
67- ],
68- [
69- 'env ' => 'BACKUP_JOB_TIMEOUT ' ,
70- 'value ' => config ('backup.job_timeout ' ) ?: '- ' ,
71- 'description ' => __ ('Maximum seconds a job can run. ' ),
72- ],
73- [
74- 'env ' => 'BACKUP_JOB_TRIES ' ,
75- 'value ' => config ('backup.job_tries ' ) ?: '- ' ,
76- 'description ' => __ ('Number of times to attempt the job. ' ),
77- ],
78- [
79- 'env ' => 'BACKUP_JOB_BACKOFF ' ,
80- 'value ' => config ('backup.job_backoff ' ) ?: '- ' ,
81- 'description ' => __ ('Seconds to wait before retrying. ' ),
82- ],
83- [
84- 'env ' => 'BACKUP_DAILY_CRON ' ,
85- 'value ' => config ('backup.daily_cron ' ) ?: '- ' ,
86- 'description ' => __ ('Cron schedule for daily backups. ' ),
87- ],
88- [
89- 'env ' => 'BACKUP_WEEKLY_CRON ' ,
90- 'value ' => config ('backup.weekly_cron ' ) ?: '- ' ,
91- 'description ' => __ ('Cron schedule for weekly backups. ' ),
92- ],
93- [
94- 'env ' => 'BACKUP_CLEANUP_CRON ' ,
95- 'value ' => config ('backup.cleanup_cron ' ) ?: '- ' ,
96- 'description ' => __ ('Cron schedule for snapshot cleanup. ' ),
97- ],
98- ];
99- }
100-
101- /**
102- * @return array<int, array{value: mixed, env: string, description: string}>
103- */
104- public function getNotificationConfig (): array
105- {
106- return [
107- [
108- 'env ' => 'NOTIFICATION_ENABLED ' ,
109- 'value ' => config ('notifications.enabled ' ) ? 'true ' : 'false ' ,
110- 'description ' => __ ('Enable failure notifications for backup and restore jobs. ' ),
111- ],
112- [
113- 'env ' => 'NOTIFICATION_MAIL_TO ' ,
114- 'value ' => config ('notifications.mail.to ' ) ?: '- ' ,
115- 'description ' => __ ('Email address for failure notifications. ' ),
116- ],
117- [
118- 'env ' => 'NOTIFICATION_SLACK_WEBHOOK_URL ' ,
119- 'value ' => $ this ->maskSensitiveValue (config ('notifications.slack.webhook_url ' )),
120- 'description ' => __ ('Slack webhook URL for failure notifications. ' ),
121- ],
122- [
123- 'env ' => 'NOTIFICATION_DISCORD_BOT_TOKEN ' ,
124- 'value ' => $ this ->maskSensitiveValue (config ('notifications.discord.token ' )),
125- 'description ' => __ ('Discord bot token for failure notifications. ' ),
126- ],
127- [
128- 'env ' => 'NOTIFICATION_DISCORD_CHANNEL_ID ' ,
129- 'value ' => config ('notifications.discord.channel_id ' ) ?: '- ' ,
130- 'description ' => __ ('Discord channel ID for failure notifications. ' ),
131- ],
132- ];
133- }
134-
13568 /**
13669 * @return array<int, array{value: mixed, env: string, description: string}>
13770 */
@@ -176,23 +109,35 @@ public function getSsoConfig(): array
176109 ];
177110 }
178111
179- private function maskSensitiveValue ( mixed $ value ): string
112+ public function saveBackupConfig ( ): void
180113 {
181- return $ value ? '******** ' : '- ' ;
114+ abort_unless (auth ()->user ()->isAdmin (), Response::HTTP_FORBIDDEN );
115+
116+ $ this ->form ->saveBackup ();
117+ $ this ->restartScheduler ();
118+
119+ $ this ->success (__ ('Backup configuration saved. ' ), position: 'toast-bottom ' );
182120 }
183121
184- public function isNotificationEnabled (): bool
122+ public function saveNotificationConfig (): void
185123 {
186- return (bool ) config ('notifications.enabled ' );
124+ abort_unless (auth ()->user ()->isAdmin (), Response::HTTP_FORBIDDEN );
125+
126+ $ this ->form ->saveNotifications ();
127+
128+ $ this ->dispatch ('notification-saved ' );
129+ $ this ->success (__ ('Notification configuration saved. ' ), position: 'toast-bottom ' );
187130 }
188131
189132 public function sendTestNotification (): void
190133 {
134+ abort_unless (auth ()->user ()->isAdmin (), Response::HTTP_FORBIDDEN );
135+
191136 $ service = app (FailureNotificationService::class);
192137 $ routes = $ service ->getNotificationRoutes ();
193138
194139 if (empty ($ routes )) {
195- Session:: flash ( ' notification- error' , __ ('No notification channels configured. Please set at least one of: NOTIFICATION_MAIL_TO, NOTIFICATION_SLACK_WEBHOOK_URL , or NOTIFICATION_DISCORD_BOT_TOKEN and NOTIFICATION_DISCORD_CHANNEL_ID . ' ));
140+ $ this -> error ( __ ('No notification channels configured. Please set at least one of: mail recipient, Slack webhook URL , or Discord bot token and channel ID . ' ), position: ' toast-bottom ' );
196141
197142 return ;
198143 }
@@ -210,20 +155,68 @@ public function sendTestNotification(): void
210155 $ service ->notifyBackupFailed ($ snapshot , $ exception );
211156
212157 $ channelNames = implode (', ' , array_keys ($ routes ));
213- Session:: flash ( ' notification- success' , __ ('Test notification sent to: :channels ' , ['channels ' => $ channelNames ]));
158+ $ this -> success ( __ ('Test notification sent to: :channels ' , ['channels ' => $ channelNames ]), position: ' toast-bottom ' );
214159 } catch (\Throwable $ e ) {
215- Session::flash ('notification-error ' , __ ('Failed to send test notification: :message ' , ['message ' => $ e ->getMessage ()]));
160+ $ this ->error (__ ('Failed to send test notification: :message ' , ['message ' => $ e ->getMessage ()]), position: 'toast-bottom ' );
161+ }
162+ }
163+
164+ public function translateCron (string $ expression ): string
165+ {
166+ try {
167+ return CronTranslator::translate ($ expression );
168+ } catch (\Throwable ) {
169+ return '' ;
170+ }
171+ }
172+
173+ /**
174+ * @return array<int, array{id: string, name: string}>
175+ */
176+ public function getCompressionOptions (): array
177+ {
178+ return [
179+ ['id ' => 'gzip ' , 'name ' => 'gzip ' ],
180+ ['id ' => 'zstd ' , 'name ' => 'zstd ' ],
181+ ['id ' => 'encrypted ' , 'name ' => 'encrypted ' ],
182+ ];
183+ }
184+
185+ private function restartScheduler (): void
186+ {
187+ $ process = new Process (['supervisorctl ' , 'restart ' , 'schedule-run ' ]);
188+ $ process ->setTimeout (10 );
189+ $ process ->run ();
190+
191+ if (! $ process ->isSuccessful ()) {
192+ Log::warning ('Failed to restart schedule-run ' , [
193+ 'exit_code ' => $ process ->getExitCode (),
194+ 'error ' => $ process ->getErrorOutput (),
195+ ]);
196+ $ this ->warning (__ ('Saved, but scheduler restart failed. Schedule changes take effect after container restart. ' ), position: 'toast-bottom ' );
216197 }
217198 }
218199
200+ /**
201+ * @return array<int, array{id: string, name: string}>
202+ */
203+ public function getChannelOptions (): array
204+ {
205+ return [
206+ ['id ' => 'email ' , 'name ' => __ ('Email ' )],
207+ ['id ' => 'slack ' , 'name ' => __ ('Slack ' )],
208+ ['id ' => 'discord ' , 'name ' => __ ('Discord ' )],
209+ ];
210+ }
211+
219212 public function render (): View
220213 {
221214 return view ('livewire.configuration.index ' , [
222215 'headers ' => $ this ->getHeaders (),
223216 'appConfig ' => $ this ->getAppConfig (),
224- 'backupConfig ' => $ this ->getBackupConfig (),
225- 'notificationConfig ' => $ this ->getNotificationConfig (),
226217 'ssoConfig ' => $ this ->getSsoConfig (),
218+ 'compressionOptions ' => $ this ->getCompressionOptions (),
219+ 'channelOptions ' => $ this ->getChannelOptions (),
227220 ]);
228221 }
229222}
0 commit comments