Dateien nach "/" hochladen
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -1,3 +1,123 @@
|
|||||||
# zeitkonto
|
# Zeitkonto
|
||||||
|
|
||||||
Erfassung der Arbeitszeit für die Arbeit
|
Eine kleine Web-App zur Arbeitszeiterfassung. Du trägst pro Tag Start-,
|
||||||
|
End- und Pausenzeit ein, die App berechnet daraus automatisch deine
|
||||||
|
Über- bzw. Minusstunden (dein "Zeitkonto") — pro Tag, pro Monat und als
|
||||||
|
laufenden Gesamtsaldo.
|
||||||
|
|
||||||
|
**Stack:** Node.js / Express / EJS, Daten werden in MongoDB gespeichert.
|
||||||
|
Login ist über einen einzelnen Benutzer geschützt (Zugangsdaten per
|
||||||
|
Umgebungsvariable).
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
- Tägliche Buchung von Start-, Endzeit und Pause → automatische Berechnung
|
||||||
|
der geleisteten Stunden
|
||||||
|
- Soll-Stunden pro Tag und Arbeitstage frei einstellbar (z. B. 8 h, Mo–Fr)
|
||||||
|
- Laufender Gesamtsaldo (Überstunden/Minusstunden), inkl. optionalem
|
||||||
|
Start-Guthaben für bereits bestehende Überstunden
|
||||||
|
- Monatsübersicht
|
||||||
|
- Bearbeiten und Löschen einzelner Buchungen
|
||||||
|
- Einfacher Login-Schutz (ein Benutzer, Zugangsdaten per Env-Variable)
|
||||||
|
|
||||||
|
## Lokal testen
|
||||||
|
|
||||||
|
Voraussetzung: Docker und Docker Compose.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# .env nach Bedarf anpassen (APP_PASSWORD, SESSION_SECRET ...)
|
||||||
|
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Die App ist danach unter `http://localhost:3000` erreichbar. Login mit den
|
||||||
|
Werten aus `APP_USERNAME` / `APP_PASSWORD` (Standard: `admin` / siehe
|
||||||
|
`.env.example`).
|
||||||
|
|
||||||
|
## Deployment in Coolify
|
||||||
|
|
||||||
|
Die App bringt ein fertiges `docker-compose.yml` mit (App + MongoDB inkl.
|
||||||
|
Volume für die Datenbank), das sich als ein einziges Coolify-Projekt
|
||||||
|
deployen lässt.
|
||||||
|
|
||||||
|
1. **Code in ein Git-Repository pushen** (GitHub, GitLab, Gitea, Bitbucket
|
||||||
|
— egal, Coolify unterstützt alle gängigen Anbieter, auch selbst
|
||||||
|
gehostete).
|
||||||
|
2. In Coolify: **New Resource → Docker Compose** und das Repository
|
||||||
|
auswählen bzw. die URL eingeben. Coolify erkennt die `docker-compose.yml`
|
||||||
|
automatisch (Build Pack: *Docker Compose*).
|
||||||
|
3. **Umgebungsvariablen setzen** (Tab *Environment Variables* der
|
||||||
|
Resource): mindestens
|
||||||
|
- `APP_USERNAME`
|
||||||
|
- `APP_PASSWORD`
|
||||||
|
- `SESSION_SECRET` (langer zufälliger String)
|
||||||
|
|
||||||
|
`MONGODB_URI` und `PORT` sind in der `docker-compose.yml` bereits fest
|
||||||
|
auf die interne Mongo-Verbindung gesetzt und müssen nicht verändert
|
||||||
|
werden.
|
||||||
|
4. **Domain für die App vergeben.** Dafür zwei Möglichkeiten:
|
||||||
|
- Im Coolify-UI bei der Resource den Service `app` öffnen und unter
|
||||||
|
*Domains* eine Domain (oder Coolifys generierte `sslip.io`-Domain)
|
||||||
|
eintragen, **oder**
|
||||||
|
- in der `docker-compose.yml` die Zeile `# - SERVICE_FQDN_APP_3000`
|
||||||
|
einkommentieren — Coolify generiert dann automatisch eine Domain auf
|
||||||
|
Basis deiner Wildcard-Domain.
|
||||||
|
5. **Deploy** klicken. Coolify baut das Image, startet MongoDB und die App
|
||||||
|
im selben internen Docker-Netzwerk und richtet automatisch HTTPS
|
||||||
|
(Let's Encrypt) für die vergebene Domain ein.
|
||||||
|
|
||||||
|
Die MongoDB ist dabei **nicht** öffentlich erreichbar — sie läuft nur im
|
||||||
|
internen Netzwerk und ist für die App über den Service-Namen `mongo`
|
||||||
|
ansprechbar. Die Daten liegen in einem persistenten Volume (`mongo_data`)
|
||||||
|
und bleiben bei Redeploys erhalten.
|
||||||
|
|
||||||
|
### Alternative: Coolifys eigene Mongo-Datenbank nutzen
|
||||||
|
|
||||||
|
Statt MongoDB im `docker-compose.yml` mitlaufen zu lassen, kannst du in
|
||||||
|
Coolify auch **New Resource → Database → MongoDB** als eigene, separat
|
||||||
|
verwaltete Datenbank anlegen und die App stattdessen per `Dockerfile` als
|
||||||
|
normale *Application*-Resource deployen. Trag in diesem Fall die von
|
||||||
|
Coolify angezeigte Connection-String als `MONGODB_URI` in den
|
||||||
|
Umgebungsvariablen der App ein. Das `docker-compose.yml` brauchst du dann
|
||||||
|
nicht.
|
||||||
|
|
||||||
|
## Einstellungen nach dem ersten Login
|
||||||
|
|
||||||
|
Unter **Einstellungen** legst du fest:
|
||||||
|
|
||||||
|
- Soll-Arbeitszeit pro Arbeitstag (z. B. 8 Stunden)
|
||||||
|
- An welchen Wochentagen überhaupt gearbeitet wird
|
||||||
|
- Ein optionales Start-Guthaben in Stunden, falls du schon vor der
|
||||||
|
Nutzung dieser App ein Über- oder Minusstunden-Konto hattest
|
||||||
|
|
||||||
|
Diese Werte wirken sich nur auf **neue** Buchungen aus — bereits
|
||||||
|
gespeicherte Tage behalten den Saldo, der zum Zeitpunkt ihrer Erfassung
|
||||||
|
berechnet wurde.
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
zeitkonto/
|
||||||
|
├── server.js Einstiegspunkt (Express-App)
|
||||||
|
├── src/
|
||||||
|
│ ├── db.js MongoDB-Verbindung (mit Retry)
|
||||||
|
│ ├── middleware/auth.js Login-Schutz
|
||||||
|
│ ├── models/ Mongoose-Schemas (TimeEntry, Settings)
|
||||||
|
│ ├── routes/ Login, Dashboard/Buchungen, Einstellungen
|
||||||
|
│ └── utils/time.js Zeit-/Saldoberechnungen
|
||||||
|
├── views/ EJS-Templates
|
||||||
|
├── public/css/style.css Styling
|
||||||
|
├── Dockerfile
|
||||||
|
├── docker-compose.yml App + MongoDB für Coolify/lokal
|
||||||
|
└── .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheitshinweis
|
||||||
|
|
||||||
|
Der Login schützt mit einem einzelnen Benutzernamen/Passwort-Paar aus den
|
||||||
|
Umgebungsvariablen. Das reicht für ein persönliches Tool hinter HTTPS
|
||||||
|
(Coolify richtet das automatisch ein), ersetzt aber kein vollwertiges
|
||||||
|
Mehrbenutzer-Auth-System. Setze in jedem Fall ein eigenes, starkes
|
||||||
|
`APP_PASSWORD` und einen zufälligen `SESSION_SECRET`, bevor du die App
|
||||||
|
öffentlich erreichbar machst.
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- MONGODB_URI=mongodb://mongo:27017/zeitkonto
|
||||||
|
- SESSION_SECRET=${SESSION_SECRET:-bitte-langen-zufaelligen-string-einsetzen}
|
||||||
|
- APP_USERNAME=${APP_USERNAME:-admin}
|
||||||
|
- APP_PASSWORD=${APP_PASSWORD:-bitte-aendern}
|
||||||
|
- PORT=3000
|
||||||
|
# Optional: Coolify kann hierüber automatisch eine Domain für diesen
|
||||||
|
# Service vergeben. Dazu einfach die nächste Zeile einkommentieren
|
||||||
|
# (siehe README, Abschnitt "Deployment in Coolify").
|
||||||
|
# - SERVICE_FQDN_APP_3000
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000/healthz']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "zeitkonto",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Arbeitszeiterfassung mit automatischer Überstundenberechnung",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
|
"mongoose": "^8.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const express = require('express');
|
||||||
|
const session = require('express-session');
|
||||||
|
|
||||||
|
const { connectDB } = require('./src/db');
|
||||||
|
const authRoutes = require('./src/routes/auth');
|
||||||
|
const dashboardRoutes = require('./src/routes/dashboard');
|
||||||
|
const settingsRoutes = require('./src/routes/settings');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'bitte-secret-setzen',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 Tage
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Healthcheck für Docker/Coolify
|
||||||
|
app.get('/healthz', (req, res) => res.status(200).send('ok'));
|
||||||
|
|
||||||
|
app.use(authRoutes);
|
||||||
|
app.use(dashboardRoutes);
|
||||||
|
app.use(settingsRoutes);
|
||||||
|
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', { title: 'Nicht gefunden', message: 'Seite nicht gefunden.' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Fehler',
|
||||||
|
message: 'Es ist ein unerwarteter Fehler aufgetreten.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await connectDB();
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Zeitkonto läuft auf Port ${PORT}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start().catch((err) => {
|
||||||
|
console.error('Start fehlgeschlagen:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user