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.
313 lines
9.5 KiB
Vue
313 lines
9.5 KiB
Vue
<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="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="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 }) }}
|
|
</NcNoteCard>
|
|
</template>
|
|
|
|
<NcNoteCard v-else type="danger">
|
|
{{ 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 setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
import { usePage, router } from '@inertiajs/vue3';
|
|
import { loadState } from '@/utils/initialState';
|
|
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';
|
|
|
|
// 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);
|
|
|
|
// Use reactive page reference
|
|
const page = usePage();
|
|
|
|
// Auto-refresh timer
|
|
let refreshTimer: NodeJS.Timeout | null = null;
|
|
const AUTO_REFRESH_INTERVAL = 30000; // 30 seconds
|
|
|
|
// Computed properties
|
|
const missingCrossReferencesCount = computed((): number => {
|
|
const count = page.props.missingCrossReferencesCount as string | number;
|
|
return typeof count === 'string' ? parseInt(count) || 0 : count || 0;
|
|
});
|
|
|
|
const lastCronTimestamp = computed((): number => {
|
|
const lastCron = page.props.lastCron as string | number;
|
|
return typeof lastCron === 'string' ? parseInt(lastCron) || 0 : lastCron || 0;
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
const maxAgeRelativeTime = computed((): string => {
|
|
return dayjs.unix(cronMaxAge).fromNow();
|
|
});
|
|
|
|
const cronLabel = computed((): string => {
|
|
let desc = t('settings', 'Use system cron service to call the cron.php file every 5 minutes.');
|
|
|
|
if (cliBasedCronPossible.value) {
|
|
desc +=
|
|
'<br>' +
|
|
t('settings', 'The cron.php needs to be executed by the system account "{user}".', {
|
|
user: cliBasedCronUser.value,
|
|
});
|
|
} 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>',
|
|
});
|
|
}
|
|
|
|
return desc;
|
|
});
|
|
|
|
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>
|
|
.error {
|
|
margin-top: 8px;
|
|
padding: 5px;
|
|
border-radius: var(--border-radius);
|
|
color: var(--color-primary-element-text);
|
|
background-color: var(--color-error);
|
|
width: initial;
|
|
}
|
|
|
|
.warning {
|
|
margin-top: 8px;
|
|
padding: 5px;
|
|
border-radius: var(--border-radius);
|
|
color: var(--color-primary-element-text);
|
|
background-color: var(--color-warning);
|
|
width: initial;
|
|
}
|
|
|
|
.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>
|