Compare commits
12 Commits
0fd2d05902
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 38a79d598d | |||
| 3ae05df785 | |||
| 57cbdca662 | |||
| 1a5954ad84 | |||
| c5f625f6f7 | |||
| 2a56faa1d1 | |||
| f9f1757ec0 | |||
| 2979302e64 | |||
| 8907602cee | |||
| aa1a9956fe | |||
| 8c317123bd | |||
| 3bbc18a253 |
@@ -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"
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -1,135 +1,10 @@
|
|||||||
## monitor-im-flur
|
# monitor-im-flur
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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 auto‑refresh 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 Open‑Meteo.
|
|
||||||
* 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
|
|
||||||
* Open‑Meteo, Flatastic, KVV, Home Assistant external APIs
|
|
||||||
* Git hash pipeline script to trigger client self‑reloads
|
|
||||||
|
|
||||||
### 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 hard‑coded long‑lived 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 Auto‑Reload 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: Open‑Meteo forecast API (lat 49.0094, lon 8.4044, Europe/Berlin TZ).
|
|
||||||
* Flatastic: Auth via `x-api-key` (user provided).
|
|
||||||
* Home Assistant: Long‑lived 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 hard‑coded 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
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": false,
|
||||||
"includes": ["**", "!**/dist"]
|
"includes": ["**", "!**/dist", "!index.html"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
+2
-1
@@ -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>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "$GITHUB_SHA" > dist/git-hash.html
|
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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(
|
||||||
|
(item: FlatasticShoppingItem) => {
|
||||||
return shoppingItem(item);
|
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}>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.choreContainer {
|
.choreContainer {
|
||||||
flex: 7;
|
flex: 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoppingListContainer {
|
.shoppingListContainer {
|
||||||
flex: 3;
|
flex: 5;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-5
@@ -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: {
|
||||||
|
|||||||
@@ -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
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user