diff --git a/.gitignore b/.gitignore index 8904f6e..7b5fe5d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ .venv users/ backup/ +Archiv/ +Docker/ +docker-work/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/Zeiterfassung.iml b/.idea/Zeiterfassung.iml deleted file mode 100644 index 68b5ff6..0000000 --- a/.idea/Zeiterfassung.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 32d08e5..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 7c204cb..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f97154d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM debian:latest +RUN apt update && apt upgrade -y +RUN apt install python3 python3-pip python3.11-venv locales -y +RUN mkdir /app +RUN mkdir /.venv +RUN mkdir /backup +RUN mkdir /settings +RUN python3 -m venv /.venv +RUN /.venv/bin/pip install nicegui +RUN /.venv/bin/pip install segno +RUN /.venv/bin/pip install python-dateutil + +RUN sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen && \ + locale-gen +ENV LANG de_DE.UTF-8 +ENV LANGUAGE de_DE:de +ENV LC_ALL de_DE.UTF-8 + +COPY main.py /app/main.py +COPY favicon.svg /app/favicon.svg +COPY lib /app/lib/ +EXPOSE 8090 +ENTRYPOINT ["/.venv/bin/python", "/app/main.py"] +#ENTRYPOINT exec /app/.venv/bin/python /app/main.py --docker \ No newline at end of file diff --git a/__pycache__/zeiterfassung.cpython-38.pyc b/__pycache__/zeiterfassung.cpython-38.pyc deleted file mode 100644 index 1dcae1d..0000000 Binary files a/__pycache__/zeiterfassung.cpython-38.pyc and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9695725 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + test: + image: test:0 + restart: always + ports: + - 8090:8090 + volumes: + #- /home/alexander/Dokumente/Python/Zeiterfassung:/app + - /home/alexander/Dokumente/Python/Zeiterfassung/docker-work/users:/users + - /home/alexander/Dokumente/Python/Zeiterfassung/docker-work/backup:/backup + - /home/alexander/Dokumente/Python/Zeiterfassung/docker-work/settings:/settings \ No newline at end of file diff --git a/lib/admin.py b/lib/admin.py index 3396e10..7b1ed32 100644 --- a/lib/admin.py +++ b/lib/admin.py @@ -1,7 +1,6 @@ from datetime import datetime import dateutil.easter -from PIL.SpiderImagePlugin import isInt from dateutil.easter import * from nicegui import ui, app, events @@ -45,6 +44,14 @@ def page_admin(): updates_available = ValueBinder() updates_available.value = False + enabled_because_not_docker = ValueBinder + if is_docker(): + enabled_because_not_docker.value = False + scriptpath = "/app" + backupfolder = "/backup" + else: + enabled_because_not_docker.value = True + with ui.tabs() as tabs: time_overview = ui.tab('Zeitdaten') @@ -739,30 +746,6 @@ Dies kann nicht rückgängig gemacht werden!''') with ui.card(): ui.markdown("**Administrationsbenutzer:**") with ui.grid(columns=2): - def save_admin_settings(): - write_adminsetting("admin_user", admin_user.value) - if admin_password.value != "": - write_adminsetting("admin_password", hash_password(admin_password.value)) - else: - write_adminsetting("admin_password", data["admin_password"]) - write_adminsetting("port", port.value) - write_adminsetting("secret", secret) - write_adminsetting("touchscreen", touchscreen_switch.value) - write_adminsetting("times_on_touchscreen", timestamp_switch.value) - write_adminsetting("photos_on_touchscreen", photo_switch.value) - write_adminsetting("picture_height", picture_height_input.value) - write_adminsetting("button_height", button_height_input.value) - write_adminsetting("user_notes", notes_switch.value) - write_adminsetting("holidays", data["holidays"]) - write_adminsetting("vacation_application", va_switch.value) - - if int(old_port) != int(port.value): - with ui.dialog() as dialog, ui.card(): - ui.markdown("Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.") - ui.button("OK", on_click=lambda: dialog.close()) - dialog.open() - ui.notify("Einstellungen gespeichert") - timetable.refresh() ui.markdown("Benutzername des Adminstrators") admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein") @@ -784,7 +767,9 @@ Dies kann nicht rückgängig gemacht werden!''') ui.markdown("Port:") port = ui.input(validation={"Nur ganzzahlige Portnummern erlaubt": lambda value: check_is_number(value), - "Portnummer zu klein": lambda value: len(value)>=2}).tooltip("Geben Sie hier die Portnummer ein, unter der die Zeiterfassung erreichbar ist.").props('size=5') + "Portnummer zu klein": lambda value: len(value)>=2}).tooltip("Geben Sie hier die Portnummer ein, unter der die Zeiterfassung erreichbar ist.").props('size=5').bind_enabled_from(enabled_because_not_docker, 'value') + if is_docker(): + port.tooltip("Diese Einstellung ist beim Einsatz von Docker deaktiviert.") old_port = data["port"] port.value = old_port @@ -826,12 +811,12 @@ Dies kann nicht rückgängig gemacht werden!''') ui.markdown("**Einstellungen für Benutzerfrontend**") notes_switch = ui.switch("Notizfunktion aktiviert", value=data["user_notes"]) va_switch = ui.switch("Urlaubsanträge", value=data["vacation_application"]) - + reset_visibility = ValueBinder() def holiday_section(): with ui.card(): ui.markdown('**Feiertage:**') - reset_visibility = ValueBinder() + reset_visibility.value = False def new_holiday_entry(): @@ -1031,6 +1016,33 @@ Dies kann nicht rückgängig gemacht werden!''') holiday_section() + def save_admin_settings(): + write_adminsetting("admin_user", admin_user.value) + if admin_password.value != "": + write_adminsetting("admin_password", hash_password(admin_password.value)) + else: + write_adminsetting("admin_password", data["admin_password"]) + write_adminsetting("port", port.value) + write_adminsetting("secret", secret) + write_adminsetting("touchscreen", touchscreen_switch.value) + write_adminsetting("times_on_touchscreen", timestamp_switch.value) + write_adminsetting("photos_on_touchscreen", photo_switch.value) + write_adminsetting("picture_height", picture_height_input.value) + write_adminsetting("button_height", button_height_input.value) + write_adminsetting("user_notes", notes_switch.value) + write_adminsetting("holidays", data["holidays"]) + write_adminsetting("vacation_application", va_switch.value) + + if int(old_port) != int(port.value): + with ui.dialog() as dialog, ui.card(): + ui.markdown( + "Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.") + ui.button("OK", on_click=lambda: dialog.close()) + dialog.open() + ui.notify("Einstellungen gespeichert") + reset_visibility.value = False + timetable.refresh() + ui.button("Speichern", on_click=save_admin_settings).tooltip("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.") with ui.tab_panel(users): @@ -1393,7 +1405,7 @@ Dies kann nicht rückgängig gemacht werden!''') ui.label("Backupeinstellungen").classes('font-bold') with ui.grid(columns='auto auto auto'): ui.markdown("Backupordner:") - backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}") + backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}").bind_enabled_from(enabled_because_not_docker, 'value') def save_new_folder_name(): if os.path.exists(backupfolder_input.value): write_adminsetting("backup_folder", backupfolder_input.value) @@ -1405,7 +1417,9 @@ Dies kann nicht rückgängig gemacht werden!''') ui.label("exisitiert nicht und kann daher nicht verwendet werden.") ui.button("OK", on_click=dialog.close) dialog.open() - ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern") + save_backup_folder_button = ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern").bind_enabled_from(enabled_because_not_docker, 'value') + if is_docker(): + save_backup_folder_button.tooltip("Diese Einstellung ist beim Einsatz von Docker deaktiviert.") ui.markdown("API-Schlüssel:") backup_api_key_input = ui.input(value=api_key).tooltip("Hier den API-Schlüssel eintragen, der für Backuperzeugung mittels API-Aufruf verwendet werden soll.") @@ -1465,7 +1479,10 @@ Dies kann nicht rückgängig gemacht werden!''') except ValueError: button_string = date_string ui.markdown(button_string) - ui.markdown(f'{round(size/1_000_000,2)} MB') + if size > 1_000_000: + ui.markdown(f'{round(size/1_000_000,2)} MB') + else: + ui.markdown(f'{round(size / 1_000, 2)} kB') ui.markdown(version) ui.button(icon='download', on_click=lambda file=date_string: ui.download.file( os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip( @@ -1526,7 +1543,7 @@ Dies kann nicht rückgängig gemacht werden!''') for file in files: add = os.path.join(root, file) target.write(add) - target.write(usersettingsfilename) + target.write(os.path.join(scriptpath, usersettingsfilename), arcname=usersettingsfilename) target.writestr("app_version.txt", data=app_version) backup_list.refresh() n.dismiss() diff --git a/lib/definitions.py b/lib/definitions.py index 0196977..130b709 100644 --- a/lib/definitions.py +++ b/lib/definitions.py @@ -6,12 +6,21 @@ from pathlib import Path import hashlib app_title = "Zeiterfassung" -app_version = ("0.0.0") +app_version = "0.0.0" # Standardpfade -scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute()) + +def is_docker(): + cgroup = Path('/proc/self/cgroup') + return Path('/.dockerenv').is_file() or (cgroup.is_file() and 'docker' in cgroup.read_text()) + +if is_docker(): + scriptpath = "/settings" + backupfolder = "/backup" +else: + scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute()) + backupfolder = str(os.path.join(scriptpath, "backup")) userfolder = "users" -backupfolder = str(os.path.join(scriptpath, "backup")) # Dateinamen @@ -37,7 +46,7 @@ standard_adminsettings = { "admin_user": "admin", "button_height": 300, "user_notes": True, "vacation_application": True, - "backupfolder": backupfolder, + "backup_folder": backupfolder, "backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20), "holidays": { } } diff --git a/lib/homepage.py b/lib/homepage.py index 092bad3..71234bc 100644 --- a/lib/homepage.py +++ b/lib/homepage.py @@ -199,6 +199,7 @@ def homepage(): overviews = ui.tab('Übersichten') absence = ui.tab('Urlaubsantrag') absence.set_visibility(load_adminsettings()["vacation_application"]) + pw_change = ui.tab("Passwort") with ui.grid(columns='1fr auto 1fr').classes('w-full items-center'): ui.space() @@ -281,6 +282,31 @@ def homepage(): ui.button("Zurückziehen", on_click=retract_va).tooltip("Hiermit wird der oben gewählte Urlaubsantrag zurückgezogen.").classes('w-full') open_vacation_applications() + with ui.tab_panel(pw_change): + ui.label("Passwort ändern").classes('font-bold') + with ui.grid(columns='auto auto').classes('items-end'): + ui.label("Altes Passwort:") + old_pw_input = ui.input(password=True) + ui.label("Neues Passwort:") + new_pw_input = ui.input(password=True) + ui.label("Neues Passwort bestätigen:") + new_pw_confirm_input = ui.input(password=True) + def revert_pw_inputs(): + old_pw_input.value = "" + new_pw_input.value = "" + new_pw_confirm_input.value = "" + def save_new_password(): + if hash_password(old_pw_input.value) == current_user.password: + if new_pw_input.value == new_pw_confirm_input.value: + current_user.password = hash_password(new_pw_input.value) + current_user.write_settings() + ui.notify("Neues Passwort gespeichert") + else: + ui.notify("Passwortbestätigung stimmt nicht überein") + else: + ui.notify("Altes Passwort nicht korrekt") + ui.button("Speichern", on_click=save_new_password) + ui.button("Zurücksetzen", on_click=revert_pw_inputs) ui.space() else: diff --git a/lib/settings.json b/lib/settings.json deleted file mode 100644 index fe1226a..0000000 --- a/lib/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "admin_user": "admin", - "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", - "port": "8090", - "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", - "times_on_touchscreen": true, - "photos_on_touchscreen": true, - "touchscreen": true, - "picure_height": 200, - "button_height": 300, - "user_notes": true, - "holidays": {} -} \ No newline at end of file diff --git a/lib/users.py b/lib/users.py index 3d37674..16381cf 100644 --- a/lib/users.py +++ b/lib/users.py @@ -14,25 +14,26 @@ import shutil import re from lib.definitions import userfolder, scriptpath, usersettingsfilename, photofilename, status_in, status_out, \ - standard_adminsettings, standard_usersettings, va_file - + standard_adminsettings, standard_usersettings, va_file, is_docker # Benutzerklasse class user: def __init__(self, name): - self.userfolder = os.path.join(scriptpath, userfolder, name) + if not is_docker(): + self.userfolder = os.path.join(scriptpath, userfolder, name) + else: + self.userfolder = os.path.join("/users", name) self.settingsfile = os.path.join(self.userfolder, usersettingsfilename) self.photofile = os.path.join(self.userfolder, photofilename) # Stammdaten einlesen - try: - with open(self.settingsfile) as json_file: + #try: + with open(self.settingsfile) as json_file: data = json.load(json_file) - - except: - print("Fehler beim Erstellen des Datenarrays.") - #Hier muss noch Fehlerbehandlungcode hin + #except: + # print("Fehler beim Erstellen des Datenarrays.") + # #TODO Hier muss noch Fehlerbehandlungcode hin self.password = data["password"] self.workhours = data["workhours"] @@ -301,10 +302,8 @@ class user: return { } def write_notes(self, year, month, day, note_dict): - print(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json")) with open(os.path.join(self.userfolder, f"{int(year)}-{int(month)}.json"), "r") as json_file: json_data = json.load(json_file) - print(json_data) if len(note_dict) == 1: user_info = list(note_dict)[0] json_data["notes"][str(day)] = { } diff --git a/zeiterfassung.py b/main.py similarity index 94% rename from zeiterfassung.py rename to main.py index f6ebcb3..89c263a 100644 --- a/zeiterfassung.py +++ b/main.py @@ -15,8 +15,7 @@ import argparse from lib.web_ui import hash_password - -class Commandline_Header: +def commandline_header(): message_string = f"{app_title} {app_version}" underline = "" for i in range(len(message_string)): @@ -37,8 +36,7 @@ def main(): def startup_message(): - Commandline_Header() - + commandline_header() url_string = "" for i in list(app.urls): url_string += f"{i}, " @@ -60,8 +58,13 @@ if __name__ in ("__main__", "__mp_main__"): parser = argparse.ArgumentParser(description=f'{app_title} {app_version}') parser.add_argument('--admin-access', help='Zugangsdaten für Administrator einstellen', action="store_true") args = parser.parse_args() + + if is_docker(): + scriptpath = "/app" + backupfolder = "/backup" + if args.admin_access: - Commandline_Header() + commandline_header() print("Lade Administrationseinstellungen") admin_settings = load_adminsettings() print("Geben Sie den neuen Benutzernamen für den Administrationsbenutzer an:") diff --git a/playgound.py b/playgound.py deleted file mode 100644 index 8d8184f..0000000 --- a/playgound.py +++ /dev/null @@ -1,23 +0,0 @@ -import json -import urllib.request - -from nicegui import ui, app - - -import segno - -@app.get("/data") -async def deliver_data(): - with open("settings.json") as json_file: - data = json.load(json_file) - return data - -string = "" -for i in range(1000): - string += str(i) - -qr_code = segno.make_qr(string).svg_data_uri() -#qr_code.save("qr_code.png", scale=5, border=0) -ui.image(qr_code) - -ui.run(language="de-DE", port=9000) \ No newline at end of file diff --git a/qr_scanner.py b/qr_scanner.py deleted file mode 100644 index 3f4a21e..0000000 --- a/qr_scanner.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -import base64 -import signal -import time -import argparse -import requests - -import cv2 -import numpy as np -from fastapi import Response -from playsound3 import playsound -from definitions import app_title, app_version - -from nicegui import Client, app, core, run, ui - -class Commandline_Header: - message_string = f"{app_title} {app_version}" - underline = "" - for i in range(len(message_string)): - underline += "-" - print(message_string) - print(underline) - -def visual_interface(port=9000): - # In case you don't have a webcam, this will provide a black placeholder image. - black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII=' - placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png') - - global convert - def convert(frame: np.ndarray) -> bytes: - """Converts a frame from OpenCV to a JPEG image. - - This is a free function (not in a class or inner-function), - to allow run.cpu_bound to pickle it and send it to a separate process. - """ - _, imencode_image = cv2.imencode('.jpg', frame) - return imencode_image.tobytes() - - global setup - def setup() -> None: - - url_string = "" - for i in list(app.urls): - url_string += f"{i}, " - url_string = url_string[0:-2] - print("Weboberfläche erreichbar unter: " + url_string) - - # OpenCV is used to access the webcam. - video_capture = cv2.VideoCapture(0) - detector = cv2.QRCodeDetector() - - blocker = False - blockset = 0 - - - @app.get('/video/frame') - # Thanks to FastAPI's `app.get` it is easy to create a web route which always provides the latest image from OpenCV. - async def grab_video_frame() -> Response: - nonlocal blocker - if time.time() - blockset > 5: - blocker = False - - if not video_capture.isOpened(): - return placeholder - # The `video_capture.read` call is a blocking function. - # So we run it in a separate thread (default executor) to avoid blocking the event loop. - _, frame = await run.io_bound(video_capture.read) - if frame is None: - return placeholder - # `convert` is a CPU-intensive function, so we run it in a separate process to avoid blocking the event loop and GIL. - jpeg = await run.cpu_bound(convert, frame) - - # QR-Handling - - def function_call(): - r = requests.get(str(a)) - print(r.content()) - print("Inside Function_call") - #b = webbrowser.open(str(a)) - if r.status_code == 200: - print('Erkannt') - if r.json()["stampstatus"]: - playsound('ui-on.mp3') - elif not r.json()["stampstatus"]: - playsound('ui-off.mp3') - else: - playsound('ui-sound.mp3') - nonlocal blocker - nonlocal blockset - blocker = True - blockset = time.time() - - if not blocker: - _, img = video_capture.read() - # detect and decode - data, bbox, _ = detector.detectAndDecode(img) - # check if there is a QRCode in the image - if data: - a = data - function_call() - # cv2.imshow("QRCODEscanner", img) - if cv2.waitKey(1) == ord("q"): - function_call() - - return Response(content=jpeg, media_type='image/jpeg') - - # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`. - video_image = ui.interactive_image().classes('w-full h-full') - # A timer constantly updates the source of the image. - # Because data from same paths is cached by the browser, - # we must force an update by adding the current timestamp to the source. - - ui.timer(interval=0.1, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}')) - - async def disconnect() -> None: - """Disconnect all clients from current running server.""" - for client_id in Client.instances: - await core.sio.disconnect(client_id) - - def handle_sigint(signum, frame) -> None: - # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so. - ui.timer(0.1, disconnect, once=True) - # Delay the default handler to allow the disconnect to complete. - ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True) - - async def cleanup() -> None: - # This prevents ugly stack traces when auto-reloading on code change, - # because otherwise disconnected clients try to reconnect to the newly started server. - await disconnect() - # Release the webcam hardware so it can be used by other applications again. - video_capture.release() - - app.on_shutdown(cleanup) - # We also need to disconnect clients when the app is stopped with Ctrl+C, - # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown. - signal.signal(signal.SIGINT, handle_sigint) - - - # All the setup is only done when the server starts. This avoids the webcam being accessed - # by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321). - app.on_startup(setup) - ui.run(favicon="favicon.svg", port=port, language='de-DE', show_welcome_message=False) - -if __name__ in ("__main__", "__mp_main__"): - parser = argparse.ArgumentParser(description=f'{app_title}-QR-Scanner {app_version}') - parser.add_argument('--webgui', help='Web-GUI starten', action="store_true") - parser.add_argument('-p', help="Port, über den die Weboberfläche erreichbar ist") - args = parser.parse_args() - - Commandline_Header() - print("QR-Scanner") - - if args.webgui: - try: - port = int(args.p) - except: - port = False - if not port == False: - visual_interface(port) - else: - print("Ungültiger Port") - print("Beende") - quit() diff --git a/qr_scanner_example.py b/qr_scanner_example.py deleted file mode 100644 index a641a08..0000000 --- a/qr_scanner_example.py +++ /dev/null @@ -1,22 +0,0 @@ -import cv2 -import webbrowser - -cap = cv2.VideoCapture(0) -# initialize the cv2 QRCode detector -detector = cv2.QRCodeDetector() - -while True: - _, img = cap.read() - # detect and decode - data, bbox, _ = detector.detectAndDecode(img) - # check if there is a QRCode in the image - if data: - a = data - break - cv2.imshow("QRCODEscanner", img) - if cv2.waitKey(1) == ord("q"): - break - -b = webbrowser.open(str(a)) -cap.release() -cv2.destroyAllWindows() \ No newline at end of file diff --git a/settings.json b/settings.json index 3a7dc0b..72a00b8 100644 --- a/settings.json +++ b/settings.json @@ -73,6 +73,7 @@ "2030-10-30": "Reformationstag", "2030-12-25": "1. Weihnachtsfeiertag", "2030-12-26": "2. Weihnachtsfeiertag", - "2025-06-11": "Testeintrag" + "2025-06-11": "Testeintrag", + "2025-05-31": "Testeintrag" } } \ No newline at end of file diff --git a/settings.json_bak b/settings.json_bak deleted file mode 100644 index b1a23bc..0000000 --- a/settings.json_bak +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin_user": "admin", - "admin_password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", - "port": "8090", - "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", - "holidays": { - "2024-05-01": "Tag der Arbeit", - "2024-12-25": "1. Weihnachtsfeiertag", - "2025-01-01": "Neujahr", - "2025-05-01": "Tag der Arbeit" - } -} \ No newline at end of file diff --git a/sounds/3beeps.mp3 b/sounds/3beeps.mp3 deleted file mode 100644 index c14243e..0000000 Binary files a/sounds/3beeps.mp3 and /dev/null differ diff --git a/sounds/beep.mp3 b/sounds/beep.mp3 deleted file mode 100644 index 0d1f255..0000000 Binary files a/sounds/beep.mp3 and /dev/null differ diff --git a/sounds/power-on.mp3 b/sounds/power-on.mp3 deleted file mode 100644 index c659616..0000000 Binary files a/sounds/power-on.mp3 and /dev/null differ diff --git a/sounds/store_beep.mp3 b/sounds/store_beep.mp3 deleted file mode 100644 index ac50b9c..0000000 Binary files a/sounds/store_beep.mp3 and /dev/null differ diff --git a/sounds/success.mp3 b/sounds/success.mp3 deleted file mode 100644 index c86f6c6..0000000 Binary files a/sounds/success.mp3 and /dev/null differ diff --git a/sounds/ui-off.mp3 b/sounds/ui-off.mp3 deleted file mode 100644 index 1b8ccb7..0000000 Binary files a/sounds/ui-off.mp3 and /dev/null differ diff --git a/sounds/ui-on.mp3 b/sounds/ui-on.mp3 deleted file mode 100644 index 834f291..0000000 Binary files a/sounds/ui-on.mp3 and /dev/null differ diff --git a/sounds/ui-sound.mp3 b/sounds/ui-sound.mp3 deleted file mode 100644 index 1da7ff3..0000000 Binary files a/sounds/ui-sound.mp3 and /dev/null differ diff --git a/test.json b/test.json deleted file mode 100644 index f304e61..0000000 --- a/test.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "2024-04-01": { - "0": "0", - "1": "8", - "2": "8", - "3": "8", - "4": "8", - "5": "8", - "6": "0", - "vacation": "30" - }, - "2024-04-07": { - "0": "0", - "1": "6", - "2": "6", - "3": "6", - "4": "8", - "5": "6", - "6": "0", - "vacation": "28" - } -} \ No newline at end of file diff --git a/users/testuser10/2025-5.json b/users/testuser10/2025-5.json index ece2b10..942539d 100644 --- a/users/testuser10/2025-5.json +++ b/users/testuser10/2025-5.json @@ -1,5 +1,6 @@ { - "archived": 0, + "archived": 1, "total_hours": 0, - "absence": {} + "absence": {}, + "overtime": -406441 } \ No newline at end of file diff --git a/webcam_example.py b/webcam_example.py deleted file mode 100644 index b4fbfc4..0000000 --- a/webcam_example.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -import base64 -import signal -import time -import webbrowser - -import cv2 -import numpy as np -from fastapi import Response - -from nicegui import Client, app, core, run, ui - -# In case you don't have a webcam, this will provide a black placeholder image. -black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII=' -placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png') - - -def convert(frame: np.ndarray) -> bytes: - """Converts a frame from OpenCV to a JPEG image. - - This is a free function (not in a class or inner-function), - to allow run.cpu_bound to pickle it and send it to a separate process. - """ - _, imencode_image = cv2.imencode('.jpg', frame) - return imencode_image.tobytes() - - -def setup() -> None: - # OpenCV is used to access the webcam. - video_capture = cv2.VideoCapture(0) - detector = cv2.QRCodeDetector() - - blocker = False - blockset = 0 - - @app.get('/video/frame') - # Thanks to FastAPI's `app.get` it is easy to create a web route which always provides the latest image from OpenCV. - async def grab_video_frame() -> Response: - nonlocal blocker - if time.time() - blockset > 5: - blocker = False - - if not video_capture.isOpened(): - return placeholder - # The `video_capture.read` call is a blocking function. - # So we run it in a separate thread (default executor) to avoid blocking the event loop. - _, frame = await run.io_bound(video_capture.read) - if frame is None: - return placeholder - # `convert` is a CPU-intensive function, so we run it in a separate process to avoid blocking the event loop and GIL. - jpeg = await run.cpu_bound(convert, frame) - - # QR-Handling - - def function_call(): - b = webbrowser.open(str(a)) - print('\a') - nonlocal blocker - nonlocal blockset - blocker = True - blockset = time.time() - - if not blocker: - _, img = video_capture.read() - # detect and decode - data, bbox, _ = detector.detectAndDecode(img) - # check if there is a QRCode in the image - if data: - a = data - function_call() - # cv2.imshow("QRCODEscanner", img) - if cv2.waitKey(1) == ord("q"): - function_call() - - return Response(content=jpeg, media_type='image/jpeg') - - # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`. - video_image = ui.interactive_image().classes('w-full h-full') - # A timer constantly updates the source of the image. - # Because data from same paths is cached by the browser, - # we must force an update by adding the current timestamp to the source. - - ui.timer(interval=0.1, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}')) - - async def disconnect() -> None: - """Disconnect all clients from current running server.""" - for client_id in Client.instances: - await core.sio.disconnect(client_id) - - def handle_sigint(signum, frame) -> None: - # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so. - ui.timer(0.1, disconnect, once=True) - # Delay the default handler to allow the disconnect to complete. - ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True) - - async def cleanup() -> None: - # This prevents ugly stack traces when auto-reloading on code change, - # because otherwise disconnected clients try to reconnect to the newly started server. - await disconnect() - # Release the webcam hardware so it can be used by other applications again. - video_capture.release() - - app.on_shutdown(cleanup) - # We also need to disconnect clients when the app is stopped with Ctrl+C, - # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown. - signal.signal(signal.SIGINT, handle_sigint) - - -# All the setup is only done when the server starts. This avoids the webcam being accessed -# by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321). -app.on_startup(setup) - -ui.run(port=9005) \ No newline at end of file