first commit

This commit is contained in:
2026-02-18 20:34:33 -03:00
parent 2f819da943
commit 60f0dcaac4
50 changed files with 8099 additions and 1471 deletions

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block content %}
<h1>⚙️ Admin Panel</h1>
<!-- USERS -->
<div class="card">
<h2>👤 Users</h2>
<p class="small">
Create, edit and manage system users and permissions.
</p>
<a href="{{ url_for('users.list_users') }}" class="btn">
Open User Management
</a>
</div>
<!-- KNOWLEDGE -->
<div class="card">
<h2>🔐 Knowledge Governance</h2>
<p class="small">
Invalidate outdated knowledge or manually add validated information to the RAG base.
</p>
<a href="{{ url_for('admin.invalidate_page') }}" class="btn">
Open Governance Tools
</a>
</div>
<div class="card">
<h2>♻️ Maintenance</h2>
<p class="small">
Reload all knowledge indexes, embeddings and caches without restarting the server.
</p>
<button class="btn" onclick="rebootSystem()">
Reload Knowledge
</button>
<pre id="rebootResult" style="margin-top:10px;"></pre>
</div>
<script>
async function rebootSystem() {
const box = document.getElementById("rebootResult");
box.textContent = "⏳ Reloading...";
const res = await fetch("/admin/reboot", {
method: "POST"
});
const data = await res.json();
box.textContent = "✅ " + data.message;
}
</script>
{% endblock %}

321
files/templates/base.html Normal file
View File

@@ -0,0 +1,321 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ORACLE RFP AI Platform</title>
<style>
/* =========================
GLOBAL THEME (Oracle light)
========================= */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
/* antes: gradiente dark */
background: #f5f6f7;
/* antes: texto claro */
color: #1f2937;
min-height: 100vh;
}
/* =========================
LINKS
========================= */
a {
color: #C74634; /* Oracle red */
text-decoration: none;
}
a:hover {
opacity: 0.85;
}
/* =========================
NAVBAR
========================= */
.navbar {
position: fixed; /* 🔥 cola no topo real */
top: 0;
left: 0;
width: 100%;
background: #E30613;
z-index: 9999;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.navbar-inner {
width: 100%;
/* 🔥 padding mínimo só vertical */
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: space-between;
}
/* texto branco */
.navbar a,
.nav-left {
color: white;
font-weight: 500;
}
.nav-links {
display: flex;
gap: 20px;
}
.nav-left {
font-weight: bold;
font-size: 18px;
/* destaque Oracle */
color: #FFFFFF;
}
.nav-links {
display: flex;
gap: 18px;
font-size: 14px;
}
/* =========================
LAYOUT
========================= */
.container {
max-width: 1400px;
margin: 90px auto 40px auto; /* 🔥 espaço para navbar */
padding: 0 24px;
}
/* =========================
CARD
========================= */
.card {
/* antes: #1e293b */
background: #ffffff;
border-radius: 14px;
padding: 24px;
/* antes sombra pesada */
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
border: 1px solid #e5e7eb;
margin-bottom: 24px;
}
/* =========================
TABLE
========================= */
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
text-align: left;
}
th {
color: #374151;
}
/* =========================
INPUTS
========================= */
input, select, textarea {
/* antes fundo escuro */
background: #ffffff;
border: 1px solid #e5e7eb;
color: #111;
padding: 10px;
border-radius: 8px;
width: 100%;
}
textarea {
resize: vertical;
}
/* =========================
BUTTONS
========================= */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: #F80000;
color: white;
padding: 10px 18px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 500;
font-size: 14px;
text-decoration: none;
line-height: 1;
gap: 6px;
}
.btn:hover {
opacity: 0.9;
}
.btn-success {
background: #16a34a;
}
.btn-danger {
background: #dc2626;
}
/* =========================
FLASH MESSAGES
========================= */
.flash {
padding: 10px;
border-radius: 8px;
margin-bottom: 14px;
}
.flash-success {
background: #e7f7ed;
color: #166534;
}
.flash-error {
background: #fee2e2;
color: #991b1b;
}
/* =========================
MOBILE
========================= */
@media (max-width: 768px) {
.container {
margin: 20px 12px;
}
table {
font-size: 12px;
}
}
.logo {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
font-size: 18px;
color: white; /* texto branco também */
}
.oracle-icon {
width: 22px;
height: 22px;
flex-shrink: 0;
}
</style>
</head>
<body>
<!-- =========================
NAVBAR
========================= -->
<div class="navbar">
<div class="navbar-inner">
<div class="nav-left logo">
<img
src="{{ url_for('static', filename='oracle.webp') }}"
class="oracle-icon"
alt="Oracle"
/>
<span>ORACLE RFP AI Platform</span>
</div>
{% if request.endpoint != 'auth.login_page' %}
<div class="nav-links">
<a href="/">Chat</a>
{% if current_user and current_user.role == "admin" %}
<a href="/admin">Admin</a>
{% endif %}
{% if current_user %}
<a href="/logout">Logout</a>
{% else %}
<a href="/login">Login</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- =========================
PAGE CONTENT
========================= -->
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash flash-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block content %}
<style>
.job-card {
max-width: 520px;
margin: 80px auto;
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #eee;
border-top: 4px solid #E30613;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
</style>
<div class="card job-card">
<h2>Excel Processing</h2>
<p>Job ID: <b>{{ job_id }}</b></p>
<div id="status-area">
<div class="spinner"></div>
<p>Processing...</p>
</div>
<div id="download-area" style="display:none">
<a id="download-btn" class="btn btn-success">Download Result</a>
</div>
<div id="error-area" style="display:none; color:#dc2626">
<p><b>Error occurred</b></p>
<pre id="error-detail"></pre>
<a href="/job/{{ job_id }}/logs" target="_blank">View logs</a>
</div>
</div>
<script>
const jobId = "{{ job_id }}";
async function checkStatus() {
const r = await fetch(`/job/${jobId}/status`);
const s = await r.json();
if (s.status === "DONE") {
document.getElementById("status-area").style.display = "none";
document.getElementById("download-area").style.display = "block";
document.getElementById("download-btn").href =
`/download/${jobId}`;
return;
}
if (s.status === "ERROR") {
document.getElementById("status-area").style.display = "none";
document.getElementById("error-area").style.display = "block";
document.getElementById("error-detail").innerText =
s.detail || "Unknown error";
return;
}
setTimeout(checkStatus, 2000);
}
checkStatus();
</script>
{% endblock %}

1023
files/templates/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,203 @@
{% extends "base.html" %}
{% block content %}
<h1>🔐 RAG Knowledge Governance</h1>
<!-- ===================== -->
<!-- 🔎 INVALIDATE SEARCH -->
<!-- ===================== -->
<section style="margin-bottom: 40px;">
<h2>❌ Invalidate Knowledge</h2>
<form method="post" action="/admin/search">
<textarea name="statement" rows="4" style="width:100%;"
placeholder="Paste the invalid or outdated statement here">{{ statement }}</textarea>
<br><br>
<button type="submit">Search occurrences</button>
</form>
</section>
<hr>
<!-- ===================== -->
<!-- MANUAL KNOWLEDGE -->
<!-- ===================== -->
<section style="margin-bottom: 40px;">
<h2> Add Manual Knowledge</h2>
<form id="manualForm">
<textarea id="manualText" rows="8" style="width:100%;"
placeholder="Paste validated Oracle technical information here..."></textarea>
<br><br>
<input type="text" id="reason" style="width:100%;"
placeholder="Reason / justification (e.g. official Oracle clarification)"/>
<br><br>
<button type="button" onclick="submitManual()">Add Knowledge</button>
</form>
<pre id="manualResult" style="margin-top:15px;"></pre>
</section>
<hr>
<!-- ===================== -->
<!-- 📚 SEARCH RESULTS -->
<!-- ===================== -->
<section>
<h2>📚 Knowledge Matches</h2>
{% if results|length == 0 %}
<p><i>No matching knowledge found.</i></p>
{% endif %}
{% for r in results %}
<div style="border:1px solid #ccc; padding:15px; margin:15px 0; border-radius:6px;">
<strong>Chunk Hash:</strong>
<span data-hash-label>{{ r.chunk_hash or "—" }}</span><br>
<strong>Origin:</strong> {{ r.origin or "UNKNOWN" }}<br>
<strong>Created at:</strong> {{ r.created_at or "—" }}<br>
<strong>Status:</strong> {{ r.status }}<br>
<strong>Source:</strong> {{ r.source }}<br>
<div style="margin-top:10px;">
<strong>Content:</strong>
<pre style="
white-space: pre-wrap;
background:#f8f8f8;
padding:10px;
border-radius:4px;
border:1px solid #ddd;
max-height:400px;
overflow:auto;
">{{ r.text }}</pre>
<div style="margin-top:10px;">
<strong>Change to:</strong>
<textarea class="edit-box" data-hash="{{ r.chunk_hash }}"
style="
width:100%;
min-height:140px;
background:#f8f8f8;
padding:10px;
border-radius:4px;
border:1px solid #ddd;
font-family: monospace;
"
data-original="{{ r.text | e }}"
>{{ r.text }}</textarea>
</div>
</div>
{% if r.chunk_hash %}
<button
style="color:red;"
data-hash="{{ (r.chunk_hash or '') | string | e }}"
onclick="invalidateChunk(this)">
Invalidate
</button>
<button
style="color:blue; margin-left:10px;"
data-hash="{{ (r.chunk_hash or '') | string | e }}"
onclick="updateChunk(this)">
Change content
</button>
{% else %}
<p><i>Derived from Knowledge Graph (non-revocable)</i></p>
{% endif %}
</div>
{% endfor %}
</section>
<script>
async function submitManual() {
const text = document.getElementById("manualText").value;
const reason = document.getElementById("reason").value;
const res = await fetch("/admin/add-knowledge", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ text, reason })
});
const data = await res.json();
document.getElementById("manualResult").textContent = JSON.stringify(data, null, 2);
}
async function invalidateChunk(btn) {
const chunkHash = btn.dataset.hash;
console.log("chunkHash:", chunkHash, "type:", typeof chunkHash);
const res = await fetch("/admin/revoke", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({
chunk_hash: chunkHash,
reason: "Invalidated via admin interface"
})
});
const data = await res.json().catch(() => ({}));
console.log("server:", res.status, data);
if (res.ok) btn.closest("div").remove();
}
async function updateChunk(btn) {
const card = btn.closest("div");
const chunkHash = btn.dataset.hash;
const textarea = card.querySelector(`.edit-box[data-hash="${chunkHash}"]`);
const newText = textarea.value;
if (!chunkHash) return;
if (!confirm("Update this chunk content?")) return;
btn.disabled = true;
btn.textContent = "Saving...";
const res = await fetch("/admin/update-chunk", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({
chunk_hash: chunkHash,
text: newText
})
});
const data = await res.json().catch(() => ({}));
btn.disabled = false;
if (res.ok) {
const newHash = data.chunk_hash;
const hashLabel = card.querySelector("[data-hash-label]");
if (hashLabel && newHash) {
hashLabel.textContent = newHash;
}
btn.dataset.hash = newHash;
textarea.style.borderColor = "green";
setTimeout(() => {
btn.textContent = "Change content";
textarea.style.borderColor = "#ddd";
}, 1200);
} else {
alert("Failed to update");
btn.textContent = "Change content";
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<div class="card">
<h2>{{ "Edit User" if user else "New User" }}</h2>
<form method="post">
<input name="name" placeholder="Name" value="{{ user.name if user else '' }}" required>
<input name="email" placeholder="Email" value="{{ user.email if user else '' }}" required>
<select name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<label>
<input type="checkbox" name="active" {% if user and user.active %}checked{% endif %}>
Active
</label>
<button class="btn btn-success">Save</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block content %}
<div class="card">
<h2>Users</h2>
<a class="btn btn-primary" href="{{ url_for('users.new_user') }}">+ New User</a>
<table class="table table-dark">
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Active</th>
<th></th>
</tr>
{% for u in users %}
<tr>
<td>{{ u.name }}</td>
<td>{{ u.email }}</td>
<td>{{ u.role }}</td>
<td>{{ "Yes" if u.active else "No" }}</td>
<td>
<a href="{{ url_for('users.edit_user', user_id=u.id) }}">Edit</a> |
<a href="{{ url_for('users.delete_user', user_id=u.id) }}">Delete</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block content %}
<style>
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 120px);
}
.login-card {
width: 380px;
background: #ffffff;
border-radius: 14px;
padding: 32px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
border: 1px solid #e5e7eb;
}
.login-title {
text-align: center;
font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
color: #111827;
}
.login-sub {
text-align: center;
font-size: 13px;
color: #6b7280;
margin-bottom: 22px;
}
.login-card input {
margin-bottom: 14px;
}
.login-btn {
width: 100%;
margin-top: 8px;
}
.login-footer {
text-align: center;
margin-top: 18px;
font-size: 12px;
color: #6b7280;
}
.oracle-accent {
height: 4px;
width: 100%;
background: #E30613;
border-radius: 8px 8px 0 0;
margin: -32px -32px 24px -32px;
}
</style>
<div class="login-wrapper">
<div class="login-card">
<!-- barra vermelha Oracle -->
<div class="oracle-accent"></div>
<div class="login-title">
Sign in
</div>
<div class="login-sub">
Oracle RFP AI Platform
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for msg in messages %}
<div class="flash flash-error">{{ msg }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="/login">
<input
type="text"
name="username"
placeholder="Email"
required
/>
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button class="btn login-btn">
Sign In
</button>
</form>
<div class="login-footer">
© Oracle RFP AI Platform
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<div class="card">
<h2>Set Password</h2>
{% if expired %}
<p>Link expired or invalid.</p>
{% else %}
<form method="post">
<input type="password" name="password" placeholder="New password" required>
<input type="password" name="password2" placeholder="Confirm password" required>
<button class="btn btn-primary">Save</button>
</form>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block content %}
<div class="card">
<h2>Create Access</h2>
<p class="small">
Enter your email to receive a secure link and set your password.
</p>
<form method="post">
<input
type="email"
name="email"
placeholder="your@email.com"
required
/>
<br><br>
<input
type="text"
name="name"
placeholder="name (optional)"
/>
<br><br>
<button type="submit">
Send password link
</button>
</form>
<hr>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="info-box">
{{ msg }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% endblock %}