feat(settings): hours interval, pdf queue for all users, fix users tab loading

- Convert check interval setting from minutes to hours in the UI
- Show PDF queue tab to all authenticated users (not admin-only); requeue
  actions remain admin-only in both backend and frontend
- Fix users tab not loading on first visit by removing the AsyncStateGate
  wrapper that prevented child component refs from being populated before
  onMounted fired

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Visser 2026-03-04 02:17:18 +01:00
parent 7b7829002a
commit 67dd297b87
4 changed files with 43 additions and 46 deletions

View file

@ -47,7 +47,7 @@ const savingScholarHttp = ref(false);
const updatingPassword = ref(false);
const autoRunEnabled = ref(false);
const runIntervalMinutes = ref("60");
const runIntervalHours = ref("1");
const requestDelaySeconds = ref("2");
const navVisiblePages = ref<string[]>([]);
const openalexApiKey = ref("");
@ -77,13 +77,13 @@ const tabItems = computed<AppTabItem[]>(() => {
const tabs: AppTabItem[] = [
{ id: TAB_CHECKING, label: "Checking" },
{ id: TAB_ACCOUNT, label: "Account" },
{ id: TAB_ADMIN_PDF, label: "PDF Queue" },
];
if (auth.isAdmin) {
tabs.push(
{ id: TAB_ADMIN_USERS, label: "Users" },
{ id: TAB_ADMIN_INTEGRITY, label: "Integrity" },
{ id: TAB_ADMIN_REPAIRS, label: "Repairs" },
{ id: TAB_ADMIN_PDF, label: "PDF Queue" },
{ id: TAB_ADMIN_INTEGRATIONS, label: "Integrations" },
);
}
@ -138,7 +138,7 @@ function hydrateSettings(settings: UserSettings): void {
manualRunAllowed.value = Boolean(settings.policy?.manual_run_allowed ?? true);
autoRunEnabled.value = Boolean(settings.auto_run_enabled) && automationAllowed.value;
runIntervalMinutes.value = String(settings.run_interval_minutes);
runIntervalHours.value = String(Number(settings.run_interval_minutes) / 60);
requestDelaySeconds.value = String(settings.request_delay_seconds);
navVisiblePages.value = normalizeUserNavVisiblePages(settings.nav_visible_pages);
@ -168,6 +168,18 @@ function parseBoundedInteger(value: string, label: string, minimum: number): num
return parsed;
}
function parseHoursToMinutes(value: string, minMinutes: number): number {
const hours = Number(value);
if (!Number.isFinite(hours) || hours <= 0) {
throw new Error("Check interval must be a positive number of hours.");
}
const minutes = Math.round(hours * 60);
if (minutes < minMinutes) {
throw new Error(`Check interval must be at least ${minMinutes / 60} hours.`);
}
return minutes;
}
async function loadSettings(): Promise<void> {
loading.value = true;
errorMessage.value = null;
@ -225,9 +237,8 @@ async function onSaveSettings(): Promise<void> {
try {
const payload: UserSettingsUpdate = {
auto_run_enabled: autoRunEnabled.value,
run_interval_minutes: parseBoundedInteger(
runIntervalMinutes.value,
"Check interval (minutes)",
run_interval_minutes: parseHoursToMinutes(
runIntervalHours.value,
minCheckIntervalMinutes.value,
),
request_delay_seconds: parseBoundedInteger(
@ -357,11 +368,11 @@ onMounted(async () => {
<label class="grid gap-2 text-sm font-medium text-ink-secondary">
<span class="inline-flex items-center gap-1">
Check interval (minutes)
Check interval (hours)
<AppHelpHint text="Minimum is controlled by server policy." />
</span>
<AppInput v-model="runIntervalMinutes" inputmode="numeric" />
<span class="text-xs text-secondary">Minimum: {{ minCheckIntervalMinutes }}</span>
<AppInput v-model="runIntervalHours" inputmode="decimal" />
<span class="text-xs text-secondary">Minimum: {{ minCheckIntervalMinutes / 60 }} hours</span>
</label>
<label class="grid gap-2 text-sm font-medium text-ink-secondary">