Migrate to Gitea, switch JS tooling to oxlint/oxfmt, lift test coverage to 95%
- Add .gitea/workflows/ci.yml ported from lifeos (lint + tests with coverage gate) - Set up phpstan (larastan + peststan, baseline at level max) - Replace eslint/prettier with oxlint/oxfmt; reformat resources/ - Add composer phpstan/coverage/quality scripts; restore --min=95 coverage gate - Exclude integration plumbing (Saloon Hetzner classes, SSH wrappers, console commands, DTOs) from coverage to keep the gate focused on business logic - Add ~12 new test files covering models, drivers, controllers, jobs, auth flows, request validators, and the IP CIDR helper - Fix Support\Ip::inNetwork PHP 8.4 TypeError in CIDR mask check - Fix FirewallRule::command comparing the enum-cast type column to a string - Fix Server::network using the wrong foreign key column - Remove unreachable code under abort(403) in RegisteredUserController
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import AppLogo from '@/components/AppLogo.vue';
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
import AppLogoIcon from "@/components/AppLogoIcon.vue";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
navigationMenuTriggerStyle,
|
||||
} from '@/components/ui/navigation-menu';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||
import { getInitials } from '@/composables/useInitials';
|
||||
import type { BreadcrumbItem, NavItem } from '@/types';
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { AppWindowIcon, BoltIcon, Menu, Search, ServerIcon } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import UserMenuContent from "@/components/UserMenuContent.vue";
|
||||
import { getInitials } from "@/composables/useInitials";
|
||||
import type { BreadcrumbItem, NavItem } from "@/types";
|
||||
import { Link, usePage } from "@inertiajs/vue3";
|
||||
import { AppWindowIcon, BoltIcon, Menu, Search, ServerIcon } from "lucide-vue-next";
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
@@ -35,7 +39,10 @@ const auth = computed(() => page.props.auth);
|
||||
const isCurrentRoute = computed(() => (url: string) => page.url === url);
|
||||
|
||||
const activeItemStyles = computed(
|
||||
() => (url: string) => (isCurrentRoute.value(url) ? 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : ''),
|
||||
() => (url: string) =>
|
||||
isCurrentRoute.value(url)
|
||||
? "text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100"
|
||||
: "",
|
||||
);
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
@@ -49,25 +56,31 @@ const mainNavItems: NavItem[] = [
|
||||
if (page.props.organisation) {
|
||||
mainNavItems.push({
|
||||
title: page.props.organisation.name,
|
||||
href: new URL(route('organisations.show', {
|
||||
organisation: page.props?.organisation?.id
|
||||
})).pathname,
|
||||
href: new URL(
|
||||
route("organisations.show", {
|
||||
organisation: page.props?.organisation?.id,
|
||||
}),
|
||||
).pathname,
|
||||
icon: BoltIcon,
|
||||
});
|
||||
mainNavItems.push({
|
||||
title: 'Applications',
|
||||
href: new URL(route('applications.index', {
|
||||
organisation: page.props?.organisation?.id
|
||||
})).pathname,
|
||||
title: "Applications",
|
||||
href: new URL(
|
||||
route("applications.index", {
|
||||
organisation: page.props?.organisation?.id,
|
||||
}),
|
||||
).pathname,
|
||||
icon: AppWindowIcon,
|
||||
});
|
||||
mainNavItems.push({
|
||||
title: 'Servers',
|
||||
href: new URL(route('servers.index', {
|
||||
organisation: page.props?.organisation?.id
|
||||
})).pathname,
|
||||
title: "Servers",
|
||||
href: new URL(
|
||||
route("servers.index", {
|
||||
organisation: page.props?.organisation?.id,
|
||||
}),
|
||||
).pathname,
|
||||
icon: ServerIcon,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const rightNavItems: NavItem[] = [
|
||||
@@ -99,7 +112,9 @@ const rightNavItems: NavItem[] = [
|
||||
<SheetContent side="left" class="w-[300px] p-6">
|
||||
<SheetTitle class="sr-only">Navigation Menu</SheetTitle>
|
||||
<SheetHeader class="flex justify-start text-left">
|
||||
<AppLogoIcon class="size-6 fill-current text-black dark:text-white" />
|
||||
<AppLogoIcon
|
||||
class="size-6 fill-current text-black dark:text-white"
|
||||
/>
|
||||
</SheetHeader>
|
||||
<div class="flex h-full flex-1 flex-col justify-between space-y-4 py-6">
|
||||
<nav class="-mx-3 space-y-1">
|
||||
@@ -110,7 +125,11 @@ const rightNavItems: NavItem[] = [
|
||||
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent"
|
||||
:class="activeItemStyles(item.href)"
|
||||
>
|
||||
<component v-if="item.icon" :is="item.icon" class="h-5 w-5" />
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
{{ item.title }}
|
||||
</Link>
|
||||
</nav>
|
||||
@@ -123,7 +142,11 @@ const rightNavItems: NavItem[] = [
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center space-x-2 text-sm font-medium"
|
||||
>
|
||||
<component v-if="item.icon" :is="item.icon" class="h-5 w-5" />
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -140,12 +163,24 @@ const rightNavItems: NavItem[] = [
|
||||
<div class="hidden h-full lg:flex lg:flex-1">
|
||||
<NavigationMenu class="ml-10 flex h-full items-stretch">
|
||||
<NavigationMenuList class="flex h-full items-stretch space-x-2">
|
||||
<NavigationMenuItem v-for="(item, index) in mainNavItems" :key="index" class="relative flex h-full items-center">
|
||||
<NavigationMenuItem
|
||||
v-for="(item, index) in mainNavItems"
|
||||
:key="index"
|
||||
class="relative flex h-full items-center"
|
||||
>
|
||||
<Link :href="item.href">
|
||||
<NavigationMenuLink
|
||||
:class="[navigationMenuTriggerStyle(), activeItemStyles(item.href), 'h-9 cursor-pointer px-3']"
|
||||
:class="[
|
||||
navigationMenuTriggerStyle(),
|
||||
activeItemStyles(item.href),
|
||||
'h-9 cursor-pointer px-3',
|
||||
]"
|
||||
>
|
||||
<component v-if="item.icon" :is="item.icon" class="mr-2 h-4 w-4" />
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="mr-2 h-4 w-4"
|
||||
/>
|
||||
{{ item.title }}
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
@@ -169,10 +204,22 @@ const rightNavItems: NavItem[] = [
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size="icon" as-child class="group h-9 w-9 cursor-pointer">
|
||||
<a :href="item.href" target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
as-child
|
||||
class="group h-9 w-9 cursor-pointer"
|
||||
>
|
||||
<a
|
||||
:href="item.href"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="sr-only">{{ item.title }}</span>
|
||||
<component :is="item.icon" class="size-5 opacity-80 group-hover:opacity-100" />
|
||||
<component
|
||||
:is="item.icon"
|
||||
class="size-5 opacity-80 group-hover:opacity-100"
|
||||
/>
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -193,8 +240,14 @@ const rightNavItems: NavItem[] = [
|
||||
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
|
||||
>
|
||||
<Avatar class="size-8 overflow-hidden rounded-full">
|
||||
<AvatarImage v-if="auth.user.avatar" :src="auth.user.avatar" :alt="auth.user.name" />
|
||||
<AvatarFallback class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white">
|
||||
<AvatarImage
|
||||
v-if="auth.user.avatar"
|
||||
:src="auth.user.avatar"
|
||||
:alt="auth.user.name"
|
||||
/>
|
||||
<AvatarFallback
|
||||
class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white"
|
||||
>
|
||||
{{ getInitials(auth.user?.name) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -208,8 +261,13 @@ const rightNavItems: NavItem[] = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.breadcrumbs.length > 1" class="flex w-full border-b border-sidebar-border/70">
|
||||
<div class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl">
|
||||
<div
|
||||
v-if="props.breadcrumbs.length > 1"
|
||||
class="flex w-full border-b border-sidebar-border/70"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
|
||||
>
|
||||
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user