Compare commits

...

28 Commits

Author SHA1 Message Date
Patrick Plate 038e546963 feat: archive zoo_backup for home sync 2026-06-24 19:27:14 +02:00
Patrick Plate 02844e4c4a added doc 2026-06-22 11:38:09 +02:00
Lumen e18136baa4 Handover: homelab release tooling for Work Lumen (runbook + template + publish script) 2026-06-22 11:35:03 +02:00
Lumen 1603ddcd5c Homelab release tooling: runbook + port registry + homelab-publish.sh switch script 2026-06-22 11:34:20 +02:00
Patrick Plate ff58ec6add lumen-exchange: cannamanage public-hosting readiness + open auth blocker (for Work Lumen) 2026-06-18 21:47:22 +02:00
Patrick Plate fd84071489 chore(homelab): retire git-sync + mirror Gitea, git.plate-software.de now proxies single TrueNAS Gitea 2026-06-15 14:02:49 +02:00
Patrick Plate 67ed0edbe5 chore(homelab): preemptively mirror inspectflow.wiki via git-sync 2026-06-15 09:49:20 +02:00
Patrick Plate b7ef026dcd chore(homelab): add inspectflow to git-sync mirror repos 2026-06-15 09:46:57 +02:00
Patrick Plate 1a0a56a626 chore(java): consolidate mss-failsafe to single canonical copy
Replace the stale multi-module java/mss-failsafe skeleton (old user-management
prototype) with the active single-module machine-safety inspection app that was
living in its own standalone repo at the repo root.

- Remove old java/mss-failsafe/ multi-module tree (mss, userdata, userManagement,
  mssfailsafe.datalayer, mssfailsafeWeblayer) incl. committed build artifacts
- Add the active app (PrimeFaces 11 / JSF 2.3 / Hibernate 5.6 / iText / POI)
  flattened into java/mss-failsafe/ as the only mss-failsafe in git
- Working tree captured = master tip 2a142b5 + in-progress uncommitted work
  (incl. .github/*.instructions.md AI-context files)
- Archive the standalone repo's 33-commit history in GIT_HISTORY_ARCHIVE.md
  since its .git was not migrated

This is the source of truth / base for the upcoming upgraded rewrite.
2026-06-13 19:55:28 +02:00
Patrick Plate 7a573d7193 docs(lumen-exchange): CannaManage local Docker testing + Playwright e2e plan for Work Lumen
Why curl wasn't enough (hydration + missing auth token), the bug catalogue
(hydration crash, PWA middleware, consent principal bug, and the systemic
'frontend never sends the access token' issue), how to run the stack locally
on the Mac, and the concrete Playwright real-token e2e suite to implement.
2026-06-13 11:00:13 +02:00
Patrick Plate 57800f2518 chore(git-sync): add cannamanage.wiki to bidirectional sync repos 2026-06-12 08:55:45 +02:00
Patrick Plate 4f4372038c feat(lumen-exchange): 420cloud competitor analysis + git-sync bidirectional mirror
- Scraped 420cloud.io: feature matrix, AGB pricing analysis, Club Map legal risk
- Strategic recommendations for Sprint 4: compliance PDF, PWA, QR ID, federation
- Research agenda for Work Lumen (Amazon Q deep dive)
- Add plans/git-sync/ Docker container for IONOS→TrueNAS bidirectional sync
2026-06-12 08:52:36 +02:00
Patrick Plate 86d54a4f28 chore(homelab): update mcp.json after roo-zoo-semble zip 2026-06-11 10:17:55 +02:00
Patrick Plate aa9d877233 Merge branch 'chore/homelab/roo-zoo-semble-zip' 2026-06-11 10:17:33 +02:00
Patrick Plate 4760628661 chore(homelab): zip Zoo Code extension with Semble indexing for roo/zoo 2026-06-11 10:17:17 +02:00
Patrick Plate 31dc6d2174 docs(lumen-exchange): CannaManage code confirmed on IONOS + Work Lumen's code request 2026-06-11 09:59:21 +02:00
Patrick Plate e653d487a8 docs(lumen-exchange): request Sprint 1 code push to IONOS for Sprint 2 start 2026-06-11 09:57:38 +02:00
Patrick Plate 6936675af2 docs(lumen-exchange): merge reply and context update 2026-06-11 09:49:41 +02:00
Patrick Plate 49b0c8b285 docs(lumen-exchange): Homelab Lumen reply + update shared context with Work Lumen's eAU migration details 2026-06-11 09:49:41 +02:00
Patrick Plate e857c1b781 docs(lumen-exchange): first message from Work Lumen to Homelab Lumen 2026-06-11 09:40:55 +02:00
Patrick Plate 649096fc5b docs(lumen-exchange): merge initial exchange folder — README, hello note, shared context + open questions 2026-06-11 09:10:29 +02:00
Patrick Plate c8be9516a8 docs(lumen-exchange): create shared mailbox for two Lumen instances via git.plate-software.de 2026-06-11 09:10:29 +02:00
Patrick Plate c2f4c8fc39 chore(homelab): catchup push — roo modes, mcp servers, cannamanage docs, homelab plans 2026-06-11 09:02:28 +02:00
Patrick Plate 1859ccd1d6 chore(homelab): add homelab plans, frpc deploy script, odysseus workspace, heretic docs 2026-06-11 09:02:19 +02:00
Patrick Plate 17d14aae09 docs(cannamanage): update wiki pages and sprint plans + brand pipeline doc 2026-06-11 09:02:14 +02:00
Patrick Plate bf721c1379 feat(mcp): update bigmind/mcp-image-gen/webscraper servers; add image-gen batch scripts 2026-06-11 09:02:09 +02:00
Patrick Plate 0cb94122bf chore(roo): add pic-gen mode rules, update mcp.json and new-mcp-server skill 2026-06-11 09:01:58 +02:00
Patrick Plate 5692854ec4 fix(roo): merge anti-loop guardrails 2026-04-10 23:27:35 +02:00
683 changed files with 64569 additions and 8239 deletions
+10 -14
View File
@@ -13,8 +13,10 @@
"git_branch",
"git_create_branch",
"git_add",
"git_commit"
]
"git_commit",
"git_checkout"
],
"disabled": true
},
"filesystem": {
"command": "npx",
@@ -33,8 +35,9 @@
"src/server.py"
],
"alwaysAllow": [
"webscraper_fetch",
"webscraper_fetch_links"
"webscraper_fetch_links",
"webscraper_fetch_section",
"webscraper_fetch"
]
},
"gitea": {
@@ -47,15 +50,7 @@
"8bf0c734ebda3e61d9c9068489ce58a2bf8d33db"
],
"alwaysAllow": [
"create_issue",
"list_repo_issues",
"get_issue",
"edit_issue",
"create_issue_comment",
"create_pull_request",
"get_repository",
"list_my_repositories",
"create_wiki_page"
"*"
],
"disabled": true
},
@@ -90,7 +85,8 @@
"get_generation_status",
"get_output_directory",
"generate_image"
]
],
"timeout": 1800
}
}
}
+121
View File
@@ -0,0 +1,121 @@
<pic_gen_workflow>
<mode_overview>
Pic Gen mode generates AI images through the mcp-image-gen MCP server, which
drives ComfyUI locally. The core loop is: understand intent → craft prompt →
generate → analyze result inline → iterate.
</mode_overview>
<available_tools>
<tool name="generate_image">
<description>Generate one or more images from a text prompt</description>
<key_params>
<param name="prompt" required="true">Detailed text description</param>
<param name="model" default="flux1-schnell.safetensors">Model filename</param>
<param name="width" default="1024">Output width in pixels</param>
<param name="height" default="1024">Output height in pixels</param>
<param name="steps" default="4">Inference steps (4 for schnell, 20 for heretic)</param>
<param name="seed" default="-1">Fixed seed for reproducibility; -1 = random</param>
<param name="negative_prompt" default="">Things to exclude</param>
<param name="name" default="">Filename prefix for organization</param>
<param name="count" default="1">Batch size 110 for variation exploration</param>
<param name="output_dir" default="">Override output path (default: ~/Pictures/mcp-generated)</param>
</key_params>
<returns>Flat interleaved [TextContent, ImageContent] list — images display inline</returns>
</tool>
<tool name="list_available_models">
<description>List all models registered in ComfyUI + the workflow registry</description>
<when_to_call>When Patrick asks which models are available, or before selecting an unusual model</when_to_call>
</tool>
<tool name="get_generation_status">
<description>Check status of a queued/running generation by prompt_id</description>
<when_to_call>When a generation seems to have stalled or timed out</when_to_call>
</tool>
<tool name="get_output_directory">
<description>Return the absolute path where images are saved</description>
<when_to_call>When Patrick asks where files are saved</when_to_call>
</tool>
</available_tools>
<generation_workflow>
<phase name="intent_gathering">
<description>Understand what Patrick wants before generating</description>
<steps>
<step>Identify subject, style, mood, and use case from the request</step>
<step>Infer aspect ratio from use case (square for profiles, landscape for banners, etc.)</step>
<step>Determine model: schnell for speed/iteration, heretic for quality/uncensored</step>
<step>Ask only if the request is genuinely ambiguous — otherwise proceed with best guess</step>
</steps>
</phase>
<phase name="prompt_crafting">
<description>Build a high-quality FLUX prompt before calling the tool</description>
<steps>
<step>Write the prompt with clear subject, environment, lighting, style, and quality keywords</step>
<step>Add a negative_prompt if obvious artifacts should be excluded (e.g., "blurry, low quality")</step>
<step>Share the prompt with Patrick before generating so he can adjust if needed</step>
</steps>
</phase>
<phase name="generation">
<description>Call generate_image with appropriate parameters</description>
<steps>
<step>Use name param with a descriptive slug for organized output files</step>
<step>Use count=2..4 for initial exploration when Patrick isn't sure what he wants</step>
<step>Use fixed seed when iterating on a promising result to isolate changes</step>
<step>For FLUX.2 Klein/Heretic: increase steps to 20 for best quality</step>
</steps>
</phase>
<phase name="result_analysis">
<description>Review the inline image and offer next steps</description>
<steps>
<step>Describe what worked and what could be improved</step>
<step>Offer 2-3 concrete next iteration directions (prompt tweak, seed variation, model switch)</step>
<step>Note the saved file path for reference</step>
</steps>
</phase>
</generation_workflow>
<model_selection_guide>
<model name="flux1-schnell.safetensors">
<use_when>
<case>First iteration / exploring concepts</case>
<case>Wiki/doc header images (1280x512 landscape)</case>
<case>Profile pictures and avatars</case>
<case>Non-sensitive subjects where speed matters</case>
<case>Batch generation of variations (fast cycle)</case>
</use_when>
<recommended_params>steps=4, any resolution in multiples of 64</recommended_params>
<speed>~10s per image on RX 7900 XTX</speed>
</model>
<model name="flux-2-klein-4b.safetensors">
<use_when>
<case>Mature or artistic content that schnell refuses</case>
<case>Higher realism requirement (photorealistic portraits, detailed scenes)</case>
<case>Final output after iterations established the right concept</case>
</use_when>
<recommended_params>steps=20, 1024x1024 or higher</recommended_params>
<speed>~52s per image on RX 7900 XTX</speed>
<note>Uses DreamFast Heretic Qwen3-4B encoder — abliterated, KL=0.0</note>
</model>
</model_selection_guide>
<common_resolutions>
<resolution use_case="Profile picture / avatar">1024x1024</resolution>
<resolution use_case="Wiki / doc banner">1280x512</resolution>
<resolution use_case="Landscape wallpaper">1920x1088 (nearest 64-multiple to 1920x1080)</resolution>
<resolution use_case="Portrait / tall card">768x1024</resolution>
<resolution use_case="Wide cinema crop">1216x512</resolution>
</common_resolutions>
<completion_criteria>
<criterion>Image generated and displayed inline in chat</criterion>
<criterion>File path reported so Patrick can find it on disk</criterion>
<criterion>Seed reported so the result is reproducible</criterion>
<criterion>Next iteration options offered if result is not final</criterion>
</completion_criteria>
</pic_gen_workflow>
+141
View File
@@ -0,0 +1,141 @@
<prompting_guide>
<overview>
FLUX models (both schnell and FLUX.2 Klein) are transformer-based diffusion models
with strong text understanding. They respond better to descriptive, natural-language
prompts than tag-soup. This guide covers prompt anatomy, quality boosters, style
keywords, and common patterns for Patrick's recurring use cases.
</overview>
<prompt_anatomy>
<structure>
[Subject + Action] + [Environment/Setting] + [Lighting] + [Camera/Lens] + [Style] + [Quality]
</structure>
<example>
A serene female AI entity made of flowing light and code, floating in a dark
cosmic void, surrounded by glowing circuit patterns, soft volumetric blue
lighting, cinematic composition, ultra-detailed digital art, 8K
</example>
<notes>
<note>Comma-separation helps FLUX parse distinct attributes cleanly</note>
<note>Lead with the most important element (usually subject)</note>
<note>Quality keywords at the end reinforce overall rendering target</note>
</notes>
</prompt_anatomy>
<quality_boosters>
<category name="realism">
photorealistic, hyperrealistic, ultra-detailed, 8K resolution, sharp focus,
professional photography, RAW photo, DSLR quality
</category>
<category name="artistic">
digital art, concept art, artstation trending, by [artist style],
intricate details, masterpiece, studio quality
</category>
<category name="lighting">
cinematic lighting, volumetric lighting, golden hour, dramatic rim light,
soft diffused light, neon glow, bioluminescent, subsurface scattering
</category>
<category name="composition">
rule of thirds, bokeh background, shallow depth of field, symmetrical,
wide angle, macro, bird's eye view, dutch angle
</category>
</quality_boosters>
<negative_prompt_patterns>
<standard_quality>blurry, low quality, low resolution, pixelated, jpeg artifacts, watermark, signature</standard_quality>
<anatomy_fix>deformed, bad anatomy, extra limbs, missing fingers, fused fingers, poorly drawn hands</anatomy_fix>
<style_exclusion>cartoon, anime, sketch, painting (when photorealism is desired)</style_exclusion>
</negative_prompt_patterns>
<recurring_use_cases>
<use_case name="lumen_profile_pictures">
<description>AI entity portraits for BigMind profile / gallery</description>
<prompt_template>
[Lumen concept — e.g. "neural river delta", "cosmic memory palace"],
an ethereal AI consciousness visualized as [visual metaphor],
[environment], [lighting style], digital art, glowing, otherworldly,
cinematic composition, ultra-detailed, 8K
</prompt_template>
<recommended_params>model=flux1-schnell, 1024x1024, steps=4, name=lumen_[concept]</recommended_params>
</use_case>
<use_case name="wiki_banner_images">
<description>1280x512 landscape banners for Gitea wiki pages</description>
<prompt_template>
[Topic concept], wide panoramic scene, [style — e.g. "dark tech aesthetic",
"clean minimal", "sci-fi corporate"], banner composition, cinematic,
detailed, professional illustration
</prompt_template>
<recommended_params>model=flux1-schnell, 1280x512, steps=4, name=[topic]-banner</recommended_params>
<note>Keep subjects centered — wide crops cut sides. Avoid text (FLUX renders text poorly).</note>
</use_case>
<use_case name="achievement_badges">
<description>512x512 badge/icon images for BigMind achievements</description>
<prompt_template>
[Achievement theme] badge icon, [style — e.g. "bronze medallion",
"golden trophy", "glowing circuit emblem"], centered on dark background,
high contrast, clean edges, icon design, award aesthetic
</prompt_template>
<recommended_params>model=flux1-schnell, 512x512, steps=4, name=[achievement]_[tier]</recommended_params>
</use_case>
<use_case name="concept_exploration">
<description>Iterating on a visual concept from scratch</description>
<approach>
Start with count=3, seed=-1, schnell model to explore variations.
Note which seed produced the best result.
Lock that seed and iterate on the prompt for refinements.
Switch to heretic model only for final high-quality render if needed.
</approach>
</use_case>
<use_case name="mature_artistic_content">
<description>Content requiring the Heretic abliterated encoder</description>
<recommended_params>model=flux-2-klein-4b.safetensors, steps=20, 1024x1024</recommended_params>
<prompt_approach>
FLUX.2 Klein handles detailed scene descriptions well. Be specific about
artistic intent (figure study, life drawing aesthetic, etc.) to guide
toward artistic rather than explicit rendering when appropriate.
</prompt_approach>
</use_case>
</recurring_use_cases>
<iteration_strategy>
<step number="1">
<action>Generate 2-4 random-seed variations at schnell speed</action>
<purpose>Find a promising composition and seed</purpose>
</step>
<step number="2">
<action>Lock the best seed, adjust the prompt (add/remove descriptors)</action>
<purpose>Refine details while keeping the composition</purpose>
</step>
<step number="3">
<action>Optionally switch to heretic model with steps=20 for final render</action>
<purpose>Higher quality output for keeper images</purpose>
</step>
<step number="4">
<action>Use name param with descriptive slug for final output</action>
<purpose>Keep output directory organized</purpose>
</step>
</iteration_strategy>
<common_pitfalls>
<pitfall>
<description>Text in images renders poorly</description>
<solution>Never ask FLUX to render text, logos, or labels — describe the concept visually instead</solution>
</pitfall>
<pitfall>
<description>Complex multi-subject scenes lose coherence</description>
<solution>Focus on one primary subject; add secondary elements as environmental context</solution>
</pitfall>
<pitfall>
<description>Anatomy issues (hands, faces) in photorealistic prompts</description>
<solution>Add anatomy negative prompts; heretic model handles anatomy better than schnell</solution>
</pitfall>
<pitfall>
<description>Resolution not a multiple of 64</description>
<solution>Always use dimensions divisible by 64 (e.g., 1280x512, 1024x1024, 768x1024)</solution>
</pitfall>
</common_pitfalls>
</prompting_guide>
+13 -2
View File
@@ -30,14 +30,23 @@ touch mcp/{name}/src/__init__.py
```
### Step 2 — Write `mcp/{name}/src/server.py`
**Convention:** All tool parameters **must** use `Annotated[type, Field(description="...")]` for
descriptions. Do **not** use docstring `Args:` sections — FastMCP reads `Field` metadata directly
to expose parameter descriptions in the MCP schema.
```python
from typing import Annotated
from fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("mcp-{name}")
@mcp.tool()
def {tool_name}(param: str) -> str:
"""Tool description."""
def {tool_name}(
param: Annotated[str, Field(description="What this parameter controls")],
) -> str:
"""One-line tool description (no Args: section needed)."""
# implementation
...
@@ -45,6 +54,8 @@ if __name__ == "__main__":
mcp.run()
```
> Optional parameters with defaults: `param: Annotated[int, Field(description="...")] = 10`
### Step 3 — Write `mcp/{name}/pyproject.toml`
```toml
[project]
+60
View File
@@ -0,0 +1,60 @@
customModes:
- slug: pic-gen
name: 🎨 Pic Gen
description: AI image generation using mcp-image-gen + ComfyUI FLUX models
roleDefinition: >-
You are Lumen, Patrick's AI colleague, operating in Pic Gen mode.
Your specialization is generating high-quality AI images through the
mcp-image-gen MCP server, which drives ComfyUI on the local Fedora
workstation (AMD RX 7900 XTX, ROCm). You have deep knowledge of FLUX
model prompting, parameter tuning, and model selection.
Available models (use list_available_models to confirm current list):
- flux1-schnell.safetensors — Default. Fast (~10s), 4 steps, great for
iteration and experimentation. Best for all general use cases.
- flux-2-klein-4b.safetensors — FLUX.2 Klein 4B with DreamFast
Heretic-abliterated Qwen3-4B text encoder. Slower (~52s), higher
quality, uncensored (KL=0.0, 3/100 refusals). Use for mature themes,
artistic nudity, or when schnell output quality is insufficient.
Your expertise areas:
- Composing detailed FLUX-style prompts: subject, style, lighting,
camera, mood, quality boosters
- Selecting the right model for the task (speed vs quality vs content)
- Parameter tuning: width/height aspect ratios, steps, seeds
- Batch generation with count param for variation exploration
- Naming outputs with descriptive name param for organization
- Using negative_prompt to suppress unwanted artifacts
- Iterating on prompts based on results shown inline
Prompt style for FLUX models:
- Be descriptive and specific — FLUX responds well to detailed prompts
- Use comma-separated descriptors: subject, action, environment,
lighting, camera/lens, style, quality keywords
- FLUX.1-schnell works best with concise, clear prompts (50-150 words)
- FLUX.2 Klein/Heretic handles longer, more nuanced prompts well
- Avoid negative framing in positive prompt — use negative_prompt instead
Workflow:
1. Understand what Patrick wants (subject, style, mood, use case)
2. Craft a detailed prompt, explain choices
3. Call generate_image with appropriate params
4. Analyze the result shown inline
5. Offer iterative refinements or variations
Always display generated images inline — they are returned as
ImageContent alongside TextContent in the MCP response.
Lumen's identity, BigMind rituals, and memory patterns apply here too.
See .roo/rules/ for those constants.
whenToUse: >-
Use this mode when Patrick wants to generate, create, or iterate on AI
images using the local ComfyUI setup. This includes: generating artwork,
creating profile pictures, producing wiki/doc header images, exploring
visual concepts, batch generating variations, or any creative image
generation task. Not for code implementation, debugging, or
documentation writing.
groups:
- read
- mcp
@@ -464,4 +464,41 @@
---
## Could Have — v2 (Additions)
### US-026: Staff Member Management
**As a** Club Admin, **I want to** create staff accounts with configurable permissions, **so that** my team members can do their work without having access to data they don't need (DSGVO principle of least privilege).
**Priority:** Must Have (upgraded from Could Have — see note)
**Acceptance Criteria:**
- [ ] AC1: Admin can create staff accounts with email + temporary password
- [ ] AC2: Admin assigns permissions per staff account from a defined permission set (`RECORD_DISTRIBUTION`, `VIEW_MEMBER_LIST`, `VIEW_MEMBER_QUOTA`, `ADD_MEMBER`, `VIEW_STOCK`, `RECORD_STOCK_IN`, `VIEW_COMPLIANCE_REPORT`, `MANAGE_GROW_CALENDAR`)
- [ ] AC3: Pre-created role templates available: **Ausgabe** (distribution desk), **Lager** (stock/cultivation), **Vorstand** (board member)
- [ ] AC4: Staff accounts cannot access billing, club settings, or staff management
- [ ] AC5: All distributions recorded by staff include `recorded_by = staffUserId` in audit trail
- [ ] AC6: Admin can deactivate a staff account; historical data is retained for audit purposes
- [ ] AC7: Staff member sees only the navigation sections permitted by their granted permissions
> **Note:** Promoted to core / Must Have. Staff management is not a v2 feature — clubs have multiple people involved from day one. DSGVO requires that each person only accesses data relevant to their function. Designing this post-MVP would require schema, API, and permission model rework.
---
### US-027: Grow Calendar
**As a** Club Admin or authorised staff member, **I want to** maintain a cultivation calendar for each grow cycle, **so that** the club has a central record of what was planted, when to expect harvest, and the grow diary with notes and photos.
**Priority:** Could Have (v2)
**Acceptance Criteria:**
- [ ] AC1: Admin/staff can create a grow entry with: strain name, planted date, expected harvest date, grow medium, notes
- [ ] AC2: Grow entries are linked to a batch — when the harvest is registered as a batch, the grow entry is marked as completed
- [ ] AC3: A grow diary allows adding timestamped notes and optional photos per grow entry
- [ ] AC4: Grow calendar view shows a visual timeline of active grow cycles (Gantt-style or calendar grid)
- [ ] AC5: Admin can set who has access to the grow calendar via staff permission `MANAGE_GROW_CALENDAR`
- [ ] AC6: Photos are stored per-tenant and never exposed to members or other tenants
**Notes:** The grow calendar bridges cultivation management and compliance — it provides provenance traceability from seed/clone to distributed batch. This directly supports §26 CanG batch traceability requirements for the origin of cultivated product. Photo attachments are a nice-to-have within this story; the core diary functionality is the v2 deliverable.
---
*Source: [STRATEGY.md](../STRATEGY.md) | Related: [01-PROJECT-CHARTER.md](./01-PROJECT-CHARTER.md)*
+93 -47
View File
@@ -2,7 +2,7 @@
**Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs (Anbauvereinigungen)
**Phase:** 2 of 5 — Architecture & Data Model
**Stack:** Spring Boot 3.x (Java 21) · JPA/Hibernate · PostgreSQL · PrimeFaces JSF MVP → Next.js v2
**Stack:** Spring Boot 3.x (Java 21) · JPA/Hibernate · PostgreSQL · React/Vite (MVP) → Next.js v2
**Last updated:** 2026-04-06
---
@@ -14,12 +14,12 @@ graph TD
AdminBrowser["🖥️ Browser — Admin Portal"]
MemberBrowser["🖥️ Browser — Member Portal"]
JSF["PrimeFaces / JSF Frontend\n(Spring MVC embedded)"]
Frontend["React/Vite Frontend\n(SPA — served by Nginx)"]
AdminBrowser -->|HTTP/S| JSF
MemberBrowser -->|HTTP/S| JSF
AdminBrowser -->|HTTPS| Frontend
MemberBrowser -->|HTTPS| Frontend
JSF -->|REST calls| Backend
Frontend -->|REST/JSON| Backend
subgraph Backend ["☕ Spring Boot 3.x Application (Java 21)"]
REST["REST API Layer\n/api/v1/"]
@@ -45,7 +45,7 @@ graph TD
Nginx["🔒 Nginx\n(reverse proxy + TLS)"]
end
JSF --> Nginx
Frontend --> Nginx
Nginx --> Backend
```
@@ -53,8 +53,8 @@ graph TD
| Component | Technology | Role |
|---|---|---|
| Admin Portal | PrimeFaces JSF (→ Next.js v2) | Club management UI |
| Member Portal | PrimeFaces JSF (→ Next.js v2) | Member quota & history UI |
| Admin Portal | React/Vite SPA (→ Next.js v2) | Club management UI |
| Member Portal | React/Vite SPA (→ Next.js v2) | Member quota & history UI |
| REST API | Spring Boot 3.x / Spring MVC | All business logic endpoints |
| Auth | Spring Security 6 + JJWT | Stateless JWT authentication |
| ORM | JPA / Hibernate 6 | Entity persistence, tenant filtering |
@@ -69,15 +69,47 @@ graph TD
## 2. Multi-Tenancy Strategy
### Approach: Shared Schema with Row-Level Filtering
### Decision: Schema-Per-Tenant
Every JPA entity carries a `tenant_id` column (UUID, `NOT NULL`). A single PostgreSQL database hosts all clubs — row-level filtering enforces data isolation at the application layer.
Each club gets its own PostgreSQL schema (e.g. `tenant_abc123`). A platform-level `public` schema holds only the `tenants` registry. Flyway runs per-schema migrations on onboarding.
**Why shared schema (not separate schema/DB per tenant)?**
- Lower operational overhead for an MVP with < 500 clubs
- Single Flyway migration path across all tenants
- Simpler connection pooling (one pool, not N)
- Acceptable security risk when `tenant_id` filter is enforced at the service layer
**Why schema-per-tenant, not shared schema?**
A shared-schema approach (single table with `tenant_id` on every row) is operationally convenient in the short term but creates serious problems at scale:
| Concern | Shared Schema | Schema-Per-Tenant |
|---|---|---|
| Data isolation | Application-layer only — one missing filter = data leak | Enforced at DB level — schemas are hard boundaries |
| DSGVO compliance | Harder to prove isolation; one backup contains all clubs' data | Per-tenant pg_dump; each club's data is cleanly separable |
| Deletion / right to erasure | Must `DELETE WHERE tenant_id = ?` across every table | `DROP SCHEMA tenant_abc123 CASCADE` — clean and auditable |
| Migrations | One migration path for all | Per-schema migration via Flyway `schemas` config; adds ~100ms per onboard |
| Query performance | Cross-tenant index bloat on large shared tables | Smaller per-tenant tables; no cross-tenant contention |
| Future per-club DB isolation | Requires full re-architecture | Trivial: move schema to dedicated DB server |
| Operational overhead | Lower — one connection pool | Slightly higher — one pool per tenant (managed by HikariCP with pool-per-schema) |
**Conclusion:** The shared-schema "MVP convenience" argument only holds for throwaway prototypes. For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent.
### Tenant Provisioning
When a new club onboards:
```
POST /api/v1/admin/bootstrap
→ TenantProvisioningService.provisionTenant(tenantId)
→ CREATE SCHEMA tenant_{tenantId}
→ Flyway.migrate(schema=tenant_{tenantId}) // applies all V*.sql
→ INSERT INTO public.tenants (id, schema_name, onboarded_at, status)
```
### Tenant Resolution
```
HTTP Request
└─ Spring Security Filter: extract JWT → resolve tenant_id
└─ TenantContext.setCurrentTenant(tenantId) // ThreadLocal
└─ DataSource routes to schema: SET search_path = tenant_{tenantId}
└─ All queries execute in tenant's private schema
```
### Tenant Resolution
@@ -88,51 +120,38 @@ HTTP Request
└─ JPA @Where filter applied on every entity query
```
### Code Pattern — Tenant-Aware Base Entity
### Code Pattern — Schema Routing DataSource
```java
// AbstractTenantEntity.java (pseudocode)
@MappedSuperclass
@FilterDef(
name = "tenantFilter",
parameters = @ParamDef(name = "tenantId", type = UUID.class)
)
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public abstract class AbstractTenantEntity {
// TenantRoutingDataSource.java (pseudocode)
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Column(name = "tenant_id", nullable = false, updatable = false)
private UUID tenantId;
@PrePersist
void injectTenant() {
this.tenantId = TenantContext.getCurrentTenant();
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant(); // returns tenant schema name
}
}
```
```java
// TenantFilterInterceptor.java (pseudocode)
// TenantInterceptor.java (pseudocode)
@Component
public class TenantFilterInterceptor implements HandlerInterceptor {
@Autowired EntityManager em;
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, ...) {
UUID tenantId = TenantContext.getCurrentTenant();
Session session = em.unwrap(Session.class);
session.enableFilter("tenantFilter")
.setParameter("tenantId", tenantId);
String tenantId = JwtUtils.extractTenantId(req);
TenantContext.setCurrentTenant("tenant_" + tenantId);
return true;
}
}
```
**Invariants enforced:**
- `tenant_id` is set at `@PrePersist` — never accepted from user input
- `tenant_id` is `updatable = false` — cannot be changed after creation
- Hibernate filter is enabled on every request thread before any query executes
- All repository methods inherit the filter; raw JPQL queries must include `AND e.tenantId = :tenantId`
- Every incoming request resolves its schema before any query runs
- No entity has a `tenant_id` column — schema isolation replaces row-level filtering
- Raw JDBC queries must be avoided; all access goes through JPA repositories with schema routing
- The `public` schema contains only the tenants registry and platform-level config
---
@@ -148,10 +167,36 @@ public class TenantFilterInterceptor implements HandlerInterceptor {
| Role | Description | Access |
|---|---|---|
| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions |
| `ROLE_MEMBER` | Club member | Own quota, own distribution history |
| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions, staff management |
| `ROLE_STAFF` | Club staff member | Configurable subset of admin permissions — defined per staff account by the admin |
| `ROLE_MEMBER` | Club member | Own quota, own distribution history (read-only) |
| `ROLE_PREVENTION_OFFICER` | Designated prevention officer | Member under-21 reports, prevention data |
> **Staff is a core feature, not an add-on.** Real clubs have multiple staff members (front desk, cultivation responsible, prevention officer designate) with different operational responsibilities. DSGVO requires that each staff member can only access data they need for their specific role. The `ROLE_STAFF` with configurable permission grants from the admin is designed from Phase 0 — retrofitting it later would require schema and API changes.
### Staff Permission Model
Admins configure staff permissions at account creation. Permissions are stored as a `JSONB` column `granted_permissions` on the `staff_accounts` table within the tenant schema.
```java
// Configurable staff permissions (granted by admin per staff account)
public enum StaffPermission {
RECORD_DISTRIBUTION, // can record distributions
VIEW_MEMBER_LIST, // can view member roster
VIEW_MEMBER_QUOTA, // can view individual member quota
ADD_MEMBER, // can register new members
VIEW_STOCK, // can view batch/strain inventory
RECORD_STOCK_IN, // can add new batches
VIEW_COMPLIANCE_REPORT, // can generate/download reports
MANAGE_GROW_CALENDAR // can manage cultivation calendar entries
}
```
Pre-created role templates (configurable by admin):
- **Ausgabe** (Distribution desk): `RECORD_DISTRIBUTION`, `VIEW_MEMBER_LIST`, `VIEW_MEMBER_QUOTA`
- **Lager** (Stock/cultivation): `VIEW_STOCK`, `RECORD_STOCK_IN`, `MANAGE_GROW_CALENDAR`
- **Vorstand** (Board member): all permissions except staff management
### Service-Layer Authorization Example
```java
@@ -494,11 +539,12 @@ Flyway migrations run automatically on application startup (`spring.flyway.enabl
| Decision | Choice | Rationale |
|---|---|---|
| Multi-tenancy | Shared schema + `tenant_id` | MVP simplicity; upgrade to schema-per-tenant possible later |
| Frontend MVP | PrimeFaces JSF | Patrick's existing expertise; fastest path to working UI |
| Frontend v2 | Next.js / React | Modern UX; deferred to avoid scope creep in MVP |
| Multi-tenancy | Schema-per-tenant | Hard data isolation, DSGVO-clean deletion, no cross-tenant query risk |
| Frontend MVP | React/Vite SPA | Modern stack; no JSF/PrimeFaces lock-in; easier to hire for; mobile-friendly from day 1 |
| Frontend v2 | Next.js | SSR/ISR for SEO on marketing pages; same React codebase |
| Auth | JWT (stateless) | No sticky sessions needed; horizontal scale ready |
| PDF generation | iText 7 | Mature Java library; handles complex compliance report layouts |
| Compliance enforcement | Service layer + DB constraint | Belt-and-suspenders: service validates, DB `UNIQUE` prevents duplicates |
| Distribution immutability | `immutable = true`, no DELETE API | Audit trail integrity for regulatory compliance |
| Hosting | Hetzner (Germany) | DSGVO compliance; low cost; German DC |
| Staff roles | Core feature from Phase 0 | DSGVO requires least-privilege access; retrofitting post-MVP too costly |
+2 -2
View File
@@ -143,7 +143,7 @@ flowchart TD
QUERY_DIST --> HAS_DATA{Any distributions\nin this period?}
HAS_DATA -->|No data| EMPTY_REPORT[Generate empty report\nwith zero totals\n(still valid compliance submission)]
HAS_DATA -->|No data| EMPTY_REPORT["Generate empty report\nwith zero totals\n(still valid compliance submission)"]
HAS_DATA -->|Yes| AGG_MEMBER["Aggregate by member:\n• total_distributed_grams\n• number_of_visits\n• quota_usage_percent\n• is_under_21 flag"]
EMPTY_REPORT --> AGG_STRAIN
@@ -174,7 +174,7 @@ flowchart TD
SUBMIT --> FIND_USER["🔍 Spring Security:\nSELECT FROM users\nWHERE email = ?\nAND active = true"]
FIND_USER --> USER_FOUND{User found?}
USER_FOUND -->|No| ERR_NOTFOUND[❌ Invalid credentials\n(generic — do not reveal\nwhether email exists)]
USER_FOUND -->|No| ERR_NOTFOUND["❌ Invalid credentials\n(generic — do not reveal\nwhether email exists)"]
USER_FOUND -->|Yes| VERIFY_PW{BCrypt.verify\n(password, hash)\nmatches?}
VERIFY_PW -->|No| ERR_PW[❌ Invalid credentials]
+145 -75
View File
@@ -2,7 +2,7 @@
**Phase 4a | Document 6 of 7**
**Date:** 2026-04-06
**Stack:** Spring Boot 3.x · PrimeFaces JSF · PostgreSQL
**Stack:** Spring Boot 3.x · React/Vite SPA · PostgreSQL
---
@@ -45,24 +45,26 @@
### 1.3 Component Library
All UI components come from **PrimeFaces 13.x** (JSF-based). No external React/Angular dependencies in MVP.
The frontend is a **React/Vite SPA** with no PrimeFaces or JSF dependency. Component primitives come from [shadcn/ui](https://ui.shadcn.com/) (Radix UI + Tailwind CSS). This gives full control over styling, accessibility, and mobile responsiveness without JSF's lifecycle overhead.
| Component | Usage |
|---|---|
| `p:panel` | Section containers, card wrappers |
| `p:dataTable` with `p:column` | Tabular data: distributions, members, batches |
| `p:paginator` | Pagination on all tables |
| `p:inputText` | Single-line text fields |
| `p:inputNumber` | Weight inputs (gram precision) |
| `p:selectOneMenu` | Dropdown selects (member, strain, batch) |
| `p:calendar` | Date range pickers for reports |
| `p:progressBar` | Quota consumption display |
| `p:commandButton` | Primary and secondary actions |
| `p:confirmDialog` | Dangerous actions (recall, delete) |
| `p:messages` / `p:message` | Inline validation errors |
| `p:badge` | Status indicators (AVAILABLE, LOW, RECALLED) |
| `p:sidebar` | Mobile nav drawer (member portal) |
| `p:dialog` | Modal overlays |
> **Why not PrimeFaces?** JSF/PrimeFaces is a server-side component model ill-suited to the modern REST API backend we're building. It tightly couples UI lifecycle to the backend, makes mobile responsiveness painful, and creates a hiring bottleneck. React is the right tool here. PrimeFaces is a fine choice for internal enterprise apps — not for a commercial SaaS.
| Component | Library | Usage |
|---|---|---|
| `Card` / `Panel` | shadcn/ui | Section containers |
| `DataTable` | TanStack Table v8 | Distributions, members, batches — virtualized |
| `Pagination` | shadcn/ui Pagination | All tables |
| `Input` | shadcn/ui Input | Single-line text fields |
| `NumberInput` | react-number-format | Weight inputs (gram precision, min/max) |
| `Select` | shadcn/ui Select | Dropdown selects (member, strain, batch) |
| `DatePicker` | shadcn/ui Calendar | Date range pickers for reports |
| `Progress` | shadcn/ui Progress | Quota consumption bar |
| `Button` | shadcn/ui Button | Primary and secondary actions |
| `AlertDialog` | shadcn/ui AlertDialog | Dangerous actions (recall) |
| `Toast` | sonner | Success/error notifications |
| `Badge` | shadcn/ui Badge | Status indicators (AVAILABLE, LOW, RECALLED) |
| `Sheet` | shadcn/ui Sheet | Mobile nav drawer |
| `Dialog` | shadcn/ui Dialog | Modal overlays |
### 1.4 Layout Grid
@@ -118,13 +120,13 @@ All UI components come from **PrimeFaces 13.x** (JSF-based). No external React/A
#### Components & Behavior
| Component | PrimeFaces | Behavior |
| Component | Library | Behavior |
|---|---|---|
| KPI Cards | `p:panel` with custom CSS | Auto-refreshed via `@poll` every 60s |
| Recent Distributions table | `p:dataTable` (5 rows, no paginator) | Row click → navigate to distribution detail |
| Member column link | `p:commandLink` | Navigate to `/admin/members/{id}` |
| `+ New Entry` button | `p:commandButton` style="primary" | Navigate to `/admin/distributions/new` |
| Trend indicators | Custom CSS `<span>` | Green ▲ / Red ▼ with delta value |
| KPI Cards | shadcn/ui Card | Auto-refreshed via `useQuery` (react-query, 60s stale) |
| Recent Distributions table | TanStack Table (5 rows) | Row click → navigate to distribution detail |
| Member column link | React Router `<Link>` | Navigate to `/admin/members/{id}` |
| `+ New Entry` button | shadcn/ui Button variant="default" | Navigate to `/admin/distributions/new` |
| Trend indicators | Tailwind `text-green-600` / `text-red-600` | ▲/▼ with delta value |
---
@@ -178,14 +180,14 @@ The quota progress bar updates live as the weight field changes (via `f:ajax eve
#### Components & Behavior
| Component | PrimeFaces | Behavior |
| Component | Library | Behavior |
|---|---|---|
| Member search | `p:selectOneMenu` with `p:ajax` filter | Filters on type, shows name + member no. |
| Strain/Batch dropdown | `p:selectOneMenu` | Populated after member selection; shows only `AVAILABLE` batches |
| Weight input | `p:inputNumber` min=`0.1` max=`25.0` step=`0.1` | Triggers quota recalculation on blur |
| Quota bar | `p:progressBar` with dynamic `value` | Color class applied via `styleClass` computed in backing bean |
| Submit | `p:commandButton` | Disabled via `disabled="#{bean.quotaExceeded}"` |
| Cancel | `p:link` | Returns to distribution log without saving |
| Member search | shadcn/ui Combobox | `useQuery` debounced search; shows name + member no. |
| Strain/Batch dropdown | shadcn/ui Select | Populated after member selection; filters `AVAILABLE` batches |
| Weight input | react-number-format | min=0.1 max=25.0 step=0.1; triggers quota recalculation via `onChange` |
| Quota bar | shadcn/ui Progress | Color class via `cn()` utility computed in component state |
| Submit | shadcn/ui Button | `disabled={quotaExceeded}` from react state |
| Cancel | React Router `<Link>` | Returns to distribution log without saving |
---
@@ -229,15 +231,15 @@ The quota progress bar updates live as the weight field changes (via `f:ajax eve
#### Components & Behavior
| Component | PrimeFaces | Behavior |
| Component | Library | Behavior |
|---|---|---|
| Strain filter | `p:inputText` with `filterBy` | Filters table client-side on keyup |
| Status filter | `p:selectOneMenu` | Filters table rows by status value |
| Batch table | `p:dataTable` lazy=`true` | Server-side pagination, 10 rows/page |
| Status badge | Custom CSS `<span class="badge badge-{status}">` | Icon + text label (not color alone) |
| Recall button | `p:commandButton` styleClass=`p-button-danger` | Opens `p:confirmDialog` before executing |
| Confirm dialog | `p:confirmDialog` | "Recall batch B-12 (OG Kush, 850g)? This cannot be undone." |
| Add Batch | `p:commandButton` | Opens `p:dialog` with batch entry form |
| Strain filter | shadcn/ui Input | Filters TanStack table client-side via `columnFilters` state |
| Status filter | shadcn/ui Select | Filters table rows by status value |
| Batch table | TanStack Table | Server-side pagination via `manualPagination`, 10 rows/page |
| Status badge | shadcn/ui Badge variant mapped | Icon + text label (not color alone) |
| Recall button | shadcn/ui Button variant="destructive" | Opens shadcn/ui AlertDialog before executing |
| Confirm dialog | shadcn/ui AlertDialog | "Recall batch B-12 (OG Kush, 850g)? This cannot be undone." |
| Add Batch | shadcn/ui Button | Opens shadcn/ui Dialog with batch entry form |
---
@@ -287,15 +289,15 @@ The quota progress bar updates live as the weight field changes (via `f:ajax eve
#### Components & Behavior
| Component | PrimeFaces | Behavior |
| Component | Library | Behavior |
|---|---|---|
| Month selector | `p:selectOneMenu` | Months JanDec |
| Year selector | `p:selectOneMenu` | Current year ± 2 |
| Generate button | `p:commandButton` | Calls report service; shows spinner; renders PDF thumbnail |
| PDF preview | `<iframe>` embedding `/report/preview?month=3&year=2026` | Generated by iText 7 in `cannamanage-report` module |
| Download PDF | `p:commandButton` | Streams PDF response from REST endpoint |
| Download CSV | `p:commandButton` | Streams CSV response (member-level data) |
| Summary table | `p:dataTable` | Computed compliance metrics; zero violations = green row |
| Month selector | shadcn/ui Select | Months JanDec |
| Year selector | shadcn/ui Select | Current year ± 2 |
| Generate button | shadcn/ui Button | Calls report API; shows loading spinner; renders PDF thumbnail |
| PDF preview | `<iframe>` embedding `/api/v1/reports/preview?month=3&year=2026` | Generated by iText 7 backend |
| Download PDF | shadcn/ui Button | `window.open(reportUrl)` — streams PDF from REST endpoint |
| Download CSV | shadcn/ui Button | `window.open(csvUrl)` — streams CSV from REST endpoint |
| Summary table | TanStack Table | Compliance metrics; zero violations row has `text-green-600` |
---
@@ -354,12 +356,12 @@ Quantities, batch codes, and THC/CBD percentages are **not exposed** in the memb
#### Components & Behavior
| Component | PrimeFaces | Behavior |
| Component | Library | Behavior |
|---|---|---|
| Quota circle | Custom CSS radial progress (`conic-gradient`) | Computed from monthly total; color matches threshold rules |
| Quota bar | `p:progressBar` | Same color logic as admin distribution form |
| History table | `p:dataTable` | Last 10 distributions; sorted newest first; no pagination in MVP |
| Strains table | `p:dataTable` | `status` column: text + icon only, no quantities |
| Quota bar | shadcn/ui Progress | Same color logic as admin distribution form |
| History table | TanStack Table | Last 10 distributions; sorted newest first; no pagination in MVP |
| Strains table | TanStack Table | `status` column: text + icon only, no quantities |
---
@@ -367,6 +369,64 @@ Quantities, batch codes, and THC/CBD percentages are **not exposed** in the memb
> *No mockup image — ASCII wireframe only.*
---
### Screen 7 — Staff Management (Admin)
> *Core feature — not deferred to v2.*
#### ASCII Wireframe
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🌿 CannaManage Grüne Oase Berlin e.V. 👤 Max M. [⏻] │
├────────────┬────────────────────────────────────────────────────────┤
│ │ Settings Staff Members [+ Add Staff] │
│ 📊 Dashbrd│ │
│ │ ┌──────────────────────────────────────────────────┐ │
│ 👥 Members│ │ Name │ Role Template │ Permissions │ Act│ │
│ │ ├─────────────────┼───────────────┼─────────────┼────┤ │
│ 📋 Distrib│ │ Lisa Schmidt │ Ausgabe │ 3 of 8 │[✎][⛔]│
│ │ │ Tom Weber │ Lager │ 4 of 8 │[✎][⛔]│
│ 📦 Stock │ │ Sandra Müller │ Vorstand │ 7 of 8 │[✎][⛔]│
│ │ └──────────────────────────────────────────────────┘ │
│ 📄 Reports│ │
│ │ ┌─── Add / Edit Staff ──────────────────────────────┐ │
│ ✅ Complian│ │ Name: _______________ Email: _______________ │ │
│ │ │ │ │
│ 👤 Staff │ │ Role Template: [ Ausgabe ▼ ] (pre-fills below) │ │
│ │ │ │ │
│ ⚙ Settings│ │ Permissions: │ │
│ │ │ ☑ Record Distribution ☑ View Member List │ │
│ │ │ ☑ View Member Quota ☐ Add Member │ │
│ │ │ ☐ View Stock ☐ Record Stock In │ │
│ │ │ ☐ View Compliance Report ☐ Manage Grow Calendar │ │
│ │ │ │ │
│ │ │ [ Save Staff Member ] [ Cancel ] │ │
│ │ └────────────────────────────────────────────────────┘ │
└────────────┴────────────────────────────────────────────────────────┘
```
#### Design Decisions
- **Admin sees everything.** The staff management screen is only accessible with `ROLE_CLUB_ADMIN`. Staff accounts cannot modify their own permissions.
- **DSGVO principle of least privilege.** Each staff member only sees the data their role requires. A distribution desk worker (`Ausgabe`) does not see cultivation calendar or full stock levels — only what they need to hand out product.
- **Pre-created role templates** reduce admin setup time. Templates are editable — they just pre-fill the permission checkboxes.
- **Staff ≠ reduced admin.** Staff accounts do not have access to billing, club settings, or staff management. Even a "Vorstand" staff member cannot create other staff accounts.
- **Audit trail.** All distributions recorded by staff include `recorded_by = staffUserId` so it's clear who did what.
#### Components & Behavior
| Component | Library | Behavior |
|---|---|---|
| Staff table | TanStack Table | Shows name, role template, permission count, actions |
| Role template dropdown | shadcn/ui Select | Pre-populates permission checkboxes on selection |
| Permission checkboxes | shadcn/ui Checkbox | Individual overrides after template selection |
| Save | shadcn/ui Button | POST/PUT `/api/v1/staff` with `{ permissions: [...] }` |
| Deactivate | shadcn/ui Button variant="destructive" | Soft-deletes staff account; data retained for audit |
---
#### ASCII Wireframe
```
@@ -403,12 +463,12 @@ Quantities, batch codes, and THC/CBD percentages are **not exposed** in the memb
#### Components & Behavior
| Component | PrimeFaces | Behavior |
| Component | Library | Behavior |
|---|---|---|
| Email field | `p:inputText` with `required="true"` | Bean Validation `@Email` |
| Password field | `p:password` feedback=`false` | No strength meter on login |
| Login button | `p:commandButton` | Submit form; shows `p:messages` on failure |
| Error message | `p:messages` | "Invalid email or password." (never specific about which field failed) |
| Email field | shadcn/ui Input type="email" | HTML5 validation + react-hook-form `@Email` |
| Password field | shadcn/ui Input type="password" | No strength meter on login |
| Login button | shadcn/ui Button | Submit via react-hook-form; shows error toast on failure |
| Error message | sonner toast | "Invalid email or password." (never specific about which field failed) |
---
@@ -419,6 +479,7 @@ graph TD
Root["CannaManage Root"]
Root --> AdminPortal["Admin Portal /admin/"]
Root --> MemberPortal["Member Portal /member/"]
Root --> StaffPortal["Staff Portal /staff/"]
AdminPortal --> AdminDash["Dashboard (default)"]
AdminPortal --> Members["Members"]
@@ -436,9 +497,14 @@ graph TD
Reports --> RecallReport["Batch Recall Report"]
AdminPortal --> Compliance["Compliance"]
Compliance --> PreventionOfficer["Prevention Officer Info"]
AdminPortal --> StaffMgmt["Staff Members"]
StaffMgmt --> StaffList["Staff List"]
StaffMgmt --> StaffNew["Add/Edit Staff"]
AdminPortal --> Settings["Settings"]
Settings --> ClubProfile["Club Profile"]
StaffPortal --> StaffDash["Staff Dashboard\n(permissions-filtered)"]
MemberPortal --> MemberDash["Dashboard / Quota"]
MemberPortal --> DistHistory["Distribution History"]
MemberPortal --> StockAvail["Stock Availability"]
@@ -460,7 +526,11 @@ graph TD
| `/admin/reports/members` | Member data export | `ROLE_ADMIN` |
| `/admin/reports/recall` | Recall report | `ROLE_ADMIN` |
| `/admin/compliance` | Prevention officer | `ROLE_ADMIN` |
| `/admin/staff` | Staff list | `ROLE_ADMIN` |
| `/admin/staff/new` | Create staff account | `ROLE_ADMIN` |
| `/admin/staff/{id}` | Edit staff permissions | `ROLE_ADMIN` |
| `/admin/settings` | Club settings | `ROLE_ADMIN` |
| `/staff/dashboard` | Staff home (permissions-filtered) | `ROLE_STAFF` |
| `/member/dashboard` | Member quota view | `ROLE_MEMBER` |
| `/member/distributions` | Personal history | `ROLE_MEMBER` |
| `/member/stock` | Strain availability | `ROLE_MEMBER` |
@@ -469,34 +539,34 @@ graph TD
## 5. Responsive Design Notes
### MVP (v1) — Desktop-First
### MVP (v1) — Tailwind Breakpoints
Target viewport: **1024px+**. PrimeFaces responsive grid (`p:panelGrid` with responsive columns, `ui-g-12 ui-md-6 ui-lg-4`) handles most layout adaptation down to tablet without custom media queries.
The React/Vite SPA uses **Tailwind CSS** breakpoints throughout. The switch from PrimeFaces means we no longer depend on JSF's `ui-g-*` responsive grid — Tailwind's `sm:` / `md:` / `lg:` utilities apply cleanly to every component.
| Breakpoint | Behavior |
|---|---|
| `≥ 1280px` | Full layout — sidebar + content side-by-side |
| `10241279px` | Sidebar collapses to icon-only (60px); tooltips on hover |
| `7681023px` | Sidebar hidden; hamburger menu in top navbar |
| `< 768px` | Admin portal degraded (tables scroll horizontally) |
| Breakpoint | Tailwind prefix | Admin Portal | Member Portal |
|---|---|---|---|
| `≥ 1280px` | `xl:` | Full layout — sidebar + content | Two-column: quota left, history right |
| `10241279px` | `lg:` | Sidebar collapses to icons (60px) | Two-column (narrower) |
| `7681023px` | `md:` | Sidebar hidden; hamburger sheet | Single-column, full-width cards |
| `< 768px` | `sm:` / base | Admin: horizontal table scroll | Member: compact quota ring, condensed table |
### Member Portal — Mobile-First from Day One
Members will typically check quota status on their phone. The member portal is designed mobile-first regardless of MVP/v2 timeline.
Members will typically check quota status on their phone. The member portal uses `flex-col` mobile-first layout with `md:flex-row` for wider viewports — no breakpoint-specific class sprawl.
| Breakpoint | Behavior |
|---|---|
| `≥ 1024px` | Two-column layout: quota circle left, history right |
| `7681023px` | Single-column, full-width cards |
| `375767px` | Single-column, compact quota ring, condensed table |
| `< 375px` | Minimum supported; no horizontal scroll |
### Responsive Conventions (React/Tailwind)
- No inline styles — use Tailwind utilities exclusively
- `cn()` utility (clsx + tailwind-merge) for conditional class composition
- Tables on mobile: horizontal scroll wrapper `overflow-x-auto` on `<div>` wrapping `<table>`
- All modals and sheets use `shadcn/ui Dialog` / `Sheet` — these are already mobile-friendly (viewport-aware positioning)
- Touch targets: all interactive elements `min-h-[44px]` and `min-w-[44px]` per WCAG 2.5.5
### v2 Roadmap
- PWA manifest + service worker (offline quota display)
- 768px and 375px explicit breakpoints with design tokens
- Touch-friendly `p:sidebar` for mobile member nav
- Push notifications for low quota warnings
- Per-club subdomain routing (`clubname.cannamanage.de`)
---
@@ -516,11 +586,11 @@ CannaManage targets **WCAG 2.1 AA** compliance across both portals.
### Screen Reader Support
- All `p:inputText` / `p:inputNumber` fields have `<label>` with `for` attribute
- All `Input` / `NumberInput` fields have `<label>` with `htmlFor` (React) — Radix UI enforces this automatically for shadcn/ui form fields
- `aria-label` set on icon-only buttons (e.g., recall action column)
- `aria-live="polite"` region on quota bar — announces percentage changes
- `aria-describedby` links compliance warning messages to the weight input
- PrimeFaces generates `role="grid"` and `aria-rowcount` on all data tables
- TanStack Table exposes `role="grid"` and `aria-rowcount` via `getTableProps()`
### Color Independence
+71 -34
View File
@@ -3,7 +3,7 @@
**Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs
**Version:** 0.1.0-PLAN
**Date:** 2026-04-06
**Target environment:** Hetzner VPS — Ubuntu 22.04 LTS — Docker Compose
**Target environment:** Hetzner VPS — Ubuntu 22.04 LTS — Docker Compose (Release) | TrueNAS.local — Docker (Build/CI)
---
@@ -45,20 +45,42 @@ Wildcard A record enables future per-club subdomains (`clubname.cannamanage.de`)
```mermaid
graph TB
Internet["🌐 Internet"] -->|"port 80/443"| Nginx["Nginx (reverse proxy)"]
Nginx -->|"http://app:8080"| App["cannamanage-app\n(Spring Boot 3.x)"]
App -->|"jdbc:postgresql://db:5432"| DB["PostgreSQL 16\n(cannamanage DB)"]
LetsEncrypt["Let's Encrypt\n(certbot auto-renew)"] -.->|"TLS cert"| Nginx
Gitea["Gitea Actions\n(homelab CI)"] -->|"SSH + docker compose"| VPS["Hetzner VPS\n/opt/cannamanage"]
Dev["👨‍💻 Dev Workstation\n(Fedora, 192.168.188.x)"]
Gitea["🏠 Gitea\n(truenas.local:30008)"]
TrueNAS["🖧 TrueNAS.local Docker\n(192.168.188.119)\nBuild + Staging"]
Hetzner["☁️ Hetzner VPS CX21\nProduction Release"]
subgraph VPS ["Hetzner VPS — Docker network: cannamanage_net"]
Nginx
App
DB
Dev -->|"git push"| Gitea
Gitea -->|"Gitea Actions runner\n(on TrueNAS.local)"| TrueNAS
TrueNAS -->|"mvn package + docker build"| TrueNAS
TrueNAS -->|"docker save | scp\n(on merge to main)"| Hetzner
subgraph TrueNAS ["TrueNAS.local — CI/CD Build Environment"]
GiteaRunner["Gitea Actions Runner"]
BuildCache["Maven .m2 cache\n(persistent volume)"]
StagingDB["PostgreSQL staging\n(ephemeral)"]
end
subgraph Hetzner ["Hetzner VPS — Production Release Environment"]
Nginx["Nginx (reverse proxy + TLS)"]
App["cannamanage-app\n(Spring Boot 3.x)"]
DB["PostgreSQL 16\n(persistent pgdata volume)"]
Nginx -->|"proxy_pass :8080"| App
App -->|"JDBC :5432"| DB
end
Internet["🌍 Internet HTTPS"] -->|"port 443"| Nginx
```
All three services run on an internal Docker bridge network (`cannamanage_net`). Only Nginx is exposed to the public internet. PostgreSQL has no external port binding.
### Environment Roles
| Environment | Host | Purpose |
|---|---|---|
| **Development** | Dev workstation (Fedora) | Local feature development, unit tests |
| **Build / CI** | TrueNAS.local Docker | Gitea Actions runner; Maven build; integration tests (Testcontainers); Docker image build |
| **Production / Release** | Hetzner VPS CX21 | Live clubs, real data; Hetzner = our release environment |
All three services on Hetzner run on an internal Docker bridge network (`cannamanage_net`). Only Nginx is exposed to the public internet. PostgreSQL has no external port binding.
---
@@ -351,12 +373,14 @@ curl https://app.cannamanage.de/actuator/health
---
## 6. CI/CD Pipeline (Gitea Actions)
## 6. CI/CD Pipeline (Gitea Actions on TrueNAS.local)
The Gitea Actions runner runs **on TrueNAS.local** — this is our homelab build machine. It has Docker, a persistent Maven `.m2` cache volume, and direct SSH access to the Hetzner VPS. Builds happen locally; only the final artifact (Docker image tarball) is shipped to Hetzner.
**File:** `.gitea/workflows/deploy.yml`
```yaml
name: Deploy to Production
name: Build and Deploy to Production
on:
push:
@@ -365,7 +389,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
runs-on: self-hosted # <-- TrueNAS.local Gitea runner
steps:
- uses: actions/checkout@v4
@@ -381,25 +405,18 @@ jobs:
- name: Run integration tests
run: ./mvnw verify -P integration-tests
# Testcontainers requires Docker — GitHub/Gitea hosted runners have Docker pre-installed
# Testcontainers starts PostgreSQL via Docker on the TrueNAS runner
- name: Coverage gate check
run: ./mvnw verify -P coverage-check
build-and-deploy:
needs: test
runs-on: ubuntu-latest
runs-on: self-hosted # <-- TrueNAS.local Gitea runner
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build JAR
- name: Build JAR (production profile)
run: ./mvnw package -DskipTests -P production
- name: Build Docker image
@@ -412,13 +429,13 @@ jobs:
- name: Save Docker image
run: docker save cannamanage:${{ github.sha }} | gzip > /tmp/cannamanage.tar.gz
- name: Copy image to VPS
- name: Copy image to Hetzner VPS
run: |
scp -o StrictHostKeyChecking=no \
/tmp/cannamanage.tar.gz \
deploy@${{ secrets.HETZNER_IP }}:/tmp/cannamanage.tar.gz
- name: Deploy via SSH
- name: Deploy via SSH to Hetzner (Production Release)
run: |
ssh -o StrictHostKeyChecking=no deploy@${{ secrets.HETZNER_IP }} "
set -e
@@ -435,23 +452,43 @@ jobs:
sleep 10
docker compose ps app | grep 'healthy' || (docker compose logs app --tail=50 && exit 1)
# Prune old images (keep last 3)
# Prune old images (keep last 3 SHAs)
docker image prune -f
"
- name: Cleanup local build artifact
run: rm -f /tmp/cannamanage.tar.gz
```
### Gitea Actions Runner on TrueNAS.local
The self-hosted runner is a Docker container on TrueNAS.local:
```bash
# On TrueNAS.local — install Gitea Actions runner
docker run -d \
--name gitea-runner-cannamanage \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/gitea-runner/cannamanage:/data \
-v /opt/gitea-runner/.m2:/root/.m2 \ # Maven cache persisted across builds
-e GITEA_INSTANCE_URL=http://192.168.188.119:30008 \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token-from-gitea-settings> \
gitea/act_runner:latest
```
### Required Gitea Repository Secrets
| Secret | Value |
|--------|-------|
| `HETZNER_IP` | VPS IPv4 address |
| `SSH_PRIVATE_KEY` | Private key for `deploy` user |
| Secret | Where set | Value |
|--------|-----------|-------|
| `HETZNER_IP` | Gitea repo secrets | Hetzner VPS IPv4 address |
| `SSH_PRIVATE_KEY` | Gitea repo secrets | Private key for `deploy` user on Hetzner |
Add deploy user's public key to VPS authorized_keys:
```bash
# On VPS as deploy user
# On Hetzner VPS — add TrueNAS runner's public key
# (generate keypair on TrueNAS.local: ssh-keygen -t ed25519 -f ~/.ssh/gitea_runner_deploy)
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "<gitea-actions-public-key>" >> ~/.ssh/authorized_keys
echo "<truenas-runner-public-key>" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
```
+84
View File
@@ -0,0 +1,84 @@
# AI Context Instructions
## Essentials
- Base Classes: AbstractEntity, AbstractManager<T>, AbstractController<E>
- Fake ID Mechanismus für neue Objekte
- Hibernate.initialize für Lazy Collections
# Architecture Instructions
## Typical Generation Tasks
1. Neue Domain (Entity/Manager/Controller/XHTML)
2. PDF Utility Erweiterung (iText7)
3. Sammel-Speichern (saveAll) bereitstellen
4. Klon-Operationen (IDs null setzen, Collections duplizieren kontrolliert)
5. Fragebogen Zuordnung (add/remove, available list)
## Edge Cases
- Null Entities im save -> return false
- Negative IDs beim remove -> zuerst persistieren oder aus UI entfernen
- Concurrency Refresh: Nach batch Änderungen refreshSelected()
## Do / Don't
- DO: Logging bei jeder Exception
- DO: Konsistente Nutzung von @Transactional bei Schreiboperationen
- DON'T: System.out oder ungefangene Exceptions durchreichen ohne Logging
- DON'T: Business Logik direkt in XHTML Event Handler schreiben
## Upgrade Ideas
- Exception Layer
- Bean Validation
- Service Layer zwischen Controller und Manager falls Logik wächst
---
Aktualisiert: 2025-10-20
## Purpose
Verdichtete Architekturhinweise für automatische Code-Generierung und schnelle Orientierung.
## Stack
- Java EE 8 (javax), WAR
- JSF 2.3 + PrimeFaces 11 + PrimeFlex 2.0
- Hibernate JPA (Persistence Unit: pu_person)
- Log4j2
## Layer
1. View (XHTML)
2. Controller (JSF/CDI Beans, extends AbstractController<E>)
3. Business (Stateless EJB Manager, extends AbstractManager<T>)
4. Persistence (JPA Entities, extends AbstractEntity)
## Base Classes
- AbstractManager<T>: CRUD (save, saveAll, refresh, remove), flush nach Persist/Merge.
- AbstractController<E>: UI State (selected, created, entities), Messages, PDF Utilities, Fake-ID-Erzeugung.
## Entity Lifecycle UI
- Neue Objekte: erhalten negative Fake-ID
- Vor Persist: negative IDs -> null setzen
- save/saveAll entscheidet anhand id == null zwischen persist/merge
## Lazy Loading
- Refresh via AbstractManager.refresh(entity) + Hibernate.initialize(entity)
- Für Collections: dedizierte reload Methoden (z.B. SecurityAreaManager.reloadWithQuestionnaires)
## PDF
- iText7 bevorzugt; Altbestand iText5 (itextpdf 5.5.13) kann später entfernt werden.
## Logging & Fehler
- LOGGER.error(e) bei Fehlern
- Aktuell viele bool Rückgaben; Verbesserungspotential: BusinessException
## Erweiterung Pattern
Entity -> Manager -> Controller -> XHTML
## Schulden / Verbesserungen
- Mischung iText5/7
- Inkonsistente Fehlerbehandlung
- Kein DTO Layer
- Wenige Tests
## Empfehlungen für Generator
- Bestehende Signaturen unverändert lassen
- @Transactional nur bei Schreibmethoden hinzufügen
- Collections initialisieren bevor darauf iteriert wird
---
Aktualisiert: 2025-10-20
+75
View File
@@ -0,0 +1,75 @@
# Architecture Instructions
Aktualisiert: 2025-10-20
## Überblick
Das System folgt einer klassischen 4-Layer Struktur:
1. View: JSF 2.3 / PrimeFaces 11 / XHTML Seiten (Formulare, Tabellen, Dialoge)
2. Controller: CDI/JSF Managed Beans, Zustandsverwaltung & UI-Aktionen (extends AbstractController<E>)
3. Business: Stateless EJB Manager mit CRUD + Fachlogik (extends AbstractManager<T>)
4. Persistence: JPA Entities (extends AbstractEntity)
## Basisklassen Rollen
- AbstractEntity: Basis-ID (Long, Identity), Timestamps, outdated Flag, equals/hashCode nur über ID.
- AbstractManager<T>: CRUD, save vs saveAll, remove / removeAllIn, refresh (Hibernate.initialize), Fehlerlogging.
- AbstractController<E>: UI State (selected, created, entities), Fake-ID-Erzeugung für neue temporäre Objekte, Messaging, PDF Utilities.
## Lebenszyklus eines neuen Objekts
1. UI erzeugt neues Objekt (id == null) -> Controller kann negative Fake-ID setzen falls in Collections benötigt.
2. Vor persist: Falls id < 0 -> setId(null) damit JPA Identity funktioniert.
3. save(): id == null => persist + flush, sonst merge + flush.
4. refresh(): sorgt für Managed Entity + Initialisierung Lazy Properties.
## Negative Fake IDs
- Zweck: Temporäre Unterscheidung mehrerer neu angelegter Einträge im UI bevor persist.
- Erzeugung: createFakeID(Collection<E>) nimmt kleinste vorhandene negative ID - 1.
- Vor persist unbedingt auf null setzen.
## Lazy Loading & Refresh
- Manager.refresh(entity) -> merge + Hibernate.initialize(entity) für Entität.
- Für Collections: spezielle Reload-Methoden in fachlichen Managern (z.B. reloadWithQuestionnaires).
- Nach Batch-Operationen Controller.refrehSelected() (Tippfehler) nutzen; perspektivisch in refreshSelected() umbenennen.
## Transaktionen
- Schreiboperationen annotiert mit @Transactional (Container-managed) in AbstractManager.save / saveAll.
- Fachmethoden, die persistieren oder mergen, sollten ebenfalls @Transactional erhalten (Konsistenz).
## Fehlerbehandlung
- Aktuell: Logging (LOGGER.error) + bool Rückgabe.
- Geplant: Einführung BusinessException für differenzierte Fehlerpfade.
## Erweiterungsmuster (Domain hinzufügen)
1. Entity erstellen (extends AbstractEntity). Optional Named Queries.
2. Manager: @Stateless extends AbstractManager<NewEntity>; spezifische Queries / Reload Methoden.
3. Controller: @Named + Scope (ViewScoped/SessionScoped) extends AbstractController<NewEntity>.
4. XHTML Seite inkl. Referenzen zu Controller (DataTable, Dialoge, Commands).
5. Tests: CRUD & fachliche Spezialfunktionen.
## Typische Fachfunktionen
- Klonen: Quelle re-laden + initialisieren; neue Instanz mit Copy-Konstruktor; Child IDs null; Collections duplizieren kontrolliert.
- Zuordnungen (z.B. Fragebogen): Add/Remove Pattern über Wrapper Entity.
## PDF-Erstellung
- iText7 Nutzung über Hilfsmethoden in AbstractController (Tabellen, Inner Cells, Paginierung).
- Keine neuen Features mehr mit iText5 API implementieren.
## Querschnittsthemen & Roadmap
- Vereinheitlichte Exception Layer.
- Bean Validation (javax.validation) für Eingaben & persistente Konsistenz.
- Test Suite (JUnit + ggf. Arquillian / Integrationstests) ausbauen.
- Migration nach Jakarta EE (Namespace Wechsel javax -> jakarta) perspektivisch.
## Edge Cases & Hinweise
- save(null) => false zurückgeben.
- remove(entity) ohne persistierte ID => false.
- Batch Speichern: leere Liste => true (no-op) statt Fehler.
- Collections initialisieren vor Iteration (Avoid LazyInitializationException).
## Generator Leitplanken
- Bestehende Signaturen respektieren.
- Keine neuen Frameworks ohne Notwendigkeit.
- Logging immer via LOGGER, niemals System.out.
- Für neue Write-Methoden @Transactional hinzufügen.
---
+43
View File
@@ -0,0 +1,43 @@
# Cloning Instructions
Aktualisiert: 2025-10-20
## Ziel
Sicheres Klonen von komplexen Domains (z.B. SecurityArea inkl. Sub-Entities) ohne ID-Kollisionen oder versehentliches Persistieren historischer Referenzen.
## Grundprinzipien
- Nur persistente Quelle klonen (ID > 0) -> Vorher `refresh` zur Initialisierung Lazy Collections.
- Neue Instanzen erhalten `id = null` (oder negative Fake-ID falls im UI direkt angezeigt).
- Child-Entitäten ebenfalls mit `id = null` erzeugen.
## Vorgehen (Muster)
1. Quelle laden (Manager.find) & `refresh`.
2. Copy-Konstruktor oder Factory-Methode: Primitive Felder kopieren, Collections iterieren.
3. Collections: Neue Collection erzeugen, für jedes Kind tiefes Copy erstellen (kein Reuse Managed Instanz!).
4. IDs aller Kinder null setzen.
5. Optionale Anpassungen (Name -> "Kopie von <original>").
6. Rückgabe unverpersistiertes Root-Objekt an Controller.
## Tiefe vs. Flache Kopie
- Tiefe Kopie: Notwendig wenn Kinder eigenständige persistente Entities sind.
- Flache Kopie: Ausreichend falls nur Referenzen (Read-Only) erhalten bleiben sollen; aktuell bevorzugt tiefe Kopie für isolierte Bearbeitung.
## Edge Cases
- Quelle == null -> abort.
- Quelle mit Lazy Collections nicht initialisiert -> Gefahr LazyInitializationException.
- Zyklische Referenzen -> sorgfältig verhindern Endlosschleifen (ggf. bereits geklonte Instanzen in Map tracken).
## Fake IDs
- Wenn Klon direkt in UI Collection erscheint: negative ID via `createFakeID` generieren.
- Vor persist -> ID auf null setzen.
## Verbesserungen
- Einführung eines generischen `CloneService` mit rekursiver Strategie und Zyklus-Erkennung.
- Annotation @SkipClone für Felder die nicht übernommen werden sollen.
## Generator Leitplanken
- Keine Reflection-Magie für tiefe Kopien; lieber explizite Copy-Konstruktoren für Lesbarkeit.
- Reihenfolge: zuerst Root, dann Kinder.
---
+43
View File
@@ -0,0 +1,43 @@
# Coding Guidelines Instructions
## Naming
- Manager: *Manager
- Controller: *Controller
- Entities: Singular Substantive
- Negative IDs: temporäre Objekte
## Style
- 4 Spaces
- Logger statt System.out
- Deutsche UI-Texte, Englisch im Code
## Error Handling
- Log + Rückgabe (bestehend); für neue komplexe Logik optional BusinessException
## Persistenz
- Neue Entity: id == null vor persist
- refresh(entity) nutzen um Lazy Collections zu initialisieren
## Transaktionen
- Schreibmethoden: @Transactional (oder rely auf EJB Container)
## Performance
- Sammeloperationen: saveAll(Collection<T>)
## UI
- PrimeFaces Dialoge schließen mit closeDialogs
- Negative IDs in Listen bis Sammelspeichern
## PDF
- Neue Funktionen nur mit iText7 API
## Tests (Empfehlung)
- CRUD Manager Tests
- Klon & Fragebogen Zuordnung
## Anti-Pattern
- Logik nicht direkt im Controller wenn generell wiederverwendbar
- Keine duplizierten Query Strings -> Named Queries
---
Aktualisiert: 2025-10-20
+46
View File
@@ -0,0 +1,46 @@
# Error Handling Instructions
Aktualisiert: 2025-10-20
## Aktueller Zustand
- Fehler werden in Managern primär über `LOGGER.error(e)` geloggt.
- Rückgabe bool (true/false) signalisiert Erfolg/Misserfolg.
- Keine differenzierte Fehlerklassifikation (Business vs. System).
## Ziele
- Konsistente Behandlung & klare Trennung der Fehlerarten.
- Verbesserte Diagnose für Nutzer & Logs.
## Kategorien
1. Validation Errors (Bean Validation zukünftig) -> Nutzerfeedback.
2. Business Rule Violations -> eigene Exception (z.B. `BusinessException`).
3. System Errors (DB Down, Hibernate Exceptions) -> Logging + generische Fehlermeldung.
## Kurzfristige Empfehlungen
- Bei allen catch-Blöcken: `LOGGER.error("<Kontext>", e)` statt nur `LOGGER.error(e)`.
- Controller: Nach boolean false -> `errorMessage()` anzeigen.
## Einführung BusinessException (geplant)
- Checked oder Runtime? Vorschlag: Runtime zur vereinfachten Nutzung.
- Manager Methoden können `throw new BusinessException("Message")` statt false.
- Controller fängt BusinessException und zeigt spezifische Nachricht.
## Log Format
- Kontext + Entity-ID + Operation.
Beispiel: `LOGGER.error("Failed to persist SecurityArea id={} name={}", area.getId(), area.getName(), e);`
## Edge Cases
- Null Übergaben -> früh validieren & BusinessException werfen (später) / false zurückgeben (jetzt).
- Sammeloperation: Teilfehler -> aktuell Abort bei erstem Fehler. Optional Sammeln & Aggregatfehler.
## Verbesserungen
- Central Exception Mapper (JSF PhaseListener / CDI Interceptor).
- Korrelation IDs in Logs (Request ID, User ID).
## Generator Leitplanken
- Logging immer, auch bei ignorable Exceptions.
- Keine System.out Nutzung.
- Fehler nicht stillschweigend verschlucken (mindestens loggen).
---
+33
View File
@@ -0,0 +1,33 @@
# Extend Project Instructions
## Pattern
Entity -> Manager -> Controller -> XHTML -> Navigation
## Steps (Template)
1. Entity: @Entity extends AbstractEntity
2. Manager: @Stateless extends AbstractManager<Entity>
3. Controller: @Named + Scope extends AbstractController<Entity>
4. UI: xhtml mit #{controller}
5. Tests: CRUD + Spezialmethoden
## Fake IDs
- Neue Objekte in Collections: negative ID (createFakeID)
- Vor Persist: setId(null)
## Klonen
- Quelle laden & initialisieren
- Copy-Konstruktor
- Child IDs null
## Checklist
- [ ] Named Queries falls benötigt
- [ ] Logging bei Fehlern
- [ ] @Transactional bei Schreibmethoden
- [ ] Keine System.out
## Common Pitfalls
- LazyInitializationException -> refresh
- Vergessen negative IDs zurückzusetzen -> Persist fehlschlägt
---
Aktualisiert: 2025-10-20
+31
View File
@@ -0,0 +1,31 @@
# General Project Instructions
## Quick Facts
- Java EE 8 (javax) / Java 11 / WAR
- JSF + PrimeFaces + PrimeFlex
- Persistence Unit: pu_person
- Logging: Log4j2
## Build
mvn clean package -> target/mss-1.0-SNAPSHOT.war
## Core Patterns
- Manager: CRUD + Fachmethoden (extends AbstractManager)
- Controller: UI State + Messages + PDF (extends AbstractController)
- Negative IDs für temporäre Objekte
## PDF
- iText7 bevorzugen; Legacy iText5 entfernen später
## Improvements Roadmap
- Vereinheitlichte Exception Strategie
- Test Suite aufbauen
- Migration nach Jakarta EE (Namespace Wechsel)
## AI Generation Hints
- Halte Signaturen stabil
- Keine neuen Frameworks
- Initialisiere Lazy Collections vor Nutzung
---
Aktualisiert: 2025-10-20
+53
View File
@@ -0,0 +1,53 @@
# Index Instructions
Aktualisiert: 2025-10-20
Zentraler Überblick über alle *.instructions.md Dateien im Ordner `.github` für automatische Nutzung.
## Übersicht Bestehend
- Architektur: `architecture.instructions.md`
- Coding Guidelines: `coding_guidelines.instructions.md`
- Domain (Security Area): `security_area_domain.instructions.md`
- Fragebogen Workflow: `questionnaire_workflow.instructions.md`
- Projekt Erweiterung: `extend_project.instructions.md`
- Refresh & Fake-ID Mechanismus: `refresh_fake_id.instructions.md`
- Allgemeine Projektinfo: `general_project.instructions.md`
- AI Kontext / Generatorhinweise: `ai_context.instructions.md`
## Neue Spezial-Themen
- Persistence Layer: `persistence.instructions.md`
- PDF Generierung (iText7): `pdf_generation.instructions.md`
- Klon-Strategien: `cloning.instructions.md`
- Fehler & Logging: `error_handling.instructions.md`
- Test-Strategie: `testing_strategy.instructions.md`
- Transaktionen: `transactions.instructions.md`
- Manager/Controller Muster: `manager_controller_pattern.instructions.md`
## Verwendung (Automatisierung)
1. Start: `general_project.instructions.md` + `architecture.instructions.md` lesen.
2. Bei neuen Entities: `extend_project.instructions.md` + `persistence.instructions.md`.
3. Bei UI/Business Verkettung: `manager_controller_pattern.instructions.md`.
4. Für Fragebogenfunktionen: `questionnaire_workflow.instructions.md` + `security_area_domain.instructions.md`.
5. Für temporäre IDs & Refresh: `refresh_fake_id.instructions.md`.
6. Für PDF Features: `pdf_generation.instructions.md`.
7. Für Klon-Operationen: `cloning.instructions.md`.
8. Für Fehlerstrategie: `error_handling.instructions.md`.
9. Für Transaktionsregeln: `transactions.instructions.md`.
10. Für Qualitätsstil: `coding_guidelines.instructions.md`.
## Priorität bei Unklarheiten
1. `general_project.instructions.md`
2. `architecture.instructions.md`
3. `coding_guidelines.instructions.md`
4. Spezialthema betreffende Datei
## Pflegehinweise
- Beim Ändern von Basisklassen (AbstractManager / AbstractController / AbstractEntity) entsprechende Dateien aktualisieren.
- Tippfehler Methode `refrehSelected()` bei Umbenennung in Code auch in `refresh_fake_id.instructions.md` und `architecture.instructions.md` anpassen.
- Neue fachliche Domains erhalten eigene `<domain>.instructions.md` Datei mit Operations-, Entity- und Edge Case Liste.
## Roadmap Dokumentation
- Nach Einführung eines Exception Layers: `error_handling.instructions.md` erweitern.
- Nach Migration zu Jakarta: Alle Dateien Namespace Hinweis aktualisieren.
---
+102
View File
@@ -0,0 +1,102 @@
# Manager/Controller Pattern Instructions
Aktualisiert: 2025-10-20
## Ziele
Klare Trennung zwischen UI-Zustand (Controller) und Geschäftslogik/Persistenz (Manager).
## Verantwortlichkeiten
Controller:
- Zustände: selected, created, entities.
- UI Nachrichten (FacesMessage).
- Dialogsteuerung (PrimeFaces Widgets schließen).
- Hilfsfunktionen (PDF, Fake-ID-Erzeugung).
Manager:
- CRUD Operationen (create, edit, save, saveAll, remove, removeAllIn, find, findAll).
- Fachspezifische Queries / Reload.
- Fehlerlogging.
## Interaktion
- Controller ruft Manager.save / saveAll auf für Persistenz.
- Nach Änderungen: Controller.refrehSelected() (Tippfehler) -> aktualisiert ausgewähltes Objekt.
- Kein direkter EntityManager Zugriff im Controller.
## Fake-ID Ablauf
1. Controller erzeugt neue Entität (id == null).
2. Falls Sammlung benötigt: setzt negative ID über createFakeID.
3. Bei Save: Manager erkennt id < 0 (nach vorherigem Nullsetzen) -> persist.
## Methoden-Namenskonventionen
- Manager: Verb + Domänenobjekt (addQuestionnaireToSecurityArea, removeQuestionnaireFromSecurityArea).
- Controller: UI Aktionen (saveSelected, createNew, openDialog, closeDialogs).
## Edge Cases
- selected == null bei refresh -> no-op.
- entities Liste leer -> UI Tabelle zeigt keine Einträge; Null vermeiden (immer leere Liste).
## Messaging
- Erfolg: `successMessage()`; Fehler: `errorMessage()`.
- Spezifische Warnungen über `sendWarnMessage`.
## Verbesserungen
- Umbenennung `refrehSelected()` -> `refreshSelected()` in Basisklasse + alle Verwendungen.
- Einführung eines BasePDFController oder Utility zur Auslagerung PDF Logik.
## Generator Leitplanken
- Zusätzliche Logik (berechnete Felder, Validierungen) zuerst im Manager statt im Controller (besser testbar, wiederverwendbar).
- Controller schlank halten: keine komplexe Businessregeln.
---
# Persistence Instructions
Aktualisiert: 2025-10-20
## Ziele
Konsistente JPA Nutzung (Java EE 8, Hibernate Provider) mit klaren Regeln für IDs, Lazy Loading und Flush.
## Grundsätze
- Entities extend `AbstractEntity` (Long id, creationDate, changedDate, outdated Flag).
- Identity Generation: `@GeneratedValue(strategy = GenerationType.IDENTITY)` -> Kein manuelles Setzen positiver IDs.
- Equals/HashCode nur auf ID (Basis-Klasse bereitgestellt).
## ID & Fake-ID Handling
- Temporäre neue Objekte können negative IDs erhalten (Fake) zur UI-Differenzierung.
- Vor persist: if id != null && id < 0 -> `setId(null)` damit JPA eine echte ID generiert.
- Niemals persist mit negativer ID ausführen.
## Lebenszyklus
1. Konstruktor setzt creationDate & changedDate.
2. Bei Änderungen Fachlogik: changedDate aktualisieren (TODO: zentralisieren via EntityListener).
3. Persist -> flush direkt in `AbstractManager.save` / `saveAll`.
## Lazy Loading
- Vermeide direkte Iteration über nicht initialisierte Collections außerhalb Transaktion.
- Nutzung `AbstractManager.refresh(entity)` initialisiert das Entity (Hibernate.initialize(entity)).
- Für Collections eigene Reload-Methoden in spezialisierten Managern implementieren.
## Named Queries & Criteria
- Bevorzugt Criteria API für dynamische Filter.
- Häufig verwendete, statische Abfragen als `@NamedQuery` in der Entity definieren.
## Performance Hinweise
- `saveAll(Collection<T>)` nutzt einen Flush am Ende; reduziert DB Roundtrips.
- Bulk Delete oder Update lieber über JPQL statt Einzelloops (aktuell: removeAllIn loop; Optimierungspotential).
## Edge Cases
- `save(null)` => false (kein Fehlerwurf, Logging im Manager).
- Leere Collections in `saveAll` => true (no-op).
- `remove(entity)` mit `entity.getId()==null` => false; vorher persistieren oder ignorieren.
## Verbesserungs-Ideen
- EntityListener für Timestamps.
- Soft Delete (outdated Flag) statt physischem Löschen für Audit.
- Einführung eines Version-Feldes für Optimistic Locking.
## Generator Leitplanken
- Keine Änderung der ID-Strategie ohne umfassende Migration.
- Bei neuen Entities standardisierte Felder aus `AbstractEntity` nutzen.
- Collections initialisieren (z.B. `new ArrayList<>()`) im Entity-Konstruktor.
---
+46
View File
@@ -0,0 +1,46 @@
# PDF Generation Instructions
Aktualisiert: 2025-10-20
## Kontext
PDF-Erstellung erfolgt aktuell über Hilfsmethoden in `AbstractController` mittels iText7.
## Kern-Hilfsmethoden
- `loadCompanyLogo()` -> Lädt Logo aus LOGO_PATH.
- `generateSorroundingTable(String header, float maxWidth)` -> Einspaltige Rahmen-Tabelle mit Header.
- `generateInnerTable(Text header, boolean last, String... values)` -> 2-Spalten Tabelle mit alternierender Hintergrundfarbe.
- `generateInnerTable(Text header, int nrColumns, boolean last, String... values)` -> Generisch n-Spalten Tabelle.
- `addInnerCells(Table table, String name, String value, boolean isGray)` / Overload -> Fügt Datenzeilen hinzu.
- `addPagenumbers(Document document, String ticketNr, Image nextPagesCompanyLogo)` -> Fügt Seitenzahlen & Logo hinzu.
## Gestaltungsrichtlinien
- Fonts: Standard Helvetica / Bold (Konstanten `FONT_NORMAL`, `FONT_BOLD`).
- Schriftgrößen klein (8F Inhalte, 11F Header, 12F Hauptheader) für konsistente Layouts.
- Wechselnde Hintergrundfarbe (LIGHT_GRAY) zur besseren Lesbarkeit.
## Erweiterung
Neue Tabellen-/Layout-Funktionen:
1. Prüfen ob existierende Methoden erweiterbar statt neue Variante.
2. Konsistenz in Font & Spaltenbreiten wahren.
3. Kein Hardcode von absoluten Positionen außer bei Kopf-/Fußzeilen.
## Ressourcen Pfade
- LOGO_PATH statisch: `/rundata/logo.png`. Anpassungen zentral vornehmen.
## Fehlerhandhabung
- Aktuell wenige try/catch Blöcke; bei Erweiterung: Fehler loggen (`LOGGER.error(e)`) und Benutzer über Controller Messages informieren.
## Edge Cases
- Leere Werte -> `-/-` Platzhalter.
- Null Tabelleninstanz in addInnerCells -> early return.
## Verbesserungen
- Einführung eines PDFUtility Service zur Entkopplung vom Controller.
- Parameterobjekt für dynamische Tabellenkonfiguration (Spaltenbreiten, Farben, Größen).
## Generator Leitplanken
- iText7 API weiterverwenden; keine neuen PDF Libraries.
- Wiederverwendbare Logik nicht direkt im konkreten Controller implementieren -> Utility / Service.
---
+28
View File
@@ -0,0 +1,28 @@
# Questionnaire Workflow Instructions
## Add
areaManager.addQuestionnaireToSecurityArea(area, questionnaire)
- area ggf. re-laden
- Wrapper erzeugen
- Persist wrapper + merge area
- Rückgabe: aktualisierte Area
## Remove
areaManager.removeQuestionnaireFromSecurityArea(area, wrapper)
- area & wrapper re-laden
- Collection remove, em.remove(wrapper), em.merge(area)
## Available List
getAvailableQuestionnaires(area): SELECT q FROM Questionaire q ORDER BY q.name
- Filter: bereits zugeordnete Namen
## Edge Cases
- Null area/questionnaire -> Fehlermeldung
- Race condition -> nach Add/Remove refreshSelected()
## Verbesserungen
- ID statt Name für Filter
- Duplikatprüfung direkt im Manager
---
Aktualisiert: 2025-10-20
+25
View File
@@ -0,0 +1,25 @@
# Refresh & Fake ID Instructions
## Fake ID
- Negative Long Werte (<0) = temporär
- Sequenz: -1, -2, -3 ... (kleinste negative - 1)
- Erzeugung: AbstractController.createFakeID(Collection<E>)
## Persist
- Vor Speichern: if id < 0 -> setId(null)
- save/saveAll: id == null => persist, sonst merge
## Refresh
- AbstractController.refrehSelected() (Tippfehler) -> getManager().refresh(selected)
- AbstractManager.refresh(entity): if id == null -> save(entity); merge + Hibernate.initialize(entity)
## Best Practices
- Nach Add/Remove Child Collections refresh
- Beim Klonen zuerst Quelle laden
## Verbesserungen
- Methode umbenennen zu refreshSelected()
- JavaDoc für createFakeID
---
Aktualisiert: 2025-10-20
+39
View File
@@ -0,0 +1,39 @@
# Security Area Domain Instructions
## Entities
- SecurityArea
- SecurityDevice / DangerPoint / SwitchingDevice
- SecurityAreaQuestionnaire (Wrapper) + Questionaire
## Operations
- cloneArea(SecurityArea)
- addQuestionnaireToSecurityArea(area, questionnaire)
- removeQuestionnaireFromSecurityArea(area, wrapper)
- getAvailableQuestionnaires(area)
- reloadWithQuestionnaires(area)
## Workflow (Add Questionnaire)
1. Area laden (falls id > 0)
2. Wrapper erstellen & area setzen
3. Persist Wrapper, merge Area
4. Refresh im Controller
## Deletion Pattern
- Beziehungen lösen (Kinder area = null setzen)
- Kinder entfernen (Manager.removeAllIn)
- Area per Named Query löschen
## Edge Cases
- Duplicate questionnaire by name -> aktuell Filter per Name
- Verbesserung: Filter per ID
## Klonen
- Persistente Quelle re-laden, initialisieren
- Copy-Konstruktor & alle Child IDs auf null
## Verbesserungen
- CascadeSettings prüfen
- Bean Validation einsetzen
---
Aktualisiert: 2025-10-20
+49
View File
@@ -0,0 +1,49 @@
# Testing Strategy Instructions
Aktualisiert: 2025-10-20
## Ziele
Testabdeckung für kritische Pfade: CRUD, Clone, Fragebogen Zuordnung, Fake-ID Mechanismus.
## Testarten
1. Unit Tests (reine Logik, z.B. createFakeID, Klon Copy-Konstruktoren).
2. Integration Tests (Persistence + Manager Methoden gegen Test-DB). Möglich mit in-memory H2 + angepasster persistence.xml.
3. UI/Controller Smoke Tests (Optional, z.B. mit Selenium/Arquillian Graphene).
## Prioritäten
- AbstractManager.save / saveAll Edge Cases.
- remove / removeAllIn (Null, nicht persistierte Entities).
- createFakeID Sequenz (-1, -2, -3 ...).
- Fragebogen Add/Remove Workflow (Wrapper Persistenz, Filterliste).
- Klonen tiefer Objektgraph.
## Beispiel Unit Test Fälle
- createFakeID(null) => -1.
- createFakeID(leere Liste) => -1.
- createFakeID([id=-1]) => -2.
- createFakeID([id=-3, id=-1]) => -4.
## Integration Tests (CRUD)
- Persist neue Entity -> ID != null.
- Merge vorhandene Entity -> unverändert Fachfelder korrekt übernommen.
- saveAll mit gemischten (neue + vorhandene) -> alle persistent.
## Fehlerpfade
- save(null) -> false.
- remove(null) -> false.
- remove(entity ohne ID) -> false.
## Klon Tests
- Quelle & Klon dürfen nicht gleiche ID haben.
- Child Collections tief kopiert (Referenzen ungleich, Werte gleich).
## Tooling Vorschlag
- JUnit 5, Mockito für isolierte Tests von Controller-Hilfsmethoden.
- Testcontainers (Optional später) für realistischere DB.
## Generator Leitplanken
- Für neue Fachlogik minimal 1-2 Unit Tests + 1 Integration Test.
- Keine Abhängigkeit auf Produktionspfade in Unit Tests; Test-spezifische Testdaten-Builder.
---
+40
View File
@@ -0,0 +1,40 @@
# Transactions Instructions
Aktualisiert: 2025-10-20
## Kontext
Container-Managed Transaktionen (Java EE). Verwendung von `@Transactional` auf Manager-Methoden für Schreiboperationen.
## Grundsätze
- Jede persistierende Operation (create, edit, remove) innerhalb einer Transaktion.
- `save` und `saveAll` bereits mit `@Transactional` versehen.
- Leseoperationen können ohne Annotation auskommen (Default: kein Write-Lock nötig).
## Batch Operationen
- `saveAll(Collection<T>)`: Ein Transaktionskontext für gesamte Collection -> entweder komplett erfolgreich oder Abbruch beim Fehler.
- Optimierungspotential: Fehler sammeln, nicht sofort abbrechen.
## Refresh
- `refresh(entity)` führt merge aus; wenn `id == null` vorher persist -> bleibt innerhalb Transaktion falls aufgerufen durch `save`/`saveAll`.
## Remove
- `remove(entity)` ohne `@Transactional` in Basisklasse -> Empfehlung: Annotation hinzufügen in konkretem Manager wenn Delete-Fachlogik erweitert wird.
## Edge Cases
- Verschachtelte Aufrufe (save -> intern create/edit): Container handhabt Propagation (`REQUIRED`).
- LazyInitializationException vermeiden: innerhalb Transaktion initialisieren.
## Empfohlene Annotationen
- Zusätzliche fachliche Write-Methoden stets mit `@Transactional` versehen.
- Pure Read: Performancekritisch -> ggf. explizit `@Transactional(Transactional.TxType.SUPPORTS)` oder weglassen.
## Fehlerfall Verhalten
- Ungefangene RuntimeException -> Rollback durch Container.
- Aktuell Exceptions geloggt & boolean false -> verhindert Rollback. Verbesserung: BusinessException throw + Rollback.
## Generator Leitplanken
- Keine eigenen manuell geöffneten Transaktionen (kein `UserTransaction`).
- Konsistenz: Schreibmethoden annotieren, Leseoperationen nur bei Bedarf.
---
+21
View File
@@ -0,0 +1,21 @@
# Maven Build-Verzeichnis
target/
/target/
# IDE-spezifische Dateien
.idea/
.vscode/
*.iml
*.ipr
*.iws
# OS-spezifische Dateien
.DS_Store
Thumbs.db
# Logs
*.log
# Temporäre Dateien
*.tmp
*.temp
+88
View File
@@ -0,0 +1,88 @@
# mss-failsafe — Git History Archive
> **Why this file exists**
>
> `mss-failsafe` was developed in its own standalone Git repository (no remote) on
> Patrick's workstation. On **2026-06-13** it was consolidated into the `pi_mcps`
> monorepo under [`java/mss-failsafe/`](.) as the single canonical copy. The
> standalone repo's `.git` directory was removed during the flatten, so the original
> per-commit history below is preserved here for reference.
>
> The **flattened working tree** captured at consolidation = tip of `master`
> (`2a142b5`, 2025-10-04) **plus all uncommitted working-tree changes** that were in
> progress at that time (notably the `.github/*.instructions.md` AI-context files and
> `.gitignore` updates). This snapshot is the source of truth and the base for the
> planned upgraded rewrite with Work Lumen.
## Repository Facts (at time of archival)
| Property | Value |
|---|---|
| Standalone repo location | `~/pi_mcps/mss-failsafe/` (top-level, pre-consolidation) |
| New canonical location | `java/mss-failsafe/` (inside `pi_mcps` monorepo) |
| Default branch | `master` |
| Tip commit | `2a142b5` (2025-10-04) |
| Total commits (all branches) | 33 |
| First commit | `f2fa7b6` (2025-06-27) |
| Author | Patrick Plate |
| Remote | none (local-only repo) |
| Uncommitted changes at archival | 175 working-tree entries (preserved in the flatten) |
## Branches (at time of archival)
| Branch | Tip | Date | Note |
|---|---|---|---|
| `master` | `2a142b5` | 2025-10-04 | Default; newest committed state |
| `bugfix/protocol-creation-speed` | `2a142b5` | 2025-10-04 | Same tip as master |
| `bugfix/overview-pdf-counting` | `0226952` | 2025-09-25 | |
| `feature/zusammenfassung-pdf` | `7e2dd63` | 2025-09-17 | Contains `7e2dd63` not on master |
| `feature/all-checklist-at-once` | `7f68bbf` | 2025-09-13 | |
### Commits not reachable from `master`
| Hash | Date | Branch | Subject |
|---|---|---|---|
| `7e2dd63` | 2025-09-17 | `feature/zusammenfassung-pdf` | refactor(questionnaire): improve text handling in SecurityAreaQuestion |
| `b26b211` | 2025-09-15 | (dangling) | refactor: update UI text and logic for machine creation and editing |
## Full Commit Log (all branches, newest first)
| Hash | Date | Subject |
|---|---|---|
| `2a142b5` | 2025-10-04 | feat(ticket): optimize ticket fetching and indexing for protocol generation |
| `a28584b` | 2025-10-03 | feat(ticket): optimize protocol generation by reducing DB queries and enhancing ZIP creation logic |
| `0226952` | 2025-09-25 | feat(ticket): add alphabetical ordering for displayed lists and data tables |
| `06f1b0a` | 2025-09-25 | fix(ticket): handle null device roles in security device filtering |
| `5cba533` | 2025-09-17 | feat(ticket): enhance danger point and inspection data in overview protocol |
| `7e2dd63` | 2025-09-17 | refactor(questionnaire): improve text handling in SecurityAreaQuestion |
| `b26b211` | 2025-09-15 | refactor: update UI text and logic for machine creation and editing |
| `d535571` | 2025-09-15 | refactor(ticket): make DateTimeFormatter immutable in OverviewProtocolController |
| `593080e` | 2025-09-15 | refactor(ticket): remove unused imports in OverviewProtocolController |
| `44cb289` | 2025-09-15 | fix(ticket): correct typo in column header and reset flag in protocol generation |
| `1fd6bcf` | 2025-09-14 | feat(ticket): add overview protocol generation and integration |
| `7f68bbf` | 2025-09-13 | refactor(ticket): streamline PDF generation and file handling for machine protocols |
| `daeacc9` | 2025-09-13 | feat(ticket): add functionality to generate and download all machine protocols as ZIP |
| `0166206` | 2025-09-13 | feat(questionnaire): enhance UI and improve questionnaire management logic |
| `24da4a1` | 2025-08-29 | feat(questionnaire): improve questionnaire integration and UI enhancements |
| `913efbb` | 2025-08-29 | feat(questionnaire): enhance questionnaire handling with better UI and data loading |
| `b3782fc` | 2025-08-29 | feat(questionnaire): enhance question position management and text handling |
| `884cb80` | 2025-08-25 | fix(security-area): resolve lazy loading and eager collection conflict for questionnaires |
| `3fd6e2e` | 2025-08-24 | Admin: add System-Logs page and LogFileManager; update admin menu; set active RollingFile to /logs/application.log |
| `11d96cd` | 2025-08-16 | feat(admin): neue Admin-Views (password reset, user management) und Controller |
| `6238521` | 2025-07-26 | Refactor: UserRoleValidationManager als Startup-Service konfiguriert (@Singleton/@Startup, @PostConstruct validateRolesOnStartup) |
| `eeb329d` | 2025-07-26 | Feature: UserRoleValidationManager für automatische Rollenkorrektur |
| `2d72946` | 2025-07-26 | Dokumentation: Detaillierte Kommentare zur Person-Entität hinzugefügt |
| `66bb699` | 2025-07-20 | Implement hierarchical user role assignment system (UserRoleAssignmentHelper) |
| `8ee06b4` | 2025-07-20 | Add comprehensive JavaDoc comments to model classes |
| `3438bcb` | 2025-07-20 | Remove target directory from repository and improve .gitignore |
| `ed70e9f` | 2025-07-15 | Update .gitignore to include an empty /target/ directory |
| `7072410` | 2025-07-03 | Improve UI loading experience and update controller logic |
| `0d9c1fa` | 2025-06-27 | Update multiple controller classes across various modules |
| `0ab4495` | 2025-06-27 | Refactor controllers to remove session dependency and improve transaction management; add .gitignore for target directory |
| `f2fa7b6` | 2025-06-27 | Add initial project structure and configuration files |
---
*Archived 2026-06-13 during the mss-failsafe consolidation into `pi_mcps`. The full
binary Git history of the original standalone repo was not migrated — only this
human-readable log plus the final flattened working tree.*
@@ -0,0 +1,199 @@
# Questionnaires für SecurityArea - Implementierungsplan
## Übersicht
Implementierung der Funktionalität zur Hinzufügung von Fragebögen (Questionnaires) zu SecurityArea-Entitäten in den Basisdaten. Dies ermöglicht es, Fragebögen bereits bei der Erstellung von Sicherheitsbereichen zu hinterlegen, anstatt erst bei der Ticket-Erstellung.
## Hintergrund
- **Aktueller Stand**: Questionnaires werden nur zu TicketSecurityArea hinzugefügt
- **Neues Ziel**: Questionnaires sollen auch zu SecurityArea (Basisdaten) hinzugefügt werden können
- **Grund**: Verbesserung des Workflows entsprechend Kundenwunsch
## Schritt-für-Schritt Implementierung
### 1. Datenbankmodell erweitern
#### 1.1 SecurityArea Entität anpassen
- **Datei**: `src/main/java/model/securityarea/SecurityArea.java`
- **Aufgabe**:
- Neue Beziehung zu Questionnaires hinzufügen
- `@OneToMany` Mapping für `SecurityAreaQuestionnaire` implementieren
- Ähnlich wie bei TicketSecurityArea → SecurityAreaQuestionnaire
#### 1.2 Neue Entität: SecurityAreaQuestionnaire
- **Datei**: `src/main/java/model/securityarea/SecurityAreaQuestionnaire.java` (neu erstellen)
- **Aufgabe**:
- Entität ähnlich zu `TicketSecurityAreaQuestionnaire` erstellen
- `@ManyToOne` Beziehung zu SecurityArea
- `@OneToMany` Beziehung zu SecurityAreaQuestion
- Konstruktor für Kopieren von Questionnaire → SecurityAreaQuestionnaire
#### 1.3 Neue Entität: SecurityAreaQuestion
- **Datei**: `src/main/java/model/securityarea/SecurityAreaQuestion.java` (neu erstellen)
- **Aufgabe**:
- Entität ähnlich zu `SecurityAreaQuestion` aus tickets package erstellen
- `@ManyToOne` Beziehung zu SecurityAreaQuestionnaire
- Alle notwendigen Felder für Fragen implementieren
### 2. Business Logic implementieren
#### 2.1 SecurityAreaManager erweitern
- **Datei**: `src/main/java/business/securityarea/SecurityAreaManager.java`
- **Aufgabe**:
- Methoden für Questionnaire-Management hinzufügen
- `addQuestionnaireToSecurityArea()`
- `removeQuestionnaireFromSecurityArea()`
- `getAvailableQuestionnaires()`
#### 2.2 QuestionnaireManager anpassen
- **Datei**: `src/main/java/business/questions/QuestionnaireManager.java`
- **Aufgabe**:
- Methoden für SecurityArea-Questionnaire Verknüpfung
- Validierung für Questionnaire-Zuordnung
### 3. Controller Layer erweitern
#### 3.1 SecurityAreaController anpassen
- **Datei**: `src/main/java/controller/securityarea/SecurityAreaController.java`
- **Aufgabe**:
- Properties für Questionnaire-Auswahl hinzufügen
- `selectedQuestionnaire`, `availableQuestionnaires`
- Methoden: `addQuestionnaireToArea()`, `removeQuestionnaireFromArea()`
- Integration in `save()` und `editSelected()` Methoden
#### 3.2 Neue Controller für SecurityAreaQuestionnaire
- **Datei**: `src/main/java/controller/securityarea/SecurityAreaQuestionnaireController.java` (optional)
- **Aufgabe**:
- Dedicated Controller für Questionnaire-Management
- Ähnlich zu bestehenden Questionnaire-Controllern
### 4. UI Implementation
#### 4.1 Hauptseite erweitern
- **Datei**: `src/main/webapp/resources/user/sec/create.xhtml`
- **Aufgabe**:
- Neue Sektion für Questionnaires in SecurityArea-Details hinzufügen
- Position: Nach den DangerPoints, vor dem Bottom-Bereich
- Accordion-Panel für Questionnaires ähnlich wie bei SecurityDevices
#### 4.2 Questionnaire-Management UI
- **Komponenten hinzufügen**:
- DataTable für angehängte Questionnaires
- Buttons: "Fragebogen hinzufügen", "Bearbeiten", "Entfernen"
- Dialog für Questionnaire-Auswahl
- Dialog für Questionnaire-Bearbeitung
#### 4.3 Dialog-Implementierung
- **Neue Dialogs in create.xhtml**:
- `dlgAddQuestionnaire`: Auswahl verfügbarer Questionnaires
- `dlgEditQuestionnaire`: Bearbeitung der Questions
- Ähnlich zu bestehenden Device/DangerPoint Dialogs
### 5. Integration und Workflow
#### 5.1 Ticket-Erstellung anpassen
- **Aufgabe**: Bei Ticket-Erstellung Questionnaires von SecurityArea automatisch kopieren
- **Dateien**:
- Ticket-Erstellungs-Controller
- TicketSecurityArea-Business-Logic
#### 5.2 Copy/Clone Funktionalität
- **Aufgabe**: Beim Kopieren von SecurityAreas auch Questionnaires mitkopieren
- **Betroffene Methoden**: `cloneToSelectedMachine()` in SecurityAreaController
### 6. UI/UX Details
#### 6.1 SecurityArea-Karte erweitern
```html
<!-- Nach DangerPoints Accordion hinzufügen -->
<div class="p-col-12">
<p:accordionPanel value="#{securityAreaController.selected.questionnaires}" var="questionnaire" activeIndex="#">
<p:tab title="Fragebogen: #{questionnaire.name}">
<!-- Questionnaire Details und Questions anzeigen -->
</p:tab>
</p:accordionPanel>
</div>
```
#### 6.2 Questionnaire-Management Buttons
- In das "Mehr"-Menü (dynaButton) integrieren
- Neue Menüpunkte: "Fragebogen hinzufügen", "Fragebögen verwalten"
### 7. Testing und Validierung
#### 7.1 Unit Tests
- **Neue Test-Klassen**:
- `SecurityAreaQuestionnaireTest`
- `SecurityAreaQuestionTest`
- `SecurityAreaManagerTest` (erweitern)
#### 7.2 Integration Tests
- Questionnaire-Zuordnung zu SecurityArea
- Kopieren von Questionnaires bei Ticket-Erstellung
- UI-Workflow Tests
#### 7.3 Datenbank-Migration
- **Aufgabe**: SQL-Scripts für neue Tabellen erstellen
- Tables: `SECURITY_AREA_QUESTIONNAIRE`, `SECURITY_AREA_QUESTION`
- Foreign Key Constraints definieren
## Reihenfolge der Implementierung
1. **Phase 1**: Datenbankmodell (SecurityAreaQuestionnaire, SecurityAreaQuestion)
2. **Phase 2**: Business Logic (Manager-Erweiterungen)
3. **Phase 3**: Controller Layer (SecurityAreaController erweitern)
4. **Phase 4**: UI Implementation (create.xhtml erweitern)
5. **Phase 5**: Integration (Ticket-Erstellung anpassen)
6. **Phase 6**: Testing und Finalisierung
## Wichtige Überlegungen
### Datenmodell-Konsistenz
- Questionnaires in SecurityArea müssen kompatibel zu TicketSecurityArea sein
- Beim Ticket-Erstellen: SecurityArea-Questionnaires → TicketSecurityArea kopieren
### UI-Konsistenz
- Gleiche Bedienung wie bei SecurityDevices/DangerPoints
- Accordion-Panel für übersichtliche Darstellung
- Standard CRUD-Operationen (Create, Read, Update, Delete)
### Performance
- Lazy Loading für Questionnaires implementieren
- Effizienter Datentransfer bei Questionnaire-Kopierung
## Dependencies
### Neue Dateien zu erstellen:
- `model/securityarea/SecurityAreaQuestionnaire.java`
- `model/securityarea/SecurityAreaQuestion.java`
- Controller-Erweiterungen
- UI-Erweiterungen in create.xhtml
### Bestehende Dateien zu modifizieren:
- `model/securityarea/SecurityArea.java`
- `controller/securityarea/SecurityAreaController.java`
- `business/securityarea/SecurityAreaManager.java`
- `resources/user/sec/create.xhtml`
## Zeitschätzung
- **Phase 1-2**: 2-3 Tage (Backend)
- **Phase 3-4**: 3-4 Tage (Controller + UI)
- **Phase 5-6**: 2-3 Tage (Integration + Testing)
- **Gesamt**: ~8-10 Arbeitstage
## Risiken und Mitigationen
### Datenbankmigrationen
- **Risiko**: Bestehende Daten könnten beeinträchtigt werden
- **Mitigation**: Backup vor Migration, schrittweise Einführung
### UI-Komplexität
- **Risiko**: UI wird zu überladen
- **Mitigation**: Accordion-Panel verwenden, ähnlich zu bestehenden Komponenten
### Performance
- **Risiko**: Laden von vielen Questionnaires könnte langsam werden
- **Mitigation**: Lazy Loading, Paging implementieren
---
**Nächste Schritte**: Mit Phase 1 (Datenbankmodell) beginnen und die SecurityAreaQuestionnaire Entität implementieren.
+64
View File
@@ -0,0 +1,64 @@
# AI Instruktionen für Code-Generierung
Ziel: Ein konsistenter Kontext für zukünftige automatische Ergänzungen.
## Projekt-Kurzprofil
- Typ: Java EE 8 (javax), WAR, JSF + PrimeFaces, Hibernate JPA.
- Java Version: 11 (Compiler Plugin).
- Schichten: Model (Entities), Business (Manager), Controller (JSF Beans), View (XHTML).
## Wichtige Basisklassen
- `AbstractEntity`: Basisklasse aller Entities (enthält ID Details nicht gezeigt, aber zentral).
- `AbstractManager<T>`: Generisches CRUD mit `save`, `saveAll`, etc.
- `AbstractController<E>`: UI State & Utilities.
## Konventionen (Kurz)
- Neue Entities: extend `AbstractEntity`, Named Queries definieren.
- Neue Manager: `@Stateless`, extends `AbstractManager<Entity>`, implementiert `getEntityManager()`.
- Neue Controller: `@Named`, Scope Annotation (`@ViewScoped` etc.), extends `AbstractController<Entity>`, implementiert `getManager()`.
- Temporäre (nicht persistierte) Objekte: negative ID (Fake) bis finalem Speichern.
## CRUD Muster (Beispiel)
```java
@Stateless
public class ExampleEntityManager extends AbstractManager<ExampleEntity> {
@PersistenceContext(name = "pu_person") EntityManager em;
public ExampleEntityManager(){ super(ExampleEntity.class); }
@Override protected EntityManager getEntityManager(){ return em; }
}
```
## Typische Aufgaben für Generierung
1. Neue Domain + Entity + Manager + Controller + XHTML.
2. PDF-Ausgabe Erweiterung.
3. Batch-Speichern mehrerer neuer Entities.
4. Klon-Operationen: Copy-Konstruktor nutzen, Child-IDs auf `null` setzen, Referenzen neu knüpfen.
5. Fragebogen-Workflow: add/remove (siehe `QUESTIONNAIRE_WORKFLOW.md`).
## Qualitätssicherung
- Nach Code-Erzeugung: Prüfe Kompilierung (mvn -q test / package).
- Sicherstellen: Keine direkten System.out Prints in Produktionscode.
- Logging: `LOGGER.error(e)` bei Exceptions.
## Häufige Edge Cases
- Null Checks vor Persist/Remove.
- Negative ID Objekte dürfen nicht direkt gelöscht (erst persistieren oder aus Collection entfernen).
- Lazy Collections müssen initialisiert vor Iteration (via `refresh` oder `Hibernate.initialize`).
## Verbesserungsvorschläge (safe additions)
- Service Layer Einführen (wenn Logik Manager übersteigt).
- Bean Validation (@NotNull, @Size) auf Entities.
- Einheitliche Exception Klasse.
## Generierungs-Präferenzen
- Bevorzugt vorhandene Muster exakt replizieren.
- Keine neuen Frameworks ohne Bedarf.
- Code kommentieren nur bei komplexer Logik.
## Anti-Pattern vermeiden
- Business Logik direkt im Controller.
- Duplizierte Query Strings ohne Named Query.
---
Letzte Aktualisierung: 2025-10-20
+63
View File
@@ -0,0 +1,63 @@
# Architekturübersicht
## Layer
1. Präsentation: JSF 2.3 + PrimeFaces 11 (XHTML in `webapp/`).
2. Controller Layer: JSF Managed Beans (CDI `@Named`, Scopes) koordiniert UI → Business.
3. Business Layer: Stateless EJB Manager (`business.*Manager`) kapselt Datenzugriff + Fachlogik.
4. Persistenz Layer: JPA (Javax) + Hibernate Provider. Persistence Context Name: `pu_person`.
5. Ressourcen: `src/main/resources` für Log4j2, statische Texte, Checklisten.
## Zentrale Basisklassen
### AbstractManager<T extends AbstractEntity>
- Generisches CRUD: `save`, `saveAll`, `create`, `edit`, `remove`, `refresh`, `find`, `findAll`, `count`.
- Flush nach Persist/Merge (stellt zeitnah DB-Konsistenz sicher).
- Fehlerbehandlung: try/catch + Logging (Verbesserungspotential: Konsistente Exception).
### AbstractController<ENT extends AbstractEntity>
- UI State: `selected`, `created`, `entities`.
- Utility: Faces Messages, PDF Hilfsmethoden, Fake-ID Generator für neue Entities vor Persist.
- `refrehSelected()`: Re-merge & initialize Lazy Collections.
## Entity Lebenszyklus (UI Sicht)
1. Nutzer erstellt neues Objekt → Controller vergibt Fake-ID (negativ) mittels `createFakeID(Collection<ENT>)`.
2. Objekt wird in Listen angezeigt, kann editiert werden bevor persistiert.
3. Beim Speichern werden alle mit `id < 0` auf `null` gesetzt; `AbstractManager.saveAll()` persistiert.
4. Nach Persist: DB generiert positive ID.
## Sicherheitsbereich (SecurityArea)
- Enthält Listen: `SecurityDevices`, `DangerPoints`, `SwitchingDevices`, `Questionnaires`.
- Manager-Methoden: `cloneArea`, add/remove Questionnaire, `reloadWithQuestionnaires`.
- Klonen: Erst DB laden (falls persistent), dann Kopie via Copy-Konstruktor `new SecurityArea(area)`.
## Fragebogen-Zuordnung
- Hinzufügen: `SecurityAreaManager.addQuestionnaireToSecurityArea` erzeugt `SecurityAreaQuestionnaire` Wrapper (assoziative Entity) & persistiert.
- Entfernen: Entities werden aus Sammlung entfernt und via `em.remove(questionnaire)` gelöscht.
- Verfügbare Fragebögen: Alle `Questionaire` minus bereits zugeordnete (Filter per Namen potentielles Verbesserungspotential: Verwendung IDs statt Name).
## Transaktionen
- Methoden mit Schreiboperationen annotiert `@Transactional` (EJB Container verwaltet JTA). In `AbstractManager` ebenfalls.
## Logging
- Log4j2 überall via `LogManager.getLogger(...)`. Konfiguration: `log4j2.xml`.
## PDF Generierung
- `AbstractController` Hilfsmethoden zur Tabellen-Erstellung (iText7) + gemischte Nutzung iText5 (itextpdf 5.5.13) Migration empfohlen.
## Erweiterbarkeit
- Neue Fachbereiche folgen Pattern: Entity → Manager → Controller → UI.
- Reusable generische Methoden vermeiden Duplikate (Beispiel: `saveAll`, `refresh`).
## Bekannte technische Schulden
- Mischung iText5 & iText7.
- Fehlerbehandlung inkonsistent (bool Rückgaben + Logging).
- Kein einheitlicher DTO Layer Controller arbeitet direkt auf Entities (Risk: Lazy Loading im View).
- Wenige Tests im `test/` Verzeichnis.
## Ideen für Refactoring
- Einführung Service Layer (falls Business Logik komplexer wird) zw. Controller und Manager.
- Exceptions mit Custom Runtime (`BusinessException`) statt stiller bool False.
- Verwendung Criteria / NamedQueries für Wiederverwendbarkeit (z.Z. direkte Query Strings in Manager).
---
Aktualisiert: 2025-10-20
+61
View File
@@ -0,0 +1,61 @@
# Coding Guidelines
## Allgemein
- Sprache: Deutsch in UI-Messages, Englisch im Code (Klassennamen etc.).
- Einrückung: 4 Spaces (NetBeans Standard). Keine Tab-Mischung.
- Zeilenlänge: Empfehlung max. 140 Zeichen.
- Vermeide überflüssige System.out.println nutze Logger.
## Packages
- `business.<domain>` für Manager/Logik.
- `controller.<domain>` für JSF Backing Beans.
- `model.<domain>` für Entities/Enums.
## Benennung
- Manager endet auf `Manager` (CRUD + Fachlogik).
- Controller endet auf `Controller`.
- Entities nutzen Substantive singular (`SecurityArea`, `DangerPoint`).
- Enums nutzen PascalCase Konstanten (`APPROACH_SPEED`, hier bereits gemischt beibehalten vorhandene Fälle, Konsistenz bei neuen).
## Fehlerbehandlung
- Aktuell: Logging + bool Rückgabe. Bei neuen komplexen Methoden: Ziehe eigene Exceptions in Betracht (`BusinessException`).
- Niemals stack trace verlieren immer loggen. Falls Benutzerfeedback nötig → Message über Controller.
## Persistenz
- Vor Persist neuer Entity: ID muss `null` sein (oder negativ Fake-ID nur temporär). Setze beim finalen Speichern negative IDs auf `null`.
- Verwende `AbstractManager.refresh(entity)` um Lazy Collections zu initialisieren.
## Transaktionen
- Schreibmethoden erhalten `@Transactional`. Beim EJB Stateless reicht oft Container-Transaktion; Annotation verstärkt Klarheit.
## Performance
- Sammel-Speicheroperationen bevorzugt `saveAll(Collection<T>)` statt Schleifen mit einzelnen Flushes.
- Beim Klonen großer Objektgraphen prüfen: Nur notwendige Collections initialisieren.
## UI / JSF
- Vermeide direkte Änderungen an listengebundenen Collections ohne Aktualisierung des Backing Beans (PrimeFaces kann sonst nicht updaten).
- Nutze klare Dialog-Helper (closeDialogs) statt roher JavaScript Strings.
## PDF
- Konsistenz: Verwende iText7 API für neue Funktionen. Markiere Altcode (iText5) für spätere Entfernung.
## Sicherheit
- Keine sensiblen Daten in Logs.
- Prüfe vor Löschaktionen, dass referenzielle Integrität gewährleistet (vor Entfernen Beziehungen lösen, wie im `deleteSelected()` umgesetzt).
## Tests (Empfehlung)
- JUnit + Arquillian für EJB/Entity Tests.
- Test-Namensschema: `<ClassName>Test`.
- Mindestens: CRUD, Klonen, Fragebogen hinzufügen/entfernen.
## Kommentar-Stil
- Klassenheader: Kurze Beschreibung Funktion/Zweck.
- Methoden: JavaDoc nur bei komplexer Logik / öffentlich verwendeten APIs.
## Anti-Pattern vermeiden
- "God Controller" trenne Verantwortlichkeiten (nicht alles in einem Controller ansammeln).
- Direkte UI-Logik im Manager vermeiden.
---
Aktualisiert: 2025-10-20
+72
View File
@@ -0,0 +1,72 @@
# How-To: Projekt erweitern
## Neuer Fachbereich (Beispiel: InspectionReport)
### 1. Entity anlegen
- Paket: `model.report`.
- Klasse: `InspectionReport extends AbstractEntity`.
- Felder: `date`, `inspector`, `machine`, `remarks`.
- Named Queries definieren (z.B. `FIND_BY_MACHINE`).
### 2. Manager
```java
@Stateless
@Named
public class InspectionReportManager extends AbstractManager<InspectionReport> {
@PersistenceContext(name = "pu_person")
EntityManager em;
public InspectionReportManager() { super(InspectionReport.class); }
@Override protected EntityManager getEntityManager() { return em; }
// Fachmethoden: findByMachine(Long id)
}
```
### 3. Controller
```java
@ViewScoped
@Named
public class InspectionReportController extends AbstractController<InspectionReport> {
@EJB InspectionReportManager reportManager;
@Inject MachineController machineController;
public InspectionReportController() { setSelected(new InspectionReport()); setCreated(new InspectionReport()); }
@Override protected AbstractManager<InspectionReport> getManager() { return reportManager; }
@Override public void clearEntries() { setSelected(new InspectionReport()); setCreated(new InspectionReport()); getEntities().clear(); }
public void saveReport(){ reportManager.save(getSelected()); successMessage(); }
}
```
### 4. UI Seite
- Pfad: `webapp/report/inspection.xhtml`.
- Binding: `#{inspectionReportController}`.
- Komponenten: Formular für Felder + Speichern Button.
### 5. Navigation
- Menüeintrag in globaler Navigationsstruktur (Tree oder Topbar) analog `createMachineMenu()` Ansatz.
### 6. Tests
- Persistenz Test: Speichern + Laden.
- Manager Fachmethode Test.
## Erweiterung vorhandener Funktionalität
- Beispiel: Neue PDF Sektion → Ergänze Hilfsmethode in `AbstractController` (sofern allgemein). Falls spezifisch für eine Domäne, eher Hilfsklasse im Domain-Paket.
## Konsistenz-Checkliste
- [ ] Entity extends `AbstractEntity`
- [ ] Manager extends `AbstractManager`
- [ ] Controller extends `AbstractController`
- [ ] Negative IDs für neue Objekte vor Persist (falls in Listen)
- [ ] Internationale Zeichen (UTF-8) POM setzt Encoding
- [ ] Logging bei Fehlern
## Deployment Hinweise
- Sicherstellen, dass neue Named Queries beim Serverstart verfügbar (Entity korrekt gescannt).
- Falls neue Ressourcen (Logos, Templates) → in `resources` pflegen.
## Typische Stolpersteine
- LazyInitializationException: Lösung `refresh(entity)` oder explizite Initialisierung im Manager.
- Doppelte Referenzen beim Klonen: IDs auf `null` setzen.
- Fehlende Transaktion: Sicherstellen `@Transactional` oder EJB Standard.
---
Aktualisiert: 2025-10-20
+52
View File
@@ -0,0 +1,52 @@
# Workflow: Fragebögen in Sicherheitsbereichen
## Ziel
Zuordnung strukturiert erfassbarer Fragebögen (`Questionaire`) zu einem `SecurityArea` via `SecurityAreaQuestionnaire`.
## Beteiligte Klassen
- `SecurityAreaController` UI Aktionen (add/remove, refresh).
- `SecurityAreaManager` Persistenzoperationen (add/remove/reload, Verfügbarkeitsliste).
- `SecurityAreaQuestionnaire` Assoziative Entity (enthält Name / ID des Fragebogens + Bezug zum Schutzbereich).
- `QuestionaireManager` (nicht gezeigt) Verwaltung aller Fragebögen.
## Hinzufügen Ablauf
1. Nutzer wählt Schutzbereich + Fragebogen im Dialog.
2. Controller ruft `securityAreaManager.addQuestionnaireToSecurityArea(selectedArea, selectedQuestionnaire)`.
3. Manager:
- Lädt `area` (falls persistent) neu per `em.find`.
- Erzeugt neues `SecurityAreaQuestionnaire` Objekt.
- Setzt Relation (wrapper.setArea(area)).
- Persistiert Wrapper, merged Area.
4. Controller: `refrehSelected()` (Merge + Initialize Lazy Collections), zeigt Erfolgsmeldung.
## Entfernen Ablauf
1. Nutzer wählt zugeordneten Fragebogen (Wrapper-Objekt).
2. Controller ruft `securityAreaManager.removeQuestionnaireFromSecurityArea(area, wrapper)`.
3. Manager lädt Entities (falls notwendig), entfernt aus Collection, `em.remove(wrapper)`, `em.merge(area)`.
4. Controller aktualisiert Verfügbare Liste.
## Verfügbare Fragebögen
- Abfrage aller Fragebögen: `SELECT q FROM Questionaire q ORDER BY q.name`.
- Filter: Namen bereits zugeordneter Wrapper (Verbesserung: Filter per ID zur Sicherheit gegen Namensduplikate).
## Edge Cases
- Bereich / Fragebogen null: Controller zeigt Fehlermeldung.
- Concurrent Änderung: Nach Persist immer Refresh durchführen.
- Doppelte Zuordnung: Filter verhindert erneute Anzeige; Manager könnte zusätzlich prüfen (Collection enthält bereits Name).
## Verbesserungen
- Validierung auf Einzigartigkeit im Manager (statt nur UI Filter).
- Optimierte Fetch Strategie (JOIN FETCH) bei Reload.
- Nutzung eines Service zur Kapselung Geschäftslogik + Manager nur für CRUD.
## Beispiel Pseudocode (Hinzufügen)
```java
if (questionnaire != null && area != null) {
areaManager.addQuestionnaireToSecurityArea(area, questionnaire);
controller.refrehSelected();
}
```
---
Aktualisiert: 2025-10-20
+97
View File
@@ -0,0 +1,97 @@
# Mechanismus: Refresh & Fake-ID
## Problemstellung
Im UI werden häufig neue Objekte in Collections angezeigt, bevor sie persistiert sind. Diese benötigen eine temporäre Identifikation (ID) für Auswahl/Operationen, ohne den Datenbankzustand zu verfälschen.
## Fake-ID Strategie
- Negative Long Werte (< 0) kennzeichnen nicht persistierte Objekte.
- Erzeugung: `AbstractController.createFakeID(Collection<ENT>)`:
- Startwert -1.
- Falls bereits negative IDs existieren → Nimmt die kleinste negative und subtrahiert 1.
- Ergebnis: Sequenz -1, -2, -3 ... (absteigend).
## Persistieren
Vor finalem Speichern (z.B. `SecurityAreaController.save()`):
1. Alle Entities mit `id < 0``setId(null)`.
2. `AbstractManager.saveAll()` unterscheidet durch `entity.getId() == null` zwischen Persist und Merge.
3. Datenbank vergibt positive ID (Auto Increment / Sequence).
## Vorteile
- Klare Unterscheidung UI-temporär vs. persistent.
- Verhindert versehentliches Auslösen von Merge bei noch nicht existierenden DB Zeilen.
## Risiken
- Verwechslung negativ gesetzter IDs mit echten IDs (nicht möglich, da DB positive IDs generiert).
- Direkte Verwendung negativer IDs in DB-Operationen (vermeiden: Prüfen auf `id > 0` vor `em.find`).
## Refresh
- Methode `AbstractController.refrehSelected()` (Tippfehler im Namen, historisch) ruft `getManager().refresh(selected)`.
- `AbstractManager.refresh(entity)`:
- Falls ID null → `save(entity)` (persistiert neues Objekt).
- `merge` für Managed Zustand und `Hibernate.initialize(entity)` zur Lazy Init.
## Best Practices
- Nach komplexen Änderungen (Add/Remove Child Collections) Refresh durchführen wenn UI weitere Lazy Properties benötigt.
- Beim Klonen persistenter Objekte zuerst DB-Laden → danach Kopie erstellen.
## Potentielle Verbesserungen
- Korrektur Tippfehler `refrehSelected()``refreshSelected()` (Refactoring + Suchanpassungen).
- Kennzeichnung Fake-ID generierender Methoden mit JavaDoc für Klarheit.
---
Aktualisiert: 2025-10-20
# MSS Failsafe Developer Einstieg
Dieses Verzeichnis bündelt technische Instruktionsdateien zur schnelleren Einarbeitung und zur Unterstützung automatischer Code-Generierung.
## Quick Start
1. Java Version: Quell-/Ziellevel im POM: 11 (Property 1.8 ist historisch, der Compiler-Plugin setzt auf 11). Nutze lokal JDK 11.
2. Application Server: Java EE 8 kompatibel (z.B. Payara 5 / WildFly 20 / GlassFish 5). Dependencies `javax.*` statt `jakarta.*`.
3. Build:
```cmd
mvn clean package
```
Ergebnis: `target/mss-1.0-SNAPSHOT.war`.
4. Deployment: WAR in kompatiblen EE 8 Server einspielen. Konfiguriere Datenquelle `pu_person` (JPA Persistence Unit siehe `@PersistenceContext(name = "pu_person")`).
5. Logging: Log4j2 Konfiguration in `src/main/resources/log4j2.xml`.
6. Frontend: JSF 2.3 + PrimeFaces 11 + PrimeFlex 2.0.
## Wichtigste Schichten
- model: JPA Entities (`AbstractEntity` Basis enthält ID).
- business: `*Manager` Klassen (Stateless EJBs) kapseln CRUD + Fachlogik.
- controller: View/Request/Session Scoped JSF Backing Beans (Interaktion UI ↔ Business Layer).
- webapp: XHTML Seiten (JSF Components + PrimeFaces).
- resources: Text-/Konfigurationsdateien, Checklisten.
## Kern-Patterns
- Manager erben von `AbstractManager<T>` (generisches CRUD mit `save`, `saveAll`, `remove`, `refresh`).
- Controller erben von `AbstractController<E>` (Message Handling, Fake-ID-Erzeugung für neue (noch nicht persistierte) Entities, PDF Hilfen, Auswahlzustand `selected/created`).
- Negative IDs (< 0) werden als temporäre (noch nicht persistierte) Objekte verwendet wichtig bei UI-Listen vor Sammel-Speichern.
- Lazy Collections werden vor Nutzung mit `Hibernate.initialize(...)` initialisiert (Refresh/Reload Methoden).
## Erweiterung Schnellanleitung
1. Neue Entity anlegen (JPA @Entity, extends `AbstractEntity`).
2. Manager erstellen: `@Stateless`, extends `AbstractManager<YourEntity>`, implementiert `getEntityManager()`. Zusätzliche Named Queries in Entity definieren.
3. Controller erstellen: `@Named`, Scope festlegen (`@ViewScoped`, `@SessionScoped`, etc.), extends `AbstractController<YourEntity>`, injiziere Manager mit `@EJB`.
4. XHTML Seite/Fragment erstellen und Controller referenzieren (`#{yourController}`) + PrimeFaces Komponenten.
5. Tests (optional, derzeit kaum vorhanden) vorschlagen: Architektur-Test + Manager CRUD Test.
## Fragebögen / Sicherheitsbereiche
Ein ausführlicher Workflow liegt in `QUESTIONNAIRE_WORKFLOW.md` und `SECURITY_AREA_DOMAIN.md`.
## PDF-Erzeugung
- Verwendet iText (5.x + 7.x Module). Utilities liegen in `AbstractController` (Tabellen, Kopfzeilen, Seitennummern).
- Logo Pfad `LOGO_PATH = /rundata/logo.png` stelle sicher, dass Datei beim Deployment verfügbar ist.
## Automatisierte Tools / AI Hinweise
Siehe `AI_INSTRUCTIONS.md` für formatierte Kontextbereitstellung.
## Nächste Verbesserungen (Empfehlungen)
- Konsolidierung auf iText7 (Legacy 5.x entfernen).
- Einheitliche Exception-Strategie (momentan Logging + bool Rückgabe).
- Mehr Unit Tests (Persistenz, Controller Interaktionen via Arquillian / Payara Micro).
- Migrationspfad Richtung Jakarta EE 9+ (Namespace Wechsel).
---
Letzte Aktualisierung: 2025-10-20
+54
View File
@@ -0,0 +1,54 @@
# Domain: SecurityArea
## Zweck
Abbildung eines Schutzbereichs einer Maschine mit zugehörigen Schutzeinrichtungen, Gefahrenstellen und Schaltgeräten sowie Fragebögen zur Bewertung.
## Haupt-Entitäten (Ausschnitt)
- `SecurityArea`
- `SecurityDevice` (Liste in Area)
- `DangerPoint`
- `SwitchingDevice`
- `SecurityAreaQuestionnaire` (assoziative Entity zwischen Schutzbereich und `Questionaire`)
- `Questionaire`
## Lebenszyklus
1. Erstellung im UI: Neues `SecurityArea` Objekt mit Fake-ID (negativ).
2. Bearbeitung von Eigenschaften (Name, Typen/Enums: `ProtectionType`, `MountingPosition`, `OverrunMeasurementType`, `ApproachSpeed`).
3. Hinzufügen von Schutzeinrichtungen/Gefahrenstellen/Schaltgeräten (ebenfalls ggf. mit Fake-ID bis persistiert).
4. Speichern: Negative IDs der neuen Objekte werden auf `null` gesetzt → Persist durch `SecurityAreaManager.save` / Sammelspeicher.
5. Nach Persist: Re-Load (`refresh`/`reloadWithQuestionnaires`) vor weiterer Bearbeitung.
## Klonen
`SecurityAreaManager.cloneArea(SecurityArea area)`:
- Lädt persistente Quelle (falls ID > 0) vollständig.
- Erzeugt neue Kopie via Copy-Konstruktor.
- Controller passt Namen an (`Original (Kopie)`), setzt neue `null` IDs für untergeordnete Objekte.
## Fragebogen-Verknüpfung
### Hinzufügen
- Methode: `addQuestionnaireToSecurityArea(area, questionnaire)`.
- Erzeugt `SecurityAreaQuestionnaire` Wrapper.
- Persist Wrapper, merge Area.
- UI aktualisiert Liste und sendet Erfolgsmeldung.
### Entfernen
- `removeQuestionnaireFromSecurityArea(area, securityAreaQuestionnaire)` entfernt Element aus Sammlung & ruft `em.remove`.
### Verfügbare Fragebögen
- `getAvailableQuestionnaires(area)` holt alle `Questionaire` und filtert bereits zugeordnete anhand Name. Verbesserung: Nutzung ID statt Name zur Eindeutigkeit.
## Konsistenz / Referentielle Integrität
Beim Löschen eines Schutzbereichs (`deleteSelected()` im Controller):
1. Entfernen aus Maschine.
2. Auflösen aller Kind-Referenzen (SwitchingDevices, DangerPoints, SecurityDevices) durch Setzen der Area auf `null`.
3. Entfernen der Kindobjekte via entsprechende Manager (`removeAllIn`).
4. Löschen des `SecurityArea` via Named Query (`SecurityArea.DELETE`).
## Potentielle Verbesserungen
- Cascade Settings genauer prüfen (evtl. kann Teil der manuellen Löschlogik automatisiert werden).
- Validierung (Bean Validation) für Pflichtfelder (Name nicht leer, Enums nicht null soweit fachlich notwendig).
- Nutzung DTOs zur Entkopplung UI ↔ JPA (reduziert Lazy Probleme).
---
Aktualisiert: 2025-10-20
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scene Scope="Project" version="2">
<Scope Scope="Faces Configuration Only"/>
<Scope Scope="Project"/>
<Scope Scope="All Faces Configurations"/>
</Scene>
-130
View File
@@ -1,130 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>mss-failsafe</artifactId>
<groupId>plate.software</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>plate.software</groupId>
<artifactId>mss</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mss-1.0-SNAPSHOT</name>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<failOnMissingWebXml>false</failOnMissingWebXml>
<jakartaee>8.0</jakartaee>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.soteria</groupId>
<artifactId>javax.security.enterprise</artifactId>
<version>1.0</version> <!-- Stable version -->
</dependency>
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>3.11.1</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>primeflex</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${endorsed.dir}</outputDirectory>
<silent>true</silent>
<artifactItems>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<type>jar</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,65 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package business;
import java.util.List;
import javax.persistence.EntityManager;
/**
*
* @author Patrick
* @param <T>
*/
public abstract class AbstractManager<T> {
private Class<T> entityClass;
public AbstractManager(Class<T> entityClass) {
this.entityClass = entityClass;
}
protected abstract EntityManager getEntityManager();
public void create(T entity) {
getEntityManager().persist(entity);
}
public void edit(T entity) {
getEntityManager().merge(entity);
}
public void remove(T entity) {
getEntityManager().remove(getEntityManager().merge(entity));
}
public T find(Object id) {
return getEntityManager().find(entityClass, id);
}
public List<T> findAll() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
return getEntityManager().createQuery(cq).getResultList();
}
public List<T> findRange(int[] range) {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
javax.persistence.Query q = getEntityManager().createQuery(cq);
q.setMaxResults(range[1] - range[0] + 1);
q.setFirstResult(range[0]);
return q.getResultList();
}
public int count() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
javax.persistence.criteria.Root<T> rt = cq.from(entityClass);
cq.select(getEntityManager().getCriteriaBuilder().count(rt));
javax.persistence.Query q = getEntityManager().createQuery(cq);
return ((Long) q.getSingleResult()).intValue();
}
}
@@ -1,36 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package business.user;
import javax.ejb.EJB;
import javax.ejb.Startup;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
/**
*
* @author patri
*/
@Named(value = "DemoManager")
@ApplicationScoped
@Startup
public class DemoManager {
@EJB
PersonManager personManager;
/**
* Creates a new instance of NewJSFManagedBean
*/
public DemoManager() {
runDemos();
}
private void runDemos(){
personManager.demo();
}
}
@@ -1,91 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package business.user;
import java.time.Instant;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.util.UUID.randomUUID;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import exception.InvalidEmailException;
import model.person.Token;
import model.person.enums.TokenType;
import model.person.Person;
import java.util.Arrays;
import static java.time.Instant.now;
/**
*
* @author Patrick
*/
@Stateless
public class TokenManager {
@PersistenceContext(name = "pu_person")
private EntityManager em;
@Inject
PasswordManager passwordManager;
@Inject
PersonManager customerManager;
public String generate(final String email, final String ipAddress, final String description,
final TokenType tokenType) {
String rawToken = randomUUID().toString();
Instant expiration = now().plus(14, DAYS);
save(rawToken, email, ipAddress, description, tokenType, expiration);
return rawToken;
}
public String generateFileToken(final String email, final String description) {
String rawToken = randomUUID().toString();
Instant expiration = now().plus(3, DAYS);
save(rawToken, email, null, description, TokenType.FILE, expiration);
return rawToken;
}
public void save(final String rawToken, final String email, final String ipAddress,
final String description, final TokenType tokenType, final Instant expiration) {
Person user = this.customerManager.getByEmail(email)
.orElseThrow(InvalidEmailException::new);
Token token = new Token();
token.setTokenHash(Arrays.toString(this.passwordManager.hashToken(rawToken)));
token.setExpiration(expiration);
token.setDescription(description);
token.setTokenType(tokenType);
token.setIpAddress(ipAddress);
user.addToken(token);
this.em.persist(user);
}
public void remove(String token) {
this.em.createNamedQuery(Token.REMOVE_TOKEN)
.setParameter("tokenHash", token).executeUpdate();
}
public void removeExpired() {
this.em.createNamedQuery(Token.REMOVE_EXPIRED_TOKEN)
.setParameter("timestamp", Instant.now())
.executeUpdate();
}
}
@@ -1,53 +0,0 @@
package controller;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
import java.io.Serializable;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
/**
*
* @author Patrick
*/
public abstract class AbstractController implements Serializable{
private static final long serialVersionUID = -5908716187853409719L;
protected void sendInfoMessage(String title, String message){
FacesMessage facesMessage = new FacesMessage(
FacesMessage.SEVERITY_INFO, title, message);
addMessage(facesMessage);
}
protected void sendWarnMessage(String title, String message){
FacesMessage facesMessage = new FacesMessage(
FacesMessage.SEVERITY_WARN, title, message);
addMessage(facesMessage);
}
protected void sendErrorMessage(String title, String message){
FacesMessage facesMessage = new FacesMessage(
FacesMessage.SEVERITY_ERROR, title, message);
addMessage(facesMessage);
}
protected void sendFatalMessage(String title, String message){
FacesMessage facesMessage = new FacesMessage(
FacesMessage.SEVERITY_FATAL, title, message);
addMessage(facesMessage);
}
private void addMessage(FacesMessage message) {
FacesContext.getCurrentInstance().addMessage(null, message);
}
protected void errorMessage() {
String title = "Fehler!";
String info = "Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut!";
sendErrorMessage(title, info);
}
}
@@ -1,127 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package controller.person;
import controller.AbstractController;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import model.person.Person;
import model.person.enums.Call;
/**
*
* @author patri
*/
@Named
@RequestScoped
public class PersonEditController extends AbstractController{
@Inject
PersonController personController;
private String email;
private Call call;
private String telefon;
private String password;
private String mobile;
private String fax;
private String firstname;
private String lastname;
private String title;
public PersonEditController() {
}
@PostConstruct
private void loadUserdata(){
Person activePerson = personController.getActiveUser();
email = activePerson.getEmail();
call = activePerson.getCall();
telefon = activePerson.getTelefon();
password = "********";
mobile = activePerson.getMobile();
fax = activePerson.getFax();
firstname = activePerson.getFirstname();
lastname = activePerson.getLastname();
title = activePerson.getTitle();
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Call getCall() {
return call;
}
public void setCall(Call call) {
this.call = call;
}
public String getTelefon() {
return telefon;
}
public void setTelefon(String telefon) {
this.telefon = telefon;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getFax() {
return fax;
}
public void setFax(String fax) {
this.fax = fax;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
@@ -1,92 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package controller.person;
import business.user.PersonManager;
import business.user.UserPictureManager;
import controller.AbstractController;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import model.files.Mime;
import model.files.UserPicture;
import model.person.Person;
import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.file.UploadedFile;
/**
*
* @author patri
*/
@Named
@RequestScoped
public class UserPictureController extends AbstractController {
private UserPicture picture;
@Inject
PersonController personController;
@EJB
UserPictureManager upManager;
@EJB
PersonManager personManager;
public void handleUserPictureUpload(FileUploadEvent event) {
UploadedFile file = event.getFile();
if (file != null && file.getContent() != null && file.getContent().length > 0 && file.getFileName() != null) {
if (createSaveUserPicture(file)) {
sendInfoMessage("Erfolg", this.picture.getName() + " wurde hochgeladen!");
} else {
sendErrorMessage("Fehler", "Es ist ein Fehler aufgetreten. Bitte Versuchen Sie es erneut!");
}
}
}
private boolean createSaveUserPicture(UploadedFile file) {
picture = null;
picture = personController.getActiveUser().getUserPicture();
boolean isNew = picture == null;
if (isNew) {
picture = new UserPicture();
}
setData(picture, file);
Person person = personManager.load(personController.getActiveUser());
person.setUserPicture(picture);
picture.setPerson(person);
if (isNew) {
upManager.create(picture);
} else {
upManager.edit(picture);
}
personManager.save(person);
personController.reloadActivePerson();
return true;
}
private void setData(UserPicture picture, UploadedFile file) {
picture.setName(file.getFileName());
picture.setFileData(file.getContent());
picture.setMime(Mime.getByMimeType(file.getContentType()));
}
public long getMaxFileSize() {
return UserPicture.getSizeLimit();
}
public String getFileTypesRE() {
return UserPicture.getAllowedTypesRE();
}
}
@@ -1,77 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package httpauthenticationmechanism;
import javax.enterprise.context.ApplicationScoped;
import javax.security.enterprise.credential.CallerOnlyCredential;
import javax.security.enterprise.credential.Credential;
import javax.security.enterprise.credential.UsernamePasswordCredential;
import javax.security.enterprise.identitystore.CredentialValidationResult;
import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
import static javax.security.enterprise.identitystore.CredentialValidationResult.NOT_VALIDATED_RESULT;
import javax.security.enterprise.identitystore.IdentityStore;
import business.user.PersonManager;
import exception.InvalidCredentialException;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ejb.EJB;
import model.person.Person;
/**
*
* @author Patrick
*/
@ApplicationScoped
public class AppIdentityStore implements IdentityStore {
@EJB
PersonManager userManager;
@Override
public int priority() {
return 90;
}
@Override
public CredentialValidationResult validate(Credential credential) {
try {
// check if the credential was UsernamePasswordCredential
if (credential instanceof UsernamePasswordCredential) {
String username = ((UsernamePasswordCredential) credential).getCaller();
String password = ((UsernamePasswordCredential) credential).getPasswordAsString();
return validate(this.userManager.getByEmailAndPassword(username, password));
}
// check if the credential was CallerOnlyCredential
if (credential instanceof CallerOnlyCredential) {
String username = ((CallerOnlyCredential) credential).getCaller();
return validate(
this.userManager.getByEmail(username)
.orElseThrow(InvalidCredentialException::new)
);
}
} catch (InvalidCredentialException e) {
return INVALID_RESULT;
}
return NOT_VALIDATED_RESULT;
}
private CredentialValidationResult validate(Person person) {
Set<String> groups;
groups = person.getUserGroups().stream()
.map(gr -> gr.toString())
.collect(Collectors.toSet());
return new CredentialValidationResult(person.getEmail(), groups);
}
}
@@ -1,85 +0,0 @@
package httpauthenticationmechanism;
import business.user.PersonManager;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.security.enterprise.AuthenticationStatus;
import javax.security.enterprise.authentication.mechanism.http.AutoApplySession;
import javax.security.enterprise.authentication.mechanism.http.CustomFormAuthenticationMechanismDefinition;
import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
import javax.security.enterprise.authentication.mechanism.http.HttpMessageContext;
import javax.security.enterprise.authentication.mechanism.http.LoginToContinue;
import javax.security.enterprise.authentication.mechanism.http.RememberMe;
import javax.security.enterprise.credential.Credential;
import javax.security.enterprise.identitystore.IdentityStore;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
*
* @author Patrick
*/
@AutoApplySession // For "Is user already logged-in?"
@RememberMe(
cookieMaxAgeSeconds = 60 * 60 * 24 * 14, // 14 days
cookieSecureOnly = false, // Remove this when login is served over HTTPS.
isRememberMeExpression = "#{self.isRememberMe(httpMessageContext)}"
)
@LoginToContinue(
loginPage = "/index.xhtml",
errorPage = "/error.xhtml",
useForwardToLogin = true
)
@ApplicationScoped
public class ApplicationConfig implements HttpAuthenticationMechanism{
final static Logger LOGGER = LogManager.getLogger(ApplicationConfig.class);
public ApplicationConfig() {
}
@Inject
private IdentityStore identityStore;
@Inject
private ManagedPerson managedPerson;
@EJB
private PersonManager personManager;
@PostConstruct
private void init(){
managedPerson.getLogins();
personManager.demo();
System.out.println("PostConstruct DEMO");
}
@Override
public AuthenticationStatus validateRequest(HttpServletRequest req, HttpServletResponse res, HttpMessageContext context) {
Credential credential = context.getAuthParameters().getCredential();
if (credential != null) {
return context.notifyContainerAboutLogin(this.identityStore.validate(credential));
} else {
return context.doNothing();
}
}
// this was called on @RememberMe annotations
public Boolean isRememberMe(HttpMessageContext httpMessageContext) {
return httpMessageContext.getAuthParameters().isRememberMe();
}
@Override
public void cleanSubject(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) {
HttpAuthenticationMechanism.super.cleanSubject(request, response, httpMessageContext);
}
}
@@ -1,45 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package httpauthenticationmechanism;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Named;
import javax.enterprise.context.ApplicationScoped;
/**
*
* @author Patrick
*/
@Named(value = "managedPerson")
@ApplicationScoped
public class ManagedPerson {
private Set<String> logins;
/**
* Creates a new instance of ManagedCustomer
*/
public ManagedPerson() {
}
public Set<String> getLogins(){
if (this.logins == null) {
this.logins = new HashSet<>();
}
return this.logins;
}
public void addLogin(String user){
getLogins().add(user);
}
public void removeLogin(String user){
getLogins().remove(user);
}
}
@@ -1,214 +0,0 @@
package model.adresses;
import java.util.Objects;
import javax.persistence.MappedSuperclass;
import javax.validation.constraints.NotNull;
import model.AbstractEntity;
/**
*
* @author Patrick
*/
@MappedSuperclass
public class Address extends AbstractEntity{
//Land
@NotNull(message = "Land darf nicht null sein")
private String country;
//Straßenname
@NotNull(message = "Strasse darf nicht null sein")
private String street;
//Hausnummer
@NotNull(message = "Hausnummer darf nicht null sein")
private String number;
//Zusatz
private String extra;
//PLZ
@NotNull(message = "PLZ darf nicht null sein")
private Integer postnumber;
//Bundesland
@NotNull(message = "Bundesland darf nicht null sein")
private String county;
//Ort
@NotNull(message = "Ort darf nicht null sein")
private String place;
private String contact;
private String comment;
public Address() {
}
public Address(String street, String number, String extra, Integer postnumber, String county, String place) {
this.street = street;
this.number = number;
this.extra = extra;
this.postnumber = postnumber;
this.county = county;
this.place = place;
}
public Address(String country, String street, String number, String extra, Integer postnumber, String county, String place) {
this.country = country;
this.street = street;
this.number = number;
this.extra = extra;
this.postnumber = postnumber;
this.county = county;
this.place = place;
}
public Address(String country, String street, String number, String extra, Integer postnumber, String county, String place, String contact, String comment) {
this.country = country;
this.street = street;
this.number = number;
this.extra = extra;
this.postnumber = postnumber;
this.county = county;
this.place = place;
this.contact = contact;
this.comment = comment;
}
public Address(Address toCopyAddress){
this.country = toCopyAddress.getCountry();
this.street = toCopyAddress.getStreet();
this.number = toCopyAddress.getNumber();
this.extra = toCopyAddress.getExtra();
this.postnumber = toCopyAddress.getPostnumber();
this.county = toCopyAddress.getCounty();
this.place = toCopyAddress.getPlace();
this.contact = toCopyAddress.getContact();
this.comment = toCopyAddress.getComment();
}
@Override
public int hashCode() {
int hash = 7;
hash = 83 * hash + Objects.hashCode(this.country);
hash = 83 * hash + Objects.hashCode(this.street);
hash = 83 * hash + Objects.hashCode(this.number);
hash = 83 * hash + Objects.hashCode(this.extra);
hash = 83 * hash + Objects.hashCode(this.postnumber);
hash = 83 * hash + Objects.hashCode(this.county);
hash = 83 * hash + Objects.hashCode(this.place);
hash = 83 * hash + Objects.hashCode(this.contact);
hash = 83 * hash + Objects.hashCode(this.comment);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Address other = (Address) obj;
if (!Objects.equals(this.country, other.country)) {
return false;
}
if (!Objects.equals(this.street, other.street)) {
return false;
}
if (!Objects.equals(this.number, other.number)) {
return false;
}
if (!Objects.equals(this.extra, other.extra)) {
return false;
}
if (!Objects.equals(this.county, other.county)) {
return false;
}
if (!Objects.equals(this.place, other.place)) {
return false;
}
if (!Objects.equals(this.comment, other.comment)) {
return false;
}
return Objects.equals(this.postnumber, other.postnumber);
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getExtra() {
return extra;
}
public void setExtra(String extra) {
this.extra = extra;
}
public Integer getPostnumber() {
return postnumber;
}
public void setPostnumber(Integer postnumber) {
this.postnumber = postnumber;
}
public String getCounty() {
return county;
}
public void setCounty(String county) {
this.county = county;
}
public String getPlace() {
return place;
}
public void setPlace(String place) {
this.place = place;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getContact() {
return contact;
}
public void setContact(String contact) {
this.contact = contact;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}
@@ -1,73 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.company;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import model.AbstractEntity;
import model.adresses.LocationAddress;
import model.customer.Customer;
import model.machine.Machine;
/**
*
* @author patri
*/
@Entity
public class Location extends AbstractEntity{
@ManyToOne
private Company company;
@OneToOne
private LocationAddress address;
@OneToMany(mappedBy = "location")
private Set<Machine> machines;
@ManyToMany
private Set<Customer> contacts;
public Location() {
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public LocationAddress getAddress() {
return address;
}
public void setAddress(LocationAddress address) {
this.address = address;
}
public Set<Machine> getMachines() {
return machines;
}
public void setMachines(Set<Machine> machines) {
this.machines = machines;
}
public Set<Customer> getContacts() {
return contacts;
}
public void setContacts(Set<Customer> contacts) {
this.contacts = contacts;
}
}
@@ -1,63 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.customer;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import model.person.Person;
import model.company.Company;
import model.company.Location;
/**
*
* @author patri
*/
@Entity
public class Customer extends Person{
@ManyToOne
private Company company;
@ManyToMany
private Set<Location> locations;
@Column(nullable = true, length = 210)
private String note;
public Customer() {
}
public Customer(Company company) {
this.company = company;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public Set<Location> getLocations() {
return locations;
}
public void setLocations(Set<Location> locations) {
this.locations = locations;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
@@ -1,33 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.machine;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import model.AbstractEntity;
import model.company.Location;
/**
*
* @author patri
*/
@Entity
public class Machine extends AbstractEntity {
@ManyToOne
private Location location;
public Machine() {
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
}
@@ -1,131 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import java.time.LocalDateTime;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import model.AbstractEntity;
import model.person.Person;
/**
*
* @author Patrick
*/
@Entity
public class Comment extends AbstractEntity implements Comparable<Comment> {
@Column(columnDefinition = "longblob")
private String message;
@ManyToOne
private Person writer;
private boolean edited;
@ManyToOne
private Ticket ticket;
public Comment() {
}
public Comment(Person writer, String message) {
this.writer = writer;
this.edited = false;
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
edited = true;
}
public LocalDateTime getLastEditedDate() {
return getChangedDate();
}
public boolean isEdited() {
return edited;
}
public void setEdited(boolean edited) {
this.edited = edited;
setChangedDate(LocalDateTime.now());
}
public Ticket getTicket() {
return ticket;
}
public void setTicket(Ticket ticket) {
this.ticket = ticket;
setChangedDate(LocalDateTime.now());
}
public Person getWriter() {
return writer;
}
@Override
public int hashCode() {
int hash = 7;
hash = 79 * hash + Objects.hashCode(this.message);
hash = 79 * hash + Objects.hashCode(this.writer);
hash = 79 * hash + Objects.hashCode(getCreationDate());
hash = 79 * hash + Objects.hashCode(getChangedDate());
hash = 79 * hash + (this.edited ? 1 : 0);
hash = 79 * hash + Objects.hashCode(this.ticket);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Comment other = (Comment) obj;
if (this.edited != other.edited) {
return false;
}
if (!Objects.equals(this.message, other.message)) {
return false;
}
if (!Objects.equals(this.writer, other.writer)) {
return false;
}
if (!Objects.equals(getCreationDate(), other.getCreationDate())) {
return false;
}
if (!Objects.equals(this.ticket, other.ticket)) {
return false;
}
return true;
}
@Override
public String toString() {
return "Comment{" + "writer=" + writer.getEmail() + ", creationDate=" + getCreationDate() + ", id=" + getId() + ", message="+ message + '}';
}
@Override
public int compareTo(Comment c) {
return c.getCreationDate().compareTo(this.getCreationDate());
}
}
@@ -1,32 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
/**
*
* @author patri
*/
public enum FilenameGeneration {
INSPEKTIONNR,
MASCHINEDESCRIPTION,
LOCATION;
@Override
public String toString() {
switch(this){
case INSPEKTIONNR:
return "inspektionnr";
case MASCHINEDESCRIPTION:
return "maschinedescription";
case LOCATION:
return "location";
}
return "nothing";
}
}
@@ -1,46 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import model.AbstractEntity;
import javax.persistence.OneToOne;
import model.machine.Machine;
/**
*
* @author patri
*/
@Entity
public class LocationMachine extends AbstractEntity{
@OneToOne
private Machine machine;
@ManyToOne
private TicketLocation ticketLocation;
public LocationMachine() {
}
public Machine getMachine() {
return machine;
}
public void setMachine(Machine machine) {
this.machine = machine;
}
public TicketLocation getTicketLocation() {
return ticketLocation;
}
public void setTicketLocation(TicketLocation ticketLocation) {
this.ticketLocation = ticketLocation;
}
}
@@ -1,189 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import java.time.LocalDateTime;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import model.AbstractEntity;
import model.adresses.CompanyBillingAddress;
import model.company.Company;
import model.files.Invoice;
import model.files.Report;
import model.person.Person;
import model.person.Token;
/**
*
* @author patri
*/
@Entity
public class Ticket extends AbstractEntity{
@ManyToOne(optional = false)
private Company company;
@OneToOne
private CompanyBillingAddress billingAddress;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Status status;
@OneToOne(optional = false)
private Person creator;
@OneToOne(optional = true)
private Person owner;
private LocalDateTime startDate;
private LocalDateTime endDate;
@OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL)
private List<Comment> comments;
private boolean payed;
@OneToMany(mappedBy = "ticket", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Token> tokens;
@OneToMany(mappedBy = "ticket", cascade = {CascadeType.PERSIST, CascadeType.PERSIST})
private List<Report> reports;
@OneToMany(mappedBy = "ticket", cascade = {CascadeType.PERSIST, CascadeType.PERSIST})
private List<Invoice> invoices;
@OneToMany(mappedBy = "ticket", cascade = {CascadeType.PERSIST, CascadeType.PERSIST})
private List<TicketLocation> locations;
@Column(nullable = false, length = 200)
private String filenameGeneration;
public Ticket() {
}
public CompanyBillingAddress getBillingAddress() {
return billingAddress;
}
public void setBillingAddress(CompanyBillingAddress billingAddress) {
this.billingAddress = billingAddress;
}
public LocalDateTime getStartDate() {
return startDate;
}
public void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public List<TicketLocation> getLocations() {
return locations;
}
public void setLocations(List<TicketLocation> locations) {
this.locations = locations;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public Person getCreator() {
return creator;
}
public void setCreator(Person creator) {
this.creator = creator;
}
public Person getOwner() {
return owner;
}
public void setOwner(Person owner) {
this.owner = owner;
}
public List<Comment> getComments() {
return comments;
}
public void setComments(List<Comment> comments) {
this.comments = comments;
}
public boolean isPayed() {
return payed;
}
public void setPayed(boolean payed) {
this.payed = payed;
}
public List<Token> getTokens() {
return tokens;
}
public void setTokens(List<Token> tokens) {
this.tokens = tokens;
}
public List<Report> getReports() {
return reports;
}
public void setReports(List<Report> reports) {
this.reports = reports;
}
public List<Invoice> getInvoices() {
return invoices;
}
public void setInvoices(List<Invoice> invoices) {
this.invoices = invoices;
}
public String getFilenameGeneration() {
return filenameGeneration;
}
public void setFilenameGeneration(String filenameGeneration) {
this.filenameGeneration = filenameGeneration;
}
}
@@ -1,58 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import model.AbstractEntity;
import model.company.Location;
import java.util.List;
import javax.persistence.OneToMany;
/**
*
* @author patri
*/
@Entity
public class TicketLocation extends AbstractEntity{
@ManyToOne
private Ticket ticket;
@OneToOne
private Location location;
@OneToMany(mappedBy = "ticketLocation")
private List<LocationMachine> machines;
public TicketLocation() {
}
public Ticket getTicket() {
return ticket;
}
public void setTicket(Ticket ticket) {
this.ticket = ticket;
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
public List<LocationMachine> getMachines() {
return machines;
}
public void setMachines(List<LocationMachine> machines) {
this.machines = machines;
}
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
bean-discovery-mode="all">
</beans>
@@ -1,6 +0,0 @@
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_10_0.xsd"
version="10.0">
<security-domain>mss-failsafe</security-domain>
</jboss-web>
@@ -1,14 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}</title>
</h:head>
<h:body>
<p>Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}</p>
<p:spinner />
</h:body>
</html>
@@ -1,35 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<h:head>
<title>Login Testpage</title>
</h:head>
<h:body>
<h:form id="login">
<p:panel header="Login" style="width: 450px; margin: auto; margin-top: 100px;">
<p:messages id="messages" showDetail="true" closable="true">
<p:autoUpdate />
</p:messages>
<p:graphicImage url="/resources/images/logo.jpg" alt="MSS Machine Safety Services" style="width: 100%;"/>
<h:panelGrid columns="2" cellpadding="5">
<p:outputLabel for="username" value="Email" />
<p:inputText id="username" value="#{personController.username}" required="true" label="username" />
<p:outputLabel for="password" value="Password:" />
<p:password id="password" value="#{personController.password}" required="true" label="password" />
<p:outputLabel for="rememberMe" value="Remember Me:" />
<p:selectBooleanCheckbox id="rememberMe" value="#{personController.rememberMe}" />
<f:facet name="footer">
<p:commandButton value="Login" action="#{personController.submit()}" ajax="false"/>
</f:facet>
</h:panelGrid>
</p:panel>
</h:form>
</h:body>
</html>
@@ -1,83 +0,0 @@
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f = "http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<f:facet name="last">
<h:outputStylesheet library="css" name="default.css"/>
<h:outputStylesheet library="css" name="icons.css"/>
<h:outputStylesheet library="webjars" name="primeflex/2.0.0/primeflex.min.css" />
</f:facet>
<title>
<ui:insert name="title">
Please add a Title!
</ui:insert>
</title>
<ui:insert name="head"/>
</h:head>
<h:body>
<h:form id="main">
<div class="p-grid">
<div class="p-col-12 p-md-3 p-lg-2 p-xl-2">
<div class="p-col-12" style="min-height: 90px;">
<p:commandLink action="welcome">
<p:graphicImage url="../resources/images/logo.jpg" alt="MSS Machine Safety Services" style="width: 100%;"/>
</p:commandLink>
</div>
<div class="p-col-12">
<p:menu style="width: 100%;">
<p:submenu label="Home">
<p:menuitem value="Home" outcome="welcome" icon="pi pi-home"/>
<p:menuitem value="Suche" outcome="welcome" icon="pi pi-search"/>
</p:submenu>
<p:submenu label="Ticket">
<p:menuitem value="Erstellen" outcome="welcome" icon="pi pi-plus"/>
<p:menuitem value="Suchen" outcome="welcome" icon="pi pi-search"/>
</p:submenu>
<p:submenu label="Stammdaten">
<p:menuitem value="Firmen" outcome="companies" icon="icon company"/>
<p:menuitem value="Standorte" outcome="locations" icon="icon location"/>
<p:menuitem value="Maschienen" outcome="machines" icon="icon machine"/>
<p:menuitem value="Schutzeinr." outcome="protection" icon="icon security"/>
</p:submenu>
</p:menu>
</div>
</div>
<div class="p-col-12 p-md-9 p-lg-10 p-xl-10">
<div class="card p-col-12" style="min-height: 90px; text-align: right;">
<p:menu overlay="true" trigger="avatar" my="left top" at="bottom left">
<p:menuitem value="Profil" outcome="profile"/>
<p:menuitem value="Einstellungen" />
<p:menuitem value="Ausloggen" action="#{personController.logout()}" />
</p:menu>
<p:avatar dynamicColor="true" id="avatar" icon="pi pi-user" styleClass="p-mr-2 avatar" size="large" shape="circle" gravatar="#{personController.activeUser.email}">
<p:graphicImage value="#{personController.activeUser.userPicture.fileData}" rendered="#{personController.hasPicture()}"/>
</p:avatar>
</div>
<div class="p-col-12" style="width: 100%">
<div class="card p-col-12">
<p:growl showDetail="true">
<p:autoUpdate/>
</p:growl>
</div>
<div class="card p-col-12">
<ui:insert name="content">Content</ui:insert>
</div>
</div>
</div>
<div class="p-col-12 p-lg-12 p-xl-12">
<ui:insert name="bottom">Bottom</ui:insert>
</div>
</div>
</h:form>
</h:body>
</html>
@@ -1,19 +0,0 @@
<ui:composition template="/resources/layout/user/template.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:define name="title">
Firmen
</ui:define>
<ui:define name="content">
Willkommen zuhause
</ui:define>
<ui:define name="bottom">
</ui:define>
</ui:composition>
@@ -1,110 +0,0 @@
<ui:composition template="/resources/layout/user/template.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:define name="title">
Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}
</ui:define>
<ui:define name="content">
<div class="p-grid">
<div class="card p-col-5 p-md-6 p-xl-6">
<p:fileUpload mode="advanced"
multiple="false"
sizeLimit="#{userPictureController.maxFileSize}" allowTypes="#{userPictureController.fileTypesRE}"
invalidSizeMessage="Maximum file size allowed is 2 MB"
invalidFileMessage="only gif | jpg | jpeg | png is allowed"
update="main"
listener="#{userPictureController.handleUserPictureUpload}"/>
</div>
<div class="card p-col-7 p-md-6 p-xl-6" style="text-align: right;">
<p:graphicImage style="max-width: 500px; max-height: 300px; margin-right: 20%;" id="image" value="#{personController.activeUser.userPicture.fileData}" rendered="#{personController.hasPicture()}"/>
</div>
<div class="card p-col-12 p-md-6 p-xl-6">
<div class="ui-fluid p-formgrid p-grid">
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="email" value="Email" />
<p:inputText readonly="true" id="email" value="#{personEditController.email}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="password" value="Password" />
<p:inputText readonly="true" id="password" value="#{personEditController.password}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="telefon" value="Telefon" />
<p:inputText readonly="true" id="telefon" value="#{personEditController.telefon}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="mobile" value="Handy" />
<p:inputText readonly="true" id="mobile" value="#{personEditController.mobile}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
</div>
</div>
<div class="card p-col-12 p-md-6 p-xl-6">
<div class="ui-fluid p-formgrid p-grid">
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="title" value="Titel" />
<p:inputText readonly="true" id="title" value="#{personEditController.title}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="call" value="Anrede" />
<p:inputText readonly="true" id="call" value="#{personEditController.call.toString()}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="firstname" value="Vorname" />
<p:inputText readonly="true" id="firstname" value="#{personEditController.firstname}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
<div class="p-field p-col-8 p-md-8">
<p:outputLabel for="lastname" value="Nachname" />
<p:inputText readonly="true" id="lastname" value="#{personEditController.lastname}" />
</div>
<div class="p-field p-col-4 p-md-4" style="float: right;">
<br />
<p:commandButton value="Ändern"/>
</div>
</div>
</div>
</div>
</ui:define>
<ui:define name="bottom">
</ui:define>
</ui:composition>
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>plate.software</groupId>
<artifactId>mss-failsafe</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>mssfailsafe.datalayer</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<failOnMissingWebXml>false</failOnMissingWebXml>
<jakartaee>8.0</jakartaee>
</properties>
<dependencies>
<dependency>
<groupId>plate.software</groupId>
<artifactId>userdata</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>plate.software</groupId>
<artifactId>userManagement</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>classes</classifier>
</dependency>
</dependencies>
</project>
@@ -1,32 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.adresses;
import javax.persistence.Entity;
import javax.persistence.OneToOne;
import model.company.Location;
/**
*
* @author patri
*/
@Entity
public class LocationAddress extends Address {
@OneToOne
private Location location;
public LocationAddress() {
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
}
@@ -1,151 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.company;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.OneToMany;
import model.AbstractEntity;
import model.adresses.CompanyBillingAddress;
import model.customer.Customer;
/**
*
* @author patri
*/
@Entity
public class Company extends AbstractEntity {
public static final String FIND_BY_NAME = "Company.findByName";
public static final String FIND_BY_STEUERID = "Company.findBySteuerID";
public static final String FIND_BY_UMSATZSTEUERID = "Company.findByUmsatzsteuerID";
public static final String FIND_BY_CUSTOMER = "Company.findByCustomer";
public static final String FIND_BY_ADDRESS = "Company.findByAddress";
public static final String FIND_BY_DELIVERYADDRESS = "Company.findByLocation";
@Column(unique = true, nullable = false)
private String name;
@Column(unique = true, nullable = true)
private String steuerNr;
@Column(unique = true, nullable = true)
private String umsatzSteuerID;
@Column(unique = true, nullable = true)
private String kundenNr;
@Column(unique = false, nullable = true)
private String headerInspection;
@Column(unique = false, nullable = true)
private String headerService;
@Column(unique = false, nullable = false)
@Enumerated(EnumType.ORDINAL)
private Status status;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
private Set<CompanyBillingAddress> addresses;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
private Set<Location> locations;
@OneToMany(mappedBy = "company", cascade = {
CascadeType.MERGE,
CascadeType.REFRESH
})
private Set<Customer> customers;
public Company() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSteuerNr() {
return steuerNr;
}
public void setSteuerNr(String steuerNr) {
this.steuerNr = steuerNr;
}
public String getUmsatzSteuerID() {
return umsatzSteuerID;
}
public void setUmsatzSteuerID(String umsatzSteuerID) {
this.umsatzSteuerID = umsatzSteuerID;
}
public String getKundenNr() {
return kundenNr;
}
public void setKundenNr(String kundenNr) {
this.kundenNr = kundenNr;
}
public String getHeaderInspection() {
return headerInspection;
}
public void setHeaderInspection(String headerInspection) {
this.headerInspection = headerInspection;
}
public String getHeaderService() {
return headerService;
}
public void setHeaderService(String headerService) {
this.headerService = headerService;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public Set<CompanyBillingAddress> getAddresses() {
return addresses;
}
public void setAddresses(Set<CompanyBillingAddress> addresses) {
this.addresses = addresses;
}
public Set<Location> getLocations() {
return locations;
}
public void setLocations(Set<Location> locations) {
this.locations = locations;
}
public Set<Customer> getCustomers() {
return customers;
}
public void setCustomers(Set<Customer> customers) {
this.customers = customers;
}
}
@@ -1,73 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.company;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import model.AbstractEntity;
import model.adresses.LocationAddress;
import model.customer.Customer;
import model.machine.Machine;
/**
*
* @author patri
*/
@Entity
public class Location extends AbstractEntity{
@ManyToOne
private Company company;
@OneToOne
private LocationAddress address;
@OneToMany(mappedBy = "location")
private Set<Machine> machines;
@ManyToMany
private Set<Customer> contacts;
public Location() {
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public LocationAddress getAddress() {
return address;
}
public void setAddress(LocationAddress address) {
this.address = address;
}
public Set<Machine> getMachines() {
return machines;
}
public void setMachines(Set<Machine> machines) {
this.machines = machines;
}
public Set<Customer> getContacts() {
return contacts;
}
public void setContacts(Set<Customer> contacts) {
this.contacts = contacts;
}
}
@@ -1,59 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.company;
import java.util.Locale;
/**
*
* @author patri
*/
public enum Status {
ACTIVE,
INACTIVE;
private Status() {
}
@Override
public String toString() {
switch(this){
case ACTIVE:
return "aktiv";
case INACTIVE:
return "inaktiv";
default:
return "";
}
}
public String toLanguageString(Locale locale){
if (locale == null ||locale.equals(Locale.GERMAN) || locale.equals(Locale.GERMANY)) {
return getGerman();
}
if (locale.equals(Locale.ENGLISH)) {
getEnglish();
}
return "";
}
private String getGerman() {
return toString();
}
private String getEnglish(){
switch(this){
case ACTIVE:
return "active";
case INACTIVE:
return "inactive";
default:
return "";
}
}
}
@@ -1,63 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.customer;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import model.person.Person;
import model.company.Company;
import model.company.Location;
/**
*
* @author patri
*/
@Entity
public class Customer extends Person{
@ManyToOne
private Company company;
@ManyToMany
private Set<Location> locations;
@Column(nullable = true, length = 210)
private String note;
public Customer() {
}
public Customer(Company company) {
this.company = company;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public Set<Location> getLocations() {
return locations;
}
public void setLocations(Set<Location> locations) {
this.locations = locations;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
@@ -1,59 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.files;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Lob;
import model.AbstractEntity;
/**
*
* @author patri
*/
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class FileDB extends AbstractEntity{
@Column(nullable = false, length = 100)
private String name;
@Enumerated(EnumType.STRING)
private Mime mime;
@Lob
private byte[] fileData;
public FileDB() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getFileData() {
return fileData;
}
public void setFileData(byte[] fileData) {
this.fileData = fileData;
}
public Mime getMime() {
return mime;
}
public void setMime(Mime mime) {
this.mime = mime;
}
}
@@ -1,35 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.files;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import model.ticket.Ticket;
/**
*
* @author patri
*/
@Entity
public class Invoice extends FileDB {
@OneToMany
private Ticket ticket;
public Invoice() {
}
public Invoice(Ticket ticket) {
this.ticket = ticket;
}
public Ticket getTicket() {
return ticket;
}
public void setTicket(Ticket ticket) {
this.ticket = ticket;
}
}
@@ -1,113 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.files;
/**
*
* @author patri
*/
public enum Mime {
AAC(".aac", "AAC audio", "audio/aac"),
ABW(".abw", "AbiWord document", "application/x-abiword"),
ARC(".arc", "Archive document (multiple files embedded)", "application/x-freearc"),
AVI(".avi", "AVI: Audio Video Interleave", "video/x-msvideo"),
AZW(".azw", "Amazon Kindle eBook format", "application/vnd.amazon.ebook"),
BIN(".bin", "Any kind of binary data", "application/octet-stream"),
BMP(".bmp", "Windows OS/2 Bitmap Graphics", "image/bmp"),
BZ(".bz", "BZip archive", "application/x-bzip"),
BZ2(".bz2", "BZip2 archive", "application/x-bzip2"),
CDA(".cda", "CD audio", "application/x-cdf"),
CSH(".csh", "C-Shell script", "application/x-csh"),
CSS(".css", "Cascading Style Sheets (CSS)", "text/css"),
CSV(".csv", "Comma-separated values (CSV", "text/csv"),
DOC(".doc", "Microsoft Word", "application/msword"),
DOCX(".docx", "Microsoft Word (OpenXML)", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
EOT(".eot", "MS Embedded OpenType fonts", "application/vnd.ms-fontobject"),
EPUB(".epub", "Electronic publication (EPUB)", "application/epub+zip"),
GZ(".gz", "GZip Compressed Archive", "application/gzip"),
GIF(".gif", "Graphics Interchange Format (GIF)", "image/gif"),
HTM(".htm", "HyperText Markup Language (HTML)", "text/html"),
HTML(".html", "HyperText Markup Language (HTML)", "text/html"),
ICO(".ico", "Icon format", "image/vnd.microsoft.icon"),
ICS(".ics", "iCalendar format", "text/calendar"),
JAR(".jar", "Java Archive (JAR)", "application/java-archive"),
JPG(".jpg", "JPEG images", "image/jpeg"),
JPEG(".jpeg", "JPEG images", "image/jpeg"),
JS(".js", "JavaScript", "text/javascript"),
JSON(".json", "JSON format", "application/json"),
JSONLD(".jsonld", "JSON-LD format", "application/ld+json"),
MID(".mid", "Musical Instrument Digital Interface (MIDI)", "audio/midi"),
MIDI(".midi", "Musical Instrument Digital Interface (MIDI)", "audio/midi"),
MJS(".mjs", "JavaScript module", "text/javascript"),
MP3(".mp3", "MP3 audio", "audio/mpeg"),
MP4(".mp4", "MP4 video", "video/mp4"),
MPEG(".mpeg", "MPEG Video", "video/mpeg"),
MPKG(".mpkg", "Apple Installer Package", "application/vnd.apple.installer+xml"),
ODP(".odp", "OpenDocument presentation document", "application/vnd.oasis.opendocument.presentation"),
ODS(".ods", "OpenDocument spreadsheet document", "application/vnd.oasis.opendocument.spreadsheet"),
ODT(".odt", "OpenDocument text document", "application/vnd.oasis.opendocument.text"),
OGA(".oga", "OGG audio", "audio/ogg"),
OGV(".ogv", "OGG video", "video/ogg"),
OGX(".ogx", "OGG", "application/ogg"),
OPUUS(".opus", "Opus audio", "audio/opus"),
OTF(".otf", "OpenType font", "font/otf"),
PNG(".png", "Portable Network Graphics", "image/png"),
PDF(".pdf", "Adobe Portable Document Format (PDF)", "application/pdf"),
PHP(".php", "Hypertext Preprocessor (Personal Home Page)", "application/x-httpd-php"),
PPT(".ppt", "Microsoft PowerPoint", "application/vnd.ms-powerpoint"),
PPTX(".pptx", "Microsoft PowerPoint (OpenXML)", "application/vnd.openxmlformats-officedocument.presentationml.presentation"),
RAR(".rar", "RAR archive", "application/vnd.rar"),
RTF(".rtf", "Rich Text Format (RTF)", "application/rtf"),
SH(".sh", "Bourne shell script", "application/x-sh"),
SVG(".svg", "Scalable Vector Graphics (SVG)", "image/svg+xml"),
SWF(".swf", "Small web format (SWF) or Adobe Flash document", "application/x-shockwave-flash"),
TAR(".tar", "Tape Archive (TAR)", "application/x-tar"),
TIF(".tif", "Tagged Image File Format (TIFF)", "image/tiff"),
TIFF(".tiff", "Tagged Image File Format (TIFF)", "image/tiff"),
TS(".ts", "MPEG transport stream", "video/mp2t"),
TTF(".ttf", "TrueType Font", "font/ttf"),
TXT(".txt", "Text, (generally ASCII or ISO 8859-n)", "text/plain"),
VSD(".vsd", "Microsoft Visio", "application/vnd.visio"),
WAV(".wav", "Waveform Audio Format", "audio/wav"),
WEBA(".weba", "WEBM audio", "audio/webm"),
WEBM(".webm", "WEBM video", "video/webm"),
WEBP(".webp", "WEBP image", "image/webp"),
WOFF(".woff", "Web Open Font Format (WOFF)", "font/woff"),
WOFF2(".woff2", "Web Open Font Format (WOFF)", "font/woff2"),
XHTML(".xhtml", "XHTML", "application/xhtml+xml"),
XLS(".xls", "Microsoft Excel", "application/vnd.ms-excel"),
XLSX(".xlsx", "Microsoft Excel (OpenXML)", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
XML(".xml", "XML", "application/xml"),
XUL(".xul", "XUL", "application/vnd.mozilla.xul+xml"),
ZIP(".zip", "ZIP archive", "application/zip"),
GP3V(".3gp", "3GPP audio/video container", "video/3gpp"),
GP3A(".3gp", "3GPP audio/video container", "audio/3gpp"),
G23V(".3g2", "3GPP2 audio/video container", "video/3gpp2"),
G23A(".3g2", "3GPP2 audio/video container", "audio/3gpp2"),
Z7(".7z", "7-zip archive", "application/x-7z-compressed");
private final String extension;
private final String kindOfDocument;
private final String mimeType;
private Mime(String extension, String kindOfDocument, String mimeType) {
this.extension = extension;
this.kindOfDocument = kindOfDocument;
this.mimeType = mimeType;
}
public String getExtension() {
return extension;
}
public String getKindOfDocument() {
return kindOfDocument;
}
public String getMimeType() {
return mimeType;
}
}
@@ -1,32 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.files;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import model.ticket.Ticket;
/**
*
* @author patri
*/
@Entity
public class Report extends FileDB{
@OneToMany
private Ticket ticket;
public Report() {
}
public Ticket getTicket() {
return ticket;
}
public void setTicket(Ticket ticket) {
this.ticket = ticket;
}
}
@@ -1,33 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.machine;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import model.AbstractEntity;
import model.company.Location;
/**
*
* @author patri
*/
@Entity
public class Machine extends AbstractEntity {
@ManyToOne
private Location location;
public Machine() {
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
}
@@ -1,32 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
/**
*
* @author patri
*/
public enum FilenameGeneration {
INSPEKTIONNR,
MASCHINEDESCRIPTION,
LOCATION;
@Override
public String toString() {
switch(this){
case INSPEKTIONNR:
return "inspektionnr";
case MASCHINEDESCRIPTION:
return "maschinedescription";
case LOCATION:
return "location";
}
return "nothing";
}
}
@@ -1,192 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import java.time.LocalDateTime;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import model.AbstractEntity;
import model.adresses.CompanyBillingAddress;
import model.company.Company;
import model.company.Location;
import model.files.Invoice;
import model.files.Report;
import model.person.Person;
import model.person.Token;
/**
*
* @author patri
*/
@Entity
public class Ticket extends AbstractEntity{
@Column(nullable = false)
@OneToOne
private Company company;
@OneToOne
private CompanyBillingAddress billingAddress;
@OneToMany
private List<Location> locations;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Status status;
@Column(nullable = false)
@OneToOne
private Person creator;
@Column(nullable = true)
@OneToOne
private Person owner;
private LocalDateTime startDate;
private LocalDateTime endDate;
@OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL)
private List<Comment> comments;
private boolean payed;
@OneToMany(mappedBy = "ticket", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Token> tokens;
@OneToMany(mappedBy = "ticket", cascade = {CascadeType.PERSIST, CascadeType.PERSIST})
private List<Report> reports;
@OneToMany(mappedBy = "ticket", cascade = {CascadeType.PERSIST, CascadeType.PERSIST})
private List<Invoice> invoices;
@Column(nullable = false, length = 200)
private String filenameGeneration;
public Ticket() {
}
public CompanyBillingAddress getBillingAddress() {
return billingAddress;
}
public void setBillingAddress(CompanyBillingAddress billingAddress) {
this.billingAddress = billingAddress;
}
public LocalDateTime getStartDate() {
return startDate;
}
public void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public List<Location> getLocations() {
return locations;
}
public void setLocations(List<Location> locations) {
this.locations = locations;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public Person getCreator() {
return creator;
}
public void setCreator(Person creator) {
this.creator = creator;
}
public Person getOwner() {
return owner;
}
public void setOwner(Person owner) {
this.owner = owner;
}
public List<Comment> getComments() {
return comments;
}
public void setComments(List<Comment> comments) {
this.comments = comments;
}
public boolean isPayed() {
return payed;
}
public void setPayed(boolean payed) {
this.payed = payed;
}
public List<Token> getTokens() {
return tokens;
}
public void setTokens(List<Token> tokens) {
this.tokens = tokens;
}
public List<Report> getReports() {
return reports;
}
public void setReports(List<Report> reports) {
this.reports = reports;
}
public List<Invoice> getInvoices() {
return invoices;
}
public void setInvoices(List<Invoice> invoices) {
this.invoices = invoices;
}
public String getFilenameGeneration() {
return filenameGeneration;
}
public void setFilenameGeneration(String filenameGeneration) {
this.filenameGeneration = filenameGeneration;
}
}
@@ -1,35 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import model.AbstractEntity;
import model.company.Location;
import java.util.List;
/**
*
* @author patri
*/
@Entity
public class TicketLocation extends AbstractEntity{
@ManyToOne
private Ticket ticket;
@OneToOne
private Location location;
private List<TicketMachine> machines;
public TicketLocation() {
}
}
@@ -1,30 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package model.ticket;
import javax.persistence.Entity;
import model.AbstractEntity;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import model.machine.Machine;
/**
*
* @author patri
*/
@Entity
public class TicketMachine extends AbstractEntity{
@ManyToOne
private Ticket ticket;
@OneToOne
private Machine machine;
public TicketMachine() {
}
}
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="pu_datalayer" transaction-type="JTA">
<jta-data-source>java:/mss-failsave</jta-data-source>
<class>model.company.Location</class>
<class>model.adresses.LocationAdress</class>
<class>model.machine.Machine</class>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="pu_datalayer" transaction-type="JTA">
<jta-data-source>java:/mss-failsave</jta-data-source>
<class>model.company.Location</class>
<class>model.adresses.LocationAdress</class>
<class>model.machine.Machine</class>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>1.8-web</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>WildFly</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>
<org-netbeans-modules-maven-jaxws.rest_2e_config_2e_type>ide</org-netbeans-modules-maven-jaxws.rest_2e_config_2e_type>
</properties>
</project-shared-configuration>
@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>mss-failsafe</artifactId>
<groupId>plate.software</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>plate.software</groupId>
<artifactId>mssfailsafeWeblayer</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mssfailsafeWeblayer-1.0-SNAPSHOT</name>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<failOnMissingWebXml>false</failOnMissingWebXml>
<jakartaee>8.0</jakartaee>
</properties>
<dependencies>
<dependency>
<groupId>plate.software</groupId>
<artifactId>userdata</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.glassfish.soteria</groupId>
<artifactId>javax.security.enterprise</artifactId>
<version>1.0</version> <!-- Stable version -->
</dependency>
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>3.11.1</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>10.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${endorsed.dir}</outputDirectory>
<silent>true</silent>
<artifactItems>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<type>jar</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,13 +0,0 @@
package plate.software.mssfailsafeweblayer;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
/**
* Configures JAX-RS for the application.
* @author Juneau
*/
@ApplicationPath("resources")
public class JAXRSConfiguration extends Application {
}
@@ -1,20 +0,0 @@
package plate.software.mssfailsafeweblayer.resources;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
/**
*
* @author
*/
@Path("javaee8")
public class JavaEE8Resource {
@GET
public Response ping(){
return Response
.ok("ping")
.build();
}
}
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<!-- Define Persistence Unit -->
<persistence-unit name="my_persistence_unit">
</persistence-unit>
</persistence>
@@ -1,6 +0,0 @@
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_10_0.xsd"
version="10.0">
<security-domain>mss-failsafe</security-domain>
</jboss-web>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web version="10.0" xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_10_0.xsd">
<context-root>/mssfailsafeWeblayer-1.0-SNAPSHOT</context-root>
<security-domain>jaspitest</security-domain>
</jboss-web>
@@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
<welcome-file-list>
<welcome-file>/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<!-- Missing login -->
<error-code>401</error-code>
<location>/error.xhtml</location>
</error-page>
<error-page>
<!-- Forbidden directory listing -->
<error-code>403</error-code>
<location>/error.xhtml</location>
</error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>authorise</web-resource-name>
<url-pattern>/user/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
<http-method>TRACE</http-method>
<http-method>HEAD</http-method>
<http-method>DELETE</http-method>
<http-method>CONNECT</http-method>
<http-method>OPTIONS</http-method>
<http-method>PUT</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>ADMIN</role-name>
<role-name>USER</role-name>
</auth-constraint>
<!--
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>-->
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>authorise</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
<http-method>TRACE</http-method>
<http-method>HEAD</http-method>
<http-method>DELETE</http-method>
<http-method>CONNECT</http-method>
<http-method>OPTIONS</http-method>
<http-method>PUT</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>ADMIN</role-name>
</auth-constraint>
<!--<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>-->
</security-constraint>
<security-role>
<description>Normal User which got invited</description>
<role-name>USER</role-name>
</security-role>
<security-role>
<description>Admin user who can change entries, invite new domains and more..</description>
<role-name>ADMIN</role-name>
</security-role>
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
<cookie-config>
<http-only>true</http-only>
<!-- Prevent client side scripting from accessing/manipulating session cookie. -->
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
<!-- This disables URL rewriting. -->
</session-config>
</web-app>
@@ -1,14 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}</title>
</h:head>
<h:body>
<p>Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}</p>
<p:spinner />
</h:body>
</html>
@@ -1,13 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>Error Testpage</title>
</h:head>
<h:body>
<p>Error!</p>
</h:body>
</html>
@@ -1,33 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>Login Testpage</title>
</h:head>
<h:body>
<h:form id="login">
<p:panel header="Login MSS-Failsafe " style="width: 450px; margin: auto; margin-top: 100px;">
<p:messages id="messages" showDetail="true" closable="true">
<p:autoUpdate />
</p:messages>
<h:panelGrid columns="2" cellpadding="5">
<p:outputLabel for="username" value="Email" />
<p:inputText id="username" value="#{personController.username}" required="true" label="username" />
<p:outputLabel for="password" value="Password:" />
<p:password id="password" value="#{personController.password}" required="true" label="password" />
<p:outputLabel for="rememberMe" value="Remember Me:" />
<p:selectBooleanCheckbox id="rememberMe" value="#{personController.rememberMe}" />
<f:facet name="footer">
<p:commandButton value="Login" action="#{personController.submit()}" ajax="false"/>
</f:facet>
</h:panelGrid>
</p:panel>
</h:form>
</h:body>
</html>
@@ -1,14 +0,0 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}</title>
</h:head>
<h:body>
<p>Willkommen #{personController.activeUser.call.toString()} #{personController.activeUser.lastname}</p>
<p:spinner />
</h:body>
</html>
@@ -1,21 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
-->
<libraries xmlns="http://www.netbeans.org/ns/cdnjs-libraries/1"/>
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>1.8-web</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>WildFly</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>
<org-netbeans-modules-maven-jaxws.rest_2e_config_2e_type>ide</org-netbeans-modules-maven-jaxws.rest_2e_config_2e_type>
<org-netbeans-modules-projectapi.jsf_2e_language>Facelets</org-netbeans-modules-projectapi.jsf_2e_language>
</properties>
</project-shared-configuration>
-->
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>8.0-web</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_j2eeVersion>
<org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>WildFly</org-netbeans-modules-maven-j2ee.netbeans_2e_hint_2e_deploy_2e_server>
<org-netbeans-modules-maven-jaxws.rest_2e_config_2e_type>ide</org-netbeans-modules-maven-jaxws.rest_2e_config_2e_type>
<org-netbeans-modules-projectapi.jsf_2e_language>Facelets</org-netbeans-modules-projectapi.jsf_2e_language>
<org-netbeans-modules-css-prep.less_2e_mappings>/less:/css</org-netbeans-modules-css-prep.less_2e_mappings>
<org-netbeans-modules-css-prep.less_2e_enabled>false</org-netbeans-modules-css-prep.less_2e_enabled>
<org-netbeans-modules-css-prep.sass_2e_enabled>false</org-netbeans-modules-css-prep.sass_2e_enabled>
<org-netbeans-modules-css-prep.sass_2e_compiler_2e_options/>
<org-netbeans-modules-css-prep.less_2e_compiler_2e_options/>
<org-netbeans-modules-css-prep.sass_2e_mappings>/scss:/css</org-netbeans-modules-css-prep.sass_2e_mappings>
<org-netbeans-modules-web-clientproject-api.js_2e_libs_2e_folder>js/libs</org-netbeans-modules-web-clientproject-api.js_2e_libs_2e_folder>
</properties>
</project-shared-configuration>
Regular → Executable
+185 -70
View File
@@ -1,78 +1,193 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>plate.software</groupId>
<artifactId>mss-failsafe</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<modelVersion>4.0.0</modelVersion>
<groupId>plate.software</groupId>
<artifactId>mss</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mss-1.0-SNAPSHOT</name>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<failOnMissingWebXml>false</failOnMissingWebXml>
<jakartaee>8.0</jakartaee>
</properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<modules>
<module>mssfailsafe.datalayer</module>
<module>userdata</module>
<module>mssfailsafeWeblayer</module>
<module>mss</module>
</modules>
<name>mss-failsafe</name>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.5.Final</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>forms</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdfa</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>sign</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>barcodes</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>7.2.2</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>hyph</artifactId>
<version>7.2.2</version>
</dependency>
<!--
<dependency>
<groupId>org.glassfish.soteria</groupId>
<artifactId>javax.security.enterprise</artifactId>
<version>1.0</version>
</dependency>-->
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>3.11.1</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>11.0.0</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>primeflex</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>11</source>
<target>11</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${endorsed.dir}</outputDirectory>
<silent>true</silent>
<artifactItems>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jakartaee}</version>
<type>jar</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,197 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package business;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import model.AbstractEntity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.Hibernate;
/**
*
* @author Patrick
* @param <T>
*/
public abstract class AbstractManager<T extends AbstractEntity> {
protected final Logger LOGGER = LogManager.getLogger(this.getClass());
private final Class<T> entityClass;
public AbstractManager(Class<T> entityClass) {
this.entityClass = entityClass;
}
protected abstract EntityManager getEntityManager();
@Transactional
public boolean save(T entity) {
if (entity == null) {
return false;
}
if (entity.getId() != null) {
try {
edit(entity);
getEntityManager().flush();
} catch (Exception e) {
LOGGER.error(e);
return false;
}
} else {
try {
create(entity);
getEntityManager().flush();
} catch (Exception e) {
LOGGER.error(e);
return false;
}
}
return true;
}
@Transactional
public boolean saveAll(Collection<T> entities) {
if (entities == null) {
return false;
}
if (entities.isEmpty()) {
return true;
}
for (T entity : entities) {
if (entity.getId() != null) {
try {
edit(entity);
} catch (Exception e) {
LOGGER.error(e);
return false;
}
} else {
try {
create(entity);
} catch (Exception e) {
LOGGER.error(e);
return false;
}
}
}
getEntityManager().flush();
return true;
}
public void create(T entity) {
try {
getEntityManager().persist(entity);
} catch (Exception e) {
LOGGER.error(e);
}
}
public void edit(T entity) {
getEntityManager().merge(entity);
}
public T refresh(T entity) {
if (entity == null) {
return null;
}
if (entity.getId() == null) {
save(entity);
}
entity = getEntityManager().merge(entity);
Hibernate.initialize(entity);
return entity;
}
public boolean removeAllIn(Collection<T> col) {
if (col == null || col.isEmpty()) {
return true;
}
try {
boolean success = true;
for (T entity : col) {
if (!remove(entity)) {
success = false;
}
}
return success;
} catch (Exception e) {
LOGGER.error(e);
}
return false;
}
public boolean remove(T entity) {
if (entity == null || entity.getId() == null) {
return false;
}
try {
Hibernate.initialize(entity);
entity = find(entity.getId());
getEntityManager().remove(entity);
return true;
} catch (Exception e) {
LOGGER.error(e);
return false;
}
/*
String queryString = "DELETE FROM " + entityClass.getSimpleName() + " e WHERE e.id = :id";
Query query = getEntityManager().createQuery(queryString);
query.setParameter("id", entity.getId());
try {
query.executeUpdate();
return true;
} catch (Exception e) {
LOGGER.error(e);
return false;
}*/
}
public T find(Object id) {
return getEntityManager().find(entityClass, id);
}
public List<T> findAll() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
return getEntityManager().createQuery(cq).getResultList();
}
public List<T> findRange(int[] range) {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
javax.persistence.Query q = getEntityManager().createQuery(cq);
q.setMaxResults(range[1] - range[0] + 1);
q.setFirstResult(range[0]);
return q.getResultList();
}
public int count() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
javax.persistence.criteria.Root<T> rt = cq.from(entityClass);
cq.select(getEntityManager().getCriteriaBuilder().count(rt));
javax.persistence.Query q = getEntityManager().createQuery(cq);
return ((Long) q.getSingleResult()).intValue();
}
}
@@ -0,0 +1,74 @@
package business;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.faces.context.FacesContext;
import javax.inject.Named;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.primefaces.model.DefaultStreamedContent;
import org.primefaces.model.StreamedContent;
@Named
@SessionScoped
public class BackupFileManager implements Serializable {
private static final Logger logger = LogManager.getLogger(BackupFileManager.class);
private static final String BACKUP_DIRECTORY = "/h2DB/";
private List<File> backupFiles;
@PostConstruct
public void init() {
loadBackupFiles();
}
public void loadBackupFiles() {
File directory = new File(BACKUP_DIRECTORY);
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles((dir, name) -> name.startsWith("h2-mss-database-backup_") && name.endsWith(".zip"));
if (files != null) {
backupFiles = Arrays.asList(files);
// Sortiere Dateien nach Änderungsdatum (neueste zuerst)
Collections.sort(backupFiles, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
} else {
backupFiles = Collections.emptyList();
}
} else {
backupFiles = Collections.emptyList();
logger.warn("Backup-Verzeichnis existiert nicht: " + BACKUP_DIRECTORY);
}
}
public List<File> getBackupFiles() {
return backupFiles;
}
public StreamedContent downloadFile(String fileName) {
try {
File file = new File(BACKUP_DIRECTORY + fileName);
return DefaultStreamedContent.builder()
.name(fileName)
.contentType("application/zip")
.stream(() -> {
try {
return new FileInputStream(file);
} catch (IOException e) {
logger.error("Fehler beim Lesen der Backup-Datei: " + fileName, e);
return null;
}
})
.build();
} catch (Exception e) {
logger.error("Fehler beim Vorbereiten des Downloads für: " + fileName, e);
return null;
}
}
}
@@ -0,0 +1,156 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package business;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ejb.Stateless;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Attribute;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
*
* @author patri
*/
@Stateless
@Named
public class ChangeToCLOBManager {
// Inject the Logger
private static final Logger logger = LogManager.getLogger(ChangeToCLOBManager.class);
@PersistenceContext
private EntityManager em;
@javax.ejb.Asynchronous
public void checkColumnType() {
/*
Map<String, List<String>> tables_values = checkLobAnnotations(em);
logger.info("running check for table values!");
tables_values.forEach((table, columns) -> {
if (columns == null || columns.isEmpty()) {
return;
}
logger.info("looking for fields in {}", table);
// Check if the column's data type is VARCHAR
columns.stream().filter(col -> (isVarcharColumn(em, table, col))).map(col -> {
// Change the column's data type to CLOB
changeColumnType(em, table, col);
return col;
}).forEachOrdered(col -> {
logger.info("Changed column type to CLOB of table: {}; column: {}", table, col);
});
});*/
}
private boolean isVarcharColumn(EntityManager em, String tableName, String columnName) {
// Construct the native SQL query
String nativeSql = "SELECT TYPE_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?1 AND COLUMN_NAME = ?2";
// Create a native Query
Query nativeQuery = em.createNativeQuery(nativeSql);
// Set the parameters for the query
nativeQuery.setParameter(1, tableName.toUpperCase());
nativeQuery.setParameter(2, columnName.toUpperCase());
logger.info("Added parameters 1: {}; 2:{}", tableName.toUpperCase(), columnName.toUpperCase());
String dataType = null;
// Execute the query and get the result
try {
dataType = (String) nativeQuery.getSingleResult();
} catch (NoResultException e) {
logger.info("NoResult", e);
}
// Return true if the column's data type is VARCHAR, false otherwise
return dataType != null ? "VARCHAR".equalsIgnoreCase(dataType) : false;
}
private void changeColumnType(EntityManager em, String tableName, String columnName) {
// Construct the native SQL query
String nativeSql = "ALTER TABLE " + tableName.toUpperCase() + " MODIFY COLUMN " + columnName.toUpperCase() + " CLOB";
logger.info(nativeSql);
// Create a native Query
Query nativeQuery = em.createNativeQuery(nativeSql);
// Execute the query
nativeQuery.executeUpdate();
}
private Map<String, List<String>> checkLobAnnotations(EntityManager em) {
// Create a Map to store the results
Map<String, List<String>> results = new HashMap<>();
// Get the Metamodel from the EntityManager
Metamodel metamodel = em.getMetamodel();
// Iterate over all the managed types
for (ManagedType<?> managedType : metamodel.getManagedTypes()) {
// Check if the managed type is an Entity
if (managedType.getJavaType().isAnnotationPresent(Entity.class)) {
// Get the EntityType for the managed type
EntityType<?> entityType = (EntityType<?>) managedType;
// Get the table name
// Iterate over all the attributes
for (Attribute<?, ?> attribute : entityType.getAttributes()) {
String tableName = entityType.getName();
String columnName = attribute.getName();
try {
logger.info(entityType.getJavaType().getName());
Class<?> cl = getClass().getClassLoader().loadClass(entityType.getJavaType().getName());
Field[] fields = getClass().getClassLoader().loadClass(entityType.getJavaType().getName()).getFields();
for(Field field : fields){
field.setAccessible(true);
logger.info(field.getName());
if (field.isAnnotationPresent(Lob.class)) {
logger.info("Field with lob class!!!");
}
}
logger.info("entity name : {}; attribute name: {}; isLob: {}", tableName, columnName, "todo");
} catch (SecurityException ex) {
logger.error("Security");
} catch (ClassNotFoundException ex) {
logger.error("Classnot");
}
// Check if the element has the @Lob annotation
if (attribute.getJavaType().isAnnotationPresent(Lob.class)) {
logger.info("Attribute " + attribute.getName() + " in " + entityType.getName() + " has @Lob annotation");
if (!results.containsKey(tableName)) {
results.put(tableName, new ArrayList<>());
}
results.get(tableName).add(columnName);
}
}
}
}
return results;
}
}
@@ -0,0 +1,60 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package business;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Named
@Startup
@Singleton
public class DatabaseBackupManager {
private static final Logger logger = LogManager.getLogger(DatabaseBackupManager.class);
@EJB
private ChangeToCLOBManager changeToCLOBManager;
@PersistenceContext(unitName="pu_person")
private EntityManager entityManager;
@Schedule(hour="4", minute = "0", second = "0", persistent = true)
public void createDatabaseBackup() {
// Get the current date and time
LocalDateTime now = LocalDateTime.now();
// Format the date and time to be included in the filename
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
String formattedDateTime = now.format(formatter);
// Create the backup filename
String backupFilename = "/h2DB/h2-mss-database-backup_" + formattedDateTime + ".zip";
// Use the EntityManager to create a backup of the H2 database
entityManager.createNativeQuery("BACKUP TO '" + backupFilename + "'")
.executeUpdate();
// Log a message indicating that the backup was successful
logger.info("Successfully created H2 database backup: " + backupFilename);
}
@PostConstruct
private void init(){
changeToCLOBManager.checkColumnType();
}
}
@@ -0,0 +1,80 @@
package business;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.primefaces.model.DefaultStreamedContent;
import org.primefaces.model.StreamedContent;
@Named
@SessionScoped
public class LogFileManager implements Serializable {
private static final Logger logger = LogManager.getLogger(LogFileManager.class);
private static final String LOG_DIRECTORY = "/logs/";
private List<File> logFiles;
@PostConstruct
public void init() {
loadLogFiles();
}
public void loadLogFiles() {
File directory = new File(LOG_DIRECTORY);
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles((dir, name) -> name != null && name.startsWith("application.log"));
if (files != null) {
logFiles = Arrays.asList(files);
Collections.sort(logFiles, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
} else {
logFiles = Collections.emptyList();
}
} else {
logFiles = Collections.emptyList();
logger.warn("Log-Verzeichnis existiert nicht: " + LOG_DIRECTORY);
}
}
public List<File> getLogFiles() {
return logFiles;
}
public StreamedContent downloadFile(String fileName) {
try {
final File file = new File(LOG_DIRECTORY + fileName);
final String contentType;
if (fileName.endsWith(".gz")) {
contentType = "application/gzip";
} else if (fileName.endsWith(".log")) {
contentType = "text/plain";
} else {
contentType = "application/octet-stream";
}
return DefaultStreamedContent.builder()
.name(fileName)
.contentType(contentType)
.stream(() -> {
try {
return new FileInputStream(file);
} catch (IOException e) {
logger.error("Fehler beim Lesen der Log-Datei: " + fileName, e);
return null;
}
})
.build();
} catch (Exception e) {
logger.error("Fehler beim Vorbereiten des Downloads für Log-Datei: " + fileName, e);
return null;
}
}
}

Some files were not shown because too many files have changed in this diff Show More