cindyy287 commited on
Commit
e6da2e3
·
verified ·
1 Parent(s): 2bd80d4

Upload 12 files

Browse files
Files changed (12) hide show
  1. .dockerignore +8 -0
  2. .gitignore +58 -0
  3. app.py +219 -0
  4. dockerfile +27 -0
  5. main.py +7 -0
  6. note.md +264 -0
  7. pytest.ini +4 -0
  8. requirements.txt +11 -3
  9. server_err.log +39 -0
  10. server_err_pwa.log +23 -0
  11. server_out.log +2 -0
  12. server_out_pwa.log +0 -0
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .venv/
6
+ .env
7
+ .git/
8
+ .ipynb_checkpoints/
.gitignore ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # C extensions
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ .Python
10
+ env/
11
+ venv/
12
+ ENV/
13
+ *.egg-info/
14
+ *.egg
15
+ dist/
16
+ build/
17
+
18
+ # Unit test / coverage reports
19
+ htmlcov/
20
+ .tox/
21
+ .nox/
22
+ .coverage
23
+ coverage.xml
24
+ *.cover
25
+ .hypothesis/
26
+
27
+ # Jupyter Notebook
28
+ .ipynb_checkpoints
29
+
30
+ # pyenv
31
+ .python-version
32
+
33
+ # mypy
34
+ .mypy_cache/
35
+ .dmypy.json
36
+ dmypy.json
37
+
38
+ # Pyre type checker
39
+ .pyre/
40
+
41
+ # IDEs
42
+ .vscode/
43
+ .idea/
44
+ *.swp
45
+
46
+ # Logs
47
+ *.log
48
+ *.out
49
+ *.err
50
+
51
+ # Models and large files
52
+ models/*.joblib
53
+ models/*.json
54
+
55
+ # Ignore sensitive files
56
+ .env
57
+ *.key
58
+ *.pem
app.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import time
4
+ import json
5
+ import logging
6
+
7
+ import joblib
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from flask import Flask, request, jsonify
12
+ from sklearn.pipeline import Pipeline
13
+
14
+ from features.feature_builder import build_features
15
+ from schemas.request_schema import PredictRequest
16
+
17
+
18
+ # ======================
19
+ # PATH SETUP
20
+ # ======================
21
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22
+ sys.path.insert(0, BASE_DIR)
23
+
24
+
25
+ # ======================
26
+ # APP INIT
27
+ # ======================
28
+ app = Flask(__name__)
29
+
30
+
31
+ # ======================
32
+ # LOGGING
33
+ # ======================
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format="%(asctime)s - %(levelname)s - %(message)s"
37
+ )
38
+
39
+
40
+ # ======================
41
+ # SECURITY CONFIG
42
+ # ======================
43
+ API_KEY = os.getenv("FRAUD_API_KEY")
44
+ MAX_REQUEST_SIZE = 10_000 # 10 KB
45
+ RATE_LIMIT = 30
46
+ RATE_LIMIT_WINDOW = 60 # seconds
47
+
48
+ rate_limit_store = {}
49
+
50
+
51
+ def is_rate_limited(client_ip: str) -> bool:
52
+ now = time.time()
53
+
54
+ if client_ip not in rate_limit_store:
55
+ rate_limit_store[client_ip] = []
56
+
57
+ rate_limit_store[client_ip] = [
58
+ t for t in rate_limit_store[client_ip]
59
+ if now - t < RATE_LIMIT_WINDOW
60
+ ]
61
+
62
+ if len(rate_limit_store[client_ip]) >= RATE_LIMIT:
63
+ return True
64
+
65
+ rate_limit_store[client_ip].append(now)
66
+ return False
67
+
68
+
69
+ # ======================
70
+ # GLOBAL API KEY GUARD
71
+ # ======================
72
+ @app.before_request
73
+ def check_api_key():
74
+ # Health endpoint is public
75
+ if request.path == "/health":
76
+ return
77
+
78
+ # Skip static files if any
79
+ if request.path.startswith("/static"):
80
+ return
81
+
82
+ if not API_KEY:
83
+ logging.error("FRAUD_API_KEY environment variable not set")
84
+ return jsonify({"error": "Server misconfigured"}), 500
85
+
86
+ client_key = request.headers.get("X-API-KEY")
87
+ if not client_key or client_key != API_KEY:
88
+ return jsonify({"error": "Unauthorized"}), 401
89
+
90
+
91
+ # ======================
92
+ # LOAD MODEL & PREPROCESSOR
93
+ # ======================
94
+ MODEL_PATH = os.path.join(BASE_DIR, "models", "ensemble_model_enhanced.joblib")
95
+ PREPROCESSOR_PATH = os.path.join(BASE_DIR, "models", "preprocessor_enhanced.joblib")
96
+
97
+ if not os.path.exists(MODEL_PATH):
98
+ raise FileNotFoundError(f"Model tidak ditemukan: {MODEL_PATH}")
99
+
100
+ if not os.path.exists(PREPROCESSOR_PATH):
101
+ raise FileNotFoundError(f"Preprocessor tidak ditemukan: {PREPROCESSOR_PATH}")
102
+
103
+ model = joblib.load(MODEL_PATH)
104
+ preprocessor = joblib.load(PREPROCESSOR_PATH)
105
+
106
+ pipeline_model = Pipeline([
107
+ ("preprocess", preprocessor),
108
+ ("classifier", model)
109
+ ])
110
+
111
+ THRESHOLD = 0.6
112
+
113
+
114
+ # ======================
115
+ # HEALTH CHECK
116
+ # ======================
117
+ @app.route("/health", methods=["GET"])
118
+ def health():
119
+ return jsonify({
120
+ "status": "ok",
121
+ "model_loaded": model is not None,
122
+ "timestamp": time.time()
123
+ })
124
+
125
+
126
+ # ======================
127
+ # PREDICT
128
+ # ======================
129
+ @app.route("/predict", methods=["POST"])
130
+ def predict():
131
+ start_time = time.time()
132
+
133
+ # ---------- REQUEST SIZE ----------
134
+ if request.content_length and request.content_length > MAX_REQUEST_SIZE:
135
+ return jsonify({"error": "Request too large"}), 413
136
+
137
+ # ---------- RATE LIMIT ----------
138
+ client_ip = request.remote_addr or "unknown"
139
+ if is_rate_limited(client_ip):
140
+ return jsonify({"error": "Too many requests"}), 429
141
+
142
+ # ---------- PARSE & VALIDATE ----------
143
+ try:
144
+ payload = request.get_json()
145
+ req = PredictRequest(**payload)
146
+ data = req.model_dump()
147
+ logging.info("Request valid: %s", data)
148
+ except Exception as e:
149
+ return jsonify({
150
+ "error": "Invalid request schema",
151
+ "detail": str(e)
152
+ }), 422
153
+
154
+ # ---------- BUSINESS VALIDATION ----------
155
+ amount = data.get("amount", 0)
156
+ location = data.get("location", -1)
157
+
158
+ if amount <= 0 or amount > 100_000_000:
159
+ return jsonify({"error": "Invalid amount value"}), 400
160
+
161
+ if location < 0:
162
+ return jsonify({"error": "Invalid location value"}), 400
163
+
164
+ # ---------- FEATURE BUILD ----------
165
+ try:
166
+ X_df = build_features(data)
167
+ logging.info("Feature DF: %s", X_df.to_dict(orient="records"))
168
+ except Exception as e:
169
+ logging.error(f"Feature building error: {e}")
170
+ return jsonify({
171
+ "error": "Feature building error",
172
+ "detail": str(e)
173
+ }), 500
174
+
175
+ # ---------- PREDICT ----------
176
+ try:
177
+ X = preprocessor.transform(X_df)
178
+ fraud_prob = model.predict_proba(X)[0][1]
179
+ except Exception as e:
180
+ logging.error(f"Prediction error: {e}")
181
+ return jsonify({
182
+ "error": "Preprocessing or prediction error",
183
+ "detail": str(e)
184
+ }), 500
185
+
186
+ # ---------- DECISION ----------
187
+ is_fraud = fraud_prob >= THRESHOLD
188
+
189
+ if fraud_prob >= 0.85:
190
+ decision = "BLOCK"
191
+ elif fraud_prob >= THRESHOLD:
192
+ decision = "REVIEW"
193
+ else:
194
+ decision = "ALLOW"
195
+
196
+ latency_ms = round((time.time() - start_time) * 1000, 2)
197
+
198
+ # ---------- STRUCTURED LOG ----------
199
+ logging.info({
200
+ "event": "fraud_decision",
201
+ "fraud_probability": float(fraud_prob),
202
+ "decision": decision,
203
+ "threshold": THRESHOLD,
204
+ "latency_ms": latency_ms
205
+ })
206
+
207
+ return jsonify({
208
+ "fraud_probability": round(float(fraud_prob), 4),
209
+ "is_fraud": bool(is_fraud),
210
+ "decision": decision,
211
+ "latency_ms": latency_ms
212
+ })
213
+
214
+
215
+ # ======================
216
+ # RUN SERVER
217
+ # ======================
218
+ if __name__ == "__main__":
219
+ app.run(debug=True, port=5001)
dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Environment safety
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ # Workdir
8
+ WORKDIR /app
9
+
10
+ # System deps (minimal)
11
+ RUN apt-get update && apt-get install -y \
12
+ build-essential \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy requirements first (cache friendly)
16
+ COPY requirements.txt .
17
+
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copy app code
21
+ COPY . .
22
+
23
+ # Expose port
24
+ EXPOSE 5001
25
+
26
+ # Run with gunicorn (PRODUCTION)
27
+ CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5001", "app:app"]
main.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ app = FastAPI()
4
+
5
+ @app.get("/")
6
+ def root():
7
+ return {"status": "ok"}
note.md ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # note
2
+
3
+ 🔖 **Ringkasan singkat (untuk dibaca besok)**
4
+
5
+ - Project: API deteksi fraud berbasis Flask.
6
+ - Status saat ini: semua unit & integration tests lulus (2 passed).
7
+ - Model files: `ensemble_model_enhanced.joblib` (prioritas) / `ensemble_model.joblib` / `Ensemble_model.joblib`.
8
+ - Preprocessor files: `preprocessor_enhanced.joblib` (prioritas) / `preprocessor.joblib` / `Preprocessor.joblib`.
9
+ - Config: `anscombe.json` (cari case-insensitive).
10
+ - Dependencies penting: `scikit-learn==1.6.1` (dipin), `imbalanced-learn` (untuk unpickle model), `requests` (untuk integrasi).
11
+
12
+ ---
13
+
14
+ ## Cara menjalankan server (cepat)
15
+
16
+ 1. Pastikan dependencies terpasang:
17
+ ```powershell
18
+ & "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" -m pip install -r requirements.txt
19
+ ```
20
+ 2. Jalankan server Flask (foreground):
21
+ ```powershell
22
+ & "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" app.py
23
+ ```
24
+ Server akan tersedia di: `http://127.0.0.1:5000` (default). Tekan `Ctrl+C` untuk stop.
25
+
26
+ 3. Jalankan server di background dan simpan log:
27
+ ```powershell
28
+ Start-Process -FilePath "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" -ArgumentList "app.py" -RedirectStandardOutput server_out.log -RedirectStandardError server_err.log -PassThru
29
+ Get-Content server_out.log -Tail 50 -Wait
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Cara menguji endpoint `/predict`
35
+
36
+ - PowerShell:
37
+ ```powershell
38
+ Invoke-RestMethod -Uri http://127.0.0.1:5000/predict -Method Post -ContentType 'application/json' -Body '{"features":[200,1,0,500]}'
39
+ ```
40
+ - curl:
41
+ ```powershell
42
+ curl -X POST http://127.0.0.1:5000/predict -H "Content-Type: application/json" -d "{\"features\":[200,1,0,500]}"
43
+ ```
44
+ - Python one-liner:
45
+ ```powershell
46
+ & "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" -c "import requests; print(requests.post('http://127.0.0.1:5000/predict', json={'features':[200,1,0,500]}).json())"
47
+ ```
48
+
49
+ Respons contoh:
50
+ ```json
51
+ {"fraud":0, "fraud_prediction":0, "probability":0.83}
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Menguji frontend (quick smoke test)
57
+
58
+ 1. Pastikan server Flask berjalan (lihat bagian "Cara menjalankan server").
59
+ 2. Serving file frontend agar fetch berjalan tanpa masalah CORS dari file://, mis. jalankan dari folder `frontend`:
60
+ ```powershell
61
+ cd frontend
62
+ python -m http.server 8000
63
+ # lalu buka http://localhost:8000/fraud_detection_frontend.html di browser
64
+ ```
65
+ 3. Periksa `API_BASE_URL` di `frontend/fraud_detection_frontend.html` — default saya set ke `http://localhost:5000` karena endpoint `/predict` ada di root.
66
+ 4. Form akan mengirimkan payload lengkap yang mencakup field yang diharapkan preprocessor (contoh payload yang berhasil diuji):
67
+ ```json
68
+ {
69
+ "merchant_name":"Test",
70
+ "avg_amount_per_transaction":123.45,
71
+ "day_of_week":2,
72
+ "amount_deviation_from_location_mean":0,
73
+ "transaction_category":"retail",
74
+ "customer_no_transactions":0,
75
+ "customer_lat":null,
76
+ "transaction_type":"online",
77
+ "customer_place_name":null,
78
+ "merchant_id":null,
79
+ "location":"New York",
80
+ "customer_job":null,
81
+ "age":null,
82
+ "merchant_long":null,
83
+ "amount_per_city_pop":0,
84
+ "customer_long":null,
85
+ "distance_customer_merchant":0,
86
+ "transactions_per_customer_ratio":0,
87
+ "customer_city_population":0,
88
+ "merchant_lat":null,
89
+ "customer_no_payments":0,
90
+ "customer_no_orders":0,
91
+ "payments_per_order_ratio":0,
92
+ "hour_of_day":12,
93
+ "amount":123.45,
94
+ "customer_zip_code":null,
95
+ "mean_amount_by_location":0,
96
+ "fraud_rate_by_location":0,
97
+ "customer_gender":null
98
+ }
99
+ ```
100
+
101
+ Hasil uji lokal (contoh): server merespons 200 dan body JSON seperti `{"fraud":0,"fraud_prediction":0,"probability":0.766...}`
102
+
103
+ ---
104
+
105
+ ## Menjadikan Frontend ke Aplikasi Mobile (ringkasan & langkah cepat)
106
+
107
+ Jika ingin agar frontend bisa diakses seperti aplikasi mobile, ada 2 jalur praktis yang saya rekomendasikan:
108
+
109
+ - **PWA (Progressive Web App)** — tercepat dan paling mudah dicoba.
110
+ - Buat `manifest.json` (name, short_name, icons, start_url, display: standalone).
111
+ - Tambah `sw.js` (service worker) untuk caching (app shell) dan memungkinkan akses offline terbatas.
112
+ - Pastikan serve via HTTPS (Chrome/Edge mewajibkan HTTPS untuk installable PWA). Untuk development, pakai `ngrok http 8000` atau host sementara.
113
+ - Pengguna bisa pilih "Add to Home screen" di browser Android/Chrome.
114
+ - Cocok untuk prototipe dan distribusi cepat tanpa perlu build native.
115
+
116
+ - **Capacitor (WebView wrapper → Android/iOS native app)** — bila mau jadi APK/IPA.
117
+ - Butuh Node.js dan Android SDK (untuk Android) atau Xcode (untuk iOS).
118
+ - Langkah ringkas:
119
+ ```powershell
120
+ # di folder project root
121
+ npm init -y
122
+ npm install @capacitor/core @capacitor/cli
123
+ npx cap init my-app com.example.myapp
124
+ # Copy output static (letakkan file html ke folder `www/` atau build pipeline)
125
+ npx cap add android
126
+ npx cap copy android
127
+ npx cap open android
128
+ ```
129
+ - Untuk dev lokal saat backend di localhost, gunakan `ngrok` (atau host backend) supaya device nyata bisa reach API.
130
+
131
+ ### Checklist penting sebelum rilis
132
+ - Pastikan backend reachable dari device (hosted / ngrok untuk testing).
133
+ - Gunakan HTTPS untuk API (Play Store & browser modern membatasi HTTP).
134
+ - Pastikan CORS/Origin sudah diatur (PWA: origin penting; WebView biasanya tidak terkena CORS sama cara).
135
+ - Otentikasi & keamanan: pakai token, jangan hard-code credentials di kode klien.
136
+ - Ukuran & performa: minimalkan asset (gambar, script) dan aktifkan caching service worker untuk PWA.
137
+ - Untuk Play Store: sign APK dengan key, perhatikan kebijakan privasi & permission.
138
+
139
+ ### Contoh resource cepat (starter files)
140
+ - `manifest.json` (minimal):
141
+ ```json
142
+ {
143
+ "name": "Fraud Detection AI",
144
+ "short_name": "FraudAI",
145
+ "start_url": "/fraud_detection_frontend.html",
146
+ "display": "standalone",
147
+ "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" } ]
148
+ }
149
+ ```
150
+ - `sw.js` (very small cache-first):
151
+ ```js
152
+ const CACHE_NAME = 'fraud-ai-v1';
153
+ const ASSETS = ['/', '/fraud_detection_frontend.html', '/styles.css'];
154
+ self.addEventListener('install', e => {
155
+ e.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)));
156
+ });
157
+ self.addEventListener('fetch', e => {
158
+ e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
159
+ });
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Implementasi PWA & Capacitor (yang sudah saya tambahkan)
165
+
166
+ - Sudah saya tambahkan ke repo:
167
+ - `frontend/manifest.json`, `frontend/sw.js`, `frontend/icons/icon.svg`
168
+ - `frontend/README_PWA.md` (cara test & catatan)
169
+ - Perubahan pada `frontend/fraud_detection_frontend.html` (link `manifest.json`, register `sw.js`)
170
+ - `mobile/README_CAPACITOR.md` berisi langkah awal untuk membungkus aplikasi dengan Capacitor
171
+
172
+ Jika Anda ingin saya lanjut scaffold project Capacitor (init + add android, script npm), saya bisa kerjakan — beri tahu saya, dan saya akan buat branch terpisah karena itu menambahkan file Node.js/platform yang besar.
173
+
174
+
175
+ ---
176
+
177
+ ## Menjalankan tests
178
+
179
+ - Jalankan semua tests (unit + integration):
180
+ ```powershell
181
+ & "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" -m pytest -q
182
+ ```
183
+ - Hanya unit tests (skip integration):
184
+ ```powershell
185
+ & "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" -m pytest -q -m "not integration"
186
+ ```
187
+ - Hanya integration tests:
188
+ ```powershell
189
+ & "C:\Users\CINDY\AppData\Local\Programs\Python\Python310\python.exe" -m pytest -q -m integration
190
+ ```
191
+
192
+ > Catatan: integration test men-start server otomatis pada port `5001`.
193
+
194
+ ---
195
+
196
+ ## Debugging cepat (jika error)
197
+
198
+ - Jika mendapat `Connection refused`: server tidak jalan atau port diblokir. Cek log `server_out.log` / `server_err.log` dan jalankan server di foreground untuk melihat traceback.
199
+ - Jika mendapat `500 Internal Server Error`: buka `server_err.log`, salin traceback, lalu perbaiki (bisa minta saya terjemahkan/diagnosa).
200
+
201
+ Perintah cek log:
202
+ ```powershell
203
+ Get-Content server_err.log -Tail 200
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Hal lain yang sudah saya lakukan
209
+ - Menambahkan file `tests/test_integration.py` dan `scripts/run_integration_debug.py` (debug helper).
210
+ - Menambahkan `.vscode/settings.json` untuk pytest discovery.
211
+ - Membuat `pytest.ini` dengan marker `integration`.
212
+ - Membuat perubahan di `app/model.py` agar lebih fleksibel memuat model/preprocessor dan meng-handle input fitur yang berbeda panjang.
213
+
214
+ ---
215
+
216
+ ## Rencana selanjutnya (opsional)
217
+ - Commit & push perubahan jika Anda mau (saya bisa bantu buat pesan commit).
218
+ - Tambahkan CI (GitHub Actions) agar tests jalan otomatis di push/PR.
219
+ - Buatkan `DEVELOPMENT_NOTES.md` yang lebih panjang (jika Anda mau dokumentasi lebih lengkap).
220
+
221
+ ---
222
+
223
+ Jika mau, saya bisa langsung commit file `note.md` ini (atau buat branch & PR). Mau saya commit sekarang? ✅
224
+
225
+ ---
226
+
227
+ ## Uji Stabilitas / Load testing
228
+
229
+ Kalau Anda mengalami kesulitan membuat uji stabilitas, berikut langkah praktis yang bisa dilakukan secara lokal untuk mengecek bagaimana backend `predict` berperilaku di bawah beban.
230
+
231
+ 1) Jalankan server dengan server WSGI produksi
232
+ - Windows: gunakan `waitress` (sederhana dan cocok di Windows)
233
+ ```powershell
234
+ python -m pip install waitress
235
+ waitress-serve --listen=0.0.0.0:5000 app:app
236
+ ```
237
+ - Linux/macOS: gunakan `gunicorn`
238
+ ```bash
239
+ python -m pip install gunicorn
240
+ gunicorn -w 4 -b 0.0.0.0:5000 app:app
241
+ ```
242
+
243
+ 2) Jalankan Locust (sudah saya siapkan file `load_tests/locustfile.py`)
244
+ ```powershell
245
+ python -m pip install locust
246
+ locust -f load_tests/locustfile.py --host=http://localhost:5000
247
+ # buka http://localhost:8089 untuk mengatur jumlah users dan spawn rate
248
+ ```
249
+
250
+ 3) Panduan uji bertahap
251
+ - Mulai kecil: 5 users, spawn rate 1/s selama 30s. Naikkan perlahan (20 → 50 → 100) sambil memonitor CPU/RAM dan error rate.
252
+ - Perhatikan latency p50/p95/p99 dan request failures di Locust UI.
253
+
254
+ 4) Jika Anda ingin menguji dari perangkat mobile (Capacitor) atau perangkat nyata:
255
+ - Pastikan backend dapat dijangkau dari device — gunakan `ngrok http 5000` untuk membuat HTTPS tunnel, lalu gunakan URL ngrok sebagai `API_BASE_URL` di frontend.
256
+
257
+ 5) Interpretasi singkat
258
+ - Error rate tinggi: kemungkinan server kehabisan worker/connection atau unhandled exceptions. Cek `server_err.log`.
259
+ - Latency tinggi: periksa penggunaan CPU (model inference mungkin bottleneck). Solusi: batching, memindahkan inference ke worker terpisah, atau skalakan server (multiple instances/load balancer).
260
+
261
+ 6) File & tool yang saya tambahkan
262
+ - `load_tests/locustfile.py` — contoh script Locust untuk POST `/predict`.
263
+
264
+ Kalau mau, saya siap bantu menjalankan uji ini bersama (saya pandu perintah di terminal Anda), atau scaffold runner otomatis di cloud (mis. k6 script + GitHub Actions) untuk uji berkelanjutan.
pytest.ini ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [pytest]
2
+ markers =
3
+ integration: mark a test as an integration test (requires services)
4
+ testpaths = tests
requirements.txt CHANGED
@@ -1,3 +1,11 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
1
+ python-dotenv
2
+ flask
3
+ flask-cors
4
+ gunicorn
5
+
6
+ numpy
7
+ pandas
8
+ joblib
9
+ scikit-learn==1.6.1
10
+ imbalanced-learn
11
+ requests
server_err.log ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
2
+ * Running on http://127.0.0.1:5000
3
+ Press CTRL+C to quit
4
+ * Restarting with stat
5
+ * Debugger is active!
6
+ * Debugger PIN: 362-990-034
7
+ 127.0.0.1 - - [16/Dec/2025 01:27:49] "POST /predict HTTP/1.1" 200 -
8
+ 127.0.0.1 - - [16/Dec/2025 01:34:34] "GET / HTTP/1.1" 404 -
9
+ 127.0.0.1 - - [16/Dec/2025 01:34:36] "GET /favicon.ico HTTP/1.1" 404 -
10
+ * Detected change in 'C:\\Users\\CINDY\\OneDrive\\Desktop\\fraud_detection_api\\app\\model.py', reloading
11
+ * Restarting with stat
12
+ * Debugger is active!
13
+ * Debugger PIN: 362-990-034
14
+ 127.0.0.1 - - [16/Dec/2025 01:45:01] "POST /api/predict HTTP/1.1" 404 -
15
+ 127.0.0.1 - - [16/Dec/2025 01:45:15] "GET / HTTP/1.1" 404 -
16
+ 127.0.0.1 - - [16/Dec/2025 01:46:04] "POST /predict HTTP/1.1" 500 -
17
+ 127.0.0.1 - - [16/Dec/2025 01:47:03] "POST /predict HTTP/1.1" 500 -
18
+ 127.0.0.1 - - [16/Dec/2025 01:47:28] "POST /predict HTTP/1.1" 500 -
19
+ 127.0.0.1 - - [16/Dec/2025 01:48:59] "POST /predict HTTP/1.1" 200 -
20
+ * Detected change in 'C:\\Users\\CINDY\\OneDrive\\Desktop\\fraud_detection_api\\scripts\\run_integration_debug.py', reloading
21
+ * Restarting with stat
22
+ * Debugger is active!
23
+ * Debugger PIN: 362-990-034
24
+ * Detected change in 'C:\\Users\\CINDY\\OneDrive\\Desktop\\fraud_detection_api\\tests\\test_api.py', reloading
25
+ * Restarting with stat
26
+ * Debugger is active!
27
+ * Debugger PIN: 362-990-034
28
+ * Detected change in 'C:\\Users\\CINDY\\OneDrive\\Desktop\\fraud_detection_api\\app.py', reloading
29
+ * Restarting with stat
30
+ * Debugger is active!
31
+ * Debugger PIN: 362-990-034
32
+ * Detected change in 'C:\\Users\\CINDY\\OneDrive\\Desktop\\fraud_detection_api\\app\\__init__.py', reloading
33
+ * Restarting with stat
34
+ * Debugger is active!
35
+ * Debugger PIN: 362-990-034
36
+ * Detected change in 'C:\\Users\\CINDY\\OneDrive\\Desktop\\fraud_detection_api\\app\\routes.py', reloading
37
+ * Restarting with stat
38
+ * Debugger is active!
39
+ * Debugger PIN: 362-990-034
server_err_pwa.log ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ::1 - - [16/Dec/2025 17:47:47] "GET /manifest.json HTTP/1.1" 200 -
2
+ ::1 - - [16/Dec/2025 17:47:55] "GET /sw.js HTTP/1.1" 200 -
3
+ ::1 - - [16/Dec/2025 18:02:21] "GET /fraud_detection_frontend.html HTTP/1.1" 200 -
4
+ ::1 - - [16/Dec/2025 18:02:22] "GET /manifest.json HTTP/1.1" 200 -
5
+ ::1 - - [16/Dec/2025 18:02:22] "GET /icons/icon.svg HTTP/1.1" 200 -
6
+ ::1 - - [16/Dec/2025 18:16:13] "GET / HTTP/1.1" 200 -
7
+ ::1 - - [16/Dec/2025 18:16:14] code 404, message File not found
8
+ ::1 - - [16/Dec/2025 18:16:14] "GET /favicon.ico HTTP/1.1" 404 -
9
+ ::1 - - [16/Dec/2025 18:16:19] "GET / HTTP/1.1" 200 -
10
+ ::1 - - [16/Dec/2025 18:16:21] "GET / HTTP/1.1" 200 -
11
+ ::1 - - [16/Dec/2025 18:16:57] "GET /manifest.json HTTP/1.1" 304 -
12
+ ::1 - - [16/Dec/2025 18:16:57] "GET /icons/icon.svg HTTP/1.1" 304 -
13
+ ::1 - - [16/Dec/2025 18:20:52] "GET / HTTP/1.1" 200 -
14
+ ::1 - - [16/Dec/2025 18:21:25] "GET / HTTP/1.1" 200 -
15
+ ::1 - - [16/Dec/2025 18:21:41] "GET / HTTP/1.1" 200 -
16
+ ::1 - - [16/Dec/2025 18:21:44] "GET / HTTP/1.1" 200 -
17
+ ::1 - - [16/Dec/2025 18:21:52] "GET / HTTP/1.1" 200 -
18
+ ::1 - - [16/Dec/2025 18:23:17] "GET / HTTP/1.1" 200 -
19
+ ::1 - - [16/Dec/2025 18:23:17] "GET /manifest.json HTTP/1.1" 304 -
20
+ ::1 - - [16/Dec/2025 18:23:17] "GET /icons/icon.svg HTTP/1.1" 304 -
21
+ ::1 - - [16/Dec/2025 18:31:25] "GET / HTTP/1.1" 304 -
22
+ ::1 - - [16/Dec/2025 18:31:25] "GET /manifest.json HTTP/1.1" 304 -
23
+ ::1 - - [16/Dec/2025 18:31:25] "GET /icons/icon.svg HTTP/1.1" 304 -
server_out.log ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ * Serving Flask app 'app'
2
+ * Debug mode: on
server_out_pwa.log ADDED
File without changes