diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java b/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java
index 2325e0a..98185d1 100644
--- a/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java
+++ b/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java
@@ -28,7 +28,7 @@ public SecurityConfig(LocalUserDetailsService localUserDetailsService) {
public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2UserService customOAuth2UserService) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
- .requestMatchers("/login**", "/register", "/error**", "/static/**", "/app.css", "/app.js", "/webjars/**").permitAll()
+ .requestMatchers("/", "/login**", "/register", "/error**", "/static/**", "/app.css", "/app.js", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
@@ -36,12 +36,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2Us
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
- .defaultSuccessUrl("/", true)
+ .defaultSuccessUrl("/home", true)
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
- .defaultSuccessUrl("/", true)
+ .defaultSuccessUrl("/home", true)
.failureUrl("/login?error=true")
)
.logout(logout -> logout
diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/UserAccountEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/UserAccountEntity.java
index 32506f7..8e6268c 100644
--- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/UserAccountEntity.java
+++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/UserAccountEntity.java
@@ -49,3 +49,4 @@ public void onPrePersist() {
}
}
}
+
diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java
index ac9f866..57efd1d 100644
--- a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java
+++ b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java
@@ -47,6 +47,11 @@ public String addNote(@PathVariable UUID caseId, @RequestParam("content") String
}
@GetMapping("/")
+ public String landing() {
+ return "landing";
+ }
+
+ @GetMapping("/home")
public String index() {
return "index";
}
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
index 4f099cb..734607f 100644
--- a/src/main/resources/data.sql
+++ b/src/main/resources/data.sql
@@ -1,49 +1,267 @@
--- Seed Patients
+-- ------------------------------------------------------------
+-- Core user accounts (shared seed)
+--
+-- ------------------------------------------------------------
+-- Uses GITHUB provider to avoid shipping known local credentials in shared data.
+INSERT INTO user_account (id, email, password_hash, role, provider, provider_user_id, enabled, created_at)
+VALUES ('120e8400-e29b-41d4-a716-446655440000', 'admin.seed@traumateam.local', NULL, 'MANAGER', 'GITHUB', 'seed-admin-001', true, CURRENT_TIMESTAMP - INTERVAL '45 days')
+ON CONFLICT (email) DO NOTHING;
+
+INSERT INTO user_account (id, email, password_hash, role, provider, provider_user_id, enabled, created_at)
+VALUES ('120e8400-e29b-41d4-a716-446655440001', 'doctor.seed@traumateam.local', NULL, 'DOCTOR', 'GITHUB', 'seed-doctor-001', true, CURRENT_TIMESTAMP - INTERVAL '42 days')
+ON CONFLICT (email) DO NOTHING;
+
+INSERT INTO user_account (id, email, password_hash, role, provider, provider_user_id, enabled, created_at)
+VALUES ('120e8400-e29b-41d4-a716-446655440002', 'patient.seed@traumateam.local', NULL, 'PATIENT', 'GITHUB', 'seed-patient-001', true, CURRENT_TIMESTAMP - INTERVAL '40 days')
+ON CONFLICT (email) DO NOTHING;
+
+-- ------------------------------------------------------------
+-- Patients
+-- ------------------------------------------------------------
+INSERT INTO patients (id, first_name, last_name, personal_identity_number, created_at)
+VALUES ('550e8400-e29b-41d4-a716-446655440000', 'John', 'Doe', '19850512-1234', CURRENT_TIMESTAMP - INTERVAL '30 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO patients (id, first_name, last_name, personal_identity_number, created_at)
+VALUES ('550e8400-e29b-41d4-a716-446655440001', 'Jane', 'Smith', '19920824-5678', CURRENT_TIMESTAMP - INTERVAL '28 days')
+ON CONFLICT (id) DO NOTHING;
+
INSERT INTO patients (id, first_name, last_name, personal_identity_number, created_at)
-VALUES ('550e8400-e29b-41d4-a716-446655440000', 'John', 'Doe', '19850512-1234', CURRENT_TIMESTAMP)
+VALUES ('550e8400-e29b-41d4-a716-446655440002', 'Anders', 'Andersson', '19780315-9012', CURRENT_TIMESTAMP - INTERVAL '27 days')
ON CONFLICT (id) DO NOTHING;
INSERT INTO patients (id, first_name, last_name, personal_identity_number, created_at)
-VALUES ('550e8400-e29b-41d4-a716-446655440001', 'Jane', 'Smith', '19920824-5678', CURRENT_TIMESTAMP)
+VALUES ('550e8400-e29b-41d4-a716-446655440003', 'Maria', 'Nilsson', '19951201-7731', CURRENT_TIMESTAMP - INTERVAL '25 days')
ON CONFLICT (id) DO NOTHING;
INSERT INTO patients (id, first_name, last_name, personal_identity_number, created_at)
-VALUES ('550e8400-e29b-41d4-a716-446655440002', 'Anders', 'Andersson', '19780315-9012', CURRENT_TIMESTAMP)
+VALUES ('550e8400-e29b-41d4-a716-446655440004', 'Erik', 'Larsson', '19691109-1942', CURRENT_TIMESTAMP - INTERVAL '24 days')
ON CONFLICT (id) DO NOTHING;
--- Seed Employees
-INSERT INTO employees (id, display_name, role, created_at)
-VALUES ('770e8400-e29b-41d4-a716-446655440000', 'Admin User', 'MANAGER', CURRENT_TIMESTAMP)
+INSERT INTO patients (id, first_name, last_name, personal_identity_number, created_at)
+VALUES ('550e8400-e29b-41d4-a716-446655440005', 'Sofia', 'Berg', '20010218-3307', CURRENT_TIMESTAMP - INTERVAL '20 days')
+ON CONFLICT (id) DO NOTHING;
+
+-- ------------------------------------------------------------
+-- Employees
+-- ------------------------------------------------------------
+INSERT INTO employees (id, display_name, github_username, role, created_at)
+VALUES ('770e8400-e29b-41d4-a716-446655440000', 'Admin User', 'seed-admin', 'MANAGER', CURRENT_TIMESTAMP - INTERVAL '45 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO employees (id, display_name, github_username, role, created_at)
+VALUES ('770e8400-e29b-41d4-a716-446655440001', 'Dr. Alice Roberts', 'alice-roberts-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '42 days')
ON CONFLICT (id) DO NOTHING;
-INSERT INTO employees (id, display_name, role, created_at)
-VALUES ('770e8400-e29b-41d4-a716-446655440001', 'Dr. Alice Roberts', 'DOCTOR', CURRENT_TIMESTAMP)
+INSERT INTO employees (id, display_name, github_username, role, created_at)
+VALUES ('770e8400-e29b-41d4-a716-446655440002', 'Nurse Bob Jones', 'bob-jones-rn', 'NURSE', CURRENT_TIMESTAMP - INTERVAL '39 days')
ON CONFLICT (id) DO NOTHING;
-INSERT INTO employees (id, display_name, role, created_at)
-VALUES ('770e8400-e29b-41d4-a716-446655440002', 'Nurse Bob Jones', 'NURSE', CURRENT_TIMESTAMP)
+INSERT INTO employees (id, display_name, github_username, role, created_at)
+VALUES ('770e8400-e29b-41d4-a716-446655440003', 'Dr. Sara Lindholm', 'sara-lindholm-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '35 days')
ON CONFLICT (id) DO NOTHING;
--- Seed Cases
+INSERT INTO employees (id, display_name, github_username, role, created_at)
+VALUES ('770e8400-e29b-41d4-a716-446655440004', 'Nurse Emma Karlsson', 'emma-karlsson-rn', 'NURSE', CURRENT_TIMESTAMP - INTERVAL '33 days')
+ON CONFLICT (id) DO NOTHING;
+
+-- ------------------------------------------------------------
+-- Cases
+-- ------------------------------------------------------------
INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
-VALUES ('990e8400-e29b-41d4-a716-446655440000', 'Acute Chest Pain', 'Patient arrived with severe chest pain and shortness of breath.', 'ASSIGNED', '550e8400-e29b-41d4-a716-446655440000', '770e8400-e29b-41d4-a716-446655440001', '770e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP)
+VALUES ('990e8400-e29b-41d4-a716-446655440000', 'Acute Chest Pain',
+ 'Patient arrived with severe chest pain and shortness of breath during exercise.',
+ 'COMMUNICATION', '550e8400-e29b-41d4-a716-446655440000', '770e8400-e29b-41d4-a716-446655440001',
+ '770e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP - INTERVAL '12 days')
ON CONFLICT (id) DO NOTHING;
INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
-VALUES ('990e8400-e29b-41d4-a716-446655440001', 'Follow-up: Fracture', 'Routine follow-up for a healed radial fracture.', 'ASSIGNED', '550e8400-e29b-41d4-a716-446655440001', '770e8400-e29b-41d4-a716-446655440001', '770e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP)
+VALUES ('990e8400-e29b-41d4-a716-446655440001', 'Follow-up: Fracture',
+ 'Routine follow-up after distal radius fracture. Verify mobility and pain level.',
+ 'ASSIGNED', '550e8400-e29b-41d4-a716-446655440001', '770e8400-e29b-41d4-a716-446655440001',
+ '770e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP - INTERVAL '11 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
+VALUES ('990e8400-e29b-41d4-a716-446655440002', 'Persistent Migraine',
+ 'Recurring migraine episodes for two weeks with light sensitivity and nausea.',
+ 'UPDATED', '550e8400-e29b-41d4-a716-446655440002', '770e8400-e29b-41d4-a716-446655440003',
+ '770e8400-e29b-41d4-a716-446655440004', CURRENT_TIMESTAMP - INTERVAL '10 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
+VALUES ('990e8400-e29b-41d4-a716-446655440003', 'Post-surgery Wound Check',
+ 'Follow-up wound inspection after appendectomy; monitor healing and infection markers.',
+ 'CLOSED', '550e8400-e29b-41d4-a716-446655440003', '770e8400-e29b-41d4-a716-446655440001',
+ '770e8400-e29b-41d4-a716-446655440004', CURRENT_TIMESTAMP - INTERVAL '9 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
+VALUES ('990e8400-e29b-41d4-a716-446655440004', 'Hypertension Medication Review',
+ 'Blood pressure fluctuations despite current medication. Evaluate treatment adjustment.',
+ 'COMMUNICATION', '550e8400-e29b-41d4-a716-446655440004', '770e8400-e29b-41d4-a716-446655440003',
+ '770e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP - INTERVAL '8 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
+VALUES ('990e8400-e29b-41d4-a716-446655440005', 'Allergic Reaction Observation',
+ 'Mild allergic reaction after new antibiotic; monitor symptoms and response to treatment.',
+ 'CREATED', '550e8400-e29b-41d4-a716-446655440005', '770e8400-e29b-41d4-a716-446655440001',
+ '770e8400-e29b-41d4-a716-446655440004', CURRENT_TIMESTAMP - INTERVAL '3 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
+VALUES ('990e8400-e29b-41d4-a716-446655440006', 'Knee Pain After Sports Injury',
+ 'Suspected meniscus strain after football practice; reduced range of motion.',
+ 'ASSIGNED', '550e8400-e29b-41d4-a716-446655440001', '770e8400-e29b-41d4-a716-446655440003',
+ '770e8400-e29b-41d4-a716-446655440004', CURRENT_TIMESTAMP - INTERVAL '2 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO cases (id, title, description, status, patient_id, owner_id, handler_id, created_at)
+VALUES ('990e8400-e29b-41d4-a716-446655440007', 'Diabetes Checkup and Education',
+ 'Quarterly diabetes review with glucose trends, lifestyle coaching and medication adherence.',
+ 'UPDATED', '550e8400-e29b-41d4-a716-446655440000', '770e8400-e29b-41d4-a716-446655440001',
+ '770e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP - INTERVAL '1 days')
+ON CONFLICT (id) DO NOTHING;
+
+-- ------------------------------------------------------------
+-- Case notes (comments)
+-- ------------------------------------------------------------
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440000', '990e8400-e29b-41d4-a716-446655440000',
+ 'ECG completed. Awaiting blood panel for troponin and CRP.',
+ 'Dr. Alice Roberts', 'alice-roberts-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '12 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440001', '990e8400-e29b-41d4-a716-446655440000',
+ 'Patient stable on observation ward. Chest pain now rated 3/10.',
+ 'Nurse Bob Jones', 'bob-jones-rn', 'NURSE', CURRENT_TIMESTAMP - INTERVAL '11 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440002', '990e8400-e29b-41d4-a716-446655440001',
+ 'Cast removed. Physiotherapy exercises explained and demonstrated.',
+ 'Dr. Alice Roberts', 'alice-roberts-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '10 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440003', '990e8400-e29b-41d4-a716-446655440002',
+ 'Triggered by stress and lack of sleep. Recommended headache diary.',
+ 'Dr. Sara Lindholm', 'sara-lindholm-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '9 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440004', '990e8400-e29b-41d4-a716-446655440003',
+ 'Wound dry and clean. Sutures removed. No signs of infection.',
+ 'Nurse Emma Karlsson', 'emma-karlsson-rn', 'NURSE', CURRENT_TIMESTAMP - INTERVAL '8 days')
ON CONFLICT (id) DO NOTHING;
--- Creates the new table for all user accounts (both local and GitHub)
-CREATE TABLE IF NOT EXISTS user_account (
- id UUID PRIMARY KEY,
- email VARCHAR(255) UNIQUE,
- password_hash VARCHAR(255),
- role VARCHAR(50) NOT NULL,
- provider VARCHAR(50) NOT NULL,
- provider_user_id VARCHAR(255),
- enabled BOOLEAN NOT NULL,
- created_at TIMESTAMP WITH TIME ZONE NOT NULL
- );
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440005', '990e8400-e29b-41d4-a716-446655440004',
+ 'Home blood-pressure log reviewed. Evening values remain elevated.',
+ 'Dr. Sara Lindholm', 'sara-lindholm-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '7 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440006', '990e8400-e29b-41d4-a716-446655440006',
+ 'Swelling reduced. MRI referral submitted for meniscus evaluation.',
+ 'Dr. Sara Lindholm', 'sara-lindholm-md', 'DOCTOR', CURRENT_TIMESTAMP - INTERVAL '1 days')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO case_notes (id, case_id, content, author_display_name, author_github_username, author_role, created_at)
+VALUES ('a90e8400-e29b-41d4-a716-446655440007', '990e8400-e29b-41d4-a716-446655440007',
+ 'Diet plan and glucose monitoring schedule reviewed with patient.',
+ 'Nurse Bob Jones', 'bob-jones-rn', 'NURSE', CURRENT_TIMESTAMP - INTERVAL '20 hours')
+ON CONFLICT (id) DO NOTHING;
+
+-- ------------------------------------------------------------
+-- Documents
+-- ------------------------------------------------------------
+INSERT INTO documents (id, file_name, s3_key, content_type, file_size, uploaded_at, uploaded_by, case_id)
+VALUES ('b90e8400-e29b-41d4-a716-446655440000', 'ecg-initial.pdf', 'cases/990e8400-e29b-41d4-a716-446655440000/ecg-initial.pdf',
+ 'application/pdf', 238412, CURRENT_TIMESTAMP - INTERVAL '12 days', '770e8400-e29b-41d4-a716-446655440001',
+ '990e8400-e29b-41d4-a716-446655440000')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO documents (id, file_name, s3_key, content_type, file_size, uploaded_at, uploaded_by, case_id)
+VALUES ('b90e8400-e29b-41d4-a716-446655440001', 'xray-fracture-followup.png', 'cases/990e8400-e29b-41d4-a716-446655440001/xray-fracture-followup.png',
+ 'image/png', 484190, CURRENT_TIMESTAMP - INTERVAL '10 days', '770e8400-e29b-41d4-a716-446655440002',
+ '990e8400-e29b-41d4-a716-446655440001')
+ON CONFLICT (id) DO NOTHING;
--- Local admin seed intentionally moved to data-local.sql so production/shared seeds
--- do not embed a known credential hash.
+INSERT INTO documents (id, file_name, s3_key, content_type, file_size, uploaded_at, uploaded_by, case_id)
+VALUES ('b90e8400-e29b-41d4-a716-446655440002', 'migraine-diary-week1.docx', 'cases/990e8400-e29b-41d4-a716-446655440002/migraine-diary-week1.docx',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 91324,
+ CURRENT_TIMESTAMP - INTERVAL '8 days', '770e8400-e29b-41d4-a716-446655440004', '990e8400-e29b-41d4-a716-446655440002')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO documents (id, file_name, s3_key, content_type, file_size, uploaded_at, uploaded_by, case_id)
+VALUES ('b90e8400-e29b-41d4-a716-446655440003', 'post-op-wound-photo.jpg', 'cases/990e8400-e29b-41d4-a716-446655440003/post-op-wound-photo.jpg',
+ 'image/jpeg', 356778, CURRENT_TIMESTAMP - INTERVAL '9 days', '770e8400-e29b-41d4-a716-446655440004',
+ '990e8400-e29b-41d4-a716-446655440003')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO documents (id, file_name, s3_key, content_type, file_size, uploaded_at, uploaded_by, case_id)
+VALUES ('b90e8400-e29b-41d4-a716-446655440004', 'bp-home-readings.xlsx', 'cases/990e8400-e29b-41d4-a716-446655440004/bp-home-readings.xlsx',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 120512,
+ CURRENT_TIMESTAMP - INTERVAL '6 days', '770e8400-e29b-41d4-a716-446655440002', '990e8400-e29b-41d4-a716-446655440004')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO documents (id, file_name, s3_key, content_type, file_size, uploaded_at, uploaded_by, case_id)
+VALUES ('b90e8400-e29b-41d4-a716-446655440005', 'mri-referral.pdf', 'cases/990e8400-e29b-41d4-a716-446655440006/mri-referral.pdf',
+ 'application/pdf', 204889, CURRENT_TIMESTAMP - INTERVAL '22 hours', '770e8400-e29b-41d4-a716-446655440003',
+ '990e8400-e29b-41d4-a716-446655440006')
+ON CONFLICT (id) DO NOTHING;
+
+-- ------------------------------------------------------------
+-- Audit events
+-- ------------------------------------------------------------
+INSERT INTO audit_events (id, occurred_at, actor_id, actor_role, principal_name, request_path, query_string, handler,
+ response_status, error_type, case_id, status_change, event_name, description, client_ip, user_agent)
+VALUES ('c90e8400-e29b-41d4-a716-446655440000', CURRENT_TIMESTAMP - INTERVAL '12 days',
+ '770e8400-e29b-41d4-a716-446655440001', 'DOCTOR', 'doctor.seed@traumateam.local',
+ '/ui/cases/990e8400-e29b-41d4-a716-446655440000', NULL, 'CaseUiController#showCase',
+ 200, NULL, '990e8400-e29b-41d4-a716-446655440000', 'CREATED->ASSIGNED', 'CASE_STATUS_CHANGED',
+ 'Case owner assigned to Dr. Alice Roberts.', '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO audit_events (id, occurred_at, actor_id, actor_role, principal_name, request_path, query_string, handler,
+ response_status, error_type, case_id, status_change, event_name, description, client_ip, user_agent)
+VALUES ('c90e8400-e29b-41d4-a716-446655440001', CURRENT_TIMESTAMP - INTERVAL '11 days',
+ '770e8400-e29b-41d4-a716-446655440002', 'NURSE', 'nurse.bob@traumateam.local',
+ '/ui/cases/990e8400-e29b-41d4-a716-446655440000/comments', NULL, 'CaseNoteUiController#create',
+ 302, NULL, '990e8400-e29b-41d4-a716-446655440000', NULL, 'CASE_NOTE_ADDED',
+ 'Nurse added observation note after first medication cycle.', '127.0.0.1',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO audit_events (id, occurred_at, actor_id, actor_role, principal_name, request_path, query_string, handler,
+ response_status, error_type, case_id, status_change, event_name, description, client_ip, user_agent)
+VALUES ('c90e8400-e29b-41d4-a716-446655440002', CURRENT_TIMESTAMP - INTERVAL '10 days',
+ '770e8400-e29b-41d4-a716-446655440001', 'DOCTOR', 'doctor.seed@traumateam.local',
+ '/ui/cases/990e8400-e29b-41d4-a716-446655440001/documents', NULL, 'DocumentUiController#upload',
+ 201, NULL, '990e8400-e29b-41d4-a716-446655440001', NULL, 'DOCUMENT_UPLOADED',
+ 'Follow-up x-ray uploaded and attached to case timeline.', '127.0.0.1',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO audit_events (id, occurred_at, actor_id, actor_role, principal_name, request_path, query_string, handler,
+ response_status, error_type, case_id, status_change, event_name, description, client_ip, user_agent)
+VALUES ('c90e8400-e29b-41d4-a716-446655440003', CURRENT_TIMESTAMP - INTERVAL '8 days',
+ '770e8400-e29b-41d4-a716-446655440003', 'DOCTOR', 'doctor.sara@traumateam.local',
+ '/api/cases/990e8400-e29b-41d4-a716-446655440004/status', 'target=COMMUNICATION', 'CaseController#updateStatus',
+ 200, NULL, '990e8400-e29b-41d4-a716-446655440004', 'ASSIGNED->COMMUNICATION', 'CASE_STATUS_CHANGED',
+ 'Case moved to communication after medication discussion with patient.', '127.0.0.1',
+ 'PostmanRuntime/7.43.0')
+ON CONFLICT (id) DO NOTHING;
+
+INSERT INTO audit_events (id, occurred_at, actor_id, actor_role, principal_name, request_path, query_string, handler,
+ response_status, error_type, case_id, status_change, event_name, description, client_ip, user_agent)
+VALUES ('c90e8400-e29b-41d4-a716-446655440004', CURRENT_TIMESTAMP - INTERVAL '2 days',
+ '770e8400-e29b-41d4-a716-446655440000', 'MANAGER', 'admin.seed@traumateam.local',
+ '/ui/admin/audit', NULL, 'AdminAuditUiController#index',
+ 200, NULL, NULL, NULL, 'AUDIT_VIEWED',
+ 'Manager reviewed audit overview dashboard.', '127.0.0.1',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
+ON CONFLICT (id) DO NOTHING;
diff --git a/src/main/resources/static/app.css b/src/main/resources/static/app.css
index 28dd506..90cb613 100644
--- a/src/main/resources/static/app.css
+++ b/src/main/resources/static/app.css
@@ -19,9 +19,7 @@ html, body { height: 100%; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Apple Color Emoji", "Segoe UI Emoji";
- background: radial-gradient(1200px 700px at 20% 0%, rgba(110, 168, 255, 0.25), transparent 60%),
- radial-gradient(900px 500px at 70% 20%, rgba(142, 97, 255, 0.18), transparent 60%),
- var(--bg);
+ background: var(--bg);
color: var(--text);
line-height: 1.5;
}
@@ -101,7 +99,7 @@ p { margin: 0 0 14px; color: var(--muted); max-width: 75ch; }
display: block;
text-decoration: none;
color: var(--text);
- background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
+ background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
@@ -110,13 +108,13 @@ p { margin: 0 0 14px; color: var(--muted); max-width: 75ch; }
}
.card:hover {
border-color: rgba(110, 168, 255, 0.45);
- background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
+ background: rgba(255,255,255,0.05);
transform: translateY(-1px);
}
.card p { margin: 6px 0 0; }
.panel {
- background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
+ background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
@@ -256,15 +254,862 @@ textarea.input { padding: 12px; resize: vertical; }
}
.footer-row { display: flex; justify-content: space-between; align-items: center; }
+.landing-page {
+ display: grid;
+ gap: 18px;
+}
+
+.landing-progress {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ width: 100%;
+ height: 3px;
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.landing-progress-bar {
+ width: 0;
+ height: 100%;
+ background: linear-gradient(90deg, var(--accent), var(--accent-strong));
+ transition: width 180ms ease-out;
+}
+
+.landing-topbar {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+}
+
+.landing-login {
+ min-width: 112px;
+}
+
+.landing-hero {
+ text-align: center;
+ display: grid;
+ gap: 10px;
+ justify-items: center;
+ padding: 28px;
+}
+
+.landing-hero--presentation {
+ gap: 12px;
+ padding: 32px 28px;
+}
+
+.landing-hero-meta {
+ display: inline-flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.landing-hero-time {
+ font-size: 12px;
+ color: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ padding: 4px 10px;
+}
+
+.landing-lead {
+ font-size: 17px;
+ max-width: 72ch;
+}
+
+.landing-hero-footnote {
+ margin: 2px 0 0;
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.landing-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.landing-cta-primary { min-width: 170px; }
+.landing-cta-secondary { min-width: 160px; }
+
+.landing-actions .button {
+ transform: translateY(0);
+}
+
+.landing-actions .button:hover {
+ transform: translateY(-1px);
+}
+
+.landing-actions .button:active {
+ transform: translateY(0);
+}
+
+.landing-nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.landing-nav-link {
+ text-decoration: none;
+ color: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ padding: 6px 12px;
+ font-size: 13px;
+ transition: all var(--transition-fast);
+}
+
+.landing-nav-link:hover {
+ color: var(--text);
+ border-color: rgba(110, 168, 255, 0.55);
+ background: rgba(110, 168, 255, 0.12);
+}
+
+.landing-nav-link.active {
+ color: var(--text);
+ border-color: rgba(143, 190, 255, 0.8);
+ background: rgba(110, 168, 255, 0.22);
+}
+
+.landing-section {
+ display: grid;
+ gap: 14px;
+}
+
+.landing-section-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.landing-section-kicker {
+ margin: 0;
+ font-size: 14px;
+ color: var(--muted);
+}
+
+.landing-columns {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.landing-subpanel {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.landing-subpanel p {
+ margin-bottom: 0;
+}
+
+.landing-subpanel--problem { border-color: rgba(255,255,255,0.14); }
+.landing-subpanel--impact { border-color: rgba(255,110,138,0.28); }
+.landing-subpanel--solution { border-color: rgba(110,168,255,0.45); }
+
+.landing-subpanel {
+ transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
+}
+
+.landing-subpanel:hover {
+ background: rgba(255,255,255,0.05);
+ transform: translateY(-2px);
+}
+
+.landing-status-flow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+}
+
+.landing-chip {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ border: 1px solid rgba(110, 168, 255, 0.55);
+ background: rgba(110, 168, 255, 0.16);
+ color: var(--text);
+ min-height: 30px;
+ padding: 0 12px;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.landing-flow-arrow {
+ color: var(--muted);
+ font-weight: 700;
+}
+
+.section-intro {
+ margin: 0;
+ color: var(--text);
+ font-size: 15px;
+}
+
+.lifecycle-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.lifecycle-card {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.step-kicker {
+ margin: 0 0 4px;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-size: 11px;
+}
+
+.status-legend {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ border-radius: 999px;
+ padding: 0 10px;
+ border: 1px solid var(--border);
+ font-size: 12px;
+ color: var(--text);
+}
+
+.chip-owner {
+ border-color: rgba(110, 168, 255, 0.4);
+ background: rgba(110, 168, 255, 0.14);
+}
+
+.chip-status-open {
+ border-color: rgba(110, 168, 255, 0.4);
+ background: rgba(110, 168, 255, 0.14);
+}
+
+.chip-status-progress {
+ border-color: rgba(255, 210, 120, 0.45);
+ background: rgba(255, 210, 120, 0.14);
+}
+
+.chip-status-closed {
+ border-color: rgba(123, 228, 149, 0.45);
+ background: rgba(123, 228, 149, 0.14);
+}
+
+.roles-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.role-card {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.role-summary {
+ margin: 0 0 10px;
+}
+
+.chip-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.chip-allow::before {
+ content: "✓";
+ margin-right: 6px;
+ opacity: 0.9;
+}
+
+.chip-deny {
+ border-color: rgba(255, 110, 138, 0.35);
+ background: rgba(255, 110, 138, 0.12);
+}
+
+.chip-deny::before {
+ content: "—";
+ margin-right: 6px;
+ opacity: 0.9;
+}
+
+.module-head {
+ display: grid;
+ gap: 6px;
+ margin-bottom: 6px;
+}
+
+.module-intro {
+ margin: 0;
+ color: var(--text);
+ font-size: 15px;
+ max-width: 70ch;
+}
+
+.trust-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.trust-chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ padding: 0 10px;
+ border-radius: 999px;
+ border: 1px solid rgba(123, 228, 149, 0.45);
+ background: rgba(123, 228, 149, 0.12);
+ color: #dff7e7;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
+.proof-card h3,
+.evidence-note h3 {
+ margin-bottom: 10px;
+}
+
+.proof-list {
+ margin: 0;
+ padding-left: 18px;
+ color: var(--muted);
+}
+
+.proof-list li + li {
+ margin-top: 6px;
+}
+
+.evidence-note {
+ border-color: rgba(110, 168, 255, 0.42);
+ background: linear-gradient(180deg, rgba(110, 168, 255, 0.12), rgba(110, 168, 255, 0.06));
+}
+
+.landing-section-proof {
+ gap: 12px;
+}
+
+.landing-proof-lead {
+ margin: 0;
+ font-size: 16px;
+ color: var(--text);
+ max-width: 78ch;
+}
+
+.landing-proof-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.landing-proof-card {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.03);
+ box-shadow: inset 0 0 0 1px rgba(110, 168, 255, 0.06);
+}
+
+.landing-proof-card p {
+ margin-bottom: 0;
+}
+
+.landing-close {
+ display: grid;
+ gap: 8px;
+ justify-items: start;
+}
+
+.landing-close-cta {
+ border-color: rgba(110, 168, 255, 0.55);
+ background: rgba(255,255,255,0.02);
+ gap: 10px;
+}
+
+.landing-close-kicker {
+ margin: 0;
+ font-size: 12px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--muted);
+}
+
+.landing-close-lead {
+ margin: 0;
+ color: var(--text);
+ font-size: 16px;
+ max-width: 72ch;
+}
+
+.landing-close-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.landing-close-actions {
+ margin-top: 4px;
+}
+
+.button-strong {
+ border-color: rgba(143, 190, 255, 0.95);
+ background: linear-gradient(180deg, rgba(110, 168, 255, 0.42), rgba(110, 168, 255, 0.24));
+ box-shadow: 0 0 0 1px rgba(143, 190, 255, 0.25), 0 8px 20px rgba(110, 168, 255, 0.18);
+}
+
+.button-strong:hover {
+ background: linear-gradient(180deg, rgba(110, 168, 255, 0.5), rgba(110, 168, 255, 0.3));
+}
+
+.js-reveal {
+ opacity: 1;
+ transform: none;
+}
+
+.js-enhanced .js-reveal {
+ opacity: 0;
+ transform: translateY(18px);
+}
+
+.js-enhanced .js-reveal.revealed {
+ opacity: 1;
+ transform: translateY(0);
+ transition: opacity 360ms ease, transform 360ms ease;
+}
+
+@keyframes ctaPulse {
+ 0%, 100% { transform: translateY(0); }
+ 35% { transform: translateY(-1px) scale(1.01); }
+ 70% { transform: translateY(0); }
+}
+
+.cta-attention {
+ animation: ctaPulse 500ms ease-in-out 2;
+}
+
@media (max-width: 700px) {
.container { width: min(1140px, calc(100% - 24px)); }
.page { padding-top: 24px; }
.page-title { align-items: flex-start; flex-direction: column; }
.auth-label { margin-right: 0; display: block; margin-bottom: 8px; }
+ .landing-columns { grid-template-columns: 1fr; }
+ .landing-nav { gap: 6px; }
+ .landing-nav-link { font-size: 12px; }
+ .landing-flow-arrow { display: none; }
+ .landing-hero { padding: 22px; }
+ .roles-grid { grid-template-columns: 1fr; }
+ .landing-proof-grid { grid-template-columns: 1fr; }
}
@media (max-width: 980px) {
.header-row { grid-template-columns: 1fr; align-items: start; }
.cards { grid-template-columns: 1fr; }
.auth-grid { grid-template-columns: 1fr; }
+ .landing-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .landing-proof-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .lifecycle-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .landing-progress-bar,
+ .landing-nav-link,
+ .js-reveal.revealed {
+ transition: none;
+ }
+
+ .js-reveal {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+/* Calm presentation overrides */
+.landing-page {
+ gap: 24px;
+}
+
+.landing-section,
+#hero-cta,
+#final-cta {
+ padding: 24px;
+}
+
+.landing-nav-link {
+ background: rgba(255, 255, 255, 0.01);
+}
+
+.landing-section-kicker,
+.module-intro,
+.landing-close-lead {
+ max-width: 58ch;
+}
+
+.segmented-tabs {
+ display: inline-flex;
+ gap: 6px;
+ padding: 4px;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.segmented-tabs__btn {
+ border: 0;
+ border-radius: 999px;
+ min-height: 32px;
+ padding: 0 12px;
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+}
+
+.segmented-tabs__btn.is-active {
+ color: var(--text);
+ background: rgba(110, 168, 255, 0.2);
+}
+
+.tab-panel {
+ margin-top: 12px;
+}
+
+.icon-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
+ gap: 12px;
+}
+
+.icon-card {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.icon-card h3 {
+ margin: 0;
+}
+
+.icon-dot {
+ display: inline-grid;
+ place-items: center;
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ margin-bottom: 8px;
+ font-weight: 700;
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.icon-card--alert .icon-dot {
+ background: rgba(255, 110, 138, 0.2);
+}
+
+.icon-card--good .icon-dot {
+ background: rgba(123, 228, 149, 0.2);
+}
+
+.flow-timeline {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.flow-step {
+ width: 100%;
+ min-height: 36px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.03);
+ color: var(--text);
+ cursor: pointer;
+}
+
+.flow-step.is-active {
+ border-color: rgba(143, 190, 255, 0.8);
+ background: rgba(110, 168, 255, 0.2);
+}
+
+.flow-detail {
+ margin-top: 12px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.role-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.role-tabs__btn {
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ min-height: 32px;
+ padding: 0 12px;
+ background: rgba(255, 255, 255, 0.02);
+ color: var(--muted);
+ cursor: pointer;
+}
+
+.role-tabs__btn.is-active {
+ color: var(--text);
+ border-color: rgba(143, 190, 255, 0.8);
+ background: rgba(110, 168, 255, 0.2);
+}
+
+.role-panel {
+ margin-top: 12px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 14px;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.metrics-row,
+.permission-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.metric-chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ border-radius: 999px;
+ padding: 0 10px;
+ border: 1px solid var(--border);
+ font-size: 12px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.permission-chip--allow {
+ border-color: rgba(123, 228, 149, 0.45);
+ background: rgba(123, 228, 149, 0.14);
+}
+
+.permission-chip--deny {
+ border-color: rgba(255, 110, 138, 0.35);
+ background: rgba(255, 110, 138, 0.12);
+}
+
+.accordion {
+ display: grid;
+ gap: 8px;
+}
+
+.accordion__trigger {
+ text-align: left;
+ min-height: 38px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.03);
+ color: var(--text);
+ padding: 0 12px;
+ cursor: pointer;
+}
+
+.accordion__panel {
+ margin-top: -2px;
+ margin-bottom: 4px;
+ border-left: 2px solid rgba(110, 168, 255, 0.45);
+ padding: 8px 12px;
+ color: var(--muted);
+}
+
+.lite-details {
+ margin-top: 10px;
+}
+
+.lite-details summary {
+ cursor: pointer;
+ color: var(--text);
+}
+
+@media (max-width: 900px) {
+ .flow-timeline {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+/* Cohesive layout fix: avoid split panel feeling */
+.landing-page {
+ border: 0;
+ border-radius: 0;
+ background: none;
+ padding: 0;
+}
+
+.landing-progress {
+ position: relative;
+ top: auto;
+}
+
+.landing-page > .panel {
+ background: rgba(255,255,255,0.03);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ padding: 18px;
+}
+
+.landing-page > .panel + .panel {
+ margin-top: 0;
+ border-top: 0;
+}
+
+.landing-close-cta {
+ background: rgba(255,255,255,0.03);
+ border-color: var(--border);
+}
+
+/* Presentation pop polish: higher contrast + playful depth */
+.landing-page > .panel {
+ border-color: rgba(143, 190, 255, 0.26);
+ box-shadow: 0 12px 30px rgba(2, 8, 28, 0.42);
+}
+
+.landing-page > .panel:hover {
+ border-color: rgba(143, 190, 255, 0.48);
+}
+
+h1, h2 {
+ color: #f4f7ff;
+}
+
+.landing-lead,
+.module-intro,
+.landing-section-kicker,
+.landing-close-lead {
+ color: rgba(236, 242, 255, 0.88);
+}
+
+.notice-badge,
+.landing-hero-time {
+ border-color: rgba(143, 190, 255, 0.66);
+ background: rgba(110, 168, 255, 0.26);
+ color: #f3f7ff;
+}
+
+.landing-nav-link {
+ color: rgba(239, 244, 255, 0.9);
+ border-color: rgba(143, 190, 255, 0.25);
+ background: rgba(110, 168, 255, 0.07);
+}
+
+.landing-nav-link:hover,
+.landing-nav-link.active {
+ border-color: rgba(143, 190, 255, 0.8);
+ background: rgba(110, 168, 255, 0.26);
+ box-shadow: 0 0 0 1px rgba(143, 190, 255, 0.2) inset;
+}
+
+.button {
+ border-color: rgba(143, 190, 255, 0.78);
+ background: rgba(110, 168, 255, 0.33);
+}
+
+.button:hover {
+ background: rgba(110, 168, 255, 0.46);
+ box-shadow: 0 8px 18px rgba(67, 131, 255, 0.28);
+}
+
+.button-secondary {
+ border-color: rgba(231, 236, 255, 0.32);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.button-secondary:hover {
+ background: rgba(255, 255, 255, 0.16);
+}
+
+.button-strong {
+ border-color: rgba(180, 214, 255, 0.95);
+ background: linear-gradient(180deg, rgba(110, 168, 255, 0.6), rgba(93, 154, 251, 0.42));
+ box-shadow: 0 0 0 1px rgba(180, 214, 255, 0.24), 0 10px 24px rgba(72, 142, 255, 0.34);
+}
+
+.icon-card,
+.flow-detail,
+.role-panel {
+ border-color: rgba(143, 190, 255, 0.22);
+ background: rgba(255, 255, 255, 0.045);
+}
+
+.icon-card:hover {
+ transform: translateY(-2px);
+ border-color: rgba(143, 190, 255, 0.56);
+ background: rgba(255, 255, 255, 0.07);
+}
+
+.icon-dot {
+ color: #f2f6ff;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
+}
+
+.metric-chip,
+.landing-chip {
+ border-color: rgba(143, 190, 255, 0.45);
+ background: rgba(110, 168, 255, 0.18);
+ color: #f2f6ff;
+}
+
+.permission-chip--allow {
+ border-color: rgba(123, 228, 149, 0.7);
+ background: rgba(123, 228, 149, 0.24);
+}
+
+.permission-chip--deny {
+ border-color: rgba(255, 110, 138, 0.62);
+ background: rgba(255, 110, 138, 0.2);
+}
+
+.flow-step,
+.role-tabs__btn,
+.segmented-tabs__btn,
+.accordion__trigger {
+ border-color: rgba(143, 190, 255, 0.34);
+ color: #ebf1ff;
+}
+
+.flow-step.is-active,
+.role-tabs__btn.is-active,
+.segmented-tabs__btn.is-active {
+ border-color: rgba(180, 214, 255, 0.95);
+ background: rgba(110, 168, 255, 0.34);
+ box-shadow: 0 0 0 1px rgba(180, 214, 255, 0.24) inset;
+}
+
+.accordion__trigger:hover {
+ background: rgba(110, 168, 255, 0.14);
}
diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js
new file mode 100644
index 0000000..15de759
--- /dev/null
+++ b/src/main/resources/static/app.js
@@ -0,0 +1,253 @@
+(() => {
+ document.documentElement.classList.add("js-enhanced");
+
+ const landingRoot = document.querySelector(".landing-page");
+ if (!landingRoot) {
+ return;
+ }
+
+ const sectionIds = [
+ "problem-solution",
+ "case-lifecycle",
+ "role-based-access",
+ "documents-reliability",
+ "audit-trust",
+ "architecture-quality",
+ ];
+
+ const navLinks = Array.from(document.querySelectorAll(".landing-nav-link"));
+ const sections = sectionIds
+ .map((id) => document.getElementById(id))
+ .filter((section) => section !== null);
+ const progressBar = document.getElementById("landingProgressBar");
+ const revealTargets = Array.from(document.querySelectorAll(".js-reveal"));
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ // Smooth scroll between module anchors for presentation flow.
+ navLinks.forEach((link) => {
+ link.addEventListener("click", (event) => {
+ const href = link.getAttribute("href");
+ if (!href || !href.startsWith("#")) {
+ return;
+ }
+ const target = document.querySelector(href);
+ if (!target) {
+ return;
+ }
+
+ event.preventDefault();
+ target.scrollIntoView({ behavior: prefersReducedMotion ? "auto" : "smooth", block: "start" });
+ history.replaceState(null, "", href);
+ });
+ });
+
+ const updateActiveLink = (activeId) => {
+ navLinks.forEach((link) => {
+ const isActive = link.getAttribute("href") === `#${activeId}`;
+ link.classList.toggle("active", isActive);
+ if (isActive) {
+ link.setAttribute("aria-current", "true");
+ } else {
+ link.removeAttribute("aria-current");
+ }
+ });
+ };
+
+ const sectionObserver = new IntersectionObserver(
+ (entries) => {
+ let topVisible = null;
+ for (const entry of entries) {
+ if (!entry.isIntersecting) {
+ continue;
+ }
+ if (!topVisible || entry.boundingClientRect.top < topVisible.boundingClientRect.top) {
+ topVisible = entry;
+ }
+ }
+ if (topVisible && topVisible.target.id) {
+ updateActiveLink(topVisible.target.id);
+ }
+ },
+ { threshold: 0.45, rootMargin: "-10% 0px -45% 0px" }
+ );
+
+ sections.forEach((section) => sectionObserver.observe(section));
+
+ const updateProgress = () => {
+ if (!progressBar) {
+ return;
+ }
+ const scrollTop = window.scrollY || window.pageYOffset;
+ const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
+ const progress = documentHeight <= 0 ? 100 : Math.min(100, Math.max(0, (scrollTop / documentHeight) * 100));
+ progressBar.style.width = `${progress}%`;
+ };
+
+ window.addEventListener("scroll", updateProgress, { passive: true });
+ window.addEventListener("resize", updateProgress);
+ updateProgress();
+
+ if (prefersReducedMotion) {
+ revealTargets.forEach((node) => node.classList.add("revealed"));
+ } else {
+ const revealObserver = new IntersectionObserver(
+ (entries, observer) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) {
+ return;
+ }
+ entry.target.classList.add("revealed");
+ observer.unobserve(entry.target);
+ });
+ },
+ { threshold: 0.18 }
+ );
+
+ revealTargets.forEach((node) => revealObserver.observe(node));
+ }
+
+ const finalCta = document.getElementById("final-cta");
+ const primaryCta = finalCta ? finalCta.querySelector(".button-strong") : null;
+ if (finalCta && primaryCta && !prefersReducedMotion) {
+ const ctaObserver = new IntersectionObserver(
+ (entries, observer) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) {
+ return;
+ }
+ primaryCta.classList.add("cta-attention");
+ window.setTimeout(() => primaryCta.classList.remove("cta-attention"), 1200);
+ observer.unobserve(entry.target);
+ });
+ },
+ { threshold: 0.45 }
+ );
+ ctaObserver.observe(finalCta);
+ }
+
+ const tabButtons = Array.from(document.querySelectorAll(".segmented-tabs__btn"));
+ tabButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const targetSelector = button.getAttribute("data-target");
+ if (!targetSelector) return;
+
+ tabButtons.forEach((btn) => {
+ const selected = btn === button;
+ btn.classList.toggle("is-active", selected);
+ btn.setAttribute("aria-selected", String(selected));
+ });
+
+ document.querySelectorAll(".tab-panel").forEach((panel) => {
+ panel.hidden = true;
+ panel.classList.remove("is-active");
+ });
+
+ const target = document.querySelector(targetSelector);
+ if (target) {
+ target.hidden = false;
+ target.classList.add("is-active");
+ }
+ });
+ });
+
+ const flowData = {
+ create: {
+ title: "🆕 Skapa ärende",
+ chips: ["🧑 Patientkontext", "🏁 Startstatus", "🧭 Tydlig start"],
+ },
+ assign: {
+ title: "👤 Tilldela ansvar",
+ chips: ["👑 Ägare", "🛠️ Handläggare", "🎯 Klart ansvar"],
+ },
+ update: {
+ title: "📝 Uppdatera och kommunicera",
+ chips: ["🗒️ Anteckningar", "📍 Status", "🤝 Samarbete"],
+ },
+ close: {
+ title: "✅ Avsluta med spårbarhet",
+ chips: ["🏁 Avslut", "📚 Historik", "📈 Uppföljning"],
+ },
+ };
+
+ const flowDetail = document.getElementById("flowDetail");
+ const renderFlow = (key) => {
+ const item = flowData[key];
+ if (!flowDetail || !item) return;
+ flowDetail.innerHTML = `
+
${item.title}
+
+ ${item.chips.map((chip) => `${chip}`).join("")}
+
+ `;
+ };
+
+ const flowSteps = Array.from(document.querySelectorAll(".flow-step"));
+ flowSteps.forEach((button) => {
+ button.addEventListener("click", () => {
+ flowSteps.forEach((btn) => btn.classList.remove("is-active"));
+ button.classList.add("is-active");
+ renderFlow(button.getAttribute("data-step"));
+ });
+ });
+ renderFlow("create");
+
+ const roleData = {
+ manager: {
+ allow: ["👀 Se alla ärenden", "🧩 Hantera tilldelning", "📜 Granska loggar"],
+ deny: ["🚫 Ingen patientimitation"],
+ },
+ doctor: {
+ allow: ["✏️ Uppdatera tilldelade", "🗒️ Lägga anteckningar", "📁 Hantera dokument"],
+ deny: ["🚫 Ingen användaradministration"],
+ },
+ nurse: {
+ allow: ["👀 Se handlagda ärenden", "🗒️ Lägga anteckningar"],
+ deny: ["🚫 Ingen full adminbehörighet"],
+ },
+ patient: {
+ allow: ["📲 Följa egna ärenden", "💬 Lägga kommunikationsnotis"],
+ deny: ["🚫 Ingen åtkomst till andras ärenden"],
+ },
+ };
+
+ const rolePanel = document.getElementById("rolePanel");
+ const renderRole = (roleKey) => {
+ const role = roleData[roleKey];
+ if (!role || !rolePanel) return;
+ rolePanel.innerHTML = `
+
+ ${role.allow.map((item) => `${item}`).join("")}
+ ${role.deny.map((item) => `${item}`).join("")}
+
+ `;
+ };
+
+ const roleButtons = Array.from(document.querySelectorAll(".role-tabs__btn"));
+ roleButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ roleButtons.forEach((btn) => btn.classList.remove("is-active"));
+ button.classList.add("is-active");
+ renderRole(button.getAttribute("data-role"));
+ });
+ });
+ renderRole("manager");
+
+ document.querySelectorAll("[data-accordion]").forEach((accordion) => {
+ const triggers = Array.from(accordion.querySelectorAll(".accordion__trigger"));
+ triggers.forEach((trigger) => {
+ trigger.addEventListener("click", () => {
+ const expanded = trigger.getAttribute("aria-expanded") === "true";
+ triggers.forEach((btn) => {
+ btn.setAttribute("aria-expanded", "false");
+ const panel = document.getElementById(btn.getAttribute("aria-controls") || "");
+ if (panel) panel.hidden = true;
+ });
+ if (!expanded) {
+ trigger.setAttribute("aria-expanded", "true");
+ const panel = document.getElementById(trigger.getAttribute("aria-controls") || "");
+ if (panel) panel.hidden = false;
+ }
+ });
+ });
+ });
+})();
diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html
new file mode 100644
index 0000000..5a2811b
--- /dev/null
+++ b/src/main/resources/templates/landing.html
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+ 🎓 Projektpresentation
+ ⏱️ 5-7 minuter
+
+ 🏥 Vårdens ärenden i ett tryggt flöde
+ Ett samlat system för registrering, ansvar och uppföljning utan onödiga överlämningar.
+
+
+
+
+
+
+
+
🧭 Från splittrat till samlat
+
Två lägen: problem och lösning.
+
+
+
+
+
+
+
+
🧰Många verktyg
+
🧠Tappar kontext
+
🐢Långsamma överlämningar
+
+
+
+
+
🗺️Ett tydligt flöde
+
🎯Klara ansvar
+
📍Spårbara händelser
+
+
+
+
+
+ 🔄 Enkelt flöde, tydligt ansvar
+
+
+
+
+
+
+
+
+
+
+ 👥 Rätt åtkomst för rätt roll
+
+
+
+
+
+
+
+
+
+
+
+
📄 Dokument som håller
+
Mindre text, mer överblick.
+
+
+ 🧪 MIME-kontroll
+ 🔁 Automatisk retry
+ 🛡️ Säker uppföljning
+
+
+
+
Felaktiga filtyper stoppas innan lagring.
+
+
Tillfälliga lagringsfel hanteras med retry och återhämtning.
+
+
+
+
+
+
🕵️ Full spårbarhet
+
Vem, vad, när - direkt synligt.
+
+
+ 👤 Aktör
+ 📌 Händelse
+ ⏰ Tidpunkt
+
+
+
🔎 Filter
Chef ser relevanta händelser snabbt.
+
📡 Live
Uppdateringar kan följas i nära realtid.
+
📊 Kontroll
Bättre underlag för uppföljning och kvalitet.
+
+
+
+
+ 🧱 Byggt för stabil utveckling
+
+
🏗️ Tydliga lager
+
🔐 Säker standard
+
🧪 Automatiska tester
+
+
+ 🔍 Visa tekniska bevis
+ MVC-struktur, servicevalidering och testade kärnflöden ger tryggare ändringar över tid.
+
+
+
+
+ 🎬 Avslut
+ Redo att se helheten?
+ En plattform för säkrare samarbete och snabbare beslut i vården.
+
+ ⚡ Snabbare samordning
+ 🔒 Säkrare åtkomst
+ 🧾 Full spårbarhet
+
+
+
+
+
+
+
diff --git a/start-local.ps1 b/start-local.ps1
index 12d8698..aca10f0 100644
--- a/start-local.ps1
+++ b/start-local.ps1
@@ -37,7 +37,7 @@ function Invoke-CommandWithMode {
if ($LASTEXITCODE -ne 0) {
Write-Host "Command failed in quiet mode (exit code: $LASTEXITCODE)." -ForegroundColor Red
- Write-Host "Recent output from $tempLog:" -ForegroundColor DarkRed
+ Write-Host "Recent output from ${tempLog}:" -ForegroundColor DarkRed
Get-Content -Path $tempLog -Tail 25 | ForEach-Object { Write-Host $_ -ForegroundColor DarkGray }
}
}