Compare commits

...

9 Commits

Author SHA1 Message Date
ad5ce609fc Improve URL parameter handling and add error handling in filters
Refactored the `initFromUrl` function to handle edge cases, including invalid or malformed URL parameters, and added a fallback to default filter options in case of errors. Improved validation for `page`, `status`, and `repo` parameters, ensuring robust behavior and logging errors for debugging.
2025-04-14 21:58:54 +02:00
c458b564ce Update dependencies and fix watch logic in usePackageFilters
Adjusted the watcher in `usePackageFilters` to track the correct reactive object and call `applyFilters` when necessary. Updated various dependencies in `yarn.lock` to their latest versions for compatibility and improved functionality.
2025-04-14 21:52:13 +02:00
bcb9569b26 Refactor package filtering and display components
Replaced inline package filtering and table logic with dedicated reusable components: `PackageFilters` and `PackageTable`. Introduced a composable (`usePackageFilters`) for managing filtering logic and state, and optimized display logic with a new `usePackageDisplay` composable. This improves maintainability, readability, and separation of concerns.
2025-04-14 21:43:18 +02:00
90be95afda Refactor CurrentlyBuilding to use centralized data store
Replaced local state and fetching logic with a Vuex-like data store for better state management. Updated import paths, removed redundant code, and ensured reactive updates using computed properties and watchers. This enhances maintainability and reduces code duplication.
2025-04-14 21:42:52 +02:00
c864664536 Refactor iframe height computation for responsiveness
Replaces the hard-coded iframe height logic with a computed property to dynamically adjust height based on screen width. This simplifies the code and improves maintainability while ensuring proper responsive design behavior.
2025-04-14 21:41:55 +02:00
9762505a24 Refactor BuildStats and MainNav components
Modularize BuildStats by splitting it into smaller components (StatsListSection, StatItem) for better code reusability and readability. Update MainNav to reflect this restructuring and improve the handling of dynamic styles, computed properties, and data binding.
2025-04-14 21:41:28 +02:00
9adeaa4483 Update UI to handle loading and error states for data fetch
Introduced conditional UI components to display loading and error messages during data fetching. Integrated the data store to manage state and added lifecycle hooks for starting and stopping auto-refresh. These changes improve user experience and ensure better feedback during data operations.
2025-04-14 21:39:20 +02:00
43ce135fc6 Add Pinia for state management and API data handling.
Introduced Pinia as the state management library and integrated it with the app. Implemented an `ApiClient` utility and a `dataStore` to fetch, categorize, and manage packages and stats data. Updated application entry to include Pinia and adjusted dependencies in `package.json`.
2025-04-14 21:38:48 +02:00
4e722e5e60 Add type definitions and example .env file for environment vars
Introduce `env.d.ts` to define types for VITE environment variables, ensuring better type safety and development experience. Provide a sample `.env.example` file to guide configuration setup.
2025-04-14 21:37:32 +02:00
24 changed files with 1358 additions and 721 deletions

3
frontend/.env.example Normal file
View File

@@ -0,0 +1,3 @@
VITE_BASE_URL="API_URL"
VITE_UPDATE_INTERVAL=5
VITE_LIMIT=50

View File

@@ -9,10 +9,14 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BuildServerStats: typeof import('./src/components/BuildServerStats.vue')['default']
BuildStats: typeof import('./src/components/BuildStats.vue')['default']
BuildStats: typeof import('./src/components/MainNav/BuildStats.vue')['default']
CurrentlyBuilding: typeof import('./src/components/CurrentlyBuilding.vue')['default']
MainNav: typeof import('./src/components/MainNav.vue')['default']
PackageFilters: typeof import('./src/components/Packages/PackageFilters.vue')['default']
Packages: typeof import('./src/components/Packages.vue')['default']
QueuedPackagesList: typeof import('./src/components/QueuedPackagesList.vue')['default']
PackageTable: typeof import('./src/components/Packages/PackageTable.vue')['default']
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']
}
}

13
frontend/env.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_BASE_URL: string
readonly VITE_UPDATE_INTERVAL: number
readonly VITE_LIMIT: number
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare module '@/*'

View File

@@ -10,21 +10,22 @@
"@fontsource/roboto": "^5.2.5",
"@mdi/font": "7.4.47",
"fork-awesome": "^1.2.0",
"pinia": "^3.0.2",
"roboto-fontface": "^0.10.0",
"vue": "^3.5.13",
"vuetify": "^3.7.18"
"vuetify": "^3.8.1"
},
"devDependencies": {
"@babel/types": "^7.26.10",
"@types/node": "^22.13.11",
"@babel/types": "^7.27.0",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
"prettier": "^3.5.3",
"sass": "^1.86.0",
"typescript": "^5.8.2",
"sass": "^1.86.3",
"typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"vite-plugin-vuetify": "^2.1.0",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8"
},
"packageManager": "yarn@4.7.0"

View File

@@ -4,9 +4,19 @@
<main-nav />
<v-main>
<build-server-stats />
<currently-building />
<packages />
<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>
</v-main>
</v-container>
</v-app>
@@ -15,6 +25,25 @@
<script lang="ts" setup>
import MainNav from '@/components/MainNav.vue'
import BuildServerStats from '@/components/BuildServerStats.vue'
import Packages from '@/components/Packages.vue'
import CurrentlyBuilding from '@/components/CurrentlyBuilding.vue'
import Packages from '@/components/Packages.vue'
import { useDataStore } from '@/stores/dataStore'
import { computed, onMounted, onUnmounted } from 'vue'
const dataStore = useDataStore()
const isLoading = computed(() => dataStore.loading)
const error = computed(() => dataStore.error)
const lastUpdated = computed(() => {
if (!dataStore.data) return null
return dataStore.data.lastUpdated
})
onMounted(async () => {
await dataStore.startAutoRefresh()
})
onUnmounted(() => {
dataStore.stopAutoRefresh()
})
</script>

View File

@@ -2,7 +2,7 @@
<v-sheet class="mt-2" color="transparent">
<h5 class="text-h5">Buildserver Stats</h5>
<iframe
:height="width <= 800 ? `${IFRAME_HEIGHT}px` : '420px'"
:height="iframeHeight"
allowtransparency="true"
class="w-100 border-0"
src="https://stats.itsh.dev/public-dashboards/0fb04abb0c5e4b7390cf26a98e6dead1"></iframe>
@@ -11,40 +11,16 @@
<script lang="ts" setup>
import { useDisplay } from 'vuetify'
import { computed } from 'vue'
const { width } = useDisplay()
/**
* Represents the height of a graph in pixels.
* This constant is used to define the vertical dimension of the graph
* when rendering it in a user interface or graphical representation.
*
* @constant {number} GRAPH_HEIGHT
*/
// Configuration constants
const GRAPH_HEIGHT = 335
/**
* Represents the total number of graphs to be processed or displayed.
*
* This variable is used to define the fixed amount of graphs that the system
* is intended to handle at a given time. It can be leveraged to set limits
* on iterations, arrays, or display logic related to graph management.
*
* Default value: 4
*
* Type: number
*/
const NUMBER_OF_GRAPHS = 4
/**
* Represents the height of an iframe calculated dynamically based on the number of graphs
* and the height of each graph.
*
* IFRAME_HEIGHT is determined by multiplying the number of graphs (`NUMBER_OF_GRAPHS`)
* displayed in the iframe by the height of a single graph (`GRAPH_HEIGHT`).
*
* This variable is commonly used to ensure the iframe adjusts correctly to accommodate
* all graphs without introducing unnecessary scrollbars.
*
* @constant {number} IFRAME_HEIGHT
*/
const IFRAME_HEIGHT = NUMBER_OF_GRAPHS * GRAPH_HEIGHT
// Computed property for responsive iframe height
const iframeHeight = computed(() =>
width.value <= 800 ? `${NUMBER_OF_GRAPHS * GRAPH_HEIGHT}px` : '420px'
)
</script>

View File

@@ -1,102 +0,0 @@
<template>
<v-sheet :style="sheetStyles" class="d-flex" color="transparent">
<!-- Dynamically render the "Stats" section -->
<v-list :style="listStyles" bg-color="transparent" class="d-flex">
<v-list-subheader>Stats:</v-list-subheader>
<v-list-item
v-for="item in statsList"
:key="item.label"
:style="{ color: item.color }"
:title="item.label">
{{ item.value }}
</v-list-item>
</v-list>
<!-- Dynamically render the "LTO" section -->
<v-list :style="listStyles" bg-color="transparent" class="d-flex">
<v-list-subheader>LTO:</v-list-subheader>
<v-list-item
v-for="item in ltoList"
:key="item.label"
:style="{ color: item.color }"
:title="item.label">
{{ item.value }}
</v-list-item>
</v-list>
</v-sheet>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { type Stats } from '@/types/Stats'
// Define reusable styles and colors
const sheetStyles = { gap: '50px' }
const listStyles = { borderRadius: '5px' }
const colors = {
latest: '#069b35',
queued: '#b97808',
skipped: '#878787',
failed: '#b30303',
enabled: '#069b35',
disabled: '#b30303',
unknown: '#878787'
}
// Reactive stats object
const stats = ref<Stats>({
latest: 0,
queued: 0,
skipped: 0,
failed: 0,
lto: {
enabled: 0,
disabled: 0,
unknown: 0
}
})
// Map stats and LTO data for simpler rendering in the template
const statsList = ref([
{ label: 'latest', value: stats.value.latest, color: colors.latest },
{ label: 'queued', value: stats.value.queued, color: colors.queued },
{ label: 'skipped', value: stats.value.skipped, color: colors.skipped },
{ label: 'failed', value: stats.value.failed, color: colors.failed }
])
const ltoList = ref([
{ label: 'enabled', value: stats.value.lto.enabled, color: colors.enabled },
{ label: 'disabled', value: stats.value.lto.disabled, color: colors.disabled },
{ label: 'unknown', value: stats.value.lto.unknown, color: colors.unknown }
])
// Function to fetch stats data (refactored with async/await)
const getStats = async (): Promise<void> => {
try {
const response = await fetch('https://api.alhp.dev/stats')
if (!response.ok) throw new Error(response.statusText)
const statistics = await response.json()
stats.value = statistics
// Update lists with fresh data
statsList.value = [
{ label: 'latest', value: stats.value.latest, color: colors.latest },
{ label: 'queued', value: stats.value.queued, color: colors.queued },
{ label: 'skipped', value: stats.value.skipped, color: colors.skipped },
{ label: 'failed', value: stats.value.failed, color: colors.failed }
]
ltoList.value = [
{ label: 'enabled', value: stats.value.lto.enabled, color: colors.enabled },
{ label: 'disabled', value: stats.value.lto.disabled, color: colors.disabled },
{ label: 'unknown', value: stats.value.lto.unknown, color: colors.unknown }
]
} catch (error) {
console.error('Failed to fetch stats:', error)
}
}
// Fetch stats on mount
onMounted(() => {
getStats()
})
</script>

View File

@@ -74,138 +74,84 @@
</v-list>
<v-sheet class="ps-4" color="transparent" rounded width="100%">
<h4 class="mb-2 font-weight-light text-grey">Queued</h4>
<QueuedPackagesList :packages="packages.queued" />
<queued-packages-list :packages="packages.queued" />
</v-sheet>
</v-card-text>
</v-card>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import type { Packages } from '@/types/Packages'
import { Package } from '@/types/Package'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useDisplay } from 'vuetify'
import QueuedPackagesList from './QueuedPackagesList.vue'
const BASE_URL = 'https://api.alhp.dev/packages'
const UPDATE_INTERVAL_MINUTES = 5
import { useDataStore } from '@/stores/dataStore'
import QueuedPackagesList from '@/components/CurrentlyBuilding/QueuedPackagesList.vue'
const { mobile } = useDisplay()
const lastUpdatedTime = ref(0)
const dataStore = useDataStore()
const lastUpdatedSeconds = ref(0)
const updateFailed = ref(false)
const rtf = new Intl.RelativeTimeFormat('en', {
localeMatcher: 'best fit',
numeric: 'always',
style: 'long'
})
const packageCount = ref({
total: 0,
building: 0,
built: 0,
queued: 0
})
// Computed properties to access store data
const updateFailed = computed(() => !!dataStore.error)
const packages = ref<{
built: Array<Package>
building: Array<Package>
queued: Array<Package>
}>({
built: [],
building: [],
queued: []
})
let timerInterval: NodeJS.Timeout // Declare timerInterval outside the function
const fetchPackages = async (offset: string, status?: string) => {
let url = `${BASE_URL}?limit=0&offset=${offset}`
if (status) {
url += `&status=${status}`
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
}
})
try {
const response = await fetch(url, { method: 'GET' })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
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 json: Packages = await response.json()
lastUpdatedTime.value = Date.now()
lastUpdatedSeconds.value = 0 // Reset seconds counter
updateFailed.value = false
return json
} catch (error) {
console.error('Error fetching packages:', error)
updateFailed.value = true
return null
}
}
const getTotalPackages = async () => {
const json = await fetchPackages('1')
if (json) {
packageCount.value.total = json.total
}
}
const getBuiltPackages = async () => {
const json = await fetchPackages('0', 'built')
if (json) {
packageCount.value.built = json.total
packages.value.built = json.packages
}
}
const getBuildingPackages = async () => {
const json = await fetchPackages('0', 'building')
if (json) {
packageCount.value.building = json.total
packages.value.building = json.packages
}
}
const getQueuedPackages = async () => {
const json = await fetchPackages('0', 'queued')
if (json) {
packageCount.value.queued = json.total
packages.value.queued = json.packages
}
}
// Function to start the timer
const startTimer = () => {
stopTimer() // Clear any existing timer
getTotalPackages()
getBuiltPackages()
getBuildingPackages()
getQueuedPackages()
timerInterval = setInterval(
() => {
getTotalPackages()
getBuiltPackages()
getBuildingPackages()
getQueuedPackages()
},
UPDATE_INTERVAL_MINUTES * 60 * 1000
)
}
// Function to stop the timer
const stopTimer = () => {
clearInterval(timerInterval)
}
)
onMounted(() => {
startTimer() // Start the timer when the component is mounted
startLastUpdatedTimer()
})
// Update seconds counter every second
setInterval(() => {
lastUpdatedSeconds.value++
}, 1000)
onUnmounted(() => {
if (updateTimer) {
clearInterval(updateTimer)
}
})
</script>
<style lang="scss">

View File

@@ -1,11 +1,11 @@
<template>
<v-app-bar color="#191c2a" style="background: #0d1538">
<v-container :class="width < 1440 ? 'ms-3' : 'mx-auto'" fluid style="max-width: 1440px">
<v-app-bar :color="appBarColors.background">
<v-container :class="containerClasses" :style="{ maxWidth: maxContainerWidth }" fluid>
<v-row>
<v-app-bar-title style="align-self: center">
<span style="font-size: 20px">
ALHP Status
<a class="ms-2 gitea-link" href="https://somegit.dev/ALHP/ALHP.GO" target="_blank">
<v-app-bar-title class="app-title">
<span>
{{ appTitle }}
<a :href="repoUrl" class="ms-2 gitea-link" target="_blank">
<i class="fa fa-gitea"></i>
</a>
</span>
@@ -18,21 +18,40 @@
</template>
<script lang="ts" setup>
import BuildStats from '@/components/BuildStats.vue'
import BuildStats from '@/components/MainNav/BuildStats.vue'
import { useDisplay } from 'vuetify'
import { computed } from 'vue'
const { mobile, width } = useDisplay()
const appTitle = 'ALHP Status'
const repoUrl = 'https://somegit.dev/ALHP/ALHP.GO'
const maxContainerWidth = '1440px'
const appBarColors = {
background: '#0d1538',
accent: '#609926'
}
const containerClasses = computed(() => ({
'ms-3': width.value < 1440,
'mx-auto': width.value >= 1440
}))
</script>
<style scoped>
.app-title {
align-self: center;
font-size: 20px;
}
.gitea-link {
color: white;
font-size: 25px;
text-decoration: none; /* Optional: To prevent underlining */
transition: color 0.2s; /* Smooth color transition */
text-decoration: none;
transition: color 0.2s;
}
.gitea-link:hover {
color: #609926;
color: v-bind('appBarColors.accent');
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<v-sheet :style="sheetStyles" class="d-flex" color="transparent">
<StatsListSection title="Stats">
<StatItem
v-for="(value, key) in generalStats"
:key="key"
:color="value.color"
:count="value.count"
:title="key" />
</StatsListSection>
<StatsListSection title="LTO">
<StatItem
v-for="(value, key) in customStats.lto"
:key="key"
:color="value.color"
:count="value.count"
:title="key" />
</StatsListSection>
</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 StatsListSection from '@/components/MainNav/BuildStats/StatsListSection.vue'
import StatItem from '@/components/MainNav/BuildStats/StatItem.vue'
interface CustomStatItem {
count: number
color: string
}
interface CustomStats {
latest: CustomStatItem
queued: CustomStatItem
building: CustomStatItem
skipped: CustomStatItem
failed: CustomStatItem
lto: {
enabled: CustomStatItem
disabled: CustomStatItem
unknown: CustomStatItem
}
}
const COLORS = {
SUCCESS: '#069b35',
WARNING: '#b97808',
ERROR: '#b30303',
NEUTRAL: '#878787'
}
const sheetStyles = { gap: '50px' }
const dataStore = useDataStore()
const stats = ref<dataStore['stats']>()
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(() => {
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()
}
}
)
onMounted(updateStats)
</script>

View File

@@ -0,0 +1,13 @@
<template>
<v-list-item :style="{ color }" :title="title">
{{ count }}
</v-list-item>
</template>
<script lang="ts" setup>
defineProps<{
title: string
count: number
color: string
}>()
</script>

View File

@@ -0,0 +1,15 @@
<template>
<v-list :style="listStyles" bg-color="transparent" class="d-flex">
<v-list-subheader>{{ title }}:</v-list-subheader>
<slot></slot>
</v-list>
</template>
<script lang="ts" setup>
defineProps<{
title: string
}>()
// Reusable styles
const listStyles = { borderRadius: '5px' }
</script>

View File

@@ -1,398 +1,23 @@
<template>
<v-sheet :color="TRANSPARENT_COLOR" class="mt-6" width="100%">
<h5 class="text-h5 mb-4">Packages</h5>
<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="inputPkgBase"
clearable
color="primary"
placeholder="Search Pkgbase"
variant="outlined"></v-text-field>
</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="selectedRepo"
:items="selectRepoItems"
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="selectedStatuses"
:items="selectStatusItems"
chips
closable-chips
color="primary"
density="default"
item-title="title"
item-value="value"
multiple
placeholder="Any Status"
return-object
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="enableExact" 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="page"
:length="pagesMax"
:total-visible="mobile ? undefined : 3"
active-color="primary"
density="comfortable"
start="1"
variant="text"></v-pagination>
</v-col>
</v-row>
<v-table class="mt-2 mt-sm-6" style="width: 100%; background: transparent; font-size: 1rem">
<thead>
<tr>
<th scope="col">Repository</th>
<th scope="col">Pkgbase</th>
<th scope="col">Status</th>
<th scope="col">Reason</th>
<th scope="col">
LTO
<v-tooltip activator="parent" location="bottom">
Link time optimization;
<br />
Does not guarantee that package is actually built with LTO
</v-tooltip>
</th>
<th scope="col">
DS
<v-tooltip activator="parent" location="bottom">
Debug-symbols available via debuginfod
</v-tooltip>
</th>
<th scope="col">Archlinux Version</th>
<th scope="col">ALHP Version</th>
<th class="text-end" scope="col" style="width: 30px">Info</th>
</tr>
</thead>
<tbody id="main-tbody">
<tr v-if="noPackagesFound">
No Packages found
</tr>
<template v-else>
<tr
v-for="(pkg, index) in 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"
density="comfortable"
label
variant="flat">
{{ repoVersion(pkg.repo) }}
</v-chip>
</td>
<td class="text-no-wrap">{{ pkg.pkgbase }}</td>
<td>{{ pkg.status.toLocaleUpperCase() }}</td>
<td>{{ pkg.skip_reason || '' }}</td>
<td><i :class="getLto(pkg.lto).class" :title="getLto(pkg.lto).title"></i></td>
<td>
<i
:class="getDs(pkg.debug_symbols).class"
:title="getDs(pkg.debug_symbols).title"></i>
</td>
<td>{{ pkg.arch_version }}</td>
<td>{{ pkg.repo_version }}</td>
<td class="d-flex align-center" style="gap: 3px">
<a
v-if="pkg.status === 'failed'"
:href="`https://alhp.dev/logs/${pkg.repo.slice(pkg.repo.indexOf('-') + 1)}/${pkg.pkgbase}.log`"
class="text-decoration-none"
style="
color: darkgrey;
transform: translateY(-3px) translateX(-22px);
max-width: 15px;
"
target="_blank">
<i class="fa fa-file-text fa-lg"></i>
</a>
<span v-else style="width: 15px"></span>
<a
:href="`https://archlinux.org/packages/?q=${pkg.pkgbase}`"
class="text-decoration-none font-weight-bold"
style="
color: darkgrey;
font-size: 18px;
padding: 0;
margin: 0;
width: 15px;
transform: translateX(-15px);
"
target="_blank"
title="ArchWeb">
AW
</a>
<span
v-if="pkg.build_date && pkg.peak_mem"
class="fa fa-info-circle fa-lg"
style="color: darkgrey; transform: translateY(-1px); max-width: 15px !important">
<v-tooltip activator="parent" location="start">
{{ `Built on ${pkg.build_date}` }}
<br />
{{ `Peak-Memory: ${pkg.peak_mem}` }}
</v-tooltip>
</span>
<span v-else style="max-width: 15px !important"></span>
</td>
</tr>
</template>
</tbody>
</v-table>
<PackageFilters :filter-options="filterOptions" :total-pages="filterResult.totalPages" />
<PackageTable :no-results="filterResult.noResults" :packages="filterResult.packages" />
</v-sheet>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
import { type Package } from '@/types/Package'
import { type Packages } from '@/types/Packages'
import { useDisplay } from 'vuetify'
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'
// Constants
const OFFSET = 50
const ROW_HEIGHT = 60
const TRANSPARENT_COLOR = 'transparent'
const { filterOptions, filterResult, initFromUrl } = usePackageFilters()
// Refs
const page = ref(0)
const pagesMax = ref(1)
const packages = ref<Array<Package>>([])
const noPackagesFound = ref(false)
const init = ref(false)
const inputPkgBase = ref('')
const selectedRepo = ref<{ title: string; value: string } | undefined>(undefined)
const selectedStatuses = ref<Array<{ title: string; value: string }>>([])
const enableExact = ref(false)
// Display
const { mobile } = useDisplay()
// Select Items
const selectRepoItems = [
{ title: 'core-x86-64-v2', value: 'core-x86-64-v2' },
{ title: 'extra-x86-64-v2', value: 'extra-x86-64-v2' },
{ title: 'multilib-x86-64-v2', value: 'multilib-x86-64-v2' },
{ title: 'core-x86-64-v3', value: 'core-x86-64-v3' },
{ title: 'extra-x86-64-v3', value: 'extra-x86-64-v3' },
{ title: 'multilib-x86-64-v3', value: 'multilib-x86-64-v3' },
{ title: 'core-x86-64-v4', value: 'core-x86-64-v4' },
{ title: 'extra-x86-64-v4', value: 'extra-x86-64-v4' },
{ title: 'multilib-x86-64-v4', value: 'multilib-x86-64-v4' }
]
const selectStatusItems = [
{ title: 'Latest', value: 'latest' },
{ title: 'Built', value: 'built' },
{ title: 'Failed', value: 'failed' },
{ title: 'Skipped', value: 'skipped' },
{ title: 'Delayed', value: 'delayed' },
{ title: 'Queued', value: 'queued' },
{ title: 'Building', value: 'building' },
{ title: 'Signing', value: 'signing' },
{ title: 'Unknown', value: 'unknown' }
]
// Computed properties
const repoName = (repo: string) => repo.split('-')[0]
const repoVersion = (repo: string) => repo.split('-')[repo.split('-').length - 1]
// Helper function for getting the color associated with a version
const getVersionColor = (version: string) => {
switch (version) {
case 'v2':
return '#3498db'
case 'v3':
return '#f39c12'
case 'v4':
return '#2ecc71'
default:
return 'grey'
}
}
// Helper function for getting the status color
const getStatusColor = (status: Package['status']) => {
switch (status) {
case 'skipped':
return '#373737'
case 'queued':
return '#5d2f03'
case 'latest':
return ''
case 'failed':
return '#4f140f'
case 'signing':
return '#093372'
case 'building':
return '#084f46'
case 'unknown':
return '#191919'
default:
return ''
}
}
const getLto = (lto: Package['lto']) => {
switch (lto) {
case 'enabled':
return {
title: 'built with LTO',
class: 'fa fa-check fa-lg text-success'
}
case 'unknown':
return {
title: 'not built with LTO yet',
class: 'fa fa-hourglass-o fa-lg text-grey'
}
case 'disabled':
return {
title: 'LTO explicitly disabled',
class: 'fa fa-times fa-lg text-red'
}
case 'auto_disabled':
return {
title: 'LTO automatically disabled',
class: 'fa fa-times-circle-o fa-lg text-amber'
}
default:
return { title: '', class: '' }
}
}
const getDs = (ds: Package['debug_symbols']) => {
switch (ds) {
case 'available':
return {
title: 'Debug symbols available',
class: 'fa fa-check fa-lg text-success'
}
case 'unknown':
return {
title: 'Not built yet',
class: 'fa fa-hourglass-o fa-lg text-grey'
}
case 'not_available':
return {
title: 'Not built with debug symbols',
class: 'fa fa-times fa-lg text-red'
}
default:
return { title: '', class: '' }
}
}
// Helper function for updating URL params dynamically
const updateUrlParams = () => {
const params = new URLSearchParams()
if (page.value > 0) {
params.set('offset', (page.value * OFFSET).toString())
}
if (inputPkgBase.value) {
params.set('pkgbase', inputPkgBase.value.toLowerCase())
}
if (selectedRepo.value) {
params.set('repo', selectedRepo.value.value)
}
if (selectedStatuses.value.length > 0) {
selectedStatuses.value.forEach((status) => {
params.append('status', status.value)
})
}
if (enableExact.value) {
params.set('exact', '')
}
window.history.pushState(null, '', `${window.location.pathname}?${params}`)
}
const searchPackages = () => {
const offset = OFFSET * page.value
noPackagesFound.value = false
const params = new URLSearchParams()
if (page.value > 0) {
params.set('offset', offset.toString())
}
if (inputPkgBase.value) {
params.set('pkgbase', inputPkgBase.value.toLowerCase())
}
if (selectedRepo.value) {
params.set('repo', selectedRepo.value.value)
}
if (selectedStatuses.value.length > 0) {
selectedStatuses.value.forEach((status) => {
params.append('status', status.value)
})
}
if (enableExact.value) {
params.set('exact', '')
}
params.set('limit', OFFSET.toString())
params.set('offset', offset.toString())
fetch('https://api.alhp.dev/packages?' + params, {
method: 'GET'
})
.then((response) => {
if (response.ok) return response.json()
if (response.status === 404) {
noPackagesFound.value = true
pagesMax.value = 1
} else {
throw new Error(response.statusText)
}
})
.then((json: Packages) => {
if (!json) return
pagesMax.value = json.total / OFFSET + 1
packages.value = json.packages
})
.catch((error) => {
console.error(error)
})
}
// Properly set the initial values from URL params
onMounted(() => {
const urlParams = new URLSearchParams(window.location.search)
page.value = urlParams.has('offset')
? Math.floor(parseInt(urlParams.get('offset') || '0') / OFFSET)
: 0
inputPkgBase.value = urlParams.get('pkgbase') || ''
const repoValue = urlParams.get('repo')
if (repoValue) {
selectedRepo.value = { title: repoValue, value: repoValue }
} else {
selectedRepo.value = undefined
}
const statuses = urlParams.getAll('status')
selectedStatuses.value = statuses.map((status) => ({
title: status.toUpperCase(),
value: status
}))
enableExact.value = urlParams.has('exact')
searchPackages()
initFromUrl()
})
// Watchers
watch(
[page, inputPkgBase, selectedRepo, selectedStatuses, enableExact],
() => {
if (init.value) return
updateUrlParams()
searchPackages()
},
{ deep: true }
)
</script>

View File

@@ -0,0 +1,66 @@
<template>
<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"
clearable
color="primary"
placeholder="Search Pkgbase"
variant="outlined"></v-text-field>
</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"
: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"
:items="STATUS_ITEMS"
chips
closable-chips
color="primary"
density="default"
item-title="title"
item-value="value"
multiple
placeholder="Any Status"
return-object
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-col>
<v-col :class="mobile ? 'mt-n6' : ''" :cols="mobile ? 12 : 'auto'" class="ms-auto">
<v-pagination
v-model="filterOptions.page"
:length="totalPages"
:total-visible="mobile ? undefined : 3"
active-color="primary"
density="comfortable"
start="1"
variant="text"></v-pagination>
</v-col>
</v-row>
</template>
<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
}>()
const { mobile } = useDisplay()
</script>

View File

@@ -0,0 +1,115 @@
<template>
<v-table class="mt-2 mt-sm-6" style="width: 100%; background: transparent; font-size: 1rem">
<thead>
<tr>
<th scope="col">Repository</th>
<th scope="col">Pkgbase</th>
<th scope="col">Status</th>
<th scope="col">Reason</th>
<th scope="col">
LTO
<v-tooltip activator="parent" location="bottom">
Link time optimization;
<br />
Does not guarantee that package is actually built with LTO
</v-tooltip>
</th>
<th scope="col">
DS
<v-tooltip activator="parent" location="bottom">
Debug-symbols available via debuginfod
</v-tooltip>
</th>
<th scope="col">Archlinux Version</th>
<th scope="col">ALHP Version</th>
<th class="text-end" scope="col" style="width: 30px">Info</th>
</tr>
</thead>
<tbody id="main-tbody">
<tr v-if="noResults">
No Packages found
</tr>
<template v-else>
<tr
v-for="(pkg, index) in 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"
density="comfortable"
label
variant="flat">
{{ repoVersion(pkg.repo) }}
</v-chip>
</td>
<td class="text-no-wrap">{{ pkg.pkgbase }}</td>
<td>{{ pkg.status.toLocaleUpperCase() }}</td>
<td>{{ pkg.skip_reason || '' }}</td>
<td><i :class="getLto(pkg.lto).class" :title="getLto(pkg.lto).title"></i></td>
<td>
<i :class="getDs(pkg.debug_symbols).class" :title="getDs(pkg.debug_symbols).title"></i>
</td>
<td>{{ pkg.arch_version }}</td>
<td>{{ pkg.repo_version }}</td>
<td class="d-flex align-center" style="gap: 3px">
<a
v-if="pkg.status === 'failed'"
:href="`https://alhp.dev/logs/${pkg.repo.slice(pkg.repo.indexOf('-') + 1)}/${pkg.pkgbase}.log`"
class="text-decoration-none"
style="
color: darkgrey;
transform: translateY(-3px) translateX(-22px);
max-width: 15px;
"
target="_blank">
<i class="fa fa-file-text fa-lg"></i>
</a>
<span v-else style="width: 15px"></span>
<a
:href="`https://archlinux.org/packages/?q=${pkg.pkgbase}`"
class="text-decoration-none font-weight-bold"
style="
color: darkgrey;
font-size: 18px;
padding: 0;
margin: 0;
width: 15px;
transform: translateX(-15px);
"
target="_blank"
title="ArchWeb">
AW
</a>
<span
v-if="pkg.build_date && pkg.peak_mem"
class="fa fa-info-circle fa-lg"
style="color: darkgrey; transform: translateY(-1px); max-width: 15px !important">
<v-tooltip activator="parent" location="start">
{{ `Built on ${pkg.build_date}` }}
<br />
{{ `Peak-Memory: ${pkg.peak_mem}` }}
</v-tooltip>
</span>
<span v-else style="max-width: 15px !important"></span>
</td>
</tr>
</template>
</tbody>
</v-table>
</template>
<script lang="ts" setup>
import { type Package } from '@/types/Package'
import { usePackageDisplay } from '@/composables/Packages/usePackageDisplay'
defineProps<{
packages: Array<Package>
noResults: boolean
}>()
const { repoName, repoVersion, getVersionColor, getStatusColor, getLto, getDs } =
usePackageDisplay()
</script>

View File

@@ -0,0 +1,98 @@
import { type Package } from '@/types/Package'
export function usePackageDisplay() {
const repoName = (repo: string) => repo.split('-')[0]
const repoVersion = (repo: string) => repo.split('-')[repo.split('-').length - 1]
const getVersionColor = (version: string) => {
switch (version) {
case 'v2':
return '#3498db'
case 'v3':
return '#f39c12'
case 'v4':
return '#2ecc71'
default:
return 'grey'
}
}
const getStatusColor = (status: Package['status']) => {
switch (status) {
case 'skipped':
return '#373737'
case 'queued':
return '#5d2f03'
case 'latest':
return ''
case 'failed':
return '#4f140f'
case 'signing':
return '#093372'
case 'building':
return '#084f46'
case 'unknown':
return '#191919'
default:
return ''
}
}
const getLto = (lto: Package['lto']) => {
switch (lto) {
case 'enabled':
return {
title: 'built with LTO',
class: 'fa fa-check fa-lg text-success'
}
case 'unknown':
return {
title: 'not built with LTO yet',
class: 'fa fa-hourglass-o fa-lg text-grey'
}
case 'disabled':
return {
title: 'LTO explicitly disabled',
class: 'fa fa-times fa-lg text-red'
}
case 'auto_disabled':
return {
title: 'LTO automatically disabled',
class: 'fa fa-times-circle-o fa-lg text-amber'
}
default:
return { title: '', class: '' }
}
}
const getDs = (ds: Package['debug_symbols']) => {
switch (ds) {
case 'available':
return {
title: 'Debug symbols available',
class: 'fa fa-check fa-lg text-success'
}
case 'unknown':
return {
title: 'Not built yet',
class: 'fa fa-hourglass-o fa-lg text-grey'
}
case 'not_available':
return {
title: 'Not built with debug symbols',
class: 'fa fa-times fa-lg text-red'
}
default:
return { title: '', class: '' }
}
}
return {
repoName,
repoVersion,
getVersionColor,
getStatusColor,
getLto,
getDs
}
}

View File

@@ -0,0 +1,214 @@
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

@@ -0,0 +1,28 @@
// UI Constants
export const TRANSPARENT_COLOR = 'transparent'
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' }
]
export const STATUS_ITEMS = [
{ title: 'Latest', value: 'latest' },
{ title: 'Built', value: 'built' },
{ title: 'Failed', value: 'failed' },
{ title: 'Skipped', value: 'skipped' },
{ title: 'Delayed', value: 'delayed' },
{ title: 'Queued', value: 'queued' },
{ title: 'Building', value: 'building' },
{ title: 'Signing', value: 'signing' },
{ title: 'Unknown', value: 'unknown' }
]

View File

@@ -1,22 +1,13 @@
/**
* main.ts
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import {registerPlugins} from '@/plugins'
// Components
import { registerPlugins } from '@/plugins'
import App from './App.vue'
// Composables
import {createApp} from 'vue'
// Styles
import { createApp } from 'vue'
import '@/assets/styles/base.scss'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
registerPlugins(app)

View File

@@ -0,0 +1,258 @@
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,44 @@
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

@@ -0,0 +1,50 @@
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()

View File

@@ -40,6 +40,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/types@npm:7.27.0"
dependencies:
"@babel/helper-string-parser": "npm:^7.25.9"
"@babel/helper-validator-identifier": "npm:^7.25.9"
checksum: 10c0/6f1592eabe243c89a608717b07b72969be9d9d2fce1dee21426238757ea1fa60fdfc09b29de9e48d8104311afc6e6fb1702565a9cc1e09bc1e76f2b2ddb0f6e1
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.25.1":
version: 0.25.1
resolution: "@esbuild/aix-ppc64@npm:0.25.1"
@@ -590,12 +600,12 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^22.13.11":
version: 22.13.11
resolution: "@types/node@npm:22.13.11"
"@types/node@npm:^22.14.1":
version: 22.14.1
resolution: "@types/node@npm:22.14.1"
dependencies:
undici-types: "npm:~6.20.0"
checksum: 10c0/f6ee33d36372242535c38640fe7550a6640d8a775ec19b55bfc11775b521cba072d892ca92a912332ce01b317293d645c1bf767f3f882ec719f2404a3d2a5b96
undici-types: "npm:~6.21.0"
checksum: 10c0/d49c4d00403b1c2348cf0701b505fd636d80aabe18102105998dc62fdd36dcaf911e73c7a868c48c21c1022b825c67b475b65b1222d84b704d8244d152bb7f86
languageName: node
linkType: hard
@@ -696,6 +706,39 @@ __metadata:
languageName: node
linkType: hard
"@vue/devtools-api@npm:^7.7.2":
version: 7.7.2
resolution: "@vue/devtools-api@npm:7.7.2"
dependencies:
"@vue/devtools-kit": "npm:^7.7.2"
checksum: 10c0/418d3c868143a91518bc846965f7c8a955f072b8526d0f739f4d7dc00b13a0f56b214d876bfff338dc841762b526a1a4c11b5e8b0ab6dd7f3250a694ec8dfbe3
languageName: node
linkType: hard
"@vue/devtools-kit@npm:^7.7.2":
version: 7.7.2
resolution: "@vue/devtools-kit@npm:7.7.2"
dependencies:
"@vue/devtools-shared": "npm:^7.7.2"
birpc: "npm:^0.2.19"
hookable: "npm:^5.5.3"
mitt: "npm:^3.0.1"
perfect-debounce: "npm:^1.0.0"
speakingurl: "npm:^14.0.1"
superjson: "npm:^2.2.1"
checksum: 10c0/e052ba756558040855304b6ee13ba39131a44c89a9f78ab262c79f8a0e6b58fa379e1efa306a9a50675cac3e48baeb3f86b1560f64edf48cbc0695165d0b2be6
languageName: node
linkType: hard
"@vue/devtools-shared@npm:^7.7.2":
version: 7.7.2
resolution: "@vue/devtools-shared@npm:7.7.2"
dependencies:
rfdc: "npm:^1.4.1"
checksum: 10c0/6399135da41a91f48c3db7c59cedb01ad331af7784ef0877c15c669ad5a5d1cce68f73d50d81f85a31a90b0d6323ff807ebe5b1fb041d1e86932f2c983a0cdad
languageName: node
linkType: hard
"@vue/language-core@npm:2.2.8":
version: 2.2.8
resolution: "@vue/language-core@npm:2.2.8"
@@ -818,23 +861,24 @@ __metadata:
version: 0.0.0-use.local
resolution: "alhp-web@workspace:."
dependencies:
"@babel/types": "npm:^7.26.10"
"@babel/types": "npm:^7.27.0"
"@fontsource/roboto": "npm:^5.2.5"
"@mdi/font": "npm:7.4.47"
"@types/node": "npm:^22.13.11"
"@types/node": "npm:^22.14.1"
"@vitejs/plugin-vue": "npm:^5.2.3"
fork-awesome: "npm:^1.2.0"
pinia: "npm:^3.0.2"
prettier: "npm:^3.5.3"
roboto-fontface: "npm:^0.10.0"
sass: "npm:^1.86.0"
typescript: "npm:^5.8.2"
sass: "npm:^1.86.3"
typescript: "npm:^5.8.3"
unplugin-fonts: "npm:^1.3.1"
unplugin-vue-components: "npm:^28.4.1"
vite: "npm:^6.2.2"
vite-plugin-vuetify: "npm:^2.1.0"
unplugin-vue-components: "npm:^28.5.0"
vite: "npm:^6.2.6"
vite-plugin-vuetify: "npm:^2.1.1"
vue: "npm:^3.5.13"
vue-tsc: "npm:^2.2.8"
vuetify: "npm:^3.7.18"
vuetify: "npm:^3.8.1"
languageName: unknown
linkType: soft
@@ -899,6 +943,13 @@ __metadata:
languageName: node
linkType: hard
"birpc@npm:^0.2.19":
version: 0.2.19
resolution: "birpc@npm:0.2.19"
checksum: 10c0/be3c6a4044e3041a5d8eb4c4d50b57b46158dc8149ada718ead20544e50b68b72b34c9d8bf0457d23d5f18e5a66d206b8bef5ff22c1018e1e39d373187eed455
languageName: node
linkType: hard
"brace-expansion@npm:^2.0.1":
version: 2.0.1
resolution: "brace-expansion@npm:2.0.1"
@@ -1018,6 +1069,15 @@ __metadata:
languageName: node
linkType: hard
"copy-anything@npm:^3.0.2":
version: 3.0.5
resolution: "copy-anything@npm:3.0.5"
dependencies:
is-what: "npm:^4.1.8"
checksum: 10c0/01eadd500c7e1db71d32d95a3bfaaedcb839ef891c741f6305ab0461398056133de08f2d1bf4c392b364e7bdb7ce498513896e137a7a183ac2516b065c28a4fe
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
@@ -1393,6 +1453,13 @@ __metadata:
languageName: node
linkType: hard
"hookable@npm:^5.5.3":
version: 5.5.3
resolution: "hookable@npm:5.5.3"
checksum: 10c0/275f4cc84d27f8d48c5a5cd5685b6c0fea9291be9deea5bff0cfa72856ed566abde1dcd8cb1da0f9a70b4da3d7ec0d60dc3554c4edbba647058cc38816eced3d
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
@@ -1506,6 +1573,13 @@ __metadata:
languageName: node
linkType: hard
"is-what@npm:^4.1.8":
version: 4.1.16
resolution: "is-what@npm:4.1.16"
checksum: 10c0/611f1947776826dcf85b57cfb7bd3b3ea6f4b94a9c2f551d4a53f653cf0cb9d1e6518846648256d46ee6c91d114b6d09d2ac8a07306f7430c5900f87466aae5b
languageName: node
linkType: hard
"isexe@npm:^2.0.0":
version: 2.0.0
resolution: "isexe@npm:2.0.0"
@@ -1540,7 +1614,7 @@ __metadata:
languageName: node
linkType: hard
"local-pkg@npm:^1.0.0":
"local-pkg@npm:^1.1.1":
version: 1.1.1
resolution: "local-pkg@npm:1.1.1"
dependencies:
@@ -1716,6 +1790,13 @@ __metadata:
languageName: node
linkType: hard
"mitt@npm:^3.0.1":
version: 3.0.1
resolution: "mitt@npm:3.0.1"
checksum: 10c0/3ab4fdecf3be8c5255536faa07064d05caa3dd332bd318ff02e04621f7b3069ca1de9106cfe8e7ced675abfc2bec2ce4c4ef321c4a1bb1fb29df8ae090741913
languageName: node
linkType: hard
"mkdirp@npm:^1.0.3":
version: 1.0.4
resolution: "mkdirp@npm:1.0.4"
@@ -1868,6 +1949,13 @@ __metadata:
languageName: node
linkType: hard
"perfect-debounce@npm:^1.0.0":
version: 1.0.0
resolution: "perfect-debounce@npm:1.0.0"
checksum: 10c0/e2baac416cae046ef1b270812cf9ccfb0f91c04ea36ac7f5b00bc84cb7f41bdbba087c0ab21b4e02a7ef3a1f1f6db399f137cecec46868bd7d8d88c2a9ee431f
languageName: node
linkType: hard
"picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
@@ -1889,6 +1977,21 @@ __metadata:
languageName: node
linkType: hard
"pinia@npm:^3.0.2":
version: 3.0.2
resolution: "pinia@npm:3.0.2"
dependencies:
"@vue/devtools-api": "npm:^7.7.2"
peerDependencies:
typescript: ">=4.4.4"
vue: ^2.7.0 || ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
checksum: 10c0/4c21412ddb32c48c1d9fb9fb47a2cd40bc4af9198e65392423ab97f6a9da31d0b880bc59b008967058643a988cb574025f885a1d0e4faf47bec25521933bb27f
languageName: node
linkType: hard
"pkg-types@npm:^1.3.0":
version: 1.3.1
resolution: "pkg-types@npm:1.3.1"
@@ -1999,6 +2102,13 @@ __metadata:
languageName: node
linkType: hard
"rfdc@npm:^1.4.1":
version: 1.4.1
resolution: "rfdc@npm:1.4.1"
checksum: 10c0/4614e4292356cafade0b6031527eea9bc90f2372a22c012313be1dcc69a3b90c7338158b414539be863fa95bfcb2ddcd0587be696841af4e6679d85e62c060c7
languageName: node
linkType: hard
"roboto-fontface@npm:^0.10.0":
version: 0.10.0
resolution: "roboto-fontface@npm:0.10.0"
@@ -2094,9 +2204,9 @@ __metadata:
languageName: node
linkType: hard
"sass@npm:^1.86.0":
version: 1.86.0
resolution: "sass@npm:1.86.0"
"sass@npm:^1.86.3":
version: 1.86.3
resolution: "sass@npm:1.86.3"
dependencies:
"@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0"
@@ -2107,7 +2217,7 @@ __metadata:
optional: true
bin:
sass: sass.js
checksum: 10c0/921caea1fd8a450d4a986e5570ce13c4ca7b2a57da390811add3d2087ad8f46f53b34652ddcb237d8bdaad49c560b8d6eee130c733c787d058bc5a71a914c139
checksum: 10c0/ba819a0828f732adf7a94cd8ca017bce92bc299ffb878836ed1da80a30612bfbbf56a5e42d6dff3ad80d919c2025afb42948fc7b54a7bc61a9a2d58e1e0c558a
languageName: node
linkType: hard
@@ -2192,6 +2302,13 @@ __metadata:
languageName: node
linkType: hard
"speakingurl@npm:^14.0.1":
version: 14.0.1
resolution: "speakingurl@npm:14.0.1"
checksum: 10c0/1de1d1b938a7c4d9e79593ff7a26d312ec04a7c3234ca40b7f9b8106daf74ea9d2110a077f5db97ecf3762b83069e3ccbf9694431b51d4fcfd863f0b3333c342
languageName: node
linkType: hard
"sprintf-js@npm:^1.1.3":
version: 1.1.3
resolution: "sprintf-js@npm:1.1.3"
@@ -2248,6 +2365,15 @@ __metadata:
languageName: node
linkType: hard
"superjson@npm:^2.2.1":
version: 2.2.2
resolution: "superjson@npm:2.2.2"
dependencies:
copy-anything: "npm:^3.0.2"
checksum: 10c0/aa49ebe6653e963020bc6a1ed416d267dfda84cfcc3cbd3beffd75b72e44eb9df7327215f3e3e77528f6e19ad8895b16a4964fdcd56d1799d14350db8c92afbc
languageName: node
linkType: hard
"tar@npm:^6.1.11, tar@npm:^6.1.2":
version: 6.2.1
resolution: "tar@npm:6.2.1"
@@ -2281,23 +2407,23 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.8.2":
version: 5.8.2
resolution: "typescript@npm:5.8.2"
"typescript@npm:^5.8.3":
version: 5.8.3
resolution: "typescript@npm:5.8.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6
checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.8.2#optional!builtin<compat/typescript>":
version: 5.8.2
resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin<compat/typescript>::version=5.8.2&hash=5786d5"
"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin<compat/typescript>":
version: 5.8.3
resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=5786d5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2
checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb
languageName: node
linkType: hard
@@ -2308,10 +2434,10 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~6.20.0":
version: 6.20.0
resolution: "undici-types@npm:6.20.0"
checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf
"undici-types@npm:~6.21.0":
version: 6.21.0
resolution: "undici-types@npm:6.21.0"
checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04
languageName: node
linkType: hard
@@ -2359,17 +2485,17 @@ __metadata:
languageName: node
linkType: hard
"unplugin-vue-components@npm:^28.4.1":
version: 28.4.1
resolution: "unplugin-vue-components@npm:28.4.1"
"unplugin-vue-components@npm:^28.5.0":
version: 28.5.0
resolution: "unplugin-vue-components@npm:28.5.0"
dependencies:
chokidar: "npm:^3.6.0"
debug: "npm:^4.4.0"
local-pkg: "npm:^1.0.0"
local-pkg: "npm:^1.1.1"
magic-string: "npm:^0.30.17"
mlly: "npm:^1.7.4"
tinyglobby: "npm:^0.2.12"
unplugin: "npm:^2.2.0"
unplugin: "npm:^2.3.2"
unplugin-utils: "npm:^0.2.4"
peerDependencies:
"@babel/parser": ^7.15.8
@@ -2380,7 +2506,7 @@ __metadata:
optional: true
"@nuxt/kit":
optional: true
checksum: 10c0/f05448285e6d049b8aeadf5747cf7cda23105bfe8691326217abf3a5aa924768279f1c26f37ed0cd98c00f97ea7ce1cd5d5ed2916fb09f2b817f25563ece825a
checksum: 10c0/4a2419cee6a8d19e0dd121fa56afef29c981434ee4f632e972e46aec52f161608c85b0806737308c597ca6d19b07d62f654f4070bd4302c1691a1c865acb9248
languageName: node
linkType: hard
@@ -2394,13 +2520,14 @@ __metadata:
languageName: node
linkType: hard
"unplugin@npm:^2.2.0":
version: 2.2.2
resolution: "unplugin@npm:2.2.2"
"unplugin@npm:^2.3.2":
version: 2.3.2
resolution: "unplugin@npm:2.3.2"
dependencies:
acorn: "npm:^8.14.1"
picomatch: "npm:^4.0.2"
webpack-virtual-modules: "npm:^0.6.2"
checksum: 10c0/76ba320f0c5d18c31c6efab0bcf1f487e900193da7d9a63d50ccb87ea3c50bc9952111caee4ec5017bdcb53445dce275b994c6aeca6b92567db283ec5d9fc01b
checksum: 10c0/157a50072601b9bfbf3ab27a76a04685fb0af0c1a579d958787ffcb28a4d64e09acf42f0176e8767ccd940f27ee52d97a7f6aa6ce2e1e0dbe666ec26519750ef
languageName: node
linkType: hard
@@ -2411,9 +2538,9 @@ __metadata:
languageName: node
linkType: hard
"vite-plugin-vuetify@npm:^2.1.0":
version: 2.1.0
resolution: "vite-plugin-vuetify@npm:2.1.0"
"vite-plugin-vuetify@npm:^2.1.1":
version: 2.1.1
resolution: "vite-plugin-vuetify@npm:2.1.1"
dependencies:
"@vuetify/loader-shared": "npm:^2.1.0"
debug: "npm:^4.3.3"
@@ -2422,13 +2549,13 @@ __metadata:
vite: ">=5"
vue: ^3.0.0
vuetify: ^3.0.0
checksum: 10c0/c9b6b3ee4b75ffc9b1f124f8f635d372f3258bd6f2abb48db42a80a4efe54127c9e325ad12eb92278ddbd629dfd1111810ec2bd6b1fb076f0724b789f33054e7
checksum: 10c0/629893488ae23ffd9e9a32fccf2d6ff4d5a00826329ec90e9765a17d182a9200ffe11430bd418227119e8ef3ed21eaec1bab0635a77fdcb4b64aac10f38adcb0
languageName: node
linkType: hard
"vite@npm:^6.2.2":
version: 6.2.2
resolution: "vite@npm:6.2.2"
"vite@npm:^6.2.6":
version: 6.2.6
resolution: "vite@npm:6.2.6"
dependencies:
esbuild: "npm:^0.25.0"
fsevents: "npm:~2.3.3"
@@ -2474,7 +2601,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10c0/52f5b1c10cfe5e3b6382c6de1811ebbf76df9b5a8bab3d65169446c6b54a5f1528f775b1548009a6d8aad11def20fba046bb3e9abb10c0c2c9ccd78118623bb8
checksum: 10c0/68a2ed3e61bdd654c59b817b4f3203065241c66d1739faa707499130f3007bc3a666c7a8320a4198e275e62b5e4d34d9b78a6533f69e321d366e76f5093b2071
languageName: node
linkType: hard
@@ -2517,14 +2644,14 @@ __metadata:
languageName: node
linkType: hard
"vuetify@npm:^3.7.18":
version: 3.7.18
resolution: "vuetify@npm:3.7.18"
"vuetify@npm:^3.8.1":
version: 3.8.1
resolution: "vuetify@npm:3.8.1"
peerDependencies:
typescript: ">=4.7"
vite-plugin-vuetify: ">=1.0.0"
vue: ^3.3.0
webpack-plugin-vuetify: ">=2.0.0"
vite-plugin-vuetify: ">=2.1.0"
vue: ^3.5.0
webpack-plugin-vuetify: ">=3.1.0"
peerDependenciesMeta:
typescript:
optional: true
@@ -2532,7 +2659,7 @@ __metadata:
optional: true
webpack-plugin-vuetify:
optional: true
checksum: 10c0/98686640dc11bb59e12fdb214e0fc8b2a4ecd2f8286448fddb6abecd858a0fd5db5927d443e9b3be7ff92303ae1c8bc01bf4712278a9b6e4f4d72358a36e0a0e
checksum: 10c0/b6fd1a96325b16a1f88acd1ef9d385118c09f25793bc7eddc3da2fe57e84a17d200d77dc14a2e0bc1b67fb6f301ce15b760185d2faafeaf61c584a92df6b0b45
languageName: node
linkType: hard