appku
This commit is contained in:
parent
cd996ba56a
commit
bef5d9f151
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
3
ai/__init__.py
Normal file
3
ai/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Helpers for interacting with the Flatlogic AI proxy from Django code."""
|
||||
|
||||
from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401
|
||||
282
ai/local_ai_api.py
Normal file
282
ai/local_ai_api.py
Normal file
@ -0,0 +1,282 @@
|
||||
"""
|
||||
LocalAIApi — lightweight Python client for the Flatlogic AI proxy.
|
||||
|
||||
Usage (inside the Django workspace):
|
||||
|
||||
from ai.local_ai_api import LocalAIApi
|
||||
|
||||
response = LocalAIApi.create_response({
|
||||
"input": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Summarise this text in two sentences."},
|
||||
],
|
||||
"text": {"format": {"type": "json_object"}},
|
||||
})
|
||||
|
||||
if response.get("success"):
|
||||
data = LocalAIApi.decode_json_from_response(response)
|
||||
# ...
|
||||
|
||||
The helper automatically injects the project UUID header and falls back to
|
||||
reading executor/.env if environment variables are missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
from urllib import error as urlerror
|
||||
from urllib import request as urlrequest
|
||||
|
||||
__all__ = [
|
||||
"LocalAIApi",
|
||||
"create_response",
|
||||
"request",
|
||||
"decode_json_from_response",
|
||||
]
|
||||
|
||||
|
||||
_CONFIG_CACHE: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class LocalAIApi:
|
||||
"""Static helpers mirroring the PHP implementation."""
|
||||
|
||||
@staticmethod
|
||||
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
return create_response(params, options or {})
|
||||
|
||||
@staticmethod
|
||||
def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None,
|
||||
options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
return request(path, payload or {}, options or {})
|
||||
|
||||
@staticmethod
|
||||
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return decode_json_from_response(response)
|
||||
|
||||
|
||||
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Signature compatible with the OpenAI Responses API."""
|
||||
options = options or {}
|
||||
payload = dict(params)
|
||||
|
||||
if not isinstance(payload.get("input"), list) or not payload["input"]:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "input_missing",
|
||||
"message": 'Parameter "input" is required and must be a non-empty list.',
|
||||
}
|
||||
|
||||
cfg = _config()
|
||||
if not payload.get("model"):
|
||||
payload["model"] = cfg["default_model"]
|
||||
|
||||
return request(options.get("path"), payload, options)
|
||||
|
||||
|
||||
def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Perform a raw request to the AI proxy."""
|
||||
cfg = _config()
|
||||
options = options or {}
|
||||
|
||||
resolved_path = path or options.get("path") or cfg["responses_path"]
|
||||
if not resolved_path:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "project_id_missing",
|
||||
"message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.",
|
||||
}
|
||||
|
||||
project_uuid = cfg["project_uuid"]
|
||||
if not project_uuid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "project_uuid_missing",
|
||||
"message": "PROJECT_UUID is not defined; aborting AI request.",
|
||||
}
|
||||
|
||||
if "project_uuid" not in payload and project_uuid:
|
||||
payload["project_uuid"] = project_uuid
|
||||
|
||||
url = _build_url(resolved_path, cfg["base_url"])
|
||||
timeout = int(options.get("timeout", cfg["timeout"]))
|
||||
verify_tls = options.get("verify_tls", cfg["verify_tls"])
|
||||
|
||||
headers: Dict[str, str] = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
cfg["project_header"]: project_uuid,
|
||||
}
|
||||
extra_headers = options.get("headers")
|
||||
if isinstance(extra_headers, Iterable):
|
||||
for header in extra_headers:
|
||||
if isinstance(header, str) and ":" in header:
|
||||
name, value = header.split(":", 1)
|
||||
headers[name.strip()] = value.strip()
|
||||
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urlrequest.Request(url, data=body, method="POST")
|
||||
for name, value in headers.items():
|
||||
req.add_header(name, value)
|
||||
|
||||
context = None
|
||||
if not verify_tls:
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
with urlrequest.urlopen(req, timeout=timeout, context=context) as resp:
|
||||
status = resp.getcode()
|
||||
response_body = resp.read().decode("utf-8", errors="replace")
|
||||
except urlerror.HTTPError as exc:
|
||||
status = exc.getcode()
|
||||
response_body = exc.read().decode("utf-8", errors="replace")
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
return {
|
||||
"success": False,
|
||||
"error": "request_failed",
|
||||
"message": str(exc),
|
||||
}
|
||||
|
||||
decoded = None
|
||||
if response_body:
|
||||
try:
|
||||
decoded = json.loads(response_body)
|
||||
except json.JSONDecodeError:
|
||||
decoded = None
|
||||
|
||||
if 200 <= status < 300:
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"data": decoded if decoded is not None else response_body,
|
||||
}
|
||||
|
||||
error_message = "AI proxy request failed"
|
||||
if isinstance(decoded, dict):
|
||||
error_message = decoded.get("error") or decoded.get("message") or error_message
|
||||
elif response_body:
|
||||
error_message = response_body
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"status": status,
|
||||
"error": error_message,
|
||||
"response": decoded if decoded is not None else response_body,
|
||||
}
|
||||
|
||||
|
||||
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Attempt to decode JSON emitted by the model (handles markdown fences)."""
|
||||
text = _extract_text(response)
|
||||
if text == "":
|
||||
return None
|
||||
|
||||
try:
|
||||
decoded = json.loads(text)
|
||||
if isinstance(decoded, dict):
|
||||
return decoded
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
stripped = text.strip()
|
||||
if stripped.startswith("```json"):
|
||||
stripped = stripped[7:]
|
||||
if stripped.endswith("```"):
|
||||
stripped = stripped[:-3]
|
||||
stripped = stripped.strip()
|
||||
if stripped and stripped != text:
|
||||
try:
|
||||
decoded = json.loads(stripped)
|
||||
if isinstance(decoded, dict):
|
||||
return decoded
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _extract_text(response: Dict[str, Any]) -> str:
|
||||
payload = response.get("data") if response.get("success") else response.get("response")
|
||||
if isinstance(payload, dict):
|
||||
output = payload.get("output")
|
||||
if isinstance(output, list):
|
||||
combined = ""
|
||||
for item in output:
|
||||
content = item.get("content") if isinstance(item, dict) else None
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"):
|
||||
combined += str(block["text"])
|
||||
if combined:
|
||||
return combined
|
||||
choices = payload.get("choices")
|
||||
if isinstance(choices, list) and choices:
|
||||
message = choices[0].get("message")
|
||||
if isinstance(message, dict) and message.get("content"):
|
||||
return str(message["content"])
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
return ""
|
||||
|
||||
|
||||
def _config() -> Dict[str, Any]:
|
||||
global _CONFIG_CACHE # noqa: PLW0603
|
||||
if _CONFIG_CACHE is not None:
|
||||
return _CONFIG_CACHE
|
||||
|
||||
_ensure_env_loaded()
|
||||
|
||||
base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com")
|
||||
project_id = os.getenv("PROJECT_ID") or None
|
||||
responses_path = os.getenv("AI_RESPONSES_PATH")
|
||||
if not responses_path and project_id:
|
||||
responses_path = f"/projects/{project_id}/ai-request"
|
||||
|
||||
_CONFIG_CACHE = {
|
||||
"base_url": base_url,
|
||||
"responses_path": responses_path,
|
||||
"project_id": project_id,
|
||||
"project_uuid": os.getenv("PROJECT_UUID"),
|
||||
"project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"),
|
||||
"default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5"),
|
||||
"timeout": int(os.getenv("AI_TIMEOUT", "30")),
|
||||
"verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"},
|
||||
}
|
||||
return _CONFIG_CACHE
|
||||
|
||||
|
||||
def _build_url(path: str, base_url: str) -> str:
|
||||
trimmed = path.strip()
|
||||
if trimmed.startswith("http://") or trimmed.startswith("https://"):
|
||||
return trimmed
|
||||
if trimmed.startswith("/"):
|
||||
return f"{base_url}{trimmed}"
|
||||
return f"{base_url}/{trimmed}"
|
||||
|
||||
|
||||
def _ensure_env_loaded() -> None:
|
||||
"""Populate os.environ from executor/.env if variables are missing."""
|
||||
if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"):
|
||||
return
|
||||
|
||||
env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
|
||||
if not os.path.exists(env_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(env_path, "r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||
continue
|
||||
key, value = stripped.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('\'"')
|
||||
if key and not os.getenv(key):
|
||||
os.environ[key] = value
|
||||
except OSError:
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18
core/templates/base.html
Normal file
18
core/templates/base.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}KapiohoNet{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
14
core/templates/core/article_detail.html
Normal file
14
core/templates/core/article_detail.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ article.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h1>{{ article.title }}</h1>
|
||||
<p class="text-muted">Published on {{ article.created_at|date:"F d, Y" }}</p>
|
||||
<hr>
|
||||
<div>
|
||||
{{ article.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
8
core/templates/core/create_hotspot_voucher.html
Normal file
8
core/templates/core/create_hotspot_voucher.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "core/dashboard_base.html" %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="header-bar">
|
||||
<h1 class="page-title">Buat Voucher (Hotspot)</h1>
|
||||
</div>
|
||||
<p>Halaman ini dalam pengembangan.</p>
|
||||
{% endblock %}
|
||||
8
core/templates/core/create_pppoe_profile.html
Normal file
8
core/templates/core/create_pppoe_profile.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "core/dashboard_base.html" %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="header-bar">
|
||||
<h1 class="page-title">Buat Profil PPPoE</h1>
|
||||
</div>
|
||||
<p>Halaman ini dalam pengembangan.</p>
|
||||
{% endblock %}
|
||||
62
core/templates/core/dashboard.html
Normal file
62
core/templates/core/dashboard.html
Normal file
@ -0,0 +1,62 @@
|
||||
{% extends "core/dashboard_base.html" %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="header-bar">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="summary-cards">
|
||||
<div class="card">
|
||||
<h4>Total Pelanggan</h4>
|
||||
<p>{{ total_customers }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>Pengguna Aktif Hotspot</h4>
|
||||
<p>{{ total_hotspot }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>Pengguna Aktif PPPoE</h4>
|
||||
<p>{{ total_pppoe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="customer-table-container">
|
||||
<h3 class="table-title">Daftar Pelanggan</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nama User</th>
|
||||
<th>Mac Address</th>
|
||||
<th>Tipe Koneksi</th>
|
||||
<th>Paket/Profile</th>
|
||||
<th>Tanggal Kadaluarsa</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for customer in customers %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ customer.name }}</td>
|
||||
<td>{{ customer.mac_address }}</td>
|
||||
<td>{{ customer.type }}</td>
|
||||
<td>{{ customer.profile }}</td>
|
||||
<td>{{ customer.comment }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Tidak ada data pelanggan yang ditemukan.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
core/templates/core/dashboard_base.html
Normal file
24
core/templates/core/dashboard_base.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - KapiohoNet{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard-container">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2 class="app-name">KapiohoNet</h2>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="{% url 'dashboard' %}" class="nav-link">Dashboard</a>
|
||||
<a href="{% url 'create_pppoe_profile' %}" class="nav-link">Buat Profil PPPoE</a>
|
||||
<a href="{% url 'input_pppoe_customer' %}" class="nav-link">Input Pelanggan (PPPoE)</a>
|
||||
<a href="{% url 'create_hotspot_voucher' %}" class="nav-link">Buat Voucher (Hotspot)</a>
|
||||
<a href="{% url 'edit_route' %}" class="nav-link">Edit Route</a>
|
||||
<a href="{% url 'monthly_report' %}" class="nav-link">Laporan Bulanan</a>
|
||||
<a href="{% url 'logout' %}" class="nav-link">Log Out</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="main-content">
|
||||
{% block dashboard_content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
8
core/templates/core/edit_route.html
Normal file
8
core/templates/core/edit_route.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "core/dashboard_base.html" %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="header-bar">
|
||||
<h1 class="page-title">Edit Route</h1>
|
||||
</div>
|
||||
<p>Halaman ini dalam pengembangan.</p>
|
||||
{% endblock %}
|
||||
@ -1,157 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ project_name }}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.08);
|
||||
--card-border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
{% block title %}KapiohoNet - Akses On-Demand{% endblock title %}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
{% block content %}
|
||||
<div class="hero-section">
|
||||
<div class="container text-center">
|
||||
<h1 class="hero-title">Selamat Datang di KapiohoNet</h1>
|
||||
<p class="hero-subtitle">Dashboard Akses On-Demand untuk Jaringan RT/RW Anda.</p>
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-cta">Dashboard Login</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(130deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'><path d='M-20 20L160 20M20 -20L20 160' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 24s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(-140px, -140px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: clamp(2rem, 4vw, 3rem);
|
||||
width: min(640px, 92vw);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 20px;
|
||||
padding: clamp(2rem, 4vw, 3rem);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 1.2rem;
|
||||
font-weight: 700;
|
||||
font-size: clamp(2.2rem, 3vw + 1.3rem, 3rem);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.6rem 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2.4rem;
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="container features-section">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-4">
|
||||
<div class="feature-card">
|
||||
<h3 class="feature-title">Manajemen Pengguna</h3>
|
||||
<p>Kelola data pelanggan, status langganan, dan riwayat pembayaran dengan mudah.</p>
|
||||
</div>
|
||||
<p>Appwizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p>This page will refresh automatically as the plan is implemented.</p>
|
||||
<p>
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> —
|
||||
UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<div class="col-md-4">
|
||||
<div class="feature-card">
|
||||
<h3 class="feature-title">Integrasi MikroTik</h3>
|
||||
<p>Monitor dan kelola traffic, bandwidth, dan koneksi pengguna secara real-time.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="feature-card">
|
||||
<h3 class="feature-title">Laporan & Statistik</h3>
|
||||
<p>Dapatkan wawasan tentang penggunaan jaringan dan pendapatan melalui laporan terperinci.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
8
core/templates/core/input_customer.html
Normal file
8
core/templates/core/input_customer.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "core/dashboard_base.html" %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="header-bar">
|
||||
<h1 class="page-title">Input Pelanggan (PPPoE)</h1>
|
||||
</div>
|
||||
<p>Halaman ini dalam pengembangan.</p>
|
||||
{% endblock %}
|
||||
32
core/templates/core/login.html
Normal file
32
core/templates/core/login.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Login - KapiohoNet{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<h2 class="text-center mb-4">KapiohoNet</h2>
|
||||
<p class="text-center text-secondary mb-4">Akses On-Demand</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="router_ip" class="form-label">Mikrotik IP (ZeroTier)</label>
|
||||
<input type="text" class="form-control" id="router_ip" name="router_ip" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Masuk</button>
|
||||
<div id="status-message" class="text-center mt-3">
|
||||
<!-- Status messages will appear here -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
8
core/templates/core/monthly_report.html
Normal file
8
core/templates/core/monthly_report.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "core/dashboard_base.html" %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="header-bar">
|
||||
<h1 class="page-title">Laporan Bulanan</h1>
|
||||
</div>
|
||||
<p>Halaman ini dalam pengembangan.</p>
|
||||
{% endblock %}
|
||||
16
core/urls.py
16
core/urls.py
@ -1,7 +1,17 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from .views import (
|
||||
login_view, dashboard_view, logout_view,
|
||||
create_pppoe_profile_view, input_pppoe_customer_view, create_hotspot_voucher_view,
|
||||
edit_route_view, monthly_report_view
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path('login/', login_view, name='login'),
|
||||
path('dashboard/', dashboard_view, name='dashboard'),
|
||||
path('logout/', logout_view, name='logout'),
|
||||
path('create-pppoe-profile/', create_pppoe_profile_view, name='create_pppoe_profile'),
|
||||
path('input-pppoe-customer/', input_pppoe_customer_view, name='input_pppoe_customer'),
|
||||
path('create-hotspot-voucher/', create_hotspot_voucher_view, name='create_hotspot_voucher'),
|
||||
path('edit-route/', edit_route_view, name='edit_route'),
|
||||
path('monthly-report/', monthly_report_view, name='monthly_report'),
|
||||
]
|
||||
|
||||
158
core/views.py
158
core/views.py
@ -1,37 +1,135 @@
|
||||
import os
|
||||
import platform
|
||||
import routeros_api
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views.generic.edit import CreateView
|
||||
# A dummy user is used to authenticate against Django's session management.
|
||||
# This is not a real user in the database, but a temporary, in-memory user.
|
||||
# This is required to use the @login_required decorator.
|
||||
# We create the user if it does not exist.
|
||||
try:
|
||||
User.objects.get(username='dummy')
|
||||
except User.DoesNotExist:
|
||||
User.objects.create_user('dummy', password='dummy')
|
||||
|
||||
from .forms import TicketForm
|
||||
from .models import Ticket
|
||||
def login_view(request):
|
||||
if request.method == 'POST':
|
||||
router_ip = request.POST.get('router_ip')
|
||||
username = request.POST.get('username')
|
||||
password = request.POST.get('password')
|
||||
|
||||
try:
|
||||
connection = routeros_api.RouterOsApiPool(
|
||||
router_ip,
|
||||
username=username,
|
||||
password=password,
|
||||
plaintext_login=True
|
||||
)
|
||||
api = connection.get_api()
|
||||
# If connection is successful, store credentials in session
|
||||
request.session['router_ip'] = router_ip
|
||||
request.session['username'] = username
|
||||
request.session['password'] = password
|
||||
|
||||
user = User.objects.get(username='dummy')
|
||||
login(request, user)
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
return redirect('dashboard')
|
||||
except Exception as e:
|
||||
# Handle connection error
|
||||
return render(request, 'core/login.html', {'error': str(e)})
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
return render(request, 'core/login.html')
|
||||
|
||||
@login_required(login_url='/')
|
||||
def dashboard_view(request):
|
||||
router_ip = request.session.get('router_ip')
|
||||
username = request.session.get('username')
|
||||
password = request.session.get('password')
|
||||
|
||||
class TicketCreateView(CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = "core/ticket_create.html"
|
||||
success_url = reverse_lazy("home")
|
||||
if not all([router_ip, username]):
|
||||
return redirect('login')
|
||||
|
||||
try:
|
||||
connection = routeros_api.RouterOsApiPool(
|
||||
router_ip,
|
||||
username=username,
|
||||
password=password,
|
||||
plaintext_login=True
|
||||
)
|
||||
api = connection.get_api()
|
||||
|
||||
# Fetch PPPoE secrets
|
||||
ppp_secrets = api.get_resource('/ppp/secret').get()
|
||||
|
||||
# Fetch Hotspot users
|
||||
hotspot_users = api.get_resource('/ip/hotspot/user').get()
|
||||
|
||||
# Process data
|
||||
customers = []
|
||||
for user in ppp_secrets:
|
||||
customers.append({
|
||||
'id': user.get('id'),
|
||||
'name': user.get('name'),
|
||||
'mac_address': user.get('caller-id', '-'),
|
||||
'type': 'PPPoE',
|
||||
'profile': user.get('profile', '-'),
|
||||
'expires': user.get('comment', '-')
|
||||
})
|
||||
|
||||
for user in hotspot_users:
|
||||
customers.append({
|
||||
'id': user.get('id'),
|
||||
'name': user.get('name'),
|
||||
'mac_address': user.get('mac-address', '-'),
|
||||
'type': 'Hotspot',
|
||||
'profile': user.get('profile', '-'),
|
||||
'expires': user.get('comment', '-')
|
||||
})
|
||||
|
||||
total_customers = len(customers)
|
||||
total_hotspot = len(hotspot_users)
|
||||
total_pppoe = len(ppp_secrets)
|
||||
|
||||
context = {
|
||||
'customers': customers,
|
||||
'total_customers': total_customers,
|
||||
'total_hotspot': total_hotspot,
|
||||
'total_pppoe': total_pppoe,
|
||||
}
|
||||
|
||||
return render(request, 'core/dashboard.html', context)
|
||||
|
||||
except Exception as e:
|
||||
# On error, redirect to login and show error
|
||||
return redirect('login')
|
||||
|
||||
def logout_view(request):
|
||||
logout(request)
|
||||
return redirect('login')
|
||||
|
||||
def create_pppoe_profile_view(request):
|
||||
if not request.session.get('router_ip'):
|
||||
return redirect('login')
|
||||
return render(request, 'core/create_pppoe_profile.html')
|
||||
|
||||
def input_pppoe_customer_view(request):
|
||||
if not request.session.get('router_ip'):
|
||||
return redirect('login')
|
||||
return render(request, 'core/input_customer.html')
|
||||
|
||||
def create_hotspot_voucher_view(request):
|
||||
if not request.session.get('router_ip'):
|
||||
return redirect('login')
|
||||
return render(request, 'core/create_hotspot_voucher.html')
|
||||
|
||||
def edit_route_view(request):
|
||||
if not request.session.get('router_ip'):
|
||||
return redirect('login')
|
||||
return render(request, 'core/edit_route.html')
|
||||
|
||||
def monthly_report_view(request):
|
||||
if not request.session.get('router_ip'):
|
||||
return redirect('login')
|
||||
return render(request, 'core/monthly_report.html')
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
routeros-api
|
||||
101
static/css/custom.css
Normal file
101
static/css/custom.css
Normal file
@ -0,0 +1,101 @@
|
||||
/* General Body Styling */
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
/* Login Page Styling */
|
||||
.login-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: #eef2f7;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-box h2 {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-box .form-control {
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.login-box .btn-primary {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-box .btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Dashboard Layout */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #1f2937;
|
||||
color: #ffffff;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 20px 20px 20px;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #d1d5db;
|
||||
padding: 12px 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active,
|
||||
.sidebar .nav-link:hover {
|
||||
color: #ffffff;
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 260px;
|
||||
width: calc(100% - 260px);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
@ -1,21 +1,101 @@
|
||||
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
/* General Body Styling */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
/* Login Page Styling */
|
||||
.login-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: #eef2f7;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-box h2 {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-box .form-control {
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.login-box .btn-primary {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-box .btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Dashboard Layout */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #1f2937;
|
||||
color: #ffffff;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 20px 20px 20px;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #d1d5db;
|
||||
padding: 12px 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active,
|
||||
.sidebar .nav-link:hover {
|
||||
color: #ffffff;
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 260px;
|
||||
width: calc(100% - 260px);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user