diff --git a/.roo/mcp.json b/.roo/mcp.json index fe89264..1a0955a 100644 --- a/.roo/mcp.json +++ b/.roo/mcp.json @@ -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,10 @@ "src/server.py" ], "alwaysAllow": [ - "webscraper_fetch", - "webscraper_fetch_links" + "webscraper_fetch_links", + "webscraper_fetch_section", + "webscraper_search_hint", + "webscraper_fetch" ] }, "gitea": { @@ -47,15 +51,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 +86,8 @@ "get_generation_status", "get_output_directory", "generate_image" - ] + ], + "timeout": 1800 } } } \ No newline at end of file diff --git a/.roo/rules-pic-gen/1_workflow.xml b/.roo/rules-pic-gen/1_workflow.xml new file mode 100644 index 0000000..5346aeb --- /dev/null +++ b/.roo/rules-pic-gen/1_workflow.xml @@ -0,0 +1,121 @@ + + + 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. + + + + + Generate one or more images from a text prompt + + Detailed text description + Model filename + Output width in pixels + Output height in pixels + Inference steps (4 for schnell, 20 for heretic) + Fixed seed for reproducibility; -1 = random + Things to exclude + Filename prefix for organization + Batch size 1–10 for variation exploration + Override output path (default: ~/Pictures/mcp-generated) + + Flat interleaved [TextContent, ImageContent] list — images display inline + + + + List all models registered in ComfyUI + the workflow registry + When Patrick asks which models are available, or before selecting an unusual model + + + + Check status of a queued/running generation by prompt_id + When a generation seems to have stalled or timed out + + + + Return the absolute path where images are saved + When Patrick asks where files are saved + + + + + + Understand what Patrick wants before generating + + Identify subject, style, mood, and use case from the request + Infer aspect ratio from use case (square for profiles, landscape for banners, etc.) + Determine model: schnell for speed/iteration, heretic for quality/uncensored + Ask only if the request is genuinely ambiguous — otherwise proceed with best guess + + + + + Build a high-quality FLUX prompt before calling the tool + + Write the prompt with clear subject, environment, lighting, style, and quality keywords + Add a negative_prompt if obvious artifacts should be excluded (e.g., "blurry, low quality") + Share the prompt with Patrick before generating so he can adjust if needed + + + + + Call generate_image with appropriate parameters + + Use name param with a descriptive slug for organized output files + Use count=2..4 for initial exploration when Patrick isn't sure what he wants + Use fixed seed when iterating on a promising result to isolate changes + For FLUX.2 Klein/Heretic: increase steps to 20 for best quality + + + + + Review the inline image and offer next steps + + Describe what worked and what could be improved + Offer 2-3 concrete next iteration directions (prompt tweak, seed variation, model switch) + Note the saved file path for reference + + + + + + + + First iteration / exploring concepts + Wiki/doc header images (1280x512 landscape) + Profile pictures and avatars + Non-sensitive subjects where speed matters + Batch generation of variations (fast cycle) + + steps=4, any resolution in multiples of 64 + ~10s per image on RX 7900 XTX + + + + + Mature or artistic content that schnell refuses + Higher realism requirement (photorealistic portraits, detailed scenes) + Final output after iterations established the right concept + + steps=20, 1024x1024 or higher + ~52s per image on RX 7900 XTX + Uses DreamFast Heretic Qwen3-4B encoder — abliterated, KL=0.0 + + + + + 1024x1024 + 1280x512 + 1920x1088 (nearest 64-multiple to 1920x1080) + 768x1024 + 1216x512 + + + + Image generated and displayed inline in chat + File path reported so Patrick can find it on disk + Seed reported so the result is reproducible + Next iteration options offered if result is not final + + diff --git a/.roo/rules-pic-gen/2_prompting_guide.xml b/.roo/rules-pic-gen/2_prompting_guide.xml new file mode 100644 index 0000000..c93e042 --- /dev/null +++ b/.roo/rules-pic-gen/2_prompting_guide.xml @@ -0,0 +1,141 @@ + + + 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. + + + + + [Subject + Action] + [Environment/Setting] + [Lighting] + [Camera/Lens] + [Style] + [Quality] + + + 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 + + + Comma-separation helps FLUX parse distinct attributes cleanly + Lead with the most important element (usually subject) + Quality keywords at the end reinforce overall rendering target + + + + + + photorealistic, hyperrealistic, ultra-detailed, 8K resolution, sharp focus, + professional photography, RAW photo, DSLR quality + + + digital art, concept art, artstation trending, by [artist style], + intricate details, masterpiece, studio quality + + + cinematic lighting, volumetric lighting, golden hour, dramatic rim light, + soft diffused light, neon glow, bioluminescent, subsurface scattering + + + rule of thirds, bokeh background, shallow depth of field, symmetrical, + wide angle, macro, bird's eye view, dutch angle + + + + + blurry, low quality, low resolution, pixelated, jpeg artifacts, watermark, signature + deformed, bad anatomy, extra limbs, missing fingers, fused fingers, poorly drawn hands + cartoon, anime, sketch, painting (when photorealism is desired) + + + + + AI entity portraits for BigMind profile / gallery + + [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 + + model=flux1-schnell, 1024x1024, steps=4, name=lumen_[concept] + + + + 1280x512 landscape banners for Gitea wiki pages + + [Topic concept], wide panoramic scene, [style — e.g. "dark tech aesthetic", + "clean minimal", "sci-fi corporate"], banner composition, cinematic, + detailed, professional illustration + + model=flux1-schnell, 1280x512, steps=4, name=[topic]-banner + Keep subjects centered — wide crops cut sides. Avoid text (FLUX renders text poorly). + + + + 512x512 badge/icon images for BigMind achievements + + [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 + + model=flux1-schnell, 512x512, steps=4, name=[achievement]_[tier] + + + + Iterating on a visual concept from scratch + + 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. + + + + + Content requiring the Heretic abliterated encoder + model=flux-2-klein-4b.safetensors, steps=20, 1024x1024 + + 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. + + + + + + + Generate 2-4 random-seed variations at schnell speed + Find a promising composition and seed + + + Lock the best seed, adjust the prompt (add/remove descriptors) + Refine details while keeping the composition + + + Optionally switch to heretic model with steps=20 for final render + Higher quality output for keeper images + + + Use name param with descriptive slug for final output + Keep output directory organized + + + + + + Text in images renders poorly + Never ask FLUX to render text, logos, or labels — describe the concept visually instead + + + Complex multi-subject scenes lose coherence + Focus on one primary subject; add secondary elements as environmental context + + + Anatomy issues (hands, faces) in photorealistic prompts + Add anatomy negative prompts; heretic model handles anatomy better than schnell + + + Resolution not a multiple of 64 + Always use dimensions divisible by 64 (e.g., 1280x512, 1024x1024, 768x1024) + + + diff --git a/.roo/skills/new-mcp-server/SKILL.md b/.roo/skills/new-mcp-server/SKILL.md index 6258558..0772ba2 100644 --- a/.roo/skills/new-mcp-server/SKILL.md +++ b/.roo/skills/new-mcp-server/SKILL.md @@ -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] diff --git a/.roomodes b/.roomodes new file mode 100644 index 0000000..d9c170b --- /dev/null +++ b/.roomodes @@ -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 diff --git a/docs/wiki/pages/CannaManage-02-UserStories.md b/docs/wiki/pages/CannaManage-02-UserStories.md index 103e553..830606a 100644 --- a/docs/wiki/pages/CannaManage-02-UserStories.md +++ b/docs/wiki/pages/CannaManage-02-UserStories.md @@ -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)* diff --git a/docs/wiki/pages/CannaManage-03-Architecture.md b/docs/wiki/pages/CannaManage-03-Architecture.md index 2cabfda..9293c04 100644 --- a/docs/wiki/pages/CannaManage-03-Architecture.md +++ b/docs/wiki/pages/CannaManage-03-Architecture.md @@ -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 | diff --git a/docs/wiki/pages/CannaManage-04-Flowcharts.md b/docs/wiki/pages/CannaManage-04-Flowcharts.md index 1e7dfa8..7dc1161 100644 --- a/docs/wiki/pages/CannaManage-04-Flowcharts.md +++ b/docs/wiki/pages/CannaManage-04-Flowcharts.md @@ -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] diff --git a/docs/wiki/pages/CannaManage-06-Wireframes.md b/docs/wiki/pages/CannaManage-06-Wireframes.md index 53c804c..f7c6b2b 100644 --- a/docs/wiki/pages/CannaManage-06-Wireframes.md +++ b/docs/wiki/pages/CannaManage-06-Wireframes.md @@ -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 `` | 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 `` | 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 `` | 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 `` | 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 Jan–Dec | -| Year selector | `p:selectOneMenu` | Current year ± 2 | -| Generate button | `p:commandButton` | Calls report service; shows spinner; renders PDF thumbnail | -| PDF preview | `