inital commit

This commit is contained in:
wolf-demon 2026-10-02 16:22:16 +01:00
commit aed6d1a64c
14 changed files with 365 additions and 0 deletions

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
# Copy this file to .env for local use. Do not put passwords in this file.
# The directory must contain the three secret files documented in README.md.
SECRETS_DIR=./.local/secrets
# Sandbox-only loopback port for direct Grafana access.
GRAFANA_PORT=3000

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# Local environment and generated configuration
.env
.env.*
!.env.example
.local/
# Secrets and authentication material
secrets/
*.htpasswd
*.password
*.token
*.key
*.pem
*.p12
*.pfx
*.crt
*.cer
# Runtime and database state
data/
postgres-data/
grafana-data/
loki-data/
*.dump
*.backup
*.sql.gz
*.tar.gz
# Logs and unreviewed application samples
*.log
testing/logs/incoming/**
!testing/logs/incoming/.gitkeep
# Tooling and operating-system files
.DS_Store
Thumbs.db
.idea/
.vscode/
__pycache__/
.pytest_cache/

98
README.md Normal file
View File

@ -0,0 +1,98 @@
# Kiosk observability proof of concept
This repository contains the first local milestone for the kiosk reporting
platform: Grafana OSS backed by a dedicated PostgreSQL metadata database. Loki,
reporting aggregation, Nginx/TLS, tenant bootstrap, and production deployment
will be added in later milestones.
Grafana is available only through a loopback sandbox port. PostgreSQL has no
published host port. Nginx will replace the direct Grafana port when HTTPS is
introduced.
## Prerequisites
- Docker Engine with the Docker Compose plugin
- PowerShell 7 or Windows PowerShell 5.1 for the local secret helper
The pinned images are `grafana/grafana:13.2.2` and
`postgres:18.6-alpine`. Do not replace them with `latest`.
## Prepare local secrets
Copy the non-secret environment template and create three random secret files:
```powershell
Copy-Item .env.example .env
./scripts/initialize-local-secrets.ps1
```
The helper creates these ignored files without printing their values:
```text
.local/secrets/grafana_admin_password
.local/secrets/grafana_database_password
.local/secrets/postgres_admin_password
```
It does not overwrite an existing secret. For a deployed Linux environment,
create equivalent restricted files below `/etc/kiosk-observability/secrets` and
set `SECRETS_DIR` in the host's untracked `.env` file to that directory.
## Start and verify
Start the local stack:
```powershell
docker compose -f compose.yaml -f compose.sandbox.yaml up -d
docker compose -f compose.yaml -f compose.sandbox.yaml ps
```
Open <http://127.0.0.1:3000> and sign in as `admin` with the value stored in
`.local/secrets/grafana_admin_password`. The health endpoint is
<http://127.0.0.1:3000/api/health>.
Confirm that PostgreSQL is not published to the host:
```powershell
docker compose -f compose.yaml -f compose.sandbox.yaml port postgres 5432
```
The command should report that no public port exists. To confirm Grafana is
using PostgreSQL, inspect the health response and container logs; the health
response should report `"database": "ok"`.
## Stop or reset
Stop containers while preserving their named volumes:
```powershell
docker compose -f compose.yaml -f compose.sandbox.yaml down
```
Starting the stack again should retain Grafana metadata. Removing volumes
permanently deletes the local databases and must only be done when an intentional
clean reset is required:
```powershell
docker compose -f compose.yaml -f compose.sandbox.yaml down --volumes
```
## Supply test logs
Put logs awaiting review in `testing/logs/incoming/`. The directory is present
in Git, but its contents are ignored. Read `testing/logs/README.md` before adding
files. Only fully sanitized, explicitly approved samples may later be placed in
`tests/fixtures/`.
## Validate configuration
After preparing `.env` and the local secrets, render the merged configuration:
```powershell
docker compose -f compose.yaml -f compose.sandbox.yaml config
```
The output must show secret file paths only. It must not contain the contents of
any password file. Do not commit `.env`, `.local/`, raw logs, certificates,
private keys, database dumps, or runtime data.

5
compose.sandbox.yaml Normal file
View File

@ -0,0 +1,5 @@
services:
grafana:
ports:
- "127.0.0.1:${GRAFANA_PORT:-3000}:3000"

70
compose.yaml Normal file
View File

@ -0,0 +1,70 @@
name: kiosk-observability
services:
postgres:
image: postgres:18.6-alpine
restart: unless-stopped
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres_admin
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_admin_password
secrets:
- postgres_admin_password
- grafana_database_password
volumes:
- postgres_data:/var/lib/postgresql
- ./postgres/init/10-create-grafana-database.sh:/docker-entrypoint-initdb.d/10-create-grafana-database.sh:ro
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres_admin -d postgres"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
grafana:
image: grafana/grafana:13.2.2
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
GF_DATABASE_TYPE: postgres
GF_DATABASE_HOST: postgres:5432
GF_DATABASE_NAME: grafana
GF_DATABASE_USER: grafana
GF_DATABASE_PASSWORD__FILE: /run/secrets/grafana_database_password
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password
secrets:
- grafana_admin_password
- grafana_database_password
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro
- ./grafana/provisioning:/etc/grafana/provisioning:ro
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s
networks:
backend:
internal: true
volumes:
grafana_data:
postgres_data:
secrets:
grafana_admin_password:
file: ${SECRETS_DIR:-./.local/secrets}/grafana_admin_password
grafana_database_password:
file: ${SECRETS_DIR:-./.local/secrets}/grafana_database_password
postgres_admin_password:
file: ${SECRETS_DIR:-./.local/secrets}/postgres_admin_password

38
grafana/grafana.ini Normal file
View File

@ -0,0 +1,38 @@
app_mode = production
[server]
protocol = http
http_addr = 0.0.0.0
http_port = 3000
domain = localhost
root_url = http://localhost:3000/
enforce_domain = false
[database]
ssl_mode = disable
[analytics]
reporting_enabled = false
check_for_updates = false
check_for_plugin_updates = false
[security]
disable_gravatar = true
cookie_secure = false
cookie_samesite = strict
strict_transport_security = false
[users]
allow_sign_up = false
allow_org_create = false
auto_assign_org = false
[auth.anonymous]
enabled = false
[log]
mode = console
level = info
[paths]
provisioning = /etc/grafana/provisioning

View File

@ -0,0 +1,6 @@
# Grafana provisioning
Version-controlled data-source and dashboard provisioning will be added here as
the Loki and reporting services are implemented. The empty subdirectories are
intentional and keep the eventual layout stable.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,36 @@
#!/bin/sh
set -eu
password_file=/run/secrets/grafana_database_password
if [ ! -r "$password_file" ]; then
echo "Grafana database password file is missing or unreadable" >&2
exit 1
fi
grafana_password=$(cat "$password_file")
if [ -z "$grafana_password" ]; then
echo "Grafana database password must not be empty" >&2
exit 1
fi
psql \
--set=ON_ERROR_STOP=1 \
--set=grafana_password="$grafana_password" \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" <<-'EOSQL'
SELECT format('CREATE ROLE grafana LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT PASSWORD %L', :'grafana_password')
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'grafana') \gexec
ALTER ROLE grafana PASSWORD :'grafana_password';
SELECT 'CREATE DATABASE grafana OWNER grafana'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_database WHERE datname = 'grafana') \gexec
REVOKE ALL ON DATABASE grafana FROM PUBLIC;
GRANT CONNECT, TEMPORARY ON DATABASE grafana TO grafana;
EOSQL
unset grafana_password

View File

@ -0,0 +1,39 @@
[CmdletBinding()]
param(
[string]$Destination = (Join-Path $PSScriptRoot "..\.local\secrets")
)
$ErrorActionPreference = "Stop"
$secretNames = @(
"grafana_admin_password",
"grafana_database_password",
"postgres_admin_password"
)
$resolvedDestination = [System.IO.Path]::GetFullPath($Destination)
[System.IO.Directory]::CreateDirectory($resolvedDestination) | Out-Null
foreach ($secretName in $secretNames) {
$secretPath = Join-Path $resolvedDestination $secretName
if (Test-Path -LiteralPath $secretPath) {
Write-Host "Keeping existing secret: $secretName"
continue
}
$bytes = New-Object byte[] 32
$generator = [System.Security.Cryptography.RandomNumberGenerator]::Create()
try {
$generator.GetBytes($bytes)
}
finally {
$generator.Dispose()
}
$value = [Convert]::ToBase64String($bytes)
[System.IO.File]::WriteAllText($secretPath, $value, [System.Text.UTF8Encoding]::new($false))
Write-Host "Created secret: $secretName"
}
Write-Host "Local secrets are ready in $resolvedDestination"

16
testing/logs/README.md Normal file
View File

@ -0,0 +1,16 @@
# Kiosk log intake
Place log files for initial review in `incoming/`. Everything below that
directory is ignored by Git so an unreviewed file cannot be committed by
accident.
Logs may still contain personal or payment-related information. Before a sample
is approved as a version-controlled test fixture, replace guest names, booking
references, room numbers, session and correlation identifiers, transaction
identifiers, payment details, free-text notes, and any other identifying values.
Keep the original line structure, delimiters, timestamps, event names, and field
layout so that parser development remains representative.
Reviewed and fully sanitized samples can later be copied deliberately into
`tests/fixtures/`. There is no automatic promotion from this inbox.

View File

@ -0,0 +1 @@

6
tests/fixtures/README.md vendored Normal file
View File

@ -0,0 +1,6 @@
# Approved log fixtures
Only reviewed, synthetic or fully sanitized log samples belong here. Raw files
from `testing/logs/incoming/` must never be copied here without an explicit
privacy review.