This file is a merged representation of the entire codebase, combining all repository files into a single document.
Generated by Repomix on: 2024-12-09T06:29:51.427Z
================================================================
File Summary
================================================================
Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Repository structure
4. Multiple file entries, each consisting of:
a. A separator line (================)
b. The file path (File: path/to/file)
c. Another separator line
d. The full contents of the file
e. A blank line
Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
original repository files, not this packed version.
- When processing this file, use the file path to distinguish
between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
the same level of security as you would the original repository.
Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's
configuration.
- Binary files are not included in this packed representation. Please refer to
the Repository Structure section for a complete list of file paths, including
binary files.
Additional Info:
----------------
For more information about Repomix, visit: https://github.com/yamadashy/repomix
================================================================
Repository Structure
================================================================
.dockerignore
.env.example
.gitignore
biome.json
components.json
docker/build.sh
docker/caddy/Caddyfile
docker/compose.yml
docker/deploy.sh
docker/images/.gitattributes
docker/save.sh
Dockerfile
drizzle.config.ts
drizzle/0000_overjoyed_strong_guy.sql
drizzle/meta/_journal.json
drizzle/meta/0000_snapshot.json
next.config.mjs
package.json
postcss.config.mjs
public/next.svg
public/vercel.svg
README.md
scripts/generate-data.js
src/app/admin/about/page.tsx
src/app/admin/admin-sidebar.tsx
src/app/admin/charts/printer-error-chart.tsx
src/app/admin/charts/printer-error-rate.tsx
src/app/admin/charts/printer-forecast.tsx
src/app/admin/charts/printer-utilization.tsx
src/app/admin/charts/printer-volume.tsx
src/app/admin/jobs/page.tsx
src/app/admin/layout.tsx
src/app/admin/page.tsx
src/app/admin/printers/columns.tsx
src/app/admin/printers/data-table.tsx
src/app/admin/printers/dialogs/create-printer.tsx
src/app/admin/printers/dialogs/delete-printer.tsx
src/app/admin/printers/dialogs/edit-printer.tsx
src/app/admin/printers/form.tsx
src/app/admin/printers/page.tsx
src/app/admin/settings/download/route.ts
src/app/admin/settings/page.tsx
src/app/admin/users/columns.tsx
src/app/admin/users/data-table.tsx
src/app/admin/users/dialog.tsx
src/app/admin/users/form.tsx
src/app/admin/users/page.tsx
src/app/api/job/[jobId]/remaining-time/route.ts
src/app/api/printers/route.ts
src/app/auth/login/callback/route.ts
src/app/auth/login/route.ts
src/app/globals.css
src/app/job/[jobId]/cancel-form.tsx
src/app/job/[jobId]/edit-comments.tsx
src/app/job/[jobId]/extend-form.tsx
src/app/job/[jobId]/finish-form.tsx
src/app/job/[jobId]/page.tsx
src/app/layout.tsx
src/app/my/jobs/columns.tsx
src/app/my/jobs/data-table.tsx
src/app/my/profile/page.tsx
src/app/not-found.tsx
src/app/page.tsx
src/app/printer/[printerId]/reserve/form.tsx
src/app/printer/[printerId]/reserve/page.tsx
src/components/data-card.tsx
src/components/dynamic-printer-cards.tsx
src/components/header/index.tsx
src/components/header/navigation.tsx
src/components/login-button.tsx
src/components/logout-button.tsx
src/components/personalized-cards.tsx
src/components/printer-availability-badge.tsx
src/components/printer-card/countdown.tsx
src/components/printer-card/index.tsx
src/components/ui/alert-dialog.tsx
src/components/ui/alert.tsx
src/components/ui/avatar.tsx
src/components/ui/badge.tsx
src/components/ui/breadcrumb.tsx
src/components/ui/button.tsx
src/components/ui/card.tsx
src/components/ui/chart.tsx
src/components/ui/dialog.tsx
src/components/ui/dropdown-menu.tsx
src/components/ui/form.tsx
src/components/ui/hover-card.tsx
src/components/ui/input.tsx
src/components/ui/label.tsx
src/components/ui/scroll-area.tsx
src/components/ui/select.tsx
src/components/ui/skeleton.tsx
src/components/ui/sonner.tsx
src/components/ui/table.tsx
src/components/ui/tabs.tsx
src/components/ui/textarea.tsx
src/components/ui/toast.tsx
src/components/ui/toaster.tsx
src/components/ui/use-toast.ts
src/server/actions/authentication/logout.ts
src/server/actions/printers.ts
src/server/actions/printJobs.ts
src/server/actions/timer.ts
src/server/actions/user/delete.ts
src/server/actions/user/update.ts
src/server/actions/users.ts
src/server/auth/index.ts
src/server/auth/oauth.ts
src/server/auth/permissions.ts
src/utils/analytics/error-rate.ts
src/utils/analytics/errors.ts
src/utils/analytics/forecast.ts
src/utils/analytics/utilization.ts
src/utils/analytics/volume.ts
src/utils/drizzle.ts
src/utils/errors.ts
src/utils/fetch.ts
src/utils/guard.ts
src/utils/printers.ts
src/utils/strings.ts
src/utils/styles.ts
tailwind.config.ts
tsconfig.json
================================================================
Repository Files
================================================================
================
File: .dockerignore
================
# Build and utility assets
docker/
scripts/
# Ignore node_modules as they will be installed in the container
node_modules
# Ignore build artifacts
.next
# Ignore runtime data
db/
# Ignore local configuration files
.env
.env.example
# Ignore version control files
.git
.gitignore
# Ignore IDE/editor specific files
*.log
*.tmp
*.DS_Store
.vscode/
.idea/
================
File: .env.example
================
# OAuth Configuration
OAUTH_CLIENT_ID=client_id
OAUTH_CLIENT_SECRET=client_secret
================
File: .gitignore
================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# db folder
db/
# Env file
.env
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================
File: biome.json
================
{
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"lineWidth": 120
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedImports": "error"
}
}
}
}
================
File: components.json
================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/utils/styles"
}
}
================
File: docker/build.sh
================
#!/bin/bash
# Define image name
MYP_RP_IMAGE_NAME="myp-rp"
# Function to build Docker image
build_image() {
local image_name=$1
local dockerfile=$2
local platform=$3
echo "Building $image_name Docker image for $platform..."
docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load .
if [ $? -eq 0 ]; then
echo "$image_name Docker image built successfully"
else
echo "Error occurred while building $image_name Docker image"
exit 1
fi
}
# Create and use a builder instance (if not already created)
BUILDER_NAME="myp-rp-arm64-builder"
docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME
# Build myp-rp image
build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64"
# Remove the builder instance
docker buildx rm $BUILDER_NAME
================
File: docker/caddy/Caddyfile
================
{
debug
}
m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net {
reverse_proxy myp-rp:3000
tls internal
}
================
File: docker/compose.yml
================
services:
caddy:
image: caddy:2.8
container_name: caddy
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- ./caddy/data:/data
- ./caddy/config:/config
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
myp-rp:
image: myp-rp:latest
container_name: myp-rp
env_file: "/srv/myp-env/github.env"
volumes:
- /srv/MYP-DB:/usr/src/app/db
restart: unless-stopped
================
File: docker/deploy.sh
================
#!/bin/bash
# Directory containing the Docker images
IMAGE_DIR="docker/images"
# Load all Docker images from the tar.xz files in the IMAGE_DIR
echo "Loading Docker images from $IMAGE_DIR..."
for image_file in "$IMAGE_DIR"/*.tar.xz; do
if [ -f "$image_file" ]; then
echo "Loading Docker image from $image_file..."
docker load -i "$image_file"
# Check if the image loading was successful
if [ $? -ne 0 ]; then
echo "Error occurred while loading Docker image from $image_file"
exit 1
fi
else
echo "No Docker image tar.xz files found in $IMAGE_DIR."
fi
done
# Execute docker compose
echo "Running docker compose..."
docker compose -f "docker/compose.yml" up -d
# Check if the operation was successful
if [ $? -eq 0 ]; then
echo "Docker compose executed successfully"
else
echo "Error occurred while executing docker compose"
exit 1
fi
echo "Deployment completed successfully"
================
File: docker/images/.gitattributes
================
caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text
myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text
================
File: docker/save.sh
================
#!/bin/bash
# Get image name as argument
IMAGE_NAME=$1
PLATFORM="linux/arm64"
# Define paths
IMAGE_DIR="docker/images"
IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar"
COMPRESSED_FILE="${IMAGE_FILE}.xz"
# Function to pull the image
pull_image() {
local image=$1
if [[ $image == arm64v8/* ]]; then
echo "Pulling image $image without platform specification..."
docker pull $image
else
echo "Pulling image $image for platform $PLATFORM..."
docker pull --platform $PLATFORM $image
fi
return $?
}
# Pull the image if it is not available locally
if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then
if pull_image ${IMAGE_NAME}; then
echo "Image $IMAGE_NAME pulled successfully."
else
echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM"
echo "Trying to pull $IMAGE_NAME without platform specification..."
# Attempt to pull again without platform
if pull_image ${IMAGE_NAME}; then
echo "Image $IMAGE_NAME pulled successfully without platform."
else
echo "Error occurred while pulling $IMAGE_NAME without platform."
echo "Trying to pull arm64v8/${IMAGE_NAME} instead..."
# Construct new image name
NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}"
if pull_image ${NEW_IMAGE_NAME}; then
echo "Image $NEW_IMAGE_NAME pulled successfully."
IMAGE_NAME=${NEW_IMAGE_NAME} # Update IMAGE_NAME to use the new one
else
echo "Error occurred while pulling $NEW_IMAGE_NAME"
exit 1
fi
fi
fi
else
echo "Image $IMAGE_NAME found locally. Skipping pull."
fi
# Save the Docker image
echo "Saving $IMAGE_NAME Docker image..."
docker save ${IMAGE_NAME} > $IMAGE_FILE
# Compress the Docker image (overwriting if file exists)
echo "Compressing $IMAGE_FILE..."
xz -z --force $IMAGE_FILE
if [ $? -eq 0 ]; then
echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE"
else
echo "Error occurred while compressing $IMAGE_NAME Docker image"
exit 1
fi
================
File: Dockerfile
================
FROM node:20-bookworm-slim
# Create application directory
RUN mkdir -p /usr/src/app
# Set environment variables
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
WORKDIR /usr/src/app
# Copy package.json and pnpm-lock.yaml
COPY package.json /usr/src/app
COPY pnpm-lock.yaml /usr/src/app
# Install pnpm
RUN corepack enable pnpm
# Install dependencies
RUN pnpm install
# Copy the rest of the application code
COPY . /usr/src/app
# Initialize Database, if it not already exists
RUN pnpm run db
# Build the application
RUN pnpm run build
EXPOSE 3000
# Start the application
CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"]
================
File: drizzle.config.ts
================
import { defineConfig } from "drizzle-kit";
//@ts-ignore - better-sqlite driver throws an error even though its an valid value
export default defineConfig({
dialect: "sqlite",
schema: "./src/server/db/schema.ts",
out: "./drizzle",
driver: "libsql",
dbCredentials: {
url: "file:./db/sqlite.db",
},
});
================
File: drizzle/0000_overjoyed_strong_guy.sql
================
CREATE TABLE `printJob` (
`id` text PRIMARY KEY NOT NULL,
`printerId` text NOT NULL,
`userId` text NOT NULL,
`startAt` integer NOT NULL,
`durationInMinutes` integer NOT NULL,
`comments` text,
`aborted` integer DEFAULT false NOT NULL,
`abortReason` text,
FOREIGN KEY (`printerId`) REFERENCES `printer`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `printer` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`description` text NOT NULL,
`status` integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`github_id` integer NOT NULL,
`name` text,
`displayName` text,
`email` text NOT NULL,
`role` text DEFAULT 'guest'
);
================
File: drizzle/meta/_journal.json
================
{
"version": "6",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1715416514336,
"tag": "0000_overjoyed_strong_guy",
"breakpoints": true
}
]
}
================
File: drizzle/meta/0000_snapshot.json
================
{
"version": "6",
"dialect": "sqlite",
"id": "791dc197-5254-4432-bd9f-1368d1a5aa6a",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"printJob": {
"name": "printJob",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"printerId": {
"name": "printerId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"startAt": {
"name": "startAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"durationInMinutes": {
"name": "durationInMinutes",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"comments": {
"name": "comments",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"aborted": {
"name": "aborted",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"abortReason": {
"name": "abortReason",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"printJob_printerId_printer_id_fk": {
"name": "printJob_printerId_printer_id_fk",
"tableFrom": "printJob",
"tableTo": "printer",
"columnsFrom": [
"printerId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"printJob_userId_user_id_fk": {
"name": "printJob_userId_user_id_fk",
"tableFrom": "printJob",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"printer": {
"name": "printer",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"github_id": {
"name": "github_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"displayName": {
"name": "displayName",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'guest'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
================
File: next.config.mjs
================
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "m040tbaraspi001.de040.corpintra.net",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
},
};
export default nextConfig;
================
File: package.json
================
{
"name": "myp-rp",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.12.1",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:create-default": "mkdir -p db/",
"db:generate-sqlite": "pnpm drizzle-kit generate",
"db:clean": "rm -rf db/ drizzle/",
"db:migrate": "pnpm drizzle-kit migrate",
"db": "pnpm db:create-default && pnpm db:generate-sqlite && pnpm db:migrate",
"db:reset": "pnpm db:clean && pnpm db"
},
"dependencies": {
"@faker-js/faker": "^9.2.0",
"@headlessui/react": "^2.1.10",
"@headlessui/tailwindcss": "^0.2.1",
"@hookform/resolvers": "^3.9.0",
"@libsql/client": "^0.14.0",
"@lucia-auth/adapter-drizzle": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@remixicon/react": "^4.3.0",
"@tanstack/react-table": "^8.20.5",
"@tremor/react": "^3.18.3",
"arctic": "^1.9.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.30.10",
"lodash": "^4.17.21",
"lucia": "^3.2.1",
"lucide-react": "^0.378.0",
"luxon": "^3.5.0",
"next": "14.2.3",
"next-themes": "^0.3.0",
"oslo": "^1.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-if": "^4.1.5",
"react-timer-hook": "^3.0.7",
"recharts": "^2.13.3",
"regression": "^2.0.1",
"sonner": "^1.5.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"use-debounce": "^10.0.3",
"uuid": "^11.0.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "^1.9.3",
"@tailwindcss/forms": "^0.5.9",
"@types/lodash": "^4.17.13",
"@types/luxon": "^3.4.2",
"@types/node": "^20.16.11",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"drizzle-kit": "^0.21.4",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}
================
File: postcss.config.mjs
================
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
================
File: public/next.svg
================
================
File: public/vercel.svg
================
================
File: README.md
================
# MYP - Manage Your Printer
MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern.
Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.
## Deployment
### Voraussetzungen
- Netzwerk auf Raspberry Pi ist eingerichtet
- Docker ist installiert
### Schritte
1. Docker-Container bauen (docker/build.sh)
2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest)
3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh)
## Entwicklerinformationen
### Raspberry Pi Einstellungen
Auf dem Raspberry Pi wurde Raspbian Lite installiert.
Unter /srv/* sind die Projektdateien zu finden.
### Anmeldedaten
```
Benutzer: myp
Passwort: (persönlich bekannt)
```
================
File: scripts/generate-data.js
================
const sqlite3 = require("sqlite3");
const faker = require("@faker-js/faker").faker;
const { random, sample, sampleSize, sum } = require("lodash");
const { DateTime } = require("luxon");
const { open } = require("sqlite");
const { v4: uuidv4 } = require("uuid");
const dbPath = "./db/sqlite.db";
// Configuration for test data generation
let startDate = DateTime.fromISO("2024-10-08");
let endDate = DateTime.fromISO("2024-11-08");
let numberOfPrinters = 5;
// Use weekday names for better readability and ease of setting trends
let avgPrintTimesPerDay = {
Monday: 4,
Tuesday: 2,
Wednesday: 5,
Thursday: 2,
Friday: 3,
Saturday: 0,
Sunday: 0,
}; // Average number of prints for each weekday
let avgPrintDurationPerDay = {
Monday: 240, // Total average duration in minutes for Monday
Tuesday: 30,
Wednesday: 45,
Thursday: 40,
Friday: 120,
Saturday: 0,
Sunday: 0,
}; // Average total duration of prints for each weekday
let printerUsage = {
"Drucker 1": 0.5,
"Drucker 2": 0.7,
"Drucker 3": 0.6,
"Drucker 4": 0.3,
"Drucker 5": 0.4,
}; // Usage percentages for each printer
// **New Configurations for Error Rates**
let generalErrorRate = 0.05; // 5% chance any print job may fail
let printerErrorRates = {
"Drucker 1": 0.02, // 2% error rate for Printer 1
"Drucker 2": 0.03,
"Drucker 3": 0.01,
"Drucker 4": 0.05,
"Drucker 5": 0.04,
}; // Error rates for each printer
const holidays = []; // Example holidays
const existingJobs = [];
const initDB = async () => {
console.log("Initializing database connection...");
return open({
filename: dbPath,
driver: sqlite3.Database,
});
};
const createUser = (isPowerUser = false) => {
const name = [faker.person.firstName(), faker.person.lastName()];
const user = {
id: uuidv4(),
github_id: faker.number.int(),
username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(),
displayName: `${name[0]} ${name[1]}`.toUpperCase(),
email: `${name[0]}.${name[1]}@example.com`,
role: sample(["user", "admin"]),
isPowerUser,
};
console.log("Created user:", user);
return user;
};
const createPrinter = (index) => {
const printer = {
id: uuidv4(),
name: `Drucker ${index}`,
description: faker.lorem.sentence(),
status: random(0, 2),
};
console.log("Created printer:", printer);
return printer;
};
const isPrinterAvailable = (printer, startAt, duration) => {
const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds
return !existingJobs.some((job) => {
const jobStart = job.startAt;
const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000;
return (
printer.id === job.printerId &&
((startAt >= jobStart && startAt < jobEnd) ||
(endAt > jobStart && endAt <= jobEnd) ||
(startAt <= jobStart && endAt >= jobEnd))
);
});
};
const createPrintJob = (users, printers, startAt, duration) => {
const user = sample(users);
let printer;
// Weighted selection based on printer usage
const printerNames = Object.keys(printerUsage);
const weightedPrinters = printers.filter((p) => printerNames.includes(p.name));
// Create a weighted array of printers based on usage percentages
const printerWeights = weightedPrinters.map((p) => ({
printer: p,
weight: printerUsage[p.name],
}));
const totalWeight = sum(printerWeights.map((pw) => pw.weight));
const randomWeight = Math.random() * totalWeight;
let accumulatedWeight = 0;
for (const pw of printerWeights) {
accumulatedWeight += pw.weight;
if (randomWeight <= accumulatedWeight) {
printer = pw.printer;
break;
}
}
if (!printer) {
printer = sample(printers);
}
if (!isPrinterAvailable(printer, startAt, duration)) {
console.log("Printer not available, skipping job creation.");
return null;
}
// **Determine if the job should be aborted based on error rates**
let aborted = false;
let abortReason = null;
// Calculate the combined error rate
const printerErrorRate = printerErrorRates[printer.name] || 0;
const combinedErrorRate = 1 - (1 - generalErrorRate) * (1 - printerErrorRate);
if (Math.random() < combinedErrorRate) {
aborted = true;
const errorMessages = [
"Unbekannt",
"Keine Ahnung",
"Falsch gebucht",
"Filament gelöst",
"Druckabbruch",
"Düsenverstopfung",
"Schichthaftung fehlgeschlagen",
"Materialmangel",
"Dateifehler",
"Temperaturproblem",
"Mechanischer Fehler",
"Softwarefehler",
"Kalibrierungsfehler",
"Überhitzung",
];
abortReason = sample(errorMessages); // Generate a random abort reason
}
const printJob = {
id: uuidv4(),
printerId: printer.id,
userId: user.id,
startAt,
durationInMinutes: duration,
comments: faker.lorem.sentence(),
aborted,
abortReason,
};
console.log("Created print job:", printJob);
return printJob;
};
const generatePrintJobsForDay = async (users, printers, dayDate, totalJobsForDay, totalDurationForDay, db, dryRun) => {
console.log(`Generating print jobs for ${dayDate.toISODate()}...`);
// Generate random durations that sum up approximately to totalDurationForDay
const durations = [];
let remainingDuration = totalDurationForDay;
for (let i = 0; i < totalJobsForDay; i++) {
const avgJobDuration = remainingDuration / (totalJobsForDay - i);
const jobDuration = Math.max(
Math.round(random(avgJobDuration * 0.8, avgJobDuration * 1.2)),
5, // Minimum duration of 5 minutes
);
durations.push(jobDuration);
remainingDuration -= jobDuration;
}
// Shuffle durations to randomize job lengths
const shuffledDurations = sampleSize(durations, durations.length);
for (let i = 0; i < totalJobsForDay; i++) {
const duration = shuffledDurations[i];
// Random start time between 8 AM and 6 PM, adjusted to avoid overlapping durations
const possibleStartHours = Array.from({ length: 10 }, (_, idx) => idx + 8); // 8 AM to 6 PM
let startAt;
let attempts = 0;
do {
const hour = sample(possibleStartHours);
const minute = random(0, 59);
startAt = dayDate.set({ hour, minute, second: 0, millisecond: 0 }).toMillis();
attempts++;
if (attempts > 10) {
console.log("Unable to find available time slot, skipping job.");
break;
}
} while (!isPrinterAvailable(sample(printers), startAt, duration));
if (attempts > 10) continue;
const printJob = createPrintJob(users, printers, startAt, duration);
if (printJob) {
if (!dryRun) {
await db.run(
`INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
printJob.id,
printJob.printerId,
printJob.userId,
printJob.startAt,
printJob.durationInMinutes,
printJob.comments,
printJob.aborted ? 1 : 0,
printJob.abortReason,
],
);
}
existingJobs.push(printJob);
console.log("Inserted print job into database:", printJob.id);
}
}
};
const generateTestData = async (dryRun = false) => {
console.log("Starting test data generation...");
const db = await initDB();
// Generate users and printers
const users = [
...Array.from({ length: 7 }, () => createUser(false)),
...Array.from({ length: 3 }, () => createUser(true)),
];
const printers = Array.from({ length: numberOfPrinters }, (_, index) => createPrinter(index + 1));
if (!dryRun) {
// Insert users into the database
for (const user of users) {
await db.run(
`INSERT INTO user (id, github_id, name, displayName, email, role)
VALUES (?, ?, ?, ?, ?, ?)`,
[user.id, user.github_id, user.username, user.displayName, user.email, user.role],
);
console.log("Inserted user into database:", user.id);
}
// Insert printers into the database
for (const printer of printers) {
await db.run(
`INSERT INTO printer (id, name, description, status)
VALUES (?, ?, ?, ?)`,
[printer.id, printer.name, printer.description, printer.status],
);
console.log("Inserted printer into database:", printer.id);
}
}
// Generate print jobs for each day within the specified date range
let currentDay = startDate;
while (currentDay <= endDate) {
const weekdayName = currentDay.toFormat("EEEE"); // Get weekday name (e.g., 'Monday')
if (holidays.includes(currentDay.toISODate()) || avgPrintTimesPerDay[weekdayName] === 0) {
console.log(`Skipping holiday or no jobs scheduled: ${currentDay.toISODate()}`);
currentDay = currentDay.plus({ days: 1 });
continue;
}
const totalJobsForDay = avgPrintTimesPerDay[weekdayName];
const totalDurationForDay = avgPrintDurationPerDay[weekdayName];
await generatePrintJobsForDay(users, printers, currentDay, totalJobsForDay, totalDurationForDay, db, dryRun);
currentDay = currentDay.plus({ days: 1 });
}
if (!dryRun) {
await db.close();
console.log("Database connection closed. Test data generation complete.");
} else {
console.log("Dry run complete. No data was written to the database.");
}
};
const setConfigurations = (config) => {
if (config.startDate) startDate = DateTime.fromISO(config.startDate);
if (config.endDate) endDate = DateTime.fromISO(config.endDate);
if (config.numberOfPrinters) numberOfPrinters = config.numberOfPrinters;
if (config.avgPrintTimesPerDay) avgPrintTimesPerDay = config.avgPrintTimesPerDay;
if (config.avgPrintDurationPerDay) avgPrintDurationPerDay = config.avgPrintDurationPerDay;
if (config.printerUsage) printerUsage = config.printerUsage;
if (config.generalErrorRate !== undefined) generalErrorRate = config.generalErrorRate;
if (config.printerErrorRates) printerErrorRates = config.printerErrorRates;
};
// Example usage
setConfigurations({
startDate: "2024-10-08",
endDate: "2024-11-08",
numberOfPrinters: 6,
avgPrintTimesPerDay: {
Monday: 4, // High usage
Tuesday: 2, // Low usage
Wednesday: 3, // Low usage
Thursday: 2, // Low usage
Friday: 8, // High usage
Saturday: 0,
Sunday: 0,
},
avgPrintDurationPerDay: {
Monday: 300, // High total duration
Tuesday: 60, // Low total duration
Wednesday: 90,
Thursday: 60,
Friday: 240,
Saturday: 0,
Sunday: 0,
},
printerUsage: {
"Drucker 1": 2.3,
"Drucker 2": 1.7,
"Drucker 3": 0.1,
"Drucker 4": 1.5,
"Drucker 5": 2.4,
"Drucker 6": 0.3,
"Drucker 7": 0.9,
"Drucker 8": 0.1,
},
generalErrorRate: 0.05, // 5% general error rate
printerErrorRates: {
"Drucker 1": 0.02,
"Drucker 2": 0.03,
"Drucker 3": 0.1,
"Drucker 4": 0.05,
"Drucker 5": 0.04,
"Drucker 6": 0.02,
"Drucker 7": 0.01,
"PrinteDrucker 8": 0.03,
},
});
generateTestData(process.argv.includes("--dry-run"))
.then(() => {
console.log("Test data generation script finished.");
})
.catch((err) => {
console.error("Error generating test data:", err);
});
================
File: src/app/admin/about/page.tsx
================
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Über MYP",
};
export default async function AdminPage() {
return (
Über MYPMYP — Manage Your Printer
MYP ist eine Webanwendung zur Reservierung von 3D-Druckern. Sie wurde im Rahmen des
Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische
Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.