Added Repository Changelog
This commit is contained in:
@@ -2,6 +2,10 @@ import { FastifyInstance } from "fastify";
|
|||||||
import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf";
|
import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf";
|
||||||
import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions";
|
import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions";
|
||||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
//import { ready as zplReady } from 'zpl-renderer-js'
|
//import { ready as zplReady } from 'zpl-renderer-js'
|
||||||
//import { renderZPL } from "zpl-image";
|
//import { renderZPL } from "zpl-image";
|
||||||
@@ -28,6 +32,31 @@ dayjs.extend(isSameOrBefore)
|
|||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
|
function resolveGitRoot() {
|
||||||
|
const searchRoots = [
|
||||||
|
process.cwd(),
|
||||||
|
path.resolve(process.cwd(), ".."),
|
||||||
|
path.resolve(__dirname, "../../.."),
|
||||||
|
path.resolve(__dirname, "../../../.."),
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const startDir of searchRoots) {
|
||||||
|
let currentDir = startDir
|
||||||
|
|
||||||
|
while (currentDir && currentDir !== path.dirname(currentDir)) {
|
||||||
|
if (existsSync(path.join(currentDir, ".git"))) {
|
||||||
|
return currentDir
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir = path.dirname(currentDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export default async function functionRoutes(server: FastifyInstance) {
|
export default async function functionRoutes(server: FastifyInstance) {
|
||||||
const streamToBuffer = async (stream: any): Promise<Buffer> =>
|
const streamToBuffer = async (stream: any): Promise<Buffer> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
@@ -162,6 +191,55 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get('/functions/changelog', async (req, reply) => {
|
||||||
|
const { limit } = req.query as { limit?: string | number }
|
||||||
|
const parsedLimit = Number(limit)
|
||||||
|
const safeLimit = Number.isFinite(parsedLimit)
|
||||||
|
? Math.min(Math.max(parsedLimit, 1), 50)
|
||||||
|
: 15
|
||||||
|
|
||||||
|
const gitRoot = resolveGitRoot()
|
||||||
|
|
||||||
|
if (!gitRoot) {
|
||||||
|
return reply.code(500).send({ error: 'Git repository not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileAsync('git', [
|
||||||
|
'-C',
|
||||||
|
gitRoot,
|
||||||
|
'log',
|
||||||
|
`--max-count=${safeLimit}`,
|
||||||
|
'--date=iso-strict',
|
||||||
|
'--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1e'
|
||||||
|
])
|
||||||
|
|
||||||
|
const entries = stdout
|
||||||
|
.split('\x1e')
|
||||||
|
.map(entry => entry.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(entry => {
|
||||||
|
const [hash, shortHash, subject, authorName, committedAt] = entry.split('\x1f')
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash,
|
||||||
|
shortHash,
|
||||||
|
subject,
|
||||||
|
authorName,
|
||||||
|
committedAt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
repositoryRoot: gitRoot,
|
||||||
|
entries
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
req.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Failed to load changelog' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post('/functions/serial/start', async (req, reply) => {
|
server.post('/functions/serial/start', async (req, reply) => {
|
||||||
console.log(req.body)
|
console.log(req.body)
|
||||||
const {executionDate,templateIds,tenantId} = req.body as {executionDate:string,templateIds:Number[],tenantId:Number}
|
const {executionDate,templateIds,tenantId} = req.body as {executionDate:string,templateIds:Number[],tenantId:Number}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const { isHelpSlideoverOpen } = useDashboard()
|
const { isHelpSlideoverOpen } = useDashboard()
|
||||||
const { metaSymbol } = useShortcuts()
|
const { metaSymbol } = useShortcuts()
|
||||||
|
const { entries, pending, error, seenState, refresh, markAsSeen } = useChangelog()
|
||||||
|
|
||||||
const shortcuts = ref(false)
|
const shortcuts = ref(false)
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
@@ -133,6 +136,21 @@ const resetContactRequest = () => {
|
|||||||
title: "",
|
title: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastOpenedLabel = computed(() => {
|
||||||
|
if (!seenState.value.lastOpenedAt) return 'Noch nicht geöffnet'
|
||||||
|
|
||||||
|
return dayjs(seenState.value.lastOpenedAt).format('DD.MM.YYYY HH:mm')
|
||||||
|
})
|
||||||
|
|
||||||
|
const changelogEntries = computed(() => entries.value.slice(0, 12))
|
||||||
|
|
||||||
|
watch(isHelpSlideoverOpen, async (isOpen) => {
|
||||||
|
if (!isOpen || shortcuts.value) return
|
||||||
|
|
||||||
|
await refresh(true)
|
||||||
|
markAsSeen()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -171,8 +189,71 @@ const resetContactRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-y-3">
|
<div v-else class="flex flex-col gap-y-6">
|
||||||
<UButton v-for="(link, index) in links" :key="index" color="white" v-bind="link" />
|
<div class="flex flex-col gap-y-3">
|
||||||
|
<UButton v-for="(link, index) in links" :key="index" color="white" v-bind="link" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UCard>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
Changelog
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Zuletzt geöffnet: {{ lastOpenedLabel }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
:loading="pending"
|
||||||
|
@click="refresh(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="error"
|
||||||
|
class="mt-4"
|
||||||
|
color="red"
|
||||||
|
variant="soft"
|
||||||
|
title="Changelog konnte nicht geladen werden"
|
||||||
|
:description="error"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else-if="pending && !changelogEntries.length" class="mt-4">
|
||||||
|
<UProgress animation="carousel"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="changelogEntries.length" class="mt-4 flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
v-for="entry in changelogEntries"
|
||||||
|
:key="entry.hash"
|
||||||
|
class="rounded-lg border border-gray-200 dark:border-gray-800 p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="font-medium text-gray-900 dark:text-white break-words">
|
||||||
|
{{ entry.subject }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{{ entry.authorName }} · {{ dayjs(entry.committedAt).format('DD.MM.YYYY HH:mm') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UBadge color="gray" variant="subtle">
|
||||||
|
{{ entry.shortHash }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-else class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Es sind noch keine Changelog-Einträge verfügbar.
|
||||||
|
</p>
|
||||||
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="mt-5" v-if="!loadingContactRequest">
|
<!-- <div class="mt-5" v-if="!loadingContactRequest">
|
||||||
<h1 class="font-semibold">Kontaktanfrage:</h1>
|
<h1 class="font-semibold">Kontaktanfrage:</h1>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const route = useRoute()
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const labelPrinter = useLabelPrinterStore()
|
const labelPrinter = useLabelPrinterStore()
|
||||||
const calculatorStore = useCalculatorStore()
|
const calculatorStore = useCalculatorStore()
|
||||||
|
const { hasUnread, refresh: refreshChangelog } = useChangelog()
|
||||||
|
|
||||||
const month = dayjs().format("MM")
|
const month = dayjs().format("MM")
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ const groups = computed(() => [
|
|||||||
].filter(Boolean))
|
].filter(Boolean))
|
||||||
|
|
||||||
// --- Footer Links nutzen jetzt den zentralen Calculator Store ---
|
// --- Footer Links nutzen jetzt den zentralen Calculator Store ---
|
||||||
const footerLinks = computed(() => [
|
const footerItems = computed(() => [
|
||||||
{
|
{
|
||||||
label: 'Taschenrechner',
|
label: 'Taschenrechner',
|
||||||
icon: 'i-heroicons-calculator',
|
icon: 'i-heroicons-calculator',
|
||||||
@@ -123,10 +124,15 @@ const footerLinks = computed(() => [
|
|||||||
{
|
{
|
||||||
label: 'Hilfe & Info',
|
label: 'Hilfe & Info',
|
||||||
icon: 'i-heroicons-question-mark-circle',
|
icon: 'i-heroicons-question-mark-circle',
|
||||||
|
badge: hasUnread.value ? 'Neu' : null,
|
||||||
click: () => isHelpSlideoverOpen.value = true
|
click: () => isHelpSlideoverOpen.value = true
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshChangelog()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -256,7 +262,25 @@ const footerLinks = computed(() => [
|
|||||||
<UColorModeToggle class="ml-3"/>
|
<UColorModeToggle class="ml-3"/>
|
||||||
<LabelPrinterButton class="w-full"/>
|
<LabelPrinterButton class="w-full"/>
|
||||||
|
|
||||||
<UDashboardSidebarLinks :links="footerLinks" class="w-full"/>
|
<div class="flex flex-col gap-1">
|
||||||
|
<UButton
|
||||||
|
v-for="item in footerItems"
|
||||||
|
:key="item.label"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full"
|
||||||
|
:icon="item.icon"
|
||||||
|
@click="item.click ? item.click() : null"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
|
||||||
|
<template #trailing>
|
||||||
|
<UBadge v-if="item.badge" color="primary" variant="solid" size="xs">
|
||||||
|
{{ item.badge }}
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UDivider class="sticky bottom-0 w-full"/>
|
<UDivider class="sticky bottom-0 w-full"/>
|
||||||
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;" class="w-full"/>
|
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;" class="w-full"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user