---
title: Admin UI
description: A dashboard for every collection, and for running the server, at /_/.
section: Operations
order: 2
---

# Admin UI

<p class="lead">Every Sluurp server has a dashboard at <code>/_/</code>, like Django admin: generated from your schema, zero code. Each collection gets a table to browse, filter and edit, plus screens to operate the server.</p>

## For your data

- **A table per collection** with a filter box (`paid = true && total > 100`), sorting, column picker and paging.
- **A form per record** with the right input per field: date pickers, searchable relation pickers, image preview and upload.
- **History and undo:** every change with who and why, restore any version, deleted records in a recycle bin.
- **Explore and pivot tables,** charts and formulas over a collection, no export needed.
- **Rules** for read and write access, edited next to the collection.

## For the server

Logs, SQL console, backups, scheduled jobs, rate limits, mail, sign-in providers, payments, forms, feeds, translations and storage, each on its own screen. Reorder the Platform items by dragging or with `Alt`+arrow keys. `Ctrl+K` (`⌘K`) jumps to any screen, collection or record.

## Logs

Every request is logged: time, method, path, status, duration, user and IP. Search them with [Kibana's query language (KQL)](https://www.elastic.co/guide/en/kibana/current/kuery-query.html):

```text
status:5xx and path:/api/*
method:POST or took>500
not who:_superusers* and (status:4xx or status:5xx)
time>=2026-09-20 and time<now-2h
timeout 10.0.0.7
```

- `field:value` matches: `status` by class (`2xx`) or code, `method` exactly, `path`, `who` and `ip` by substring, or by wildcard with `*`.
- `=`, `!=`, `>`, `>=`, `<`, `<=` compare. `time` accepts a date, a datetime, or `now-15m`, `now-2h`, `now-7d`.
- Combine with `and`, `or`, `not` and parentheses. Adjacent terms are ANDed. A bare word matches anywhere.

The database keeps one week of logs. Older entries are archived to a gzipped file per day (`logs/2026-09-18.jsonl.gz` in the data directory), kept for 90 days. Searching a longer window transparently includes the archives.

## API

The **API** screen lists every endpoint from the project's [OpenAPI document](/docs/api#openapi), grouped by collection. Choose one, fill in its parameters and body (pre-filled from the collection's fields), and send it as yourself. You see the status, the time and the response, and can copy the request as `curl` or download `openapi.json` for a client generator.

## UI kit

The **UI kit** screen is a component workshop, like [Storybook](https://storybook.js.org). It has three modes:

- **Docs**: a page per component, with every prop taken from its source and each example with the code that draws it.
- **Controls**: one component with a field for each prop it takes.
- **Playground**: edit the code and see it drawn as you type, at one width or at phone, tablet and desktop side by side.

Stories use Storybook's [Component Story Format](https://storybook.js.org/docs/api/csf) (CSF 3), so story files work in both. Any `*.stories.tsx` next to a component is picked up:

```tsx title="client/kit/button.stories.tsx"
import { Button } from "./button.js";

export default {
  title: "Actions/Button",
  component: Button,
  args: { children: "Save changes", disabled: false },
  argTypes: { variant: { control: "select", options: ["default", "outline", "destructive"] } },
};

export const Primary = {};
export const Destructive = { args: { variant: "destructive", children: "Delete" } };
```

Without `render`, a story draws `component(args)`. Controls are inferred from the args' values; `argTypes` is only needed for a select. `play({ canvasElement, args })` runs after drawing. `parameters.order` places a story within its group when A to Z is not the order to read them in.

One Sluurp extension (Storybook ignores it): `argTypes: { collection: { control: "collection" } }` lets you pick a collection from the current project, and its rows are passed to `render(args, { loaded: { rows } })`. Every story is also drawn in a test.

### Your own components

An app's own story files show up too, under **Custom**, after the kit's. So a separate app can be a library of your own components, including ones built on the kit's:

```tsx title="components/gradient-button.tsx"
import { Button, type ButtonProps } from "sluurp/kit/button.js";

export interface GradientButtonProps extends ButtonProps {
  /** Where the gradient starts and ends. */
  tone?: "sunset" | "ocean";
}

export function GradientButton({ tone = "sunset", className, ...rest }: GradientButtonProps = {}) {
  return Button({ ...rest, className: `bg-linear-to-r text-white ${tone === "sunset" ? "from-amber-400 to-rose-500" : "from-sky-400 to-indigo-600"} ${className ?? ""}` });
}
```

```tsx title="components/gradient-button.stories.tsx"
import { GradientButton } from "./gradient-button.tsx";

export default {
  title: "Buttons/Gradient Button",
  component: GradientButton,
  args: { children: "Book the trip" },
};

export const Sunset = {};
export const Ocean = { args: { tone: "ocean" } };
```

To give it its own icon in the UI kit's list, set `parameters.icon` to an SVG, as markup or an element. Without one, it gets the default grid icon:

```tsx
export default {
  title: "Buttons/Gradient Button",
  component: GradientButton,
  parameters: {
    icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="10" x="3" y="7" rx="5"/></svg>',
  },
};
```

Use `stroke="currentColor"` (or `fill`) so the icon follows the list's colours, light and dark, selected or not.

Its props table lists `tone` and everything it takes from `ButtonProps`. Its examples import it from its own file and open in the Playground like the kit's. The app can be the one at `/` or one mounted beside it (`--public ui=path/to/app`).

## Compared with Django admin

**Sluurp has, Django admin doesn't:**

- Live updates: records changed by others update on your screen.
- Built-in history, restore and recycle bin for every collection.
- SQL console, backups, logs, jobs and rate limits in one place.
- Nothing to register: a collection appears as soon as it exists.
- Collaborative live translation editing.

**Django admin has, Sluurp doesn't (yet):**

- Per-model admin customisation in code: visible columns, form grouping, read-only fields.
- Custom bulk actions on selected rows.
- Inline editing of related records in the parent form.
- Date drill-down (year → month → day) above lists.

The admin UI is built with the same [UI kit](/docs/ui-kit) your apps use.
