Inventar und Handbücher Dinge digital ordnen und einfach wiederfinden Dinge digital ordnen und einfach wiederfinden ... das klingt nach einer schweren Aufgabe - muss es aber nicht sein! Teedy (vormals "Sismics Docs") ist ein Open Source Enterprise Content Management-System (ECM) bzw. Dokumenten Management System (DMS) mit vielen Funktionen und einer modernen Benutzeroberfläche. Als Begrifflichkeit klingt das natürlich ziemlich langweilig. Allerdings ist ein solches System umseitig einsetzbar. Teedy ist ein auf Java basierter Webdienst, mit dem sich verschiedene Dateien hochladen und klassifizieren lassen. Durch geschicktes Tagging (Verstichworten) und die Wahl der Titel, sowie eine Volltextsuche mit OCR-Funktion erlauben ein sehr flinkes und einfaches Filtern. Wir nutzen so Teedy nicht nur für unseren ganzen Vereinspapierkram, sondern auch zum Verwalten unseres kompletten physischen Inventars. Die Digitialisierung unserer Werkstatt ist sehr hilfreich. Wir haben reichlich Werkzeuge und Maschinen in den einzelnen Bereichen und es ist manchmal einfach unübersichtlich - insbesondere für noch nicht so alteingesessene Stammmitglieder. Damit wir den Überblick nicht verlieren, arbeiten wir an einer dem Inventar gewidmeten Teedy-Instanz, mit der wir unsere Werkzeuge und Maschinen durchsuchen können. Ziel soll es unter anderem sein, dass hierbei nach Bereichen, Tätigkeiten, Herstellern und mehr gefiltert werden kann. Bei guter Pflege des Systems dauert es so nur wenige Sekunden, um sämtliche Maschinen zu finden, mit denen man beispielsweise etwas sägen oder bohren kann. Besonders toll ist, dass sich hierbei auch zugehörige Fotos, Handbücher, Protokolle und andere Dokumente eingefügt werden können. Ziel ist es zudem, dass jeder wichtige Gegenstand im FabLab seinen festen Platz hat, welcher von allen Mitgliedern stets nachvollziehbar ist. Da jedes Objekt im FabLab seine eigene eindeutige Artikelnummer und zugewiesene Eigenschaften bekommen soll, ist dann auch jedes Objekt nachvollziehbar in seiner kompletten Historie und Beschaffenheit. Als physischer Gegenpart zum Portal steht deshalb als Aufgabe das Labeln der Objekte mit Inventaraufklebern. Auf diesen Aufklebern sollen sich Angaben befinden, die das Objekt identifizieren. Die Aufkleber sind dann über eine Barcode Scanner-App einlesbar und öffnen direkt die spezifische Objekt-URL der Inventarplattplattform. Ein paar Screenshots unserer Instanz Dokumentation Wir nutzen Teedy nicht nur selbst, sondern haben auch jede Menge Dokumentation zur Server-Installation, Konfiguration und Nutzung dazu geschrieben. Siehe Teedy (DMS) - How To's. Die ausführliche Dokumentation zum Inventar-System (Konzept) findet sich unter Werkstattorientierung im FabLab - Digtales Inventar. Handbuchkisten in den Werkstätten Seit Februar 2026 gibt es in jeder Werkstatt eine Eurobox mit Deckel, die für Handbücheraufbewahrung konzipiert ist. Darin finden sich die Handbücher für die im jeweiligen Raum relevanten Dokumentationen in Papierform. Parallel finden sich alle Handbücher auch digital in unserer Inventarplattform things.fablabchemmnitz.de. Gedanken zur Handbuchkiste Sichtbarkeit & Handling: die Kisten sollen in jeder Werkstatt gleich aussehen und somit schnell erkennbar sein die Kisten sollen auffallen: deshalb bekommen sie eine große, farbige Beschriftung zum Schutz vor Staub gibt es einen Klappdeckel Abmessungen: die Kisten sind so hoch, dass alle Handbücher reinpassen und  noch Platz ist, außerdem sollen Dokumente ungeachtet ihres Formats stehen oder liegen können. In der Regel sind Handbücher A4, A5 oder kleiner. Außerdem gibt es für viele Geräte häufig verschiedene Dokumente. Deshalb benötigt es transparente Mappen oder Fächer, um dies sauber ablegen zu können. Dafür gibt es eine simple Einsatzkonstruktion. (ToDo) Dubletten und Synchronisierung: Manche Dinge im Verein haben wir mehrfach. Entsprechend gibt es auch verschiedene Handbücher mehrfach. Entsprechend der jeweiligen Standorte verteilen wir auch die Handbücher auf die korrekten Kisten. Im Falle, dass eine Sache dauerhaft ihren Raum wechselt, muss ggf. auch ein Handbuch von einer Kiste in eine andere Kiste wandern. Wird ein Objekt aus dem Vereinsinventar ausgebucht, z.B. durch Verkauf, dann muss auch das Handbuch entsprechend mitgegeben bzw. aus der Kiste entnommen werden. Beschriften: von außen: Jede Kiste bekommt ein Schild mit Beschriftung und QR-Codes, die u.a. auch auf diese Seite hier verlinken. von innen: auf der Deckelinnenseite befindet sich ein Register mit Inhalt. Die Listendaten werden über eine SQL-Abfrage generiert. Das Register enthält die Spalten: Id, Objekt, Spitzname, Werkstatt(unter)bereich, eine Überschrift mit Inhalt der Raumnummern, für die die Kiste genutzt wird, sowie ein Datum des Registerauszugs. von innen: Ein Hinweis, dass alle Ids über eine URL auch direkt aufgerufen werden können. Kistenumfang: Wir fassen kleinere Bereiche zusammen, damit keine fast leeren Kisten rumstehen. Die Verteilung der Handbuchkisten auf die Räume: A000 + A003 + A005, befindlich in der Grafikwerkstatt A001 + A002, befindlich an der Säule im Workshopbereich A004, befindlich in der Elektronikwerkstatt A006 + A007, befindlich in der Metallwerkstatt A008, befindlich in der Holzwerkstatt Wir verwenden diese 40 x 30 x 32 cm Euroboxen mit Deckel. Teedy API Scripts / database queries Auto-delete guest comments In case you have a guest login enabled and don't want to accept guest spamming you can prevent it using the following bash script with cron trigger (running every 10 minutes). Guest comments are even useless because each guest can delete the guest comments from another guest session. So nobody can guarantee that those will exist a longer time. Deleting such stuff helps to keep clean useful documents which were not created by guests but regular users who wanted to put them to public. Comment deletion This script is looking within i time fence of 10 minutes. If the script skipped in the meantime, it's possible that comments were overlooked. They have to be cleaned manually then. vim /opt/teedy-clean-comments.sh #!/bin/bash #check for commments which have been created the last 10 minutes. if result is not empty we send a new email DB_USER="db_user" DB_NAME="db_name" OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" SELECT t_document.doc_title_c, t_comment.com_content_c, t_comment.com_createdate_d||'\n' FROM t_comment JOIN t_user ON t_comment.com_iduser_c = t_user.use_id_c JOIN t_document ON t_comment.com_iddoc_c = t_document.doc_id_C WHERE t_document.doc_deletedate_d IS NULL AND t_comment.com_deletedate_d IS NULL AND t_user.use_username_c = 'guest' AND t_comment.com_createdate_d + interval '10 minute' >= now() ; ") if [[ ! -z $OUT ]]; then #echo -e -n _${OUT}_ #first inform about the comment via mail echo -e -n " "$OUT | mail -s "dms.yourdomain.de guest comments" post@fix.de OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" SELECT t_comment.com_id_c FROM t_comment JOIN t_user ON t_comment.com_iduser_c = t_user.use_id_c JOIN t_document ON t_comment.com_iddoc_c = t_document.doc_id_C WHERE t_document.doc_deletedate_d IS NULL AND t_comment.com_deletedate_d IS NULL AND t_user.use_username_c = 'guest' AND t_comment.com_createdate_d + interval '10 minute' >= now() ; ") #echo $OUT BASE_URL="https://dms.yourdomain.de" BASE_URL="http://localhost:8080/dms" TEEDY_USER="teedy" AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else for VAR in $OUT; do curl --silent -X DELETE -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/comment/$VAR" -k done fi else echo "Nothing to send and nothing to fix ..." fi cron.d script in /etc/cron.d/teedy-clean-comments SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin */10 * * * * root /opt/teedy-clean-comments.sh > /dev/null You can also directly perform a drop statement by SQL instead (but that might hurt the audit log) DELETE FROM t_comment WHERE com_id_c IN ( SELECT com_id_c FROM t_comment JOIN t_user ON t_comment.com_iduser_c = t_user.use_id_c JOIN t_document ON t_comment.com_iddoc_c = t_document.doc_id_c WHERE t_document.doc_deletedate_d IS NULL AND t_comment.com_deletedate_d IS NULL AND t_user.use_username_c = 'guest' AND t_comment.com_createdate_d + interval '1 minute' >= now()) ; Auto-delete guest documents In case you have a guest login enabled and don't want to accept guest spamming you can prevent it using the following bash script with cron trigger (running every 10 minutes). Guest documents  are even useless because each guest can delete the guest documents from another guest session. So nobody can guarantee that those will exist a longer time. Deleting such stuff helps to keep clean useful documents which were not created by guests but regular users who wanted to put them to public. Document deletion This script is looking within i time fence of 10 minutes. If the script skipped in the meantime, it's possible that documents were overlooked. They have to be cleaned manually then. vim /opt/teedy-clean-documents.sh #!/bin/bash #check for documents which have been created the last 10 minutes. if result is not empty we send a new email DB_USER="user" DB_NAME="db" OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" SELECT D.doc_title_c, U.use_username_c, D.doc_createdate_d||'\n' FROM t_document AS D JOIN t_user AS U ON U.use_id_c = D.doc_iduser_c WHERE D.doc_deletedate_d IS NULL AND ( D.doc_iduser_c IN ( SELECT use_id_c FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL AND G.grp_name_c NOT IN ('Editoren','Administratoren') AND D.doc_createdate_d + interval '10 minute' >= now() ) OR D.doc_iduser_c = 'guest') AND /*guest ist in keiner Gruppe, deshalb muss er gesondert aufgeführt werden*/ D.doc_createdate_d + interval '10 minute' >= now() ; ") if [[ ! -z $OUT ]]; then #echo -e -n _${OUT}_ #first inform about the document via mail echo -e -n " "$OUT | mail -s "your.dms.de guest documents" webmaster@stadtfabrikanten.org OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" SELECT D.doc_id_c FROM t_document AS D JOIN t_user AS U ON U.use_id_c = D.doc_iduser_c WHERE D.doc_deletedate_d IS NULL AND ( D.doc_iduser_c IN ( SELECT use_id_c FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL AND G.grp_name_c NOT IN ('Editoren','Administratoren') AND D.doc_createdate_d + interval '10 minute' >= now() ) OR D.doc_iduser_c = 'guest') AND /*guest ist in keiner Gruppe, deshalb muss er gesondert aufgeführt werden*/ D.doc_createdate_d + interval '10 minute' >= now() ; ") #echo $OUT BASE_URL="https://your.dms.de" BASE_URL="http://localhost:8080/dms" TEEDY_USER="pass" AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else for VAR in $OUT; do curl --silent -X DELETE -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document/$VAR" -k done fi else echo "Nothing to send and nothing to fix ..." fi cron.d script in /etc/cron.d/teedy-clean-documents SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin */10 * * * * root /opt/teedy-clean-documents.sh > /dev/null Auto-delete guest tags In case you have a guest login enabled and don't want to accept guest spamming you can prevent it using the following bash script with cron trigger (running every 10 minutes). Guest tags are even useless because each guest can delete the guest tags from another guest session. So nobody can guarantee that those will exist a longer time. Deleting such stuff helps to keep clean useful tags which were not created by guests but regular users who wanted to put them to public. Tag deletion This script is looking within i time fence of 10 minutes. If the script skipped in the meantime, it's possible that comments were overlooked. They have to be cleaned manually then. vim /opt/teedy-clean-tags.sh #!/bin/bash #check for tags which have been created the last 10 minutes. if result is not empty we send a new email DB_USER="user" DB_NAME="db" OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" /*werden Tags gelöscht, wenn der Benutzer gelöscht wird?*/ SELECT T.tag_name_c, U.use_username_c, T.tag_createdate_d||'\n' FROM t_tag AS T JOIN t_user AS U ON U.use_id_c = T.tag_iduser_c WHERE T.tag_deletedate_d IS NULL AND ( T.tag_iduser_c IN ( SELECT use_id_c FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL AND G.grp_name_c NOT IN ('Editoren','Administratoren') AND T.tag_createdate_d + interval '10 minute' >= now() ) OR T.tag_iduser_c = 'guest') AND /*guest ist in keiner Gruppe, deshalb muss er gesondert aufgeführt werden*/ T.tag_createdate_d + interval '10 minute' >= now() ; ") if [[ ! -z $OUT ]]; then #echo -e -n _${OUT}_ #first inform about the document via mail echo -e -n " "$OUT | mail -s "your.dms.de guest tags" post@fix.org OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" SELECT T.tag_id_c FROM t_tag AS T JOIN t_user AS U ON U.use_id_c = T.tag_iduser_c WHERE T.tag_deletedate_d IS NULL AND ( T.tag_iduser_c IN ( SELECT use_id_c FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL AND G.grp_name_c NOT IN ('Editoren','Administratoren') AND T.tag_createdate_d + interval '10 minute' >= now() ) OR T.tag_iduser_c = 'guest') AND T.tag_createdate_d + interval '10 minute' >= now() ; ") #echo $OUT BASE_URL="https://your.dms.de" BASE_URL="http://localhost:8080/dms" TEEDY_USER="password" AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else for VAR in $OUT; do echo curl --silent -X DELETE -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/tag/$VAR" -k done fi else echo "Nothing to send and nothing to fix ..." fi cron.d script in /etc/cron.d/teedy-clean-comments SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin */10 * * * * root /opt/teedy-clean-tags.sh > /dev/null Change owner of tags/files/documents Within  Teedy, there is no function to change the owner of a document. So we cannot transfer docs from one user to another. In case we want to keep documents in the system, we may not delete the author of the document. We can only deactivate the user. Under some circumstances this is going to make the system untidy. The following steps should be only be done in convenience with DSGVO. Warning: all files in Teedy are encrypted by the users use_privatekey_c value from t_user table. In case we change the owner, we have to decrypt and encrypt the file again. We need to know the correct mapping between file and user each. So please do not try to migrate multiple users at once. At the moment we have no routine to do the re-encryption by console commands. The belonging files are: https://github.com/sismics/docs/blob/master/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java https://github.com/sismics/docs/blob/master/docs-core/src/main/java/com/sismics/docs/core/util/EncryptionUtil.java So do the following steps at your own risk!  Stop Jetty sudo systemctl stop jetty11 Take care to make a backup of /var/docs and the database before doing the following steps: --get some info select * from t_document where doc_title_c = ' /dev/null # “At 01:00 on day-of-month 1.” vim /opt/teedy-prepare-monthly.sh #!/bin/bash BASE_URL="https://dms.yourdomain.de" DB_USER="teedy" DB_NAME="teedy_db" TEEDY_USER="admin" AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else THIS_MONTH=`date +'%m' -d 'now'` #return the recent month in format 01 ... 12 THIS_YEAR=`date +'%Y' -d 'now'` #return the recent year #echo $THIS_MONTH #echo $THIS_YEAR #generate the date of the last day of the recent month TARGET_DATESTRING=$(date --date="$(date +$THIS_YEAR'-'$THIS_MONTH'-'01) + 1 month - 1 day 00:00" +"%s")000 #echo $TARGET_DATESTRING #list of desired tags (clear name). Get the ID from database TAGID_SAMMELDOKUMENT=$( psql -t -U$DB_USER $DB_NAME --command="SELECT tag_id_c FROM t_tag WHERE tag_name_c = 'Sammeldokument' AND tag_deletedate_d IS NULL;") TAGID_RECHNUNG=$( psql -t -U$DB_USER $DB_NAME --command="SELECT tag_id_c FROM t_tag WHERE tag_name_c = 'Rechnung' AND tag_deletedate_d IS NULL;") TAGID_RECHNUNGSKORREKTUR=$(psql -t -U$DB_USER $DB_NAME --command="SELECT tag_id_c FROM t_tag WHERE tag_name_c = 'Rechnungskorrektur' AND tag_deletedate_d IS NULL;") TAGID_AUFTRAG=$( psql -t -U$DB_USER $DB_NAME --command="SELECT tag_id_c FROM t_tag WHERE tag_name_c = 'Auftrag' AND tag_deletedate_d IS NULL;") TAGID_LIEFERSCHEIN=$( psql -t -U$DB_USER $DB_NAME --command="SELECT tag_id_c FROM t_tag WHERE tag_name_c = 'Lieferschein' AND tag_deletedate_d IS NULL;") TAGID_ANGEBOT=$( psql -t -U$DB_USER $DB_NAME --command="SELECT tag_id_c FROM t_tag WHERE tag_name_c = 'Angebot' AND tag_deletedate_d IS NULL;") TAGID_SAMMELDOKUMENT=${TAGID_SAMMELDOKUMENT:1} TAGID_RECHNUNG=${TAGID_RECHNUNG:1} TAGID_RECHNUNGSKORREKTUR=${TAGID_RECHNUNGSKORREKTUR:1} TAGID_AUFTRAG=${TAGID_AUFTRAG:1} TAGID_LIEFERSCHEIN=${TAGID_LIEFERSCHEIN:1} TAGID_ANGEBOT=${TAGID_ANGEBOT:1} #Create new documents - WARNING: NO CHECK FOR DUPLICATE DOCUMENTS RIGHT NOW curl --silent -X PUT -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document" -d "title=Ausgangsrechnungen "$THIS_YEAR"\\"$THIS_MONTH -d "create_date="$TARGET_DATESTRING -d "language=deu" -d "tags="$TAGID_RECHNUNG -d "tags="$TAGID_SAMMELDOKUMENT curl --silent -X PUT -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document" -d "title=Ausgangsrechnungskorrekturen "$THIS_YEAR"\\"$THIS_MONTH -d "create_date="$TARGET_DATESTRING -d "language=deu" -d "tags="$TAGID_RECHNUNGSKORREKTUR -d "tags="$TAGID_SAMMELDOKUMENT curl --silent -X PUT -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document" -d "title=Ausgangsaufträge "$THIS_YEAR"\\"$THIS_MONTH -d "create_date="$TARGET_DATESTRING -d "language=deu" -d "tags="$TAGID_AUFTRAG -d "tags="$TAGID_SAMMELDOKUMENT curl --silent -X PUT -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document" -d "title=Ausgangslieferscheine "$THIS_YEAR"\\"$THIS_MONTH -d "create_date="$TARGET_DATESTRING -d "language=deu" -d "tags="$TAGID_LIEFERSCHEIN -d "tags="$TAGID_SAMMELDOKUMENT curl --silent -X PUT -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document" -d "title=Ausgangsangebote "$THIS_YEAR"\\"$THIS_MONTH -d "create_date="$TARGET_DATESTRING -d "language=deu" -d "tags="$TAGID_ANGEBOT -d "tags="$TAGID_SAMMELDOKUMENT fi Find ugly document titles This statement looks for titles with unrequired whitespace duplicates /*title which contain doubled whitespaces*/ SELECT doc_title_c FROM t_document WHERE LENGTH(RTRIM(LTRIM(doc_title_c))) <> LENGTH(doc_title_c) OR doc_title_c LIKE '% %' AND doc_deletedate_d IS NULL ; Gastzugang und spezielle Anpassungen (serverseitiger Code/Scripts) Wir wollen unsere Inventarplattform so aufbereiten, dass sie auch für Gäste etwas zum Stöbern bietet und die Funktionen von Teedy als praktibale Open Source Software zeigt. Teedy hat einen Gastmodus, der von Haus aus leider neben der regulären Verwendung auch ein gewisses Spamming per Web-Browser und API erlaubt. Zur Reduktion haben wir Scripts geschrieben, die dies unterbinden. Wir möchten Kommentare und wild angelegte Tags und Dokumente damit vermeiden, da sie die Struktur zu leicht unlesbar machen. Gäste können Kommentare, Tags und Dokumente von anderen Gastsitzungen generell einfach modifizieren oder löschen. Deshalb eignet sich der Gastmodus nur als reiner Betrachtermodus. Selbiges trifft auch geteilte/generische) ReadOnly-Benutzer zu. Die Speicher-Quota für den Gast und für sonstige ReadOnly-Benutzer beträgt 0 MB. Neben Gast-Tags werden Tags von anderen Nutzern, die keine Admins oder Editoren sind, ebenso vom System erkannt und automatisch gelöscht. Wir behalten uns vor unser System für Mitglieder zu kapseln, sodass das Tool nur noch vereinsintern und nicht durch Gäste genutzt werden kann, falls zu viele Schabernackversuche auftreten. Scripts (diese laufen jeweils als cron job alle 10 Minuten) Kommentare (erzeugt von "guest" user) werden automatisch gescannt und gelöscht Siehe Auto-delete guest comments. Ein Wiederherstellen ist mit dem Zurücksetzen der com_deletedate_d Spalte möglich Der Haupt-Admin erhält eine Mail über alle gelöschten Kommentare ins Webmaster-Postfach Dokumente (erzeugt von "guest" user oder anderen Benutzern, die nicht den Gruppen "Editoren" oder "Administratoren" angehören) werden automatisch gescannt und ebenso gelöscht Siehe Auto-delete guest documents. Ein Wiederherstellen ist mit dem Zurücksetzen der doc_deletedate_d Spalte möglich. Ein Wiederherstellen der Dateien ist allerdings nicht möglich Der Haupt-Admin erhält eine Mail über alle gelöschten Dokumente ins Webmaster-Postfach Tags (erzeugt von "guest" user oder anderen Benutzern, die nicht den Gruppen "Editoren" oder "Administratoren" angehören) werden automatisch gescannt und gelöscht Siehe Auto-delete guest tags Ein Wiederherstellen ist mit dem Zurücksetzen der tag_deletedate_d Spalte möglich Der Haupt-Admin erhält eine Mail über alle gelöschten Tags ins Webmaster-Postfach Manuelle Pflege der Tag-Besitzer Wer Tags sehen und bearbeiten kann, kann vom Admin entweder über die Datenbank oder das User Interface gesteuert werden. Aus Gründen der Gesamtübersicht (Grafana ACL Tabelle) entfernen wir Einzelbenutzer und definieren nur Gruppen. Leider können in Teedy Tags nicht von ihrem Einzelbenutzer getrennt werden. Zumindest können die Tags nicht gelöscht werden, die vom Haupt-Admin erstellt worden sind. Letzteres ist nur über Datenbank Update SQL Scripts änderbar: /*Finde alle Tags, die nicht dem Haupt-Admin gehören. Gast-Tags tauchen hier generell nicht auf*/ SELECT T.tag_name_c AS "Tag", U.use_username_c AS "Ersteller", T.tag_createdate_d AS "Erstelldatum" FROM t_tag AS T JOIN t_user AS U ON U.use_id_c = T.tag_iduser_c WHERE T.tag_deletedate_d IS NULL AND T.tag_iduser_c IN ( SELECT use_id_c FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL ) AND U.use_id_c != 'admin' ; /* Übertrage Ersteller/Eigentümer des Tags von anderen Editor/Administratoren auf den Haupt-Administrator. Dazu müssen nachträglich ebenso die ACLs angepasst werden. Die Spalte "tag_iduser_c" spielt für die Berechtigung keine direkte Rolle sondern enthält nur die Information, wer den Tag initial erstellt hat. Aber da wir in den ACLs den User gegen admin tauschen, machen wir es somit konsistent! */ /*UPDATE t_tag SET tag_iduser_c = 'admin';*/ /*dieses Statement ist überflüssig und sollte nicht ausgeführt werden*/ /* Finde alles, was nicht "Editoren", "ErweiterteBetrachter" oder "Aktivmitglieder" ist. */ SELECT A.acl_id_c, T.tag_name_c "Tag", A.acl_perm_c AS "Permission", U.use_username_c FROM t_tag AS T JOIN t_acl AS A ON A.acl_sourceid_c = T.tag_id_c LEFT JOIN t_user AS U ON U.use_id_c = A.acl_targetid_c WHERE T.tag_deletedate_d IS NULL AND --A.acl_deletedate_d IS NULL AND U.use_disabledate_d IS NULL AND U.use_id_c = 'admin' ORDER BY T.tag_name_c ; /* Wir wollen die ACLs nur auf Gruppen festlegen. Wir nehmen auch dem Haupt-Admin die Benutzerberechtigungen, da dieser Einzelbenutzer die Übersichtlichkeit (in Grafana) künstlich aufbläht. Berechtigungen für Einzelnutzer soll es generell nicht geben (Ausnahme Tag "public" für den User "guest") */ UPDATE t_acl SET acl_deletedate_d = NOW() WHERE acl_id_c IN ( SELECT A.acl_id_c FROM t_tag AS T JOIN t_acl AS A ON A.acl_sourceid_c = T.tag_id_c LEFT JOIN t_user AS U ON U.use_id_c = A.acl_targetid_c WHERE T.tag_deletedate_d IS NULL AND U.use_disabledate_d IS NULL AND U.use_id_c = 'admin' ) ; Manuelle Pflege der Dokumenten-Besitzer Wer Dokumente sehen und bearbeiten kann, kann vom Admin entweder über die Datenbank oder das User Interface gesteuert werden: /*Finde alle Dokumente, die nicht dem Haupt-Admin gehören. Gast-Tags tauchen hier generell nicht auf*/ SELECT D.doc_title_c AS "Gegenstand", U.use_username_c AS "Ersteller", D.doc_createdate_d AS "Erstelldatum" FROM t_document AS D JOIN t_user AS U ON U.use_id_c = D.doc_iduser_c WHERE D.doc_deletedate_d IS NULL AND D.doc_iduser_c IN ( SELECT use_id_c FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL ) AND U.use_id_c != 'admin' ; /* Übertrage Ersteller/Eigentümer des Tags von anderen Editor/Administratoren auf den Haupt-Administrator. Dazu müssen nachträglich ebenso die ACLs angepasst werden. Die Spalte "doc_iduser_c" spielt für die Berechtigung keine direkte Rolle sondern enthält nur die Information, wer das Dokument initial erstellt hat. Aber da wir in den ACLs den User gegen admin tauschen, machen wir es somit konsistent! */ /*UPDATE t_document SET doc_iduser_c = 'admin';*/ /*dieses Statement ist überflüssig und sollte nicht ausgeführt werden*/ /* Finde alles, was nicht "Editoren", "ErweiterteBetrachter" oder "Aktivmitglieder" ist. */ SELECT A.acl_id_c, D.doc_title_c "Gegenstand", A.acl_perm_c AS "Permission", U.use_username_c FROM t_document AS D JOIN t_acl AS A ON A.acl_sourceid_c = D.doc_id_c LEFT JOIN t_user AS U ON U.use_id_c = A.acl_targetid_c WHERE D.doc_deletedate_d IS NULL AND --A.acl_deletedate_d IS NULL AND U.use_disabledate_d IS NULL AND U.use_id_c = 'admin' ORDER BY D.doc_title_c ; Reindex / index repairing script This script is for use as cron for example. By teedy username and password #!/bin/bash BASE_URL="https://dms.yourdomain.de" AUTH_TOKEN=$(curl -i -X POST -d username="username" -d password="password" "$BASE_URL/api/user/login" -k|grep "auth_token"|cut -c24-59) if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/app/batch/reindex" -k curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/user/logout" -k fi By database user and password #!/bin/bash BASE_URL="https://dms.yourdomain.de" DB_USER="teedy DB_NAME="teedy_db" TEEDY_USER="theuser" AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/app/batch/reindex" -k fi Reprocess all files for a given user Note that you can only re-process files which are your's! If you want to reprocess everything for everybody you will need to loop over each user while getting his username/passwort or auth_token (only way if user has 2FA secured account) from database. An auth_token can only stripped out from database if the user did not log off (otherwise it's null/empty). reprocessing makes sense in case of updating tesseract version which might improve the OCR text output quality. So for longterm storing of documents it might be worth to reprocess files. Reprocessing will raise a lot of background tasks into schedule: Note: Reprocessing of files does not work in quick upload mask (only works for files assigned to existing documents): Way 1: pure API calls → for a regular user (no 2FA secured) #!/bin/bash BASE_URL="https://dms.yourdomain.de" TEEDY_USER="admin" TEEDY_USER_PASS="password" AUTH_TOKEN=$(curl -i -X POST -d username="$TEEDY_USER" -d password="$TEEDY_USER_PASS" "$BASE_URL/api/user/login" -k|grep "auth_token"|cut -c24-59) BACKUP_DIR="/backup/teedy" TARGET_DOCLIST_JSON=$BACKUP_DIR"/documentlist_forfiles.json" TARGET_FILELIST_JSON=$BACKUP_DIR"/filelist.json" mkdir -p "$BACKUP_DIR" rm $TARGET_DOCLIST_JSON rm $TARGET_FILELIST_JSON echo "Retrieving document list" curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document/list?limit=0" -k | jq . > "$TARGET_DOCLIST_JSON" echo "Retrieving file list based on document list" COUNT=0 jq -c '.|{documents}|.[]|.[]|{id}+{title}+{create_date}' "$TARGET_DOCLIST_JSON" | while read -r i; do COUNT=$((COUNT + 1)) DOC_ID=$(jq -c '.|{id}|.id' <<< $(printf '%s\n' "$i")) DOC_ID=${DOC_ID:1:-1} curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/list?id=$DOC_ID" >> "$TARGET_FILELIST_JSON" echo Getting $COUNT : $DOC_ID #put some sleep time here if your server has less ressources. Otherwise you might overload PostgreSQL as well as Jetty. It leads to unstability of the running instance #have a look at https://dms.yourdomain.de/#/settings/monitoring to check the average OCR time per document. Usually 3 to 10 seconds should be normal #sleep 5 done echo "Starting to process files" COUNT=0 jq -c '.[]|.[]|{id}' "$TARGET_FILELIST_JSON" | while read -r i; do COUNT=$((COUNT + 1)) FILE_ID=$(jq -c '.|{id}|.id' <<< $(printf '%s\n' "$i")) FILE_ID=${FILE_ID:1:-1} echo Processing $COUNT : $FILE_ID curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/$FILE_ID/process" done curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/user/logout" -k Way 2: API + psql calls → for a regular user (no 2FA secured) This script is much shorter and more elegant to reprocess. Note that this script logins in the user by a fresh created token. It will fail if the user login is secured with 2FA. In this case see below! #!/bin/bash BASE_URL="https://dms.yourdomain.de" DB_USER="teedy" DB_NAME="teedy_db" TEEDY_USER="admin" TEEDY_USER_PASS="password" AUTH_TOKEN=$(curl -i -X POST -d username="$TEEDY_USER" -d password="$TEEDY_USER_PASS" "$BASE_URL/api/user/login" -k|grep "auth_token"|cut -c24-59) for FILE_ID in $(psql -t -U$DB_USER $DB_NAME --command="SELECT fil_id_c FROM t_file AS F JOIN t_user AS U ON U.use_id_c = F.fil_iduser_c WHERE F.fil_deletedate_d IS NULL AND U.use_username_c = '$TEEDY_USER';"); do echo PROCESSING "$FILE_ID" curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/$FILE_ID/process" sleep 5 done #logout curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/user/logout" -k Way 3: API + psql calls → for a single 2FA secured user The following script can be used if your user is secured with 2FA. Note that the user needs to be logged in once (if he logs off the recent token will be destroyed). #!/bin/bash BASE_URL="https://dms.yourdomain.de" DB_USER="teedy" DB_NAME="teedy_db" TEEDY_USER="admin" #this reads exactly one token from the given user. If the user is not logged in it will be null and the token will be null too! AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") for FILE_ID in $(psql -t -U$DB_USER $DB_NAME --command="SELECT fil_id_c FROM t_file AS F JOIN t_user AS U ON U.use_id_c = F.fil_iduser_c WHERE F.fil_deletedate_d IS NULL AND U.use_username_c = '$TEEDY_USER';"); do echo PROCESSING "$FILE_ID" #curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/$FILE_ID/process" sleep 2 done #do not logout to keep the token Way 4: API + psql calls → loop over all users (2FA secured users) #!/bin/bash BASE_URL="https://dms.yourdomain.de" DB_USER="teedy" DB_NAME="teedy_db" TEEDY_USERS=$(psql -t -U$DB_USER $DB_NAME --command="SELECT use_username_c FROM t_user;") for TEEDY_USER in $TEEDY_USERS; do echo LOGGING IN AS $TEEDY_USER #this reads exactly one token from the given user. If the user is not logged in it will be null and the token will be null too! AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") for FILE_ID in $(psql -t -U$DB_USER $DB_NAME --command="SELECT fil_id_c FROM t_file AS F JOIN t_user AS U ON U.use_id_c = F.fil_iduser_c WHERE F.fil_deletedate_d IS NULL AND U.use_username_c = '$TEEDY_USER';"); do echo PROCESSING "$FILE_ID" curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/$FILE_ID/process" sleep 2 done done #do not logout after processing (to keep the token alive!) Scan for users without groups To have better control about users, which logged in first time by LDAP and did not get mapped to default group, we use some SQL / bash script to inform the webmaster about that: vim /opt/teedy-unmapped-users.sh #!/bin/bash #check for new users which have been created the last 10 minutes, but have no propery group membership. if result is not empty we send a new email DB_USER="db_user" DB_NAME="db_name" OUT=$(psql -t -U$DB_USER $DB_NAME --no-align --command=" SELECT DISTINCT use_username_c, use_email_c FROM t_user AS U WHERE U.use_deletedate_d IS NULL AND U.use_disabledate_d IS NULL AND U.use_username_c NOT IN ('guest') EXCEPT SELECT DISTINCT use_username_c, use_email_c --count(use_id_c) AS "Gruppenanzahl" FROM t_user AS U JOIN t_user_group AS UG ON UG.ugp_iduser_c = U.use_id_c JOIN t_group AS G ON G.grp_id_c = UG.ugp_idgroup_c WHERE U.use_deletedate_d IS NULL AND U.use_disabledate_d IS NULL AND UG.ugp_deletedate_d IS NULL AND G.grp_deletedate_d IS NULL AND G.grp_name_c NOT IN ('Administratoren', 'Group2', 'Editors', 'Viewers') AND U.use_username_c NOT IN ('admin', 'anotherUser', 'anotherUser2') GROUP BY use_email_c, use_username_c, use_id_c ; ") if [[ ! -z $OUT ]]; then #echo -e -n _${OUT}_ BASE_URL="https://dms.domain.org" BASE_URL="http://localhost:8080/dms" TEEDY_USER="ujoZzkKw2g" AUTH_TOKEN=$(psql -t -U$DB_USER $DB_NAME --command="SELECT aut_id_c FROM t_authentication_token AS A JOIN t_user AS U ON U.use_id_c = A.aut_iduser_c WHERE use_username_c = '$TEEDY_USER' AND aut_lastconnectiondate_d IS NOT NULL LIMIT 1;") if [ -z "$AUTH_TOKEN" ] then echo "NO AUTHTOKEN. Please create a session for the user first to automate things!" >&2 #print to stderr to trigger cron.d mail on error exit 1 else for LINE in $OUT; do IFS='|' read -r -a LINE <<< $OUT USER=${LINE[0]} EMAIL=${LINE[1]} echo $USER echo $EMAIL MSG="Mapping $USER to 'Aktivmitglieder'" curl -X PUT -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/group/Aktivmitglieder" -d "username=$USER" -k | jq . echo $MSG echo -e -n " "$MSG | mail -s "dms.domain.org | Dein Account wurde freigeschalten" webmaster@domain.org #copy echo -e -n " "$MSG | mail -s "dms.domain.org | Dein Account wurde freigeschalten" $MAIL done fi else echo "No users to map to groups ..." fi Undelete document Once a document was deleted, we can restore most information except document files: SELECT * FROM t_document WHERE doc_id_c = 'b9a538d2-906f-4566-b00d-ee4aa70d8ff8'; UPDATE t_document SET doc_deletedate_d = NULL WHERE doc_id_c = 'b9a538d2-906f-4566-b00d-ee4aa70d8ff8'; Teedy File and Document Processing Check index integrity / recompute quota Sometimes the used space is wrong and may look like this: See also: https://github.com/sismics/docs/issues/345 Checks on file system cd /var/docs/ find ./ -type f -name '*' -exec du -ch {} + | grep total$ find ./ -type f -name '*_web' -exec du -ch {} + | grep total$ find ./ -type f -name '*_thumb' -exec du -ch {} + | grep total$ cd /var/docs/storage/ ll |grep -v "thumb\|web" |wc -l #Anzahl der Dateien, die weder _web noch _thumb sind ll | grep "web" | wc -l #Anzahl _web Dateien (sollte idealerweise mit _thumb deckend sein) ll | grep "thumb" | wc -l #Anzahl _thumb Dateien (sollte idealerweise mit _web deckend sein) Checks in Database / Recalculate Quota Get a list of all files from database which should exist/not exist on filesystem /*Get all files which should be existent on HDD*/ SELECT fil_id_c AS "FileID", fil_iduser_c AS "Besitzer" FROM t_file WHERE fil_deletedate_d IS NULL ; /*Get all files which should be deteled from HDD*/ SELECT fil_id_c AS "FileID", fil_iduser_c AS "Besitzer" FROM t_file WHERE fil_deletedate_d IS NOT NULL ; Get the filesystem storage list The following commands generate file output and send them by mail to you. If your application server and your database are on the same server you just can create some bash script to make some complete scripting solution automating psql. cd /var/docs/ #get recent storage list as CSV ll | grep -v "thumb\|web" | awk 'NR > 3 {print "\"",$5,"\";\"",$9,"\""}' | sed 's/[[:blank:]]//g' | mail -s "Teedy FileSystem" your@mail.address #or get it as SQL statement ll | grep -v "thumb\|web" | awk 'NR > 3 {print "UPDATE#TMP_QUOTA_CHECK#SET#fil_size=\x27",$5,"\x27#WHERE#fil_id_c=\x27",$9,"\x27;"}' | sed 's/[[:blank:]]//g' | sed 's/#/ /g' | mail -s "Teedy FileSystem" your@mail.address Create a temporary SQL table to calculate quota CREATE TABLE TMP_QUOTA_CHECK( fil_id_c character varying(36), fil_iduser_c character varying(36), fil_size integer ); Pre-Fill the table with existing data INSERT INTO TMP_QUOTA_CHECK SELECT fil_id_c, fil_iduser_c FROM t_file WHERE fil_deletedate_d IS NULL ; Insert file size data from upper generated SQL UPDATE awk statements (bash), then perform some check and calculate total quota sizes SELECT * FROM tmp_quota_check WHERE fil_size IS NOT NULL; SELECT * FROM tmp_quota_check WHERE fil_size IS NULL; --if your filesystem is consistent this must be empty! If not please check if Teedy failed to delete files in the past SELECT CONCAT('UPDATE t_user SET use_storagecurrent_n=''',SUM(fil_size),''' WHERE use_id_c =''',fil_iduser_c,''';') FROM TMP_QUOTA_CHECK GROUP BY fil_iduser_c ; Insert the new values into existing target table using the generated output statements from above Drop temporary table DROP TABLE TMP_QUOTA_CHECK; Removed commit https://github.com/sismics/docs/commit/d0335b6b161058250ec8cc44eeb2357f96176f54#diff-54e77110186f974f07aeb29c2673496fL690 Fix Preview Bug In case the file preview is erroneous/empty but the file can be processed and it can be downloaded by URL like https://dms.yourdomain.de/api/file/:FILE_ID/data: Root Cause: unkown. Seems to happen after migration from H2 to PostgreSQL Fixing proposal Remove the _thumb and _web files and let Teedy create new ones by running a (complete) re-processing cd /var/docs/storage ll | grep "" mv _thumb _thumb.bak mv _web _web.bak #or just move all stuff to some sub directory if you plan to re-process the complete file system: mkdir thumb_web_bak mv *_thumb *_web thumb_web_bak/ #restart your instance to let Teedy recognize that changes due to caching sudo systemctl restart jetty9.service Reprocess documents See Teedy API Scripts / database queries for reprocessing of everything. Grafana Monitoring / Statistics Description A Grafana monitoring dashboard for Teedy (Sismics Docs) statistics. Helpful to have a look over security things and the effort you put in your instance. Please check if this is okay for your own use - regarding privacy protection of the mates working together on the same instance. Sorry the language for that dashboard is german but you can translate it easily using tools like deepl.com. Download https://gitea.fablabchemnitz.de/vmario/teedy-statistics/src/branch/master https://grafana.com/grafana/dashboards/11556 Icons in document titles Inside Teedy we can use  funny icons, if we know the correct Unicode number. Copy + Paste. Just select the one's you need and paste them into Teedy. It works for title and description ☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☔ ☕ ☖ ☗ ☘ ☙ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯ ♰ ♱ ♲ ♳ ♴ ♵ ♶ ♷ ♸ ♹ ♺ ♻ ♼ ♽ ♾ ♿ ⚀ ⚁ ⚂ ⚃ ⚄ ⚅ ⚆ ⚇ ⚈ ⚉ ⚊ ⚋ ⚌ ⚍ ⚎ ⚏ ⚐ ⚑ ⚒ ⚓ ⚔ ⚕ ⚖ ⚗ ⚘ ⚙ ⚚ ⚛ ⚜ ⚝ ⚞ ⚟ ⚠ ⚡ ⚢ ⚣ ⚤ ⚥ ⚦ ⚧ ⚨ ⚩ ⚪ ⚫ ⚬ ⚭ ⚮ ⚯ ⚰ ⚱ ⚲ ⚳ ⚴ ⚵ ⚶ ⚷ ⚸ ⚹ ⚺ ⚻ ⚼ ⚽ ⚾ ⚿ ⛀ ⛁ ⛂ ⛃ ⛄ ⛅ ⛆ ⛇ ⛈ ⛉ ⛊ ⛋ ⛌ ⛍ ⛎ ⛏ ⛐ ⛑ ⛒ ⛓ ⛔ ⛕ ⛖ ⛗ ⛘ ⛙ ⛚ ⛛ ⛜ ⛝ ⛞ ⛟ ⛠ ⛡ ⛢ ⛣ ⛤ ⛥ ⛦ ⛧ ⛨ ⛩ ⛪ ⛫ ⛬ ⛭ ⛮ ⛯ ⛰ ⛱ ⛲ ⛳ ⛴ ⛵ ⛶ ⛷ ⛸ ⛹ ⛺ ⛻ ⛼ ⛽ ⛾ ⛿ ✅ https://en.wikipedia.org/wiki/Miscellaneous_Symbols Importer for Windows The Bulk file importer tool is based on NodeJS Documentation also available under https://github.com/sismics/docs/tree/master/docs-importer Download the importer The importer can be also downloaded at Github. The most recent version to find is https://github.com/sismics/docs/releases/download/v1.5/docs-importer-win.exe (which is an old one). We can also build ourselves. See below. Building the importer Requirements NodeJS v10.18.0 (64 Bit) → https://nodejs.org/dist/v10.18.0/node-v10.18.0-win-x64.zip - newer version will fail! Git → https://git-scm.com/download/win Check your %PATH% variable. This should contain the following executables nodejs.exe git.exe Open elevated CMD Shell press CTRL + R to open "Run" Enter "cmd" Click ok Start elevated shell from current cmd. This will open up a new cmd shell with admin privileges Clone Repository and run build cd C:\ git clone https://github.com/sismics/docs.git cd docs\docs-importer npm install npm install -g pkg pkg . Check the built output Configure to use the importer Create new share upload directory A special upload folder should be created, e.g. C:\TeedyShare - from this folder the documents will be uploaded and cut later. Start docs-importer-win.exe and configure it Please put the docs-importer-win.exe to some fixed place where it should stay, like in C:\Teedy\docs-importer-win.exe Note that the screenshot contains some older directory name. After entering the connection data this information will be persisted in %userprofile%\config\preferences\com.sismics.docs.importer.pref Start as daemon and test upload The program can be started with the switch -d. It queries the specified folder every 30 seconds and uploads any existing documents to the DMS. The files are then deleted locally. Install as Windows Service Create file C:\Teedy\teedy-service.ps1 Start-Process -WindowStyle hidden -FilePath C:\Teedy\docs-importer-win.exe -ArgumentList "-d" Create a new task in task scheduler Sorry for german screenshots. And please replace "SismicsDocs" with "Teedy" everywhere. Check if service is running. Look for docs-importer-win.exe Create a new Desktop Shortcut for your share directory Manually fix broken document relations in database Find documents which have relations to other documents which already were deleted Sometimes documents link to other documents but the links are invalid because the linked document is not available anymore. We can manually reset those links only because there is no working mechanism yet. Get all relations with their id's SELECT R.rel_id_c, F.doc_title_c "From", T.doc_title_c "To" FROM t_relation AS R JOIN t_document AS T ON R.rel_iddocfrom_c = T.doc_id_c JOIN t_document AS F ON R.rel_iddocto_c = F.doc_id_c WHERE R.rel_deletedate_d IS NULL AND T.doc_deletedate_d IS NOT NULL AND F.doc_deletedate_d IS NULL UNION SELECT R.rel_id_c, T.doc_title_c "From", F.doc_title_c "To" FROM t_relation AS R JOIN t_document AS T ON R.rel_iddocfrom_c = T.doc_id_c JOIN t_document AS F ON R.rel_iddocto_c = F.doc_id_c WHERE R.rel_deletedate_d IS NULL AND T.doc_deletedate_d IS NULL AND F.doc_deletedate_d IS NOT NULL ORDER BY "From" ; Now we just set the delete flag of the relation to a date not NULL so it will unshow in Teedy visually. That will remove invalid links. update t_relation SET rel_deletedate_d = NOW() WHERE rel_id_c IN ('your relation id 1', 'your relation id 2', .. , 'your relation id n');   Optical Character Recognition (OCR) and Scanning Handling OCR data is stored in Teedy database table t_file which containts the string column fil_content_c. In H2 the data is stored als plaintext string. In PostgreSQL the column is filled as datatype ::text. A normal select returns number. The unecrypted OCR text data can be accessed from the large object by using some SQL statement like select fil_name_c, convert_from(loread(lo_open(fil_content_c::int, 131072), 999999999), 'UTF8') from t_file WHERE fil_deletedate_d IS NULL AND fil_content_c IS NOT NULL limit 1; Teedy uses a built in process runner to start the binary tesseract with a language parameter. This works if " tesseract" is contained in $PATH (Linux) or %PATH% (Windows) environment variable. Fixing faulty fil_content_c data the easy way Some quick fix for issue described in https://github.com/sismics/docs/issues/451 SELECT fil_content_c FROM t_file WHERE LENGTH(fil_content_c) > 6 ORDER BY fil_createdate_d DESC; UPDATE t_file SET fil_content_c = NULL WHERE LENGTH(fil_content_c) > 6; Converting LOB data to plain text (was required at some point from updating Teedy 1.8 to Teedy 1.9) /* Show items which start with useless linefeeds. We need to correct those because otherwise we cannot continue with following statements (casting "fil_content_c::int" will fail and other issues) Result may be empty */ SELECT fil_id_c, fil_name_c, fil_content_c FROM t_file WHERE fil_content_c LIKE E'%\n' ; /*Trim beginning linefeeds (only) away*/ UPDATE t_file SET fil_content_c = TRIM(e'\n' FROM fil_content_c) WHERE fil_content_c LIKE E'%\n' ; /* Show faulty data which would return "invalid byte sequence for encoding "UTF8": 0x00" or similar. First we build some function to check for valid UTF8 bytea because sometimes we have faulty stuff inside DB Result may be empty */ CREATE FUNCTION is_valid_utf8(bytea) RETURNS boolean LANGUAGE plpgsql AS $$BEGIN PERFORM convert_from($1, 'UTF8'); RETURN TRUE; EXCEPTION WHEN character_not_in_repertoire THEN RAISE WARNING '%', SQLERRM; RETURN FALSE; END;$$; SELECT fil_id_c, fil_name_c, loread(lo_open(fil_content_c::int, CAST( x'20000' AS integer)), 999999999) AS BYTE_DATA, LENGTH(loread(lo_open(fil_content_c::int, CAST(x'20000' AS integer)), 999999999)) AS LEN FROM t_file WHERE fil_content_c IS NOT NULL AND fil_content_c != '' AND LENGTH(fil_content_c) <= 6 AND is_valid_utf8(fil_content_c::bytea) IS FALSE ; /*We set NULL to all items with faulty UTF-8 encoding (if there were some from previous statement)*/ UPDATE t_file SET fil_content_c = NULL WHERE fil_content_c IS NOT NULL AND fil_content_c != '' AND LENGTH(fil_content_c) <= 6 AND is_valid_utf8(fil_content_c::bytea) IS FALSE ; /* Select OCR content which is in LOB format (Large Object) and valid UTF-8 */ SELECT fil_id_c, fil_name_c, fil_content_c, fil_content_c::bytea, /*shows "invisible" data which does not trigger NULL or ''*/ loread(lo_open(fil_content_c::int, CAST( x'20000' AS integer)), 999999999) AS BYTE_DATA, /*we use the encoding we used to create the database. See setup instructions. Usually this is "UNICODE" or "UTF8"*/ LENGTH(loread(lo_open(fil_content_c::int, CAST(x'20000' AS integer)), 999999999)) AS LEN, convert_from(loread(lo_open(fil_content_c::int, CAST(x'20000' AS integer)), 999999999), 'UNICODE') as "fil_content_c" FROM t_file WHERE fil_content_c IS NOT NULL AND fil_content_c != '' AND LENGTH(fil_content_c) <= 6 AND is_valid_utf8(fil_content_c::bytea) IS TRUE ORDER BY LEN ASC ; /*Convert LOB data into plain text. First we do it for a custom selected file with fil_id_c*/ UPDATE t_file SET fil_content_c = convert_from(loread(lo_open(fil_content_c::int, CAST( x'20000' AS integer)), 999999999), 'UNICODE')::TEXT WHERE fil_id_c = '13411bb0-12fd-4e25-b483-2e2d18b344ed' ; /*Check the conversion value*/ SELECT fil_id_c, fil_name_c, fil_content_c FROM t_file WHERE fil_id_c = '13411bb0-12fd-4e25-b483-2e2d18b344ed' ; /* Now we do mass processing for LOB to plain text DO NOT CONTINUE WITH OTHER STATEMENTS IF THIS ONE FAILS AND CHECK THE UPPER ONES AGAIN */ UPDATE t_file SET fil_content_c = convert_from(loread(lo_open(fil_content_c::int, CAST( x'20000' AS integer)), 999999999), 'UNICODE')::TEXT WHERE fil_content_c IS NOT NULL AND fil_content_c != '' AND LENGTH(fil_content_c) <= 6 AND is_valid_utf8(fil_content_c::bytea) IS TRUE ; /*We fix again useless linefeeds by trimming*/ UPDATE t_file SET fil_content_c = TRIM(e'\n' FROM fil_content_c) WHERE fil_content_c LIKE E'%\n' ; /* Now that we converted all the LOB stuff we do mass processing for remaining stuff with length lesser than 6 chars because those OCR values are just crap WARNING: DO NOT RUN THIS BEFORE CONVERTING BECAUSE YOU WILL OVERWRITE. IF YOU DID YOU WILL NEED TO REPROCESS ALL DOCUMENTS! */ UPDATE t_file SET fil_content_c = NULL WHERE fil_content_c IS NOT NULL AND fil_content_c != '' AND LENGTH(fil_content_c) <= 6 ; /*Finally we check again the values visually*/ SELECT fil_id_c, fil_name_c, fil_content_c FROM t_file /*Finally re-run the indexing from background UI web interface or API to have a good search index again*/ Tesseract OCR command line binary The installation of tesseract is simple. Note that for different operating system versions there are different tesseract versions. All tesseract versions work different in their speed and quality. We figured out that tesseract 3 on Ubuntu 16 works much faster than tesseract 4 on Ubuntu 18. https://github.com/tesseract-ocr/tesseract/wiki Installation For Linux users: #install regular version sudo apt install tesseract-ocr tesseract-ocr-deu #will install the most recent version belonging to your OS. So older system you might get older tesseract #install devel version. See https://launchpad.net/~alex-p/+archive/ubuntu/tesseract-ocr-devel sudo add-apt-repository ppa:alex-p/tesseract-ocr-devel sudo apt-get update sudo apt install tesseract-ocr tesseract-ocr-deu #add your desired languages here For Windows users: https://github.com/tesseract-ocr/tesseract/wiki/4.0-with-LSTM#4x-for-windows Critical optimization https://github.com/tesseract-ocr/tesseract/issues/2611 Some users said that disabling multiprocessing in tesseract fixes speed problems. Therefore some environment flag should be set using export. See also Environment Configuration export OMP_THREAD_LIMIT=1 Scanner Apps for Smartphones There are a LOT of scanner apps in PlayStore. Most of them have nearly same naming. The following list is only a minimalistic overview of stuff around the web. Mainly we are looking for open source applications. Genius Scan CamScanner Notebloc OpenNoteScanner SwiftScan Wishes automatic upload to or sending by mail problem: what if you use multiple instances of DMS? Then you will need multiple upload locations. All known app do not deal with that feature. With app cloning the scanner app could be multiplied so each Scanner app instance has its own configuration. Then the scanner app cand send to the correct inbox per DMS instance Searching and Tags Tags Tags can be nested. For example, the "Insurance" tag can be created and, for example, the "Signal Iduna" and "Ammerländer" tags below the tag. These are child elements. If you search for "Ammerländer", you will only find documents that are tagged with Ammerländer. If you search for "insurance", you will find documents that are tagged with "insurance", "Ammerländer" or "Signal Iduna" at the same time. Unfortunately, tags can be created twice! Attention! Search operators Operator values Explanation by: String The creator of the document tag: String document with given tag !tag: String document without given tag before: date (allowed formats: yyyy or yyyy-MM or yyyy-MM-dd) created before date ubefore: date (allowed formats: yyyy or yyyy-MM or yyyy-MM-dd) edited before date after: date (allowed formats: yyyy or yyyy-MM or yyyy-MM-dd) created after date uafter: date (allowed formats: yyyy or yyyy-MM or yyyy-MM-dd) edited after date at: date (allowed formats: yyyy or yyyy-MM or yyyy-MM-dd) created at date uat: date (allowed formats: yyyy or yyyy-MM or yyyy-MM-dd) edited at date lang: "eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld", "tur", "heb" language mime: does not work yet! image/jpeg application/zip application/pdf image/png text/csv text/plain application/vnd.openxmlformats-officedocument.presentationml.presentation application/vnd.openxmlformats-officedocument.wordprocessingml.document application/octet-stream shared: yes, no workflow: "me", String full: String Use OCR full-text search (files must have been processed with Tesseract!) - full search is default since Teedy 1.9 simple: String Performs simple search instead full search (ignores OCR) * Wildcard only possible at the end of the search input string. Not allowed before or in a word | Pipe operator. Use this to filter things like "or". Example green|duck find docs which have green or duck in title "" phrases can be put into quotes. This will return a more exact result. For example: "a green duck" rreturns docs with the exact title "a green duck" a green duck returns docs which contain a, green or duck The operators ?, NOT, AND, OR are not possible - they do nothing. Lucene Core Dokumentation → Most operators unfortunately don't work in Teedy. All other things which cannot be expressed by the given search parameters can be scripted by SQL queries for H2 or PSQL database instead. You will need to have according access to do this. You can find a lot of useful SQL statement for filtering out your DMS in our Grafana Dashboard → Grafana Monitoring / Statistics Example scheme for document title for things like invocies \ - [#] Search Filter - Combination from words, tags and other operators Example: find documents which are tagged by invoice and company and which have "01" and "2018" in their title tag:company tag:invoice 2018 01 Teedy Installation and Configuration About Teedy Document Management System Welcome to the unofficial documentation space for Teedy. Teedy (prior "Sismics Docs") is an open source enterprise content management system (ECM) and/or document management system (DMS) with a lot of features and a modern user interface. We use it for different purposes. Source Code: github.com/sismics/docs Homepages: sismics.com and teedy.io Access to the H2 Database External Database access for H2 can be granted on different ways. Teedy stores its H2 database in standard path /var/docs/db Variant 1: Download H2 Database Tool Download → http://www.h2database.com/h2-2019-03-13.zip Put your database copy/backup files (docs.mv.db and dovs.trace.db) into some directory where you can access it Use it Run the h2.bat file and connect via Web Interface, or Connect via Console (Windows) cmd cd C:\Users\mario\Downloads\h2-2019-03-13\h2\bin #note: ignore the *.db ending. H2 will add it automatically. If you do it yourself it will fail! java -jar h2-1.4.199.jar -url "jdbc:h2:file:~/Downloads/docs;CACHE_SIZE=65536;LOCK_TIMEOUT=10000;IFEXISTS=TRUE;" -driver "org.h2.Driver" -user "sa" -password "" Connect via Console (Linux) Good tips: https://o7planning.org/en/11895/installing-h2-database-and-using-h2-console cd opt/ wget https://h2database.com/h2-2019-03-13.zip unzip h2-2019-03-13.zip rm h2-2019-03-13.zip cd /opt/h2/bin/ #open Database with h2 driver - enable X11 forward to recieve graphical user interface (GUI) export DISPLAY=localhost:10.0 && java -jar h2-1.4.199.jar -url "jdbc:h2:file:/var/docs.bak/db/docs;CACHE_SIZE=65536;LOCK_TIMEOUT=10000;IFEXISTS=TRUE;" -driver "org.h2.Driver" -user "sa" -password "" #note that version h2-2019-10-14 failed on the Teedy H2 database. So i used older version h2-2019-03-13 Variant 2: DBVisualizer DBVisualizer comes with bundled support for H2 including the driver → https://www.dbvis.com/features/h2-database-features Some example configuration looks like this: Ignore the *.db ending. H2 will add it automatically. If you do it yourself it will fail! So the database "docs" contains of two files and you only need to enter the principal name.     Apache Reverse Proxy and Firewall Install apache2 sudo apt install apache2 Activate modules sudo a2enmod headers rewrite proxy proxy_html proxy_http ssl vhost_alias Apache Reverse Proxy Configuration sudo vim /etc/apache2/sites-available/dms.yourdomain.de_httpd.conf ServerName dms.yourdomain.de RewriteEngine On RewriteCond %{HTTPS} off RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] ServerName dms.YOURDOMAIN.de ServerAdmin info@YOURDOMAIN.de ErrorLog ${APACHE_LOG_DIR}/error-sismics.log CustomLog ${APACHE_LOG_DIR}/access-sismics.log combined SSLEngine on SSLCertificateFile /etc/letsencrypt/live/YOURDOMAIN.de/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/YOURDOMAIN.de/privkey.pem SSLCertificateChainFile /etc/letsencrypt/live/YOURDOMAIN.de/chain.pem SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1 SSLHonorCipherOrder On Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" #Header always set X-Frame-Options DENY Header always set X-Content-Type-Options nosniff #Header set Content-Security-Policy "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';" Header unset X-Powered-By Header set Referrer-Policy "origin-when-cross-origin" Header always edit Set-Cookie (.*) "$1; HttpOnly; Secure" Header set X-XSS-Protection "1; mode=block" Header always set Content-Security-Policy "upgrade-insecure-requests;" #upgrade unsafe gravatar icons to load from https instead of http # Requires Apache >= 2.4 SSLCompression off #SSLUseStapling on #SSLStaplingCache "shmcb:logs/stapling-cache(150000)" # Requires Apache >= 2.4.11 SSLSessionTickets Off ProxyRequests Off # Auth changes in 2.4 - see http://httpd.apache.org/docs/2.4/upgrading.html#run-time Require all granted ProxyPass / http://localhost:8080/dms/ ProxyPassReverse / http://localhost:8080/dms/ SSLRenegBufferSize 100000000 Require all granted AllowOverride None Order deny,allow Deny from All AllowOverride None Allow from All RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)/$ /$1 [R=301,L] Firewall Blocking Rule Block direct access to Jetty9 on Port 8080 (ingoing and outgoing TCP traffic) to allow access only on SSL secured domain. Use iptables or similar. App for Android Sismics Docs is an easy to use, modern and nice DMS (document management system). Sismics Docs can be used on Android. But at the moment there is no downloadable App on Google PlayStore. In the offical git repository you can find the required sources to compile it for yourself. This short tutorial will show you how you can do this. Please feel free to leave comments about additions and corrections. As usual different configurations force different bugs at different users. Variant 1: Build with Android Studio Install Android Studio Run Android Studio Open the project "docs-android" in Android Studio Connect Smartphone by USB in "data transfer" mode Run the build process "Run" → "Run 'app'" - this will install Teedy on Android Finished Variant 2: Build without Android Studio Pull the repository Go to https://github.com/sismics/docs and clone the repository to your local client. Install JDK 11 https://www.oracle.com/technetwork/java/javase/downloads/index.html Install Android SDK Tools The SDK Tools cannot be found directly on android homepage. Instead search them via web: https://filehippo.com/de/download_android_sdk Download the setup and install it to the given standard directory. After that just update/upgrade the components. The installer will ask you if you like to do this. Configure Windows environment variables JAVA_HOME → C:\Program Files\Java\jdk-11 ANDROID_HOME → %userprofile%\AppData\Local\Android\android-sdk PATH → %userprofile%\AppData\Local\Android\android-sdk\tools (add to existing) PATH → %userprofile%\AppData\Local\Android\android-sdk\platform-tools (add to existing) Create local.properties file in the repository Locate the directory \docs-android\ and create a new file named local.properties. Open that file and insert: C:\Users\mario\Git\teedy\docs-android sdk.dir=C:\\Users\\mario\\AppData\\Local\\Android\\android-sdk Start the app building process Open a new cmd shell window and insert the following commands cmd cd C:\Users\mario\Git\teedy\docs-android #gradlew tasks gradlew build #gradlew --stacktrace --debug Copy the built .apk file to your phone The compiled .apk file was generated in \docs-android\app\build\outputs\apk\release. Just put it somewhere on your phone to access it from Android user interface. Sign the .apk file Because the compiled file is not signed you cannot install it on your phone until you do a signing process. I found an application called "apk-signer" in Google PlayStore which did the job for me. After downloading and installing apk-signer, start apk-signer and select the Teedy apk file within the application. By selecting it will be signed. After this start the freshly signed apk file within apk-signer and you are allowed to install it. Enter your configuration and start the app   Automatic Mail Importing (Inbox Scanning) This feature can be configured by environment variables! → Environment Configuration Backup and restore strategies Simple: Make backup of server app and data dir (H2 database) #!/bin/bash BUP_PATH="/backup/teedy/" mkdir -p $BUP_PATH rsync -lrptR /var/docs $BUP_PATH rsync -lrptR /opt/jetty-home-11.0.15/jetty-base/webapps $BUP_PATH cd $BUP_PATH FILENAME=$(date +%Y-%m-%d)-teedy.tar.gz tar -zcvf $FILENAME $BUP_PATH/var/ chown TARGETUSER:TARGETUSER $FILENAME Simple: Restore backup data dir (H2 database) systemctl stop jetty11.service cd /var; rm -rf docs mkdir -p /var/docs cp /backup/teedy/2018-12-30-teedy.tar.gz /var/docs cd /var/docs tar -xvzf 2018-12-30-teedy.tar.gz chmod 777 /var/docs systemctl start jetty11.service Simple: Backup PostgreSQL (not if you are using H2) sudo -iu postgres bash -c "pg_dump YOURDATABASE > YOURDATABASE.sql" && mv /var/lib/postgresql/YOURDATABASE.sql "$BUP_POSTGRES"/YOURDATABASE.sql Simple: Restore PostgreSQL dump The database you want to restore must exis in your psql instancet! If you move your DB from one server to another you need to recreate an empty DB first. See Teedy with PostgreSQL. #move the database dump file to a location where postgres user can read it, for example /var/lib/postgres/ chown postgres /var/lib/postgresql/teedy_db.sql su - postgres #drop old database and create a new one psql drop database teedy_inventory_db; CREATE DATABASE teedy_db WITH ENCODING 'UNICODE' LC_COLLATE 'C' LC_CTYPE 'C' TEMPLATE template0; GRANT ALL PRIVILEGES ON DATABASE teedy_inventory_db TO teedy_inventory; #now import the backup db dump psql teedy_inventory_db < teedy_inventory_db.sql API Use: Bash script for API export of all documents and tags with cURL #!/bin/bash BASE_URL="https://dms.yourdomain.de" AUTH_TOKEN=$(curl -i -X POST -d username="THEUSERNAME" -d password="THEPASSWORD" "$BASE_URL/api/user/login" -k|grep "auth_token"|cut -c24-59) BACKUP_DIR="/backup/teedy" TARGET_JSON_FILE=$BACKUP_DIR"/documentlist.json" TARGET_JSON_FILE_PARSED=$BACKUP_DIR"/documentlist.txt" TARGET_JSON_FILE_SORTED=$BACKUP_DIR"/documentlist.sorted.txt" DIR_ZIP=$BACKUP_DIR/"ZIP" DIR_PDF=$BACKUP_DIR/"PDF" #create backup directory mkdir -p "$BACKUP_DIR" mkdir -p "$DIR_ZIP" mkdir -p "$DIR_PDF" #get the documents list #curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document/list?limit=0" -k | jq . > "$TARGET_JSON_FILE" curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document/list" -d "limit=999999" -k | jq . > "$TARGET_JSON_FILE" #get the tags list curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/tag/list?limit=0" -k | jq . > "$BACKUP_DIR"/taglist.json #read the complete list of documents jq -c '.|{documents}|.[]|.[]|{id}+{title}+{create_date}+{tags}' "$TARGET_JSON_FILE" > "$TARGET_JSON_FILE_PARSED" #make sorted list which shows number of duplicates jq -c '.|{documents}|.[]|.[]|{title}' "$TARGET_JSON_FILE" | sort | uniq -c | sort > "$TARGET_JSON_FILE_SORTED" COUNT=0 TOTAL=$(jq -r '.total' $TARGET_JSON_FILE) jq -c '.|{documents}|.[]|.[]|{id}+{title}+{create_date}' "$TARGET_JSON_FILE" | while read -r i; do COUNT=$((COUNT + 1)) #parse the line and get parameters from the line DOC_ID=$(jq -c '.|{id}|.id' <<< $(printf '%s\n' "$i")) DOC_NAME=$(jq -c '.|{title}|.title' <<< $(printf '%s\n' "$i")) DOC_DATE=$(jq -c '.|{create_date}|.create_date' <<< $(printf '%s\n' "$i")) #EXPORT_FILE_NAME=$(date -d@${DOC_DATE:0:-3} +%Y-%m-%d)_${DOC_ID:1:-1}_${DOC_NAME:1:-1}.zip EXPORT_FILE_NAME=$(date -d@${DOC_DATE:0:-3} +%Y-%m-%d)_${DOC_ID:1:-1} echo $COUNT OF $TOTAL = $EXPORT_FILE_NAME ____ ${DOC_NAME:1:-1} #Export ZIP curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL"/api/file/zip?id="${DOC_ID:1:-1}" -k -o "$DIR_ZIP"/"$EXPORT_FILE_NAME".zip #Export PDF curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL"/api/document/"${DOC_ID:1:-1}"/pdf?margin=10\&metadata=false\&comments=true\&fitimagetopage=true -k -o "$DIR_PDF"/"$EXPORT_FILE_NAME".pdf done #logout if finished curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/user/logout" -k API Use: Flat Hirarchy Export + File List Overview #!/bin/bash BASE_URL="https://dms.yourdomain.de" AUTH_TOKEN=$(curl -i -X POST -d username="THEUSER" -d password="THEPASSWORD" "$BASE_URL/api/user/login" -k|grep "auth_token"|cut -c24-59) BACKUP_DIR="/backup/teedy" TARGET_DOCLIST_JSON=$BACKUP_DIR"/documentlist_forfiles.json" TARGET_FILELIST_JSON=$BACKUP_DIR"/filelist.json" mkdir -p "$BACKUP_DIR" rm $TARGET_DOCLIST_JSON rm $TARGET_FILELIST_JSON echo "Retrieving document list" #curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document/list?limit=0" -k | jq . > "$TARGET_DOCLIST_JSON" curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/document/list" -d "limit=999999" -k | jq . > "$TARGET_JSON_FILE" echo "Retrieving file list based on document list" COUNT=0 jq -c '.|{documents}|.[]|.[]|{id}+{title}+{create_date}' "$TARGET_DOCLIST_JSON" | while read -r i; do COUNT=$((COUNT + 1)) DOC_ID=$(jq -c '.|{id}|.id' <<< $(printf '%s\n' $i)) DOC_ID=${DOC_ID:1:-1} curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/list?id=$DOC_ID" >> "$TARGET_FILELIST_JSON" #echo -e "\n" >> "TARGET_JSON_FILE" echo Getting $COUNT : $DOC_ID done echo "Dumping files into flat hirarchy" mkdir "$BACKUP_DIR"/flat_hirarchy/ COUNT=0 jq -c '.[]|.[]|{create_date}+{name}+{id}+{document_id}+{mimetype}' "$TARGET_FILELIST_JSON" | while read -r i; do COUNT=$((COUNT + 1)) DOC_ID=$(jq -c '.|{document_id}|.document_id' <<< $(printf '%s\n' "$i")) DOC_ID=${DOC_ID:1:-1} FILE_NAME=$(jq -c '.|{name}|.name' <<< $(printf '%s\n' "$i")) FILE_NAME=${FILE_NAME:1:-1} FILE_DATE=$(jq -c '.|{create_date}|.create_date' <<< $(printf '%s\n' "$i")) FILE_ID=$(jq -c '.|{id}|.id' <<< $(printf '%s\n' "$i")) FILE_ID=${FILE_ID:1:-1} #MIMETYPE=$(jq -c '.|{mimetype}|.mimetype' <<< $(printf '%s\n' "$i")|sed 's#/#_#g') #MIMETYPE=${MIMETYPE:1:-1} #FILE_TYPE=$(echo "$FILE_NAME"|awk -F. '{print $(NF)}') #FILE_TYPE=${FILE_TYPE:0:-1} #EXPORT_FILE_NAME=$(date -d@${FILE_DATE:0:-3} +%Y-%m-%d)_"$FILE_ID"."$FILE_ID"."$MIMETYPE"."$FILETYPE" EXPORT_FILE_NAME=$(date -d@${FILE_DATE:0:-3} +%Y-%m-%d)_"$FILE_ID"."$FILE_ID"."$FILE_NAME" echo Getting $COUNT : $EXPORT_FILE_NAME curl --silent -X GET -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/file/$FILE_ID/data" -o "$BACKUP_DIR"/flat_hirarchy/"$EXPORT_FILE_NAME" done #logout if finished curl --silent -X POST -H "Cookie: auth_token=$AUTH_TOKEN" "$BASE_URL/api/user/logout" -k Basic installation with Jetty and H2 Database Install Required Software #install a lot of stuff sudo apt update sudo apt install tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng libtesseract-dev ffmpeg mediainfo mediainfo-gui openjdk-11-jdk #install Jetty Web Server sudo apt install jetty11 #check versions ffmpeg -version tesseract -v mediainfo --version dpkg -l | grep jetty11 dpkg -l | grep jdk You can also install jetty manually (not by apt) with ease and full control: cd /opt wget https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/11.0.15/jetty-home-11.0.15.tar.gz tar -xvzf jetty-home-11.0.15.tar.gz mkdir -p /opt/jetty-home-11.0.15/jetty-base/ cd /opt/jetty-home-11.0.15/jetty-base/ java -jar ../start.jar --add-modules=deploy,http cp /opt/teedy/docs-web/target/docs-web-1.*.war /opt/jetty-home-11.0.15/jetty-base/webapps/dms.war #copy the compiled deployment war to target dir chown jetty:adm /opt/jetty-home-11.0.15/jetty-base/webapps/dms.war chown jetty:adm /opt/jetty-home-11.0.15/jetty-base/webapps/dms.xml Create dms.xml configuration files This allows to change the default docs home dir and other things. Have a look at https://github.com/sismics/docs/blob/master/docs.xml vim /opt/jetty-home-11.0.15/jetty-base/webapps/dms.xml /dms /webapps/dms.war docs.home /var/docs Adjust the following lines according to your configured XML argument  docs.home (if changed) mkdir -p /var/docs/ chmod -T 770 /var/docs/ chown -R jetty:jetty /var/docs/ Take the pre-built  dms.war file or compile on your own and put it to /opt/jetty-home-11.0.15/jetty-base/webapps/dms.war Tuning Raise the heap space Xmx to prevent "java heap space error" - this often occures when OCR'ing a lot of files or uploading multiple files at once. This causes to restart jetty9 completely sudo vim /lib/systemd/system/jetty11.service Environment="JAVA_OPTS=-Xms1024m -Xmx3584m -Djava.awt.headless=true" See also Environment Configuration for reference JDK/JRE - Permissions Policy Adjustments (optional) that might be dangerous / unsecure sudo vim /usr/lib/jvm/java-11-openjdk-amd64/lib/security/default.policy Add to top of file: grant { permission java.security.AllPermission "", ""; }; Fix jetty read-only filesystem (since Jetty 9.4.15) Sympton → "Caused by: java.io.FileNotFoundException: /var/docs/db/docs.trace.db (file system is readonly - but it is not!)" The newest jetty package was changed to contain restricted settings in /lib/systemd/system/jetty9.service. You need to add another ReadWritePath for the /var/docs directory sudo vim /lib/systemd/system/jetty11.service ProtectSystem=strict ReadWritePaths=/var/lib/jetty11 ReadWritePaths=/var/docs/ Restart Jetty Service sudo systemctl restart jetty11.service Check the logs sudo less /var/log/jetty11/ sudo journalctl -f -u jetty11 Access to Teedy Web Interface https://YOURHOST:8080 Build web application server from Source This short tutorial shows how to build Teedy. Finally you will get a ready-to-deploy dms.war java archive file. Pre-Requisites sudo add-apt-repository ppa:openjdk-r/ppa sudo apt update sudo apt install openjdk-11-jdk maven npm jetty11 sudo ln -s /usr/bin/nodejs /usr/bin/node npm install -g grunt-cli npm install -g npm@latest Building  Additional steps are included in the following bash script. Adjust code for GDPR (data privacy → add some custom URLs to satisfy german TMG) adjusting hibernate pool size to allow more connections some CSS fix for bigger preview images #!/bin/bash BASE="/opt/teedy" #altes Repo löschen #rm -rf /opt/teedy/ #clone git clone https://github.com/sismics/docs.git ${BASE} cd ${BASE}/ #to update existing repo: git stash git pull #GDPR adjustments sed -i 's/
  • v{{ app.current_version }}<\/li>/
  • Impressum<\/a><\/li>
  • Datenschutz<\/a><\/li>
  • v{{ app.current_version }}<\/li>/g' ${BASE}/docs-web/src/main/webapp/src/share.html sed -i 's/
  • v{{ app.current_version }}<\/li>/
  • Impressum<\/a><\/li>
  • Datenschutz<\/a><\/li>
  • v{{ app.current_version }}<\/li>/g' ${BASE}/docs-web/src/main/webapp/src/index.html #CSS fix sed -i 's/42px/100px/g' ${BASE}/docs-web/src/main/webapp/src/style/main.less #Adjust hibernate.pool_size from 10 to 50 sed -i 's/\"hibernate.connection.pool_size\", \"10\"/\"hibernate.connection.pool_size\", \"50\"/g' ${BASE}/docs-core/src/main/java/com/sismics/util/jpa/EMF.java #Building cd ${BASE}/ mvn clean -DskipTests install cd ${BASE}/docs-web mvn -Pprod -DskipTests clean install #Apply JETTY_WEBAPPS_DIR="/opt/jetty-home-11.0.15/jetty-base/webapps" service jetty11 stop mv ${JETTY_WEBAPPS_DIR}/dms.war ${JETTY_WEBAPPS_DIR}/dms.war.bak cp /opt/teedy/docs-web/target/docs-web-1.*-SNAPSHOT.war ${JETTY_WEBAPPS_DIR}/dms.war chown jetty:adm ${JETTY_WEBAPPS_DIR}/dms.war systemctl restart jetty11 journalctl -f -u jetty11.service Downloads Teedy consists of Java Server Application for Windows, MacOS or Linux Server and additionally: Android App Document Importer for Windows/Linux/MacOS There's also a Grafana Dashboard for more deep analysis available. See Grafana Monitoring / Statistics You can make your own executable with source code. See Build web application server from Source Environment Configuration We use Fedora 38 Workstation, but the following steps should be similar on other systems. We also use a timer to delay the startup of jetty11 at bootup, because it sometimes struggles to perform well at the first start. vim /lib/systemd/system/jetty11.timer [Unit] Description=Timer for jetty11 Startup Delay [Timer] OnBootSec=1min [Install] WantedBy=timers.target vim /lib/systemd/system/jetty11.service [Unit] Description=Jetty 11 Web Application Server Documentation=https://www.eclipse.org/jetty/documentation/current/ After = syslog.target network.target [Service] # Configuration Environment="JETTY_HOME=/opt/jetty-home-11.0.15" Environment="JETTY_BASE=/opt/jetty-home-11.0.15/jetty-base" Environment="JETTY_USER=jetty" Environment="JETTY_HOST=127.0.0.1" Environment="JETTY_ARGS=jetty.port=8080" Environment="JETTY_STATE=/var/lib/jetty11/jetty.state" Environment="JAVA_OPTS=-Xms1024m -Xmx3584m -Djava.awt.headless=true" #Configure Jetty Service to use database connection instead of H2 local DB Environment="DATABASE_URL=jdbc:postgresql://127.0.0.1:5432/teedy_db" Environment="DATABASE_USER=teedy" Environment="DATABASE_PASSWORD=password" #set base url for password reset Environment="DOCS_BASE_URL=https://your.domain.tld" #Configure tesseract performance Environment="OMP_THREAD_LIMIT=1" # Lifecycle Type=forking ExecStart = /opt/jetty-home-11.0.15/bin/jetty.sh start ExecStop = /opt/jetty-home-11.0.15/bin/jetty.sh stop ExecReload = /opt/jetty-home-11.0.15/bin/jetty.sh restart # Logging SyslogIdentifier=jetty11 # Security User=jetty Group=jetty PrivateTmp=yes AmbientCapabilities=CAP_NET_BIND_SERVICE NoNewPrivileges=true WorkingDirectory=/usr/share/jetty11/ LogsDirectory=jetty11 LogsDirectoryMode=750 ProtectSystem= ReadWritePaths=/var/lib/jetty9/ ReadWritePaths=/mnt/data/sismics/ [Install] WantedBy=multi-user.target More environment vars can be found at https://github.com/sismics/docs/blob/dd36e08d7d6cd8248f12a9570694b4631be3b04d/README.md#available-environment-variables systemctl daemon-reload systemctl enable jetty11.timer #we do not enable jetty11.service too, because that service is just fully controlled by our timer systemctl start jetty11.service systemctl status jetty11.service Information about Teedy file structure Directory Notes /var/docs/theme Background image stored here /var/docs/storage place for alle the files and their automatically generated thumbnails (*_thumb) and web previews. Note that alle the files inside this dir are encrypted and got hashed file names. You can access the file content only inside a running Teedy instance which decrypts the files for you /var/docs/log log files. Same output which you get from journalctl -u jetty9.service (if you use systemd) /var/docs/db H2 database files. Empty but existing if PostgreSQL is used /var/docs Root Directory Known Limitations General issues can be found in https://github.com/sismics/docs/issues If you delete a user, all his assigned documents get deleted without asking. The proper way to omit data loss, just deactivate the user and properly assign permissions to all tags, so other people can continue working with these documents. standard H2 database gets really slow when having thousands of documents or documents with thousands of files inside → Migration from H2 to PostgreSQL helps! Really, PostgreSQL is the only useful way to be productive with Teedy! Migration from H2 to PostgreSQL Note that the migration will make you loosing all the OCR extracted text content from files in your documents. If you want to have those OCR strings back you will need to run a re-indexing for all of them. Stop Teedy and make backup of recent H2 file structure service jetty9 stop cd /var/ cp -R docs/ docs.bak/ chown -R jetty:jetty docs.bak/ chmod 777 docs.bak/ Create some test instance of Teedy which is setup with PostgreSQL Please see Teedy with PostgreSQL to get some points on how to do this. Dumping scheme data from (filled) existing Teedy PostgreSQL instance to get a starting point for migration You will need to configure some temporary instance of Sismics to let it create the desired structure for you. This step is only required if you don't want to use the provided SQL statements in this documentation. Do that step if you want to do it for newer releases that will come up. su - postgres #from root user pg_dump --schema-only teedy_db > teedy_db.sql #that dump gets splitted in step1 and step2 later on Wipe away the psql test instance database and create a fresh/empty one again (for production use) psql drop database teedy_db; CREATE DATABASE teedy_db WITH ENCODING 'UNICODE' LC_COLLATE 'C' LC_CTYPE 'C' TEMPLATE template0; GRANT ALL PRIVILEGES ON DATABASE teedy_db TO teedy; \q /*step1.sql*/ SET statement_timeout = 0; SET lock_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); SET check_function_bodies = false; SET xmloption = content; SET client_min_messages = warning; SET row_security = off;CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; SET default_tablespace = '';SET default_with_oids = false;CREATE TABLE public.t_acl ( acl_id_c character varying(36) NOT NULL, acl_perm_c character varying(30) NOT NULL, acl_sourceid_c character varying(36) NOT NULL, acl_targetid_c character varying(36) NOT NULL, acl_deletedate_d timestamp without time zone, acl_type_c character varying(30) DEFAULT 'USER'::character varying NOT NULL ); ALTER TABLE public.t_acl OWNER TO teedy;CREATE TABLE public.t_audit_log ( log_id_c character varying(36) NOT NULL, log_identity_c character varying(36) NOT NULL, log_classentity_c character varying(50) NOT NULL, log_type_c character varying(50) NOT NULL, log_message_c character varying(1000), log_createdate_d timestamp without time zone, log_iduser_c character varying(36) DEFAULT 'admin'::character varying NOT NULL ); ALTER TABLE public.t_audit_log OWNER TO teedy;CREATE TABLE public.t_authentication_token ( aut_id_c character varying(36) NOT NULL, aut_iduser_c character varying(36) NOT NULL, aut_longlasted_b boolean NOT NULL, aut_creationdate_d timestamp without time zone NOT NULL, aut_lastconnectiondate_d timestamp without time zone, aut_ip_c character varying(45), aut_ua_c character varying(1000) ); ALTER TABLE public.t_authentication_token OWNER TO teedy;CREATE TABLE public.t_base_function ( baf_id_c character varying(20) NOT NULL ); ALTER TABLE public.t_base_function OWNER TO teedy;CREATE TABLE public.t_comment ( com_id_c character varying(36) NOT NULL, com_iddoc_c character varying(36) NOT NULL, com_iduser_c character varying(36) NOT NULL, com_content_c character varying(4000) NOT NULL, com_createdate_d timestamp without time zone, com_deletedate_d timestamp without time zone ); ALTER TABLE public.t_comment OWNER TO teedy;CREATE TABLE public.t_config ( cfg_id_c character varying(50) NOT NULL, cfg_value_c character varying(250) NOT NULL ); ALTER TABLE public.t_config OWNER TO teedy;CREATE TABLE public.t_contributor ( ctr_id_c character varying(36) NOT NULL, ctr_iduser_c character varying(36) NOT NULL, ctr_iddoc_c character varying(36) NOT NULL ); ALTER TABLE public.t_contributor OWNER TO teedy;CREATE TABLE public.t_document ( doc_id_c character varying(36) NOT NULL, doc_iduser_c character varying(36) NOT NULL, doc_title_c character varying(100) NOT NULL, doc_description_c character varying(4000), doc_createdate_d timestamp without time zone, doc_deletedate_d timestamp without time zone, doc_language_c character varying(7) DEFAULT 'eng'::character varying NOT NULL, doc_subject_c character varying(500), doc_identifier_c character varying(500), doc_publisher_c character varying(500), doc_format_c character varying(500), doc_source_c character varying(500), doc_type_c character varying(500), doc_coverage_c character varying(500), doc_rights_c character varying(500), doc_updatedate_d timestamp without time zone NOT NULL, doc_idfile_c character varying(36) ); ALTER TABLE public.t_document OWNER TO teedy;CREATE TABLE public.t_document_metadata ( dme_id_c character varying(36) NOT NULL, dme_iddocument_c character varying(36) NOT NULL, dme_idmetadata_c character varying(36) NOT NULL, dme_value_c character varying(4000) ); ALTER TABLE public.t_document_metadata OWNER TO teedy;CREATE TABLE public.t_document_tag ( dot_id_c character varying(36) NOT NULL, dot_iddocument_c character varying(36) NOT NULL, dot_idtag_c character varying(36) NOT NULL, dot_deletedate_d timestamp without time zone ); ALTER TABLE public.t_document_tag OWNER TO teedy;CREATE TABLE public.t_file ( fil_id_c character varying(36) NOT NULL, fil_iddoc_c character varying(36), fil_iduser_c character varying(36) NOT NULL, fil_mimetype_c character varying(100) NOT NULL, fil_createdate_d timestamp without time zone, fil_deletedate_d timestamp without time zone, fil_order_n integer, fil_content_c text, fil_name_c character varying(200), fil_version_n integer DEFAULT 0 NOT NULL, fil_latestversion_b boolean DEFAULT true NOT NULL, fil_idversion_c character varying(36) ); ALTER TABLE public.t_file OWNER TO teedy;CREATE TABLE public.t_group ( grp_id_c character varying(36) NOT NULL, grp_idparent_c character varying(36), grp_name_c character varying(50) NOT NULL, grp_idrole_c character varying(36), grp_deletedate_d timestamp without time zone ); ALTER TABLE public.t_group OWNER TO teedy;CREATE TABLE public.t_metadata ( met_id_c character varying(36) NOT NULL, met_name_c character varying(50) NOT NULL, met_type_c character varying(20) NOT NULL, met_deletedate_d timestamp without time zone ); ALTER TABLE public.t_metadata OWNER TO teedy;CREATE TABLE public.t_password_recovery ( pwr_id_c character varying(36) NOT NULL, pwr_username_c character varying(50) NOT NULL, pwr_createdate_d timestamp without time zone, pwr_deletedate_d timestamp without time zone ); ALTER TABLE public.t_password_recovery OWNER TO teedy;CREATE TABLE public.t_relation ( rel_id_c character varying(36) NOT NULL, rel_iddocfrom_c character varying(36) NOT NULL, rel_iddocto_c character varying(36) NOT NULL, rel_deletedate_d timestamp without time zone ); ALTER TABLE public.t_relation OWNER TO teedy;CREATE TABLE public.t_role ( rol_id_c character varying(36) NOT NULL, rol_name_c character varying(36) NOT NULL, rol_createdate_d timestamp without time zone NOT NULL, rol_deletedate_d timestamp without time zone ); ALTER TABLE public.t_role OWNER TO teedy;CREATE TABLE public.t_role_base_function ( rbf_id_c character varying(36) NOT NULL, rbf_idrole_c character varying(36) NOT NULL, rbf_idbasefunction_c character varying(20) NOT NULL, rbf_createdate_d timestamp without time zone NOT NULL, rbf_deletedate_d timestamp without time zone ); ALTER TABLE public.t_role_base_function OWNER TO teedy;CREATE TABLE public.t_route ( rte_id_c character varying(36) NOT NULL, rte_iddocument_c character varying(36) NOT NULL, rte_name_c character varying(50) NOT NULL, rte_createdate_d timestamp without time zone NOT NULL, rte_deletedate_d timestamp without time zone ); ALTER TABLE public.t_route OWNER TO teedy;CREATE TABLE public.t_route_model ( rtm_id_c character varying(36) NOT NULL, rtm_name_c character varying(50) NOT NULL, rtm_steps_c character varying(5000) NOT NULL, rtm_createdate_d timestamp without time zone NOT NULL, rtm_deletedate_d timestamp without time zone ); ALTER TABLE public.t_route_model OWNER TO teedy;CREATE TABLE public.t_route_step ( rtp_id_c character varying(36) NOT NULL, rtp_idroute_c character varying(36) NOT NULL, rtp_name_c character varying(200) NOT NULL, rtp_type_c character varying(50) NOT NULL, rtp_transition_c character varying(50), rtp_comment_c character varying(500), rtp_idtarget_c character varying(36) NOT NULL, rtp_idvalidatoruser_c character varying(36), rtp_order_n integer NOT NULL, rtp_createdate_d timestamp without time zone NOT NULL, rtp_enddate_d timestamp without time zone, rtp_deletedate_d timestamp without time zone, rtp_transitions_c character varying(2000) ); ALTER TABLE public.t_route_step OWNER TO teedy;CREATE TABLE public.t_share ( sha_id_c character varying(36) NOT NULL, sha_name_c character varying(36), sha_createdate_d timestamp without time zone, sha_deletedate_d timestamp without time zone ); ALTER TABLE public.t_share OWNER TO teedy;CREATE TABLE public.t_tag ( tag_id_c character varying(36) NOT NULL, tag_iduser_c character varying(36) NOT NULL, tag_name_c character varying(36) NOT NULL, tag_createdate_d timestamp without time zone, tag_deletedate_d timestamp without time zone, tag_color_c character varying(7) DEFAULT '#3a87ad'::character varying NOT NULL, tag_idparent_c character varying(36) ); ALTER TABLE public.t_tag OWNER TO teedy;CREATE TABLE public.t_user ( use_id_c character varying(36) NOT NULL, use_idrole_c character varying(36) NOT NULL, use_username_c character varying(50) NOT NULL, use_password_c character varying(60) NOT NULL, use_email_c character varying(100) NOT NULL, use_createdate_d timestamp without time zone NOT NULL, use_deletedate_d timestamp without time zone, use_privatekey_c character varying(100) DEFAULT ''::character varying NOT NULL, use_storagequota_n bigint DEFAULT '10000000000'::bigint NOT NULL, use_storagecurrent_n bigint DEFAULT 0 NOT NULL, use_totpkey_c character varying(100), use_disabledate_d timestamp without time zone, use_onboarding_b boolean DEFAULT true NOT NULL ); ALTER TABLE public.t_user OWNER TO teedy;CREATE TABLE public.t_user_group ( ugp_id_c character varying(36) NOT NULL, ugp_iduser_c character varying(36) NOT NULL, ugp_idgroup_c character varying(36) NOT NULL, ugp_deletedate_d timestamp without time zone ); ALTER TABLE public.t_user_group OWNER TO teedy;CREATE TABLE public.t_vocabulary ( voc_id_c character varying(36) NOT NULL, voc_name_c character varying(50) NOT NULL, voc_value_c character varying(500) NOT NULL, voc_order_n integer NOT NULL ); ALTER TABLE public.t_vocabulary OWNER TO teedy;CREATE TABLE public.t_webhook ( whk_id_c character varying(36) NOT NULL, whk_event_c character varying(50) NOT NULL, whk_url_c character varying(1024) NOT NULL, whk_createdate_d timestamp without time zone NOT NULL, whk_deletedate_d timestamp without time zone ); psql -d teedy_db -U teedy -f step1.sql -L teedy_db_dump_step1.log Export data from H2 to CSV Access the H2 database and run the following call statements. See this tutorial on how to open your existing H2 database → Access / view H2 Database /*get an overview of all tables*/ SHOW TABLES; The following call statements were built up from the " SHOW TABLES;" output above. call CSVWRITE ('C:/users/mario/Downloads/h2data/T_ACL.csv', 'SELECT * FROM T_ACL', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_AUDIT_LOG.csv', 'SELECT * FROM T_AUDIT_LOG', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_AUTHENTICATION_TOKEN.csv', 'SELECT * FROM T_AUTHENTICATION_TOKEN', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_BASE_FUNCTION.csv', 'SELECT * FROM T_BASE_FUNCTION', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_COMMENT.csv', 'SELECT * FROM T_COMMENT', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_CONFIG.csv', 'SELECT * FROM T_CONFIG', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_CONTRIBUTOR.csv', 'SELECT * FROM T_CONTRIBUTOR', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_DOCUMENT.csv', 'SELECT * FROM T_DOCUMENT', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_DOCUMENT_METADATA.csv', 'SELECT * FROM T_DOCUMENT_METADATA', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_DOCUMENT_TAG.csv', 'SELECT * FROM T_DOCUMENT_TAG', 'charset=utf8'); /*the column "fil_content_c" is ignored because it contains a lot of weird plain text which is almost impossible to export to CSV in a clean fashion*/ call CSVWRITE ('C:/users/mario/Downloads/h2data/T_FILE.csv', 'SELECT FIL_ID_C, FIL_IDDOC_C, FIL_IDUSER_C, FIL_MIMETYPE_C, FIL_CREATEDATE_D, FIL_DELETEDATE_D, FIL_ORDER_N,NULL AS FIL_CONTENT_C,FIL_NAME_C, FIL_VERSION_N, FIL_LATESTVERSION_B, FIL_IDVERSION_C FROM T_FILE', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_GROUP.csv', 'SELECT * FROM T_GROUP', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_METADATA.csv', 'SELECT * FROM T_METADATA', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_PASSWORD_RECOVERY.csv', 'SELECT * FROM T_PASSWORD_RECOVERY', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_RELATION.csv', 'SELECT * FROM T_RELATION', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_ROLE.csv', 'SELECT * FROM T_ROLE', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_ROLE_BASE_FUNCTION.csv', 'SELECT * FROM T_ROLE_BASE_FUNCTION', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_ROUTE.csv', 'SELECT * FROM T_ROUTE', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_ROUTE_MODEL.csv', 'SELECT * FROM T_ROUTE_MODEL', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_ROUTE_STEP.csv', 'SELECT * FROM T_ROUTE_STEP', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_SHARE.csv', 'SELECT * FROM T_SHARE', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_TAG.csv', 'SELECT * FROM T_TAG', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_USER.csv', 'SELECT * FROM T_USER', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_USER_GROUP.csv', 'SELECT * FROM T_USER_GROUP', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_VOCABULARY.csv', 'SELECT * FROM T_VOCABULARY', 'charset=utf8'); call CSVWRITE ('C:/users/mario/Downloads/h2data/T_WEBHOOK.csv', 'SELECT * FROM T_WEBHOOK', 'charset=utf8'); Upload csv files to server (by SSH/SFTP) mkdir ~/h2data #put csv files here Move files to postgres directory (to grant access) #as root mv ~/h2data /var/lib/postgresql/ cd /var/lib/postgresql/ chown -R postgres:postgres h2data/ SQL Import Copying The folloing copy statements were built up bases on the "call CSVWRITE" statements in the H2 export. psql \c teedy_db; #You are now connected to database "teedy_db" as user "postgres". COPY T_ACL FROM '/var/lib/postgresql/h2data/T_ACL.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_CONFIG FROM '/var/lib/postgresql/h2data/T_CONFIG.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_GROUP FROM '/var/lib/postgresql/h2data/T_GROUP.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_ROLE FROM '/var/lib/postgresql/h2data/T_ROLE.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_USER FROM '/var/lib/postgresql/h2data/T_USER.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_CONTRIBUTOR FROM '/var/lib/postgresql/h2data/T_CONTRIBUTOR.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_METADATA FROM '/var/lib/postgresql/h2data/T_METADATA.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_WEBHOOK FROM '/var/lib/postgresql/h2data/T_WEBHOOK.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_VOCABULARY FROM '/var/lib/postgresql/h2data/T_VOCABULARY.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_BASE_FUNCTION FROM '/var/lib/postgresql/h2data/T_BASE_FUNCTION.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_AUDIT_LOG FROM '/var/lib/postgresql/h2data/T_AUDIT_LOG.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_RELATION FROM '/var/lib/postgresql/h2data/T_RELATION.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_USER_GROUP FROM '/var/lib/postgresql/h2data/T_USER_GROUP.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_ROLE_BASE_FUNCTION FROM '/var/lib/postgresql/h2data/T_ROLE_BASE_FUNCTION.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_ROUTE_MODEL FROM '/var/lib/postgresql/h2data/T_ROUTE_MODEL.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_AUTHENTICATION_TOKEN FROM '/var/lib/postgresql/h2data/T_AUTHENTICATION_TOKEN.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_TAG FROM '/var/lib/postgresql/h2data/T_TAG.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_PASSWORD_RECOVERY FROM '/var/lib/postgresql/h2data/T_PASSWORD_RECOVERY.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_SHARE FROM '/var/lib/postgresql/h2data/T_SHARE.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_COMMENT FROM '/var/lib/postgresql/h2data/T_COMMENT.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_DOCUMENT FROM '/var/lib/postgresql/h2data/T_DOCUMENT.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_DOCUMENT_METADATA FROM '/var/lib/postgresql/h2data/T_DOCUMENT_METADATA.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_DOCUMENT_TAG FROM '/var/lib/postgresql/h2data/T_DOCUMENT_TAG.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_FILE FROM '/var/lib/postgresql/h2data/T_FILE.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_ROUTE FROM '/var/lib/postgresql/h2data/T_ROUTE.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; COPY T_ROUTE_STEP FROM '/var/lib/postgresql/h2data/T_ROUTE_STEP.csv' DELIMITER ',' CSV HEADER ENCODING 'utf-8'; \q SQL Step 2 - Adjust foreign keys and indexes /*step2.sql*/ ALTER TABLE public.t_webhook OWNER TO teedy;ALTER TABLE ONLY public.t_acl ADD CONSTRAINT t_acl_pkey PRIMARY KEY (acl_id_c); ALTER TABLE ONLY public.t_audit_log ADD CONSTRAINT t_audit_log_pkey PRIMARY KEY (log_id_c); ALTER TABLE ONLY public.t_authentication_token ADD CONSTRAINT t_authentication_token_pkey PRIMARY KEY (aut_id_c); ALTER TABLE ONLY public.t_base_function ADD CONSTRAINT t_base_function_pkey PRIMARY KEY (baf_id_c); ALTER TABLE ONLY public.t_comment ADD CONSTRAINT t_comment_pkey PRIMARY KEY (com_id_c); ALTER TABLE ONLY public.t_config ADD CONSTRAINT t_config_pkey PRIMARY KEY (cfg_id_c); ALTER TABLE ONLY public.t_contributor ADD CONSTRAINT t_contributor_pkey PRIMARY KEY (ctr_id_c); ALTER TABLE ONLY public.t_document_metadata ADD CONSTRAINT t_document_metadata_pkey PRIMARY KEY (dme_id_c); ALTER TABLE ONLY public.t_document ADD CONSTRAINT t_document_pkey PRIMARY KEY (doc_id_c); ALTER TABLE ONLY public.t_document_tag ADD CONSTRAINT t_document_tag_pkey PRIMARY KEY (dot_id_c); ALTER TABLE ONLY public.t_file ADD CONSTRAINT t_file_pkey PRIMARY KEY (fil_id_c); ALTER TABLE ONLY public.t_group ADD CONSTRAINT t_group_pkey PRIMARY KEY (grp_id_c); ALTER TABLE ONLY public.t_metadata ADD CONSTRAINT t_metadata_pkey PRIMARY KEY (met_id_c); ALTER TABLE ONLY public.t_password_recovery ADD CONSTRAINT t_password_recovery_pkey PRIMARY KEY (pwr_id_c); ALTER TABLE ONLY public.t_relation ADD CONSTRAINT t_relation_pkey PRIMARY KEY (rel_id_c); ALTER TABLE ONLY public.t_role_base_function ADD CONSTRAINT t_role_base_function_pkey PRIMARY KEY (rbf_id_c); ALTER TABLE ONLY public.t_role ADD CONSTRAINT t_role_pkey PRIMARY KEY (rol_id_c); ALTER TABLE ONLY public.t_route_model ADD CONSTRAINT t_route_model_pkey PRIMARY KEY (rtm_id_c); ALTER TABLE ONLY public.t_route ADD CONSTRAINT t_route_pkey PRIMARY KEY (rte_id_c); ALTER TABLE ONLY public.t_route_step ADD CONSTRAINT t_route_step_pkey PRIMARY KEY (rtp_id_c); ALTER TABLE ONLY public.t_share ADD CONSTRAINT t_share_pkey PRIMARY KEY (sha_id_c); ALTER TABLE ONLY public.t_tag ADD CONSTRAINT t_tag_pkey PRIMARY KEY (tag_id_c); ALTER TABLE ONLY public.t_user_group ADD CONSTRAINT t_user_group_pkey PRIMARY KEY (ugp_id_c); ALTER TABLE ONLY public.t_user ADD CONSTRAINT t_user_pkey PRIMARY KEY (use_id_c); ALTER TABLE ONLY public.t_vocabulary ADD CONSTRAINT t_vocabulary_pkey PRIMARY KEY (voc_id_c); ALTER TABLE ONLY public.t_webhook ADD CONSTRAINT t_webhook_pkey PRIMARY KEY (whk_id_c); CREATE INDEX idx_acl_sourceid_c ON public.t_acl USING btree (acl_sourceid_c); CREATE INDEX idx_acl_targetid_c ON public.t_acl USING btree (acl_targetid_c); CREATE INDEX idx_doc_createdate_d ON public.t_document USING btree (doc_createdate_d); CREATE INDEX idx_doc_language_c ON public.t_document USING btree (doc_language_c); CREATE INDEX idx_doc_title_c ON public.t_document USING btree (doc_title_c); CREATE INDEX idx_dot_composite ON public.t_document_tag USING btree (dot_iddocument_c, dot_idtag_c, dot_deletedate_d); CREATE INDEX idx_log_identity_c ON public.t_audit_log USING btree (log_identity_c); ALTER TABLE ONLY public.t_authentication_token ADD CONSTRAINT fk_aut_iduser_c FOREIGN KEY (aut_iduser_c) REFERENCES public.t_user(use_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_comment ADD CONSTRAINT fk_com_iddoc_c FOREIGN KEY (com_iddoc_c) REFERENCES public.t_document(doc_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_comment ADD CONSTRAINT fk_com_iduser_c FOREIGN KEY (com_iduser_c) REFERENCES public.t_user(use_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_document_metadata ADD CONSTRAINT fk_dme_iddocument_c FOREIGN KEY (dme_iddocument_c) REFERENCES public.t_document(doc_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_document_metadata ADD CONSTRAINT fk_dme_idmetadata_c FOREIGN KEY (dme_idmetadata_c) REFERENCES public.t_metadata(met_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_document ADD CONSTRAINT fk_doc_idfile_c FOREIGN KEY (doc_idfile_c) REFERENCES public.t_file(fil_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_document ADD CONSTRAINT fk_doc_iduser_c FOREIGN KEY (doc_iduser_c) REFERENCES public.t_user(use_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_document_tag ADD CONSTRAINT fk_dot_iddocument_c FOREIGN KEY (dot_iddocument_c) REFERENCES public.t_document(doc_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_document_tag ADD CONSTRAINT fk_dot_idtag_c FOREIGN KEY (dot_idtag_c) REFERENCES public.t_tag(tag_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_file ADD CONSTRAINT fk_fil_iddoc_c FOREIGN KEY (fil_iddoc_c) REFERENCES public.t_document(doc_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_file ADD CONSTRAINT fk_fil_iduser_c FOREIGN KEY (fil_iduser_c) REFERENCES public.t_user(use_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_role_base_function ADD CONSTRAINT fk_rbf_idbasefunction_c FOREIGN KEY (rbf_idbasefunction_c) REFERENCES public.t_base_function(baf_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_role_base_function ADD CONSTRAINT fk_rbf_idrole_c FOREIGN KEY (rbf_idrole_c) REFERENCES public.t_role(rol_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_route ADD CONSTRAINT fk_rte_iddocument_c FOREIGN KEY (rte_iddocument_c) REFERENCES public.t_document(doc_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_route_step ADD CONSTRAINT fk_rtp_idroute_c FOREIGN KEY (rtp_idroute_c) REFERENCES public.t_route(rte_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_route_step ADD CONSTRAINT fk_rtp_idvalidatoruser_c FOREIGN KEY (rtp_idvalidatoruser_c) REFERENCES public.t_user(use_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_tag ADD CONSTRAINT fk_tag_iduser_c FOREIGN KEY (tag_iduser_c) REFERENCES public.t_user(use_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE ONLY public.t_user ADD CONSTRAINT fk_use_idrole_c FOREIGN KEY (use_idrole_c) REFERENCES public.t_role(rol_id_c) ON UPDATE RESTRICT ON DELETE RESTRICT; REVOKE ALL ON SCHEMA public FROM PUBLIC; REVOKE ALL ON SCHEMA public FROM postgres; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO PUBLIC; psql -d teedy_db -U teedy -f step2.sql -L teedy_db_dump_step2.log Configure Jetty Service to use database connection instead of H2 local DB See Teedy with PostgreSQL on how to do this Restart Jetty service jetty9 restart && journalctl -f -u jetty9.service Reindex all files with Tesseract OCR scanning libraries (the OCR data from is H2 is lost) There is no way to click some button for this. It has to be scripted. The provided API deals with it. Please see this page for re-processing files by API → API Scripts / database queries Before executing this please move (or remove) the *_thumb and *_web files away because they will be re-processed too. If you don't do this the following will occur: Fix Preview Bug Multilingual Support Teedy is available in different languages. Please help to make some commits to fix your own language. You can use the following tool to validate your modified json files which contain translatable strings: https://jsonlint.com Language file directories from git project: teedy\docs-web\src\main\webapp\src\locale teedy\docs-core\src\main\resources Security General Tips run Teedy in Intranet use SSL adjust apache2 to hide /api/app information (json output which contains information like used version, user count, etc.) AllowOverride None Order deny,allow Deny from All AllowOverride None Allow from All #rewrite /api/app/ to /api/app and so on. Otherwise api/app will be blocked but api/app/ will not be blocked RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)/$ /$1 [R=301,L] enable 2FA for each user account Change admin user name to something else UPDATE t_user SET use_username_c = 'yournewusername' WHERE use_username_c = 'admin'; On Linux you can use https://wiki.ubuntuusers.de/QtQR to import a QR code image from Desktop in case you just want to genate TOTP tokens with tools like KeePassXC for instance. Just run qtqr and select "Decode from File" to open such QR code. apt install qtqr qtqr #run the app Teedy with PostgreSQL Teedy 1was tested sucessfully with PostgreSQL Version 16 and lower Install and configure PostgreSQL Teedy requires at least PSQL 9.4 (PostgreSQL94Dialect) PostgreSQL 10 and upwards are configured to deliver SSL by standard! You will need to configure it's SSL cert! sudo apt install -y postgresql postgresql-client libpq-dev postgresql-contrib sudo vim /etc/postgresql//main/pg_hba.conf #add local to trust to omit password input. If you change to md5 you will need to enter passwords if you run scripts (e.g. bash) # "local" is for Unix domain socket connections only local all all trust #host all all 0.0.0.0/0 md5 hostssl all all 0.0.0.0/0 md5 sudo vim /etc/postgresql//main/postgresql.conf listen_addresses = '*' # what IP address(es) to listen on; ssl = on ssl_cert_file = '/etc/ssl/yourdomain.de.pem' ssl_key_file = '/etc/ssl/private/yourdomain.de.key' #login as postgres user su - postgres psql CREATE USER teedy WITH PASSWORD 'password'; CREATE DATABASE teedy_db WITH ENCODING 'UNICODE' LC_COLLATE 'C' LC_CTYPE 'C' TEMPLATE template0; GRANT ALL PRIVILEGES ON DATABASE teedy_db TO teedy ; #remove old database if required #service postgresql restart #kick old connections #REVOKE ALL PRIVILEGES ON DATABASE teedy_db FROM teedy; #DROP DATABASE teedy_db; #DROP USER teedy; PostgreSQL SSL cd /etc/letsencrypt/live/yourdomain.de/ cp privkey.pem /etc/ssl/private/yourdomain.de.key (cat privkey.pem; printf "\n\n"; cat cert.pem; printf "\n\n"; cat chain.pem; printf "\n\n") >> /etc/ssl/yourdomain.de.pem cd /etc/ssl/ chgrp ssl-cert /etc/ssl/private/yourdomain.de.key chmod 640 /etc/ssl/private/yourdomain.de.key chgrp ssl-cert /etc/ssl/yourdomain.de.pem chmod 640 /etc/ssl/yourdomain.de.pem less /var/log/postgresql/postgresql-9.5-main.log #check for occuring errors belonging to SSL cert Configure dms.xml (optional) vim /opt/jetty-home-11.0.15/jetty-base/webapps/dms.xml /dms /webapps/dms.war docs.home /var/docs Configuration for usage of PostgreSQL instead H2 Note: The database connection is set via a central environment variable configuration for the entire Jetty service and cannot be set for individual WebAppContext. Have a a look at Environment Configuration on how to swap to PostgreSQL External connection test with Oracle SQL Developer enable ports and configure firewall correctly download SQL Developer driver for PostgreSQL → https://jdbc.postgresql.org/download.html#current (Treiber = postgresql-42.2.6) add the driver in system settings User and database differ from the name. Therefore, the entry must be entered as follows: user: teedy hostname: yourdomain.de:5432/teedy_db? port: stays empty database: teedy_db Vom Etikettenschild zum Objekt 1. Bereitstellungskonzept Objekt-Exportdatei generieren und per Web Server bereit stellen Grundlage für die Etikettierung bildet der Objekt-Export aus der Teedy-Instanz. Der Export erfolgt durch Erzeugung einer CSV-Datei: die CSV-Datei wird alle 10 Minuten automatisch vom Server generiert die CSV-Datei wird über eine public URL vom Web Server bereitgestellt. Der Zugang ist mit Passwort und Nutzername gesichert Der Client kann die CSV-Datei mit bekannten Credentials und dem cURL Tool herunterladen Der Aufbau Document ID Objekt Id Titel Nutzungsberechtigung Werkstattbereich Export-Script / Web Server Konfiguration htpasswd -c /etc/nginx/.htpassword USER mkdir -p /var/www/vhosts/things.fablabchemnitz.de/flc/ vim /etc/nginx/sites-enabled/things.fablabchemnitz.de location /flc/ { root /var/www/vhosts/things.fablabchemnitz.de; autoindex on; autoindex_format json; auth_basic "Administrator Login"; auth_basic_user_file /etc/nginx/.htpassword; } service nginx restart vim /opt/teedy-inventorylist.sh #!/bin/bash DB_USER="user" DB_NAME="db" #OUT=$( psql -t -U$DB_USER $DB_NAME --no-align $'-F,' --command=" /*Warnung. Diese SQL-Abfrage muss mit der Tag-Struktur synchron gehalten werden, da Änderungen nicht automatisch erkannt werden*/ SELECT DISTINCT CONCAT( '\"',D.doc_id_c,'\",', '\"',REPLACE(D.doc_title_c,'\"','\"\"'),'\",', '\"',dme_value_c,'\",', '\"',NB."Nutzungsberechtigung",'\",' '\"',WB."Werkstattbereich",'\"' ) FROM t_document AS D JOIN t_document_tag ON D.doc_id_c = t_document_tag.dot_iddocument_c JOIN t_tag ON t_tag.tag_id_c = t_document_tag.dot_idtag_c FULL JOIN t_document_metadata AS M ON M.dme_iddocument_c = D.doc_id_c FULL JOIN t_metadata AS N ON M.dme_idmetadata_c = N.met_id_c FULL JOIN ( SELECT t_document.doc_id_c, t_tag.tag_name_c as "Nutzungsberechtigung" FROM t_document JOIN t_document_tag ON t_document.doc_id_c = t_document_tag.dot_iddocument_c JOIN t_tag ON t_tag.tag_id_c = t_document_tag.dot_idtag_c WHERE t_tag.tag_name_c IN ( WITH RECURSIVE RES AS (SELECT t_tag.tag_id_c, t_tag.tag_idparent_c, t_tag.tag_name_c FROM t_tag WHERE tag_name_c = 'Nutzungsberechtigung' UNION SELECT e.tag_id_c, e.tag_idparent_c, e.tag_name_c FROM t_tag AS e INNER JOIN RES s ON s.tag_id_c = e.tag_idparent_c ) SELECT tag_name_c FROM RES ) AND --t_tag.tag_name_c in ('1/Allgemeine_Arbeitsschutzeinweisung', '2/Kurzeinweisung', '3/Maschinenführerschein') AND t_document_tag.dot_deletedate_d IS NULL ) AS NB on NB.doc_id_c = D.doc_id_c FULL JOIN ( SELECT t_document.doc_id_c, t_tag.tag_name_c as "Werkstattbereich" FROM t_document JOIN t_document_tag ON t_document.doc_id_c = t_document_tag.dot_iddocument_c JOIN t_tag ON t_tag.tag_id_c = t_document_tag.dot_idtag_c WHERE t_tag.tag_name_c IN ( WITH RECURSIVE RES AS (SELECT t_tag.tag_id_c, t_tag.tag_idparent_c, t_tag.tag_name_c FROM t_tag WHERE tag_name_c = 'Werkstattbereich' UNION SELECT e.tag_id_c, e.tag_idparent_c, e.tag_name_c FROM t_tag AS e INNER JOIN RES s ON s.tag_id_c = e.tag_idparent_c ) SELECT tag_name_c FROM RES ) AND t_document_tag.dot_deletedate_d IS NULL ) AS WB on WB.doc_id_c = D.doc_id_c WHERE D.doc_deletedate_d IS NULL AND N.met_deletedate_d IS NULL AND (N.met_name_c = 'id' OR N.met_name_c IS NULL) ; " > /var/www/vhosts/things.fablabchemnitz.de/flc/inventory.csv vim /etc/cron.d/teedy-inventorylist SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin */10 * * * * root /opt/teedy-inventorylist.sh > /dev/null # Abruf der CSV-Datei curl --user "USER:PASSWORD" https://things.fablabchemnitz.de/flc/inventory.csv Output-Beispiel "00ae8d4e-8a05-4cba-b5e4-fb928168289d","Handlabelgerät P-Touch 1010 von Brother","61","","Büro" 2. Etikettenkonzept InkScape Erweiterung "Inventory Sticker" generiert Aufkleber-Vektordateien Jedes Objekt im FabLab bekommt seinen eigenen Inventaraufkleber mit einer eigenen Nummer (id) als fortlaufenden Integerwert. Wir vermeiden die Verwendung von sog. UUIDs, da diese für unseren Zweck zu lang sind. Selbst "shortened UUIDs" sind noch zu lang, wie beispielsweise "mhvXdrZT4jP5T8vBxuvm75". Wir wollen nur ganz wenige Daten in einem jeweils einzigartigen Etiketten-Barcode speichern, damit dieser auf dimensional kleine Aufkleber passt und trotzdem gut scanbar bleibt. Objekte, die es mehrfach gibt, erhalten auch jeweils eine einzigartige Nummer. Über ein client-seitiges Script können alle gewünschten Inventarobjekte aus einer Grafana .csv-Exportdatei eingelesen werden und daraus die entsprechenden Etiketten generiert werden (und anschließend gedruckt werden). Aufbau / Inhalt Die Etiketten enthalten die folgenden Daten, welche aus einer komma-separierten .csv-Datei stammen ohne Kopfzeile. Der CSV-Aufbau unter 1. Bereitstellungskonzept zu finden. Ein Beispiel: "00ae8d4e-8a05-4cba-b5e4-fb928168289d","Handlabelgerät P-Touch 1010 von Brother","61","","Büro" Kennzeichnung über Zugehörigkeit des Vereins (Konstante) DataMatrix-Code (2D Barcode) mit Smartphones scanbar enthält URL, die direkt zum Artikel im Inventarsystem hinleitet 16x16 DataMatrix für geringsten Platzbedarf (hat im Vergleich zu BeeCode, AztecCode oder QR-Code bessere Datendichte) - eignet sich mit 16x16 für Zahlen in der Größenordnung bis 99999+ QR Codes auf weißem Hintergrund, da die meisten Apps invertierte QR Code Farbschemas leider nicht erkennen Objektnummer (id) Objektname (Titel sind in Teedy auf 100 Zeichen begrenzt) Nutzungsberechtigung Nicht auf dem Inventarkleber zu finden ist der aktuelle Ort, denn dieser kann sich regelmäßig ändern (je nach Werkstattordnung). Die Folge wäre ein ständiges Re-Labeling der Objekte, was für die Gesamtordnung hinderlich ist. Etikettengrößen (Grafikgrößen, Etikettenrollen, Reelle Größen nach Druck und Schnitt) Die Inventaraufkleber (und folglich die Schilder, auf die sie kommen) sind mit Absicht sehr klein gehalten, denn sie sollen auf möglichst viele Objekte passen und keine wichtigen Stellen verdecken. Durch den platzsparenden 16x16 DataMatrix Barcode sind diese erwiesenermaßen aus ca. 25 - 50 cm ohne Probleme mit einer Standard Smarthone-Kamera einscannbar. Folgende Maße sollten beachtet werden: → Exportdatei (SVG/PNG): das von uns verwendete Aufkleberformat beträgt 696x308 Pixel. Das ergibt umgerechnet eine Etikettengröße von 62 x 27,4 mm → wir nutzen 62 mm breite Endlosetiketten von DK22205 von Brother. → Die eigentlichen Etiketten, die man vom Träger abzieht, sind 62 mm breit, aber das Trägerformat selbst ist 66 mm breit → Da der Etikettendrucker jeweils ein paar Millimeter "Feed" (scrap size) generiert, sind die Etiketten höher, als in der Grafik angegeben, nämlich 32,2 mm (also 32,2 mm - 27,4 mm = 4,8 mm Aufmaß). Das endgültige Maß mit Träger ist also 66 x 32,2 mm bzw. aufgeklebt 62 x 32,2 mm → Die Etikettenmotive sind auf dem Etikett geringfügig kleiner gedruckt, da links und rechts noch ein Rand vom Drucker eingefügt wird (ca. 1,6 mm Rand links und ca. 1,0 mm Rand rechts) Die InkScape-Erweiterung nutzt folgende Schritte zur Etikettengenerierung Objekt-Exportdatei (.csv) einlesen SVG-Vektorgrafik auf Templatebasis generieren (InkScape) → eigenes PlugIn (generator-cli  + Barcode generator Hybrid). Siehe Gitea. Für jedes Objekt wird eine eigene Datei auf dem Rechner abgelegt. Das Dateinamenschema ist _.SVG die SVG als PNG exportieren die PNG über das Tool "brother_ql" an den Etikettendrucker QL-720NW von Brother senden und ausdrucken (optional). Siehe 3. Druckerkonzept User Interface InkScape Extension "Inventory Sticker" Siehe auch Inventory Sticker Achtung Wird während des Nutzens der Erweiterung im Backend der Titel des Things geändert, dann muss ein paar Minuten gewartet werden, bis der automatisch laufende Cronjob eine neue inventory.csv Datei generiert hat! Andernfalls drucken wir alte Etiketten. Beispiel Output Die Titellänge kann maximal 100 Zeichen betragen. Entsprechend wurden die Aufkleber so designed, dass sie dies unterstützen. Um die Platzausbeute zu maximieren wurden Tricks in den Quellcode eingebaut, damit  der Text nicht Wort für Wort, sondern auch nach beliebigen Buchstaben auf neue Zeilen umgebrochen werden kann. Um das zu erreichen wird zunächst hinter jeden Buchstabe ein gesondertes Leerzeichen eingefügt. Um diese Lücken zu kompensieren benötigen wir die Attribute "xml:space" = "preserved" und das Attribut "letter-spacing". 100 Zeichen im Titel (maximal möglich) 42 Zeichen im Titel (übliche Längen) Ein ausgedrucktes Label (gerade beim Erfassen mit einem Barcode Scanner): 3. Druckerkonzept Client-Script sendet die Aufkleber an einen Etikettendrucker (Modell Brother QL-720NW) Die im Schritt 2. Etikettenkonzept erzeugten Aufkleberdateien werden durch das InkScape-PlugIn auf dem Dateisystem abgelegt. Das folgende Script kann genutzt werden, um diese Aufkleber dann auf einem Brother QL-720NW auszudrucken. Diese Funktionalität ist auch Teil des InkScape Plugins. Jedoch kann es auch separat genutzt werden. Wir nutzen dafür das Betriebssystem Ubuntu 20 LTS, den offiziellen Treiber von Brother und eine spezielle Python-Bibliothek namens "brother-ql". Zunächst muss auf dem Rechner, an dem der Etikettendrucker angeschlossen ist, eine entsprechende Installation der Treiber und des Python-Pakets vorgenommen werden. Brother QL-720NW auf Windows/Linux installieren Die Treiber können unter https://support.brother.com/g/b/downloadtop.aspx?c=de&lang=de&prod=lpql720nweuk heruntergeladen werden Ausdrucken von Labels mit brother_ql Wir nutzen nicht das Standardwerkzeug "lp" bzw. "lpr" zur Druckausgabe, da dies schlecht einstellbar ist. Stattdessen nutzen wir das Python-Paket https://pypi.org/project/brother-ql brother_ql installieren (pip notwendig) cd ~/ sudo apt install python3-pip python -m venv venv ~/venv/bin/pip3 install --upgrade brother_ql Verfügbare Drucker listen ~/venv/bin/brother_ql --backend pyusb discover Für Windows-Systeme benötigen wir zusätzlich libusb → Download libusb-win32-devel-filter-1.2.6.0.exe Normalen Nutzern außer root Zugang zum Drucker erlauben (Linux) Das fixt das Problem "usb.core.USBError: [Errno 13] Access denied (insufficient permissions)": echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="04f9", ATTR{idProduct}=="2044", MODE="666"' > /etc/udev/rules.d/99-garmin.rules && sudo udevadm trigger PNG ausdrucken Größen → 696 px sind 62 mm #Linux brother_ql -m QL-720NW --backend pyusb --printer usb://04f9:2044 print -l 62 --600dpi -r auto /home/tomate/Downloads/InventorySticker/Holzwerkstatt/1_Bandsäge_LB1200F_von_Makita.png #Windows brother_ql -m QL-720NW --backend pyusb --printer usb://04f9:2044 print -l 62 --600dpi -r auto C:\Users\tomate\Desktop\InventorySticker\1_Bandsäge_LB1200F_von_Makita.png Troubleshooting ANTIALIAS Fehler beseitigen Die Bibliothek ist veraltet und muss manuell editiert werden. Dazu bearbeiten wir: sudo vim ~/venv/lib/python3.13/site-packages/brother_ql/conversion.py Siehe https://github.com/pklaus/brother_ql/pull/169/commits/324d5c833e62e63778060aeb5287ec97187de95f 4. Schilderkonzept Etiketten werden auf farblich getrennte und geometrisch geeignete Träger aufgebracht Das Schilderkonzept für die per 3. Druckerkonzept gedruckten Aufkleber dient der übersichtlichen Beschriftung von Werkzeugen und den Markierungen, wo diese ortsgebunden hingehören. Entsprechend unserer räumlichen Trennung wird den Trägermedien für die Aufkleber jeweils eine Farbe zugewiesen - diese soll sofort in den Blick des Betrachters fallen (Infos zur Farbzuweisung siehe Werkstattorientierung im FabLab - Digitales Inventar). Ziel ist es, dass die Aufkleber am jeweiligen Inventargegenstand bei Benutzung nicht stören und nicht so schnell kaputt gehen können. Es soll möglichst auf Kleber auf den Schildern verzichtet werden, weil sich dieser meist schlecht ablösen lässt. Das Befestigen dieser sollte deshalb möglichst durch sicheres Anheften erfolgen - dazu benötigen die Schilder entsprechende geometrische Merkmale wie Löcher, Ösen oder Laschen. Wir drucken diese selbst mit dem 3D-Drucker, denn dadurch sind wir flexibel, was die Farb- und Materialauswahl angeht und können individuelle Anpassungen an Etikettengrößen vornehmen. Standardschlüsselschilder sind deshalb leider nicht hinreichend geeignet. Wir möchten außerdem darauf verzichten die gedruckten Inventaraufkleber aus 2. Etikettenkonzept vom Trägermedium abzuziehen, denn auch die Schilder sollen mehrfach wiederverwendet werden können. Die Etiketten werden auf das entsprechende Schild aufgeschoben und mit einer passend zugeschnittenen Transparentblende (0,5mm PETG Folie) versehen. Die Blende dient zum Schutz des Aufklebers, damit dieser länger haltbar und scanbar bleibt (z.B. typische Probleme durch Kratzer, Schmutz, Nasswerden, etc.) Beispiel-Beschilderung am ehemaligen 3D-Drucker "Stahlschweinmammut" Der Träger mit Schutzfolie 3D Modell siehe hier: https://manyfold.stadtfabrikanten.org/models/nv1rc9cc0ks4 Ausgelasertes Vivak-Schild mit beidseitger Schutzfolie 13x Träger auf einmal ausgedruckt Ideen und Vorschläge zum Aufbringen von Etiketten/Schildträgern Zum Befestigen der Schilder mit Aufklebern bzw. nur der Aufkleber (direktes Aufkleben) gibt es je nach benötigter Situation unzählige Möglichkeiten mit diversen Vor- und Nachteilen. Gut sind vor allem günstige Massenprodukte wie Büroklammern, Maulklammern, Nadeln, Pins, Schrauben, Nägel, Haarklammern, doppelseitiges Klebeband, Klettverschluss, Magneten oder Fäden. Für die folgenden, beispielhaft gedachten Anwendungsfälle eignen sich bestimmte Produkte besser als andere. Unser 3D-Modell vom Schildträger ist so konstruiert, dass verschiedene dieser Dinge daran befestigt werden können. So ist es versatil genug, um auf viele unterschiedliche Varianten zu verzichten. für Dokumente, Magazine und Bücher (auch Handbücher): Schildträger mit Haarklammer oder nur der blanke Aufkleber auf der Innenseite oder Rückseite des Buches für Spinde aus Metall: Schildträger mit Magneten für Kork-Pinnwände: Schildträger mit Steckerchen/Pins für Whiteboards: Schildträger mit Magneten für Regale: Schildträger mit doppelseitigem Klebeband für Flexfolien/PVC-Folien: Schildträger mit Büroklammer, Geldklammer, Haarklammer, Wäscheklammer - ggf. mit Filzen zwischendrin, um die Folien zu schonen für Filamentrollen (3D-Druck): jeweils einzeln im großen Zipperverschlussbeutel oder in großen Stopftabak-Box (1000 Gramm) für Schaumstoff oder tonartige Masse: Schildträger mit aufgebogene Büroklammer für kleine Handmaschinen: Schildträger mit doppelseitigem Klebeband, direktes Aufkleben an sichtbare Stelle, die nicht abgegriffen oder abgenutzt wird für Kleine Handwerkzeuge: Einfärben, Aufkleben und Umwickeln des Aufklebers mit durchsichtigem Klebeband oder Folie (z.N. wenn es sich in Griffnähe befinden muss und angefasst wird) für Kabel: transparenter Schrumpfschlauch mit verschiedenen Schrumpfverhältnissen von 2:1 bis 5:1, Etiketten mit Fähnchen für Objekte, wo alles andere stört: hier entfallen zum Teil die 3D-gedruckten Schildträger, weil sie zu groß sind und stören (zum Beispiel an einem Hammer oder einem Schraubendreher) Befestigungsmöglichkeiten der Schildträger - Beispiele Das von uns verwendete Trägermodell erlaubt die Verwendung von Maulklammern, handelsüblichen Haarspangen, Nadeln, Sicherheitsnadeln, Senkschrauben und Magneten (idealerweise Neodymmagneten mit dem maximalen Maß von Ø8 x 1,0 mm oder kleiner) Etiketten ohne Schildträger Falls kein Platz ist, dann kann das Etikett auch mit einem Farbstift oder einem farbigen Klebepunkt markiert werden. Weitere Tipps für die Werkstattordnung Insbesondere Werkzeugwände sind häufig chaotisch. Optimal sind Lochrasterwände, Nadelwände oder Magnetwände und dazu aufgezeichnete Werkzeugumrisse. Innerhalb oder neben jedem Umriss könnte jeweils ein Schild mit Inventaraufkleber angebracht sein, was für ein Werkzeug hingehört. 5. Scanner-Konzept Schilder (siehe 4. Schilderkonzept) werden auf die Objekte angebracht und sind per Barcode scanbar durch die Nutzer (Scanner-Konzept). Apps Geeignete (auf Funktionaität geprüfte) Barcode-Scanner Software für Android Geräte sind "Barcode Scanner" von ZXing Team (https://github.com/zxing/zxing) (Apache 2.0 License) QR & Barcode Scanner (https://github.com/dmitriy-ilchenko/QrAndBarcodeScanner) (Unlicense) Binary Eye (https://github.com/markusfisch/BinaryEye) (MIT License) Die Apps können so konfiguriert bzw. verwendet werden, dass sie die URLs automatisch nach dem Scannen öffnen. Test mit QR & Barcode Scanner 6. Web Server Redirect Konzept Nutzer scannen die Objekte und werden auf die entsprechende Objektseite im Inventarsystem umgeleitet Der mit einer Barcode Scanner App gescannte Datensatz (siehe 5. Scanner-Konzept) enthält eine URL, die direkt zum Inventarsystem weiterleitet. Dafür hat unser Verein die Domain http://qwa.es akquiriert. Auf dieser Domain lauscht ein simpler Web Server mit nginx-Redirect Konfiguration. So leitet beispielsweise die Eingabe "qwa.es/1" (8 Zeichen) an den Artikel mit der internen Objekt-ID auf folgende URL weiter: https://things.fablabchemnitz.de/#/document/view/62dced06-4883-496a-b6d0-bf33e60b4ef6/content (93 Zeichen). Es ergibt sich der Vorteil, dass der Redirect jederzeit umgebaut werden kann. Falls sich die URL der Inventarplattform verändert, so müssen keine neuen Barcodes generiert werden, somit auch nicht zwangsweise neue Inventaraufkleber bereitgestellt werden. Die interne Dokumentation zum nginx-Web Server Setup ist hier zu finden: qwa.es Iventory Document ID ↔ metaid vhost mapper 7. Werkstattmonitor-Konzept Allgemein Zur Orientierung in der Werkstatt und im Verein allgemein befinden sich in jedem größeren Bereich werkstatttaugliche Touch-Monitore. Wir nutzen das Modell FT156TMBCAPOB von Faytech. Diese sind für Multitouch-Input geeignet und außerdem staub- sowie spritzwassergeschützt. An jedem Monitor befindet sich jeweils ein Raspberry Pi 4 Model B mit PoE-Stromversorgung, da wir PoE-LAN ausbauen und so auf extra Netzteile verzichten können. Auf den Monitoren sollen wichtige Dinge angezeigt werden können, die für Mitglieder relevant sind, sowie allgemeine Infos, die auch Besuchern der offenen Werkstatt angezeigt werden (Besucherleitsystem). Das sind zum Beispiel aktuelle Veranstaltungen im Verein, ein Newsfeed unserer Blogs und sonstige Neuigkeiten aus der Werkstatt, wie neu hinzugekommenes Equipment oder der Stromverbrauch im Rückblick. Für diese Zwecke wurden mehrere Grafana Dashboards entwickelt und den Mitgliedern zur Verfügung gestellt. Die Raspberry Pi's sind so eingerichtet, dass sie beim Hochfahren automatisch einen Browser und die entsprechenden URLs öffnen. Auf diese Weise ist ein schnelles Zurechtfinden möglich und Mitglieder bzw. NutzerInnen können up to date bleiben. Raspberry Pi 4 Model B als Werkstattmonitor-Leitsteuerung Das zentrale Grafana Dashboard als Startseite Inventar-Plattform als Schnellzugriff Hardware-Konzept Für die verwendeten Monitore wurden keine fertigen Wandhalterungen gekauft. Diese wurden stattdessen selbst als integrale Komplettgehäuse aus Sperrholzplatten und 3D-gedruckten Winkeln konzipziert. Dieses Konzept wird nachfolgend vorgestellt und dokumentiert. Ein Werkstattmonitor in der Holzwerkstatt Gedanken Solide Kapselung aller Komponenten (Monitor, Raspberry Pi mit PoE Hat und Gehäuse, HDMI-Kabel (Audio + Video), USB-Kabel vom Touch Device Input) Gehäuseinneres selbst ist nicht 100%ig staubgeschützt, jedoch die wichtigsten Komponenten selbst schon Kabelage (Ständiges An- und Abklemmen der KAbel an Raspberry Pi und Monitor soll nach Möglichkeit vermieden werden) Das Monitor-Netzteil befindet sich außerhalb vom selbst hergestellten Gehäuse. Das Netzteil bleibt dauerhaft am Display angeschlossen Ein Ethernet-Kabel bleibt dauerhaft mit dem Werkstattmonitor verbunden. Die Monitore werden in die Nähe der PoE Ethernet-Dosen installiert. Es gibt Aussparung zum Bedienen des An-/Aus-Knopfes und Hell/Dunkel, nicht jedoch der oberen beiden Buttons (Quellenauswahl / Scan), da diese die Grundkonfiguration des Displays unnötig verstellbar machen. Die Halterung ist so aufgebaut, dass sie einfach an der Wand festgeschraubt werden kann (3D-gedruckte Scharniere mit definierten Abmessungen). Das Gehäuse erlaubt zusammen mit den Wandwinkeln und dem Monitorhebel mehrere angenehme Positionen, wobei der geringste Winkel ca. -5° beträgt, der maximale ca. 55° Das Display ist rückseitig verschraubt und ebenbündig mit der vordersten Platte das Display wird so an die Wand befestigt, dass die hintere Verstellung zur Anpassung des Winkels genutzt werden kann. Die Wandhalterung besteht aus mehreren Sperrholzplatten (10 Stück) die nicht verleimt, sondern verschraubt wurden. Dadurch ist ein nachträgliches Anpassen der einzelnen Bauteilschichten einfacher. Insbesondere wenn eine der unten genannten Modifikationen eingebracht werden soll (z.B. externe USB-Zugang, extra Sensoren, etc.) Anschlüsse extern Stromversorgung Raspberry Pi via USB-C Netzteil oder PoE → die Raspis sollten permanent an bleiben Monitor separat mit Strom versorgt via Kaltgerätestecker → Monitor sollte ggf. manuell via Rückseite ausgeschalten werden und/oder per Schaltsteckdose ggf. zu bestimmten Uhrzeiten automatisch an-/abgeschalten werden Wartung des Monitorsystems: Falls man an die Hardware gelangen möchte, so muss die Rückseite abgeschraubt werden. Dazu müssen alle Schrauben gelockert werden (auch die von den 3D-gedruckten Halterungen) Unsere Werkstattmonitorgehäuse wurden mit Inkscape angefertigt. Das 3D-Modell der Monitore findet sich in der vom Hersteller bereitgestellten 3D PDF Datei. Source Files Siehe https://gitea.fablabchemnitz.de/vmario/flc-werkstattmonitore/src/branch/master/README.md Benötigtes Werkzeug Bandschleifer (zum Schleifen der zusammengeschraubten Platten, die das Gehäuse ergeben) Senker mit Begrenzung (zum individuellen Senken der Scharnierteile, falls Senkschrauben genutzt werden) Torx Bit (Größe T10 für 3 mm Holzschrauben) Akkuschrauber oder Schraubendreher mit Bit-Halterung Cuttermesser Lasercutter (ca. 33 Minuten reine Laserzeit) Sechskantschüssel SW 2,5 mm (zur Befestigung des Monitors an der Halteplatte (Platte 3) Farbroller (zum gleichmäßigen Auftragen des Klarlacks) Etikettiergerät (zum Beschriften von An/Laut/Leise Knöpfen) Stückliste Stückliste Nr Teil Menge Bild Hinweise 1 Sperrholz Platte #1 (Deckplatte mit Logo) 1 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm 2 Sperrholz Platte #2 (Rahmen) 1 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm 3 Sperrholz Platte #3 (Aufnahme für Monitor / Halteplatte) 1 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm Die Platte liegt dauerhaft auf den Buttons Quellenauswahl und Scan des Monitors auf. Falls dies stört kann eine ca. 1 mm hohe Aussparung auf der Rückseite ausgekratzt werden 4 Sperrholz Platte #4 (Zwischenplatte) 4 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm 5 Sperrholz Platte #5 (Zwischenplatte mit Haltenasen) 1 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm 6 Sperrholz Platte #6 (Versteifungsplatte) 1 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm Die Löcher in dieser Platte sind enger als die aller anderen Platten. Sowohl die Schrauben aus Platte 10, als auch die der Platten 1 bis 8 werden in dieser "zentral" verankert. 7 Sperrholz Platte #7 (Bodenplatte) 1 Stück Sperrholzplatte Meranti - minimale Abmessungen: 295 x 435 x 5,6 mm 8 Raspberry Pi 4 Model B 1 Stück 9 Raspberry Pi 4 PoE Hat 1 Stück 10 Raspberry Pi 4 Gehäuse "Argon Neo" 1 Stück 11 Faytech Werkstattmonitor FT156TMBCAPOB 1 Stück 12 SD-Karte 32 GB SDHC 1 Stück 13 Faytech Werkstattmonitor Netzteil 1 Stück 14 Faytech USB-Kabel für Touch Input 1 Stück wird zum Monitor mitgeliefert 15 Faytech Monitor Tischhalterung 1 Stück wird zum Monitor mitgeliefert 16 Faytech Monitor Tischhalterung Befestigungsschrauben 4 Stück wird zum Monitor mitgeliefert 17 HDMI Kabel 1 Stück 18 Ethernet Kabel RJ45 1 Stück 19 Zylinderkopfschraube ISO 4762 A2 - M3 x 10 mm 8 Stück Fixieren das Display im Gehäuse   Warnung: keine längeren Schrauben verwenden! Dies kann das Display beschädigen 20 Unterlegscheibe DIN 125 A  - 3mm 16 Stück Für die Zylinderkopfschrauben (2 Stück je Schraube) - verhindern Durchrutschen der Schraubköpfe 21 Spanplattenschraube Senkkopf Torx T10 A2 3 x 16 mm 19 Stück zur Befestigung der Rückplatte (Platte 10) an Platte 9 - bei Bedarf können u.U. auch längere Schrauben verwendet werden (z.b. 3 x 20 mm) 22 Spanplattenschraube Senkkopf Torx T10 A2 3 x 20 mm 8 Stück zur Befestigung der Scharnierteile an der Rückseite des zusammengeschraubten Gehäuses 23 Spanplattenschraube Senkkopf Torx T10 A2 3 x 50 mm 13 Stück für Verschraubung der Sperrholzplatten miteinander (Platten 1 bis 9) 24 Scharnier (3D-Druck) 2 Stück 25 Zylinderschraube ISO 4762 - A2 M5 x 70 mm 1 Stück Bolzen für 3D-gedruckte Wandhalterung (u.U. reichen Zylinderschrauben der Länge M5 x 65 mm) 26 Sicherungsmutter ISO 10511 - A2 M5 1 Stück Mutter für Bolzen Zylinderschraube M5 x 70 27 Unterlegscheibe DIN 125 A  - 5mm 2 Stück Zwischenscheibe für M5 x 75 mm Bolzen 28 Hornbach Wohnraumlasur, farblos 750 ml 1 Dose verhindert Schmutz und Eindringen von Feuchtigkeit; macht das Gehäuse abwaschbar. Lasur zum Pinseln / Auftragen mit Farbroller 29 Bohrschablone 1 Stück Für die Befestigung an der Wand (DIN A3 Format) Dokumentation der Aufbauschritte Gehäuse Iteration 1 - Aller Anfang ist schwer (gescheitert) Der erste Entwurf unseres Werkstattmonitorprototyps hat leider nicht ganz gepasst. Selten passt ein Prototyp gleich beim ersten Mal! Beim Zusägen des Merantisperrholzes Jeder Millimeter zäht. 5,6 mm gemessen, aber aus Versehen mit 6,2 mm modelliert Ausschneiden mit dem Laser Einzelteile Mit Display eingesetzt sieht es schon hübsch aus. Kabelkollisionen stören beim Einsetzen. Außerdem ist der Raspberry Pi zu dick Erst geleimt. Und dann festgestellt, dass Verschrauben besser wäre Also noch extra verschraubt ... Nach dem Schleifen Gehäuse Iteration 2 - Besser, aber immer noch nicht so ganz passend (gescheitert)  Acrylteile zur besseren Befestigung des Klettbands Extra Kabelführungskanäle und Klebeflächen aus Acryl Mit Kabeln eingepasst und vorbereitet für den Einsatz des Displays Prototyp v2 vollbestückt. Sieht schon gut aus. Passt leider immer noch nicht. Der Platz für die Kabel im Inneren ist zu eng und das Display hält mit den verwendeten Klettveschlüssen schlechter als erhofft. Rückseite Gehäuse Iteration 3 - Die fertige Variante vor dem Einsetzen und Verschrauben des Monitors im Gehäuse ist darauf zu achten, dass HDMI als Source (Eingang) ausgewählt ist und der Sound auf 100% Volume eingestellt wurde. Denn nach dem Einbau ist der Einstellknopf nicht mehr erreichbar, da er verdeckt ist. Die Standardsettings normalerweise (100% Audiolautstärke): Vor dem Einsetzen des Displays in das Gehäuse schon alle Zylinderschrauben mit Unterlegscheiben einlegen (hilft beim besseren Einfädeln) Für das Anbringen an die Wand gibt es eine Bohrschablone Toleranzen einplanen. Die Winkel sollten sehr genau angeschraubt werden Für die Bedienung der hinten gelegenen Knöpfe sollte ein Etikettiergerät verwendet werden, um die Laut/Leise/Einschalt-Knopf zu beschriften  Abkleben der Monitorelektronik  Scharniere aus dem Prusa i3 MK3S - beim Slicing  Scharniere drucken - der fast fertige Druck  Ansicht der übereinandergelegten Platten  Fertig zusammengeschraubt Nach stundenlangem Laserschneiden und Zusammenschrauben Geschliffene Außenflächen Beim Lasieren  Fertig lasiert Rückseitendeckel verschraubt Winkel aufgeschraubt  mit Kabeln und Raspberry Pi bestückt Zugentlastung eingebaut Bohrschablone anlegen und Löcher vorbereiten Winkelbefestigungen anschrauben Im Betrieb Mögliche Probleme und Nachbetrachtungen Thermisches Die Faytech Monitore sind für Betriebstemperaturen zwischen 10 °C und 60 °C laut Datenblatt ausgelegt. Die Kapselung in ein Holzgehäuse ist damit mit höchster Wahrscheinlichkeit völlig unproblematisch. Im Gegensatz kann es u.U. Kühlungsprobleme mit der Kombination aus Raspberry Pi 4 Model B, dem PoE Hat und den verwendeten Raspi-Gehäusen geben. Der PoE Hat hat einen 25mm Axiallüfter verbaut. Da der Lüfter nach oben zum Display zeigt erfüllt eine Ausparung im magnetischen Deckel keinen Zweck. Kleine seitliche Bohrungen (Ø5 - 8 mm) im Deckel können die Zuluft-Abluft-Thematik eventuell entschärfen. Mechanisches Die verwendeten Sperrholzplatten sind relativ weich und bieten keinen Schutz gegen starke mechanische Einwirkungen. Wahrscheinlich bekommen sie mit den Jahren ihren eigenen Chabby Chic Style. Weitere Ideen zur langfristigen Ergänzung Die Werkstattmonitore könnten noch um verschiedene Gadgets und Sensoren erweitert werden, um noch mehr nutzvolle Dinge zu verrichten. Barcode Scanner Durch eine USB Webcam oder eine RPi Cam und einem Python Script, welches aus permanenter Hintergrunddienst läuft, können Objekte mit Barcode aus der Werkstatt gescannt werden und analog zum 5. Scanner-Konzept zum entsprechenden Objekt in der Inventar-Instanz weiterleiten. Ein gutes Tutorial findet sich unter https://tutorials-raspberrypi.de/raspberry-pi-barcode-scanner-qr-mit-kamera-selber-bauen. Ein passendes Python Codeschnipsel findet sich dort. Medien darstellen Eine USB-Verlängerung nach außen zu legen, um damit Geräte wie USB Sticks anzuschließen, kann bei der Werkstattnutzung praktisch sein. Nutzer könnten z.B. mitgebrachte technische Zeichnungen aufrufen und darstellen. So könnte der Monitor auch als temporäre Projekthilfe genutzt werden. Der Raspberry Pi 4 Model B ist leistungsfähig genug, um flüssig Videos von YouTube und Co. wiederzugeben. Eine weitere praktische Ergänzung könnte eine Maus-Tastatur-Kombination mit Funk-Empfänger/-Sender sein. Durchpausen von Grafiken Rollenhalter mit Pauspapier und Klemmleiste oben/unten anbringen könnte eine weitere Hilfe sein. Damit kann das Display als Durchpaus-Tool verwendet werden. An der Unterseite könnte ein Papierschneider angebracht werden, um das Pauspapier abzuschneiden. Da das Touch-Display auch reagiert, wenn ein Blatt Papier darauf liegt, muss die USB-Verbindung zwischenzeitlich manuell getrennt werden. Das kann mit einem Schalter passieren. Es gibt USB-Kabel mit an/aus Schalter zu kaufen. Ein kleiner Köcher an der Seite ergänzt das Display mit geeigneten Stiften. Werkstattdaten per Sensoren / Monitoring Ergänzen der Werkstattmonitore um Sensoren für Temperatur, Luftfeuchtigkeit und Helligkeit kann beim Monitoring helfen. Durch die gesammelten Daten ließe sich unter anderem ablesen bzw. deuten, ob Heizungen vergessen wurden auszuschalten, ob evtl. das Licht noch brennt oder ob gelüftet werden sollte. Für die Ressourcennutzung und den sparsamen Einsatz wäre dies ein u.U. sinnvoller Versuch. Die installierten PoE Hats der neu angeschafften Raspberry's verwenden bereits I2C Header, weshalb Sensoren mit 1-Wire (Digital Pin) Anschluss besser geeignet sind. Außerdem benötigen wir Passthrough Stecker (https://www.raspberrypi.org/blog/introducing-power-over-ethernet-poe-hat)und es müsste ein Loch in die Außenhülle des Gehäuses für die Kabel der Sensoren eingefeilt werden. Mögliche Sensoren zum Nachrüsten sind: https://eckstein-shop.de/DHT22-AM2302-Digital-Temperatur-Feuchtigkeit-Sensor-mit-Kabel-und-47-kOhm-Widerstand (1-Wire) http://www.uugear.com/portfolio/using-light-sensor-module-with-raspberry-pi (1-Wire mit 1x analog und/oder digital) Software-Konzept Die Software-Konfiguration der Raspberry Pi's findet sich im Admin-Bereich unter Werkstattmonitore mit Raspberry OS (überholt). Förderungen Die Anschaffung der Werkstattmonitore und Raspberry Pi's inklusive Netzwerksystem wurden gefördert von der Beauftragen der Bundesregierung für Kultur und Medien und dem Bundesverband Soziokultur im Rahmen der Initiative NEUSTART KULTUR im Jahr 2020. www.kulturstaatsministerin.de Die Erarbeitung der Werkstattmonitor-Befestigung (Lasercut Gehäuse und 3D-Druck Winkel) als technologisches Schaubeispiel und deren Dokumentation über Entwicklung, Bau und Installation wird mitfinanziert mit Steuermitteln auf Grundlage des vom Sächsischen Landtag beschlossenen Haushalts im Rahmen der Initiative Fachkräfteallianz. Intro Da jedes Objekt im FabLab seine eigene eindeutige Inventarnummer und zugewiesene Eigenschaften bekommt, ist auch jedes Objekt nachvollziehbar in seiner kompletten Historie und Beschaffenheit. Als physischer Gegenpart zur Software steht deshalb als Aufgabe das Labeln der Objekte mit Inventaraufklebern.  Die Gegenstände im FabLab Chemnitz erhalten nach und nach alle einen Inventaraufkleber zur Identifizierung. Ein Etikett dient dazu, das Objekt in Teedy anzuzeigen, indem es per Barcode Scanner gescannt wird. Dieser Barcode enthält eine eindeutige URL, die direkt im Browser aufgerufen wird. Dadurch kann der Nutzer schnell herausfinden, um welches Objekt es sich genau handelt (Fotos, Handbücher, Eigenschaften). Falls eine Sache aus mehreren Objekten zusammengesetzt ist (z.B. Gerät mit Fernbedienung), dann sollte möglichst jedes Einzelobjekt etikettiert werden, um die Zusammengehörigkeit wieder herstellen zu können. Inbesondere, wenn Unordentlichkeit herrscht und die Werkzeuge unsortier in der Werkstatt rumliegen. Der Ablauf: Objekt-Exportdatei generieren und per Web Server bereit stellen (Bereitstellungskonzept) InkScape Erweiterung generiert Aufkleber-Vektordateien (Etikettenkonzept) Client-Script sendet die Aufkleber an einen Etikettendrucker (Modell Brother QL-720NW) (Druckerkonzept) Etiketten werden auf farblich getrennte und geometrisch geeignete Träger (Schildchen) aufgebracht, Schilder werden dann auf die Objekte angebracht (Schilderkonzept) Barcodes scannen Nutzer scannen mit dem eigenen Smarthpone oder Tablet (Scanner-Konzept) Nutzer scannen die Objekte mit einem Scanner an den in den Werkstattbereichen installierten Werkstattmonitoren (Werkstattmonitor-Konzept) Nutzer werden auf die entsprechende Objektseite im Inventarsystem umgeleitet (Web Server Redirect Konzept) (auf dem eigenen Smartphone) Was benötigen wir für eine eigene Inventar-Instanz? Du hast dich für eine digitale Inventarverwaltung entschieden? Fein! Das ist der erste Schritt für echte Nachhaltigkeit in der Werkstatt. Ordnung ist das halbe Leben, so sagt man. "Ordnung ist nur was für Leute, die zu faul zum Suchen sind" - so sagt man auch. Das ist leider nur bedingt richtig. Denn diese Aussage gilt nur für Einzelpersonen, aber nicht im Punkt der Zusammenarbeit in einer Gruppe. Immer dann, wenn mehrere Menschen in einer geteilten Sphäre arbeiten wollen, dann müssen sie sich gut organisieren. Denn niemand weiß, was im Kopf des anderen vorgeht und welche Logik sich im jeweiligen (spontan) entstandenen Use-Case entwickelt. Deshalb kommt man nicht herum, Werkstattregeln zu definieren. Eine Inventarinstanz kann hier extrem helfen. Absofort willst du Handbücher und Maschinennamen schnell (wieder)finden, sinnloses Umherlaufen vermeiden und nichts mehr doppelt kaufen oder unbedarft wegschmeißen. Du willst endlich wissen, wie Dinge (auch) heißen bzw. deren Synonyme kennen und du willst vielleicht sogar allem einen eigenen Name geben. Du hast dich schon immer gefragt, wofür bestimmte Sachen oder komische Zubehörteile sich eignen, wie man sie handhabt und was man lieber nicht damit ausprobieren sollte. Du möchtest mit einem zentralen System andere in deiner Umgebung schulen und dein eigenes Wissen oder das Wissen deiner Kollegen weitergeben und darauf zeigen können, wo man es findet. Du willst rapide etwas über eine Reparatur oder ein Zubehör nachlesen und rausfinden, wo es seinen festen Ort hat oder ob es noch einen Ort braucht. Du willst verdammt nochmal wissen wem das Teil gehört und ob es Kunst ist oder weg kann. Für Beginner, die ihre vorhandene Werkstatt grundlegend digital ordnen wollen und sich weniger mit den digitalen IT-Themen auseinandersetzen, sich jedoch zutrauen eine eigene Teedy-Instanz aufzusetzen (aufsetzen zu lassen), bieten wir hier einen Schritt-für-Schritt-Plan. Notwendige Hardware und Software Im einfachsten Fall lässt sich ein eigener Teedy-Server im Büro aufsetzen. Ganz ohne Miet-Server mit monatlichen Fixkosten. Was du dazu nutzen kannst: ein Raspberry Pi 3 oder neuer (Für ca. 50-100 € zu erhalten - je nach Ausführung in puncto Netzteil, Gehäuse, Kühlung und Zubehör wie Festplatte): Wir empfehlen einen Raspberry Pi 3 B+ mit einer auf Ausdauer ausgelegte (high endurance) 8 GB SD-Karte (auf dieser soll nur das Betriebssystem sein) Für die Inventardaten empfehlen wir eine externe USB-Festplatte oder eine im Netzwerk angeschlossene NAS) Betriebssystem: Raspbian Installation von Teedy → Siehe Teedy Installation and Configuration eine Portfreigabe auf dem Router, falls externer Zugriff (außerhalb der Werkstatt - z.B. von zuhause aus) gewünscht ist ggf. dynamisches DNS, um sich keine häufig wechselnde, öffentliche IP-Adresse zu merken, sondern eine feste Domain zu haben, z.B. kostenfrei via "MyFRITZ!Net" von AVM (https://sso.myfritz.net), sofern eine Fritz!Box genutzt wird kostenfrei via Synology DDNS Service (https://account.synology.com), sofern eine NAS-Lösung von Synology genutzt wird Notwendige Kompetenzen und Vorplanung Hardware und Software kann man sich relativ schnell zusammenorganisieren. Nach dieser Hürde, die zugegeben sehr lange dauern kann, wenn etwas schief geht, kommen weitere Folgeaufgaben hinzu. Was brauchen wir? Zunächst pauschal gesagt: einen guten Plan und Durchhaltevermögen. Am Anfang ist alles chaotisch und ein riesiger, unbezwingbarer Berg an Arbeit steht scheinbar an. Konkret brauchen wir Antworten und Entscheidungen: Wer soll sich um das Inventar kümmern? Einer oder alle? Wer kann mit Software gut umgehen? Wer hat ausreichend technischen Sachverstand, um am Inventar zu pflegen oder die notwendigen Infos weiterzugeben? idealerweise eine Übersicht der Werkstatträume (wer gleich gut planen will, der vergibt folgende Attribute: Werkstattnummer, Werkstattname, zugewiesene Farbe zur Wiedererkennung) eine Entscheidung darüber, ob ... elektrische Maschinen und Werkzeuge eingepflegt werden sollen nicht-elektrische (Hand)werkzeuge eingepflegt werden sollen Möbel eingepflegt werden sollen Verbrauchsmaterialien wie Schrauben oder Kleber eingepflegt werden sollen einen Plan über notwendige Objektmerkmale, wie z.B. EAN-Nummer, Gewicht, elektrische Leistung, Besitzer, Pate, etc. etc. ein gutes Schema für Objekte, was möglichst konsequent durchgezogen wird Rollenverteilung: soll der Gastmodus aktiv sein? Wer sind die Nutzer und wer sind die Editoren/Moderatoren der Instanz? schlussendlich eine durchdachte Stichwortstruktur. Diese erlaubt das sinnvolle Einsortieren und Filtern von Objekten. Hier findest Du unsere Tag-Struktur als Beispiel (bitte beachten: die Stichworte sind höchst individuell. Blankes Copy/Paste wird euch schnell an eure eigenen Verständnis- und Umgangsgrenzen bringen):       Wie befüllen und pflegen wir das Inventarsystem? Wir brauchen jede Menge Informationen zu den einzelnen Objekten, die wir allesamt neu anlegen müssen. Je mehr Infos, desto besser. Aber kein Grund zur Sorge: Am Anfang fehlt alles und jede Information ist besser als keine. Alles kann nach und nach angereichert und verbessert (oder auch wieder gelöscht) werden. Für den Anfang ist es gut, wenn man "einfach mal" durch die Werkstattbereiche geht und alle möglichen Objekte fotografiert (zu empfehlen sind Fotos vom gesamten Objekt, vom direkten Zubehör im Umfeld, vom Maschinenmodell (Aufdrucke, Logos), vom Typenschild (Seriennummern, elektrische Kennwerte, Modell-Revision, etc.) und ggf. Defekte und Modifikationen). Am besten vom Groben ins Feine (von groben Werkstattecken bis zur aufgezogenen Werkbankschublade), ggf. im Uhrzeigersinn oder nach einem geeigneten Schema, um auch im Anschluss (digitale Pflege) vorm geistigen Auge durch die Werkstatt zu gehen. Anhand von Fotos erkennt man bereits häufig auch später im DMS ganz schnell das "Was?" und das "Wo?" der Sachen. Anhand dieser Infos können wir per Online-Suchmaschine schnell die passenden Handbücher und Stücklisten zusammenfinden und ins DMS hochladen, sofern die Maschine noch keine 20 Jahre alt ist (aus einer Zeit stammt, wo PDFs noch nicht gebräuchlich waren). Eigenschaften wie Zustand, Gewicht und Zubehör einzupflegen sind typische Folgeaufgaben einer gewachsenen Inventarinstanz und helfen bei der eigentlichen Optimierung und Vorplanung der Werkstatt. Häufig ist eine Aufgabe auch das Einscannen vorhandener papierischer Handbücher, Quick Intros oder Reparaturscheine, Garantiescheine, Verschrottungsnachweise, whatever). Professionalisierung (siehe Konzepte unter Vom Etikettenschild zum Objekt) Für die professionellere Anwendung sollten weitere Dinge beachtet oder ergänzt werden in einer idealen Inventarinstanz fließen neu angeschaffte oder entsorgte Geräte direkt als Information in das DMS ein, indem wir neue Objekte anlegen (ggf. nur vorbelegen) oder wieder löschen. Dazu helfen die Abteilungen/Rollen Buchhaltung, Einkauf und  Werkstattleitung mit ihrer Kompetenz und Informationsweitergabe aus. auf eigener Sub-Domain hosten: eigenen DNS-Server konfiguriert werden - entweder beim Domain-Hoster oder auf dem eigenen Server mit angepasstem Nameserver-Setup Background-Auswertung der Datenbank (PostgreSQL) realisieren Quellcode von Teedy anpassen und eigene Features hinzufügen/weiterentwickeln Werkstattmonitore Inventar-Stickern Werkstattorientierung im FabLab - Digitales Inventar Unser Inventarsystem: https://things.fablabchemnitz.de - (Login notwendig) Einleitung / Allgemeines Das physische Inventar im FabLab ist ein zentraler Bestandteil. Ohne Werkzeuge und Maschinen kann nichts ausprobiert oder hergestellt werden. Ohne Ordnung findet man sich nicht gut zurecht. Insbesondere an einem Ort, wo sich diese Dinge geteilt werden, ist deshalb etwas Organisation notwendig. Umso wichtiger ist neben der Pflege des Inventars (technisch einwandfreier Zustand) die Übersicht zu behalten. Transparenz ist wichtig, um sinnvoll mit den Betriebsmitteln zu wirtschaften. Insbesonders neue Mitglieder im Verein, die sich noch weniger gut auskennen, sind meistens einfach nur überfragt in Sachen Auffinden von Tools. Aber auch altangestammte Mitglieder haben meist zu wenig Einsicht in die verborgenen Schätze unserer Werkstatt. Das soll sich ändern! "Eigentum verpflichtet" heißt es so schön. Das Inventar einer Werkstatt stellt demnach eine Schlüsselkomponente dar, denn ohne gepflegtes Equipment gibt es häufig Probleme beim Nachvollziehen und Beantworten von Fragen wie ... wem gehört das Objekt? in welchem Zustand ist das Objekt gerade? wer ist Ansprechpartner für das Objekt? wo gehört das Objekt eigentlich hin? Stammt es etwa aus einem anderen Werkstattbereich und sollte mal wieder einsortiert werden? wo finde ich das Handbuch zum Objekt? wann wurde das Objekt zuletzt geprüft? Wurde es überhaupt schon einmal geprüft? Kann ich mich schneller als erwartet daran verletzen? (Stichwort "DGUV-3 Prüfung") haben wir so ein Objekt schon im Lab oder sollten wir sowas neu anschaffen? Diese und weitere Fragen ergeben sich regelmäßig, wenn man mit vielen Werkzeugen und Maschinen zu tun hat und die Werkstatt so richtig nutzen möchte. Die Idee der Inventarverwaltung ist eine alte und begleitet das FabLab schon seit vielen Jahren. Der Grundgedanke ist lange verankert und durchlebte verschiedene digitale Orte, an denen begonnen wurde, dieses Wissen zu sammeln. 2016 wurden die ersten Inventargegenstände in ein zweckentfremdetes Shop-System eingepflegt (Projektname "Farringdon"). Leider war dieses System zu unhandlich und komplex und staubte langsam ein. Währenddessen wuchs der Wunsch nach mehr Komplettierung. In einem Shop-System lassen sich zugehörige Dokumente und Verläufe von Dingen meist weniger gut darstellen (z.B. Repararaturen und Hinweise, Handnotizen, Handbücher, Software, Treiber, usw.). 2018 ging Confluence als Wiki-System online und es pflegten sich in kurzer Zeit Projektdokumentationen und Handbücher von gekauften wie selbstgebauten Maschinen ein. Dadurch gab es einen Split zwischen dem Shop-System und der Wiki. Beide vorhergehenden Lösungen waren nicht ideal, weil es Ihnen an Funktionalitäten fehlte, andere Features dafür sogar zu groß waren. 2020 wurde eine Instanz von Teedy für das Inventar online genommen, welches nun Wiki und Shop-System für die Bedürfnisse der Inventarpflege ablöst. Die alten Systeme wurden alle in das neue System integriert und vereinheitlicht. Viele Datenleichen wurden damit entfernt. Farbschema für Räume und Etikettenschilder Im FabLab nutzen wir Farben, um Räume, Bereiche und deren Inventar besser auseinander zu halten. So haben zum Beispiel unsere Schlüssel (Türschlüssel, Werkbankschlüssel, ...) Farbmarkierungen. Auch unser Inventar bekommt Stück für Stück farbige Markierungen. Unsere Inventarschildchen werden mit Filamenten in RAL-Farbe gedruckt. Die folgenden Farben nutzen wir unter anderem: Raum Nr. Artikelnr. Filament (extrudr.com) Hex Code regular Hex Code websafe RAL Nummer (mit RGB-Mapping im Icon) Brother TZe-* Band (12 mm / 24 mm) Sprühfarbe Molotow Premium von Belton A000 Flur 9010241043026 (RAL 9017) #000000 #000000 9005 Tiefschwarz 335 BLACK - WHITE INK - LAMINATED 221 deep black A001 Sozial/Empfang 9010241043194 (RAL 6001) #61993b #669933 6018 Gelbgrün MQG35 LIME GREEN - WHITE INK - LAMINATED 163 avocado A002 Textil/Workshop 9010241043217 (RAL 4008) #c4618c #cc6699 4003 Erikaviolett MQP35 BERRY-PINK  WHITE INK - LAMINATED (nicht als 24 mm Tape käuflich. Wir nutzen Washi-Tape + transparente 24 mm Labels) TZE-SC24P TZE-MQP55 BPT-TZE-MQP55 053 candy A003 Büro/Server 9010241043040 (RAL 7044) #c5c7c4 #cccccc 7035 Lichtgrau MQ531 PASTEL BLUE - BLACK INK - LAMINATED 073 thistle A004 Elektronik/3D-Druck 9010241043262 (RAL 3024) #cb555d #cc6666 3017 Rosé 431 oder 435 041 strawberrry red A005 Grafikwerkstatt 9010241043200 (RAL 5018) #00b395 #00cc99 5018 Türkisblau 731 oder 735 GREEN - WHITE / BLACK INK - LAMINATED / TURQOISE BLUE 126 lagoon blue A006 Metallwerkstatt n.A. #d8a0a6 #cc9999 3015 Hellrosa MQE31 PASTEL PINK - BLACK INK - LAMINATED 048 mauve A007 Wasserstrahlschneidwerkstatt 9010241043422 (RAL 5013) #42698c #336699 5023 Fernblau 531 oder 535 BLUE WHITE / BLACK INK - LAMINATED 096 tulip blue middle A08 Holzwerkstatt 9010241043163 (RAL 1023) #faca30 #ffcc33 1018 Zinkgelb 631 YELLOW - BLACK INK - LAMINATED 002 zink yellow Messe/Unterwegs/Ausgeliehen 9010241043019 (RAL 9003) #ecece7 #ffffff 9003 Signalweiß 123 WHITE - BLACK INK - LAMINATED 231 signal white Außenbereich 9010241043057 (RAL 9023) #9b9b9b #999999 7004 Signalgrau MQF31 PASTEL PURPLE - BLACK INK - LAMINATED 226 grey blue middle Hilfreiche Tools https://www.ralfarbpalette.de/ral-classic https://rgb.to/ral Für andere Zwecke reservierte Farben (stehen nicht zur Verfügung bzw. sollten beachtet werden): Rettungswege (grün) Feuerlöscher, Gefahrenhinweise (rot) Arbeitsschutz (blau) FabAccess (türkis) Warnschilder (gelb) Gitsymbole (orange) Raumübersicht mit Farbschemazuweisung Wir verwenden außerdem #FF9B00 Melonengelb für Objekte, die beweglich sind bzw. deren Ort nicht festgeschrieben ist (Dinge im ständigen Umlauf oder extern unterwegs). Die Vorteile digitaler Inventarverwaltung unser eigenes Inventar bereichsmäßig gut und schnell durchsuchbar machen (nach verschiedenen Aspekten wie Tätigkeiten, Bereichen, Ansprechpartnern, Zustand, etc.) Konservieren von Dokumentation (Handbücher, Gebrauchsanweisungen, Stückliste, etc.) zu Maschinen und Werkzeugen. Dokumentationen verschwinden mit der Zeit im Internet und sind dann verloren. Insbesondere für ältere Maschinen ist es zunehmend schwer Material zu finden. Außerdem verschwinden auch ausgedruckte Handbücher schnell mal im Abfall. Deshalb ist eine zentrale Speicherquelle bzw. Archivierung praktisch, um die Informationen jederzeit reproduzieren oder zumindest auffinden zu können. zentralen Überblick verschaffen → Filtern nach Kriterien wie ausführbaren Tätigkeiten, Ansprechpartnern, Bereichen, Zuständen, etc. Ausdrucken von automatisch generierten Inventaraufklebern, welche per Barcode scanbar sind. Ziel ist dabei das direkte Anzeigen des Objekts auf dem Zielgerät (z.B. Smartphone, Tablet) Teedy basiert auf einer PostgreSQL-Datenbank, welche mit Hilfe von Grafana leicht angezapft und visualisiert werden kann. Verschiedene Dashboards können so einfach auf Tablets oder in Confluence-Dokumentationen eingebunden werden. So können  z.B. DGUV-3 Prüflisten ausgeleitet werden. Siehe auch Prüfung ortsveränderlicher und ortsfester elektrischer Geräte (DGUV-3) Die Nachteile digitaler Inventarverwaltung der große initiale, sowie fortlaufende Aufwand in der Pflege der Datensätze (Handbücher, Fotos, Hersteller, Verschlagwortung, sonstige Eigenschaften wie Gewicht oder EAN) das schnelle Veralten von Informationen (Objekt hat Ort gewechselt; Objekt wurde wieder verkauft; o.ä.) bzw. Synchronisation der virtuellen Verwaltung und der physischen Werkstatt (digitale Informationen stimmemn nicht überein) Objekte, die mehrfach vorkommen, aber gleiche technische Eigenschaften haben: Kopieraufwand der Tags und Metadaten zum Synchronhalten Zur Kompensation dieser Nachteile müssen möglichst viele Werkstattverantwortliche mitwirken, um den Bestand aktuell zu halten. Abgrenzung festes Inventar zu Verbrauchsmittel Eine Werkstatt ist lebendig. Dinge kommen und gehen. Was in der Regel bleibt sind die festen Infrastrukturen, also Werkzeuge und Maschinen, sowie die Möbel, die sie tragen oder aufbewahren. Im Gegensatz dazu wandern flüchtige Verbrauchsmaterialien wie Schrauben, Holzplatten, Kleber, Öle und Co. durch die Räume und wechseln hin und her. Deshalb pflegen wir Verbrauchsmittel konsequent nicht im Inventarsystem Teedy, sondern ein einer simplen In-Out-Liste (siehe Verbrauchsmaterialübersicht). Teedy als Inventar-System Das FabLab Chemnitz betreibt ein protypisches System zur Verwaltung der physischen Werkstattbereiche und deren Inventargegenstände. Dazu nutzen wir das Dokumentenmanagementsystem Teedy, sowie ein paar daran angebundene Peripheriebausteine (Grafana-Monitoring Dashboards, Etikettenaufkleber-Generator für InkScape, 3D-gedruckte Etikettenschilder, usw.). Mit Teedy haben wir ein übersichtliches, aber sehr funktionales Werkzeug mit schneller und intuitiver Suchfunktion, welches blitzschnell reagiert und via API und Datenbank-Anbindung gut automatisierbar ist. In Teedy wird jedes Objekt (das Zeug, die Sache, das Ding, das Werkzeug, die Maschine) als sogenanntes "Dokument" angelegt. Diese Dokumente werden angereichert durch Stichwörter, Metadaten, Beschreibungstexte und Dateien.   Ein paar Screenshots von der Oberfläche Allgemeine Dokumentation zu Teedy Wir nutzen Teedy nicht nur selbst, sondern haben auch jede Menge Dokumentation zur Server-Installation, Konfiguration und Nutzung dazu geschrieben. Das ist sozusagen nochmal ein extra Projekt. Teedy Documentation Space Allgemeine Objektstruktur, Stichworte (Tags) und Berechtigungskonzept Die virtuelle Werkstattpflege macht das Inventar leicht filterbar. Allerdings gehört dazu ein gewisses Grundverstädnis dazu, um zu wissen, wie man filtern kann. Dazu ist es hilfreich ein paar begriffliche Definitionen auseinander zu halten und die Struktur von Teedy zu verstehen. In Teedy werden Objekte ("Dokumente") nicht in klassischen Ordnerstrukturen abgebildet. Die Struktur (Ordnung) der Dinge (Objekte, Tools, things, Zeug, Werkzeug, Maschinen, Automaten, ... - es lassen hundert Synonyme hierfür einsetzen) wird hauptsächlich über die mehrfache Verstichwortung erreicht (tagging). Das sind zum Beispiel Eigenschaften wie "defekt", technische Schnittstellen wie "USB" oder "HDMI" oder Orte, an denen sich das Objekt befinden kann (z.B. "Metallwerkstatt"). Es handelt sich damit um das Zuweisen von Merkmalen, Attributen, Eigenschaften, die ein Objekt genauer deklarieren. Mit Hilfe der Stichworte lässt sich so recht einfach ein defekter Monitor mit HDMI- und USB-Anschluss, in der Metallwerkstatt steht, filtern. Zur Pflege, Durchsuchbarkeit und Vergleichbarkeit nutzen wir die folgenden Stammdaten. Ein grundlegender Objektaufbau (Dokumenteneigenschaften) bildet das Skelett. Falls ein Objekt zwei mal in der Werkstatt vorkommt, wird es in Teedy zweimal angelegt, denn jedes Objekt hat seine eigene Historie (z.B. Reparaturen, Modifikationen, ...) Dokumententitel Wir nutzen folgendes Schema: von #| "" Beispiele: "Laser-Entfernungsmesser DLE 40 von Bosch" "Infrarot Heizpanel KH E-600 WS von Könighaus GmbH #2" Erstellungsdatum Das Erstellungsdatum ist das Datum, das der Nutzer im Dokument aktiv selbst festlegt. Das Erstellungsdatum entspricht nicht dem Erzeugungsdatum des Dokuments (Initiales Anlegen des Dokuments) - dieses wird automatisch vergeben und spielt keine wichtige Rolle. Das niedrigste Datum in Teedy kann der 01.01.0001 sein.  Das Erstellungsdatum kann viele verschiedene Informationen transportieren und ist als solches nicht besonders aussagekräftig. Wir nutzen es schlichtweg als das Datum, wann der Gegenstand ins System eingefügt wurde. Es entspricht nicht dem Datum, seit wann der Gegenstand im Verein geführt wird. Letzteres kann nur eindeutig durch Kaufbelege (Rechnungsdatum, Bezahldatum, Lieferdatum, ...) und Leihverträge definiert werden. Diese Daten sind jedoch eher nur für die Buchhaltung und Abschreibung relevant oder aber um zu wissen welche Dinge kürzlich neu in den Verein gelangt sind. Das Erstellungsdatum wird in der Dokumentenansicht wird neben dem Dokumententitel angezeigt. Es ist außerdem auch in der linken Dokumentenliste sichtbar. Volltext/Beschreibung Jedes Dokument sollte nach Möglichkeit eine gute Beschreibung haben Modifikationen (Reparaturen, Pimps) Details (z.B. Nutzungshinweise, Energieverbrauch, Handling, Besonderheiten, Leistung, etc.) Sonstige Sicherheitshinweise, die durch Merkmale nicht abgedeckt werden Links zu ggf. vorhandener Maschinenvorstellungen und Projektvorstellungen Links zu externen Quellen (z.B. Hersteller, Quelldateien, etc.) ggf. Link zum Wiki, falls es eine ausgiebigere Dokumentation/Tutorials bedarf Die Beschreibung wird im Reiter "Inhalt" eingefügt Kommentare Kommentare von Nutzern helfen beim Verstehen der Objekthistorie. So lässt sich hier Aktuelles wie Defekte, Veränderungen, Fragen, etc. niederschreiben Kommentare werden am rechten Rand eingeblendet Dateien Bilder (Details, Defekte, Reparaturen, Anpassungen ...) Skizzen Handbücher Stücklisten ... Dateien können als Galerie oder Liste angezeigt werden Beziehungen (Links/Relationen) In jedem Dokument können Links zu zu anderen Dokumenten gesetzt werden. Dadurch sind diese untereinander verknüpft und können einfach angeklickt werden. Damit lässt sich direktes Zubehör (z.B. Boxen/Behälter für das eigentliche Werkzeug, zusätzliche Halterungen und Adapter, etc. - z.B. Winkelaufsatz für Festool Akkuschrauber in Systainer-Box) einfach verlinken. Beziehungen werden im Dokument direkt unterhalb der "Mitwirkenden" angezeigt Stichworte (Tags) Das schwierige beim Inbetriebnehmen einer Inventarlogik in das Teedy ist das Finden geeigneter Stichworte, um schlüssige Beschreibungen auf den ersten Blick zu erzeugen. Diese sollte eindeutig sein (minimierte Redundanzen) und genügend Unterscheidungsspielraum bieten. Tags sind das Herzstück der Instanz und machen das System intelligent durchsuchbar. Die Übersicht aller Tags in unserer Teedy-Instanz kann gefunden werden unter https://things.fablabchemnitz.de/#/tag (als eingeloggtes Mitglied ist diese Baumstruktur recht groß und detailreich. Als Gast sieht man hier nur den "public" Tag). Tags können unter anderen Tags geschachtelt sein und ergeben somit logische Cluster, wenn man sie auf einer Mind Map grafisch darstellte. So sehen Tags in Teedy aus. Grundbegriffe wie Maschine, Werkzeug, Automat und das Internet of Things (IoT) Die folgenden Begriffe sind für die Filterung in Teedy nur bedingt von Bedeutung, helfen jedoch beim Verständnis ebenso. Der Begriff Maschine ist relativ umstritten und kann vielschichtig aufgefasst werden. Eine valide Definition kann zum Beispiel sein: "Eine Maschine ist ein technisches Gebilde mit durch ein Antriebssystem bewegten Teilen".  (Wikipedia) Der Begriff Werkzeug wird beschrieben als "Ein Werkzeug ist ein nicht zum Körper eines lebenden oder künstlichen Organismus gehörendes Objekt, mit dessen Hilfe die Funktionen des Körpers erweitert werden, um auf diese Weise ein unmittelbares Ziel zu erreichen.". (Wikipedia) Eine Werkzeugmaschine ist eine Maschine zur Fertigung von Werkstücken mit Werkzeugen, deren Bewegung zueinander durch die Maschine vorgegeben wird. Zu den wichtigsten Vertretern zählen Dreh- und Fräsmaschinen, Erodiermaschinen sowie mechanische Pressen und Maschinenhämmer zum Schmieden.  (Wikipedia) Demnach könnte man schlussfolgern, dass eine Handmaschine eine Maschine zur Fertigung von Werkstücken mit Werkzeugen ist, deren Bewegung zueinander durch die menschliche Hand vorgegeben wird und mit Muskelkraft bedient werden. Das können zum Beispiel sein: Akkuschrauber, Bohrmaschine, Tauchsäge, uvm. Unter dem Begriff Handwerkzeug versteht man alle Werkzeuge, die mit der Hand und mit Muskelkraft bedient werden. Hierzu zählen unter anderem Hämmer, Schraubendreher, Zangen, Feilen, Schaufeln, Äxte, Messer und Scheren.  (Wikipedia) Ein Automat ist eine Maschine, die vorbestimmte Abläufe selbsttätig („automatisch“) ausführt.  (Wikipedia) Ein Automat kann eine Werkzeugmaschine sein. Weitere Begriffe, die man definieren kann, sind u.a. Sondermaschine, Kraftmaschine, Arbeitsmaschine, Anlage, Prothese, Motor, Instrument, Apparat, Gerät, und viele mehr (sowie deren Unterbegriffe). Sie stammen historisch betrachtet stark aus dem Bereich Maschinenbau und der Durchindustrialisierung. Bei den Begriffen spalten sich teilweise die Geister über die genaue Definition. Und je genauer man definiert, desto schwieriger wird eine Einordnung der heutigen Dinge, die in unserer Gesellschaft Platz und Anwendung finden. Prinzipiell gibt es Maschinen und Werkzeuge, die ohne Interaktion etwas tun und Geräte, mit denen man manuell etwas tut - mit oder ohne Energie-, Stoff- oder Informationsstransport. In der Regel hat man als Benutzer der Werkstatt früher oder später mit den meisten Geräten die eine oder andere aktive oder passive Form der Interaktion bzw. Informationsvermittlung. Wie beschreibt man heutzutage beispielsweise ein Smartphone mit all seinen Zwecken, Sensoren, Aktoren, Apps und Möglichkeiten? Wie valide sind diese Definitionen auf die heutige Elektronik im Allgemeinen?  Müssen neue Begriffe gefunden werden oder gibt es diese schon? Wäre dies sinnvoll? Wie beschreibt man Dinge, die aus den obigen Begriffskategorien mehreren Beschreibungen gerecht werden? Denn ein Smartphone ist längst mehr als ein Telefon, oder eine Kamera, ein Navigator, ein Buch, ein Sprachassistent, ein Gesundheitsmonitor. Im Prinzip kann ein Smartphone zum Beispiel als eine Kombination aus mehreren Werkzeugen oder Geräten verstanden werden, also ein Instrumenten-, Geräte und Apparateverbund oder einer "Mikroanlage " (räumlich betrachtet im kompakten Sinne). Dieser Verbund wird heutzutage zumeist als Internet of Things (IoT) verstanden und zusammengefasst. Tätitgkeiten, Merkmale und Nutzersichtweisen Die Tätigkeiten und deren maschinelle Umsetzung sind Grundlage für obige Definitionen Maschine, Werkzeug und Co., spielen aber für den umsetzenden Zweck eine untergeordnete Rolle. Es ist viel einfacher und zweckmäßiger, wenn man nach der Tätigkeit sucht, als einer klassischen Unterteilung zu folgen und sich zu fragen, ob ein Gerät nun eine Werkzeugmaschine, ein Handwerkzeug oder ein Automat ist. Denn durch die feingranulierten Unterscheidungen dieser Gruppierungen kann man den Nutzern allgemein unterstellen, dass diese Unterschiede in der Regel viel zu wenig bekannt sind, um diese allgemeingültig als Filtermerkmale in der Software sinnvoll zu nutzen. Deshalb ist der Praxisbezug wichtiger. Tätigkeiten haben in der Regel etwas mit "Machen" oder "Verrichten" zu tun. Hier geht es um zahlreiche solcher Verben wie "Schneiden", "Bügeln", "Erwärmen", "Beleuchten" und so weiter. Als Nutzer einer Werkstatt versucht man in der Regel ein Problem zu lösen und fragt sich zweckmäßigerweise "wie kann ich ein Loch in dieses Stück Holz bohren?", "wie kann ich die Teile kleben?", "womit kann ich es am Tisch befestigen?", etc. In aller Regel fragt man sich, ob die Maschine das könnte, was gewünscht ist und ob die Maschine für den Zweck vernünftig nutzbar ist. Dazu prüfen wir als nächstes Befindlichkeiten wie Gewicht, Größe und Strombedarf der Sache. Kann ich die Sache in diesem Werkstattbereich sinnvoll nutzen? Wo kann ich sie anschließen? Wie lange hält der Akku? Welches Zubehör passt dazu? Ist mir das Teil zu schwer und kann ich damit lange stehen? Wer ist dafür eigentlich Ansprechpartner? Es gibt zwei wesentliche Sichtweisen in der gelebten Praxis einer Werkstatt bezogen auf das Finden von Ansprechpartnern: Das System filtern und checken, welches Mitglied für welche Objekte Ansprechpartner ist (Suche nach Mitgliedern) Das System filtern und checken, welchem Objekt welche Ansprechpartner zugewiesen sind (Suche nach Objekten) Wahrscheinlicher und sinnvoller ist die letzte Sicht, denn in jeder Regel möchte man für eine konkrete Anwendung den passenden Ansprechpartner finden. In der Praxis ist es relevant zu filtern, ob ein Mitglied für eine bestimmte Maschine eine entsprechende Berechtigung besitzt. Zu schauen, was das Mitglied sonst noch alles darf ist genau genommen für den Nutzer irrelevant bzw. sollte eher für interne Qualifikationszwecke verstanden werden. An der Stelle, wo Personennamen auftauchen, geht es letztlich um das Thema Datenschutz. In unserer Teedy-Instanz lassen sich Objekte einfach finden, wenn man direkt nach der Tätigkeit sucht.  Ein Link als Beispiel, um alle bohrenden Tools im FabLab zu finden: https://things.fablabchemnitz.de/#/document/search/tag:Bohren Noch mehr Tags? Ja oder nein? Je mehr Tags eingepflegt werden, desto genauer lässt sich filtern. Allerdings wird das Pflegen nicht unbedingt leichter. Es ließen sich noch viele weitere Eigenschaften in das DMS einpflegen, zum Beispiel: Gefahrensymbole/Gefahrenkennzeichen (aus dem Handbuch entnehmbar) - idealerweise müsste man hierzu Tags noch Icons zuweisen können Tags für verarbeitbare Halbzeuge wie Kunststoff, Holz, Metall, Glas, Keramik,  Textilien, Pappe, Papier, Karton → die Liste wird unendlich lang und damit fast unnutzbar, wenn Objekte damit richtig durchgetaggt würden Tag für das Markieren, ob das Objekt bereits gelabelt/beschildert wurde → würde helfen bei der Erstbeschildung des gesamten Equipments, wird danach jedoch nicht benötigt und erfordert einen hohen Synchronisierungsauwand  zwischen reeller Beschilderung und dem Datenbestand Zugriffe / Sichtbarkeit - Steuerung durch Tags, Benutzer und Gruppen (Benutzerrollen) Das Inventar soll für verschiedene Nutzerkreise bzw. Szenarien unterschiedlich granular sichtbar sein. Deshalb gibt es mehrere Login-Stufen bzw. Nutzerrollen. Das trifft auch auf die Grafana-Dashboards zu, die weitere Informationen unseres Inventars in anderen Zusammenhängen anzeigen. In Teedy wird die Sichtbarkeit der Objekte mit den Tags festgelegt. Für jedes Stichwort werden Sichtbarkeiten für Nutzer und Gruppen festgelegt. Die Sichtbarkeit hängt also direkt an den Tags: Gastlogin für Besucher: Mit diesem Login sieht man die öffentlich gelistetenen Schmankerl unserer Werkstatt, die wir preisgeben wollen. Diese sind verstichwortet mit dem Tag "public" Allgemeiner FabLab-Nutzer: zur Standardansicht (Tags "Aktueller_Zustand", "Betriebsmittelart", "Diverse_Attribute",  "Kategorie", "Nutzungsberechtigung", "Tätigkeiten", "Werkstattbereich") Erweiterte FabLab-Betrachter: damit eingeloggt sehen wir granulare Details, die für den Normalnutzer eher nicht so wichtig sind und somit zur Unübersicht beitragen würden (Tags "DGUV_Prüfintervall" , "Medium", "Schutzklasse", "Datenschnittstellen"). Hinweis: Schutzklasse ungleich Schutzart! Editoren: sind Werkstattverantwortliche oder Dokumentoren, die sich um die Datenpflege kümmern FabLab Mitglieder können sowohl die allgemeine Ansicht, als auch die erweiterte Ansicht verwenden, um das System zu filtern. Editoren-Rechte bekommen nur Werkstattbereichsverantwortliche und Vorstandsmitglieder. Die Logins können in der Übersicht unserer Web Services gefunden werden. Berechtigungen / Sichtbarkeiten für Nutzer und Gruppen können an den Tags eingestellt werden. Metadaten Metadaten sind benutzerdefinierte Felder, die in jedem bestehenden oder neuen Dokument automatisch als ausfüllbare Felder auftauchen. Damit lassen sich Dokumente besser gleichartig strukturieren. Für das Inventar gibt es ein paar Standard-Metadaten, die durchaus sinnvoll sind. Metadaten sind in Teedy leider noch nicht durchsuchbar! Außerdem kann die Daten jeder sehen, der auch das Dokument sehen kann. Wir nutzen die folgenden Metadatensätze: id - string die ID für das Mapping zwischen dem Inventar-Aufkleber und things.fablabchemnitz.de. Jedes Inventar-Objekt erhält eine einzigartige id. Die id wird manuell beim Anlegen vergeben, könnte jedoch auch automatisch vorgeschlagen werden (per SQL-Script Ausgabe). Weitere Details siehe "Verknüpfung digitale Inventarverwaltung mit reeller Nutzung - vom Etikettenschild zum Objekt (Barcodes scannen)" Gewicht (kg) - float EAN - string Nennleistung (W) - float hier wird der grobe Richtwert für die übliche/durchschnittliche oder maximale Leistungsaufnahme/Stromverbrauch angegeben. Der hier eingetragene Wert stammt entweder vom Datenblatt oder wurde manuell mit dem Messgerät erfasst. Die Werte dienen letztlich und einzig der groben Einordnung des Verbrauchers in gewisse Verbrauchsklassen. Insbesondere mit Nutzungsstatistiken kann damit beispielsweise der jährliche Stromverbrauch oder auch der notwendige physische Stromanschluss pro Werkstattbereich hochgerechnet bzw. validiert werden Eigentümer - string Eigentümer vs Besitzer - was ist der Unterschied? Ein Besitzer ist derjenige, in dessen Einflussbereich sich die Sache befindet und der deshalb auf sie zugreifen kann. Abgrenzung: Besitz ist eine Tatsache, Eigentum dagegen ist das Recht an einer Sache. Oft hat der Eigentümer seine Sache selbst. Dann ist er zugleich Besitzer. Pate/Ansprechpartner - string Letzte DGUV-3 Prüfung - date Seriennummer - string Anschaffungswert (€) Brutto - float das ist u.U. eine sensible Angabe, die für die Buchhaltung relevant ist (Abschreibungen) Verfügungsnachweis - string Rechnungsnummer/Beschaffungsnachweis (hier ggf. https-Link zum Buchhalterischen Dokument). Entweder ist der Gegenstand ein ... ... regulärer Kauf vom Verein → dann existiert ein Kaufbeleg im Buchhaltungs-DMS, oder ... Bereitstellung durch Dritte → dann existiert im Buchhaltungs-DMS ein Leihvertrag zur Dauerleihe oder zeitlich beschränkten Leihe ... geschenkter Gegenstand / Fundgegenstand / kostenfreie Überlassung → Im Idealfall gibt es eine Schenkungsurkunde oder einen Verschrottungsnachweis CE Kennzeichnung - boolean hat das Gerät ein CE Siegel oder anderweitigen Nachweis? Handbuchkiste - boolean Die Dokumentation des Objekts befindet sich in der für den Raum ausgewiesenen Handbuchkiste Ansprechpartner/Paten und Eigentümer - Warum als Metadatensatz und nicht als Tag? Ein paar Gedanken dazu ... Teilweise haben Leute Sachen zusammen gekauft. Tagging geht, aber "müllt" das Objekt mit langen Tag-Namen schnell zu. Alternativ ließen sich statt den vollen Personennamen auch nur Mitgliedsnummern als Tags eintragen. Das wäre datenschutztechnisch einfach lösbar. Es ist besser, wenn der Eigentümer nicht für jeden direkt filterbar ist. Es spielt auch keine direkte Rolle nach einem Nutzer filtern zu können. Siehe auch "Tätitgkeiten, Merkmale und Nutzersichtweisen" Pate/Ansprechpartner könnte auch eine Gruppe/Team sein! Dann müsste hierfür jeweils ein neuer Tag angelegt werde Nicht eingpflegt, sondern nur im Volltext stehend, sind Angaben wie Außendurchmesser, Innendurchmesser, Höhe, Breite, Länge, Tiefe, Dicke. Diese Angaben sind häufig nicht eindeutig, da man nicht genau weiß, ob sie sich auf die Aufbewahrungsbox des Werkzeugs, das Werkzeug selbst oder ein Teil des Werkzeuges beziehen. Praktisch ist der Aufwand viel zu hoch diese Daten penibel einzugeben. Allerdings ließen sich mit diesen Angaben unter Umständen Dichten, Traglasten und Stapelvolumina ausrechnen. Historie Aus allen Änderungen an den Objekten ergibt sich eine fortlaufende Historie (Audit). Diese ist auf der Startseite zu finden Das Kernstück ist die Suchfunktion Die Suche ist Teedy ermöglicht das schnelle Auffinden von Objekten. Dazu ist es empfehlenswert sich mit dem Suchsyntax auseinanderzusetzen. Details finden sich unter Searching and Tags. Je besser verstanden wird, wie man mit Tags, Wildcards und OCR-Suche umgeht, desto exakter werden die Ergebnisse.  Ein Wunsch-Feature, welches Teedy in Sachen Suchfunktion aufwerten würde (und noch nicht programmiert wurde), wäre die Verschlagwortung mit Synonymen und Übersetzungen. Eine Schachtel könnte man z.B. auch unter Stichworten wie Behälter, Box, Container, Etui, Dose, Kasten, Koffer, Kiste, Satz, Set, Schatulle, Tasche, etc. suchen. Wer nach "Bohren" sucht, könnte auch nach "drill" oder "Aufbohren" suchen. Im aktuellen Fall findet er dann jedoch nur  Ergebnisse für "Bohren", obwohl die Tätigkeit diesselbe ist. Aufgeklappte Suchmaske Löschen (digitales "Entsorgen") von Objekten In einer Werkstatt muss auch mal aufgeräumt werden. Vollschrott, abgebrochene Schraubendreher oder ein auseinandergefallener Heißluftföhn müssen entsorgt werden. Entsprechend sollte auch das Inventarsystem aufgeräumt werden. Es gibt zwei Möglichkeiten, wie damit umgegangen werden kann: das Objekt taggen mit dem einem geeigneten Stichwort wie "entsorgt" ergänzen und dabei Tags löschen wie Werkstattbereich und Prüfintervall. Löschen des Objekts (vergebenene Metadaten wie z.B. das als einzigartig im System vergebene Feld "id" bleiben dabei erhalten) Wir nutzen die letztere Variante und löschen das Objekt. In Grafana wird dann angezeigt, was und wann es gelöscht wurde. So kann die Historie der Entsorgung noch prinzipiell nachvollzogen werden. Leider gibt es in Teedy noch kein Papierkorb-Feature, um Dokumente darin abzulegen. Das wäre eine gute Alternative für den Fall, dass etwas wiederhergestellt werden muss, um nachträglich Apsekte nachzuvollziehen. Gastzugang und spezielle Anpassungen (serverseitiger Code/Scripts) Verschiedene Anpassungen finden sich unter Gastzugang und spezielle Anpassungen (serverseitiger Code/Scripts). Wie kann ich das Inventarsystem des FabLabs mitgestalten? Durch Aktivwerden an Dingen wie: Dateien besser benennen. Beim Einpflegen wurden tausende Dateien migriert und wünschen sich etwas mehr Ordnung Inventar kann um Kommentare, Bilder, Handbücher, Metadaten ergänzt werden generell kann jedem Inventargegenstand ein geeigneter Spitzname (einzigartig) vergeben werden. Gegenstände mit Namen fühlen sich besser an. Schreibrechte Die Pflege des Systems ist allen Administratoren und Editoren möglich. Hauptsächlich sollen Werkstattbereichsverantwortliche das System betreuen. Anonyme Nutzer Daten von anonymen Gästen und allgemein geteilten Accounts werden automatisch durch Server-Scripts bereinigt (gelöscht; Erklärung siehe oben). Bitte legt also keine Daten an, wenn ihr kein zugewiesener Editor oder Administrator für die DMS-Instanz seid! Metadaten, Beschreibungen, Share-Links werden entfernt, wenn sie vom falschen Benutzer aus angelegt wurden. Es wäre schade, wenn sich jemand viel Mühe macht, dies aber aus Versehen mit dem falschen Account tut. Achtung beim Löschen und Modifizieren von Tags Wie beschrieben definieren Tags die Sichtbarkeiten und die Suchfunktion. Tags sollten nicht leichtfertig umdefiniert oder gelöscht werden. Umbenennen oder Hinzufügen- wie Löschen von Berechtigungen sollten abgesprochen werden. Grafana Dashboards Die Daten aus Teedy können noch weiter aufbereitet werden und viele Informationen aus der Teedy-Datenbank (PostgreSQL) können in anderen Zusammenhängen dargestellt werden. So gibt es verschiedene Dashboard Views, die zusätzliche Dinge anzeigen, zum Beispiel Eine Übersicht über die neuesten Kommentare Geräte, die Nutzungsgebühren haben (etwas kosten) Objekte, die repariert werden sollten oder unkomplett sind (fehlende Teile) Übersicht, welche Maschinen speziellen Nutzungsberechtigungen unterliegen (Führerscheine) Gegenstände, die noch nicht vollständig eingepflegt worden sind, z.B. die noch keinen festgelegten Ort haben (jedes Objekt sollte seinen festen Platz haben) Anzeige des neuesten Inventars Übersicht über die vorhandene Literatur DGUV-3 Prüfliste (Liste der Geräte, die elektrisch auf Betriebssicherheit untersucht und protokolliert werden sollten) Verknüpfung digitale Inventarverwaltung mit reeller Nutzung - vom Etikettenschild zum Objekt (Barcodes scannen) Da jedes Objekt im FabLab seine eigene eindeutige Inventarnummer und zugewiesene Eigenschaften bekommt, ist auch jedes Objekt nachvollziehbar in seiner kompletten Historie und Beschaffenheit. Als physischer Gegenpart zur Software steht deshalb als Aufgabe das Labeln der Objekte mit Inventaraufklebern.  Die Gegenstände im FabLab Chemnitz erhalten nach und nach alle einen Inventaraufkleber zur Identifizierung. Ein Etikett dient dazu, das Objekt in Teedy anzuzeigen, indem es per Barcode Scanner gescannt wird. Dieser Barcode enthält eine eindeutige URL, die direkt im Browser aufgerufen wird. Dadurch kann der Nutzer schnell herausfinden, um welches Objekt es sich genau handelt (Fotos, Handbücher, Eigenschaften). Falls eine Sache aus mehreren Objekten zusammengesetzt ist (z.B. Gerät mit Fernbedienung), dann sollte möglichst jedes Einzelobjekt etikettiert werden, um die Zusammengehörigkeit wieder herstellen zu können. Inbesondere, wenn Unordentlichkeit herrscht und die Werkzeuge unsortier in der Werkstatt rumliegen. Der Ablauf: Siehe Vom Etikettenschild zum Objekt Wir nutzen sichtbare und menschenlesbare QR Codes. Theoretisch und praktisch ist jedoch auch das Labeln/Taggen mit RFID oder NFC Chips möglich! Die gleichen Daten können sehr einfach auf diese Medien bespielt werden. Weiterführendes Viele andere kluge Köpfe arbeiten an ähnlichen Werkstattkonzepten für eine sinnvolle Benutzbarkeit ihrer Werkstätten. Ein interessantes Projekt für Werkstatorganisation ist das "OpenSource-System für Sauberkeit und Ordnung" osSso.