Demo Mode: you're exploring a live BackupMGR panel with sample data. Everything is read-only. Get BackupMGR →

License Key Required

No license key entered. Enter or update your key with php artisan news:license <key>, or purchase one at scriptgain.com.

Dashboard

dashboard

Status Shipped Default Shipped File resources/views/dashboard.blade.php
dashboard 286 lines
Not Checked Yet

Optional. Shown in the version history so the next person knows why.

Saving runs the same check. An invalid template is refused, not stored.

Compiled PHP

What Blade turns your template into. This is what the syntax check parses.

Loading…

Available Variables And Components

A guide to what this template can use, derived from the shipped source. Click any item to insert it at your cursor. It is a helpful reference, not a runtime guarantee.

Always Available

Validation error bag. Always present. Pairs with @error('field').

In This Template

Activity.
Attention.
Failed24H.
Refs.
Runs.
Stale Hosts.
Stats.
Storage.
Storage Device Count.
Success Rate.
Window Total.

Drop any of these in. Click to insert the opening tag at your cursor.

General

type, info, title
initials, src, name, size, md
color, neutral, dot
action, label, Record, modal
variant, primary, size, md, href, …
title, subtitle, padding, flush
name, action, method, POST, title, …
action, name, title, message, confirm, …
href, icon, danger
align, right, width
icon, folder, title, description
label, for, hint, error, required
href, icon, title, variant, secondary, …
name
type, text
name, title, subtitle, icon, tone, …
href, icon, active
title, subtitle, icon, back
title, subtitle, href, linkLabel
ids
id, label, Row
items
label, value, icon, trend, trendColor, …
color, neutral, pulse, label
name
flush
tabs, default
name, checked, label, description

Admin Panel

title, message, icon, folder, href, …
tree, active, rootLabel, rootHref
title, subtitle, icon, edit, action, …
title, icon, folder, subtitle, records, …
search, placeholder, createHref, createLabel
edit, delete, preview, name, title, …
record, kind, Page

Layouts

title, shellMaxWidth
title, description, heroless, maxWidth

Records

name, action, reference, trigger, icon

Site (Public)

href, title, description, icon, meta, …
title, message, icon, folder
title, subtitle, eyebrow, icon, crumbs, …
supportEmail, supportPhone
disclaimer, takedownContact, retentionDays, compact
title, subtitle, href, linkLabel, tone, …
maxWidth

Common Blade

Print a value, safely escaped. The default choice.
Print raw HTML. Only for content you trust.
Show something conditionally.
Loop over a collection.
Loop, with a fallback when the collection is empty.
Show a validation message for a field.
A small block of PHP. Never on line 1. Keep logic out of templates.
A comment that never reaches the browser.

Civic Helpers

Build a URL to a named route, e.g. a department page.
Link to a single news post.
Turn a stored upload path into a public URL.
A cache-busted URL for a bundled asset.
Read a config value, e.g. the site width class.
Build an absolute URL to a path.

No History Yet

Every save, revert, and reset of this template is recorded here, with a one-click way back to any of them.

The Template That Shipped With The Product

Read only. This is what Reset To Default puts back.

@php
    use Illuminate\Support\Str;

    $badge = ['success' => 'success', 'running' => 'info', 'queued' => 'neutral', 'warn' => 'warn', 'failed' => 'danger'];
    $label = ['success' => 'Success', 'running' => 'Running', 'queued' => 'Queued', 'warn' => 'Warnings', 'failed' => 'Failed'];
    $fmt = function ($bytes) {
        if ($bytes === null) return '–';
        $u = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($u) - 1) { $bytes /= 1024; $i++; }
        return round($bytes, $i ? 1 : 0) . ' ' . $u[$i];
    };

    // KPI row – number + label + a meaningful one-line subtext, grouped with the icon.
    $kpis = [
        ['label' => 'Directors', 'value' => $stats['directors'], 'icon' => 'cloud',
            'sub' => 'Backup controllers', 'tone' => 'muted'],
        ['label' => 'Protected Hosts', 'value' => $stats['hosts'], 'icon' => 'server',
            'sub' => $staleHosts ? $staleHosts . ' ' . Str::plural('agent', $staleHosts) . ' need attention' : 'All agents reporting in',
            'tone' => $staleHosts ? 'amber' : 'emerald'],
        ['label' => 'Active Jobs', 'value' => $stats['jobs'], 'icon' => 'clock',
            'sub' => 'Scheduled and enabled', 'tone' => 'muted'],
        ['label' => 'Restore Points', 'value' => number_format($stats['restore_points']), 'icon' => 'archive',
            'sub' => 'Recoverable snapshots', 'tone' => 'muted'],
    ];
    $toneClass = ['muted' => 'text-slate-400', 'amber' => 'text-amber-600', 'emerald' => 'text-emerald-600'];

    // 14-day activity bar chart geometry (inline SVG, no chart library).
    $cw = 700; $ch = 150; $padT = 12; $padB = 22;
    $plotH = $ch - $padT - $padB;
    $baseY = $padT + $plotH;
    $n = max(1, count($activity));
    $slot = ($cw - 8) / $n;
    $barW = min(26, $slot * 0.62);
    $maxVal = max(1, max(array_column($activity, 'total') ?: [0]));

    // Storage gauge (semicircle) geometry.
    $gaugeLen = 276.46; // ~ pi * r, r = 88
    if (! empty($storage['total'])) {
        $storePct = (int) round($storage['used'] / max(1, $storage['total']) * 100);
        $storeOver = $storePct > 90;
        $gaugeDash = round(min(100, $storePct) / 100 * $gaugeLen, 1);
    }
@endphp

<x-layouts.app title="Dashboard">
    {{-- Brand accent bound to the runtime --color-brand-* var so a custom accent still applies. --}}
    <style>
        .bk-ok-fill { fill: var(--color-brand-500); }
        .bk-ok-stroke { stroke: var(--color-brand-500); }
        .bk-ok-bg { background-color: var(--color-brand-500); }
    </style>

    <x-page-header title="Dashboard" subtitle="Fleet backup health at a glance.">
        <x-slot:actions>
            <x-button variant="secondary" size="sm" icon="cloud" href="{{ route('directors.index') }}">Directors</x-button>
            <x-button size="sm" icon="plus" href="{{ route('directors.index') }}">Add Host</x-button>
        </x-slot:actions>
    </x-page-header>

    {{-- KPI row --}}
    <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
        @foreach ($kpis as $k)
            <div class="group relative flex flex-col overflow-hidden rounded-xl bg-white ring-1 ring-slate-200 shadow-sm transition hover:shadow-md hover:ring-brand-200">
                <span class="h-1 w-full bg-gradient-to-r from-brand-400 to-brand-600"></span>
                <div class="flex flex-1 items-center gap-4 p-5">
                    <span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-brand-50 text-brand-600 ring-1 ring-brand-200">
                        <x-icon :name="$k['icon']" class="h-5 w-5" />
                    </span>
                    <div class="ml-auto text-right">
                        <p class="text-2xl font-semibold tracking-tight text-slate-900 tabular">{{ $k['value'] }}</p>
                        <p class="mt-0.5 text-sm font-medium text-slate-600">{{ $k['label'] }}</p>
                        <p class="mt-0.5 text-xs font-medium {{ $toneClass[$k['tone']] }}">{{ $k['sub'] }}</p>
                    </div>
                </div>
            </div>
        @endforeach
    </div>

    {{-- Activity + storage --}}
    <div class="mt-4 grid grid-cols-1 lg:grid-cols-3 gap-4 items-stretch">
        {{-- Backup activity (signature visual) --}}
        <x-card title="Backup Activity" subtitle="Runs per day, last 14 days" class="lg:col-span-2 h-full">
            <x-slot:actions>
                @if ($successRate !== null)
                    <span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-200">
                        <x-icon name="check-circle" class="h-3.5 w-3.5" /> {{ $successRate }}% success
                    </span>
                @endif
            </x-slot:actions>

            @if ($windowTotal === 0)
                <div class="flex h-40 flex-col items-center justify-center text-center">
                    <span class="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-slate-100 text-slate-400"><x-icon name="clock" class="h-5 w-5" /></span>
                    <p class="mt-3 text-sm text-slate-500">No backup runs in the last 14 days.</p>
                </div>
            @else
                <svg viewBox="0 0 {{ $cw }} {{ $ch }}" width="100%" class="block h-auto" role="img" aria-label="Backup runs per day over the last 14 days">
                    {{-- baseline --}}
                    <line x1="4" y1="{{ $baseY + 0.5 }}" x2="{{ $cw - 4 }}" y2="{{ $baseY + 0.5 }}" stroke="#e2e8f0" stroke-width="1" />
                    @foreach ($activity as $i => $d)
                        @php
                            $cx = 4 + $slot * $i + $slot / 2;
                            $x = round($cx - $barW / 2, 1);
                            $h = $d['total'] ? max(3, round($d['total'] / $maxVal * $plotH, 1)) : 0;
                            $sh = $d['total'] ? round($d['success'] / $d['total'] * $h, 1) : 0;
                            $ih = round($h - $sh, 1);
                        @endphp
                        @if ($h === 0.0 || $h === 0)
                            <rect x="{{ $x }}" y="{{ $baseY - 3 }}" width="{{ round($barW, 1) }}" height="3" rx="1.5" fill="#e2e8f0" />
                        @else
                            @if ($ih > 0)
                                <rect x="{{ $x }}" y="{{ round($baseY - $h, 1) }}" width="{{ round($barW, 1) }}" height="{{ $ih }}" rx="2" fill="#f43f5e" />
                            @endif
                            @if ($sh > 0)
                                <rect x="{{ $x }}" y="{{ round($baseY - $sh, 1) }}" width="{{ round($barW, 1) }}" height="{{ $sh }}" rx="2" class="bk-ok-fill" />
                            @endif
                        @endif
                        @php
                            $fail = $d['total'] - $d['success'];
                            $tip = $d['label'] . ' – ' . $d['total'] . ' run' . ($d['total'] == 1 ? '' : 's');
                            if ($d['total']) {
                                $tip .= ' (' . $d['success'] . ' ok' . ($fail ? ', ' . $fail . ' failed/warn' : '') . ')';
                            }
                        @endphp
                        <rect x="{{ round(4 + $slot * $i, 1) }}" y="0" width="{{ round($slot, 1) }}" height="{{ round($baseY, 1) }}" fill="transparent" style="cursor:default" data-tip="{{ $tip }}" />
                        @if ($i === 0 || $i === intdiv($n, 2) || $i === $n - 1)
                            <text x="{{ round($cx, 1) }}" y="{{ $ch - 6 }}" text-anchor="{{ $i === 0 ? 'start' : ($i === $n - 1 ? 'end' : 'middle') }}" fill="#94a3b8" style="font-size:11px">{{ $d['label'] }}</text>
                        @endif
                    @endforeach
                </svg>

                <div class="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-xs font-medium text-slate-500">
                    <span class="inline-flex items-center gap-1.5"><span class="h-2.5 w-2.5 rounded-sm bk-ok-bg"></span> Successful</span>
                    <span class="inline-flex items-center gap-1.5"><span class="h-2.5 w-2.5 rounded-sm" style="background-color:#f43f5e"></span> Failed or warning</span>
                    <span class="ml-auto tabular text-slate-400">{{ number_format($windowTotal) }} {{ Str::plural('run', $windowTotal) }} total</span>
                </div>
            @endif

            <x-slot:footer>
                <div class="flex items-center gap-6">
                    <div class="flex items-center gap-2.5">
                        <span class="inline-flex h-9 w-9 items-center justify-center rounded-lg {{ $failed24h ? 'bg-rose-50 text-rose-600 ring-1 ring-rose-200' : 'bg-white text-slate-400 ring-1 ring-slate-200' }}"><x-icon name="warning" class="h-4 w-4" /></span>
                        <div>
                            <p class="text-lg font-semibold leading-tight tabular {{ $failed24h ? 'text-rose-600' : 'text-slate-900' }}">{{ $failed24h }}</p>
                            <p class="text-xs text-slate-500">Failures (24h)</p>
                        </div>
                    </div>
                    <span class="h-9 w-px bg-slate-200"></span>
                    <div class="flex items-center gap-2.5">
                        <span class="inline-flex h-9 w-9 items-center justify-center rounded-lg {{ $staleHosts ? 'bg-amber-50 text-amber-600 ring-1 ring-amber-200' : 'bg-white text-slate-400 ring-1 ring-slate-200' }}"><x-icon name="server" class="h-4 w-4" /></span>
                        <div>
                            <p class="text-lg font-semibold leading-tight tabular {{ $staleHosts ? 'text-amber-600' : 'text-slate-900' }}">{{ $staleHosts }}</p>
                            <p class="text-xs text-slate-500">Stale agents · 10+ min</p>
                        </div>
                    </div>
                </div>
            </x-slot:footer>
        </x-card>

        {{-- Storage gauge --}}
        <x-card title="Storage Used" subtitle="Across all repositories" class="h-full">
            @if (! empty($storage['total']))
                <div>
                    <div class="mx-auto w-full max-w-[240px]">
                        <svg viewBox="0 0 200 122" width="100%" role="img" aria-label="Storage {{ $storePct }} percent used">
                            <path d="M12 110 A88 88 0 0 1 188 110" fill="none" stroke="#e2e8f0" stroke-width="14" stroke-linecap="round" />
                            <path d="M12 110 A88 88 0 0 1 188 110" fill="none" stroke-width="14" stroke-linecap="round"
                                stroke-dasharray="{{ $gaugeDash }} 1000"
                                @class(['bk-ok-stroke' => ! $storeOver]) @style(['stroke:#f43f5e' => $storeOver]) />
                            <text x="100" y="92" text-anchor="middle" fill="#0f172a" style="font-size:38px;font-weight:700;font-variant-numeric:tabular-nums">{{ $storePct }}%</text>
                            <text x="100" y="110" text-anchor="middle" fill="#94a3b8" style="font-size:11px;letter-spacing:.02em">used</text>
                        </svg>
                    </div>
                    <div class="mt-1 flex items-baseline justify-between">
                        <span class="text-lg font-semibold text-slate-900 tabular">{{ $fmt($storage['used']) }}</span>
                        <span class="text-sm text-slate-500 tabular">of {{ $fmt($storage['total']) }}</span>
                    </div>
                    <p class="mt-1 text-xs text-slate-400">
                        {{ $storageDeviceCount }} {{ Str::plural('device', $storageDeviceCount) }} ·
                        {{ $fmt(max(0, $storage['total'] - $storage['used'])) }} free
                    </p>
                </div>
            @else
                <div class="flex flex-col items-center justify-center py-6 text-center">
                    <span class="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-slate-100 text-slate-400"><x-icon name="database" class="h-5 w-5" /></span>
                    <p class="mt-3 text-sm text-slate-500">No storage devices detected.</p>
                    <a href="{{ route('directors.index') }}" class="mt-1 text-sm font-medium text-brand-700 hover:underline">Detect disks</a>
                </div>
            @endif
        </x-card>
    </div>

    @if ($attention->isNotEmpty())
        <div class="mt-6">
            <x-card title="Needs Attention" subtitle="Recent failed or warning runs" flush>
                <div x-data="{ selected: [], confirming: false, allIds: [{{ $attention->pluck('id')->implode(',') }}], submitBulk() { const f = this.$refs.bulkForm; f.querySelectorAll('input.js-dyn').forEach(n => n.remove()); this.selected.forEach(id => { const i = document.createElement('input'); i.type='hidden'; i.name='ids[]'; i.value=id; i.className='js-dyn'; f.appendChild(i); }); f.submit(); } }">
                    <form method="POST" action="{{ route('runs.bulk-destroy') }}" x-ref="bulkForm" class="hidden">@csrf @method('DELETE')</form>
                    <div x-show="selected.length" x-cloak class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-brand-50 px-4 py-2.5">
                        <span class="text-sm font-medium text-brand-800"><span x-text="selected.length"></span> selected</span>
                        <div class="flex items-center gap-2">
                            <template x-if="! confirming"><x-button type="button" variant="danger" size="sm" icon="trash" x-on:click="confirming = true">Delete Selected</x-button></template>
                            <template x-if="confirming">
                                <div class="flex flex-wrap items-center gap-2">
                                    <span class="text-sm text-brand-800">Delete <span x-text="selected.length"></span> run(s)?</span>
                                    <x-button type="button" variant="secondary" size="sm" x-on:click="confirming = false">Cancel</x-button>
                                    <x-button type="button" variant="danger" size="sm" icon="trash" x-on:click="submitBulk()">Confirm Delete</x-button>
                                </div>
                            </template>
                        </div>
                    </div>
                    <x-table flush>
                        <thead><tr>
                            <th class="w-10">Select</th>
                            <th>Host / Job</th><th>Status</th><th class="text-right">When</th>
                        </tr></thead>
                        <tbody>
                            @foreach ($attention as $r)
                                <tr class="cursor-pointer" onclick="window.location='{{ route('runs.show', $r) }}'">
                                    <td onclick="event.stopPropagation()">@include('jobs._select-toggle', ['id' => $r->id])</td>
                                    <td>
                                        <div class="font-medium text-slate-900 truncate">{{ $r->job?->host?->name ?? '–' }}</div>
                                        <div class="text-xs text-slate-500 truncate">{{ $r->job?->name ?? '–' }}</div>
                                    </td>
                                    <td><x-badge :color="$r->status === 'failed' ? 'danger' : 'warn'" dot>{{ ucfirst($r->status) }}</x-badge></td>
                                    <td class="text-right text-slate-500" data-tip="{{ $r->created_at?->format('M j, Y g:i A') }}">{{ $r->created_at?->diffForHumans() }}</td>
                                </tr>
                            @endforeach
                        </tbody>
                    </x-table>
                </div>
            </x-card>
        </div>
    @endif

    <div class="mt-6">
        <x-card title="Recent Backup Runs" subtitle="Latest activity across all hosts" :flush="$runs->isNotEmpty()">
            <x-slot:actions>
                <x-button variant="ghost" size="sm" href="{{ route('snapshots.index') }}">View All</x-button>
            </x-slot:actions>

            @if ($runs->isEmpty())
                <x-empty-state icon="archive" title="No Runs Yet" description="Add a host and a backup job, then run it to see activity here.">
                    <x-slot:action><x-button icon="plus" href="{{ route('directors.index') }}">Add a Director</x-button></x-slot:action>
                </x-empty-state>
            @else
                <div x-data="{ selected: [], confirming: false, allIds: [{{ $runs->pluck('id')->implode(',') }}], submitBulk() { const f = this.$refs.bulkForm; f.querySelectorAll('input.js-dyn').forEach(n => n.remove()); this.selected.forEach(id => { const i = document.createElement('input'); i.type='hidden'; i.name='ids[]'; i.value=id; i.className='js-dyn'; f.appendChild(i); }); f.submit(); } }">
                    <form method="POST" action="{{ route('runs.bulk-destroy') }}" x-ref="bulkForm" class="hidden">@csrf @method('DELETE')</form>
                    <div x-show="selected.length" x-cloak class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-brand-50 px-4 py-2.5">
                        <span class="text-sm font-medium text-brand-800"><span x-text="selected.length"></span> selected</span>
                        <div class="flex items-center gap-2">
                            <template x-if="! confirming"><x-button type="button" variant="danger" size="sm" icon="trash" x-on:click="confirming = true">Delete Selected</x-button></template>
                            <template x-if="confirming">
                                <div class="flex flex-wrap items-center gap-2">
                                    <span class="text-sm text-brand-800">Delete <span x-text="selected.length"></span> run(s)?</span>
                                    <x-button type="button" variant="secondary" size="sm" x-on:click="confirming = false">Cancel</x-button>
                                    <x-button type="button" variant="danger" size="sm" icon="trash" x-on:click="submitBulk()">Confirm Delete</x-button>
                                </div>
                            </template>
                        </div>
                    </div>
                    <x-table flush>
                        <thead>
                            <tr><th class="w-10">Select</th><th>Host / Job</th><th>Status</th><th>Size</th><th class="text-right">When</th></tr>
                        </thead>
                        <tbody>
                            @foreach ($runs as $r)
                                <tr class="cursor-pointer" onclick="window.location='{{ route('runs.show', $r) }}'">
                                    <td onclick="event.stopPropagation()">@include('jobs._select-toggle', ['id' => $r->id])</td>
                                    <td>
                                        <div class="font-medium text-slate-900 truncate">{{ $r->job?->host?->name ?? '–' }}</div>
                                        <div class="text-xs text-slate-500 truncate">{{ $r->job?->name ?? '–' }}</div>
                                    </td>
                                    <td><x-badge :color="$badge[$r->status] ?? 'neutral'" dot>{{ $label[$r->status] ?? ucfirst($r->status) }}</x-badge></td>
                                    <td class="tabular text-slate-600">{{ $fmt($r->bytes_in) }}</td>
                                    <td class="text-right text-slate-500" data-tip="{{ $r->created_at?->format('M j, Y g:i A') }}">{{ $r->created_at?->diffForHumans() }}</td>
                                </tr>
                            @endforeach
                        </tbody>
                    </x-table>
                </div>
            @endif
        </x-card>
    </div>
</x-layouts.app>

Blade In One Screen

The directives you will use most.

{{ $variable }}
Print a value, escaped. Safe by default.
{!! $html !!}
Print raw HTML. Only for content you trust.
@if / @else / @endif
Show something conditionally.
@foreach / @endforeach
Loop over a collection.
<x-card>
Use a shared component. Every component in Shared Components is available.
{{-- comment --}}
A comment that never reaches the browser.

House Rules

Follow these and your edits will survive updates.

  • Never start line 1 with @php. Blade mis-parses it and the page fails. The editor refuses to save it.
  • Keep logic out of templates. If you need a calculation, it belongs in a composer or component class, not here.
  • The shipped file is never modified. Reset To Default always gets you back.
  • Every save is versioned. You can revert to any earlier version in one click.
  • Press Tab inside the editor to indent rather than jump to the next field.