Skip to main content

little sister

is watching for you.

I am little sister.

Unlike Big Brother, I’m not watching you — I’m watching for you. My eyes stay on the things you care about: disks filling up, batch jobs failing, security advisories appearing, and automated workflows turning red.

You decide where I look. I’ll keep watch and let you know when something needs your attention, so you can work peacefully — and sleep calmly.

Big Brother watches you, little sister watches out for you.


Description

little sister is a small monitoring application you can stand up in an afternoon. It runs configurable checks on background threads, aggregates their results into a single status tree, and serves that tree over a small web interface.

A check is an ordinary Python script. If you can write one, you can teach little sister to watch something — and for the usual suspects there are ready-made check types to configure instead of writing anything at all.

It is built to start small and stay useful as the situation grows. Watching a homelab needs nothing beyond a configuration directory: a few checks, a user list, and the install below. Serving as a team's dashboard inside a larger organization is the same application after some adaptation — running in the cloud behind a load balancer, taking its secrets from a secret store rather than an .env file, authenticating against something other than its own user list. Those are seams the design makes room for rather than features already shipped. What is shipped is the surface they rest on: a check type, an extra page, or a connection to the ticket system of your choice can live in a package of its own and register itself at startup.

See docs/ for the full picture — project.md (what it is), architecture.md (how the code is built), use-cases.md (the vision, in concrete scenes), implementing-checks.md (how to build a check type of your own), plugin-repository.md (how to give one a repository of its own), and testing-the-gui.md (previewing the web UI under extreme layouts and live scenarios). Design rationale is in decisions.md + adr/. The JSON API contract is docs/api/openapi.yaml (usage notes alongside). The native macOS menu-bar client lives in its own repository, little-sister-app, and consumes the JSON API.

Installation and local testing

Prerequisites

Software

Python 3.11 or later (ADR-0054). Development and the quality gate run on 3.14, so that is the best-tested interpreter; the floor is checked rather than assumed — the type check runs against it on every commit and the whole test suite runs against it on every release. No external services are required — all state is kept in memory.

Users

The allowed users are defined by the users aspect of the configuration roots: either config/users.yaml on its own or config/users/users.yaml beside optional avatar files. The first configuration root declaring either form wins whole — so a deployment that keeps its login list somewhere else gives that file a configuration root of its own and names it first in LITTLE_SISTER_CONFIG_DIR, which is the only way in (ADR-0046, ADR-0031, ADR-0045). Start with the directory form so pictures can be added without another layout change:

mkdir -p config/users/avatars
cp src/little_sister/users.example.yaml config/users/users.yaml

What you just copied — the format is one top-level key per login name:

jdoe:
  firstname: "Jane"
  lastname: "Doe"
  password: "change-me"
  admin: true
  avatar: jdoe
amorgan:
  firstname: "Alex"
  lastname: "Morgan"
  password: "change-me-too"

avatar is a bare file name inside config/users/avatars/. The extension is optional, so avatar: jdoe finds a single jdoe.svg, jdoe.png, jdoe.webp, or another supported image; avatar: jdoe.png selects that file exactly. No top-level avatar map is needed because each picture belongs to one user. A user without a usable image gets the packaged neutral avatar, and a bad reference never prevents login.

So out of the box jdoe logs in with password change-me and has admin rights, amorgan is a viewer — change both passwords before anyone else can reach the instance. (Both forms of the real user aspect are git-ignored.)

Secrets

Configuration secrets reach little-sister through the environment (ADR-0003); a git-ignored .env file in the working directory is the simple way to feed it, but any other way of setting environment variables (a launchd/systemd unit, a container orchestrator) works the same. Each piece is optional:

  • SECRET_KEY — the Flask session-signing key. Unset, a random key is generated at each start (secure by default; the cost is that logins reset on a restart). Set it to keep sessions across restarts:

    SECRET_KEY="change-me"
    
  • API tokens for the read-only JSON API (ADR-0008) — named per-client bearer tokens; clients send them as Authorization: Bearer <token> with Accept: application/json. Without them the JSON API rejects all requests:

    LITTLE_SISTER_API_TOKENS="swift-app=s3cret,satellite-eu=an0ther"
    
  • Check credentials — a check's YAML names its secrets in a secrets: block, each a secret reference (ADR-0023): env://GITHUB_TOKEN reads that environment variable, and a reference like aws-sm://team/token is resolved by a resolver the deployment registers in its own code — once at startup, never during runs. Because the reference lives in the config, one check type serves several instances with a credential each (one per team, say).

Running without a .env file

A deployment can retire the file entirely. SECRET_KEY and LITTLE_SISTER_API_TOKENS accept a value that is itself a reference — e.g. SECRET_KEY="aws-sm://team/session-key" — resolved once at startup through the same deployment-registered resolvers (which reach their store with ambient credentials, e.g. an instance role); or simply omit SECRET_KEY for the random per-start key. Check credentials move to a store via their references. What remains is the user list (config/users.yaml or config/users/users.yaml, ADR-0046) behind the built-in username/password login — replacing it (an SSO provider such as Keycloak, or password hashes in a database) is the authorization seam of the roadmap; until that lands, it is the one secret-bearing file a deployment still owns.

The configuration directory (config/)

Everything a deployment authors lives in one place — config/, whose entries are aspects: checks/, nodes.yaml, settings.yaml, csp.yaml and users.yaml or users/ (ADR-0031). LITTLE_SISTER_CONFIG_DIR selects it and is a path-list: base:sites/alpha composes a shared base with a site's own additions, and each aspect combines its layers in the way that suits it — checks union, settings merge per key, the user list is taken whole from the first root that declares it. Startup is strict about the tree and tolerant about its contents: no configuration directory, a named root that does not exist, or no checks/ anywhere and the process refuses to start, naming the path it looked at; a config/checks/ that exists but is still empty runs, warns, and shows WARN on the heartbeat.

.env stays outside it — that is secrets, not authored configuration — and so does var/, which is what the process writes rather than what you give it.

Checks

Check configs are loaded from config/checks/ in every configuration root. This repo ships templates in config/checks/examples/ — copy the ones you need up into config/checks/, or point LITTLE_SISTER_CONFIG_DIR at your own tree. A full private deployment (real per-host checks, users and secrets) lives in its own repository that consumes little-sister as a library. Writing your own check type — a new type: backed by a Python class, registered through the public CHECK_TYPES seam — is covered in docs/implementing-checks.md; giving a type its own installable repository, once it would have a second user, is docs/plugin-repository.md.

General options (config/settings.yaml)

Display and runtime options live in an optional config/settings.yaml, read once at startup and shallow-merged per key across the configuration roots, the later winning (override the whole thing with a single file via LITTLE_SISTER_CONFIG); every key has a default, so a missing file is fine (ADR-0006). A deployment carries its own copy — the file in this repo configures a local run here and doubles as the template. Three of its keys, to show the shape:

timezone: Europe/Berlin              # IANA name for displayed timestamps
time_format: "%Y-%m-%d %H:%M:%S"     # strftime for displayed timestamps
maintenance_default_expiry: 7d       # maintenance window when none is given

The shipped file documents the rest inline — the dashboard caps (reason_cap, branch_cap, maintenance_cap), whether a failing check keeps its stacktrace, the card-head placement (card_status_line: compact beside by default, or the standing second line below), and the failure-retry budget (retry_interval, retry_attempts).

Curated links (config/links.yaml)

The shared toolbox /links shows — the dashboards and tools a team reaches for while something is on fire — is an optional aspect of the same directory (ADR-0030): one flat level of groups:, each an ordered list of entries with a title, a url, an icon name and an optional one-line note. Seventeen marks are built in and need no configuration; a check: block turns on reachability probing, which runs when somebody opens the page and the answers are older than its interval:, never on a timer.

Shipping marks of your own makes this aspect a directory. The two legal forms are config/links.yaml on its own, and config/links/ holding links.yaml beside an icons/ directory. Only the second can carry files: an icons/ sitting next to a flat config/links.yaml is a directory in a configuration root, so it warns as an unrecognized aspect and is ignored (ADR-0031). Growing from one form into the other is a move, not a rename.

config/links/
├── links.yaml
└── icons/
    ├── firewall.svg              # `icon: firewall` — the file is the name
    ├── payments-on-light.svg     # a pair, named for the background it goes on
    └── payments-on-dark.svg

Content-Security-Policy sources (config/csp.yaml)

Every page carries a Content-Security-Policy-Report-Only header: a violation is logged and nothing is blocked, so it costs no page any behavior and tells you what a strict policy would have caught. The policy is assembled — the library's own defaults, plus origins a dependent package declares in code, plus this optional file (ADR-0048). Two things belong here and nowhere else: an origin no package knew about (an image host a check's Markdown embeds), and a policy keyword such as 'unsafe-inline' — a package may contribute an origin and never one of those, because one dependency able to relax the policy would make it decorative for every other. One key per directive, each a list of sources, added to the policy and layered across the configuration roots like the rest:

img-src:
  - https://assets.example.org
connect-src:
  - https://tickets.example.org

/system prints the effective policy with the contributor of every source, so what this file did is visible from the running instance. The shipped copy is an example with every entry commented out.

Installation

From the package index

For running little sister. It needs Python 3.11 or later (ADR-0054) — on anything older the resolver declines to install, which is the intent rather than a packaging mistake.

pip install little-sister            # or: uv pip install little-sister

The package is the application, not a configuration. It starts from a configuration directory and refuses to start without one; the smallest that works is a user list and a checks/ directory, which may be empty:

mkdir -p config/checks config/users
cat > config/users/users.yaml <<'YAML'
admin:
  firstname: "Admin"
  lastname: "User"
  password: "change-me"
  admin: true
YAML

# A SINGLE worker: the engine and the status tree live in-process.
gunicorn --workers 1 --threads 8 --bind 0.0.0.0:8000 little_sister.app:app

One worker is not a suggestion — the engine and the status tree live in the process (ADR-0001), so a second worker is a second, disagreeing tree. The packaged users.example.yaml documents every field of the user list, avatars included.

An empty checks/ is allowed and reports itself — nothing is being watched is a state little sister shows you, not a reason to refuse to start. Set SECRET_KEY before anyone relies on staying logged in: without one a random key is made per start, so sessions end with the process. Runtime state (the log, the maintenance file) is written to var/ beside the configuration.

On your browser, navigate to http://localhost:8000.

From a checkout

For developing little sister itself, or running a branch. The project is managed with uv (it reads uv.lock).

uv sync                              # create .venv and install from the lockfile
uv run gunicorn --workers 1 --threads 8 --bind 0.0.0.0:8000 little_sister.app:app

First-time setup: on a fresh checkout, ./setup.sh creates config/users/users.yaml, its empty avatars/ directory, and .env (a default admin login and a random session key), prompting before it overwrites anything. An existing flat config/users.yaml is kept and the script prints the manual move needed before adding avatars. Then the repo's helper scripts (they need lsof and curl) manage the run: ./start.sh runs gunicorn in the background with safe port handling — it restarts its own instance and refuses to touch a foreign process on the port — logging to var/. Its preflight accepts both users-aspect forms (and follows the configured root path-list) before it starts. ./stop.sh stops it; ./test_api.sh smoke-tests the running JSON API (token from LITTLE_SISTER_API_TOKENS). Override the bind with LITTLE_SISTER_HOST / LITTLE_SISTER_PORT.

To query the read-only JSON API, send a bearer token (from LITTLE_SISTER_API_TOKENS) with Accept: application/json:

curl -H "Accept: application/json" -H "Authorization: Bearer s3cret" \
     http://localhost:8000/status/system/db

Testing scripts locally

ssh your-host bash -xs < src/little_sister/scripts/host-metrics-linux.sh

Quality gate

ruff + mypy + pytest. Run them with uv:

uv run ruff check      # lint
uv run mypy            # type-check (strict, on src)
uv run pytest          # tests

Git hooks

A pre-commit hook that runs those three lives in hooks/ (version-controlled, unlike .git/hooks/). Enable it once per clone:

git config core.hooksPath hooks

After that, commits are blocked if lint or tests fail.

Contributing

Development happens on a private working branch; main carries releases only — one squashed commit per version. Bug reports and feature requests go through GitHub issues. Pull requests are welcome too: an accepted PR is absorbed into the working branch and lands in the next release, credited with Co-authored-by.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

little_sister-0.3.11.tar.gz (324.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

little_sister-0.3.11-py3-none-any.whl (397.4 kB view details)

Uploaded Python 3

File details

Details for the file little_sister-0.3.11.tar.gz.

File metadata

  • Download URL: little_sister-0.3.11.tar.gz
  • Upload date:
  • Size: 324.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for little_sister-0.3.11.tar.gz
Algorithm Hash digest
SHA256 ece62270868df47e56ea2867c365771f75f61843bfce92c41161234366e81603
MD5 373b44477ec432ac115e046edb9ecc72
BLAKE2b-256 d3f69212c668cf1fe46f55ce97d67471102c7917169eb5d53552887b3eecb5bc

See more details on using hashes here.

File details

Details for the file little_sister-0.3.11-py3-none-any.whl.

File metadata

  • Download URL: little_sister-0.3.11-py3-none-any.whl
  • Upload date:
  • Size: 397.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for little_sister-0.3.11-py3-none-any.whl
Algorithm Hash digest
SHA256 b076eac1ed6d92e0e72f1c9fbefbd26a7621354f43da1077a70fb452c742cba7
MD5 e11b9557eb403b5750581398ca19cb4c
BLAKE2b-256 0a996b2772c1ad61626508b769557093427522ef4620cf8081006ed33b616d2f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page