Compare commits
No commits in common. "3287df35c5ccaa72f793f309f2133a6774b5ec08" and "d81c3af3b327b37faacc5d434cadc17f46ebd829" have entirely different histories.
3287df35c5
...
d81c3af3b3
12 changed files with 0 additions and 494 deletions
|
|
@ -1,46 +0,0 @@
|
|||
name: Build docs site
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
container:
|
||||
image: python:3.13-bookworm
|
||||
volumes:
|
||||
- /srv/docs-makerfloss/html:/output
|
||||
steps:
|
||||
- name: Install git for actions/checkout
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends git rsync
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install --quiet -r requirements.txt
|
||||
|
||||
- name: Regenerate hardware index
|
||||
run: python3 scripts/gen_overview.py --category hardware
|
||||
|
||||
- name: Fail on drift in docs/hardware/index.md
|
||||
run: |
|
||||
if ! git diff --exit-code docs/hardware/index.md; then
|
||||
echo
|
||||
echo "::error::docs/hardware/index.md is stale."
|
||||
echo "Regenerate locally via 'make docs-index' and commit the result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build site (strict)
|
||||
run: mkdocs build --strict
|
||||
|
||||
- name: Publish to /output (main only)
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
run: rsync -a --delete site/ /output/
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -5,15 +5,6 @@
|
|||
slides/*.html
|
||||
slides/*.pdf
|
||||
|
||||
# ---> MkDocs build output
|
||||
site/
|
||||
.cache/
|
||||
|
||||
# ---> Python venvs / caches
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# ---> Local secrets (Forgejo API token, etc.)
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
25
Makefile
25
Makefile
|
|
@ -1,25 +0,0 @@
|
|||
.PHONY: help docs-index docs-build docs-serve docs-check slides
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " docs-index Regenerate docs/hardware/index.md from per-host frontmatter"
|
||||
@echo " docs-build Build the static MkDocs site into ./site (strict)"
|
||||
@echo " docs-serve Run a live-reload local preview server"
|
||||
@echo " docs-check Drift-check: regenerate index, fail if it differs from the committed copy"
|
||||
@echo " slides Run build-slides.sh (Marp slides)"
|
||||
|
||||
docs-index:
|
||||
python3 scripts/gen_overview.py --category hardware
|
||||
|
||||
docs-build:
|
||||
mkdocs build --strict
|
||||
|
||||
docs-serve:
|
||||
mkdocs serve
|
||||
|
||||
docs-check:
|
||||
python3 scripts/gen_overview.py --category hardware
|
||||
git diff --exit-code docs/hardware/index.md
|
||||
|
||||
slides:
|
||||
./build-slides.sh
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
hostname: fisi
|
||||
kind: server
|
||||
status: in-use
|
||||
model: HP MicroServer Gen10 Plus
|
||||
location: home rack
|
||||
cpu: Xeon E-2226G
|
||||
cpu_cores: 6
|
||||
cpu_threads: 12
|
||||
ram_gb: 64
|
||||
storage_gb: 8000
|
||||
storage_type: hdd
|
||||
storage_notes: ZFS mirror 2×8 TB HDD + 1 TB NVMe cache
|
||||
nic_gbps: 1
|
||||
---
|
||||
|
||||
# fisi
|
||||
|
||||
Primary home server in the baobab.band homelab. Hosts the bulk of
|
||||
self-hosted services: Nextcloud, Jellyfin + *arr stack, Technitium DNS,
|
||||
PhotoPrism, Matrix (conduwuit + Element), Forgejo (internal), Vaultwarden,
|
||||
and more.
|
||||
|
||||
Not part of the MakerFLOSS infrastructure proper, listed here for
|
||||
Proxmox-style placement planning when we eventually share workloads.
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
# Hardware Overview
|
||||
|
||||
_Auto-generated from `docs/hardware/*.md` — do not edit by hand. Run `make docs-index` after changing a file._
|
||||
|
||||
## Laptops
|
||||
|
||||
| Hostname | Model | Location | CPU | RAM | Storage | NIC | Status |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| [tembo](tembo.md) | ThinkPad T480 | Orange Makerspace (kiosk) | Intel Core i5-8350U · 4c/8t | 16 GB | 512 GB NVME | 1 GbE | in-use |
|
||||
|
||||
## Servers
|
||||
|
||||
| Hostname | Model | Location | CPU | RAM | Storage | NIC | Status |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| [fisi](fisi.md) | HP MicroServer Gen10 Plus | home rack | Xeon E-2226G · 6c/12t | 64 GB | 8 TB HDD | 1 GbE | in-use |
|
||||
| [makerfloss](makerfloss.md) | Hetzner CX22 | Hetzner HEL1 (cloud) | AMD EPYC (shared vCPU) · 2c | 4 GB | 40 GB NVME | 1 GbE | in-use |
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
hostname: makerfloss
|
||||
kind: server
|
||||
status: in-use
|
||||
model: Hetzner CX22
|
||||
location: Hetzner HEL1 (cloud)
|
||||
cpu: AMD EPYC (shared vCPU)
|
||||
cpu_cores: 2
|
||||
cpu_threads: 2
|
||||
ram_gb: 4
|
||||
storage_gb: 40
|
||||
storage_type: nvme
|
||||
nic_gbps: 1
|
||||
---
|
||||
|
||||
# makerfloss
|
||||
|
||||
Hetzner Cloud VPS running the public-facing MakerFLOSS stack: Forgejo
|
||||
(self-hosted git forge), Traefik with Let's Encrypt, poste.io mail
|
||||
server, a Forgejo Actions runner, and the nginx services that serve
|
||||
`slides.makerfloss.eu` and `docs.makerfloss.eu`.
|
||||
|
||||
Managed via the [`AnsibleBaobabV4`](https://forgejo.nyumbani.baobab.band/sjat/AnsibleBaobabV4)
|
||||
Ansible project. SSH on port 7576.
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
hostname: tembo
|
||||
kind: laptop
|
||||
status: in-use
|
||||
model: ThinkPad T480
|
||||
location: Orange Makerspace (kiosk)
|
||||
cpu: Intel Core i5-8350U
|
||||
cpu_cores: 4
|
||||
cpu_threads: 8
|
||||
ram_gb: 16
|
||||
storage_gb: 512
|
||||
storage_type: nvme
|
||||
nic_gbps: 1
|
||||
---
|
||||
|
||||
# tembo
|
||||
|
||||
XFCE-based touchscreen kiosk laptop at Orange Makerspace. Runs a
|
||||
rotation of dashboards (Grafana, the Marp slides at
|
||||
`slides.makerfloss.eu`, the radio page) and serves as the local
|
||||
display for the room. Also runs a Grafana/Loki/Prometheus stack.
|
||||
|
||||
Acts as an example of a re-purposed laptop being treated as a fixed
|
||||
piece of infrastructure rather than a personal device.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# MakerFLOSS Docs
|
||||
|
||||
Documentation for the [MakerFLOSS](https://forgejo.makerfloss.eu/sjat/MakerFLOSS)
|
||||
initiative at [Orange Makerspace](https://orangemakers.dk) — a bi-weekly FLOSS
|
||||
jam-session community focused on self-hosted, open-source infrastructure.
|
||||
|
||||
## Sections
|
||||
|
||||
- [Hardware](hardware/index.md) — every machine in the lab, auto-indexed from per-host
|
||||
frontmatter blocks. Use this when planning where to deploy a new service.
|
||||
- [House rules](makerFLOSS_house_rules.md) — working norms, governance, and
|
||||
what we do (and don't) do.
|
||||
|
||||
## Working norms (summary)
|
||||
|
||||
- **Language:** English for code and docs. Danish allowed in meeting notes and
|
||||
community communications.
|
||||
- **Environments:** containerised and reproducible.
|
||||
- **Hardware:** all setups documented (this site) and physically labelled.
|
||||
- **Decisions:** lightweight markdown decision logs under
|
||||
[`docs/superpowers/`](https://forgejo.makerfloss.eu/sjat/MakerFLOSS/src/branch/main/docs/superpowers).
|
||||
- **License:** FLOSS by default; MIT for what we build.
|
||||
55
mkdocs.yml
55
mkdocs.yml
|
|
@ -1,55 +0,0 @@
|
|||
site_name: MakerFLOSS Docs
|
||||
site_url: https://docs.makerfloss.eu/
|
||||
site_description: Documentation for the MakerFLOSS initiative at Orange Makerspace.
|
||||
|
||||
repo_url: https://forgejo.makerfloss.eu/sjat/MakerFLOSS
|
||||
repo_name: sjat/MakerFLOSS
|
||||
edit_uri: _edit/main/
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.indexes
|
||||
- navigation.sections
|
||||
- navigation.top
|
||||
- content.code.copy
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: deep orange
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: deep orange
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- toc:
|
||||
permalink: true
|
||||
- tables
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- pymdownx.superfences
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
|
||||
plugins:
|
||||
- search
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Hardware:
|
||||
- hardware/index.md
|
||||
- House rules: makerFLOSS_house_rules.md
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
mkdocs==1.6.*
|
||||
mkdocs-material==9.5.*
|
||||
pyyaml==6.*
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate a category overview Markdown file from per-item YAML frontmatter.
|
||||
|
||||
Reads `scripts/overview_config.yml`, picks the block named by `--category`,
|
||||
walks `source_dir/*.md` (excluding `output_file`), validates each file's
|
||||
frontmatter, and writes a grouped+sorted table to `output_file`.
|
||||
|
||||
Exits non-zero on any schema violation. Deterministic, offline, stdlib + PyYAML.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG_PATH = REPO_ROOT / "scripts" / "overview_config.yml"
|
||||
|
||||
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
||||
|
||||
|
||||
class SchemaError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def parse_frontmatter(path: Path) -> dict | None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
m = FRONTMATTER_RE.match(text)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
data = yaml.safe_load(m.group(1))
|
||||
except yaml.YAMLError as e:
|
||||
raise SchemaError(f"{path}: invalid YAML frontmatter: {e}") from e
|
||||
if not isinstance(data, dict):
|
||||
raise SchemaError(f"{path}: frontmatter is not a mapping")
|
||||
return data
|
||||
|
||||
|
||||
def validate(path: Path, fm: dict, cfg: dict) -> None:
|
||||
for field in cfg["required_fields"]:
|
||||
if field not in fm:
|
||||
raise SchemaError(f"{path}: missing required field '{field}'")
|
||||
for field, allowed in cfg.get("enums", {}).items():
|
||||
if field in fm and fm[field] not in allowed:
|
||||
raise SchemaError(
|
||||
f"{path}: {field}={fm[field]!r} not in {allowed}"
|
||||
)
|
||||
stem = path.stem
|
||||
hostname = fm["hostname"]
|
||||
if stem != hostname:
|
||||
raise SchemaError(
|
||||
f"{path}: filename stem {stem!r} != hostname {hostname!r}"
|
||||
)
|
||||
|
||||
|
||||
def fmt_cpu(fm: dict) -> str:
|
||||
model = fm.get("cpu", "")
|
||||
cores = fm.get("cpu_cores")
|
||||
threads = fm.get("cpu_threads")
|
||||
suffix = ""
|
||||
if cores and threads and threads != cores:
|
||||
suffix = f" · {cores}c/{threads}t"
|
||||
elif cores:
|
||||
suffix = f" · {cores}c"
|
||||
return (model + suffix).strip()
|
||||
|
||||
|
||||
def fmt_ram(fm: dict) -> str:
|
||||
n = fm.get("ram_gb")
|
||||
return f"{n} GB" if isinstance(n, int) else ""
|
||||
|
||||
|
||||
def fmt_storage(fm: dict) -> str:
|
||||
n = fm.get("storage_gb")
|
||||
t = fm.get("storage_type", "").upper() if fm.get("storage_type") else ""
|
||||
if not isinstance(n, int):
|
||||
return t # type alone if no capacity
|
||||
if n >= 1000 and n % 1000 == 0:
|
||||
size = f"{n // 1000} TB"
|
||||
elif n >= 1000:
|
||||
size = f"{n / 1000:.1f} TB"
|
||||
else:
|
||||
size = f"{n} GB"
|
||||
return f"{size} {t}".strip()
|
||||
|
||||
|
||||
def fmt_nic(fm: dict) -> str:
|
||||
g = fm.get("nic_gbps")
|
||||
if g is None:
|
||||
return ""
|
||||
if isinstance(g, float) and not g.is_integer():
|
||||
return f"{g} GbE"
|
||||
return f"{int(g)} GbE"
|
||||
|
||||
|
||||
def cell(fm: dict, col: dict) -> str:
|
||||
kind = col.get("kind")
|
||||
if kind == "hostname-link":
|
||||
h = fm["hostname"]
|
||||
return f"[{h}]({h}.md)"
|
||||
if kind == "cpu":
|
||||
return fmt_cpu(fm)
|
||||
if kind == "ram":
|
||||
return fmt_ram(fm)
|
||||
if kind == "storage":
|
||||
return fmt_storage(fm)
|
||||
if kind == "nic":
|
||||
return fmt_nic(fm)
|
||||
value = fm.get(col["field"], "")
|
||||
return "" if value is None else str(value)
|
||||
|
||||
|
||||
def render(cfg: dict, items: list[dict]) -> str:
|
||||
columns = cfg["columns"]
|
||||
group_by = cfg.get("group_by")
|
||||
sort_by = cfg.get("sort_by", "hostname")
|
||||
group_titles = cfg.get("group_titles", {})
|
||||
|
||||
if group_by:
|
||||
groups: dict[str, list[dict]] = {}
|
||||
for fm in items:
|
||||
groups.setdefault(fm.get(group_by, ""), []).append(fm)
|
||||
ordered = sorted(groups.items())
|
||||
else:
|
||||
ordered = [("", items)]
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append(f"# {cfg['title']}")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"_Auto-generated from `{cfg['source_dir']}/*.md` — do not edit by hand. "
|
||||
f"Run `make docs-index` after changing a file._"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
for group_key, rows in ordered:
|
||||
rows.sort(key=lambda r: r.get(sort_by, ""))
|
||||
if group_by:
|
||||
title = group_titles.get(group_key, group_key.title() + "s")
|
||||
lines.append(f"## {title}")
|
||||
lines.append("")
|
||||
header = "| " + " | ".join(c["header"] for c in columns) + " |"
|
||||
sep = "|" + "|".join("---" for _ in columns) + "|"
|
||||
lines.append(header)
|
||||
lines.append(sep)
|
||||
for fm in rows:
|
||||
lines.append("| " + " | ".join(cell(fm, c) for c in columns) + " |")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--category", required=True, help="Category key from overview_config.yml")
|
||||
args = parser.parse_args()
|
||||
|
||||
config_all = yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||
if args.category not in config_all:
|
||||
print(f"ERROR: category {args.category!r} not in {CONFIG_PATH}", file=sys.stderr)
|
||||
return 2
|
||||
cfg = config_all[args.category]
|
||||
|
||||
source_dir = REPO_ROOT / cfg["source_dir"]
|
||||
output_file = REPO_ROOT / cfg["output_file"]
|
||||
output_abs = output_file.resolve()
|
||||
|
||||
items: list[dict] = []
|
||||
errors: list[str] = []
|
||||
for path in sorted(source_dir.glob("*.md")):
|
||||
if path.resolve() == output_abs:
|
||||
continue
|
||||
try:
|
||||
fm = parse_frontmatter(path)
|
||||
except SchemaError as e:
|
||||
errors.append(str(e))
|
||||
continue
|
||||
if fm is None:
|
||||
print(f"WARNING: {path}: no YAML frontmatter, skipping", file=sys.stderr)
|
||||
continue
|
||||
try:
|
||||
validate(path, fm, cfg)
|
||||
except SchemaError as e:
|
||||
errors.append(str(e))
|
||||
continue
|
||||
items.append(fm)
|
||||
|
||||
if errors:
|
||||
for err in errors:
|
||||
print(f"ERROR: {err}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = output_file.with_suffix(output_file.suffix + ".tmp")
|
||||
tmp.write_text(render(cfg, items), encoding="utf-8")
|
||||
tmp.replace(output_file)
|
||||
print(f"Wrote {output_file.relative_to(REPO_ROOT)} ({len(items)} item(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Configuration for scripts/gen_overview.py
|
||||
#
|
||||
# Each top-level key is a category. The generator is invoked with
|
||||
# --category <name> and looks up its block here. To add a new category
|
||||
# (services, vms, ...) later, copy a block and adjust the fields.
|
||||
|
||||
hardware:
|
||||
title: "Hardware Overview"
|
||||
source_dir: docs/hardware
|
||||
output_file: docs/hardware/index.md
|
||||
required_fields:
|
||||
- hostname
|
||||
- kind
|
||||
- status
|
||||
enums:
|
||||
kind: [server, laptop, sbc, switch, ap, desktop]
|
||||
status: [in-use, spare, broken, donated]
|
||||
storage_type: [nvme, ssd, hdd, mixed]
|
||||
group_by: kind
|
||||
# Human-friendly H2 names per group_by value. Anything missing falls back
|
||||
# to the raw value title-cased + "s".
|
||||
group_titles:
|
||||
server: Servers
|
||||
laptop: Laptops
|
||||
sbc: Single-board computers
|
||||
switch: Switches
|
||||
ap: Access points
|
||||
desktop: Desktops
|
||||
sort_by: hostname
|
||||
columns:
|
||||
- { header: Hostname, kind: hostname-link }
|
||||
- { header: Model, field: model }
|
||||
- { header: Location, field: location }
|
||||
- { header: CPU, kind: cpu }
|
||||
- { header: RAM, kind: ram }
|
||||
- { header: Storage, kind: storage }
|
||||
- { header: NIC, kind: nic }
|
||||
- { header: Status, field: status }
|
||||
Loading…
Add table
Reference in a new issue