1142 lines
47 KiB
Plaintext
1142 lines
47 KiB
Plaintext
================================================================================
|
||
FLOWCHART SISTEM KLASIFIKASI TOMAT - FORMAT TEXT/ASCII
|
||
================================================================================
|
||
|
||
Dibuat untuk: Dokumentasi Sistem Klasifikasi Tingkat Kematangan Tomat
|
||
Tanggal: 2026-05-07
|
||
Stack: Laravel 11 (PHP) + Flask (Python) + Random Forest ML
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
1. ALUR UTAMA SISTEM - QUICK VIEW
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
┌─────────────┐
|
||
│ USER │
|
||
│ /ADMIN │
|
||
└──────┬──────┘
|
||
│
|
||
│ (1) Akses Web
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 🌐 LARAVEL WEB INTERFACE │
|
||
│ Frontend (Blade + JS) │
|
||
└────────────┬────────────────┘
|
||
│
|
||
┌────────────┴────────────────┐
|
||
│ │
|
||
▼ (2) GET /tomat/upload ▼ (2) POST /admin/login
|
||
|
||
┌────────────────────────┐ ┌──────────────────┐
|
||
│ Upload Gambar Form │ │ Admin Dashboard │
|
||
│ - Select Image │ │ - Manage Admins │
|
||
│ - Preview │ │ - History │
|
||
│ - Validasi │ │ - Statistics │
|
||
└────────────┬───────────┘ └──────────────────┘
|
||
│
|
||
│ (3) POST /tomat/classify
|
||
│ + File Binary
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ ⚙️ LARAVEL BACKEND │
|
||
│ TomatController │
|
||
│ - Receive upload │
|
||
│ - Validasi file │
|
||
│ - Parse to Python │
|
||
└────────────┬─────────────────┘
|
||
│
|
||
│ (4) HTTP POST
|
||
│ Multipart FormData
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ 🐍 FLASK PYTHON BACKEND │
|
||
│ app.py - predict endpoint │
|
||
│ - Receive file │
|
||
│ - Preprocess image │
|
||
│ - Extract features │
|
||
│ - Run model │
|
||
└────────────┬─────────────────┘
|
||
│
|
||
│ (5) JSON Response
|
||
│ {class, confidence}
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ ⚙️ LARAVEL BACKEND │
|
||
│ - Parse response │
|
||
│ - Save to DB │
|
||
│ - Format result │
|
||
└────────────┬─────────────────┘
|
||
│
|
||
│ (6) JSON/View
|
||
│
|
||
┌────────────┴────────────────┐
|
||
│ │
|
||
▼ ▼
|
||
┌──────────────────┐ ┌────────────────────┐
|
||
│ Result Page │ │ AJAX Update DOM │
|
||
│ - Show Result │ │ - Display Class │
|
||
│ - Confidence % │ │ - Show Confidence │
|
||
│ - Advice │ │ - Save to History │
|
||
└──────────────────┘ └────────────────────┘
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
2. DETAILED FLOW: IMAGE UPLOAD & CLASSIFICATION
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
START: USER UPLOADS IMAGE
|
||
│
|
||
├─ Step 1: FILE INPUT VALIDATION (Browser Side)
|
||
│ ├─ User selects file
|
||
│ ├─ Check: .png, .jpg, .jpeg, .gif?
|
||
│ │ └─ NO → Show error "Invalid format"
|
||
│ │ └─ YES → Continue
|
||
│ ├─ Check: File size ≤ 16MB?
|
||
│ │ └─ NO → Show error "File too large"
|
||
│ │ └─ YES → Continue
|
||
│ └─ Preview image on page
|
||
│
|
||
├─ Step 2: SEND TO LARAVEL (JavaScript AJAX)
|
||
│ ├─ FormData construction:
|
||
│ │ ├─ file: binary data
|
||
│ │ └─ csrf_token: security
|
||
│ ├─ POST /tomat/classify
|
||
│ └─ Show loading spinner
|
||
│
|
||
├─ Step 3: LARAVEL BACKEND PROCESSING
|
||
│ ├─ TomatController@classify
|
||
│ ├─ Receive multipart form data
|
||
│ ├─ File validation:
|
||
│ │ ├─ Check MIME type
|
||
│ │ ├─ Check file size (server-side)
|
||
│ │ └─ Check file integrity
|
||
│ ├─ Temporary save:
|
||
│ │ └─ storage/app/uploads/temp_[timestamp].jpg
|
||
│ ├─ Prepare request body:
|
||
│ │ ├─ Read file bytes
|
||
│ │ ├─ Encode to multipart
|
||
│ │ └─ Set headers
|
||
│ └─ Send to Python backend
|
||
│
|
||
├─ Step 4: PYTHON FLASK BACKEND
|
||
│ ├─ app.py route /api/predict (POST)
|
||
│ ├─ Parse multipart request:
|
||
│ │ ├─ Extract file object from request.files
|
||
│ │ ├─ Check file existence
|
||
│ │ └─ Check allowed extension
|
||
│ ├─ Load model (if not in memory):
|
||
│ │ ├─ joblib.load("model_tomat.pkl")
|
||
│ │ ├─ Check file exists → NO? Error 500
|
||
│ │ └─ Cache model in global variable
|
||
│ └─ Call preprocessing function
|
||
│
|
||
├─ Step 5: IMAGE PREPROCESSING
|
||
│ ├─ File stream to OpenCV:
|
||
│ │ ├─ Read bytes from file stream
|
||
│ │ ├─ cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
|
||
│ │ ├─ Output: BGR array
|
||
│ │ └─ Check decoded? → NO? Error 500
|
||
│ ├─ Image dimension check:
|
||
│ │ ├─ Get height, width = image.shape[:2]
|
||
│ │ ├─ If h×w ≠ 256×256:
|
||
│ │ │ └─ cv2.resize(image, (256, 256))
|
||
│ │ └─ Else: Use as-is
|
||
│ └─ Output: Standardized 256×256×3 BGR image
|
||
│
|
||
├─ Step 6: FEATURE EXTRACTION (Color Histogram HSV)
|
||
│ ├─ Color space conversion:
|
||
│ │ ├─ cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
||
│ │ └─ Output: HSV image
|
||
│ ├─ Histogram extraction:
|
||
│ │ ├─ For channel H (Hue):
|
||
│ │ │ ├─ cv2.calcHist([hsv], [0], None, [8], [0, 256])
|
||
│ │ │ ├─ cv2.normalize(hist)
|
||
│ │ │ └─ Result: array of 8 values
|
||
│ │ ├─ For channel S (Saturation):
|
||
│ │ │ ├─ cv2.calcHist([hsv], [1], None, [8], [0, 256])
|
||
│ │ │ ├─ cv2.normalize(hist)
|
||
│ │ │ └─ Result: array of 8 values
|
||
│ │ └─ For channel V (Value):
|
||
│ │ ├─ cv2.calcHist([hsv], [2], None, [8], [0, 256])
|
||
│ │ ├─ cv2.normalize(hist)
|
||
│ │ └─ Result: array of 8 values
|
||
│ ├─ Concatenate all histograms:
|
||
│ │ └─ [8] + [8] + [8] = [24 bins] × 3 = 192 features
|
||
│ └─ Feature vector ready: shape (1, 192)
|
||
│
|
||
├─ Step 7: MODEL PREDICTION
|
||
│ ├─ Load cached Random Forest model
|
||
│ ├─ Run prediction:
|
||
│ │ ├─ model.predict([features])
|
||
│ │ │ └─ Returns: class index (0, 1, or 2)
|
||
│ │ └─ model.predict_proba([features])
|
||
│ │ └─ Returns: [[prob_0, prob_1, prob_2]]
|
||
│ ├─ Decode class index:
|
||
│ │ ├─ label_encoder.inverse_transform([predicted_index])
|
||
│ │ ├─ 0 → "mentah"
|
||
│ │ ├─ 1 → "matang"
|
||
│ │ └─ 2 → "setengah_matang"
|
||
│ ├─ Extract confidence:
|
||
│ │ ├─ max(probabilities) × 100
|
||
│ │ └─ Example: 0.8734 → 87.34%
|
||
│ └─ Prepare results
|
||
│
|
||
├─ Step 8: PACKAGE & RETURN RESPONSE
|
||
│ ├─ Format JSON response:
|
||
│ │ {
|
||
│ │ "status": "success",
|
||
│ │ "class": "matang",
|
||
│ │ "confidence": 87.34,
|
||
│ │ "probabilities": {
|
||
│ │ "matang": 87.34,
|
||
│ │ "mentah": 5.12,
|
||
│ │ "setengah_matang": 7.54
|
||
│ │ },
|
||
│ │ "timestamp": "2026-05-07T10:30:45Z"
|
||
│ │ }
|
||
│ ├─ Set HTTP status: 200 OK
|
||
│ └─ Return to Laravel
|
||
│
|
||
├─ Step 9: LARAVEL SAVE TO DATABASE
|
||
│ ├─ Receive JSON response from Python
|
||
│ ├─ Parse JSON payload
|
||
│ ├─ Save prediction record:
|
||
│ │ {
|
||
│ │ "user_id": (admin_user_id or guest),
|
||
│ │ "image_filename": "upload_[timestamp].jpg",
|
||
│ │ "predicted_class": "matang",
|
||
│ │ "confidence_score": 0.8734,
|
||
│ │ "created_at": now(),
|
||
│ │ "updated_at": now()
|
||
│ │ }
|
||
│ ├─ Database insert:
|
||
│ │ └─ INSERT INTO predictions (...)
|
||
│ ├─ Get inserted ID
|
||
│ └─ Prepare response
|
||
│
|
||
├─ Step 10: RETURN TO FRONTEND
|
||
│ ├─ AJAX response received
|
||
│ ├─ JavaScript update DOM:
|
||
│ │ ├─ Hide loading spinner
|
||
│ │ ├─ Show result card:
|
||
│ │ │ ├─ Display image thumbnail
|
||
│ │ │ ├─ Show class: "MATANG ✓"
|
||
│ │ │ ├─ Show confidence: "87.34%"
|
||
│ │ │ ├─ Show all probabilities
|
||
│ │ │ └─ Show timestamp
|
||
│ │ ├─ Add to history list
|
||
│ │ └─ Enable action buttons:
|
||
│ │ ├─ Save to folder
|
||
│ │ ├─ Download report
|
||
│ │ └─ View history
|
||
│ └─ Display complete
|
||
│
|
||
└─ END: USER SEES RESULT
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
3. ADMIN AUTHENTICATION FLOW
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
START: ADMIN LOGIN PAGE
|
||
│
|
||
├─ Step 1: DISPLAY LOGIN FORM
|
||
│ ├─ GET /admin/login
|
||
│ ├─ Show form:
|
||
│ │ ├─ Email/Username field
|
||
│ │ ├─ Password field
|
||
│ │ └─ Login button
|
||
│ └─ Display any error messages
|
||
│
|
||
├─ Step 2: SUBMIT CREDENTIALS
|
||
│ ├─ User enters:
|
||
│ │ ├─ Email/Username
|
||
│ │ └─ Password
|
||
│ ├─ POST /admin/login
|
||
│ └─ Send via HTTPS
|
||
│
|
||
├─ Step 3: LARAVEL BACKEND VERIFICATION
|
||
│ ├─ UploadController@adminLogin
|
||
│ ├─ Validate input:
|
||
│ │ ├─ Email required? ✓
|
||
│ │ ├─ Password required? ✓
|
||
│ │ └─ Format valid? ✓
|
||
│ ├─ Query database:
|
||
│ │ ├─ SELECT * FROM users WHERE email = ?
|
||
│ │ └─ User found?
|
||
│ │ ├─ NO → Return error "Invalid credentials"
|
||
│ │ └─ YES → Continue
|
||
│ ├─ Verify password:
|
||
│ │ ├─ Hash verification: Hash::check(input_pass, db_pass)
|
||
│ │ ├─ Match?
|
||
│ │ │ ├─ NO → Return error "Invalid credentials"
|
||
│ │ │ └─ YES → Continue
|
||
│ ├─ Check user status:
|
||
│ │ ├─ User.status = "active"?
|
||
│ │ │ ├─ NO → Return error "Account inactive"
|
||
│ │ │ └─ YES → Continue
|
||
│ ├─ Check admin flag:
|
||
│ │ ├─ User.is_admin = true?
|
||
│ │ │ ├─ NO → Return error "Not admin account"
|
||
│ │ │ └─ YES → Continue
|
||
│ └─ Create session
|
||
│
|
||
├─ Step 4: CREATE SESSION
|
||
│ ├─ Laravel Session Handler:
|
||
│ │ ├─ session()->put([
|
||
│ │ │ 'admin_logged_in' => true,
|
||
│ │ │ 'admin_user_id' => user.id,
|
||
│ │ │ 'admin_name' => user.name,
|
||
│ │ │ 'admin_email' => user.email
|
||
│ │ │ ])
|
||
│ │ └─ Session saved to storage
|
||
│ ├─ Set secure cookie:
|
||
│ │ ├─ HttpOnly: true
|
||
│ │ ├─ Secure: true (HTTPS)
|
||
│ │ └─ SameSite: Strict
|
||
│ └─ Session created
|
||
│
|
||
├─ Step 5: REDIRECT TO DASHBOARD
|
||
│ ├─ HTTP redirect (302)
|
||
│ ├─ Location: /admin/dashboard
|
||
│ └─ Browser follows redirect
|
||
│
|
||
├─ Step 6: DISPLAY ADMIN DASHBOARD
|
||
│ ├─ GET /admin/dashboard
|
||
│ ├─ Check session:
|
||
│ │ ├─ admin_logged_in = true? ✓
|
||
│ │ └─ Allowed access
|
||
│ ├─ Render dashboard with:
|
||
│ │ ├─ Welcome message
|
||
│ │ ├─ Statistics cards
|
||
│ │ ├─ Navigation menu
|
||
│ │ └─ Content sections
|
||
│ └─ Display complete
|
||
│
|
||
├─ LOGOUT FLOW:
|
||
│ ├─ GET /admin/logout
|
||
│ ├─ Clear session:
|
||
│ │ └─ session()->forget(['admin_logged_in', 'admin_user_id', ...])
|
||
│ ├─ Delete cookies
|
||
│ ├─ Redirect to /admin/login
|
||
│ └─ Show "Logged out" message
|
||
│
|
||
└─ END
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
4. ADMIN DASHBOARD FEATURES
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
ADMIN DASHBOARD
|
||
│
|
||
├─ Dashboard Overview
|
||
│ ├─ Statistics Cards:
|
||
│ │ ├─ Total Predictions (Today)
|
||
│ │ ├─ Total Predictions (All time)
|
||
│ │ ├─ Accuracy Rate
|
||
│ │ ├─ Active Admins
|
||
│ │ └─ Storage Used
|
||
│ ├─ Charts:
|
||
│ │ ├─ Prediction trend (last 7 days)
|
||
│ │ ├─ Class distribution pie chart
|
||
│ │ └─ Confidence distribution histogram
|
||
│ └─ Quick actions buttons
|
||
│
|
||
├─ Classification History
|
||
│ ├─ Display table:
|
||
│ │ ├─ Columns:
|
||
│ │ │ ├─ ID
|
||
│ │ │ ├─ Date/Time
|
||
│ │ │ ├─ Image filename
|
||
│ │ │ ├─ Predicted class
|
||
│ │ │ ├─ Confidence %
|
||
│ │ │ └─ User/Admin
|
||
│ │ ├─ Pagination (50 per page)
|
||
│ │ ├─ Sort options
|
||
│ │ └─ Export to CSV
|
||
│ ├─ Filter:
|
||
│ │ ├─ By date range
|
||
│ │ ├─ By class
|
||
│ │ ├─ By confidence range
|
||
│ │ └─ By user
|
||
│ └─ Search function
|
||
│
|
||
├─ System Statistics
|
||
│ ├─ Charts & Graphs:
|
||
│ │ ├─ Class distribution (bar chart)
|
||
│ │ ├─ Confidence score distribution
|
||
│ │ ├─ Predictions over time (line chart)
|
||
│ │ ├─ Accuracy metrics
|
||
│ │ └─ Performance graph
|
||
│ ├─ Export reports:
|
||
│ │ ├─ PDF report
|
||
│ │ ├─ Excel export
|
||
│ │ └─ CSV export
|
||
│ └─ Comparison view
|
||
│
|
||
├─ Manage Admin Users
|
||
│ ├─ Admin list table:
|
||
│ │ ├─ Columns:
|
||
│ │ │ ├─ Name
|
||
│ │ │ ├─ Email
|
||
│ │ │ ├─ Status (active/inactive)
|
||
│ │ │ ├─ Last login
|
||
│ │ │ └─ Actions (Edit/Delete)
|
||
│ │ └─ Pagination
|
||
│ ├─ Add new admin:
|
||
│ │ ├─ Form with:
|
||
│ │ │ ├─ Name input
|
||
│ │ │ ├─ Email input
|
||
│ │ │ ├─ Password field
|
||
│ │ │ ├─ Role selection
|
||
│ │ │ └─ Status toggle
|
||
│ │ └─ Validation & save
|
||
│ ├─ Edit admin:
|
||
│ │ ├─ Form with current data
|
||
│ │ ├─ Update allowed fields
|
||
│ │ └─ Password optional
|
||
│ ├─ Delete admin:
|
||
│ │ ├─ Confirmation dialog
|
||
│ │ ├─ Cascade options
|
||
│ │ └─ Soft/Hard delete
|
||
│ └─ Toggle status (active/inactive)
|
||
│
|
||
├─ Model Information
|
||
│ ├─ Display model details:
|
||
│ │ ├─ Model type: Random Forest Classifier
|
||
│ │ ├─ Number of trees: X
|
||
│ │ ├─ Features count: 192
|
||
│ │ ├─ Classes: [matang, mentah, setengah_matang]
|
||
│ │ ├─ Training samples: X
|
||
│ │ ├─ Model accuracy: XX.XX%
|
||
│ │ ├─ Last updated: YYYY-MM-DD HH:MM:SS
|
||
│ │ └─ Model file size
|
||
│ ├─ Feature explanation:
|
||
│ │ └─ HSV Color Histogram (8×8×8 bins)
|
||
│ ├─ Model performance:
|
||
│ │ ├─ Precision per class
|
||
│ │ ├─ Recall per class
|
||
│ │ ├─ F1-score per class
|
||
│ │ └─ Confusion matrix
|
||
│ └─ Retrain model button
|
||
│
|
||
├─ System Status
|
||
│ ├─ Service health:
|
||
│ │ ├─ Flask API: Online/Offline
|
||
│ │ ├─ Database: Connected/Disconnected
|
||
│ │ ├─ Storage: Available/Full
|
||
│ │ └─ Cache: Working/Error
|
||
│ ├─ Resource usage:
|
||
│ │ ├─ CPU usage
|
||
│ │ ├─ Memory usage
|
||
│ │ ├─ Disk space
|
||
│ │ └─ Active processes
|
||
│ ├─ Recent logs:
|
||
│ │ ├─ System errors
|
||
│ │ ├─ API errors
|
||
│ │ └─ Database errors
|
||
│ └─ Maintenance actions
|
||
│
|
||
├─ Settings
|
||
│ ├─ General settings:
|
||
│ │ ├─ App name
|
||
│ │ ├─ App description
|
||
│ │ └─ Contact email
|
||
│ ├─ Model settings:
|
||
│ │ ├─ Model path
|
||
│ │ ├─ Confidence threshold
|
||
│ │ └─ Max upload size
|
||
│ ├─ Storage settings:
|
||
│ │ ├─ Dataset folder paths
|
||
│ │ ├─ Upload folder path
|
||
│ │ └─ Cleanup old files
|
||
│ └─ Notification settings
|
||
│
|
||
└─ Logout
|
||
└─ Clear session & redirect
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
5. MODEL TRAINING FLOW (background process)
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
START: python create_model.py
|
||
│
|
||
├─ Phase 1: DATA LOADING
|
||
│ ├─ Scan dataset folders:
|
||
│ │ ├─ matang/
|
||
│ │ ├─ mentah/
|
||
│ │ └─ setengah_matang/
|
||
│ ├─ Count images:
|
||
│ │ ├─ matang: N1 images
|
||
│ │ ├─ mentah: N2 images
|
||
│ │ └─ setengah_matang: N3 images
|
||
│ └─ Print statistics
|
||
│
|
||
├─ Phase 2: FEATURE EXTRACTION
|
||
│ ├─ For each image in dataset:
|
||
│ │ ├─ Read image with cv2.imread()
|
||
│ │ ├─ Resize to 256×256 (if needed)
|
||
│ │ ├─ Convert BGR → HSV
|
||
│ │ ├─ Extract 192-dim feature vector:
|
||
│ │ │ └─ 8×8×8 histogram (Hue, Saturation, Value)
|
||
│ │ ├─ Store feature + label
|
||
│ │ └─ Progress indicator
|
||
│ ├─ Combine all features:
|
||
│ │ └─ X shape: (N_total, 192)
|
||
│ │ └─ y shape: (N_total,) with labels
|
||
│ └─ Feature matrix ready
|
||
│
|
||
├─ Phase 3: DATA SPLITTING
|
||
│ ├─ Train-test split:
|
||
│ │ ├─ train_size: 0.8 (80%)
|
||
│ │ ├─ test_size: 0.2 (20%)
|
||
│ │ ├─ random_state: fixed (reproducible)
|
||
│ │ └─ stratify: by class (balanced split)
|
||
│ ├─ Output:
|
||
│ │ ├─ X_train, X_test
|
||
│ │ ├─ y_train, y_test
|
||
│ │ └─ Split info printed
|
||
│ └─ Ready for training
|
||
│
|
||
├─ Phase 4: MODEL TRAINING
|
||
│ ├─ Initialize Random Forest:
|
||
│ │ ├─ n_estimators: 100
|
||
│ │ ├─ max_depth: None
|
||
│ │ ├─ random_state: 42
|
||
│ │ ├─ n_jobs: -1 (parallel)
|
||
│ │ └─ class_weight: 'balanced'
|
||
│ ├─ Fit model:
|
||
│ │ ├─ clf.fit(X_train, y_train)
|
||
│ │ ├─ Training time: X seconds
|
||
│ │ └─ Print status
|
||
│ └─ Model trained
|
||
│
|
||
├─ Phase 5: MODEL EVALUATION
|
||
│ ├─ Make predictions:
|
||
│ │ ├─ y_pred = clf.predict(X_test)
|
||
│ │ └─ y_pred_proba = clf.predict_proba(X_test)
|
||
│ ├─ Calculate metrics:
|
||
│ │ ├─ Overall accuracy: XX.XX%
|
||
│ │ ├─ Per-class precision
|
||
│ │ ├─ Per-class recall
|
||
│ │ ├─ Per-class F1-score
|
||
│ │ ├─ Confusion matrix
|
||
│ │ └─ Classification report
|
||
│ ├─ Print results:
|
||
│ │ ├─ Accuracy: XX.XX%
|
||
│ │ ├─ Precision: XX.XX%
|
||
│ │ ├─ Recall: XX.XX%
|
||
│ │ └─ F1-Score: XX.XX%
|
||
│ └─ Generate confusion matrix plot
|
||
│
|
||
├─ Phase 6: LABEL ENCODING
|
||
│ ├─ Encode class labels:
|
||
│ │ ├─ LabelEncoder().fit(y)
|
||
│ │ ├─ Mapping:
|
||
│ │ │ ├─ "matang" → 1
|
||
│ │ │ ├─ "mentah" → 0
|
||
│ │ │ └─ "setengah_matang" → 2
|
||
│ │ └─ Save encoder for later use
|
||
│ └─ Encoder ready
|
||
│
|
||
├─ Phase 7: SAVE MODEL ARTIFACTS
|
||
│ ├─ Save trained model:
|
||
│ │ ├─ joblib.dump(clf, "model_tomat.pkl")
|
||
│ │ └─ File size: ~X MB
|
||
│ ├─ Save label encoder:
|
||
│ │ ├─ joblib.dump(le, "model_tomat_encoder.pkl")
|
||
│ │ └─ File size: ~X KB
|
||
│ ├─ Save metadata:
|
||
│ │ ├─ joblib.dump(metadata, "model_tomat_metadata.pkl")
|
||
│ │ ├─ Contains:
|
||
│ │ │ ├─ Model type
|
||
│ │ │ ├─ Classes list
|
||
│ │ │ ├─ Features count
|
||
│ │ │ └─ Training date
|
||
│ │ └─ File size: ~X KB
|
||
│ ├─ Print success messages
|
||
│ └─ Model artifacts ready
|
||
│
|
||
├─ Phase 8: GENERATE VISUALIZATIONS
|
||
│ ├─ Create confusion matrix plot:
|
||
│ │ ├─ Heatmap with annotations
|
||
│ │ ├─ Class names on axes
|
||
│ │ ├─ Color scale
|
||
│ │ └─ Save as confusion_matrix.png
|
||
│ ├─ Create feature importance plot:
|
||
│ │ ├─ Top 20 important features
|
||
│ │ ├─ Bar chart
|
||
│ │ └─ Save as feature_importance.png
|
||
│ └─ Create accuracy plot
|
||
│
|
||
└─ END: MODEL READY FOR PRODUCTION
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
6. DATABASE SCHEMA
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
TABLE: users
|
||
├─ id (INT, PRIMARY KEY, AUTO_INCREMENT)
|
||
├─ email (VARCHAR 255, UNIQUE)
|
||
├─ username (VARCHAR 255)
|
||
├─ password (VARCHAR 255) - hashed with bcrypt
|
||
├─ name (VARCHAR 255)
|
||
├─ role (ENUM: admin, user)
|
||
├─ status (ENUM: active, inactive) - default: active
|
||
├─ last_login (TIMESTAMP, nullable)
|
||
├─ remember_token (VARCHAR 100, nullable)
|
||
├─ created_at (TIMESTAMP)
|
||
└─ updated_at (TIMESTAMP)
|
||
|
||
TABLE: predictions
|
||
├─ id (INT, PRIMARY KEY, AUTO_INCREMENT)
|
||
├─ user_id (INT, FOREIGN KEY → users.id, nullable)
|
||
├─ image_filename (VARCHAR 255)
|
||
├─ image_path (VARCHAR 255)
|
||
├─ predicted_class (ENUM: matang, mentah, setengah_matang)
|
||
├─ confidence_score (DECIMAL 5,4) - range 0.0000 to 1.0000
|
||
├─ probabilities (JSON) - {matang: 0.87, mentah: 0.05, setengah_matang: 0.08}
|
||
├─ model_version (VARCHAR 50)
|
||
├─ notes (TEXT, nullable)
|
||
├─ created_at (TIMESTAMP)
|
||
└─ updated_at (TIMESTAMP)
|
||
|
||
TABLE: uploads (legacy)
|
||
├─ id (INT, PRIMARY KEY, AUTO_INCREMENT)
|
||
├─ user_id (INT, FOREIGN KEY → users.id, nullable)
|
||
├─ image_file (VARCHAR 255)
|
||
├─ classification_result (VARCHAR 255)
|
||
├─ confidence (DECIMAL 5,4, nullable)
|
||
├─ created_at (TIMESTAMP)
|
||
└─ updated_at (TIMESTAMP)
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
7. FOLDER STRUCTURE & FILE ORGANIZATION
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
PROJECT ROOT (klasifikasi-tomat/)
|
||
│
|
||
├─ 📁 app/
|
||
│ ├─ Http/
|
||
│ │ └─ Controllers/
|
||
│ │ ├─ TomatController.php (upload & classify logic)
|
||
│ │ ├─ AdminController.php (manage admins)
|
||
│ │ ├─ AdminDashboardController.php (dashboard)
|
||
│ │ ├─ ClassificationHistoryController.php (history)
|
||
│ │ ├─ StatistikController.php (statistics)
|
||
│ │ └─ UploadController.php (legacy)
|
||
│ ├─ Models/
|
||
│ │ ├─ User.php
|
||
│ │ ├─ Predictions.php
|
||
│ │ ├─ Upload.php
|
||
│ │ └─ ...
|
||
│ ├─ Mail/ (email notifications)
|
||
│ └─ Providers/
|
||
│
|
||
├─ 📁 routes/
|
||
│ ├─ web.php (web routes)
|
||
│ ├─ api.php (API routes - if separated)
|
||
│ └─ console.php
|
||
│
|
||
├─ 📁 resources/
|
||
│ ├─ css/ (stylesheets)
|
||
│ ├─ js/ (JavaScript)
|
||
│ └─ views/
|
||
│ ├─ layouts/
|
||
│ │ ├─ app.blade.php (main layout)
|
||
│ │ └─ guest.blade.php (guest layout)
|
||
│ ├─ Admin/
|
||
│ │ ├─ dashboard.blade.php
|
||
│ │ ├─ manage-admin.blade.php
|
||
│ │ ├─ history.blade.php
|
||
│ │ ├─ statistics.blade.php
|
||
│ │ └─ login.blade.php
|
||
│ ├─ upload.blade.php (upload form)
|
||
│ ├─ result.blade.php (classification result)
|
||
│ ├─ login.blade.php
|
||
│ ├─ welcome.blade.php
|
||
│ └─ landing_page/
|
||
│
|
||
├─ 📁 database/
|
||
│ ├─ migrations/
|
||
│ │ ├─ create_users_table.php
|
||
│ │ ├─ create_predictions_table.php
|
||
│ │ └─ ...
|
||
│ ├─ seeders/
|
||
│ └─ factories/
|
||
│
|
||
├─ 📁 storage/
|
||
│ ├─ app/
|
||
│ │ └─ uploads/ (uploaded images stored here)
|
||
│ ├─ framework/
|
||
│ ├─ logs/
|
||
│ └─ cache/
|
||
│
|
||
├─ 📁 public/
|
||
│ ├─ index.php (Laravel entry point)
|
||
│ ├─ storage -> ../storage/app/public
|
||
│ ├─ assets/ (images, icons)
|
||
│ └─ build/ (compiled assets)
|
||
│
|
||
├─ 📁 dataset (Training data)
|
||
│ ├─ matang/ (ripe tomatoes - ~N images)
|
||
│ ├─ mentah/ (unripe tomatoes - ~N images)
|
||
│ └─ setengah_matang/ (semi-ripe - ~N images)
|
||
│
|
||
├─ 🐍 Python ML Files
|
||
│ ├─ app.py (Flask API server)
|
||
│ ├─ main.py (training pipeline)
|
||
│ ├─ model_manager.py (model loading/saving)
|
||
│ ├─ create_model.py (data loading & training)
|
||
│ ├─ predict_tomat.py (standalone predictor)
|
||
│ ├─ model_tomat.pkl (trained Random Forest model)
|
||
│ ├─ model_tomat_encoder.pkl (label encoder)
|
||
│ ├─ model_tomat_metadata.pkl (model metadata)
|
||
│ ├─ requirements.txt (Python dependencies)
|
||
│ └─ [confusion_matrix.png, feature_importance.png]
|
||
│
|
||
├─ 📁 config/
|
||
│ ├─ app.php
|
||
│ ├─ database.php
|
||
│ ├─ mail.php
|
||
│ ├─ filesystems.php
|
||
│ ├─ auth.php
|
||
│ └─ ...
|
||
│
|
||
├─ 📁 bootstrap/
|
||
│ ├─ app.php
|
||
│ ├─ providers.php
|
||
│ └─ cache/
|
||
│
|
||
├─ 📁 vendor/ (composer dependencies)
|
||
├─ 📁 node_modules/ (npm dependencies)
|
||
│
|
||
├─ .env (environment variables)
|
||
├─ .env.example
|
||
├─ .gitignore
|
||
├─ composer.json
|
||
├─ package.json
|
||
├─ vite.config.js
|
||
├─ phpunit.xml
|
||
├─ artisan (Laravel CLI)
|
||
│
|
||
├─ 📄 README.md (project documentation)
|
||
├─ 📄 API_README.md
|
||
├─ 📄 FLOWCHART_SISTEM_DETAIL.md (this file)
|
||
└─ 📄 Other documentation files
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
8. REQUEST-RESPONSE FLOW DETAIL
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
┌─ REQUEST SEQUENCE ─────────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ (1) Browser │
|
||
│ POST /tomat/classify │
|
||
│ Content-Type: multipart/form-data │
|
||
│ Body: │
|
||
│ ├─ file: <binary image data> │
|
||
│ └─ _token: <CSRF token> │
|
||
│ ↓ │
|
||
│ (2) Laravel Route Handler (routes/web.php) │
|
||
│ Route::post('/tomat/classify', [TomatController::class, 'classify']) │
|
||
│ ↓ │
|
||
│ (3) TomatController@classify │
|
||
│ ├─ Receive file from request │
|
||
│ ├─ Validate file │
|
||
│ ├─ Save temporarily │
|
||
│ ├─ Prepare multipart request │
|
||
│ ↓ │
|
||
│ (4) HTTP Client sends to Flask │
|
||
│ POST http://localhost:5000/api/predict │
|
||
│ Content-Type: multipart/form-data │
|
||
│ Body: │
|
||
│ └─ file: <binary image data> │
|
||
│ ↓ │
|
||
│ (5) Flask App (app.py) │
|
||
│ @app.route('/api/predict', methods=['POST']) │
|
||
│ def predict(): │
|
||
│ ├─ Parse request.files['file'] │
|
||
│ ├─ Validate file │
|
||
│ ├─ Preprocess image │
|
||
│ ├─ Extract features │
|
||
│ ├─ Load model │
|
||
│ ├─ Run prediction │
|
||
│ └─ Return JSON response │
|
||
│ ↓ │
|
||
│ (6) Response sent back to Laravel │
|
||
│ { │
|
||
│ "status": "success", │
|
||
│ "class": "matang", │
|
||
│ "confidence": 0.8734, │
|
||
│ "probabilities": {...}, │
|
||
│ "timestamp": "2026-05-07T10:30:45Z" │
|
||
│ } │
|
||
│ ↓ │
|
||
│ (7) TomatController processes response │
|
||
│ ├─ Parse JSON │
|
||
│ ├─ Save to database │
|
||
│ ├─ Format result for frontend │
|
||
│ └─ Return view/JSON to browser │
|
||
│ ↓ │
|
||
│ (8) Browser receives response │
|
||
│ ├─ JavaScript processes JSON │
|
||
│ ├─ Update DOM │
|
||
│ ├─ Display result card │
|
||
│ └─ Show classification result to user │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
|
||
┌─ RESPONSE STRUCTURE ──────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ HTTP 200 OK │
|
||
│ Content-Type: application/json │
|
||
│ │
|
||
│ { │
|
||
│ "status": "success", │
|
||
│ "class": "matang", ← Predicted class │
|
||
│ "class_id": 1, ← Class numeric ID │
|
||
│ "confidence": 0.8734, ← Float 0-1 │
|
||
│ "confidence_percent": "87.34%", ← String format for display │
|
||
│ "probabilities": { │
|
||
│ "matang": 0.8734, │
|
||
│ "mentah": 0.0512, │
|
||
│ "setengah_matang": 0.0754 │
|
||
│ }, │
|
||
│ "timestamp": "2026-05-07T10:30:45Z", │
|
||
│ "processing_time_ms": 542, │
|
||
│ "message": "Tomato successfully classified" │
|
||
│ } │
|
||
│ │
|
||
│ OR on ERROR: │
|
||
│ │
|
||
│ HTTP 400/500 │
|
||
│ { │
|
||
│ "status": "error", │
|
||
│ "error_code": "INVALID_FILE", │
|
||
│ "message": "Uploaded file is not a valid image" │
|
||
│ } │
|
||
│ │
|
||
└────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
9. ERROR HANDLING & VALIDATION
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
VALIDATION LAYERS:
|
||
│
|
||
├─ Browser-side (Client-side)
|
||
│ ├─ File type check (extension)
|
||
│ ├─ File size check (16MB max)
|
||
│ └─ Immediate user feedback
|
||
│
|
||
├─ Laravel-side (Server-side)
|
||
│ ├─ File validation:
|
||
│ │ ├─ MIME type check
|
||
│ │ ├─ File size double-check
|
||
│ │ └─ File integrity
|
||
│ ├─ Input validation:
|
||
│ │ └─ CSRF token check
|
||
│ ├─ Rate limiting
|
||
│ └─ Error logging
|
||
│
|
||
└─ Flask-side (ML Backend)
|
||
├─ File existence check
|
||
├─ Image decode validation
|
||
├─ Feature extraction validation
|
||
├─ Model loading check
|
||
└─ Prediction success check
|
||
|
||
|
||
ERROR CODES:
|
||
│
|
||
├─ 400 Bad Request
|
||
│ ├─ NO_FILE_PROVIDED
|
||
│ ├─ INVALID_FILE_TYPE
|
||
│ ├─ FILE_TOO_LARGE
|
||
│ ├─ CORRUPTED_IMAGE
|
||
│ └─ INVALID_MIME_TYPE
|
||
│
|
||
├─ 401 Unauthorized
|
||
│ ├─ NOT_AUTHENTICATED
|
||
│ └─ SESSION_EXPIRED
|
||
│
|
||
├─ 403 Forbidden
|
||
│ ├─ NOT_ADMIN
|
||
│ └─ INSUFFICIENT_PERMISSIONS
|
||
│
|
||
├─ 404 Not Found
|
||
│ ├─ ROUTE_NOT_FOUND
|
||
│ └─ RESOURCE_NOT_FOUND
|
||
│
|
||
├─ 422 Unprocessable Entity
|
||
│ ├─ VALIDATION_FAILED
|
||
│ └─ INVALID_INPUT_DATA
|
||
│
|
||
├─ 500 Internal Server Error
|
||
│ ├─ MODEL_NOT_FOUND
|
||
│ ├─ FEATURE_EXTRACTION_FAILED
|
||
│ ├─ PREDICTION_FAILED
|
||
│ ├─ DATABASE_ERROR
|
||
│ └─ SERVICE_UNAVAILABLE
|
||
│
|
||
└─ 503 Service Unavailable
|
||
├─ FLASK_SERVICE_DOWN
|
||
└─ DATABASE_CONNECTION_FAILED
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
10. TECHNOLOGY STACK
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
FRONTEND
|
||
├─ HTML5
|
||
├─ CSS3 (+ Tailwind/Bootstrap)
|
||
├─ JavaScript (Vanilla / Alpine.js)
|
||
├─ Blade Templating Engine (Laravel)
|
||
└─ AJAX (XMLHttpRequest / Fetch API)
|
||
|
||
BACKEND - WEB
|
||
├─ PHP 8.x
|
||
├─ Laravel 11
|
||
│ ├─ Routing
|
||
│ ├─ Controllers
|
||
│ ├─ Eloquent ORM
|
||
│ ├─ Migrations
|
||
│ ├─ Middleware
|
||
│ └─ Session Management
|
||
├─ Composer (dependency manager)
|
||
└─ Apache/Nginx (web server)
|
||
|
||
BACKEND - ML
|
||
├─ Python 3.8+
|
||
├─ Flask (lightweight web framework)
|
||
├─ scikit-learn (ML library)
|
||
│ └─ RandomForestClassifier
|
||
├─ OpenCV (cv2) (image processing)
|
||
├─ NumPy (numerical computing)
|
||
├─ joblib (model serialization)
|
||
└─ Gunicorn/uWSGI (WSGI server)
|
||
|
||
DATABASE
|
||
├─ MySQL / SQLite / PostgreSQL
|
||
├─ Migrations (schema versioning)
|
||
└─ Eloquent ORM (query builder)
|
||
|
||
DEVELOPMENT TOOLS
|
||
├─ Composer (PHP package manager)
|
||
├─ npm (Node package manager)
|
||
├─ Git (version control)
|
||
├─ Vite (frontend build tool)
|
||
└─ PHPUnit (testing framework)
|
||
|
||
DEPLOYMENT
|
||
├─ Apache or Nginx
|
||
├─ PHP-FPM
|
||
├─ Python runtime
|
||
├─ Gunicorn/uWSGI
|
||
├─ Supervisor (process management)
|
||
└─ Docker (optional containerization)
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
11. FILE PROCESSING & STORAGE FLOW
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
IMAGE UPLOAD & STORAGE:
|
||
|
||
User Upload Image
|
||
↓
|
||
Frontend Validation (type, size)
|
||
↓
|
||
Send via AJAX to /tomat/classify
|
||
↓
|
||
Laravel Backend:
|
||
├─ Receive multipart form data
|
||
├─ Validate MIME type
|
||
├─ Check file size
|
||
├─ Generate unique filename:
|
||
│ └─ "upload_" + timestamp + "_" + random_hash + ".jpg"
|
||
├─ Store temporarily:
|
||
│ └─ storage/app/uploads/temp_*.jpg
|
||
└─ Prepare for Python request
|
||
↓
|
||
Send to Flask Backend
|
||
↓
|
||
Python Backend:
|
||
├─ Receive file binary
|
||
├─ Decode image (cv2.imdecode)
|
||
├─ Validate image integrity
|
||
└─ Process & extract features
|
||
↓
|
||
Get Prediction Result
|
||
↓
|
||
Laravel Backend:
|
||
├─ Save prediction record to DB
|
||
├─ Move image to permanent storage:
|
||
│ └─ storage/app/uploads/
|
||
├─ Optionally copy to class folder:
|
||
│ └─ matang/ OR mentah/ OR setengah_matang/
|
||
└─ Return result to frontend
|
||
↓
|
||
Frontend Display Result
|
||
↓
|
||
User sees:
|
||
├─ Image thumbnail
|
||
├─ Predicted class
|
||
├─ Confidence percentage
|
||
├─ All probabilities
|
||
└─ Option to save/download
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
12. PERFORMANCE METRICS & OPTIMIZATION
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
TYPICAL RESPONSE TIMES:
|
||
|
||
Image Upload & Transfer: 200-500ms
|
||
├─ Network latency
|
||
├─ File upload
|
||
└─ Laravel file handling
|
||
|
||
Python Backend Processing: 300-600ms
|
||
├─ Image decode: 10-20ms
|
||
├─ Image resize (if needed): 30-50ms
|
||
├─ HSV conversion: 20-30ms
|
||
├─ Histogram extraction: 50-80ms
|
||
├─ Model loading (first time): 200-400ms
|
||
│ └─ (cached after first use)
|
||
└─ Prediction: 20-50ms
|
||
|
||
Database Operation: 50-150ms
|
||
└─ INSERT prediction record
|
||
|
||
Response Render: 100-200ms
|
||
└─ JavaScript processing & DOM update
|
||
|
||
TOTAL TYPICAL TIME: 500-1200ms
|
||
|
||
OPTIMIZATION STRATEGIES:
|
||
├─ Load model once at Flask startup (cache in memory)
|
||
├─ Use numpy vectorized operations
|
||
├─ Resize images before feature extraction
|
||
├─ Implement response caching
|
||
├─ Use CDN for static assets
|
||
├─ Minify CSS/JavaScript
|
||
├─ Implement lazy loading
|
||
├─ Use database indexing
|
||
└─ Monitor slow queries
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
13. SECURITY CONSIDERATIONS
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
AUTHENTICATION:
|
||
├─ Password hashing (bcrypt in Laravel)
|
||
├─ Session management (secure cookies)
|
||
├─ CSRF token verification (Laravel middleware)
|
||
├─ Admin role checking (before dashboard access)
|
||
└─ Last login tracking
|
||
|
||
AUTHORIZATION:
|
||
├─ Route middleware to check admin status
|
||
├─ Controller middleware for protected routes
|
||
├─ Database-level foreign key constraints
|
||
└─ User-to-prediction relationship validation
|
||
|
||
FILE UPLOAD SECURITY:
|
||
├─ File type validation (MIME type + extension)
|
||
├─ File size limit (16MB max)
|
||
├─ Filename sanitization
|
||
├─ Store outside web root (when possible)
|
||
├─ Prevent executable file uploads
|
||
└─ Virus scanning (optional)
|
||
|
||
API SECURITY:
|
||
├─ HTTPS only (in production)
|
||
├─ Rate limiting on /tomat/classify endpoint
|
||
├─ Request validation
|
||
├─ Input sanitization
|
||
├─ Error message security (no stack traces to users)
|
||
└─ CORS configuration (if needed)
|
||
|
||
DATABASE SECURITY:
|
||
├─ SQL injection prevention (Eloquent ORM uses parameterized queries)
|
||
├─ Database user permissions (least privilege)
|
||
├─ Password field hashing
|
||
├─ Backup & restore procedures
|
||
└─ Regular security audits
|
||
|
||
ENVIRONMENT SECURITY:
|
||
├─ .env file NOT in version control
|
||
├─ Sensitive credentials in .env
|
||
├─ Different keys for dev/prod environments
|
||
├─ HTTPS enforcement
|
||
└─ Security headers (HSTS, CSP, X-Frame-Options)
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
14. FUTURE ENHANCEMENTS
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
FEATURE ENHANCEMENTS:
|
||
├─ Real-time prediction confidence threshold adjustment
|
||
├─ Batch processing for multiple images
|
||
├─ Video processing capability
|
||
├─ Model version management (A/B testing)
|
||
├─ Custom model training by admin
|
||
├─ Result export (PDF reports)
|
||
├─ Email notifications
|
||
└─ Mobile app integration
|
||
|
||
PERFORMANCE:
|
||
├─ Implement caching layer (Redis)
|
||
├─ Async job processing (Laravel Jobs)
|
||
├─ Image CDN integration
|
||
├─ Database query optimization
|
||
├─ API rate limiting configuration
|
||
└─ Load balancing (multiple Flask instances)
|
||
|
||
ML IMPROVEMENTS:
|
||
├─ Implement additional features (texture, shape)
|
||
├─ Ensemble models (combine multiple models)
|
||
├─ Transfer learning (pre-trained models)
|
||
├─ Real-time model retraining
|
||
├─ Automated hyperparameter tuning
|
||
└─ Model explainability (SHAP values)
|
||
|
||
MONITORING:
|
||
├─ Application performance monitoring (APM)
|
||
├─ Error tracking (Sentry)
|
||
├─ Logging aggregation (ELK stack)
|
||
├─ Alerts for service failures
|
||
├─ Dashboard metrics visualization
|
||
└─ User analytics tracking
|
||
|
||
DEPLOYMENT:
|
||
├─ Docker containerization
|
||
├─ Kubernetes orchestration
|
||
├─ CI/CD pipeline (GitHub Actions / GitLab CI)
|
||
├─ Automated testing
|
||
├─ Blue-green deployment
|
||
└─ Auto-scaling based on load
|
||
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
END OF FLOWCHART DOCUMENTATION
|
||
|
||
For more details, see: FLOWCHART_SISTEM_DETAIL.md (Mermaid format)
|
||
Generated: 2026-05-07
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════
|