Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,3 +0,0 @@
|
||||
"""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
|
||||
@ -1,282 +0,0 @@
|
||||
"""
|
||||
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.
@ -1,18 +0,0 @@
|
||||
{% 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>
|
||||
@ -1,14 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,8 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,8 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,62 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,24 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,8 +0,0 @@
|
||||
{% 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,36 +1,157 @@
|
||||
{% extends "base.html" %}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
{% block title %}KapiohoNet - Akses On-Demand{% endblock title %}
|
||||
<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 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>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
<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>
|
||||
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>
|
||||
<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>
|
||||
<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 %}
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,8 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,32 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,8 +0,0 @@
|
||||
{% 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,17 +1,7 @@
|
||||
from django.urls import path
|
||||
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
|
||||
)
|
||||
|
||||
from .views import home
|
||||
|
||||
urlpatterns = [
|
||||
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'),
|
||||
path("", home, name="home"),
|
||||
]
|
||||
|
||||
158
core/views.py
158
core/views.py
@ -1,135 +1,37 @@
|
||||
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
|
||||
import os
|
||||
import platform
|
||||
|
||||
# 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 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
|
||||
|
||||
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')
|
||||
from .forms import TicketForm
|
||||
from .models import Ticket
|
||||
|
||||
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)
|
||||
|
||||
return redirect('dashboard')
|
||||
except Exception as e:
|
||||
# Handle connection error
|
||||
return render(request, 'core/login.html', {'error': str(e)})
|
||||
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 render(request, 'core/login.html')
|
||||
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)
|
||||
|
||||
@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')
|
||||
|
||||
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')
|
||||
class TicketCreateView(CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = "core/ticket_create.html"
|
||||
success_url = reverse_lazy("home")
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
routeros-api
|
||||
@ -1,101 +0,0 @@
|
||||
/* 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,101 +1,21 @@
|
||||
/* General Body Styling */
|
||||
|
||||
: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);
|
||||
}
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
/* Login Page Styling */
|
||||
.login-container {
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: #eef2f7;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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