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_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 }} VITE_HA_TOKEN: ${{ secrets.VITE_HA_TOKEN }}
- name: Build - name: Build
run: bun run build run: bun run build
- name: Write Git-Hash into html
run: ./pipeline/create-git-hash-html.sh
- name: Create Build Artifact - name: Create Build Artifact
uses: christopherhx/gitea-upload-artifact@v4 uses: christopherhx/gitea-upload-artifact@v4
with: with:
@@ -80,18 +78,21 @@ jobs:
- name: Push Docker image - name: Push Docker image
run: docker push git.rivercry.com/wg/monitor-im-flur run: docker push git.rivercry.com/wg/monitor-im-flur
- name: Deploy via SSH - name: Deploy via SSH
uses: appleboy/ssh-action@v0.1.10 uses: appleboy/ssh-action@v1.0.3
with: with:
host: rivercry.com host: rivercry.com
port: 20022 port: 20022
username: docker username: docker
password: ${{ secrets.GARRISON_DOCKER_PASSWORD }} key: ${{ secrets.SSH_PRIVATE_KEY }}
script: | 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 clean -dfx
git reset --hard HEAD git reset --hard HEAD
git pull git pull
docker-compose pull docker compose pull
docker-compose down docker compose down --remove-orphans || true
docker-compose up -d docker compose up -d
pkill firefox || true
hyprctl dispatch exec 'firefox -kiosk http://localhost:9123'
+6 -131
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 ## Getting Started
* Realtime-ish autorefresh via git commit hash file (`/git-hash.html`) hot-reloads the deployed page when a new build is published. install bun
* Dynamic theming (day / evening / night) based on current time. ```bash
* Public transport departures (KVV) for two nearby stops (IDs 7000044 & 7000045). $ bun run dev
* 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).
+1 -1
View File
@@ -6,7 +6,7 @@
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"includes": ["**", "!**/dist"] "includes": ["**", "!**/dist", "!index.html"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
+2 -1
View File
@@ -3,7 +3,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>monitor im flur</title>
</head> </head>
<body> <body>
<div id="root"></div> <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; 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, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
@@ -26,7 +26,7 @@ async function fetchTentTemperature() {
return; 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, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
+1
View File
@@ -19,6 +19,7 @@ const { innerWidth: width, innerHeight: height } = window;
const getImage = (sus: Amogus) => (sus.isImposter ? imposter : amogus); const getImage = (sus: Amogus) => (sus.isImposter ? imposter : amogus);
const makeInitialCrewmates = (): Amogus[] => { const makeInitialCrewmates = (): Amogus[] => {
console.log("innerWidth", innerWidth, "innerHeight", innerHeight);
return [ return [
makeCrewmate(true), makeCrewmate(true),
makeCrewmate(false), makeCrewmate(false),
-22
View File
@@ -16,28 +16,6 @@ import Weather from "@/components/Weather/Weather";
import style from "./style.module.css"; import style from "./style.module.css";
export default function Dashboard() { 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 schemes = [style.day, style.evening, style.night];
const [schemeIndex, setSchemeIndex] = useState(0); const [schemeIndex, setSchemeIndex] = useState(0);
const scheme = schemes[schemeIndex]; const scheme = schemes[schemeIndex];
+10 -5
View File
@@ -1,7 +1,11 @@
import classNames from "classnames"; import classNames from "classnames";
import { useEffect } from "react"; import { useEffect } from "react";
import { useFlatasticStore } from "@/store/flatastic"; 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"; import style from "./style.module.css";
function choreItem(chore: FlatasticChore, idToNameMap: Record<number, string>) { function choreItem(chore: FlatasticChore, idToNameMap: Record<number, string>) {
@@ -86,9 +90,11 @@ export default function Flatastic() {
const shoppingList = flatasticData?.shoppingList || []; const shoppingList = flatasticData?.shoppingList || [];
const shoppingListRender = shoppingList.map((item) => { const shoppingListRender = shoppingList.map(
return shoppingItem(item); (item: FlatasticShoppingItem) => {
}); return shoppingItem(item);
},
);
const shoppingListContainer = shoppingList.length > 0 && ( const shoppingListContainer = shoppingList.length > 0 && (
<div className={style.shoppingListContainer}> <div className={style.shoppingListContainer}>
<h1>Shopping List</h1> <h1>Shopping List</h1>
@@ -110,7 +116,6 @@ export default function Flatastic() {
); );
} }
function shoppingItem(item: FlatasticShoppingItem) { function shoppingItem(item: FlatasticShoppingItem) {
return ( return (
<li key={item.id} className={style.shoppingListItem}> <li key={item.id} className={style.shoppingListItem}>
+2 -2
View File
@@ -5,11 +5,11 @@
} }
.choreContainer { .choreContainer {
flex: 7; flex: 8;
} }
.shoppingListContainer { .shoppingListContainer {
flex: 3; flex: 5;
padding-left: 20px; padding-left: 20px;
} }
+15 -5
View File
@@ -7,15 +7,25 @@ import type {
FlatasticUser, FlatasticUser,
} from "@/types/flatasticChore"; } from "@/types/flatasticChore";
// biome-ignore format: deep interface FlatasticState {
function parseInformationData(data): FlatasticUser[] { 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 }) => ({ return data.flatmates.map((user: { id: string; firstName: string }) => ({
id: user.id as string, id: Number(user.id) as number,
firstName: user.firstName as string, firstName: user.firstName,
})); }));
} }
const useFlatasticStore = create( const useFlatasticStore = create<FlatasticState>()(
devtools( devtools(
(set) => ({ (set) => ({
flatasticData: { flatasticData: {
+7 -1
View File
@@ -2,7 +2,13 @@ import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
import { fetchTentHumidity, fetchTentTemperature } from "@/api/homeAssistant"; import { fetchTentHumidity, fetchTentTemperature } from "@/api/homeAssistant";
const useHomeAssistantStore = create( interface HomeAssistantState {
tentTemperature: number;
tentHumidity: number;
fetch: () => Promise<void>;
}
const useHomeAssistantStore = create<HomeAssistantState>()(
devtools( devtools(
(set) => ({ (set) => ({
tentTemperature: 0, tentTemperature: 0,
+7 -8
View File
@@ -14,15 +14,14 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["./src/*"],
"@components/*": ["src/components/*"], "@components/*": ["./src/components/*"],
"@store/*": ["src/store/*"], "@store/*": ["./src/store/*"],
"@api/*": ["src/api/*"], "@api/*": ["./src/api/*"],
"@types/*": ["src/types/*"], "@types/*": ["./src/types/*"],
"@thunks/*": ["src/store/thunks/*"], "@thunks/*": ["./src/store/thunks/*"],
"@slices/*": ["src/store/slices/*"] "@slices/*": ["./src/store/slices/*"]
}, },
/* Linting */ /* Linting */