diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index ec272d8..5bb28f6 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -65,6 +65,7 @@ class ApiController extends OCSController { * @return DataResponse * * 200: Data returned @@ -77,9 +78,10 @@ class ApiController extends OCSController { } $interval = $this->config->getValueInt(AppInfo\Application::APP_ID, 'cron_interval', 24); + $retentionDays = $this->config->getValueInt(AppInfo\Application::APP_ID, 'retention_days', 30); return new DataResponse( - ['last_update' => $lastUpdate, 'interval' => $interval] + ['last_update' => $lastUpdate, 'interval' => $interval, 'retention_days' => $retentionDays] ); } @@ -140,7 +142,7 @@ class ApiController extends OCSController { /** * Update auto currency settings * - * @param array{interval: int} $data Data to update + * @param array{interval: int, retention_days?: int} $data Data to update * @return DataResponse * * 200: Data returned @@ -149,6 +151,16 @@ class ApiController extends OCSController { public function updateSettings(mixed $data): DataResponse { $interval = $data['interval']; $this->config->setValueInt(AppInfo\Application::APP_ID, 'cron_interval', $interval); + + if (isset($data['retention_days'])) { + $retentionDays = (int)$data['retention_days']; + // Ensure it's not negative (0 = no limit, >0 = days to keep) + if ($retentionDays < 0) { + $retentionDays = 0; + } + $this->config->setValueInt(AppInfo\Application::APP_ID, 'retention_days', $retentionDays); + } + return new DataResponse( ['status' => 'OK'] ); diff --git a/lib/Cron/FetchCurrenciesJob.php b/lib/Cron/FetchCurrenciesJob.php index c7f1387..9554811 100644 --- a/lib/Cron/FetchCurrenciesJob.php +++ b/lib/Cron/FetchCurrenciesJob.php @@ -20,7 +20,6 @@ class FetchCurrenciesJob extends TimedJob { $this->logger = $logger; $this->config = $config; - // Run once a day $interval = $this->config->getValueInt(AppInfo\Application::APP_ID, 'cron_interval', 24); $this->setInterval(3600 * $interval); $this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_INSENSITIVE); diff --git a/lib/Cron/RemoveOldHistoryTask.php b/lib/Cron/RemoveOldHistoryTask.php new file mode 100644 index 0000000..f798b36 --- /dev/null +++ b/lib/Cron/RemoveOldHistoryTask.php @@ -0,0 +1,35 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Cron; + +use OCA\AutoCurrency\Service\RemoveOldHistoryService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +class RemoveOldHistoryTask extends TimedJob { + public function __construct( + ITimeFactory $time, + private LoggerInterface $logger, + private RemoveOldHistoryService $removeOldHistoryService, + ) { + parent::__construct($time); + $this->setInterval(3600 * 24); + } + + protected function run($arguments): void { + $this->logger->debug('RemoveOldHistoryTask: Starting cleanup of old history records'); + + try { + $deletedCount = $this->removeOldHistoryService->removeOldHistory(); + $this->logger->info("RemoveOldHistoryTask: Successfully removed {$deletedCount} old history records"); + } catch (\Exception $e) { + $this->logger->error('RemoveOldHistoryTask: Failed to remove old history records: ' . $e->getMessage(), ['exception' => $e]); + } + } +} diff --git a/lib/Db/AutocurrencyRateHistoryMapper.php b/lib/Db/AutocurrencyRateHistoryMapper.php index 3fa9ab2..a911e8e 100644 --- a/lib/Db/AutocurrencyRateHistoryMapper.php +++ b/lib/Db/AutocurrencyRateHistoryMapper.php @@ -94,4 +94,20 @@ class AutocurrencyRateHistoryMapper extends QBMapper { return $this->findEntities($qb); } + + /** + * Delete all history records older than the specified date + * + * @param DateTimeInterface $cutoffDate Delete records with fetched_at before this date + * @return int Number of rows deleted + */ + public function deleteOlderThan(DateTimeInterface $cutoffDate): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->lt('fetched_at', $qb->createNamedParameter($cutoffDate->format('Y-m-d H:i:s'))) + ); + + return $qb->executeStatement(); + } } diff --git a/lib/Service/RemoveOldHistoryService.php b/lib/Service/RemoveOldHistoryService.php new file mode 100644 index 0000000..0be51a0 --- /dev/null +++ b/lib/Service/RemoveOldHistoryService.php @@ -0,0 +1,51 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\AutoCurrency\Service; + +use DateTimeImmutable; +use OCA\AutoCurrency\AppInfo; +use OCA\AutoCurrency\Db\AutocurrencyRateHistoryMapper; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +class RemoveOldHistoryService { + public function __construct( + private LoggerInterface $logger, + private IAppConfig $config, + private AutocurrencyRateHistoryMapper $historyMapper, + ) { + // + } + + /** + * Remove old history records based on the retention_days setting + * If retention_days is 0, no records are deleted (no limit) + * If retention_days is > 0, records older than that many days are deleted + * + * @return int Number of records deleted + */ + public function removeOldHistory(): int { + $retentionDays = $this->config->getValueInt(AppInfo\Application::APP_ID, 'retention_days', 30); + + // If retention is 0, don't delete anything (no limit) + if ($retentionDays === 0) { + $this->logger->debug('History retention is set to 0 (no limit), skipping cleanup'); + return 0; + } + + // Calculate the cutoff date + $cutoffDate = new DateTimeImmutable("-{$retentionDays} days"); + $this->logger->info("Removing history records older than {$retentionDays} days (before {$cutoffDate->format('Y-m-d H:i:s')})"); + + // Delete old records + $deletedCount = $this->historyMapper->deleteOlderThan($cutoffDate); + $this->logger->info("Removed {$deletedCount} old history records"); + + return $deletedCount; + } +} diff --git a/openapi-administration.json b/openapi-administration.json index 50adf14..2f41691 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -100,7 +100,8 @@ "type": "object", "required": [ "last_update", - "interval" + "interval", + "retention_days" ], "properties": { "last_update": { @@ -111,6 +112,10 @@ "interval": { "type": "integer", "format": "int64" + }, + "retention_days": { + "type": "integer", + "format": "int64" } } } @@ -214,6 +219,10 @@ "interval": { "type": "integer", "format": "int64" + }, + "retention_days": { + "type": "integer", + "format": "int64" } } } diff --git a/openapi-full.json b/openapi-full.json index 3d1fbba..9d1c759 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -100,7 +100,8 @@ "type": "object", "required": [ "last_update", - "interval" + "interval", + "retention_days" ], "properties": { "last_update": { @@ -111,6 +112,10 @@ "interval": { "type": "integer", "format": "int64" + }, + "retention_days": { + "type": "integer", + "format": "int64" } } } @@ -214,6 +219,10 @@ "interval": { "type": "integer", "format": "int64" + }, + "retention_days": { + "type": "integer", + "format": "int64" } } } diff --git a/src/AdminSettings.vue b/src/AdminSettings.vue index c81264b..3935d9d 100644 --- a/src/AdminSettings.vue +++ b/src/AdminSettings.vue @@ -11,110 +11,125 @@

-

-
-
-
- - +
+
+
+
+
+ + +
+ +
+ +
+ +
+ + +
-
- -
- -
- - -
+ + +
- + + {{ strings.addCurrency }}
- - - {{ strings.addCurrency }} - -
- -
- - {{ strings.save }} - +
+ + {{ strings.save }} + +
-
- +
+
+ -
- {{ strings.fetchNow }} +
+ {{ strings.fetchNow }} -
- {{ strings.lastFetched }} - {{ strings.loading }} - {{ strings.never }} - +
+ {{ strings.lastFetched }} + {{ strings.loading }} + {{ strings.never }} + +
-
-
- {{ strings.save }} +
+ +
+
+ {{ strings.save }} +
@@ -154,6 +169,7 @@ export default { return { loading: true, interval: null, + retentionDays: 30, lastUpdate: null, customCurrencies: [], originalCustomCurrencies: [], @@ -192,6 +208,7 @@ export default { addCurrency: t(APP_ID, 'Add Currency'), deleteCurrency: t(APP_ID, 'Delete Currency'), cronSettingsHeader: t(APP_ID, 'Cron Settings'), + intervalLabel: t(APP_ID, 'Update Interval'), instructionsHelp: t( APP_ID, 'See the {aStart}Personal settings{aEnd} to view instructions on how to set up your currencies.', @@ -207,6 +224,11 @@ export default { loading: t(APP_ID, 'Loading…'), never: t(APP_ID, 'Never'), save: t(APP_ID, 'Save'), + retentionDaysLabel: t(APP_ID, 'History Retention (days)'), + retentionDaysHelp: t( + APP_ID, + 'Number of days to keep currency history. Set to 0 for no limit (default: 30)', + ), }, } }, @@ -231,6 +253,10 @@ export default { console.warn('Invalid interval value', data.interval) } + if (data.retention_days !== undefined) { + this.retentionDays = data.retention_days + } + if (data.last_update) { const lastUpdate = parseDate(data.last_update, new Date()) this.lastUpdate = lastUpdate @@ -259,7 +285,13 @@ export default { try { this.loading = true const interval = this.getIntervalByLabel(this.interval)?.value ?? 24 - const resp = await ocs.put('/settings', { data: { interval } }) + const retentionDays = this.retentionDays ?? 30 + const resp = await ocs.put('/settings', { + data: { + interval, + retention_days: retentionDays, + }, + }) const data = resp.data this.loading = false console.debug('[DEBUG] Auto Currency settings saved', data) @@ -347,10 +379,6 @@ export default { margin-top: 0; } - .submit-buttons { - margin-top: 16px; - } - .cron-flex { display: flex; align-items: start; @@ -395,5 +423,16 @@ export default { } } } + + .settings-section { + display: flex; + flex-direction: column; + gap: 32px; + } + + .retention-field { + max-width: 300px; + width: 100%; + } }