---
title: "Custom cards"
package: "Telemetry UI for Laravel"
version: "v0"
version_tag: "0.3.0"
url: "https://staging.cbox.dk/packages/laravel-telemetry-ui/docs/v0/extension-points/custom-cards"
---

# Custom cards

Any package (or the app itself) can contribute pages and cards. A card is a
Livewire component: PHP + Blade, no JavaScript build, using whatever
PromQL/TraceQL/LogQL fits — including metrics and spans the core UI knows
nothing about.

```php
// e.g. in cboxdk/queue-autoscale's service provider
use Cbox\TelemetryUi\Facades\TelemetryUi;

public function boot(): void
{
    if (class_exists(TelemetryUi::class)) {
        TelemetryUi::page('autoscale', 'Autoscale', group: 'Activity');
        TelemetryUi::card(\Cbox\QueueAutoscale\Ui\ScalingDecisions::class, page: 'autoscale');
        TelemetryUi::card(\Cbox\QueueAutoscale\Ui\WorkerFleet::class, page: 'autoscale');
    }
}
```

That's all the wiring you need — **no routes to register.** The dashboard's
catch-all route resolves any registered page slug to a generic page view that
renders the page's cards, so `/telemetry-ui/autoscale` works the moment you
`page('autoscale', …)`. Register the cards on that slug and they appear.

`page()` also takes an optional `detectMetric:` name pattern: the page only
shows when a matching metric exists in the backend — the same autodetection
the built-in Statamic group uses. `group:` places it under a sidebar heading,
`icon:` sets its glyph.

```php
use Cbox\TelemetryUi\Cards\Card;

final class ScalingDecisions extends Card
{
    public function render(): View
    {
        [$start, $end] = $this->range();

        return view('queue-autoscale::cards.scaling-decisions', [
            'series' => $this->toChartSeries($this->metrics()->queryRange(
                'sum by (queue) (rate(autoscale_scaling_events_total[5m])) * 60',
                $start, $end,
            ), label: 'queue'),
        ]);
    }
}
```

```blade
<x-telemetry-ui::card title="Scaling decisions" span="2">
    <x-telemetry-ui::chart :series="$series" type="bar" unit="events/min" />
</x-telemetry-ui::card>
```

## Interactive cards

Cards are ordinary Livewire components, so actions and forms just work:

```php
final class ExceptionActions extends Card
{
    public function createTicket(string $exceptionClass): void
    {
        // call Linear/GitHub, dispatch a job, open a modal…
    }
}
```

Long-running/AI output can stream with `wire:stream`. This is the intended
home for ticket-creation and AI-resolve flows on the roadmap.

## Conventions

- Query through `$this->metrics()` / `$this->traces()` / `$this->logs()` so
  named connections and tenancy keep working.
- Catch `SourceException` and render an inline error state; a broken backend
  must not take the page down.
- Respect `$this->range()`; don't hardcode time windows.

## Add, replace, remove

The registry supports more than appending — useful for white-labelling or
embedding, where you want to reshape the built-in dashboard:

```php
use Cbox\TelemetryUi\Facades\TelemetryUi;

// Add — append a card to any page (default: the dashboard).
TelemetryUi::card(MyCard::class, page: 'requests');

// Replace — swap a page's whole card list for your own (a branded dashboard).
TelemetryUi::setCards('dashboard', [MyHeadline::class, MyChart::class]);

// Remove — drop a single built-in card…
TelemetryUi::removeCard(\Cbox\TelemetryUi\Cards\Builtin\JobsOverview::class, 'dashboard');

// …or a whole page/section from the sidebar + routing.
TelemetryUi::removePage('users');
```

Re-registering a page slug with `TelemetryUi::page(...)` overwrites it, so you
can relabel or regroup a built-in page too. To *extend* a built-in card instead
of replacing it, subclass it (the built-in overview cards are not `final` — the
detail pages do exactly this) and register your subclass.
