Daily backup time in the administrator's time zone (#19); a time saved before is converted once so it shows as it ran

This commit is contained in:
2026-10-22 16:38:54 +02:00
parent fb5b234e8a
commit 67cc350b8c
8 changed files with 233 additions and 20 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ export default {
testTarget: (id) => call('post', `/targets/${id}/test`),
setTargetLimit: (id, maxGb) => call('put', `/targets/${id}/limit`, { maxGb }),
startBackup: (targetId, label) => call('post', '/backups', { targetId, label }),
setSchedule: (time) => call('put', '/schedule', { time }),
setSchedule: (time, timezone) => call('put', '/schedule', { time, timezone }),
snapshots: (targetId) => call('get', `/targets/${targetId}/snapshots`),
browse: (targetId, snapshotId, path) => call('get', `/targets/${targetId}/snapshots/${snapshotId}/browse`, { path }),
startRestore: (targetId, snapshotId, path, mode) => call('post', '/restores', { targetId, snapshotId, path, mode }),
+10 -6
View File
@@ -6,8 +6,8 @@
<NcSettingsSection :name="t('ng_backup', 'Backups')"
:description="t('ng_backup', 'Backups run in the background through Nextcloud\'s cron. Unchanged files are not read again; only new data is uploaded.')">
<div class="ngb-row">
<NcTextField v-model="schedule" class="ngb-time" :label="t('ng_backup', 'Daily at (HH:MM, empty = off)')" />
<NcButton :disabled="schedule === status.schedule" @click="saveSchedule">{{ t('ng_backup', 'Save') }}</NcButton>
<NcTextField v-model="schedule" class="ngb-time" :label="t('ng_backup', 'Daily at (HH:MM, {zone}, empty = off)', { zone: browserZone })" />
<NcButton :disabled="schedule === status.schedule && browserZone === status.scheduleTimezone" @click="saveSchedule">{{ t('ng_backup', 'Save') }}</NcButton>
</div>
<div class="ngb-row">
<NcButton v-for="target in status.targets" :key="target.id" :disabled="isRunning(target.id)" @click="start(target)">
@@ -79,15 +79,19 @@ import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import api from '../api.js'
import { formatDateTime, formatDate, formatSize } from '../format.js'
import { formatDateTime, formatDate, formatSize, toZone } from '../format.js'
import { confirmPassword } from '@nextcloud/password-confirmation'
import { reactive } from 'vue'
const props = defineProps({ status: { type: Object, required: true }, allRuns: { type: Boolean, default: false } })
const emit = defineEmits(['changed', 'show-all-runs'])
const schedule = ref(props.status.schedule)
// The time is saved in the administrator's own zone (the browser's); a schedule saved in another
// zone is shown in this one, so it reads as the moment it runs here.
const browserZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
const shown = (s) => (s && props.status.scheduleTimezone && props.status.scheduleTimezone !== browserZone) ? toZone(s, props.status.scheduleTimezone, browserZone) : s
const schedule = ref(shown(props.status.schedule))
const error = ref('')
watch(() => props.status.schedule, (s) => { schedule.value = s })
watch(() => props.status.schedule, (s) => { schedule.value = shown(s) })
const trash = reactive({})
async function loadTrash() {
@@ -131,7 +135,7 @@ async function start(target) {
async function saveSchedule() {
error.value = ''
try {
await api.setSchedule(schedule.value.trim())
await api.setSchedule(schedule.value.trim(), browserZone)
emit('changed')
} catch (e) {
error.value = e.message
+21
View File
@@ -26,3 +26,24 @@ export function formatSize(bytes) {
}
return i === 0 ? `${b} B` : `${b.toFixed(1)} ${units[i]}`
}
/**
* A time of day "HH:MM" in zone `from`, as "HH:MM" in zone `to`, at today's offsets.
*
* @param {string} hhmm the time
* @param {string} from IANA zone it is in
* @param {string} to IANA zone to show it in
* @return {string}
*/
export function toZone(hhmm, from, to) {
const [h, m] = hhmm.split(':').map(Number)
const parts = (zone, date) => Object.fromEntries(new Intl.DateTimeFormat('en-GB', {
timeZone: zone, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
}).formatToParts(date).map((p) => [p.type, p.value]))
const today = parts(from, new Date())
const guess = Date.UTC(+today.year, +today.month - 1, +today.day, h, m)
const seen = parts(from, new Date(guess))
const offset = Date.UTC(+seen.year, +seen.month - 1, +seen.day, +seen.hour, +seen.minute) - guess
const there = parts(to, new Date(guess - offset))
return `${there.hour}:${there.minute}`
}