Migrate data management to OpenAPI-based approach

Replaced manual data handling and filtering logic with an auto-generated OpenAPI client. Introduced new modular Pinia stores for stats and packages, improving maintainability and decoupling data management. Removed outdated custom implementations to streamline the codebase.
This commit is contained in:
2025-05-04 22:16:00 +02:00
parent 555feddabf
commit 075c246710
24 changed files with 860 additions and 825 deletions

View File

@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ApiExample: typeof import('./src/components/ApiExample.vue')['default']
BuildServerStats: typeof import('./src/components/BuildServerStats.vue')['default']
BuildStats: typeof import('./src/components/MainNav/BuildStats.vue')['default']
CurrentlyBuilding: typeof import('./src/components/CurrentlyBuilding.vue')['default']
@@ -18,5 +19,6 @@ declare module 'vue' {
QueuedPackagesList: typeof import('./src/components/CurrentlyBuilding/QueuedPackagesList.vue')['default']
StatItem: typeof import('./src/components/MainNav/BuildStats/StatItem.vue')['default']
StatsListSection: typeof import('./src/components/MainNav/BuildStats/StatsListSection.vue')['default']
StoreExample: typeof import('./src/components/StoreExample.vue')['default']
}
}

View File

@@ -4,19 +4,9 @@
<main-nav />
<v-main>
<v-sheet v-if="!lastUpdated || isLoading" class="mt-2" color="transparent">
<h5 class="text-h5">Fetching Data</h5>
</v-sheet>
<v-sheet v-else-if="error" class="mt-2" color="transparent">
<h5 class="text-h5">Can't fetch data. Please try again later</h5>
</v-sheet>
<template v-else>
<build-server-stats />
<currently-building />
<packages />
</template>
<build-server-stats />
<currently-building />
<packages />
</v-main>
</v-container>
</v-app>
@@ -27,23 +17,41 @@ import MainNav from '@/components/MainNav.vue'
import BuildServerStats from '@/components/BuildServerStats.vue'
import CurrentlyBuilding from '@/components/CurrentlyBuilding.vue'
import Packages from '@/components/Packages.vue'
import { useDataStore } from '@/stores/dataStore'
import { computed, onMounted, onUnmounted } from 'vue'
import { useStatsStore } from '@/stores/statsStore'
import { onBeforeMount, onUnmounted } from 'vue'
import { usePackagesStore } from '@/stores'
const dataStore = useDataStore()
const statsStore = useStatsStore()
const packagesStore = usePackagesStore()
const isLoading = computed(() => dataStore.loading)
const error = computed(() => dataStore.error)
const lastUpdated = computed(() => {
if (!dataStore.data) return null
return dataStore.data.lastUpdated
})
let refreshInterval: number | null = null
const startAutoRefresh = (intervalMinutes = Number(import.meta.env.VITE_UPDATE_INTERVAL) || 5) => {
stopAutoRefresh()
refreshInterval = window.setInterval(
() => {
statsStore.fetchStats()
packagesStore.fetchPackages()
packagesStore.fetchCurrentlyBuilding()
},
intervalMinutes * 60 * 1000
)
}
onMounted(async () => {
await dataStore.startAutoRefresh()
const stopAutoRefresh = () => {
if (refreshInterval !== null) {
clearInterval(refreshInterval)
refreshInterval = null
}
}
onBeforeMount(() => {
statsStore.fetchStats()
packagesStore.fetchPackages(true)
packagesStore.fetchCurrentlyBuilding()
startAutoRefresh()
})
onUnmounted(() => {
dataStore.stopAutoRefresh()
stopAutoRefresh()
})
</script>

View File

@@ -15,11 +15,9 @@ import { computed } from 'vue'
const { width } = useDisplay()
// Configuration constants
const GRAPH_HEIGHT = 335
const NUMBER_OF_GRAPHS = 4
// Computed property for responsive iframe height
const iframeHeight = computed(() =>
width.value <= 800 ? `${NUMBER_OF_GRAPHS * GRAPH_HEIGHT}px` : '420px'
)

View File

@@ -13,7 +13,7 @@
:class="
updateFailed
? 'pulsating-circle-error'
: packageCount.building > 0
: packageArrays.building.length > 0
? 'pulsating-circle-amber'
: 'pulsating-circle-green'
"
@@ -22,17 +22,21 @@
{{
updateFailed
? 'Could not fetch data.'
: packageCount.building > 0
: packageArrays.building.length > 0
? 'Building'
: 'Idle'
}}
</span>
</v-row>
</v-col>
<v-col v-if="packageCount.building > 0" class="v-col-12 v-col-lg-8 mb-3">
<v-col v-if="packageArrays.building.length > 0" class="v-col-12 v-col-lg-8 mb-3">
<v-progress-linear
:max="packageCount.built + packageCount.building + packageCount.queued"
:model-value="packageCount.built"
:max="
packageArrays.built.length +
packageArrays.building.length +
packageArrays.queued.length
"
:model-value="packageArrays.built.length"
color="light-blue"
height="10"
rounded
@@ -43,25 +47,27 @@
class="text-grey v-col-12 v-col-lg-2 mb-3"
cols="auto"
style="font-size: 13px">
<template v-if="!updateFailed">
Last updated
{{
lastUpdatedSeconds > 59
? rtf.format(-Math.floor(lastUpdatedSeconds / 60), 'minutes')
: rtf.format(-lastUpdatedSeconds, 'seconds')
}}
</template>
<div v-if="!updateFailed" class="d-flex flex-column">
<span>
Last updated
{{ formatTimeAgo(lastUpdatedSeconds) }}
</span>
<span>
Last Mirror sync
{{ formatTimeAgo(lastMirrorSync) }}
</span>
</div>
<template v-else>Please try again later.</template>
</v-col>
</v-row>
</v-card-title>
<v-card-text
v-if="packageCount.building > 0 || packageCount.queued > 0"
v-if="packageArrays.building.length > 0 || packageArrays.queued.length > 0"
class="d-flex flex-column">
<v-list v-if="packageCount.building > 0" class="mb-4" width="100%">
<v-list v-if="packageArrays.building.length > 0" class="mb-4" width="100%">
<v-list-subheader>Building</v-list-subheader>
<v-list-item v-for="(pkg, index) in packages.building" :key="index">
<v-list-item v-for="(pkg, index) in packageArrays.building" :key="index">
<template v-slot:prepend>
<div class="pulsating-circle-amber me-4" />
</template>
@@ -74,20 +80,22 @@
</v-list>
<v-sheet class="ps-4" color="transparent" rounded width="100%">
<h4 class="mb-2 font-weight-light text-grey">Queued</h4>
<queued-packages-list :packages="packages.queued" />
<queued-packages-list :packages="packageArrays.queued" />
</v-sheet>
</v-card-text>
</v-card>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { useDisplay } from 'vuetify'
import { useDataStore } from '@/stores/dataStore'
import QueuedPackagesList from '@/components/CurrentlyBuilding/QueuedPackagesList.vue'
import { usePackagesStore, useStatsStore } from '@/stores'
const statsStore = useStatsStore()
const packagesStore = usePackagesStore()
const { mobile } = useDisplay()
const dataStore = useDataStore()
const lastUpdatedSeconds = ref(0)
const rtf = new Intl.RelativeTimeFormat('en', {
localeMatcher: 'best fit',
@@ -95,53 +103,43 @@ const rtf = new Intl.RelativeTimeFormat('en', {
style: 'long'
})
// Computed properties to access store data
const updateFailed = computed(() => !!dataStore.error)
const updateFailed = computed(() => !!packagesStore.state.error || !!statsStore.state.error)
const lastMirrorSync = computed(
() =>
Math.floor(Date.now() / 1000) - (statsStore.state.stats?.last_mirror_timestamp || Date.now())
)
const packageCount = computed(() => {
const stats = dataStore.getPackageStats() || { total: 0, building: 0, built: 0, queued: 0 }
return {
total: stats.total || 0,
building: stats.building || 0,
built: stats.built || 0,
queued: stats.queued || 0
}
const packageArrays = reactive({
building: computed(
() =>
packagesStore.state.currentlyBuildingPackages.filter((pkg) => pkg.status === 'building') || []
),
queued: computed(
() =>
packagesStore.state.currentlyBuildingPackages.filter((pkg) => pkg.status === 'queued') || []
),
built: computed(
() =>
packagesStore.state.currentlyBuildingPackages.filter((pkg) => pkg.status === 'built') || []
)
})
const packages = computed(() => {
return {
building: dataStore.getPackagesByStatus('building').packages || [],
queued: dataStore.getPackagesByStatus('queued').packages || [],
built: dataStore.getPackagesByStatus('built').packages || []
}
})
// Set up and clean up the timer for "last updated" display
let updateTimer: number | undefined
const resetLastUpdatedCounter = () => {
lastUpdatedSeconds.value = 0
}
const startLastUpdatedTimer = () => {
updateTimer = window.setInterval(() => {
lastUpdatedSeconds.value++
}, 1000)
}
watch(
() => dataStore.loading,
(isLoading) => {
if (!isLoading) {
resetLastUpdatedCounter()
console.log(updateFailed.value)
console.log(packageCount.value)
console.log(packages.value)
console.log(updateTimer)
}
const formatTimeAgo = (seconds: number): string => {
if (seconds >= 3600) {
return rtf.format(-Math.floor(seconds / 3600), 'hours')
} else if (seconds >= 60) {
return rtf.format(-Math.floor(seconds / 60), 'minutes')
} else {
return rtf.format(-seconds, 'seconds')
}
)
}
onMounted(() => {
startLastUpdatedTimer()
@@ -223,11 +221,11 @@ onUnmounted(() => {
}
.flex-circle {
flex-shrink: 0; /* Prevents shrinking */
flex-grow: 0; /* Prevents growing */
width: 12px; /* Ensures consistent dimensions */
flex-shrink: 0;
flex-grow: 0;
width: 12px;
height: 12px;
border-radius: 50%; /* Maintains circular appearance */
border-radius: 50%;
}
@-webkit-keyframes pulse-ring {

View File

@@ -1,17 +1,26 @@
<template>
<v-app-bar :color="appBarColors.background">
<v-app-bar :color="appBarColors.background" aria-label="Main Navigation" role="navigation">
<v-container :class="containerClasses" :style="{ maxWidth: maxContainerWidth }" fluid>
<v-row>
<v-row align="center">
<v-app-bar-title class="app-title">
<span>
<span aria-label="Home" class="home-link" role="button" tabindex="0">
{{ appTitle }}
<a :href="repoUrl" class="ms-2 gitea-link" target="_blank">
<i class="fa fa-gitea"></i>
</a>
</span>
<a
:href="repoUrl"
aria-label="ALHP GitHub Repository"
class="ms-2 gitea-link"
rel="noopener noreferrer"
target="_blank">
<i aria-hidden="true" class="fa fa-gitea"></i>
</a>
</v-app-bar-title>
<build-stats v-if="!mobile" />
<v-spacer v-if="isDesktop"></v-spacer>
<build-stats v-if="isDesktop || isTablet" :show-lto="isDesktop" />
<!-- Mobile menu button could be added here -->
</v-row>
</v-container>
</v-app-bar>
@@ -23,11 +32,18 @@ import { useDisplay } from 'vuetify'
import { computed } from 'vue'
const { mobile, width } = useDisplay()
const isTablet = computed(() => mobile && width.value >= 650 && width.value < 960)
const isDesktop = computed(() => !mobile.value && !isTablet.value)
interface AppBarColors {
background: string
accent: string
}
const appTitle = 'ALHP Status'
const repoUrl = 'https://somegit.dev/ALHP/ALHP.GO'
const maxContainerWidth = '1440px'
const appBarColors = {
const appBarColors: AppBarColors = {
background: '#0d1538',
accent: '#609926'
}
@@ -42,6 +58,21 @@ const containerClasses = computed(() => ({
.app-title {
align-self: center;
font-size: 20px;
display: flex;
align-items: center;
}
.home-link {
color: white;
text-decoration: none;
font-weight: 500;
transition: opacity 0.2s;
}
.home-link:hover,
.home-link:focus {
opacity: 0.9;
outline: none;
}
.gitea-link {
@@ -49,9 +80,23 @@ const containerClasses = computed(() => ({
font-size: 25px;
text-decoration: none;
transition: color 0.2s;
display: inline-flex;
align-items: center;
}
.gitea-link:hover {
.gitea-link:hover,
.gitea-link:focus {
color: v-bind('appBarColors.accent');
outline: none;
}
@media (max-width: 600px) {
.app-title {
font-size: 18px;
}
.gitea-link {
font-size: 22px;
}
}
</style>

View File

@@ -1,49 +1,57 @@
<template>
<v-sheet :style="sheetStyles" class="d-flex" color="transparent">
<v-sheet
v-if="!statsStore.state.loading && !statsStore.state.error"
:style="sheetStyles"
class="d-flex"
color="transparent">
<StatsListSection title="Stats">
<StatItem
v-for="(value, key) in generalStats"
v-for="(stat, key) in generalStats"
:key="key"
:color="value.color"
:count="value.count"
:color="stat.color"
:count="stat.count"
:title="key" />
</StatsListSection>
<StatsListSection title="LTO">
<StatsListSection v-if="showLto" title="LTO">
<StatItem
v-for="(value, key) in customStats.lto"
v-for="(stat, key) in ltoStats"
:key="key"
:color="value.color"
:count="value.count"
:color="stat.color"
:count="stat.count"
:title="key" />
</StatsListSection>
</v-sheet>
<v-sheet
v-else-if="statsStore.state.loading"
:style="sheetStyles"
class="d-flex align-center"
color="transparent">
<v-progress-circular class="mr-2" color="white" indeterminate size="20"></v-progress-circular>
<span class="text-caption">Loading stats...</span>
</v-sheet>
<v-sheet
v-else-if="statsStore.state.error"
:style="sheetStyles"
class="d-flex align-center"
color="transparent">
<span class="text-caption text-error">Error loading stats</span>
</v-sheet>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useDataStore } from '@/stores/dataStore'
import { type dataStore } from '@/types/dataStore'
import { computed } from 'vue'
import { useStatsStore } from '@/stores/statsStore'
import StatsListSection from '@/components/MainNav/BuildStats/StatsListSection.vue'
import StatItem from '@/components/MainNav/BuildStats/StatItem.vue'
interface CustomStatItem {
count: number
color: string
interface Props {
showLto?: boolean
}
interface CustomStats {
latest: CustomStatItem
queued: CustomStatItem
building: CustomStatItem
skipped: CustomStatItem
failed: CustomStatItem
lto: {
enabled: CustomStatItem
disabled: CustomStatItem
unknown: CustomStatItem
}
}
withDefaults(defineProps<Props>(), {
showLto: true
})
const COLORS = {
SUCCESS: '#069b35',
@@ -54,51 +62,43 @@ const COLORS = {
const sheetStyles = { gap: '50px' }
const dataStore = useDataStore()
const stats = ref<dataStore['stats']>()
const statsStore = useStatsStore()
const customStats = ref<CustomStats>({
latest: { count: 0, color: COLORS.SUCCESS },
queued: { count: 0, color: COLORS.WARNING },
building: { count: 0, color: COLORS.WARNING },
skipped: { count: 0, color: COLORS.NEUTRAL },
failed: { count: 0, color: COLORS.ERROR },
lto: {
enabled: { count: 0, color: COLORS.SUCCESS },
disabled: { count: 0, color: COLORS.ERROR },
unknown: { count: 0, color: COLORS.NEUTRAL }
const generalStats = computed(() => ({
latest: {
count: statsStore.state.stats?.latest || 0,
color: COLORS.SUCCESS
},
queued: {
count: statsStore.state.stats?.queued || 0,
color: COLORS.WARNING
},
building: {
count: statsStore.state.stats?.building || 0,
color: COLORS.WARNING
},
skipped: {
count: statsStore.state.stats?.skipped || 0,
color: COLORS.NEUTRAL
},
failed: {
count: statsStore.state.stats?.failed || 0,
color: COLORS.ERROR
}
})
}))
const generalStats = computed(() => {
const { lto, ...rest } = customStats.value
return rest
})
const updateStats = (): void => {
stats.value = dataStore.getPackageStats() || undefined
if (!stats.value) return
Object.keys(generalStats.value).forEach((key) => {
const typedKey = key as keyof typeof generalStats.value
customStats.value[typedKey].count = stats.value?.[typedKey] as number
})
Object.keys(customStats.value.lto).forEach((ltoKey) => {
const typedLtoKey = ltoKey as keyof typeof customStats.value.lto
customStats.value.lto[typedLtoKey].count = stats.value?.lto[typedLtoKey] as number
})
}
watch(
() => dataStore.loading,
(isLoading) => {
if (!isLoading) {
updateStats()
}
const ltoStats = computed(() => ({
enabled: {
count: statsStore.state.stats?.lto?.enabled || 0,
color: COLORS.SUCCESS
},
disabled: {
count: statsStore.state.stats?.lto?.disabled || 0,
color: COLORS.ERROR
},
unknown: {
count: statsStore.state.stats?.lto?.unknown || 0,
color: COLORS.NEUTRAL
}
)
onMounted(updateStats)
}))
</script>

View File

@@ -1,13 +1,18 @@
<template>
<v-list-item :style="{ color }" :title="title">
<v-list-item :color="color" :title="title">
{{ count }}
</v-list-item>
</template>
<script lang="ts" setup>
defineProps<{
const props = defineProps<{
title: string
count: number
color: string
color?: string
}>()
// Provide default values for props
const { color = 'primary' } = withDefaults(props, {
color: 'primary'
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<v-list :style="listStyles" bg-color="transparent" class="d-flex">
<v-list bg-color="transparent" class="stats-list d-flex">
<v-list-subheader>{{ title }}:</v-list-subheader>
<slot></slot>
</v-list>
@@ -9,7 +9,10 @@
defineProps<{
title: string
}>()
// Reusable styles
const listStyles = { borderRadius: '5px' }
</script>
<style lang="scss" scoped>
.stats-list {
border-radius: 5px;
}
</style>

View File

@@ -2,22 +2,14 @@
<v-sheet :color="TRANSPARENT_COLOR" class="mt-6" width="100%">
<h5 class="text-h5 mb-4">Packages</h5>
<PackageFilters :filter-options="filterOptions" :total-pages="filterResult.totalPages" />
<PackageFilters />
<PackageTable :no-results="filterResult.noResults" :packages="filterResult.packages" />
<PackageTable />
</v-sheet>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { usePackageFilters } from '@/composables/Packages/usePackageFilters'
import { TRANSPARENT_COLOR } from '@/config/constants'
import PackageFilters from '@/components/Packages/PackageFilters.vue'
import PackageTable from '@/components/Packages/PackageTable.vue'
const { filterOptions, filterResult, initFromUrl } = usePackageFilters()
onMounted(() => {
initFromUrl()
})
</script>

View File

@@ -2,7 +2,7 @@
<v-row :style="mobile ? '' : `height: ${ROW_HEIGHT}px`" width="100%">
<v-col class="v-col-12 v-col-sm-2 v-col-lg-2">
<v-text-field
v-model="filterOptions.pkgbaseSearch"
v-model="pkgbase"
clearable
color="primary"
placeholder="Search Pkgbase"
@@ -10,20 +10,16 @@
</v-col>
<v-col class="v-col-12 v-col-sm-2 v-col-lg-2 mt-n6 mt-sm-0">
<v-select
v-model="filterOptions.repo"
v-model="repo"
:items="REPO_ITEMS"
clearable
color="primary"
item-title="title"
item-value="value"
placeholder="Any Repo"
return-object
single-line
variant="outlined"></v-select>
</v-col>
<v-col class="v-col-12 v-col-sm-2 v-col-lg-3 mt-n6 mt-sm-0">
<v-select
v-model="filterOptions.statuses"
v-model="status"
:items="STATUS_ITEMS"
chips
closable-chips
@@ -37,11 +33,11 @@
variant="outlined"></v-select>
</v-col>
<v-col class="v-col-12 v-col-sm-2 v-col-lg-2 mt-n6 mt-sm-0">
<v-switch v-model="filterOptions.exactSearch" color="primary" label="Exact search"></v-switch>
<v-switch v-model="exact" color="primary" label="Exact search"></v-switch>
</v-col>
<v-col :class="mobile ? 'mt-n6' : ''" :cols="mobile ? 12 : 'auto'" class="ms-auto">
<v-pagination
v-model="filterOptions.page"
v-model="page"
:length="totalPages"
:total-visible="mobile ? undefined : 3"
active-color="primary"
@@ -55,12 +51,85 @@
<script lang="ts" setup>
import { useDisplay } from 'vuetify'
import { REPO_ITEMS, ROW_HEIGHT, STATUS_ITEMS } from '@/config/constants'
import { type FilterOptions } from '@/composables/Packages/usePackageFilters'
defineProps<{
filterOptions: FilterOptions
totalPages: number
}>()
import { usePackagesStore } from '@/stores'
import { computed, onMounted, ref, watch } from 'vue'
const { mobile } = useDisplay()
const packagesStore = usePackagesStore()
const page = ref<number>(1)
const pkgbase = ref<string>()
const repo = ref<string>()
const status = ref<{ title: string; value: string }[]>([])
const exact = ref<boolean>()
const totalPages = computed(() => Math.ceil(packagesStore.state.total / packagesStore.state.limit))
const updateFilter = (pageVal?: number) => {
if (packagesStore.state.loading) return
if (pageVal) {
page.value = pageVal
} else {
page.value = 1
}
packagesStore.setFilters(
{
exact: exact.value,
status: status.value?.map((state) => state.value),
pkgbase: pkgbase.value !== null ? pkgbase.value : undefined,
repo: repo.value
},
page.value
)
}
const initFilters = () => {
page.value = packagesStore.state.offset / packagesStore.state.limit + 1
pkgbase.value = packagesStore.state.filters.pkgbase
repo.value = packagesStore.state.filters.repo
exact.value = packagesStore.state.filters.exact
if (packagesStore.state.filters.status) {
for (const state of packagesStore.state.filters.status) {
status.value.push({ title: state.toUpperCase(), value: state.toLowerCase() })
}
}
}
watch(
() => page.value,
(pageVal) => {
packagesStore.goToPage(pageVal)
}
)
// Watcher for pkgbase with debounce
let pkgbaseTimeout: ReturnType<typeof setTimeout> | null = null
watch(
() => pkgbase.value,
() => {
if (pkgbaseTimeout) clearTimeout(pkgbaseTimeout)
pkgbaseTimeout = setTimeout(() => {
updateFilter()
}, 300)
}
)
// Watcher for other filters (repo, status, exact) without debounce
watch(
[() => repo.value, () => status.value?.map((state) => state.value), () => exact.value],
() => {
// Cancel pending pkgbase debounce if any
if (pkgbaseTimeout) {
clearTimeout(pkgbaseTimeout)
pkgbaseTimeout = null
}
updateFilter()
}
)
onMounted(() => {
initFilters()
})
</script>

View File

@@ -31,19 +31,19 @@
</tr>
<template v-else>
<tr
v-for="(pkg, index) in packages"
v-for="(pkg, index) in packagesStore.state.packages"
:key="index"
:style="`background-color: ${getStatusColor(pkg.status)};`">
<td class="font-weight-bold text-no-wrap">
{{ repoName(pkg.repo) }}
<v-chip
:color="getVersionColor(repoVersion(pkg.repo))"
class="ms-2"
class="me-2"
density="comfortable"
label
variant="flat">
{{ repoVersion(pkg.repo) }}
</v-chip>
{{ repoName(pkg.repo) }}
</td>
<td class="text-no-wrap">{{ pkg.pkgbase }}</td>
<td>{{ pkg.status.toLocaleUpperCase() }}</td>
@@ -102,14 +102,11 @@
</template>
<script lang="ts" setup>
import { type Package } from '@/types/Package'
import { usePackageDisplay } from '@/composables/Packages/usePackageDisplay'
defineProps<{
packages: Array<Package>
noResults: boolean
}>()
import { usePackagesStore } from '@/stores'
const { repoName, repoVersion, getVersionColor, getStatusColor, getLto, getDs } =
usePackageDisplay()
const packagesStore = usePackagesStore()
</script>

View File

@@ -1,214 +0,0 @@
import { ref, watch } from 'vue'
import { type Package } from '@/types/Package'
import { useDataStore } from '@/stores/dataStore'
const OFFSET = 50
export interface FilterOptions {
pkgbaseSearch: string
repo?: { title: string; value: string }
statuses: Array<{ title: string; value: string }>
exactSearch: boolean
page: number
}
export interface FilterResult {
packages: Array<Package>
totalPages: number
noResults: boolean
}
export function usePackageFilters() {
const dataStore = useDataStore()
const filterOptions = ref<FilterOptions>({
pkgbaseSearch: '',
repo: undefined,
statuses: [],
exactSearch: false,
page: 1
})
const filterResult = ref<FilterResult>({
packages: [],
totalPages: 1,
noResults: false
})
const applyFilters = () => {
const { pkgbaseSearch, repo, statuses, exactSearch, page } = filterOptions.value
const offset = OFFSET * (page - 1)
let filteredPackages: Package[] = []
if (statuses.length === 1) {
const statusValue = statuses[0].value as Package['status']
const result = dataStore.getPackagesByStatus(statusValue)
filteredPackages = result.packages
} else {
const allPackagesResult = dataStore.getAllPackages()
filteredPackages = allPackagesResult.packages
if (statuses.length > 0) {
const statusValues = statuses.map((status) => status.value)
filteredPackages = filteredPackages.filter((pkg) => statusValues.includes(pkg.status))
}
}
if (repo) {
filteredPackages = filteredPackages.filter((pkg) => pkg.repo === repo.value)
}
if (pkgbaseSearch) {
const searchTerm = pkgbaseSearch.toLowerCase()
filteredPackages = filteredPackages.filter((pkg) =>
exactSearch
? pkg.pkgbase.toLowerCase() === searchTerm
: pkg.pkgbase.toLowerCase().includes(searchTerm)
)
}
if (filteredPackages.length > 0) {
const startIndex = offset
const endIndex = startIndex + OFFSET
filterResult.value = {
packages: filteredPackages.slice(startIndex, endIndex),
totalPages: Math.ceil(filteredPackages.length / OFFSET),
noResults: false
}
} else {
filterResult.value = {
packages: [],
totalPages: 1,
noResults: true
}
}
}
const initFromUrl = () => {
try {
const urlParams = new URLSearchParams(window.location.search)
const defaultOptions: FilterOptions = {
pkgbaseSearch: '',
page: 1,
exactSearch: false,
statuses: [],
repo: undefined
}
let page = 1
if (urlParams.has('page')) {
const pageParam = urlParams.get('page') || '1'
const parsedPage = parseInt(pageParam, 10)
page = !isNaN(parsedPage) && parsedPage > 0 ? parsedPage : 1
}
const statuses = urlParams
.getAll('status')
.filter((status) => typeof status === 'string' && status.trim() !== '')
.map((status) => ({
title: status.toUpperCase(),
value: status
}))
filterOptions.value = {
...defaultOptions,
pkgbaseSearch: urlParams.get('pkgbase') || '',
page,
exactSearch: urlParams.has('exact'),
statuses
}
const repoValue = urlParams.get('repo')
if (repoValue && typeof repoValue === 'string' && repoValue.trim() !== '') {
filterOptions.value.repo = { title: repoValue, value: repoValue }
}
applyFilters()
// Check if page exceeds total pages and adjust if necessary
if (
filterOptions.value.page > filterResult.value.totalPages &&
filterResult.value.totalPages > 0
) {
filterOptions.value.page = filterResult.value.totalPages
applyFilters()
updateUrlParams()
}
} catch (error) {
console.error('Error parsing URL parameters:', error)
// Reset to default state
filterOptions.value = {
pkgbaseSearch: '',
page: 1,
exactSearch: false,
statuses: [],
repo: undefined
}
applyFilters()
}
}
const updateUrlParams = () => {
const { pkgbaseSearch, repo, statuses, exactSearch, page } = filterOptions.value
const params = new URLSearchParams()
// Only add page parameter if it's not the first page
if (page > 1) {
params.set('page', page.toString())
}
if (pkgbaseSearch) {
params.set('pkgbase', pkgbaseSearch.toLowerCase())
}
if (repo) {
params.set('repo', repo.value)
}
if (statuses.length > 0) {
statuses.forEach((status) => {
params.append('status', status.value)
})
}
if (exactSearch) {
params.set('exact', '')
}
const paramsString = params.toString()
if (paramsString) {
window.history.pushState(null, '', `${window.location.pathname}?${paramsString}`)
} else {
window.history.pushState(null, '', window.location.pathname)
}
}
watch(
filterOptions,
() => {
updateUrlParams()
applyFilters()
},
{ deep: true }
)
watch(
() => dataStore,
() => {
if (dataStore) {
applyFilters()
}
},
{ deep: true }
)
return {
filterOptions,
filterResult,
initFromUrl,
applyFilters
}
}

View File

@@ -4,15 +4,15 @@ export const ROW_HEIGHT = 60
// Select Items Constants
export const REPO_ITEMS = [
{ title: 'core-x86-64-v2', value: 'core-x86-64-v2' },
{ title: 'core-x86-64-v3', value: 'core-x86-64-v3' },
{ title: 'core-x86-64-v4', value: 'core-x86-64-v4' },
{ title: 'extra-x86-64-v2', value: 'extra-x86-64-v2' },
{ title: 'extra-x86-64-v3', value: 'extra-x86-64-v3' },
{ title: 'extra-x86-64-v4', value: 'extra-x86-64-v4' },
{ title: 'multilib-x86-64-v2', value: 'multilib-x86-64-v2' },
{ title: 'multilib-x86-64-v3', value: 'multilib-x86-64-v3' },
{ title: 'multilib-x86-64-v4', value: 'multilib-x86-64-v4' }
'core-x86-64-v2',
'core-x86-64-v3',
'core-x86-64-v4',
'extra-x86-64-v2',
'extra-x86-64-v3',
'extra-x86-64-v4',
'multilib-x86-64-v2',
'multilib-x86-64-v3',
'multilib-x86-64-v4'
]
export const STATUS_ITEMS = [

View File

@@ -0,0 +1,273 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/packages": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Retrieve package information
* @description Fetch packages from the ALHP system. You can filter results using query parameters.
*/
get: operations["getPackages"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/stats": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Retrieve general build statistics */
get: operations["getStats"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
Package: {
/**
* @description The base name of the package
* @example linux-zen
*/
pkgbase?: string;
/**
* @description Repository from which the package originates
* @example extra-x86-64-v4
*/
repo?: string;
/**
* @description List of package names split from the base package
* @example [
* "linux-zen",
* "linux-zen-headers",
* "linux-zen-docs"
* ]
*/
split_packages?: string[];
/**
* @description Current build or repository status of the package
* @example latest
* @enum {string}
*/
status?: "latest" | "failed" | "built" | "skipped" | "delayed" | "building" | "signing" | "unknown" | "queued";
/**
* @description Reason for skipping the package build (if any)
* @example blacklisted
*/
skip_reason?: string;
/**
* @description Link Time Optimization (LTO) status for the package
* @example enabled
* @enum {string}
*/
lto?: "enabled" | "unknown" | "disabled" | "auto_disabled";
/**
* @description Availability of debug symbols in the package
* @example available
* @enum {string}
*/
debug_symbols?: "available" | "unknown" | "not_available";
/**
* @description Version available in the official Arch Linux repositories
* @example 1.3.4-2
*/
arch_version?: string;
/**
* @description Version available in ALHP repositories (may be empty if not built)
* @example 1.3.4-2.1
*/
repo_version?: string;
/**
* @description Date and time when the package was built (RFC1123 format)
* @example Fri, 15 Dec 2023 03:43:11 UTC
*/
build_date?: string;
/**
* @description Peak memory usage during the build process (human-readable format)
* @example 5 GB
*/
peak_mem?: string;
};
/** @description Aggregated statistics across all packages */
Stats: {
/**
* Format: int64
* @description Number of packages that failed to build
* @example 17
*/
failed?: number;
/**
* Format: int64
* @description Number of packages that were skipped
* @example 29
*/
skipped?: number;
/**
* Format: int64
* @description Number of packages that are up-to-date
* @example 743
*/
latest?: number;
/**
* Format: int64
* @description Number of packages currently in the build queue
* @example 5
*/
queued?: number;
/**
* Format: int64
* @description Number of packages currently building
* @example 11
*/
building?: number;
/**
* Format: int64
* @description Latest mirror timestamp to detect outdated mirrors (Unix timestamp)
* @example 1702612991
*/
last_mirror_timestamp?: number;
/** @description LTO status summary across all packages */
lto?: {
/**
* Format: int64
* @description Number of packages with LTO enabled
* @example 532
*/
enabled?: number;
/**
* Format: int64
* @description Number of packages with LTO explicitly disabled
* @example 203
*/
disabled?: number;
/**
* Format: int64
* @description Number of packages with unknown LTO status
* @example 11
*/
unknown?: number;
};
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
getPackages: {
parameters: {
query: {
/** @description Filter by package status. Accepts multiple values via repeated parameters. */
status?: ("latest" | "failed" | "built" | "skipped" | "delayed" | "building" | "signing" | "unknown" | "queued")[];
/** @description Filter by the base package name (`pkgbase`). This is often the main identifier of a package group. */
pkgbase?: string;
/** @description If present, matches the `pkgbase` exactly. If not provided, allows partial matches. */
exact?: boolean;
/** @description Filter by repository name. */
repo?: string;
/** @description Number of results to skip (for pagination). */
offset: number;
/** @description Maximum number of results to return. */
limit: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Package data retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
packages?: components["schemas"]["Package"][];
/**
* Format: int64
* @description Total number of matching packages
* @example 1423
*/
total?: number;
/**
* Format: int64
* @example 0
*/
offset?: number;
/**
* Format: int64
* @example 25
*/
limit?: number;
};
};
};
/** @description No packages found matching the specified filters */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Internal server error occurred */
500: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getStats: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description General statistics retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Stats"];
};
};
/** @description Internal server error occurred */
500: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
}

View File

@@ -1,258 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { dataStore } from '@/types/dataStore'
import { apiClient } from '@/utils/fetchUtils'
import { Package } from '@/types/Package'
import { Stats } from '@/types/Stats'
const UpdateInterval = Number(import.meta.env.VITE_UPDATE_INTERVAL) || 5
export const useDataStore = defineStore('data', () => {
const data = ref<dataStore>()
const loading = ref(false)
const error = ref<Error | null>(null)
let refreshInterval: NodeJS.Timeout | null = null
const init = () => {
data.value = {
lastUpdated: Date.now(),
packages: {
offset: 0,
limit: Number(import.meta.env.VITE_OFFSET) || 50,
totalPackages: [],
byStatus: {
latest: [],
built: [],
failed: [],
skipped: [],
delayed: [],
queued: [],
building: [],
signing: [],
unknown: []
},
byRepo: {
core: {
v2: [],
v3: [],
v4: []
},
extra: {
v2: [],
v3: [],
v4: []
},
multilib: {
v2: [],
v3: [],
v4: []
}
}
},
stats: {
total: 0,
built: 0,
building: 0,
latest: 0,
queued: 0,
skipped: 0,
failed: 0,
lto: {
enabled: 0,
disabled: 0,
unknown: 0
}
}
}
}
// Fetch all package data from the API
const fetchAllPackageData = async () => {
loading.value = true
error.value = null
try {
const [totalStats, totalPackages] = await Promise.allSettled([
apiClient.getStats(),
apiClient.getPackages({ limit: 0, offset: 0 })
])
// Combine into a single data structure
if (
totalPackages.status === 'fulfilled' &&
totalStats.status === 'fulfilled' &&
totalPackages.value &&
totalStats.value
) {
// Initialize if needed
if (!data.value) init()
// Update with the fetched data
data.value!.lastUpdated = Date.now()
data.value!.packages.totalPackages = [...totalPackages.value.packages]
data.value!.packages.offset = totalPackages.value.offset
data.value!.packages.limit = totalPackages.value.limit
// Update byStatus categorization
data.value!.packages.byStatus = {
latest: totalPackages.value.packages.filter((pkg) => pkg.status === 'latest') || [],
built: totalPackages.value.packages.filter((pkg) => pkg.status === 'built') || [],
failed: totalPackages.value.packages.filter((pkg) => pkg.status === 'failed') || [],
skipped: totalPackages.value.packages.filter((pkg) => pkg.status === 'skipped') || [],
delayed: totalPackages.value.packages.filter((pkg) => pkg.status === 'delayed') || [],
queued: totalPackages.value.packages.filter((pkg) => pkg.status === 'queued') || [],
building: totalPackages.value.packages.filter((pkg) => pkg.status === 'building') || [],
signing: totalPackages.value.packages.filter((pkg) => pkg.status === 'signing') || [],
unknown: totalPackages.value.packages.filter((pkg) => pkg.status === 'unknown') || []
}
// Update byRepo categorization
data.value!.packages.byRepo = {
core: {
v2: totalPackages.value.packages.filter((pkg) => pkg.repo === 'core-x86-64-v2') || [],
v3: totalPackages.value.packages.filter((pkg) => pkg.repo === 'core-x86-64-v3') || [],
v4: totalPackages.value.packages.filter((pkg) => pkg.repo === 'core-x86-64-v4') || []
},
extra: {
v2: totalPackages.value.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v2') || [],
v3: totalPackages.value.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v3') || [],
v4: totalPackages.value.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v4') || []
},
multilib: {
v2:
totalPackages.value.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v2') || [],
v3:
totalPackages.value.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v3') || [],
v4:
totalPackages.value.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v4') || []
}
}
// Update stats
data.value!.stats = {
...totalStats.value,
total: totalPackages.value.total, // Total count from API
built: data.value!.packages.byStatus.built.length,
building: data.value!.packages.byStatus.building.length
}
}
} catch (e) {
console.error('Error fetching package data:', e)
error.value = e as Error
} finally {
loading.value = false
}
}
// Fetch a specific page of packages
const fetchPackagePage = async (offset: number, limit: number) => {
loading.value = true
error.value = null
try {
const packageData = await apiClient.getPackages({ limit, offset })
if (packageData && data.value) {
data.value.packages.totalPackages = [...packageData.packages]
data.value.packages.offset = offset
data.value.packages.limit = limit
// Update byStatus categorization
data.value.packages.byStatus = {
latest: packageData.packages.filter((pkg) => pkg.status === 'latest') || [],
built: packageData.packages.filter((pkg) => pkg.status === 'built') || [],
failed: packageData.packages.filter((pkg) => pkg.status === 'failed') || [],
skipped: packageData.packages.filter((pkg) => pkg.status === 'skipped') || [],
delayed: packageData.packages.filter((pkg) => pkg.status === 'delayed') || [],
queued: packageData.packages.filter((pkg) => pkg.status === 'queued') || [],
building: packageData.packages.filter((pkg) => pkg.status === 'building') || [],
signing: packageData.packages.filter((pkg) => pkg.status === 'signing') || [],
unknown: packageData.packages.filter((pkg) => pkg.status === 'unknown') || []
}
// Update byRepo categorization
data.value.packages.byRepo = {
core: {
v2: packageData.packages.filter((pkg) => pkg.repo === 'core-x86-64-v2') || [],
v3: packageData.packages.filter((pkg) => pkg.repo === 'core-x86-64-v3') || [],
v4: packageData.packages.filter((pkg) => pkg.repo === 'core-x86-64-v4') || []
},
extra: {
v2: packageData.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v2') || [],
v3: packageData.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v3') || [],
v4: packageData.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v4') || []
},
multilib: {
v2: packageData.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v2') || [],
v3: packageData.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v3') || [],
v4: packageData.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v4') || []
}
}
}
} catch (e) {
console.error('Error fetching page data:', e)
error.value = e as Error
} finally {
loading.value = false
}
}
const startAutoRefresh = async () => {
stopAutoRefresh()
init()
await fetchAllPackageData()
refreshInterval = setInterval(fetchAllPackageData, UpdateInterval * 60 * 1000)
}
const stopAutoRefresh = () => {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
}
}
const getAllPackages = (): { packages: Package[]; total: number } => {
if (!data.value) return { packages: [], total: 0 }
return {
packages: data.value.packages.totalPackages || [],
total: data.value.stats.total || 0
}
}
const getPackagesByStatus = (
status: Package['status']
): { packages: Array<Package>; total: number } => {
if (!data.value) return { packages: [], total: 0 }
return {
packages: data.value.packages.byStatus[status],
total:
status in data.value.stats
? typeof data.value.stats[status as keyof typeof data.value.stats] === 'number'
? (data.value.stats[status as keyof typeof data.value.stats] as number)
: 0
: 0
}
}
const getPackageStats = ():
| (Stats & { total: number; built: number; building: number })
| null => {
return data.value?.stats ?? null
}
return {
data,
loading,
error,
init,
fetchAllPackageData,
fetchPackagePage,
startAutoRefresh,
stopAutoRefresh,
getAllPackages,
getPackageStats,
getPackagesByStatus
}
})

View File

@@ -0,0 +1,6 @@
// Export all stores
export { useStatsStore } from './statsStore'
export { usePackagesStore } from './packagesStore'
// Export types
export type { PackageFilters, PaginationOptions } from './packagesStore'

View File

@@ -0,0 +1,212 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { components, getPackages } from '@/api'
export interface PackageFilters {
status?: components['schemas']['Package']['status'][]
pkgbase?: components['schemas']['Package']['pkgbase']
exact?: boolean | undefined
repo?: components['schemas']['Package']['repo']
}
export const usePackagesStore = defineStore('packages', () => {
const state = reactive({
packages: [] as components['schemas']['Package'][],
currentlyBuildingPackages: [] as components['schemas']['Package'][],
total: 0,
offset: 0,
limit: Number(import.meta.env.VITE_LIMIT) || 50,
loading: false,
error: null as string | null,
lastUpdated: Date.now(),
filters: {
status: undefined as components['schemas']['Package']['status'][] | undefined,
pkgbase: undefined as components['schemas']['Package']['pkgbase'] | undefined,
exact: undefined as boolean | undefined,
repo: undefined as components['schemas']['Package']['repo'] | undefined
} as PackageFilters
})
// Actions
const fetchPackages = (init = false) => {
state.loading = true
state.error = null
if (init) {
initFromUrl()
}
const filter = {}
if (state.filters.status && state.filters.status.length > 0) {
filter['status'] = state.filters.status
}
if (state.filters.pkgbase) {
filter['pkgbase'] = state.filters.pkgbase
}
if (state.filters.exact === true) {
filter['exact'] = ''
}
if (state.filters.repo) {
filter['repo'] = state.filters.repo
}
getPackages({
limit: state.limit,
offset: state.offset,
...filter
})
.then((response) => {
if (!response) throw new Error('No response from API')
state.packages = response.packages || []
state.total = response.total || 0
state.offset = response.offset || 0
state.limit = response.limit || state.limit
})
.catch((err) => {
if (err.statusCode === 404) {
state.packages = []
state.total = 0
state.offset = 0
state.limit = Number(import.meta.env.VITE_LIMIT) || 50
} else {
state.error = err instanceof Error ? err.message : 'Failed to fetch packages'
console.error('Error fetching packages:', err)
}
})
.finally(() => {
state.loading = false
})
}
const fetchCurrentlyBuilding = () => {
getPackages({
limit: 0,
offset: 0,
status: ['queued', 'building', 'built']
})
.then((response) => {
state.currentlyBuildingPackages = response?.packages || []
})
.catch((err) => {
if (err.statusCode === 404) {
state.currentlyBuildingPackages = []
} else {
state.error =
err instanceof Error ? err.message : 'Failed to fetch currently building packages'
console.error('Error fetching queued packages:', err)
}
})
.finally(() => {
state.loading = false
})
}
const goToPage = (page: number) => {
state.offset = (page - 1) * state.limit
updateUrlParams()
fetchPackages()
}
const setFilters = (newFilters: PackageFilters, page?: number) => {
state.filters = JSON.parse(JSON.stringify(newFilters))
if (state.filters.exact === false) {
state.filters.exact = undefined
}
if (page) {
state.offset = (page - 1) * state.limit
}
updateUrlParams()
fetchPackages()
}
const resetFilters = () => {
state.filters = {
status: undefined,
pkgbase: undefined,
exact: undefined,
repo: undefined
}
state.offset = 0
state.limit = Number(import.meta.env.VITE_LIMIT) || 50
updateUrlParams()
fetchPackages()
}
const updateUrlParams = () => {
const params = new URLSearchParams()
let page = state.offset / state.limit + 1
// Only add page parameter if it's not the first page
if (page > 1) {
params.set('page', page.toString())
}
if (state.filters.status && state.filters.status.length > 0) {
state.filters.status.forEach((status) => {
params.append('status', status)
})
}
if (state.filters.pkgbase) {
params.set('pkgbase', state.filters.pkgbase.toLowerCase())
}
if (state.filters.repo) {
params.set('repo', state.filters.repo)
}
if (state.filters.exact === true) {
params.set('exact', '')
} else {
params.delete('exact')
}
const paramsString = params.toString()
if (paramsString) {
window.history.pushState(null, '', `${window.location.pathname}?${paramsString}`)
} else {
window.history.pushState(null, '', window.location.pathname)
}
}
const initFromUrl = () => {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has('page')) {
const pageParam = urlParams.get('page') || '1'
const parsedPage = parseInt(pageParam, 10)
const page = !isNaN(parsedPage) && parsedPage > 0 ? parsedPage : 1
state.offset = (page - 1) * state.limitq
}
if (urlParams.has('status')) {
state.filters.status = urlParams.getAll('status')
}
if (urlParams.has('pkgbase')) {
state.filters.pkgbase = urlParams.get('pkgbase')
}
if (urlParams.has('repo')) {
state.filters.repo = urlParams.get('repo')
}
if (urlParams.has('exact')) {
state.filters.exact = true
}
}
return {
state,
// Actions
fetchPackages,
fetchCurrentlyBuilding,
goToPage,
setFilters,
resetFilters
}
})

View File

@@ -0,0 +1,37 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { components, getStats } from '@/api'
export const useStatsStore = defineStore('stats', () => {
const state = reactive({
stats: {} as components['schemas']['Stats'] | undefined | null,
loading: false,
error: null as string | null,
lastUpdated: Date.now()
})
// Actions
const fetchStats = () => {
state.loading = true
state.error = null
getStats()
.then((response) => {
state.stats = response
})
.catch((err) => {
state.error = err instanceof Error ? err.message : 'Failed to fetch packages'
console.error('Error fetching packages:', err)
})
.finally(() => {
state.loading = false
})
}
return {
state,
// Actions
fetchStats
}
})

View File

@@ -1,22 +0,0 @@
export interface Package {
pkgbase: string
repo: string
split_packages: Array<string>
status:
| 'latest'
| 'failed'
| 'built'
| 'skipped'
| 'delayed'
| 'building'
| 'signing'
| 'unknown'
| 'queued'
skip_reason: string
lto: 'enabled' | 'unknown' | 'disabled' | 'auto_disabled'
debug_symbols: 'available' | 'unknown' | 'not_available'
arch_version: string
repo_version: string
build_date: string
peak_mem: any
}

View File

@@ -1,8 +0,0 @@
import { type Package } from '@/types/Package'
export interface Packages {
packages: Array<Package>
total: number
offset: number
limit: number
}

View File

@@ -1,9 +0,0 @@
import { type Stats_Lto } from '@/types/Stats_Lto'
export interface Stats {
latest: number
queued: number
skipped: number
failed: number
lto: Stats_Lto
}

View File

@@ -1,5 +0,0 @@
export interface Stats_Lto {
enabled: number
disabled: number
unknown: number
}

View File

@@ -1,44 +0,0 @@
import { Stats } from '@/types/Stats'
import { Package } from '@/types/Package'
export interface dataStore {
lastUpdated: number
packages: {
offset: number
limit: number
totalPackages: Array<Package>
byStatus: {
latest: Array<Package>
built: Array<Package>
failed: Array<Package>
skipped: Array<Package>
delayed: Array<Package>
queued: Array<Package>
building: Array<Package>
signing: Array<Package>
unknown: Array<Package>
}
byRepo: {
core: {
v2: Array<Package>
v3: Array<Package>
v4: Array<Package>
}
extra: {
v2: Array<Package>
v3: Array<Package>
v4: Array<Package>
}
multilib: {
v2: Array<Package>
v3: Array<Package>
v4: Array<Package>
}
}
}
stats: Stats & {
total: number
built: number
building: number
}
}

View File

@@ -1,50 +0,0 @@
import { Packages } from '@/types/Packages'
import { Stats } from '@/types/Stats'
export class ApiClient {
private readonly baseUrl: string
constructor() {
this.baseUrl = import.meta.env.VITE_BASE_URL
}
async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error(`Error fetching from ${url}:`, error)
throw error // Re-throw to allow handling by caller
}
}
async getPackages(
params: {
limit?: string | number
offset?: string | number
status?: string
} = {}
): Promise<Packages> {
const { limit = 0, offset = 0, status } = params
let endpoint = `/packages?limit=${limit}&offset=${offset}`
if (status) {
endpoint += `&status=${status}`
}
return this.fetch<Packages>(endpoint)
}
async getStats(): Promise<Stats> {
return this.fetch<Stats>('/stats')
}
}
export const apiClient = new ApiClient()