12 Commits

Author SHA1 Message Date
arif 38a79d598d deine muuuum
CI / build (push) Successful in 9s
CI / lint (push) Successful in 10s
CI / create-and-publish-docker-image (push) Successful in 10s
2026-06-27 23:56:32 +02:00
arif 3ae05df785 dont format index.html
CI / build (push) Successful in 11s
CI / lint (push) Successful in 11s
CI / create-and-publish-docker-image (push) Successful in 11s
2026-06-27 23:23:57 +02:00
arif 57cbdca662 add tracking
CI / build (push) Successful in 9s
CI / lint (push) Failing after 10s
CI / create-and-publish-docker-image (push) Has been skipped
2026-06-27 23:22:24 +02:00
arif 1a5954ad84 update readme
CI / build (push) Successful in 9s
CI / lint (push) Successful in 10s
CI / create-and-publish-docker-image (push) Successful in 10s
2026-06-27 23:16:52 +02:00
arif c5f625f6f7 add build badge
CI / build (push) Successful in 8s
CI / lint (push) Successful in 9s
CI / create-and-publish-docker-image (push) Successful in 10s
2026-06-27 23:13:42 +02:00
arif 2a56faa1d1 fix homeassitant api endpoint
CI / build (push) Successful in 9s
CI / lint (push) Successful in 9s
CI / create-and-publish-docker-image (push) Successful in 11s
2026-06-27 23:02:39 +02:00
arif f9f1757ec0 add proper types to store
CI / build (push) Successful in 9s
CI / lint (push) Successful in 10s
CI / create-and-publish-docker-image (push) Successful in 11s
2026-06-27 22:54:31 +02:00
arif 2979302e64 remove ass
CI / build (push) Successful in 9s
CI / lint (push) Successful in 10s
CI / create-and-publish-docker-image (push) Successful in 11s
2026-06-27 22:28:21 +02:00
arif 8907602cee restart firefox after deploy
CI / build (push) Successful in 9s
CI / lint (push) Successful in 9s
CI / create-and-publish-docker-image (push) Successful in 9s
2026-06-27 22:25:42 +02:00
arif aa1a9956fe use key for ssh and not cring password
CI / build (push) Successful in 15s
CI / lint (push) Successful in 12s
CI / create-and-publish-docker-image (push) Successful in 9s
2026-06-27 21:41:13 +02:00
arif 8c317123bd try and fix ci
CI / build (push) Successful in 14s
CI / lint (push) Successful in 10s
CI / create-and-publish-docker-image (push) Failing after 35s
2026-06-27 21:30:10 +02:00
arif 3bbc18a253 fix deine mum
CI / build (push) Successful in 10s
CI / lint (push) Successful in 11s
CI / create-and-publish-docker-image (push) Failing after 41s
2026-06-27 21:22:46 +02:00
14 changed files with 66 additions and 193 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
VITE_FLATTASTIC_API_KEY="bqOh7YBZR9fChJo81bCsTinMtGpC5aok"
VITE_HA_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3N2JmOTk1ODI3MzA0ZWIzOWYwNThjMzQ4YTY3ZDJkYyIsImlhdCI6MTc1NjQ3NTM4OSwiZXhwIjoyMDcxODM1Mzg5fQ.TZZ4SUGlERuIVrhzC_wfCN-qS1wSAKNN9uMMDjkqOgA"
VITE_HA_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzYjU0MzYzZTc5ZjA0Yjk3YWQ3NjRiNTgyOWJjMjYxYiIsImlhdCI6MTc4MjU5NTkzNiwiZXhwIjoyMDk3OTU1OTM2fQ.dRYEhySSNjnOCgtr3C9ix3bIxqeH3jHnjK5vsgMaeLE"
+9 -8
View File
@@ -36,8 +36,6 @@ jobs:
VITE_HA_TOKEN: ${{ secrets.VITE_HA_TOKEN }}
- name: Build
run: bun run build
- name: Write Git-Hash into html
run: ./pipeline/create-git-hash-html.sh
- name: Create Build Artifact
uses: christopherhx/gitea-upload-artifact@v4
with:
@@ -80,18 +78,21 @@ jobs:
- name: Push Docker image
run: docker push git.rivercry.com/wg/monitor-im-flur
- name: Deploy via SSH
uses: appleboy/ssh-action@v0.1.10
uses: appleboy/ssh-action@v1.0.3
with:
host: rivercry.com
port: 20022
username: docker
password: ${{ secrets.GARRISON_DOCKER_PASSWORD }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd monitor-im-flur
export HYPRLAND_INSTANCE_SIGNATURE=$(hyprctl instances -j | jq '.[0].instance' | tr -d '"')
cd monitor-im-flur || exit 1
git clean -dfx
git reset --hard HEAD
git pull
docker-compose pull
docker-compose down
docker-compose up -d
docker compose pull
docker compose down --remove-orphans || true
docker compose up -d
pkill firefox || true
hyprctl dispatch exec 'firefox -kiosk http://localhost:9123'
+7 -132
View File
@@ -1,135 +1,10 @@
## monitor-im-flur
# monitor-im-flur
![Build Status](https://git.rivercry.com/wg/monitor-im-flur/badges/master/pipeline.svg)
![build workflow](https://git.rivercry.com/wg/monitor-im-flur/actions/workflows/ci.yml/badge.svg)
Hallway / common-area wall monitor dashboard. A single-page React + Vite app that surfaces useful household + transit + weather + fun data on a passive display.
### Key Features
* Realtime-ish autorefresh via git commit hash file (`/git-hash.html`) hot-reloads the deployed page when a new build is published.
* Dynamic theming (day / evening / night) based on current time.
* Public transport departures (KVV) for two nearby stops (IDs 7000044 & 7000045).
* Weather (current, hourly, daily min/max) via OpenMeteo.
* Flatastic chores integration (tasks + flatmates) with API key.
* Home Assistant readings (tent temperature + humidity) displayed in a faux terminal with rotating shitposts.
* 4:20 easter egg card + Amogus sprite + other playful flourishes.
* Docker / Nginx static deployment image (`git.rivercry.com/wg/monitor-im-flur:latest`).
* Strict linting + formatting (ESLint AirBnB + Prettier + Biome optional).
* Zustand + RTK (toolkit present) state management (currently Zustand in active use).
### Tech Stack
* React 19 + TypeScript + Vite
* Zustand (with devtools) for app stores (weather, kvv, flatastic, home assistant)
* Nginx (static file serving) inside minimal Docker image
* OpenMeteo, Flatastic, KVV, Home Assistant external APIs
* Git hash pipeline script to trigger client selfreloads
### Project Structure (abridged)
```
src/
api/ # External data fetchers
components/ # UI building blocks (Cards, Timetable, Weather, Terminal, etc.)
store/ # Zustand stores encapsulating fetch + state
types/ # Type definitions for external data
pipeline/
create-git-hash-html.sh # Injects current commit hash into dist
Dockerfile # Nginx static hosting
docker-compose.yml # Example runtime service definition
```
### Environment Configuration
Create a `.env` (or `.env.local`) for Vite with the following (only what you need):
```
VITE_FLATTASTIC_API_KEY=your_flatastic_api_key
# (Planned) VITE_HOME_ASSISTANT_TOKEN=your_long_lived_token # NOTE: currently hardcoded see Security section
```
Vite automatically exposes variables prefixed with `VITE_` to the client bundle. Do NOT place secrets without that prefix but remember: anything in the client bundle is public. For sensitive data consider a tiny proxy backend instead of calling APIs directly from the browser.
### Security Notice
`src/api/homeAssistant.ts` currently contains a hardcoded longlived Home Assistant token. This should be refactored before any public deployment:
1. Remove the literal token from the repository.
2. Load it via environment variable during build (`import.meta.env.VITE_HOME_ASSISTANT_TOKEN`) OR
3. Prefer a minimal server proxy so the token never ships to the browser.
### Development
Install dependencies (Bun is inferred from `bun.lock`, but npm/pnpm/yarn also work).
```
bun install
```
Run the dev server (HTTPS support possible with `vite-plugin-mkcert` if certificates are trusted):
```
bun run dev
```
Open: http://localhost:5173
### Lint & Format
```
bun run lint
```
Optionally run Biome (if desired):
```
npx biome check --apply .
```
### Build
```
bun run build
```
Outputs production bundle to `dist/`.
### Docker Image
Build locally:
```
docker build -t monitor-im-flur:local .
```
Run:
```
docker run --rm -p 9123:80 monitor-im-flur:local
```
Or use compose (uses published image):
```
docker compose up -d
```
Visit: http://localhost:9123
### Git Hash AutoReload Mechanism
`pipeline/create-git-hash-html.sh` writes the current commit (`$GITHUB_SHA`) to `dist/git-hash.html` during CI. The dashboard polls `/git-hash.html` every 10s; when the value changes it performs `window.location.reload()`. Ensure your CI runs the script after `vite build` and before creating the Docker image.
Pseudo CI step example:
```
vite build
GITHUB_SHA=$(git rev-parse HEAD) ./pipeline/create-git-hash-html.sh
docker build -t git.rivercry.com/wg/monitor-im-flur:$(git rev-parse --short HEAD) .
```
### Data Sources
* KVV Departures: Public endpoint (JSON) for stop IDs 7000044 / 7000045.
* Weather: OpenMeteo forecast API (lat 49.0094, lon 8.4044, Europe/Berlin TZ).
* Flatastic: Auth via `x-api-key` (user provided).
* Home Assistant: Longlived bearer token (refactor recommended).
### Adding a New Card
1. Create a directory under `src/components/<NewCard>/` with `NewCard.tsx` & optional `style.module.css`.
2. Wrap content with existing `Card` component (`icon` + `name` props).
3. Register inside `Dashboard.tsx` where layout lives (using `CardColumn` / `CardRow`).
### State Management Notes
All data fetching is encapsulated inside zustand store `fetch` methods invoked on an interval within the respective components. Consider centralizing polling or using React Query if complexity grows.
### Potential Improvements / TODO
* Remove hardcoded Home Assistant token.
* Error + loading states (currently optimistic, failures would be silent / console only).
* Retry & backoff strategy for network calls.
* Dark mode override / manual theme toggle.
* Accessibility pass (ARIA, focus management) current dashboard is mostly passive.
* Tests (none yet). Could add Vitest + React Testing Library.
* Switch transit API code to gracefully handle outages (KVV sometimes rate limits).
### License
Add a license file if you plan to share externally (currently unspecified).
### Support / Contact
Internal project (wg). For issues open a ticket on Gitea: https://git.rivercry.com/wg/monitor-im-flur
---
Generated README draft adjust repository paths / badge branch name if different (e.g., replace `master` with your default branch).
## Getting Started
install bun
```bash
$ bun run dev
```
+1 -1
View File
@@ -6,7 +6,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!**/dist"]
"includes": ["**", "!**/dist", "!index.html"]
},
"formatter": {
"enabled": true,
+2 -1
View File
@@ -3,7 +3,8 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>monitor im flur</title>
</head>
<body>
<div id="root"></div>
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "$GITHUB_SHA" > dist/git-hash.html
+2 -2
View File
@@ -7,7 +7,7 @@ async function fetchTentHumidity() {
return;
}
const url = `https://home.rivercry.com/api/states/sensor.third_reality_inc_3rths0224z_luftfeuchtigkeit_2`;
const url = `https://home.rivercry.com/api/states/sensor.zelt_humidity`;
const response = await fetch(url, {
method: "GET",
headers: {
@@ -26,7 +26,7 @@ async function fetchTentTemperature() {
return;
}
const url = `https://home.rivercry.com/api/states/sensor.third_reality_inc_3rths0224z_temperatur_2`;
const url = `https://home.rivercry.com/api/states/sensor.zelt_temperature`;
const response = await fetch(url, {
method: "GET",
headers: {
+1
View File
@@ -19,6 +19,7 @@ const { innerWidth: width, innerHeight: height } = window;
const getImage = (sus: Amogus) => (sus.isImposter ? imposter : amogus);
const makeInitialCrewmates = (): Amogus[] => {
console.log("innerWidth", innerWidth, "innerHeight", innerHeight);
return [
makeCrewmate(true),
makeCrewmate(false),
-22
View File
@@ -16,28 +16,6 @@ import Weather from "@/components/Weather/Weather";
import style from "./style.module.css";
export default function Dashboard() {
// '/git-hash.html' contains the current git commit hash, check it every 10 seconds and reload if it isn't the same
const [gitHash, setGitHash] = useState("");
useEffect(() => {
const interval = setInterval(async () => {
const response = await fetch("/git-hash.html");
const text = await response.text();
const newHash = text.trim();
console.log("Fetched git hash:", newHash);
if (gitHash === "") {
setGitHash(newHash);
}
if (gitHash !== "" && newHash !== gitHash) {
setGitHash(newHash);
window.location.reload();
}
}, 10000);
return () => clearInterval(interval);
}, [gitHash]);
const schemes = [style.day, style.evening, style.night];
const [schemeIndex, setSchemeIndex] = useState(0);
const scheme = schemes[schemeIndex];
+11 -6
View File
@@ -1,7 +1,11 @@
import classNames from "classnames";
import { useEffect } from "react";
import { useFlatasticStore } from "@/store/flatastic";
import type { FlatasticChore, FlatasticShoppingItem, FlatasticUser } from "@/types/flatasticChore";
import type {
FlatasticChore,
FlatasticShoppingItem,
FlatasticUser,
} from "@/types/flatasticChore";
import style from "./style.module.css";
function choreItem(chore: FlatasticChore, idToNameMap: Record<number, string>) {
@@ -86,9 +90,11 @@ export default function Flatastic() {
const shoppingList = flatasticData?.shoppingList || [];
const shoppingListRender = shoppingList.map((item) => {
return shoppingItem(item);
});
const shoppingListRender = shoppingList.map(
(item: FlatasticShoppingItem) => {
return shoppingItem(item);
},
);
const shoppingListContainer = shoppingList.length > 0 && (
<div className={style.shoppingListContainer}>
<h1>Shopping List</h1>
@@ -110,11 +116,10 @@ export default function Flatastic() {
);
}
function shoppingItem(item: FlatasticShoppingItem) {
return (
<li key={item.id} className={style.shoppingListItem}>
{item.itemName}
</li>
);
}
}
+3 -3
View File
@@ -5,11 +5,11 @@
}
.choreContainer {
flex: 7;
flex: 8;
}
.shoppingListContainer {
flex: 3;
flex: 5;
padding-left: 20px;
}
@@ -68,4 +68,4 @@
.timeLeft {
font-weight: bold;
}
}
+15 -5
View File
@@ -7,15 +7,25 @@ import type {
FlatasticUser,
} from "@/types/flatasticChore";
// biome-ignore format: deep
function parseInformationData(data): FlatasticUser[] {
interface FlatasticState {
flatasticData: {
chores: FlatasticChore[];
users: FlatasticUser[];
shoppingList: FlatasticShoppingItem[];
};
fetch: () => Promise<void>;
}
function parseInformationData(data: {
flatmates: Array<{ id: string; firstName: string }>;
}): FlatasticUser[] {
return data.flatmates.map((user: { id: string; firstName: string }) => ({
id: user.id as string,
firstName: user.firstName as string,
id: Number(user.id) as number,
firstName: user.firstName,
}));
}
const useFlatasticStore = create(
const useFlatasticStore = create<FlatasticState>()(
devtools(
(set) => ({
flatasticData: {
+7 -1
View File
@@ -2,7 +2,13 @@ import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { fetchTentHumidity, fetchTentTemperature } from "@/api/homeAssistant";
const useHomeAssistantStore = create(
interface HomeAssistantState {
tentTemperature: number;
tentHumidity: number;
fetch: () => Promise<void>;
}
const useHomeAssistantStore = create<HomeAssistantState>()(
devtools(
(set) => ({
tentTemperature: 0,
+7 -8
View File
@@ -14,15 +14,14 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@store/*": ["src/store/*"],
"@api/*": ["src/api/*"],
"@types/*": ["src/types/*"],
"@thunks/*": ["src/store/thunks/*"],
"@slices/*": ["src/store/slices/*"]
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@store/*": ["./src/store/*"],
"@api/*": ["./src/api/*"],
"@types/*": ["./src/types/*"],
"@thunks/*": ["./src/store/thunks/*"],
"@slices/*": ["./src/store/slices/*"]
},
/* Linting */