Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions app/components/header.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import Theme from '~/components/theme.vue'

const route = useRoute()
const navLinks = [
{ label: 'Tasks', to: '/tasks' },
{ label: 'Settings', to: '/settings' }
]

const isActiveRoute = (path: string) => {
if (path === '/') {
return route.path === '/'
}

return route.path === path || route.path.startsWith(`${path}/`)
}
</script>
<template>
<header class="h-14 text-foreground border-b-2 flex">
<div class="container m-auto flex justify-between">
<NuxtLink
class="no-underline"
to="/"
><h3 class="text-2xl font-bold">Taskiq Admin</h3></NuxtLink
>
<div class="container m-auto flex justify-between items-center">
<div class="flex items-center gap-8">
<NuxtLink
class="no-underline"
to="/"
><h3 class="text-2xl font-bold">Taskiq Admin</h3></NuxtLink
>
<nav class="flex gap-4 text-sm">
<NuxtLink
v-for="link in navLinks"
:key="link.to"
class="tracking-wide"
:class="[
'pb-1 transition-colors no-underline',
isActiveRoute(link.to)
? 'text-foreground border-primary'
: 'text-muted-foreground hover:text-foreground'
]"
:to="link.to"
>
{{ link.label }}
</NuxtLink>
</nav>
</div>
<Theme />
</div>
</header>
Expand Down
2 changes: 1 addition & 1 deletion app/components/tasks-table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ const searchHandler: ((value: string) => void) | undefined =
:state="task.state"
/>
</TableCell>
<TableCell>{{ task.args }}</TableCell>
<TableCell>{{ limitText(JSON.stringify(task.args), 25) }}</TableCell>
<TableCell>{{ limitText(JSON.stringify(task.kwargs), 25) }}</TableCell>
<TableCell>{{ limitText(formatReturnValue(task), 10) }}</TableCell>
<TableCell class="text-center">
Expand Down
112 changes: 112 additions & 0 deletions app/pages/settings/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { useAsyncData } from '#app'
import { toast } from 'vue-sonner'
import { Button } from '~/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from '~/components/ui/card'
import { Input } from '~/components/ui/input'
import { SETTINGS } from '~~/shared/constants/settings'
import type { SettingsSchema } from '~~/shared/schemas/settings'

const DELETE_OLD_TTL = SETTINGS.delete_old_ttl_minutes
const TTL_MINUTES_MIN = DELETE_OLD_TTL.min
const TTL_MINUTES_MAX = DELETE_OLD_TTL.max

const saving = ref(false)
const { data, pending, refresh } = useAsyncData<SettingsSchema>(
'settings',
() => $fetch('/api/settings')
)

const ttlInput = computed({
get: () => data.value?.delete_old_ttl_minutes ?? '',
set: (raw: string) => {
const parsed = parseInt(raw, 10)
if (isNaN(parsed)) {
data.value!.delete_old_ttl_minutes = null
} else {
data.value!.delete_old_ttl_minutes = parsed
}
}
})

const hasValidationError = ref(false)

async function handleSubmit() {
try {
saving.value = true
const response = await $fetch('/api/settings', {
method: 'PUT',
body: data.value
})
saving.value = false
} catch (error) {
saving.value = false
toast.error('Failed to save settings. Please try again.')
return
}

toast.success('Settings saved successfully!')
await refresh()
}
</script>

<template>
<div class="container py-6">
<Card class="max-w-xl">
<form @submit.prevent="handleSubmit">
<CardHeader>
<CardTitle>Settings</CardTitle>
<CardDescription>
Control how long task metadata should be retained before the
automated cleanup runs the delete-old job.
</CardDescription>
</CardHeader>
<CardContent
v-if="data"
class="flex flex-col gap-4"
>
<label class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground"
>TTL (minutes)</span
>
<Input
type="number"
v-model="ttlInput"
:disabled="pending"
placeholder="Enter number of minutes"
/>
<span class="text-xs text-muted-foreground">
Minimum {{ TTL_MINUTES_MIN }} minute. Maximum
{{ TTL_MINUTES_MAX }} minutes (~1 year). Leave this field empty to
disable automatic cleanup.
</span>
</label>
<p
v-if="hasValidationError"
class="text-xs text-red-500"
>
Value must be between {{ TTL_MINUTES_MIN }} and
{{ TTL_MINUTES_MAX }}.
</p>
</CardContent>
<CardFooter class="flex justify-end">
<Button
type="submit"
:disabled="saving"
class="cursor-pointer"
>
Save
</Button>
</CardFooter>
</form>
</Card>
</div>
</template>
8 changes: 8 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ export default defineNuxtConfig({
typescript: {
strict: true
},
nitro: {
experimental: {
tasks: true
},
scheduledTasks: {
'* * * * *': ['settings-dispatcher']
}
},
modules: ['@nuxt/fonts']
})
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "taskiq-admin",
"private": true,
"type": "module",
"version": "1.8.1",
"version": "1.9.0",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
Expand All @@ -16,36 +16,36 @@
"generate:deprecated:sql": "drizzle-kit export --sql | sed 's/CREATE TABLE/CREATE TABLE IF NOT EXISTS/g; s/CREATE INDEX/CREATE INDEX IF NOT EXISTS/g' > dbschema.sql"
},
"dependencies": {
"@internationalized/date": "^3.10.0",
"@internationalized/date": "^3.10.1",
"@nuxt/fonts": "0.12.1",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^14.1.0",
"better-sqlite3": "^12.5.0",
"better-sqlite3": "^12.6.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"drizzle-orm": "^0.45.1",
"lucide-vue-next": "^0.524.0",
"nuxt": "^4.2.1",
"nuxt": "^4.2.2",
"reka-ui": "^2.6.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"vue": "^3.5.27",
"vue-router": "^4.6.4",
"vue-sonner": "^2.0.9",
"zod": "^4.1.13"
"zod": "^4.3.5"
},
"packageManager": "pnpm@8.7.6+sha1.a428b12202bc4f23b17e6dffe730734dae5728e2",
"devDependencies": {
"@iconify-json/radix-icons": "^1.2.5",
"@iconify/vue": "^5.0.0",
"@nuxt/test-utils": "^3.20.1",
"@nuxt/test-utils": "^3.23.0",
"@types/better-sqlite3": "^7.6.12",
"@vue/test-utils": "^2.4.6",
"drizzle-kit": "^0.31.7",
"drizzle-kit": "^0.31.8",
"happy-dom": "^18.0.1",
"playwright-core": "^1.57.0",
"prettier": "^3.7.3",
Expand Down
Loading