> A self-hosted start page for your homelab. It shows the weather and your server's vitals at the top, and your links below, each with a live online/offline indicator.
> Source: https://orochibraru.com/bercail/canary/docs/readme · Site index: https://orochibraru.com/llms.txt

# Bercail

A self-hosted start page for your homelab. It shows the weather and your server's vitals at the
top, and your links below, each with a live online/offline indicator.

<!-- Regenerate with `bun run screenshots`; do not edit by hand. -->

[![Bercail: your homelab, on every new tab](https://orochibraru.com/images/baa65d042dc3b100c705cbb694e14ec08afaf27a.webp)](https://orochibraru.com/bercail/canary/docs/features)

## Features

- **Links in groups.** Each link has a title, description, URL, target (same or new tab) and an
  icon, either an image URL or a name from [Dashboard Icons](https://dashboardicons.com/icons).
  Drag a link by its grip to reorder it or move it to another group.
- **Online status.** The server sends a `HEAD` request to every link (5 second timeout, cached
  for 30 seconds). Any response below 500 counts as online. Self-signed certificates are accepted.
- **Weather.** Current conditions and a 5-day forecast from
  [Open-Meteo](https://open-meteo.com/), no API key needed.
- **Analytics.** Live visitors, plus visitors and pageviews over 24 hours, 7, 30 and 365 days and
  all time per website from a self-hosted [Umami](https://umami.is/) v3 instance. Connect it in
  Settings with the instance URL and an API key.
- **Tasks.** Your open tasks from [tasks.org](https://tasks.org/) or any CalDAV server (Nextcloud,
  Radicale, ...), soonest due first, with overdue ones in red. Connect it in Settings with the
  CalDAV URL, your username and an app password. Read-only.
- **System stats.** CPU, RAM, disk, temperature and GPU gauges for the host the app runs on.
- **Search.** Start typing anywhere, or press `/`, `Cmd+K` or `Ctrl+K`, to jump to any link or
  search the web.
- **Keyboard first.** The arrow keys move between links and tasks across every group, `Enter` opens
  the link or ticks the task off, and `?` lists every shortcut.
- **Backup and restore.** Export your groups and links to JSON from Settings, and import them back.
- **Appearance.** Light, dark or system per device, and for every device a Bordeaux, blue, teal,
  green, amber or rose accent, a sans or monospace font, and rounded or boxy corners.

The page refreshes its stats every 5 seconds.

![The search palette](https://orochibraru.com/images/4a04a5fc72c64a2f6395792b885116cc0e0c9430.webp)
![Settings](https://orochibraru.com/images/83a631eb9047ae85cd92a409a50148e37e85b8bc.webp)

## Running with Docker Compose

```yaml
services:
  bercail:
    image: orochibraru/bercail:latest
    restart: unless-stopped
    ports:
      - '3000:3000'
    volumes:
      - ./data:/app/data
```

Data lives in a SQLite file under `/app/data`. Database migrations run automatically on startup.

## Configuration

Everything is optional. For local development, copy [`.env.example`](https://github.com/orochibraru/bercail/blob/main/.env.example) to `.env`.

| Variable                | Default                                        | Description                                                                                   |
| ----------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `PROTOCOL_HEADER`       | `x-forwarded-proto` in Docker                  | Header a reverse proxy puts the public scheme in. Without it, the scheme is taken as `https`. |
| `HOST_HEADER`           | `x-forwarded-host` in Docker                   | Header a reverse proxy puts the public host in. Without it, the `Host` header.                |
| `PORT`                  | `3000`                                         | Port the server listens on.                                                                   |
| `HOST`                  | `0.0.0.0`                                      | Address the server binds to.                                                                  |
| `DB_FILE_NAME`          | `data/db.sqlite`                               | Path to the SQLite database.                                                                  |
| `PUID` / `PGID`         | `10001`                                        | Docker only. The uid/gid the server runs as. The data volume is chowned to match on startup.  |
| `DISK_STATS_PATH`       | `/System/Volumes/Data` on macOS, `/` elsewhere | Filesystem the disk gauge reports on.                                                         |
| `WEATHER_LAT`           | none                                           | Latitude for the weather. A location picked in Settings takes precedence.                     |
| `WEATHER_LON`           | none                                           | Longitude for the weather. Must be set together with `WEATHER_LAT`.                           |
| `WEATHER_LOCATION_NAME` | `Home`                                         | Label shown for the location set through the variables above.                                 |
| `WEATHER_UNITS`         | `celsius`                                      | `celsius` or `fahrenheit`.                                                                    |
| `OIDC_ISSUER`           | none                                           | Issuer URL of your OpenID Connect provider. Setting it turns sign-in on.                      |
| `OIDC_CLIENT_ID`        | none                                           | Client ID. Required with `OIDC_ISSUER`.                                                       |
| `OIDC_CLIENT_SECRET`    | none                                           | Client secret. Required with `OIDC_ISSUER`.                                                   |
| `OIDC_ALLOWED_EMAILS`   | none                                           | Comma-separated emails allowed to sign in. Unset lets in anyone the provider signs in.        |

Serve the app over HTTPS, through a reverse proxy. Reached over plain HTTP with no proxy in front,
it still takes the scheme as `https`: forms are rejected and sign-in breaks.

### Weather location

The first of these that is set wins:

1. A city picked in Settings.
2. `WEATHER_LAT` and `WEATHER_LON`.
3. The browser's location, if you allow it. This one is not saved on the server.

### Sign-in

Without `OIDC_ISSUER` the app is open to anyone who can reach it. With it, every page asks for a
sign-in through your provider (Pocket ID, Authentik, Authelia, Keycloak, ...). Register a
confidential client with the redirect URL `https://<your domain>/auth/callback`. Sessions last 30 days and
renew while in use.

Set `OIDC_ALLOWED_EMAILS` unless the provider already restricts who can use this client. With a
provider anyone can sign up to, leaving it unset lets anyone in.

### Host metrics

- **Temperature** is read from `/sys/class/thermal` on Linux. On macOS it needs
  [`osx-cpu-temp`](https://github.com/lavoiesl/osx-cpu-temp) (`brew install osx-cpu-temp`).
- **GPU** usage needs `nvidia-smi` on the host.

A metric that can't be read shows `N/A` without affecting the others.

## New tab extension

The extension in `extension/` shows your Bercail instance on every new tab.

1. Download `bercail-extension-X.Y.Z.zip` from the latest release and unzip it. To build it yourself,
   run `bun run package:extension`, which writes `dist/bercail-extension.zip`.
2. In Chrome, open `chrome://extensions`, turn on Developer mode, then **Load unpacked** and pick
   the unzipped folder.
3. Open a new tab and set your Bercail URL, then allow access to that site when asked. With
   sign-in on, you sign in once in the tab that opens. When the session expires, a new tab sends
   you back to the sign-in page.

If you put Bercail behind an auth proxy (Pangolin, Cloudflare Access) instead of using its own
sign-in, let `/service-worker.js` through without auth. Browsers fetch that file without cookies,
so otherwise the cache never installs.

After the first visit the page is served from a service worker cache, so new tabs open without
waiting on the server. A copy older than five minutes is skipped for a fresh one, so a tab opened
after a break doesn't paint stale data and then swap it all out.

## Development

Requires [Bun](https://bun.sh). [prek](https://prek.j178.dev/) comes in as a dev dependency.

```bash
bun install   # also installs the git hooks
bun dev
```

| Command               | What it does                                    |
| --------------------- | ----------------------------------------------- |
| `bun run check`       | Type check                                      |
| `bun run lint`        | Lint with Biome                                 |
| `bun run lint:fix`    | Lint and apply fixes                            |
| `bun run format`      | Format TypeScript, Svelte and Markdown          |
| `bun run test`        | Run the test suite                              |
| `bun run screenshots` | Regenerate the screenshots in `docs/images/`    |
| `bun x prek run -a`   | Run every git hook against the whole repository |

The hooks format, lint, type check and test on commit, scan for secrets and typos, and require
[Conventional Commits](https://www.conventionalcommits.org/) messages.

### Stack

SvelteKit 3 (prerelease) on Bun with [remote functions](https://svelte.dev/docs/kit/remote-functions), SQLite with Drizzle ORM, Tailwind CSS and
[shadcn-svelte](https://shadcn-svelte.com/) components.

## Releases

Every merge to `main` goes through [releaser](https://github.com/orochibraru/releaser). A
`feat`, `fix`, `perf`, `refactor` or `docs` commit cuts a patch release: it updates
`CHANGELOG.md`, tags `vX.Y.Z`, creates the GitHub release with `bercail-extension-X.Y.Z.zip` attached,
and publishes the image under that tag and `latest`. Pull requests publish a `pr-<number>` image,
and their titles must be Conventional Commits because the squash merge uses them as the commit
message.
