feat: Enhance background job settings UI and functionality
Some checks failed
build.yaml / feat: Enhance background job settings UI and functionality (push) Failing after 0s
Some checks failed
build.yaml / feat: Enhance background job settings UI and functionality (push) Failing after 0s
- Updated BackgroundJob.vue to improve the display of background job statuses, including missing cross-references and current job mode. - Added auto-refresh functionality for background job status. - Introduced success toast notifications for successful status refreshes. - Modified the XML serialization process in DatasetXmlSerializer for better caching and performance. - Implemented a new RuleProvider for managing custom validation rules. - Improved error handling in routes for loading background job settings. - Enhanced ClamScan configuration with socket support for virus scanning. - Refactored dayjs utility to streamline locale management.
This commit is contained in:
parent
6757bdb77c
commit
b5bbe26ec2
27 changed files with 1221 additions and 603 deletions
|
|
@ -1,21 +1,41 @@
|
|||
<template>
|
||||
<NcSettingsSection :name="t('settings', 'Background jobs')" :description="t(
|
||||
'settings',
|
||||
`For the server to work properly, it\'s important to configure background jobs correctly. Cron is the recommended setting. Please see the documentation for more information.`,
|
||||
)" :doc-url="backgroundJobsDocUrl">
|
||||
|
||||
<template v-if="lastCron !== 0">
|
||||
<NcNoteCard v-if="oldExecution" type="danger">
|
||||
{{ t('settings', `Last job execution ran {time}. Something seems wrong.`, {
|
||||
time: relativeTime,
|
||||
timestamp: lastCron
|
||||
}) }}
|
||||
<NcSettingsSection
|
||||
:name="t('settings', 'Background jobs')"
|
||||
:description="
|
||||
t(
|
||||
'settings',
|
||||
`For the server to work properly, it's important to configure background jobs correctly. Cron is the recommended setting. Please see the documentation for more information.`,
|
||||
)
|
||||
"
|
||||
:doc-url="backgroundJobsDocUrl"
|
||||
>
|
||||
<template v-if="lastCronTimestamp">
|
||||
<NcNoteCard v-if="isExecutionTooOld" type="danger">
|
||||
{{
|
||||
t(
|
||||
'settings',
|
||||
`Last job execution ran {time}. Something seems wrong.
|
||||
Timestamp of last cron: {timestamp} `,
|
||||
{
|
||||
time: relativeTime,
|
||||
timestamp: lastCronTimestamp,
|
||||
},
|
||||
)
|
||||
}}
|
||||
</NcNoteCard>
|
||||
|
||||
<!-- <NcNoteCard v-else-if="longExecutionCron" type="warning">
|
||||
{{ t('settings', `Some jobs have not been executed since {maxAgeRelativeTime}. Please consider increasing the execution frequency.`, {maxAgeRelativeTime}) }}
|
||||
</NcNoteCard> -->
|
||||
|
||||
<NcNoteCard v-else-if="isLongExecutionCron" type="warning">
|
||||
{{
|
||||
t(
|
||||
'settings',
|
||||
`Some jobs have not been executed since {maxAgeRelativeTime}. Please consider increasing the execution
|
||||
frequency.`,
|
||||
{
|
||||
maxAgeRelativeTime,
|
||||
},
|
||||
)
|
||||
}}
|
||||
</NcNoteCard>
|
||||
|
||||
<NcNoteCard v-else type="success">
|
||||
{{ t('settings', 'Last job ran {relativeTime}.', { relativeTime }) }}
|
||||
|
|
@ -23,147 +43,180 @@
|
|||
</template>
|
||||
|
||||
<NcNoteCard v-else type="danger">
|
||||
'Background job did not run yet!'
|
||||
{{ t('settings', 'Background job did not run yet!') }}
|
||||
</NcNoteCard>
|
||||
|
||||
<!-- Missing Cross References Warning -->
|
||||
<NcNoteCard v-if="missingCrossReferencesCount >= 1" type="warning">
|
||||
{{
|
||||
t('settings', 'Found {count} missing dataset cross-reference(s). You can fix this by running: node ace detect:missing-cross-references --fix', {
|
||||
count: missingCrossReferencesCount,
|
||||
})
|
||||
}}
|
||||
</NcNoteCard>
|
||||
|
||||
<!-- Background Jobs Status Display -->
|
||||
<div class="background-jobs-mode">
|
||||
<h3>{{ t('settings', 'Background Jobs Mode') }}</h3>
|
||||
|
||||
<div class="current-mode">
|
||||
<span class="mode-label">{{ t('settings', 'Current mode:') }}</span>
|
||||
<span class="mode-value">{{ getCurrentModeLabel() }}</span>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mode-description" v-if="backgroundJobsMode === 'cron'" v-html="cronLabel"></div> -->
|
||||
</div>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<div class="actions">
|
||||
<button type="button" class="primary" @click="refreshStatus" :disabled="isRefreshing">
|
||||
{{ isRefreshing ? t('settings', 'Refreshing...') : t('settings', 'Refresh Status') }}
|
||||
</button>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { usePage, router } from '@inertiajs/vue3';
|
||||
import { loadState } from '@/utils/initialState';
|
||||
import { showError } from '@/utils/toast';
|
||||
// import { generateOcsUrl } from '@nextcloud/router';
|
||||
// import { confirmPassword } from '@nextcloud/password-confirmation';
|
||||
import axios from 'axios';
|
||||
import { showError, showSuccess } from '@/utils/toast';
|
||||
import dayjs from '@/utils/dayjs';
|
||||
|
||||
import NcNoteCard from '@/Components/NCNoteCard.vue';
|
||||
import NcSettingsSection from '@/Components/NcSettingsSection.vue';
|
||||
import { translate as t } from '@/utils/tethyscloud-l10n';
|
||||
// import { useLocaleStore } from '@/Stores/locale';
|
||||
|
||||
// import '@nextcloud/password-confirmation/dist/style.css';
|
||||
// Props and reactive data
|
||||
const cronMaxAge =ref<number>(loadState('settings', 'cronMaxAge', 1758824778));
|
||||
const backgroundJobsMode = ref<string>(loadState('settings', 'backgroundJobsMode', 'cron'));
|
||||
const cliBasedCronPossible = ref<boolean>(loadState('settings', 'cliBasedCronPossible', true));
|
||||
const cliBasedCronUser = ref<string>(loadState('settings', 'cliBasedCronUser', 'www-data'));
|
||||
const backgroundJobsDocUrl = ref<string>(loadState('settings', 'backgroundJobsDocUrl', ''));
|
||||
const isRefreshing = ref<boolean>(false);
|
||||
|
||||
// const lastCron: number = 1723807502; //loadState('settings', 'lastCron'); //1723788607
|
||||
const cronMaxAge: number = 1724046901;//loadState('settings', 'cronMaxAge', 0); //''
|
||||
const backgroundJobsMode: string = loadState('settings', 'backgroundJobsMode', 'cron'); //cron
|
||||
const cliBasedCronPossible = loadState('settings', 'cliBasedCronPossible', true); //true
|
||||
const cliBasedCronUser = loadState('settings', 'cliBasedCronUser', 'www-data'); //www-data
|
||||
const backgroundJobsDocUrl: string = loadState('settings', 'backgroundJobsDocUrl'); //https://docs.nextcloud.com/server/29/go.php?to=admin-background-jobs
|
||||
// Use reactive page reference
|
||||
const page = usePage();
|
||||
|
||||
// await loadTranslations('settings');
|
||||
// Auto-refresh timer
|
||||
let refreshTimer: NodeJS.Timeout | null = null;
|
||||
const AUTO_REFRESH_INTERVAL = 30000; // 30 seconds
|
||||
|
||||
export default {
|
||||
name: 'BackgroundJob',
|
||||
// Computed properties
|
||||
const missingCrossReferencesCount = computed((): number => {
|
||||
const count = page.props.missingCrossReferencesCount as string | number;
|
||||
return typeof count === 'string' ? parseInt(count) || 0 : count || 0;
|
||||
});
|
||||
|
||||
components: {
|
||||
NcSettingsSection,
|
||||
NcNoteCard,
|
||||
},
|
||||
const lastCronTimestamp = computed((): number => {
|
||||
const lastCron = page.props.lastCron as string | number;
|
||||
return typeof lastCron === 'string' ? parseInt(lastCron) || 0 : lastCron || 0;
|
||||
});
|
||||
|
||||
data() {
|
||||
return {
|
||||
// lastCron: 0,
|
||||
cronMaxAge: cronMaxAge,
|
||||
backgroundJobsMode: backgroundJobsMode,
|
||||
cliBasedCronPossible: cliBasedCronPossible,
|
||||
cliBasedCronUser: cliBasedCronUser,
|
||||
backgroundJobsDocUrl: backgroundJobsDocUrl,
|
||||
// relativeTime: dayjs(this.lastCron * 1000).fromNow(),
|
||||
// maxAgeRelativeTime: dayjs(cronMaxAge * 1000).fromNow(),
|
||||
t: t,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lastCron(): number {
|
||||
return usePage().props.lastCron as number;
|
||||
const relativeTime = computed((): string => {
|
||||
if (!lastCronTimestamp.value) return '';
|
||||
// Reference currentTime.value to make this reactive to time changes
|
||||
let intermValue = page.props.lastCron as string | number;
|
||||
intermValue = typeof intermValue === 'string' ? parseInt(intermValue) || 0 : intermValue || 0;
|
||||
return dayjs.unix(intermValue).fromNow();
|
||||
});
|
||||
|
||||
},
|
||||
relativeTime() {
|
||||
return dayjs.unix(this.lastCron).fromNow(); // Calculate relative time for lastCron
|
||||
},
|
||||
maxAgeRelativeTime() {
|
||||
return dayjs.unix(this.cronMaxAge).fromNow(); // Calculate relative time for cronMaxAge
|
||||
},
|
||||
cronLabel() {
|
||||
let desc = 'Use system cron service to call the cron.php file every 5 minutes.';
|
||||
if (this.cliBasedCronPossible) {
|
||||
desc +=
|
||||
'<br>' +
|
||||
'The cron.php needs to be executed by the system account "{user}".', { user: this.cliBasedCronUser };
|
||||
} else {
|
||||
desc +=
|
||||
'<br>' +
|
||||
const maxAgeRelativeTime = computed((): string => {
|
||||
return dayjs.unix(cronMaxAge).fromNow();
|
||||
});
|
||||
|
||||
'The PHP POSIX extension is required. See {linkstart}PHP documentation{linkend} for more details.',
|
||||
{
|
||||
linkstart:
|
||||
'<a target="_blank" rel="noreferrer nofollow" class="external" href="https://www.php.net/manual/en/book.posix.php">',
|
||||
linkend: '</a>',
|
||||
}
|
||||
}
|
||||
return desc;
|
||||
},
|
||||
oldExecution() {
|
||||
return (dayjs().unix() - this.lastCron) > 600; // older than 10 minutes
|
||||
},
|
||||
const cronLabel = computed((): string => {
|
||||
let desc = t('settings', 'Use system cron service to call the cron.php file every 5 minutes.');
|
||||
|
||||
longExecutionCron() {
|
||||
//type of cron job and greater than 24h
|
||||
// let test = dayjs.unix(this.cronMaxAge).format('YYYY-MM-DD HH:mm:ss');
|
||||
return (dayjs().unix() - this.cronMaxAge) > 24 * 3600 && this.backgroundJobsMode === 'cron';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onBackgroundJobModeChanged(backgroundJobsMode: string) {
|
||||
const url = generateOcsUrl('/apps/provisioning_api/api/v1/config/apps/{appId}/{key}', {
|
||||
appId: 'core',
|
||||
key: 'backgroundjobs_mode',
|
||||
if (cliBasedCronPossible.value) {
|
||||
desc +=
|
||||
'<br>' +
|
||||
t('settings', 'The cron.php needs to be executed by the system account "{user}".', {
|
||||
user: cliBasedCronUser.value,
|
||||
});
|
||||
|
||||
// await confirmPassword();
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(url, {
|
||||
value: backgroundJobsMode,
|
||||
});
|
||||
this.handleResponse({
|
||||
status: data.ocs?.meta?.status,
|
||||
});
|
||||
} catch (e) {
|
||||
this.handleResponse({
|
||||
errorMessage: t('settings', 'Unable to update background job mode'),
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
async handleResponse({ status, errorMessage, error }) {
|
||||
if (status === 'ok') {
|
||||
await this.deleteError();
|
||||
} else {
|
||||
showError(errorMessage);
|
||||
console.error(errorMessage, error);
|
||||
}
|
||||
},
|
||||
async deleteError() {
|
||||
// clear cron errors on background job mode change
|
||||
const url = generateOcsUrl('/apps/provisioning_api/api/v1/config/apps/{appId}/{key}', {
|
||||
appId: 'core',
|
||||
key: 'cronErrors',
|
||||
} else {
|
||||
desc +=
|
||||
'<br>' +
|
||||
t('settings', 'The PHP POSIX extension is required. See {linkstart}PHP documentation{linkend} for more details.', {
|
||||
linkstart:
|
||||
'<a target="_blank" rel="noreferrer nofollow" class="external" href="https://www.php.net/manual/en/book.posix.php">',
|
||||
linkend: '</a>',
|
||||
});
|
||||
}
|
||||
|
||||
// await confirmPassword();
|
||||
return desc;
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.delete(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
const isExecutionTooOld = computed((): boolean => {
|
||||
if (!lastCronTimestamp.value) return false;
|
||||
return dayjs().unix() - lastCronTimestamp.value > 600; // older than 10 minutes
|
||||
});
|
||||
|
||||
const isLongExecutionCron = computed((): boolean => {
|
||||
return dayjs().unix() - cronMaxAge > 24 * 3600 && backgroundJobsMode.value === 'cron';
|
||||
});
|
||||
|
||||
// Methods
|
||||
const getCurrentModeLabel = (): string => {
|
||||
switch (backgroundJobsMode.value) {
|
||||
case 'cron':
|
||||
return t('settings', 'Cron (Recommended)');
|
||||
case 'webcron':
|
||||
return t('settings', 'Webcron');
|
||||
case 'ajax':
|
||||
return t('settings', 'AJAX');
|
||||
default:
|
||||
return t('settings', 'Unknown');
|
||||
}
|
||||
};
|
||||
|
||||
const refreshStatus = async (): Promise<void> => {
|
||||
isRefreshing.value = true;
|
||||
|
||||
try {
|
||||
// Use Inertia to refresh the current page data
|
||||
router.reload({
|
||||
only: ['lastCron', 'cronMaxAge', 'missingCrossReferencesCount'], // Also reload missing cross references count
|
||||
onSuccess: () => {
|
||||
showSuccess(t('settings', 'Background job status refreshed'));
|
||||
},
|
||||
onError: () => {
|
||||
showError(t('settings', 'Failed to refresh status'));
|
||||
},
|
||||
onFinish: () => {
|
||||
isRefreshing.value = false;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh status:', error);
|
||||
showError(t('settings', 'Failed to refresh status'));
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoRefresh = (): void => {
|
||||
refreshTimer = setInterval(() => {
|
||||
if (!isRefreshing.value) {
|
||||
router.reload({ only: ['lastCron', 'cronMaxAge', 'missingCrossReferencesCount'] });
|
||||
}
|
||||
}, AUTO_REFRESH_INTERVAL);
|
||||
};
|
||||
|
||||
const stopAutoRefresh = (): void => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
|
@ -185,7 +238,76 @@ export default {
|
|||
width: initial;
|
||||
}
|
||||
|
||||
.ajaxSwitch {
|
||||
.background-jobs-mode {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.background-jobs-mode h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.mode-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-main-text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.mode-description {
|
||||
color: var(--color-text-maxcontrast);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.actions button.primary {
|
||||
background-color: var(--color-primary-element);
|
||||
color: var(--color-primary-element-text);
|
||||
}
|
||||
|
||||
.actions button.primary:hover:not(:disabled) {
|
||||
background-color: var(--color-primary-element-hover);
|
||||
}
|
||||
|
||||
.actions button.primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.current-mode {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue