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,9 +1,9 @@
|
||||
<script setup>
|
||||
import RadioButton from '@/components/RadioButton.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Head, router, useForm } from '@inertiajs/vue3';
|
||||
import { watch } from 'vue';
|
||||
import RadioButton from "@/components/RadioButton.vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import AppLayout from "@/layouts/AppLayout.vue";
|
||||
import { Head, router, useForm } from "@inertiajs/vue3";
|
||||
import { watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
providers: Array,
|
||||
@@ -42,7 +42,7 @@ loadServerTypes();
|
||||
function loadLocations() {
|
||||
if (form.provider && !props.locations) {
|
||||
router.reload({
|
||||
only: ['locations'],
|
||||
only: ["locations"],
|
||||
data: {
|
||||
provider: form.provider,
|
||||
},
|
||||
@@ -54,7 +54,7 @@ function loadLocations() {
|
||||
function loadServerTypes() {
|
||||
if (form.location && !props.serverTypes) {
|
||||
router.reload({
|
||||
only: ['serverTypes', 'images'],
|
||||
only: ["serverTypes", "images"],
|
||||
data: {
|
||||
provider: form.provider,
|
||||
location: form.location,
|
||||
@@ -94,11 +94,20 @@ function loadServerTypes() {
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.provider" class="flex flex-wrap gap-2">
|
||||
<RadioButton v-for="location in locations" v-model="form.location" :value="location.id" :disabled="location.disabled" name="location">
|
||||
<RadioButton
|
||||
v-for="location in locations"
|
||||
v-model="form.location"
|
||||
:value="location.id"
|
||||
:disabled="location.disabled"
|
||||
name="location"
|
||||
>
|
||||
{{ location.city }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.location" class="grid gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<div
|
||||
v-if="form.location"
|
||||
class="grid gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
>
|
||||
<RadioButton
|
||||
v-for="serverType in serverTypes?.sort((a, b) => a.cores - b.cores) ?? []"
|
||||
v-model="form.server_type"
|
||||
@@ -106,19 +115,35 @@ function loadServerTypes() {
|
||||
:disabled="serverType.disabled"
|
||||
name="server-type"
|
||||
>
|
||||
<h5 class="text-lg font-semibold uppercase tracking-tight">{{ serverType.name }}</h5>
|
||||
<h5 class="text-lg font-semibold uppercase tracking-tight">
|
||||
{{ serverType.name }}
|
||||
</h5>
|
||||
<p class="text-sm opacity-60">
|
||||
{{ serverType.cores }} cores • {{ serverType.memory }} GB RAM • {{ serverType.disk }} GB disk
|
||||
{{ serverType.cores }} cores • {{ serverType.memory }} GB RAM •
|
||||
{{ serverType.disk }} GB disk
|
||||
</p>
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.server_type" class="flex flex-wrap gap-2">
|
||||
<RadioButton v-for="image in images" v-model="form.image" :value="image.id" :disabled="image.disabled" name="image">
|
||||
<RadioButton
|
||||
v-for="image in images"
|
||||
v-model="form.image"
|
||||
:value="image.id"
|
||||
:disabled="image.disabled"
|
||||
name="image"
|
||||
>
|
||||
<h5 class="text-lg font-semibold tracking-tight">{{ image.name }}</h5>
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<Button @click="form.post(route('servers.store', { organisation: $page.props.organisation.id }))">Submit</Button>
|
||||
<Button
|
||||
@click="
|
||||
form.post(
|
||||
route('servers.store', { organisation: $page.props.organisation.id }),
|
||||
)
|
||||
"
|
||||
>Submit</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import AppLayout from "@/layouts/AppLayout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
|
||||
const props = defineProps({
|
||||
servers: {
|
||||
@@ -26,21 +26,33 @@ const props = defineProps({
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="flex justify-between items-center gap-3 p-4">
|
||||
<div class="flex items-center justify-between gap-3 p-4">
|
||||
<h2 class="text-3xl font-bold tracking-tight">Servers</h2>
|
||||
<div>
|
||||
<Button :as="Link" :href="route('servers.create', {
|
||||
organisation: $page.props.organisation.id,
|
||||
})">Create</Button>
|
||||
<Button
|
||||
:as="Link"
|
||||
:href="
|
||||
route('servers.create', {
|
||||
organisation: $page.props.organisation.id,
|
||||
})
|
||||
"
|
||||
>Create</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 rounded-xl p-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="server in servers.data" :key="`server{$servers.id}`" class="relative w-full">
|
||||
<Card
|
||||
v-for="server in servers.data"
|
||||
:key="`server{$servers.id}`"
|
||||
class="relative w-full"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ server.name }}</CardTitle>
|
||||
<CardDescription>
|
||||
<Badge :variant="server.status === 'active' ? 'success' : 'secondary'">{{ server.status.replace('-', ' ') }}</Badge> •
|
||||
{{ server.ipv4 || server.ipv6 }}</CardDescription
|
||||
<Badge :variant="server.status === 'active' ? 'success' : 'secondary'">{{
|
||||
server.status.replace("-", " ")
|
||||
}}</Badge>
|
||||
• {{ server.ipv4 || server.ipv6 }}</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<Link
|
||||
@@ -53,7 +65,6 @@ const props = defineProps({
|
||||
class="absolute inset-0"
|
||||
></Link>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { useCycleList, useInterval } from '@vueuse/core';
|
||||
import { DatabaseIcon, Layers2Icon, LoaderCircleIcon, PlusIcon, RefreshCwIcon } from 'lucide-vue-next';
|
||||
import { ref, watch } from 'vue';
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import AppLayout from "@/layouts/AppLayout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
import { useCycleList, useInterval } from "@vueuse/core";
|
||||
import {
|
||||
DatabaseIcon,
|
||||
Layers2Icon,
|
||||
LoaderCircleIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
defineProps({
|
||||
server: {
|
||||
@@ -19,12 +25,12 @@ defineProps({
|
||||
const selectedStep = ref(null);
|
||||
|
||||
const { state: provisionMessage, next } = useCycleList([
|
||||
'Provisioning your server...',
|
||||
'Updating dependencies...',
|
||||
'Tightening security...',
|
||||
'Installing packages...',
|
||||
'Configuring ssh...',
|
||||
'Installing docker...',
|
||||
"Provisioning your server...",
|
||||
"Updating dependencies...",
|
||||
"Tightening security...",
|
||||
"Installing packages...",
|
||||
"Configuring ssh...",
|
||||
"Installing docker...",
|
||||
]);
|
||||
const { counter, reset, pause, resume } = useInterval(5000, { controls: true });
|
||||
|
||||
@@ -57,9 +63,13 @@ watch(counter, () => {
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-3xl font-bold tracking-tight">{{ server.name }}</h2>
|
||||
<div>
|
||||
<Badge :variant="server.status === 'active' ? 'success' : 'secondary'">{{ server.status }}</Badge>
|
||||
<Badge :variant="server.status === 'active' ? 'success' : 'secondary'">{{
|
||||
server.status
|
||||
}}</Badge>
|
||||
</div>
|
||||
<div class="leading-none opacity-40">
|
||||
{{ server.ipv4 }} • {{ server.ipv6 }}
|
||||
</div>
|
||||
<div class="leading-none opacity-40">{{ server.ipv4 }} • {{ server.ipv6 }}</div>
|
||||
</div>
|
||||
|
||||
<template v-if="server.status === 'active'">
|
||||
@@ -86,15 +96,23 @@ watch(counter, () => {
|
||||
<Card v-for="service in server.services" :key="service.id">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<DatabaseIcon v-if="service.category === 'database'" class="size-5 opacity-50" />
|
||||
<DatabaseIcon
|
||||
v-if="service.category === 'database'"
|
||||
class="size-5 opacity-50"
|
||||
/>
|
||||
<CardTitle>{{ service.name }}</CardTitle>
|
||||
<Badge :variant="service.status === 'active' ? 'success' : 'secondary'">{{
|
||||
service.status.replace('-', ' ')
|
||||
}}</Badge>
|
||||
<Badge
|
||||
:variant="
|
||||
service.status === 'active' ? 'success' : 'secondary'
|
||||
"
|
||||
>{{ service.status.replace("-", " ") }}</Badge
|
||||
>
|
||||
</div>
|
||||
<CardDescription>
|
||||
<span class="capitalize">{{ service.type }}</span> {{ service.version }} •
|
||||
<Layers2Icon class="inline-block size-4" /> {{ service.slices?.length }} slices
|
||||
<span class="capitalize">{{ service.type }}</span>
|
||||
{{ service.version }} •
|
||||
<Layers2Icon class="inline-block size-4" />
|
||||
{{ service.slices?.length }} slices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent v-if="['postgres', 'valkey'].includes(service.type)">
|
||||
@@ -121,23 +139,39 @@ watch(counter, () => {
|
||||
<h3 class="mb-3 text-2xl font-semibold tracking-tight">Operations</h3>
|
||||
<Card>
|
||||
<CardContent class="py-4">
|
||||
<div v-for="operation in server.service_operations" :key="operation.id" class="flex gap-4">
|
||||
<div
|
||||
v-for="operation in server.service_operations"
|
||||
:key="operation.id"
|
||||
class="flex gap-4"
|
||||
>
|
||||
<div class="w-48 leading-none">{{ operation.target.name }}</div>
|
||||
<div class="w-full space-y-4">
|
||||
<div v-for="step in operation.steps" :key="step.id" class="flex items-center space-y-1">
|
||||
<div
|
||||
v-for="step in operation.steps"
|
||||
:key="step.id"
|
||||
class="flex items-center space-y-1"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-semibold leading-none">
|
||||
{{ step.name ?? 'Unnamed Step' }}
|
||||
{{ step.name ?? "Unnamed Step" }}
|
||||
</div>
|
||||
<div v-if="step.error_logs">
|
||||
<pre class="text-xs text-muted-foreground"
|
||||
>{{ step.error_logs_excerpt.length !== step.error_logs ? '... ' : ''
|
||||
>{{
|
||||
step.error_logs_excerpt.length !==
|
||||
step.error_logs
|
||||
? "... "
|
||||
: ""
|
||||
}}{{ step.error_logs_excerpt }}</pre
|
||||
>
|
||||
</div>
|
||||
<div v-else-if="step.logs">
|
||||
<pre class="text-xs text-muted-foreground"
|
||||
>{{ step.logs_excerpt.length !== step.logs ? '... ' : '' }}{{ step.logs_excerpt }}</pre
|
||||
>{{
|
||||
step.logs_excerpt.length !== step.logs
|
||||
? "... "
|
||||
: ""
|
||||
}}{{ step.logs_excerpt }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +223,10 @@ watch(counter, () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog :open="!!selectedStep" @update:open="($event) => (!$event ? (selectedStep = null) : null)">
|
||||
<Dialog
|
||||
:open="!!selectedStep"
|
||||
@update:open="($event) => (!$event ? (selectedStep = null) : null)"
|
||||
>
|
||||
<DialogContent class="md:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Logs for {{ selectedStep?.name }}</DialogTitle>
|
||||
@@ -200,7 +237,9 @@ watch(counter, () => {
|
||||
</section>
|
||||
<section v-if="selectedStep?.error_logs">
|
||||
<h3 class="text-sm font-medium">Error Logs</h3>
|
||||
<pre class="max-w-full overflow-x-scroll text-xs text-muted-foreground">{{ selectedStep?.error_logs }}</pre>
|
||||
<pre class="max-w-full overflow-x-scroll text-xs text-muted-foreground">{{
|
||||
selectedStep?.error_logs
|
||||
}}</pre>
|
||||
</section>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user