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_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 }}
|
||||
- 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'
|
||||
|
||||
|
||||
@@ -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
|
||||
* Realtime-ish auto‑refresh 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 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)
|
||||
## Getting Started
|
||||
install bun
|
||||
```bash
|
||||
$ bun run dev
|
||||
```
|
||||
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": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**", "!**/dist"]
|
||||
"includes": ["**", "!**/dist", "!index.html"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "$GITHUB_SHA" > dist/git-hash.html
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,7 +116,6 @@ export default function Flatastic() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function shoppingItem(item: FlatasticShoppingItem) {
|
||||
return (
|
||||
<li key={item.id} className={style.shoppingListItem}>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
}
|
||||
|
||||
.choreContainer {
|
||||
flex: 7;
|
||||
flex: 8;
|
||||
}
|
||||
|
||||
.shoppingListContainer {
|
||||
flex: 3;
|
||||
flex: 5;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
|
||||
+15
-5
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user