Spaces:
Runtime error
Runtime error
Upload 12 files
Browse files- .dockerignore +8 -0
- .gitignore +58 -0
- app.py +219 -0
- dockerfile +27 -0
- main.py +7 -0
- note.md +264 -0
- pytest.ini +4 -0
- requirements.txt +11 -3
- server_err.log +39 -0
- server_err_pwa.log +23 -0
- server_out.log +2 -0
- 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 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|