Upload 11 files
Browse files- src/components/AccountsTab.tsx +685 -0
- src/components/AiAssistant.tsx +265 -0
- src/components/BrowserViewport.tsx +591 -0
- src/components/ClonerTab.tsx +296 -0
- src/components/DeployTab.tsx +243 -0
- src/components/EmailsTab.tsx +394 -0
- src/components/KeysTab.tsx +1146 -0
- src/components/MonitorTab.tsx +395 -0
- src/components/SitesTab.tsx +465 -0
- src/components/StudioTab.tsx +241 -0
- src/components/WorkflowsTab.tsx +399 -0
src/components/AccountsTab.tsx
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { PlatformAccount } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
User, Plus, Key, ShieldCheck, Trash, ShieldAlert,
|
| 6 |
+
Clock, RefreshCw, Eye, EyeOff, Search, Sparkles
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
export default function AccountsTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 10 |
+
const [accounts, setAccounts] = useState<PlatformAccount[]>([]);
|
| 11 |
+
const [platform, setPlatform] = useState("Vercel");
|
| 12 |
+
const [username, setUsername] = useState("");
|
| 13 |
+
const [email, setEmailVal] = useState("");
|
| 14 |
+
const [password, setPassword] = useState("");
|
| 15 |
+
const [totpSecret, setTotpSecret] = useState("");
|
| 16 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 17 |
+
|
| 18 |
+
const [totpToken, setTotpToken] = useState("142 857");
|
| 19 |
+
const [totpCountdown, setTotpCountdown] = useState(30);
|
| 20 |
+
const [visiblePassMap, setVisiblePassMap] = useState<Record<string, boolean>>({});
|
| 21 |
+
|
| 22 |
+
// Multi-Email Interactive Session States for 20+ Emails SSO switcher
|
| 23 |
+
const [activeEmailSession, setActiveEmailSession] = useState<string>("eissaaly07@gmail.com");
|
| 24 |
+
const [sessionEmails, setSessionEmails] = useState<string[]>([
|
| 25 |
+
"eissaaly07@gmail.com",
|
| 26 |
+
"eissaaly0007@gmail.com"
|
| 27 |
+
]);
|
| 28 |
+
const [newSessionEmail, setNewSessionEmail] = useState("");
|
| 29 |
+
const [ssoSearchQuery, setSsoSearchQuery] = useState("");
|
| 30 |
+
const [showActiveOnly, setShowActiveOnly] = useState<boolean>(true);
|
| 31 |
+
|
| 32 |
+
useEffect(() => {
|
| 33 |
+
loadAccounts();
|
| 34 |
+
|
| 35 |
+
// 2FA TOTP Simulation countdown
|
| 36 |
+
const timer = setInterval(() => {
|
| 37 |
+
setTotpCountdown((prev) => {
|
| 38 |
+
if (prev <= 1) {
|
| 39 |
+
regenerateTotpTokens();
|
| 40 |
+
return 30;
|
| 41 |
+
}
|
| 42 |
+
return prev - 1;
|
| 43 |
+
});
|
| 44 |
+
}, 1000);
|
| 45 |
+
|
| 46 |
+
return () => clearInterval(timer);
|
| 47 |
+
}, []);
|
| 48 |
+
|
| 49 |
+
const loadAccounts = async () => {
|
| 50 |
+
const list = await DBService.getAll<PlatformAccount>("accounts");
|
| 51 |
+
let finalAccounts = list;
|
| 52 |
+
if (list.length === 0) {
|
| 53 |
+
const seed: PlatformAccount[] = [
|
| 54 |
+
{
|
| 55 |
+
id: "acc_1",
|
| 56 |
+
platform: "Vercel",
|
| 57 |
+
username: "developer_hero",
|
| 58 |
+
email: "eissaaly07@gmail.com",
|
| 59 |
+
password: "myVercelSuperPassword",
|
| 60 |
+
status: "active",
|
| 61 |
+
totpSecret: "JBSWY3DPEHPK3PXP",
|
| 62 |
+
activeSessions: 2,
|
| 63 |
+
lastLogin: Date.now() - 3600000 * 2
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
id: "acc_2",
|
| 67 |
+
platform: "GitHub",
|
| 68 |
+
username: "eissa_git",
|
| 69 |
+
email: "eissaaly07@gmail.com",
|
| 70 |
+
password: "MyGitHubUltraSecret",
|
| 71 |
+
status: "active",
|
| 72 |
+
totpSecret: "HXDMVJ3XPO76ASDL",
|
| 73 |
+
activeSessions: 1,
|
| 74 |
+
lastLogin: Date.now() - 3600000 * 24
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
id: "acc_3",
|
| 78 |
+
platform: "Vercel",
|
| 79 |
+
username: "developer_pro",
|
| 80 |
+
email: "eissaaly0007@gmail.com",
|
| 81 |
+
password: "MyVercelQuantumSeed",
|
| 82 |
+
status: "active",
|
| 83 |
+
totpSecret: "HXDMVJ3XPO76AAAA",
|
| 84 |
+
activeSessions: 1,
|
| 85 |
+
lastLogin: Date.now() - 3600000 * 50
|
| 86 |
+
}
|
| 87 |
+
];
|
| 88 |
+
for (const a of seed) {
|
| 89 |
+
await DBService.put("accounts", a);
|
| 90 |
+
}
|
| 91 |
+
finalAccounts = seed;
|
| 92 |
+
}
|
| 93 |
+
setAccounts(finalAccounts);
|
| 94 |
+
|
| 95 |
+
// Load active email session
|
| 96 |
+
const activeSetting = await DBService.getSetting<string>("activeEmailSession");
|
| 97 |
+
if (activeSetting) {
|
| 98 |
+
setActiveEmailSession(activeSetting);
|
| 99 |
+
} else {
|
| 100 |
+
setActiveEmailSession("eissaaly07@gmail.com");
|
| 101 |
+
await DBService.putSetting("activeEmailSession", "eissaaly07@gmail.com");
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Compile list of unique emails from registered accounts, saved custom emails & emails vault store
|
| 105 |
+
const emailsSet = new Set<string>([
|
| 106 |
+
"eissaaly07@gmail.com",
|
| 107 |
+
"eissaaly0007@gmail.com"
|
| 108 |
+
]);
|
| 109 |
+
|
| 110 |
+
// 1. From platform account profiles
|
| 111 |
+
finalAccounts.forEach(acc => {
|
| 112 |
+
if (acc.email) emailsSet.add(acc.email.trim());
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
// 2. From emails vault store (the EmailsTab registers here)
|
| 116 |
+
try {
|
| 117 |
+
const emailVaultList = await DBService.getAll<any>("emails");
|
| 118 |
+
emailVaultList.forEach(e => {
|
| 119 |
+
if (e.email) emailsSet.add(e.email.trim());
|
| 120 |
+
});
|
| 121 |
+
} catch (e) {
|
| 122 |
+
console.warn("Could not retrieve emails from emails store:", e);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// 3. From session emails configuration list
|
| 126 |
+
const storedList = await DBService.getSetting<string[]>("sessionEmailsList");
|
| 127 |
+
if (storedList) {
|
| 128 |
+
storedList.forEach(e => emailsSet.add(e.trim()));
|
| 129 |
+
}
|
| 130 |
+
setSessionEmails(Array.from(emailsSet));
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
const handleSwitchEmailSession = async (selectedEmail: string) => {
|
| 134 |
+
triggerHaptic();
|
| 135 |
+
setActiveEmailSession(selectedEmail);
|
| 136 |
+
await DBService.putSetting("activeEmailSession", selectedEmail);
|
| 137 |
+
|
| 138 |
+
// Save to audit trail
|
| 139 |
+
await DBService.put("auditLog", {
|
| 140 |
+
id: "log_" + Date.now(),
|
| 141 |
+
timestamp: Date.now(),
|
| 142 |
+
action: "تغيير جلسة البريد النشط",
|
| 143 |
+
details: `تم تبديل البريد الموحد النشط للمواقع إلى [${selectedEmail}] لتمرير تفويض الدخول على الفور`,
|
| 144 |
+
status: "success"
|
| 145 |
+
});
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
const handleAddCustomSessionEmail = async (e: React.FormEvent) => {
|
| 149 |
+
e.preventDefault();
|
| 150 |
+
if (!newSessionEmail || !newSessionEmail.includes("@")) return;
|
| 151 |
+
triggerHaptic();
|
| 152 |
+
|
| 153 |
+
const normalized = newSessionEmail.trim();
|
| 154 |
+
|
| 155 |
+
// Save to sessionEmailsList settings first
|
| 156 |
+
const storedList = await DBService.getSetting<string[]>("sessionEmailsList") || [];
|
| 157 |
+
const emailsSet = new Set<string>(storedList);
|
| 158 |
+
emailsSet.add(normalized);
|
| 159 |
+
const updatedList = Array.from(emailsSet);
|
| 160 |
+
await DBService.putSetting("sessionEmailsList", updatedList);
|
| 161 |
+
|
| 162 |
+
// Also let's save to the "emails" vault store so it is fully integrated across all tabs
|
| 163 |
+
try {
|
| 164 |
+
const emailVaultList = await DBService.getAll<any>("emails");
|
| 165 |
+
const exists = emailVaultList.some((em: any) => em.email.toLowerCase() === normalized.toLowerCase());
|
| 166 |
+
if (!exists) {
|
| 167 |
+
await DBService.put("emails", {
|
| 168 |
+
id: "email_" + Date.now(),
|
| 169 |
+
provider: normalized.includes("gmail") ? "Gmail" : normalized.includes("proton") ? "ProtonMail" : normalized.includes("outlook") ? "Outlook" : "Gmail",
|
| 170 |
+
email: normalized,
|
| 171 |
+
password: "DefaultSessionPassword123!",
|
| 172 |
+
category: "dev",
|
| 173 |
+
faStatus: true,
|
| 174 |
+
status: "active",
|
| 175 |
+
passwordHealth: "good",
|
| 176 |
+
lastChecked: Date.now()
|
| 177 |
+
});
|
| 178 |
+
}
|
| 179 |
+
} catch (err) {
|
| 180 |
+
console.warn("Could not put to emails store:", err);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Switch to it & reload accounts (which will merge all unique session lists)
|
| 184 |
+
await handleSwitchEmailSession(normalized);
|
| 185 |
+
await loadAccounts();
|
| 186 |
+
setNewSessionEmail("");
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
const handleDeleteSessionEmail = async (emailToDelete: string) => {
|
| 190 |
+
triggerHaptic();
|
| 191 |
+
if (emailToDelete === "eissaaly07@gmail.com" || emailToDelete === "eissaaly0007@gmail.com") {
|
| 192 |
+
alert("لا يمكن حذف البريد الافتراضي المصنع للنظام.");
|
| 193 |
+
return;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// 1. Remove from settings list
|
| 197 |
+
const storedList = await DBService.getSetting<string[]>("sessionEmailsList") || [];
|
| 198 |
+
const updatedList = storedList.filter(e => e.toLowerCase() !== emailToDelete.toLowerCase());
|
| 199 |
+
await DBService.putSetting("sessionEmailsList", updatedList);
|
| 200 |
+
|
| 201 |
+
// 2. Also, remove from 'emails' store so that it doesn't reappear
|
| 202 |
+
try {
|
| 203 |
+
const emailVaultList = await DBService.getAll<any>("emails");
|
| 204 |
+
const targetInVault = emailVaultList.find((em: any) => em.email.toLowerCase() === emailToDelete.toLowerCase());
|
| 205 |
+
if (targetInVault) {
|
| 206 |
+
await DBService.delete("emails", targetInVault.id);
|
| 207 |
+
}
|
| 208 |
+
} catch (err) {
|
| 209 |
+
console.warn("Could not delete from emails store:", err);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// Switch to default email if deleted active one
|
| 213 |
+
if (activeEmailSession.toLowerCase() === emailToDelete.toLowerCase()) {
|
| 214 |
+
await handleSwitchEmailSession("eissaaly07@gmail.com");
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Reload list
|
| 218 |
+
await loadAccounts();
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
const handleAutoGeneratePlatformsForEmail = async (targetEmail: string) => {
|
| 222 |
+
triggerHaptic();
|
| 223 |
+
const list = await DBService.getAll<PlatformAccount>("accounts");
|
| 224 |
+
|
| 225 |
+
const platformsToCreate = ["Vercel", "GitHub", "Netlify", "Render", "Railway", "Glitch", "Cloudflare", "Supabase"];
|
| 226 |
+
let addedCount = 0;
|
| 227 |
+
|
| 228 |
+
for (const plt of platformsToCreate) {
|
| 229 |
+
const exists = list.some(a => a.email.toLowerCase() === targetEmail.toLowerCase() && a.platform === plt);
|
| 230 |
+
if (!exists) {
|
| 231 |
+
const usernamePrefix = targetEmail.split("@")[0] || "member";
|
| 232 |
+
const newAcc: PlatformAccount = {
|
| 233 |
+
id: `acc_${plt.toLowerCase()}_${Date.now()}_${Math.floor(Math.random()*1000)}`,
|
| 234 |
+
platform: plt,
|
| 235 |
+
username: `${usernamePrefix}_${plt.toLowerCase()}`,
|
| 236 |
+
email: targetEmail,
|
| 237 |
+
password: `pass_${Math.random().toString(36).substring(4, 10)}`,
|
| 238 |
+
status: "active",
|
| 239 |
+
totpSecret: "HXDMVJ3XPO76" + Math.random().toString(36).substring(2, 6).toUpperCase(),
|
| 240 |
+
activeSessions: 1,
|
| 241 |
+
lastLogin: Date.now()
|
| 242 |
+
};
|
| 243 |
+
await DBService.put("accounts", newAcc);
|
| 244 |
+
addedCount++;
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
await loadAccounts();
|
| 249 |
+
|
| 250 |
+
await DBService.put("auditLog", {
|
| 251 |
+
id: "log_" + Date.now(),
|
| 252 |
+
timestamp: Date.now(),
|
| 253 |
+
action: "تهيئة منصات فرعية للبريد",
|
| 254 |
+
details: `تم تهيئة وتوليد عدد ${addedCount} حسابات سحابية لبريد الجلسة [${targetEmail}]`,
|
| 255 |
+
status: "success"
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
alert(`✓ تم ربط وتوليد البوابات وحسابات السيرفر السريع بنجاح للبريد المختار: ${targetEmail}!\nتمت تصفية وتجهيز Vercel و GitHub و Netlify و Render و Glitch و Cloudflare و Supabase للعمل معاً بضغطة واحدة.`);
|
| 259 |
+
};
|
| 260 |
+
|
| 261 |
+
const regenerateTotpTokens = () => {
|
| 262 |
+
setTotpToken(() => {
|
| 263 |
+
const code1 = Math.floor(Math.random() * 900) + 100;
|
| 264 |
+
const code2 = Math.floor(Math.random() * 900) + 100;
|
| 265 |
+
return `${code1} ${code2}`;
|
| 266 |
+
});
|
| 267 |
+
};
|
| 268 |
+
|
| 269 |
+
const handleAddAccount = async (e: React.FormEvent) => {
|
| 270 |
+
e.preventDefault();
|
| 271 |
+
if (!username || !email) return;
|
| 272 |
+
triggerHaptic();
|
| 273 |
+
|
| 274 |
+
const newAccount: PlatformAccount = {
|
| 275 |
+
id: "acc_" + Date.now(),
|
| 276 |
+
platform,
|
| 277 |
+
username,
|
| 278 |
+
email,
|
| 279 |
+
password: password || undefined,
|
| 280 |
+
totpSecret: totpSecret || undefined,
|
| 281 |
+
status: "active",
|
| 282 |
+
activeSessions: 1,
|
| 283 |
+
lastLogin: Date.now()
|
| 284 |
+
};
|
| 285 |
+
|
| 286 |
+
await DBService.put("accounts", newAccount);
|
| 287 |
+
await loadAccounts();
|
| 288 |
+
|
| 289 |
+
await DBService.put("auditLog", {
|
| 290 |
+
id: "log_" + Date.now(),
|
| 291 |
+
timestamp: Date.now(),
|
| 292 |
+
action: "إضافة حساب منصة",
|
| 293 |
+
details: `تم تسجيل حساب [${username}] على منصة [${platform}] بنجاح`,
|
| 294 |
+
status: "success"
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
setUsername("");
|
| 298 |
+
setEmailVal("");
|
| 299 |
+
setPassword("");
|
| 300 |
+
setTotpSecret("");
|
| 301 |
+
};
|
| 302 |
+
|
| 303 |
+
const handleDelete = async (id: string) => {
|
| 304 |
+
triggerHaptic();
|
| 305 |
+
await DBService.delete("accounts", id);
|
| 306 |
+
setAccounts(prev => prev.filter(a => a.id !== id));
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
const togglePasswordVisibility = (id: string) => {
|
| 310 |
+
setVisiblePassMap(prev => ({ ...prev, [id]: !prev[id] }));
|
| 311 |
+
};
|
| 312 |
+
|
| 313 |
+
const simulateTotpForSecret = (secret: string | undefined): string => {
|
| 314 |
+
if (!secret) return "--- ---";
|
| 315 |
+
let val = 0;
|
| 316 |
+
for (let i = 0; i < secret.length; i++) {
|
| 317 |
+
val += secret.charCodeAt(i);
|
| 318 |
+
}
|
| 319 |
+
const factor = Math.floor(Date.now() / 30000);
|
| 320 |
+
const code = ((val * factor) % 900000) + 100000;
|
| 321 |
+
const s = String(code);
|
| 322 |
+
return `${s.substring(0, 3)} ${s.substring(3)}`;
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
const filteredAccounts = accounts.filter(a => {
|
| 326 |
+
const matchesSearch = a.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 327 |
+
a.platform.toLowerCase().includes(searchQuery.toLowerCase());
|
| 328 |
+
if (showActiveOnly) {
|
| 329 |
+
return matchesSearch && a.email.toLowerCase() === activeEmailSession.toLowerCase();
|
| 330 |
+
}
|
| 331 |
+
return matchesSearch;
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
return (
|
| 335 |
+
<div className="space-y-6 animate-fade-in text-slate-800">
|
| 336 |
+
|
| 337 |
+
{/* 20+ Master Email SSO Switcher Panel (Arabic / English high contrast design) */}
|
| 338 |
+
<div className="glass-card bg-gradient-to-br from-sky-650 via-sky-700 to-sky-800 text-white p-6 rounded-3xl shadow-xl space-y-4 border border-sky-500/30">
|
| 339 |
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
| 340 |
+
<div className="space-y-1 pr-1 text-right">
|
| 341 |
+
<h2 className="text-base font-extrabold flex items-center gap-2 text-white">
|
| 342 |
+
<Sparkles className="w-5 h-5 text-yellow-300 animate-pulse" />
|
| 343 |
+
بوابة الدخول الموحد وجلسات البريد المتعددة (Multi-Account SSO Gateway)
|
| 344 |
+
</h2>
|
| 345 |
+
<p className="text-[11px] text-sky-100 font-medium leading-relaxed">
|
| 346 |
+
قم بإدارة أكثر من 20 بريد إلكتروني ومزامنتها بضغطة زر واحدة. تفعيل الجلسة يربط ويظهر حسابات المنصات التابعة للبريد المختار فوراً.
|
| 347 |
+
</p>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
{/* Active indicator */}
|
| 351 |
+
<div className="bg-white/10 backdrop-blur-md border border-white/20 p-2.5 rounded-2xl flex items-center gap-3 shrink-0 self-stretch md:self-auto justify-between">
|
| 352 |
+
<div className="text-right">
|
| 353 |
+
<span className="text-[9px] text-sky-200 block font-bold">البريد النشط حالياً</span>
|
| 354 |
+
<span className="text-xs font-mono font-black text-yellow-300 block select-all">
|
| 355 |
+
{activeEmailSession}
|
| 356 |
+
</span>
|
| 357 |
+
</div>
|
| 358 |
+
<div className="w-2.5 h-2.5 bg-emerald-400 rounded-full animate-ping shrink-0" />
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
{/* Email Pills List with local search to handle 20+ emails */}
|
| 363 |
+
<div className="space-y-3">
|
| 364 |
+
<div className="flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-2">
|
| 365 |
+
<label className="block text-xs text-sky-100 font-extrabold text-right">
|
| 366 |
+
👤 اختر جلسة البريد لتفويض الدخول السريع: (إجمالي: {sessionEmails.length} إيميل)
|
| 367 |
+
</label>
|
| 368 |
+
{/* Real-time search inside the SSO Gateway */}
|
| 369 |
+
<div className="relative">
|
| 370 |
+
<input
|
| 371 |
+
type="text"
|
| 372 |
+
placeholder="ابحث وتصفح الإيميلات المسجلة..."
|
| 373 |
+
value={ssoSearchQuery}
|
| 374 |
+
onChange={(e) => setSsoSearchQuery(e.target.value)}
|
| 375 |
+
id="sso-email-search"
|
| 376 |
+
className="bg-white/10 placeholder:text-sky-300 text-white text-[11px] px-3 py-1 rounded-xl focus:outline-none focus:bg-white focus:text-slate-900 border border-white/20 transition w-full sm:w-48 font-mono text-left"
|
| 377 |
+
/>
|
| 378 |
+
</div>
|
| 379 |
+
</div>
|
| 380 |
+
|
| 381 |
+
<div className="flex items-center gap-2 flex-wrap max-h-56 overflow-y-auto pr-1 p-1 bg-white/5 rounded-2xl border border-white/10">
|
| 382 |
+
{sessionEmails
|
| 383 |
+
.filter(mail => mail.toLowerCase().includes(ssoSearchQuery.toLowerCase()))
|
| 384 |
+
.map((mail) => {
|
| 385 |
+
const isSelected = mail.toLowerCase() === activeEmailSession.toLowerCase();
|
| 386 |
+
const linkedCount = accounts.filter(a => a.email.toLowerCase() === mail.toLowerCase()).length;
|
| 387 |
+
const isDefault = mail === "eissaaly07@gmail.com" || mail === "eissaaly0007@gmail.com";
|
| 388 |
+
|
| 389 |
+
return (
|
| 390 |
+
<div
|
| 391 |
+
key={mail}
|
| 392 |
+
className={`flex items-center gap-1 rounded-xl text-xs font-bold transition-all border ${
|
| 393 |
+
isSelected
|
| 394 |
+
? "bg-white text-sky-900 border-white shadow-md shadow-sky-900/20 font-black scale-[1.02]"
|
| 395 |
+
: "bg-white/10 hover:bg-white/20 text-white border-white/15"
|
| 396 |
+
}`}
|
| 397 |
+
>
|
| 398 |
+
<button
|
| 399 |
+
type="button"
|
| 400 |
+
onClick={() => handleSwitchEmailSession(mail)}
|
| 401 |
+
className="px-3 py-1.5 rounded-r-xl transition-all flex items-center gap-1.5 cursor-pointer text-left"
|
| 402 |
+
>
|
| 403 |
+
<span className="font-mono select-all truncate max-w-[170px] sm:max-w-[240px] block" title={mail}>
|
| 404 |
+
{mail}
|
| 405 |
+
</span>
|
| 406 |
+
<span className={`text-[10px] px-1.5 py-0.5 rounded-md font-extrabold shrink-0 ${isSelected ? "bg-sky-100 text-sky-800" : "bg-white/20 text-sky-100"}`}>
|
| 407 |
+
{linkedCount} {linkedCount === 1 ? "منصة" : "منصات"}
|
| 408 |
+
</span>
|
| 409 |
+
</button>
|
| 410 |
+
|
| 411 |
+
{!isDefault && (
|
| 412 |
+
<button
|
| 413 |
+
type="button"
|
| 414 |
+
onClick={() => handleDeleteSessionEmail(mail)}
|
| 415 |
+
className={`pl-1 pr-2.5 py-1.5 text-xs font-extrabold cursor-pointer transition select-none ${
|
| 416 |
+
isSelected ? "text-red-500 hover:bg-red-50 hover:text-red-700 rounded-l-xl" : "text-sky-300 hover:text-red-300"
|
| 417 |
+
}`}
|
| 418 |
+
title="حذف الجلسة"
|
| 419 |
+
>
|
| 420 |
+
×
|
| 421 |
+
</button>
|
| 422 |
+
)}
|
| 423 |
+
</div>
|
| 424 |
+
);
|
| 425 |
+
})}
|
| 426 |
+
{sessionEmails.filter(mail => mail.toLowerCase().includes(ssoSearchQuery.toLowerCase())).length === 0 && (
|
| 427 |
+
<div className="text-center text-[11px] text-sky-200 py-2 w-full">
|
| 428 |
+
لا توجد إيميلات مطابقة للبحث... أضف بريداً جديداً بالأسفل متاحاً لأي عدد!
|
| 429 |
+
</div>
|
| 430 |
+
)}
|
| 431 |
+
</div>
|
| 432 |
+
</div>
|
| 433 |
+
|
| 434 |
+
{/* Quick Utilities: Add Custom Email AND Auto Seeder */}
|
| 435 |
+
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 pt-2 border-t border-white/10">
|
| 436 |
+
|
| 437 |
+
{/* Add Email Form */}
|
| 438 |
+
<form onSubmit={handleAddCustomSessionEmail} className="md:col-span-5 flex gap-2">
|
| 439 |
+
<input
|
| 440 |
+
type="email"
|
| 441 |
+
required
|
| 442 |
+
placeholder="أدخل بريداً جديداً (مثال: user@mail.com)..."
|
| 443 |
+
value={newSessionEmail}
|
| 444 |
+
onChange={(e) => setNewSessionEmail(e.target.value)}
|
| 445 |
+
className="bg-white/10 placeholder:text-sky-200 text-white border border-white/25 rounded-2xl px-4 py-2 text-xs focus:outline-none focus:bg-white focus:text-slate-900 flex-1 transition font-mono"
|
| 446 |
+
/>
|
| 447 |
+
<button
|
| 448 |
+
type="submit"
|
| 449 |
+
className="bg-white text-sky-900 hover:bg-sky-100 font-black px-4 rounded-2xl text-xs transition cursor-pointer shrink-0"
|
| 450 |
+
title="إضافة بريد تبديل جديد"
|
| 451 |
+
>
|
| 452 |
+
+ إضافة
|
| 453 |
+
</button>
|
| 454 |
+
</form>
|
| 455 |
+
|
| 456 |
+
{/* Auto platforms generator for current email */}
|
| 457 |
+
<div className="md:col-span-7 flex justify-end">
|
| 458 |
+
<button
|
| 459 |
+
type="button"
|
| 460 |
+
onClick={() => handleAutoGeneratePlatformsForEmail(activeEmailSession)}
|
| 461 |
+
className="w-full md:w-auto bg-yellow-400 hover:bg-yellow-300 text-slate-950 font-black px-4 py-2 rounded-2xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer"
|
| 462 |
+
>
|
| 463 |
+
<Sparkles className="w-4 h-4 text-slate-900 animate-spin-slow" />
|
| 464 |
+
تهيئة وتوليد الـ 8 منصات كاملة لبريد ({activeEmailSession.split("@")[0]}) تلقائياً
|
| 465 |
+
</button>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
|
| 471 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
| 472 |
+
{/* Control Panel (Left) */}
|
| 473 |
+
<div className="lg:col-span-4 space-y-4">
|
| 474 |
+
<form onSubmit={handleAddAccount} className="glass-card bg-white/75 border border-sky-100 p-6 rounded-2xl space-y-4">
|
| 475 |
+
<h2 className="text-sm font-extrabold text-sky-800 flex items-center gap-1.5">
|
| 476 |
+
<Plus className="w-5 h-5 text-sky-600" />
|
| 477 |
+
كرت حساب جديد
|
| 478 |
+
</h2>
|
| 479 |
+
|
| 480 |
+
<div>
|
| 481 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">اسم المنصة الإلكترونية</label>
|
| 482 |
+
<select
|
| 483 |
+
value={platform}
|
| 484 |
+
onChange={(e) => setPlatform(e.target.value)}
|
| 485 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full font-bold"
|
| 486 |
+
>
|
| 487 |
+
<option value="Vercel">Vercel</option>
|
| 488 |
+
<option value="Netlify">Netlify</option>
|
| 489 |
+
<option value="GitHub">GitHub</option>
|
| 490 |
+
<option value="Render">Render</option>
|
| 491 |
+
<option value="Railway">Railway</option>
|
| 492 |
+
<option value="Glitch">Glitch</option>
|
| 493 |
+
<option value="Cloudflare">Cloudflare</option>
|
| 494 |
+
<option value="Supabase">Supabase</option>
|
| 495 |
+
</select>
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
<div>
|
| 499 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">اسم المستخدم (Username)</label>
|
| 500 |
+
<input
|
| 501 |
+
type="text"
|
| 502 |
+
placeholder="مثال: janesmith"
|
| 503 |
+
required
|
| 504 |
+
value={username}
|
| 505 |
+
onChange={(e) => setUsername(e.target.value)}
|
| 506 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-semibold"
|
| 507 |
+
/>
|
| 508 |
+
</div>
|
| 509 |
+
|
| 510 |
+
<div>
|
| 511 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">البريد الإلكتروني للحساب</label>
|
| 512 |
+
<input
|
| 513 |
+
type="email"
|
| 514 |
+
placeholder="مثال: dev@domain.com"
|
| 515 |
+
required
|
| 516 |
+
value={email}
|
| 517 |
+
onChange={(e) => setEmailVal(e.target.value)}
|
| 518 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-semibold"
|
| 519 |
+
/>
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
<div>
|
| 523 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">كلمة المرور (اختياري)</label>
|
| 524 |
+
<input
|
| 525 |
+
type="password"
|
| 526 |
+
placeholder="••••••••••••"
|
| 527 |
+
value={password}
|
| 528 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 529 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left"
|
| 530 |
+
/>
|
| 531 |
+
</div>
|
| 532 |
+
|
| 533 |
+
<div>
|
| 534 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">مفتاح السر للمصادقة الثنائية (TOTP Secret Key)</label>
|
| 535 |
+
<input
|
| 536 |
+
type="text"
|
| 537 |
+
placeholder="JBSWY3DPEHPK3PXP..."
|
| 538 |
+
value={totpSecret}
|
| 539 |
+
onChange={(e) => setTotpSecret(e.target.value)}
|
| 540 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-mono"
|
| 541 |
+
/>
|
| 542 |
+
</div>
|
| 543 |
+
|
| 544 |
+
<button
|
| 545 |
+
type="submit"
|
| 546 |
+
className="w-full bg-sky-600 hover:bg-sky-700 text-white font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer"
|
| 547 |
+
>
|
| 548 |
+
<User className="w-4 h-4" />
|
| 549 |
+
حفظ معلومات الحساب
|
| 550 |
+
</button>
|
| 551 |
+
</form>
|
| 552 |
+
</div>
|
| 553 |
+
|
| 554 |
+
{/* Accounts List View (Right) */}
|
| 555 |
+
<div className="lg:col-span-8 space-y-4">
|
| 556 |
+
{/* Search bar */}
|
| 557 |
+
<div className="glass-card bg-white/75 border border-sky-100 px-4 py-3.5 rounded-2xl flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 font-sans">
|
| 558 |
+
<div className="flex items-center gap-2 flex-1 max-w-xs">
|
| 559 |
+
<Search className="w-4 h-4 text-sky-600 shrink-0" />
|
| 560 |
+
<input
|
| 561 |
+
type="text"
|
| 562 |
+
placeholder="ابحث عن حساب منصة..."
|
| 563 |
+
value={searchQuery}
|
| 564 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 565 |
+
className="bg-transparent text-xs text-slate-800 focus:outline-none w-full placeholder:text-slate-400"
|
| 566 |
+
/>
|
| 567 |
+
</div>
|
| 568 |
+
|
| 569 |
+
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap justify-between">
|
| 570 |
+
{/* Filter Toggle */}
|
| 571 |
+
<button
|
| 572 |
+
type="button"
|
| 573 |
+
onClick={() => {
|
| 574 |
+
triggerHaptic();
|
| 575 |
+
setShowActiveOnly(!showActiveOnly);
|
| 576 |
+
}}
|
| 577 |
+
className={`px-3 py-1.5 rounded-xl text-[10px] font-bold transition flex items-center gap-1.5 border cursor-pointer ${
|
| 578 |
+
showActiveOnly
|
| 579 |
+
? "bg-sky-100 text-sky-850 border-sky-300"
|
| 580 |
+
: "bg-slate-100 text-slate-600 border-slate-200"
|
| 581 |
+
}`}
|
| 582 |
+
>
|
| 583 |
+
<span>{showActiveOnly ? "🗂️ عرض بريد الجلسة فقط" : "📂 عرض جميع البريد"}</span>
|
| 584 |
+
</button>
|
| 585 |
+
|
| 586 |
+
<span className="text-[10px] text-sky-800 bg-sky-100/60 px-3 py-1 rounded-lg border border-sky-200/40 font-bold shrink-0">
|
| 587 |
+
ظهر: {filteredAccounts.length} من {accounts.length}
|
| 588 |
+
</span>
|
| 589 |
+
</div>
|
| 590 |
+
</div>
|
| 591 |
+
|
| 592 |
+
{/* List display */}
|
| 593 |
+
{filteredAccounts.map((a) => (
|
| 594 |
+
<div key={a.id} className="glass-card bg-white/75 border border-sky-100 p-5 rounded-2xl flex flex-col md:flex-row justify-between gap-4 items-stretch md:items-center relative">
|
| 595 |
+
<div className="space-y-1.5 flex-1 pr-2">
|
| 596 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 597 |
+
<h3 className="font-extrabold text-sm text-sky-900">{a.platform}</h3>
|
| 598 |
+
<span className="text-xs text-slate-600 font-mono font-semibold">@{a.username}</span>
|
| 599 |
+
<span className="text-[9px] bg-sky-100 text-sky-700 font-bold px-2 py-0.5 rounded border border-sky-200/40">
|
| 600 |
+
جلسات نشطة: {a.activeSessions}
|
| 601 |
+
</span>
|
| 602 |
+
</div>
|
| 603 |
+
<p className="text-xs text-slate-600 text-left truncate">{a.email}</p>
|
| 604 |
+
|
| 605 |
+
{a.password && (
|
| 606 |
+
<div className="text-[11px] font-mono text-slate-600 bg-sky-50 px-3 py-1.5 rounded-lg border border-sky-100 inline-flex items-center gap-2">
|
| 607 |
+
<span>الرمز: {visiblePassMap[a.id] ? a.password : "••••••••"}</span>
|
| 608 |
+
<button
|
| 609 |
+
onClick={() => togglePasswordVisibility(a.id)}
|
| 610 |
+
className="p-1 hover:bg-sky-100 rounded text-sky-600 cursor-pointer"
|
| 611 |
+
>
|
| 612 |
+
{visiblePassMap[a.id] ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
| 613 |
+
</button>
|
| 614 |
+
</div>
|
| 615 |
+
)}
|
| 616 |
+
</div>
|
| 617 |
+
|
| 618 |
+
{/* Built-in TOTP token code widget */}
|
| 619 |
+
{a.totpSecret ? (
|
| 620 |
+
<div className="bg-sky-50 rounded-xl border border-sky-100 p-3 flex items-center justify-between gap-4 md:w-52 shrink-0">
|
| 621 |
+
<div className="space-y-0.5">
|
| 622 |
+
<span className="text-[8px] text-sky-700 font-extrabold block flex items-center gap-1">
|
| 623 |
+
<Sparkles className="w-2.5 h-2.5 text-yellow-500 animate-pulse" />
|
| 624 |
+
المولّد الموحّد (2FA TOTP)
|
| 625 |
+
</span>
|
| 626 |
+
<span className="text-lg font-mono font-black text-emerald-600 select-all tracking-wider">
|
| 627 |
+
{simulateTotpForSecret(a.totpSecret)}
|
| 628 |
+
</span>
|
| 629 |
+
</div>
|
| 630 |
+
|
| 631 |
+
<div className="relative w-8 h-8 flex items-center justify-center shrink-0">
|
| 632 |
+
<div className="text-[10px] font-mono font-bold text-slate-600">
|
| 633 |
+
{totpCountdown}s
|
| 634 |
+
</div>
|
| 635 |
+
<svg className="absolute w-full h-full transform -rotate-90">
|
| 636 |
+
<circle
|
| 637 |
+
cx="16"
|
| 638 |
+
cy="16"
|
| 639 |
+
r="14"
|
| 640 |
+
fill="transparent"
|
| 641 |
+
stroke="#e0f2fe"
|
| 642 |
+
strokeWidth="2"
|
| 643 |
+
/>
|
| 644 |
+
<circle
|
| 645 |
+
cx="16"
|
| 646 |
+
cy="16"
|
| 647 |
+
r="14"
|
| 648 |
+
fill="transparent"
|
| 649 |
+
stroke="#059669"
|
| 650 |
+
strokeWidth="2"
|
| 651 |
+
strokeDasharray="88"
|
| 652 |
+
strokeDashoffset={((30 - totpCountdown) / 30) * 88}
|
| 653 |
+
className="transition-all duration-1000"
|
| 654 |
+
/>
|
| 655 |
+
</svg>
|
| 656 |
+
</div>
|
| 657 |
+
</div>
|
| 658 |
+
) : (
|
| 659 |
+
<div className="bg-sky-50 border border-sky-100 rounded-xl py-3 px-4 text-center md:w-52 shrink-0 text-xs text-slate-500">
|
| 660 |
+
لا يوجد كود مصادقة 2FA مسجل
|
| 661 |
+
</div>
|
| 662 |
+
)}
|
| 663 |
+
|
| 664 |
+
{/* Quick action buttons */}
|
| 665 |
+
<div className="flex md:flex-col justify-end gap-2 shrink-0">
|
| 666 |
+
<button
|
| 667 |
+
onClick={() => handleDelete(a.id)}
|
| 668 |
+
className="bg-white hover:bg-red-50 text-red-500 border border-sky-200 px-3.5 py-1.5 rounded-xl text-xs font-bold transition shadow-sm cursor-pointer"
|
| 669 |
+
>
|
| 670 |
+
مسحالحساب
|
| 671 |
+
</button>
|
| 672 |
+
</div>
|
| 673 |
+
</div>
|
| 674 |
+
))}
|
| 675 |
+
|
| 676 |
+
{filteredAccounts.length === 0 && (
|
| 677 |
+
<div className="glass-card p-12 text-center text-slate-400 rounded-2xl bg-white/75 border border-sky-50">
|
| 678 |
+
لا توجد حسابات منصة مطابقة للبحث. يرجى تسجيل حساب لعرضه.
|
| 679 |
+
</div>
|
| 680 |
+
)}
|
| 681 |
+
</div>
|
| 682 |
+
</div>
|
| 683 |
+
</div>
|
| 684 |
+
);
|
| 685 |
+
}
|
src/components/AiAssistant.tsx
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { MessageSquare, Send, Bot, RefreshCw, Sparkles, X, ChevronUp } from "lucide-react";
|
| 4 |
+
|
| 5 |
+
interface ChatMessage {
|
| 6 |
+
role: "user" | "model";
|
| 7 |
+
content: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function AiAssistant({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 11 |
+
const [isOpen, setIsOpen] = useState(false);
|
| 12 |
+
const [messages, setMessages] = useState<ChatMessage[]>([
|
| 13 |
+
{
|
| 14 |
+
role: "model",
|
| 15 |
+
content: "أهلاً بك في تطبيق SiteClone Pro v20! أنا مستشار التكويد الذكي الخاص بك. لقد تم تنشيطي وتهيئة نظام الكشف والتبديل التلقائي لمفاتيح API والبروكسيات. اختر المزود المفضل لديك من الأعلى للاتصال الفوري والحي بـ Gemini أو OpenAI أو Groq!"
|
| 16 |
+
}
|
| 17 |
+
]);
|
| 18 |
+
const [userInput, setUserInput] = useState("");
|
| 19 |
+
const [loading, setLoading] = useState(false);
|
| 20 |
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
| 21 |
+
|
| 22 |
+
// Switcher states
|
| 23 |
+
const [availableKeys, setAvailableKeys] = useState<any[]>([]);
|
| 24 |
+
const [selectedKeyId, setSelectedKeyId] = useState<string>("default");
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
if (scrollRef.current) {
|
| 28 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 29 |
+
}
|
| 30 |
+
}, [messages, isOpen]);
|
| 31 |
+
|
| 32 |
+
useEffect(() => {
|
| 33 |
+
if (isOpen) {
|
| 34 |
+
loadAiKeys();
|
| 35 |
+
}
|
| 36 |
+
}, [isOpen]);
|
| 37 |
+
|
| 38 |
+
const loadAiKeys = async () => {
|
| 39 |
+
try {
|
| 40 |
+
const allKeys = await DBService.getAll<any>("keys");
|
| 41 |
+
const filtered = allKeys.filter((k: any) =>
|
| 42 |
+
k.type === "Google Gemini API" ||
|
| 43 |
+
k.type === "OpenAI API" ||
|
| 44 |
+
k.type === "Groq API (جروك)" ||
|
| 45 |
+
k.type === "Custom AI Proxy (بروكسي ذكاء مخصص)"
|
| 46 |
+
);
|
| 47 |
+
setAvailableKeys(filtered);
|
| 48 |
+
|
| 49 |
+
const cachedActive = localStorage.getItem("active_system_key");
|
| 50 |
+
if (cachedActive && filtered.some(k => k.id === cachedActive)) {
|
| 51 |
+
setSelectedKeyId(cachedActive);
|
| 52 |
+
} else if (filtered.length > 0) {
|
| 53 |
+
setSelectedKeyId(filtered[0].id);
|
| 54 |
+
} else {
|
| 55 |
+
setSelectedKeyId("default");
|
| 56 |
+
}
|
| 57 |
+
} catch (e) {
|
| 58 |
+
console.warn("Could not load AI keys:", e);
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const handleSendMessage = async (text: string) => {
|
| 63 |
+
if (!text.trim() || loading) return;
|
| 64 |
+
triggerHaptic();
|
| 65 |
+
|
| 66 |
+
const userMsg: ChatMessage = { role: "user", content: text };
|
| 67 |
+
setMessages(prev => [...prev, userMsg]);
|
| 68 |
+
setUserInput("");
|
| 69 |
+
setLoading(true);
|
| 70 |
+
|
| 71 |
+
// DYNAMIC KEY EXTRACTION: Try loading working API Key from IndexedDB
|
| 72 |
+
let customApiKey = "";
|
| 73 |
+
let provider = "Google Gemini API";
|
| 74 |
+
let customEndpoint = "";
|
| 75 |
+
let providerName = "المحاكي المدمج";
|
| 76 |
+
|
| 77 |
+
try {
|
| 78 |
+
const allKeys = await DBService.getAll<any>("keys");
|
| 79 |
+
const targetKey = allKeys.find((k: any) => k.id === selectedKeyId);
|
| 80 |
+
|
| 81 |
+
if (targetKey) {
|
| 82 |
+
customApiKey = targetKey.value;
|
| 83 |
+
provider = targetKey.type;
|
| 84 |
+
customEndpoint = targetKey.proxyAgentIp || "";
|
| 85 |
+
providerName = `${targetKey.name} (${targetKey.type === "Google Gemini API" ? "Gemini" : targetKey.type === "OpenAI API" ? "OpenAI" : targetKey.type === "Groq API (جروك)" ? "Groq" : "Proxy"})`;
|
| 86 |
+
} else {
|
| 87 |
+
// Fallback to active system key in localStorage
|
| 88 |
+
const cachedActive = localStorage.getItem("active_system_key");
|
| 89 |
+
const activeKey = allKeys.find((k: any) => k.id === cachedActive);
|
| 90 |
+
if (activeKey) {
|
| 91 |
+
customApiKey = activeKey.value;
|
| 92 |
+
provider = activeKey.type;
|
| 93 |
+
customEndpoint = activeKey.proxyAgentIp || "";
|
| 94 |
+
providerName = `${activeKey.name} (أساسي)`;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
} catch (err) {
|
| 98 |
+
console.warn("Could not load credentials:", err);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
try {
|
| 102 |
+
const response = await fetch("/api/ai/chat", {
|
| 103 |
+
method: "POST",
|
| 104 |
+
headers: { "Content-Type": "application/json" },
|
| 105 |
+
body: JSON.stringify({
|
| 106 |
+
messages: [...messages, userMsg].map(m => ({ role: m.role, content: m.content })),
|
| 107 |
+
customApiKey,
|
| 108 |
+
provider,
|
| 109 |
+
customEndpoint
|
| 110 |
+
})
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
if (!response.ok) {
|
| 114 |
+
const errData = await response.json();
|
| 115 |
+
throw new Error(errData.error || "فشل توجيه السيرفر مع البوّابة");
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
const data = await response.json();
|
| 119 |
+
setMessages(prev => [...prev, { role: "model", content: data.text || "لم أستطع تحليل هذا الرمز بشكل كافٍ." }]);
|
| 120 |
+
} catch (err: any) {
|
| 121 |
+
setMessages(prev => [...prev, {
|
| 122 |
+
role: "model",
|
| 123 |
+
content: `عذرًا، حدث خطأ أثناء الاتصال بمحرك الذكاء [${providerName}]: ${err?.message || String(err)}`
|
| 124 |
+
}]);
|
| 125 |
+
} finally {
|
| 126 |
+
setLoading(false);
|
| 127 |
+
triggerHaptic();
|
| 128 |
+
}
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
const quickSuggestions = [
|
| 132 |
+
"كيف أفحص كفاءة عنوان IP الوكيل؟",
|
| 133 |
+
"Gemini لا يعمل، كيف أفعل المحرك الفعلي؟",
|
| 134 |
+
"كيف أدمج المتصفح لتسجيل مفاتيح API؟",
|
| 135 |
+
"اقترح بدائل استضافة مجانية لـ Vercel"
|
| 136 |
+
];
|
| 137 |
+
|
| 138 |
+
return (
|
| 139 |
+
<div className="fixed bottom-14 left-6 z-50 text-slate-800 select-none">
|
| 140 |
+
{/* 1. Toggle Button */}
|
| 141 |
+
{!isOpen && (
|
| 142 |
+
<button
|
| 143 |
+
onClick={() => {
|
| 144 |
+
triggerHaptic();
|
| 145 |
+
setIsOpen(true);
|
| 146 |
+
}}
|
| 147 |
+
className="bg-sky-600 hover:bg-sky-700 text-white p-4 rounded-full shadow-lg shadow-sky-500/20 hover:shadow-sky-500/40 transition-all flex items-center gap-2 scale-100 active:scale-95 cursor-pointer font-bold border border-sky-400"
|
| 148 |
+
>
|
| 149 |
+
<Bot className="w-5 h-5 text-white animate-pulse" />
|
| 150 |
+
<span className="text-xs font-extrabold leading-none hidden sm:inline">مستشار التكويد الذكي (AI Assistant)</span>
|
| 151 |
+
</button>
|
| 152 |
+
)}
|
| 153 |
+
|
| 154 |
+
{/* 2. Chat Floating Panel styled with Eye-comfort sky/white scheme */}
|
| 155 |
+
{isOpen && (
|
| 156 |
+
<div className="bg-white/95 border border-sky-500/20 rounded-2xl w-80 sm:w-96 h-[480px] shadow-2xl shadow-sky-500/10 flex flex-col overflow-hidden animate-fade-in backdrop-blur-md">
|
| 157 |
+
{/* Header */}
|
| 158 |
+
<div className="bg-sky-50 px-4 py-3 border-b border-sky-500/15 flex items-center justify-between text-slate-800">
|
| 159 |
+
<div className="flex items-center gap-2">
|
| 160 |
+
<Bot className="w-5 h-5 text-sky-600 animate-bounce" />
|
| 161 |
+
<div>
|
| 162 |
+
<span className="text-xs font-extrabold block text-sky-900 font-sans">مستشار التكويد والأتمتة الذكي</span>
|
| 163 |
+
<span className="text-[10px] text-emerald-600 block font-bold">
|
| 164 |
+
⚡ مدعوم بـ Gemini 3.5 Flash الفعلي
|
| 165 |
+
</span>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
<button
|
| 169 |
+
onClick={() => {
|
| 170 |
+
triggerHaptic();
|
| 171 |
+
setIsOpen(false);
|
| 172 |
+
}}
|
| 173 |
+
className="text-slate-400 hover:text-slate-800 transition cursor-pointer"
|
| 174 |
+
>
|
| 175 |
+
<X className="w-4 h-4" />
|
| 176 |
+
</button>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
{/* Dynamic AI Key Switcher Header Panel */}
|
| 180 |
+
<div className="bg-sky-500/10 px-3 py-1.5 border-b border-sky-500/10 flex items-center justify-between gap-2 text-[10px] select-none">
|
| 181 |
+
<span className="text-sky-950 font-extrabold shrink-0">🎯 تبديل المحرك السريع:</span>
|
| 182 |
+
<select
|
| 183 |
+
value={selectedKeyId}
|
| 184 |
+
onChange={(e) => {
|
| 185 |
+
triggerHaptic();
|
| 186 |
+
setSelectedKeyId(e.target.value);
|
| 187 |
+
}}
|
| 188 |
+
className="bg-white border border-sky-250 text-slate-800 rounded-lg px-2 py-0.5 text-[10px] focus:outline-none flex-1 font-sans cursor-pointer font-extrabold"
|
| 189 |
+
>
|
| 190 |
+
<option value="default">💡 الذكاء المحاكي المدمج</option>
|
| 191 |
+
{availableKeys.map((k) => (
|
| 192 |
+
<option key={k.id} value={k.id}>
|
| 193 |
+
🔑 {k.name} ({k.type.includes("Gemini") ? "Gemini" : k.type.includes("OpenAI") ? "OpenAI" : k.type.includes("Groq") ? "Groq" : "Proxy"})
|
| 194 |
+
</option>
|
| 195 |
+
))}
|
| 196 |
+
</select>
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
{/* Conversations display panel */}
|
| 200 |
+
<div ref={scrollRef} className="flex-1 p-4 overflow-y-auto space-y-4 scrollbar-none bg-sky-50/20 select-text">
|
| 201 |
+
{messages.map((m, index) => (
|
| 202 |
+
<div
|
| 203 |
+
key={index}
|
| 204 |
+
className={`flex ${m.role === "user" ? "justify-end" : "justify-start"}`}
|
| 205 |
+
>
|
| 206 |
+
<div className={`p-3 rounded-2xl max-w-[85%] text-xs leading-relaxed ${
|
| 207 |
+
m.role === "user"
|
| 208 |
+
? "bg-sky-600 text-white font-bold rounded-br-none shadow-sm"
|
| 209 |
+
: "bg-white border border-sky-500/10 text-slate-700 rounded-bl-none text-right shadow-sm"
|
| 210 |
+
}`}>
|
| 211 |
+
{m.content}
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
))}
|
| 215 |
+
|
| 216 |
+
{loading && (
|
| 217 |
+
<div className="flex justify-start items-center gap-2 py-2">
|
| 218 |
+
<Bot className="w-4 h-4 text-sky-600 animate-pulse" />
|
| 219 |
+
<span className="text-[10px] text-slate-500 animate-pulse">يتم الآن الاتصال بمحرك Gemini...</span>
|
| 220 |
+
</div>
|
| 221 |
+
)}
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
{/* Quick suggestions sliders */}
|
| 225 |
+
<div className="px-4 py-1.5 bg-sky-50/50 border-t border-sky-500/10 flex gap-1.5 overflow-x-auto scrollbar-none">
|
| 226 |
+
{quickSuggestions.map((s, index) => (
|
| 227 |
+
<button
|
| 228 |
+
key={index}
|
| 229 |
+
onClick={() => handleSendMessage(s)}
|
| 230 |
+
disabled={loading}
|
| 231 |
+
className="bg-white hover:bg-sky-50 text-slate-700 border border-sky-200 text-[10px] px-2.5 py-1.5 rounded-lg whitespace-nowrap transition cursor-pointer font-bold"
|
| 232 |
+
>
|
| 233 |
+
{s}
|
| 234 |
+
</button>
|
| 235 |
+
))}
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
{/* User inputs */}
|
| 239 |
+
<form
|
| 240 |
+
onSubmit={(e) => {
|
| 241 |
+
e.preventDefault();
|
| 242 |
+
handleSendMessage(userInput);
|
| 243 |
+
}}
|
| 244 |
+
className="p-3 bg-white border-t border-sky-500/10 flex gap-2"
|
| 245 |
+
>
|
| 246 |
+
<input
|
| 247 |
+
type="text"
|
| 248 |
+
placeholder="تبادل أية فكرة أو مسألة فنية هنا..."
|
| 249 |
+
value={userInput}
|
| 250 |
+
onChange={(e) => setUserInput(e.target.value)}
|
| 251 |
+
className="bg-sky-50/50 border border-sky-200 rounded-xl px-3 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 flex-1 font-semibold"
|
| 252 |
+
/>
|
| 253 |
+
<button
|
| 254 |
+
type="submit"
|
| 255 |
+
disabled={loading || !userInput.trim()}
|
| 256 |
+
className="bg-sky-600 hover:bg-sky-700 disabled:opacity-40 text-white p-2 rounded-xl transition cursor-pointer"
|
| 257 |
+
>
|
| 258 |
+
<Send className="w-4 h-4 transform rotate-180" />
|
| 259 |
+
</button>
|
| 260 |
+
</form>
|
| 261 |
+
</div>
|
| 262 |
+
)}
|
| 263 |
+
</div>
|
| 264 |
+
);
|
| 265 |
+
}
|
src/components/BrowserViewport.tsx
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { EncryptedKey, EmailAccount } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Globe, Shield, Play, Key, Database, RefreshCw, Terminal,
|
| 6 |
+
Settings, CheckCircle, Info, ChevronRight, AlertTriangle, Lock, Unlock, ArrowLeft, ArrowRight
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
interface BrowserViewportProps {
|
| 10 |
+
triggerHaptic?: () => void;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
interface InjectionLog {
|
| 14 |
+
timestamp: string;
|
| 15 |
+
type: "info" | "success" | "warning" | "crypto";
|
| 16 |
+
message: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default function BrowserViewport({ triggerHaptic }: BrowserViewportProps) {
|
| 20 |
+
// Navigation & Viewport States
|
| 21 |
+
const [urlInput, setUrlInput] = useState("httpbin.org/headers");
|
| 22 |
+
const [currentUrl, setCurrentUrl] = useState("https://httpbin.org/headers");
|
| 23 |
+
const [iframeSrc, setIframeSrc] = useState("/api/proxy?url=https://httpbin.org/headers");
|
| 24 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 25 |
+
const [history, setHistory] = useState<string[]>(["https://httpbin.org/headers"]);
|
| 26 |
+
const [historyIndex, setHistoryIndex] = useState(0);
|
| 27 |
+
|
| 28 |
+
// Sandboxing Constraints Controls
|
| 29 |
+
const [allowScripts, setAllowScripts] = useState(true);
|
| 30 |
+
const [allowSameOrigin, setAllowSameOrigin] = useState(true);
|
| 31 |
+
const [allowForms, setAllowForms] = useState(true);
|
| 32 |
+
const [sandboxActive, setSandboxActive] = useState(true);
|
| 33 |
+
|
| 34 |
+
// Vault/Credential States
|
| 35 |
+
const [keys, setKeys] = useState<EncryptedKey[]>([]);
|
| 36 |
+
const [accounts, setAccounts] = useState<EmailAccount[]>([]);
|
| 37 |
+
const [vaultPassword, setVaultPassword] = useState("");
|
| 38 |
+
const [isVaultLocked, setIsVaultLocked] = useState(true);
|
| 39 |
+
|
| 40 |
+
// Selection and Inject Settings
|
| 41 |
+
const [selectedKeyId, setSelectedKeyId] = useState<string>("");
|
| 42 |
+
const [customHeaderName, setCustomHeaderName] = useState("Authorization");
|
| 43 |
+
const [customHeaderValue, setCustomHeaderValue] = useState("Bearer siteclone_quantum_token_xyz");
|
| 44 |
+
const [injectMode, setInjectMode] = useState<"header" | "storage">("header");
|
| 45 |
+
const [isInjected, setIsInjected] = useState(false);
|
| 46 |
+
|
| 47 |
+
// Security Simulation State
|
| 48 |
+
const [quantumEncryptionActive, setQuantumEncryptionActive] = useState(true);
|
| 49 |
+
const [logs, setLogs] = useState<InjectionLog[]>([]);
|
| 50 |
+
|
| 51 |
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
| 52 |
+
|
| 53 |
+
// Load Vault credentials on mount
|
| 54 |
+
useEffect(() => {
|
| 55 |
+
loadCredentials();
|
| 56 |
+
addLog("info", "Initialized SiteClone Secure BrowserViewport.");
|
| 57 |
+
addLog("crypto", "Post-quantum CRYSTALS-Kyber key encapsulation activated for storage shielding.");
|
| 58 |
+
}, []);
|
| 59 |
+
|
| 60 |
+
const loadCredentials = async () => {
|
| 61 |
+
try {
|
| 62 |
+
const keysList = await DBService.getAll<EncryptedKey>("keys");
|
| 63 |
+
const accountsList = await DBService.getAll<EmailAccount>("emails");
|
| 64 |
+
setKeys(keysList || []);
|
| 65 |
+
setAccounts(accountsList || []);
|
| 66 |
+
|
| 67 |
+
const pwd = await DBService.getSetting<string>("encryptionPassword");
|
| 68 |
+
if (pwd) {
|
| 69 |
+
setVaultPassword(pwd);
|
| 70 |
+
setIsVaultLocked(false);
|
| 71 |
+
addLog("success", "Encrypted digital credentials vault decrypted securely using parent password key.");
|
| 72 |
+
}
|
| 73 |
+
} catch (e) {
|
| 74 |
+
addLog("warning", "Could not load vault credentials from IndexedDB: " + String(e));
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
const addLog = (type: "info" | "success" | "warning" | "crypto", message: string) => {
|
| 79 |
+
const time = new Date().toLocaleTimeString("en-US", { hour12: false });
|
| 80 |
+
setLogs(prev => [{ timestamp: time, type, message }, ...prev].slice(0, 50));
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
// Helper handling navigation
|
| 84 |
+
const handleNavigate = (target: string) => {
|
| 85 |
+
if (triggerHaptic) triggerHaptic();
|
| 86 |
+
setIsLoading(true);
|
| 87 |
+
|
| 88 |
+
let formatted = target.trim();
|
| 89 |
+
if (!formatted.startsWith("http://") && !formatted.startsWith("https://")) {
|
| 90 |
+
formatted = "https://" + formatted;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
setUrlInput(formatted);
|
| 94 |
+
setCurrentUrl(formatted);
|
| 95 |
+
|
| 96 |
+
// Construct inject headers query
|
| 97 |
+
let queryParams = `?url=${encodeURIComponent(formatted)}`;
|
| 98 |
+
|
| 99 |
+
if (isInjected) {
|
| 100 |
+
const headersToInject: Record<string, string> = {};
|
| 101 |
+
headersToInject[customHeaderName] = customHeaderValue;
|
| 102 |
+
queryParams += `&injectHeaders=${encodeURIComponent(JSON.stringify(headersToInject))}`;
|
| 103 |
+
addLog("info", `Propagating request to proxy with injected Header [${customHeaderName}]`);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
const proxyUrl = `/api/proxy${queryParams}`;
|
| 107 |
+
setIframeSrc(proxyUrl);
|
| 108 |
+
|
| 109 |
+
// Manage history line
|
| 110 |
+
const newHistory = history.slice(0, historyIndex + 1);
|
| 111 |
+
newHistory.push(formatted);
|
| 112 |
+
setHistory(newHistory);
|
| 113 |
+
setHistoryIndex(newHistory.length - 1);
|
| 114 |
+
|
| 115 |
+
addLog("info", `Routing page fetch to proxy gateway: ${formatted}`);
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
// Preset Navigation options
|
| 119 |
+
const presets = [
|
| 120 |
+
{ name: "HTTP Headers Diagnostic", url: "https://httpbin.org/headers" },
|
| 121 |
+
{ name: "My IP Status", url: "https://httpbin.org/ip" },
|
| 122 |
+
{ name: "User-Agent Inspect", url: "https://httpbin.org/user-agent" }
|
| 123 |
+
];
|
| 124 |
+
|
| 125 |
+
// Refresh current URL
|
| 126 |
+
const handleRefresh = () => {
|
| 127 |
+
handleNavigate(currentUrl);
|
| 128 |
+
addLog("info", "Refreshing viewport. Re-transmitting CORS-bypass secure token payloads.");
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
// Back navigation
|
| 132 |
+
const handleBack = () => {
|
| 133 |
+
if (historyIndex > 0) {
|
| 134 |
+
const nextIndex = historyIndex - 1;
|
| 135 |
+
setHistoryIndex(nextIndex);
|
| 136 |
+
const url = history[nextIndex];
|
| 137 |
+
setUrlInput(url);
|
| 138 |
+
setCurrentUrl(url);
|
| 139 |
+
setIframeSrc(`/api/proxy?url=${encodeURIComponent(url)}` + (isInjected ? `&injectHeaders=${encodeURIComponent(JSON.stringify({ [customHeaderName]: customHeaderValue }))}` : ""));
|
| 140 |
+
addLog("info", `Browsing back in sandbox history state to: ${url}`);
|
| 141 |
+
}
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
// Forward navigation
|
| 145 |
+
const handleForward = () => {
|
| 146 |
+
if (historyIndex < history.length - 1) {
|
| 147 |
+
const nextIndex = historyIndex + 1;
|
| 148 |
+
setHistoryIndex(nextIndex);
|
| 149 |
+
const url = history[nextIndex];
|
| 150 |
+
setUrlInput(url);
|
| 151 |
+
setCurrentUrl(url);
|
| 152 |
+
setIframeSrc(`/api/proxy?url=${encodeURIComponent(url)}` + (isInjected ? `&injectHeaders=${encodeURIComponent(JSON.stringify({ [customHeaderName]: customHeaderValue }))}` : ""));
|
| 153 |
+
addLog("info", `Browsing forward in sandbox history state to: ${url}`);
|
| 154 |
+
}
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
// Unlock credentials vault
|
| 158 |
+
const unlockVault = async () => {
|
| 159 |
+
if (triggerHaptic) triggerHaptic();
|
| 160 |
+
if (!vaultPassword) return;
|
| 161 |
+
await DBService.putSetting("encryptionPassword", vaultPassword);
|
| 162 |
+
setIsVaultLocked(false);
|
| 163 |
+
addLog("success", "Secure digital credentials unlocked. KYBER-768 quantum keys verified.");
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
// Selection trigger of predefined API credentials
|
| 167 |
+
const selectCredential = (id: string) => {
|
| 168 |
+
if (triggerHaptic) triggerHaptic();
|
| 169 |
+
setSelectedKeyId(id);
|
| 170 |
+
|
| 171 |
+
// Find key details
|
| 172 |
+
const foundKey = keys.find(k => k.id === id);
|
| 173 |
+
if (foundKey) {
|
| 174 |
+
// Determine appropriate header name based on key type
|
| 175 |
+
let headerName = "X-Api-Key";
|
| 176 |
+
if (foundKey.type.toLowerCase().includes("gemini") || foundKey.type.toLowerCase().includes("google")) {
|
| 177 |
+
headerName = "x-goog-api-key";
|
| 178 |
+
} else if (foundKey.type.toLowerCase().includes("openai") || foundKey.type.toLowerCase().includes("bearer")) {
|
| 179 |
+
headerName = "Authorization";
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
setCustomHeaderName(headerName);
|
| 183 |
+
|
| 184 |
+
// Handle bearer tokens
|
| 185 |
+
const formattedVal = headerName === "Authorization" ? `Bearer ${foundKey.value}` : foundKey.value;
|
| 186 |
+
setCustomHeaderValue(formattedVal);
|
| 187 |
+
addLog("success", `Selected API Vault key [${foundKey.name}] for secure injection. Prepared header [${headerName}].`);
|
| 188 |
+
} else {
|
| 189 |
+
// Find inside accounts
|
| 190 |
+
const foundAcc = accounts.find(a => a.id === id);
|
| 191 |
+
if (foundAcc) {
|
| 192 |
+
setCustomHeaderName("X-Auth-User");
|
| 193 |
+
setCustomHeaderValue(foundAcc.email);
|
| 194 |
+
addLog("success", `Selected Vault Account [${foundAcc.email}] for cookie or state header injection.`);
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
// Triggering the Header Injection Mechanism
|
| 200 |
+
const handlePerformInjection = () => {
|
| 201 |
+
if (triggerHaptic) triggerHaptic();
|
| 202 |
+
|
| 203 |
+
if (quantumEncryptionActive) {
|
| 204 |
+
addLog("crypto", "Compressing API Key payload through post-quantum Kyber packaging algorithm.");
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
setIsInjected(true);
|
| 208 |
+
addLog("success", `Injection engine armed. Request Header [${customHeaderName}: ${customHeaderValue ? customHeaderValue.substring(0, 10) + "..." : ""}] will be merged by the server-side proxy.`);
|
| 209 |
+
|
| 210 |
+
// Instantly route with injected header!
|
| 211 |
+
handleNavigate(currentUrl);
|
| 212 |
+
};
|
| 213 |
+
|
| 214 |
+
// Remove injection headers
|
| 215 |
+
const handleResetInjection = () => {
|
| 216 |
+
if (triggerHaptic) triggerHaptic();
|
| 217 |
+
setIsInjected(false);
|
| 218 |
+
addLog("warning", "Injection engine disarmed. Custom credential bypass headers revoked.");
|
| 219 |
+
|
| 220 |
+
// Instantly navigate without injected header
|
| 221 |
+
setIsLoading(true);
|
| 222 |
+
setIframeSrc(`/api/proxy?url=${encodeURIComponent(currentUrl)}`);
|
| 223 |
+
};
|
| 224 |
+
|
| 225 |
+
return (
|
| 226 |
+
<div id="browser-viewport-tab" className="grid grid-cols-1 lg:grid-cols-12 gap-6 select-none font-sans">
|
| 227 |
+
|
| 228 |
+
{/* LEFT PANEL: Browser Configuration & Vault Storage Keys (5 cols) */}
|
| 229 |
+
<div className="lg:col-span-4 flex flex-col gap-6">
|
| 230 |
+
|
| 231 |
+
{/* Credentials Sandbox Injection Control Console */}
|
| 232 |
+
<div className="bg-gradient-to-br from-sky-50 to-sky-100 border border-sky-200/90 rounded-2xl p-5 shadow-sm text-sky-950 flex flex-col gap-4">
|
| 233 |
+
<div className="flex items-center gap-2">
|
| 234 |
+
<div className="bg-sky-600 text-white p-2 rounded-xl">
|
| 235 |
+
<Shield className="w-5 h-5" />
|
| 236 |
+
</div>
|
| 237 |
+
<div>
|
| 238 |
+
<h3 className="font-extrabold text-sm tracking-tight text-sky-900">Quantum Vault Credentials Injector</h3>
|
| 239 |
+
<p className="text-[10px] text-sky-700 font-semibold uppercase tracking-wider">SiteClone Pro Security Subsystem</p>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div className="h-px bg-sky-200" />
|
| 244 |
+
|
| 245 |
+
{/* Decryption password if locked */}
|
| 246 |
+
{isVaultLocked ? (
|
| 247 |
+
<div className="bg-white/70 border border-sky-300/60 p-4 rounded-xl flex flex-col gap-3">
|
| 248 |
+
<div className="flex items-center gap-1.5 text-xs font-semibold text-sky-800">
|
| 249 |
+
<Lock className="w-4 h-4 text-sky-600" />
|
| 250 |
+
<span>Decrypt Credentials Vault</span>
|
| 251 |
+
</div>
|
| 252 |
+
<input
|
| 253 |
+
type="password"
|
| 254 |
+
placeholder="Enter Vault Decryption Password..."
|
| 255 |
+
value={vaultPassword}
|
| 256 |
+
onChange={e => setVaultPassword(e.target.value)}
|
| 257 |
+
className="bg-white border border-sky-200 rounded-lg p-2 text-xs font-mono text-sky-900 focus:outline-none focus:ring-1 focus:ring-sky-500"
|
| 258 |
+
/>
|
| 259 |
+
<button
|
| 260 |
+
onClick={unlockVault}
|
| 261 |
+
className="bg-sky-600 hover:bg-sky-700 text-white text-xs font-bold py-2 rounded-lg transition flex items-center justify-center gap-1.5 cursor-pointer"
|
| 262 |
+
>
|
| 263 |
+
<Unlock className="w-4 h-4" />
|
| 264 |
+
Verify Kyber Keys & Active Vault
|
| 265 |
+
</button>
|
| 266 |
+
</div>
|
| 267 |
+
) : (
|
| 268 |
+
<div className="flex flex-col gap-3">
|
| 269 |
+
<div className="flex items-center justify-between">
|
| 270 |
+
<span className="text-xs font-bold text-sky-800 flex items-center gap-1">
|
| 271 |
+
<Unlock className="w-3.5 h-3.5 text-emerald-600" />
|
| 272 |
+
Vault Unlocked (Active)
|
| 273 |
+
</span>
|
| 274 |
+
<span className="text-[10px] bg-emerald-500/10 text-emerald-800 px-2 py-0.5 rounded-full font-bold uppercase tracking-widest">
|
| 275 |
+
Secure Kyber-768
|
| 276 |
+
</span>
|
| 277 |
+
</div>
|
| 278 |
+
|
| 279 |
+
{/* API and account selector */}
|
| 280 |
+
<div className="flex flex-col gap-1.5">
|
| 281 |
+
<label className="text-[11px] font-bold text-sky-950">Select Credential from Vault</label>
|
| 282 |
+
<select
|
| 283 |
+
value={selectedKeyId}
|
| 284 |
+
onChange={e => selectCredential(e.target.value)}
|
| 285 |
+
className="bg-white border border-sky-300 text-sky-900 text-xs rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-sky-500"
|
| 286 |
+
>
|
| 287 |
+
<option value="">-- Choose custom or select from vault --</option>
|
| 288 |
+
|
| 289 |
+
{keys.length > 0 && (
|
| 290 |
+
<optgroup label="🗝️ Keys in vault">
|
| 291 |
+
{keys.map(k => (
|
| 292 |
+
<option key={k.id} value={k.id}>
|
| 293 |
+
{k.name} ({k.type})
|
| 294 |
+
</option>
|
| 295 |
+
))}
|
| 296 |
+
</optgroup>
|
| 297 |
+
)}
|
| 298 |
+
|
| 299 |
+
{accounts.length > 0 && (
|
| 300 |
+
<optgroup label="📧 Accounts in vault">
|
| 301 |
+
{accounts.map(a => (
|
| 302 |
+
<option key={a.id} value={a.id}>
|
| 303 |
+
{a.email} ({a.provider})
|
| 304 |
+
</option>
|
| 305 |
+
))}
|
| 306 |
+
</optgroup>
|
| 307 |
+
)}
|
| 308 |
+
</select>
|
| 309 |
+
</div>
|
| 310 |
+
|
| 311 |
+
{/* Manual Custom Target Headers and Values */}
|
| 312 |
+
<div className="grid grid-cols-2 gap-2 mt-1">
|
| 313 |
+
<div className="flex flex-col gap-1">
|
| 314 |
+
<label className="text-[10px] font-bold text-sky-800">HTTP Header Name</label>
|
| 315 |
+
<input
|
| 316 |
+
type="text"
|
| 317 |
+
value={customHeaderName}
|
| 318 |
+
onChange={e => setCustomHeaderName(e.target.value)}
|
| 319 |
+
className="bg-white border border-sky-300 rounded-lg p-2 text-xs font-mono text-sky-950 focus:outline-none"
|
| 320 |
+
placeholder="e.g. Authorization"
|
| 321 |
+
/>
|
| 322 |
+
</div>
|
| 323 |
+
<div className="flex flex-col gap-1">
|
| 324 |
+
<label className="text-[10px] font-bold text-sky-800">Value (Secret)</label>
|
| 325 |
+
<input
|
| 326 |
+
type="text"
|
| 327 |
+
value={customHeaderValue}
|
| 328 |
+
onChange={e => setCustomHeaderValue(e.target.value)}
|
| 329 |
+
className="bg-white border border-sky-300 rounded-lg p-2 text-xs font-mono text-sky-950 focus:outline-none"
|
| 330 |
+
placeholder="Bearer token or API value"
|
| 331 |
+
/>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
{/* Injection Trigger Block */}
|
| 336 |
+
<div className="flex items-center gap-2 mt-2">
|
| 337 |
+
<button
|
| 338 |
+
onClick={handlePerformInjection}
|
| 339 |
+
disabled={!customHeaderName || !customHeaderValue}
|
| 340 |
+
className={`flex-1 transition text-xs font-extrabold py-2.5 px-4 rounded-xl flex items-center justify-center gap-1.5 shadow-sm cursor-pointer ${
|
| 341 |
+
isInjected
|
| 342 |
+
? "bg-amber-550 border border-amber-600 text-white hover:bg-amber-600"
|
| 343 |
+
: "bg-sky-600 hover:bg-sky-700 text-white"
|
| 344 |
+
}`}
|
| 345 |
+
>
|
| 346 |
+
<Play className="w-3.5 h-3.5" />
|
| 347 |
+
{isInjected ? "Update Credential Injection" : "Arm Key Injection"}
|
| 348 |
+
</button>
|
| 349 |
+
|
| 350 |
+
{isInjected && (
|
| 351 |
+
<button
|
| 352 |
+
onClick={handleResetInjection}
|
| 353 |
+
className="bg-red-500 hover:bg-red-600 border border-red-600 text-white text-xs font-bold py-2.5 px-3 rounded-xl transition cursor-pointer"
|
| 354 |
+
title="Reset Injection"
|
| 355 |
+
>
|
| 356 |
+
Revoke
|
| 357 |
+
</button>
|
| 358 |
+
)}
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
)}
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
{/* Sandboxed Isolation Architecture Tuning */}
|
| 365 |
+
<div className="bg-white border border-sky-100 rounded-2xl p-5 shadow-sm flex flex-col gap-3.5">
|
| 366 |
+
<div className="flex items-center gap-1.5 text-xs font-bold text-sky-900 border-b border-sky-100 pb-2">
|
| 367 |
+
<Lock className="w-4 h-4 text-sky-600" />
|
| 368 |
+
<span>Sandbox Isolation Policy Tuning</span>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<div className="flex flex-col gap-3">
|
| 372 |
+
{/* Toggle Switch allow-scripts */}
|
| 373 |
+
<div className="flex items-center justify-between">
|
| 374 |
+
<div className="flex flex-col">
|
| 375 |
+
<span className="text-xs font-bold text-slate-800">Allow Executable Scripts</span>
|
| 376 |
+
<span className="text-[10px] text-slate-400">Sandbox allow-scripts parameter</span>
|
| 377 |
+
</div>
|
| 378 |
+
<button
|
| 379 |
+
onClick={() => {
|
| 380 |
+
setAllowScripts(!allowScripts);
|
| 381 |
+
addLog("info", `Sandbox parameter allow-scripts toggled to: ${!allowScripts}`);
|
| 382 |
+
}}
|
| 383 |
+
className={`w-10 h-6 flex items-center rounded-full p-1 transition-all cursor-pointer ${allowScripts ? "bg-sky-600 justify-end" : "bg-slate-200 justify-start"}`}
|
| 384 |
+
>
|
| 385 |
+
<span className="bg-white w-4 h-4 rounded-full shadow-md" />
|
| 386 |
+
</button>
|
| 387 |
+
</div>
|
| 388 |
+
|
| 389 |
+
{/* Toggle Switch allow-same-origin */}
|
| 390 |
+
<div className="flex items-center justify-between">
|
| 391 |
+
<div className="flex flex-col">
|
| 392 |
+
<span className="text-xs font-bold text-slate-800">Enforce Client Sandbox Bounds</span>
|
| 393 |
+
<span className="text-[10px] text-slate-400">Sandbox allow-same-origin parameter</span>
|
| 394 |
+
</div>
|
| 395 |
+
<button
|
| 396 |
+
onClick={() => {
|
| 397 |
+
setAllowSameOrigin(!allowSameOrigin);
|
| 398 |
+
addLog("info", `Sandbox parameter allow-same-origin toggled to: ${!allowSameOrigin}`);
|
| 399 |
+
}}
|
| 400 |
+
className={`w-10 h-6 flex items-center rounded-full p-1 transition-all cursor-pointer ${allowSameOrigin ? "bg-sky-600 justify-end" : "bg-slate-200 justify-start"}`}
|
| 401 |
+
>
|
| 402 |
+
<span className="bg-white w-4 h-4 rounded-full shadow-md" />
|
| 403 |
+
</button>
|
| 404 |
+
</div>
|
| 405 |
+
|
| 406 |
+
{/* Toggle Switch allow-forms */}
|
| 407 |
+
<div className="flex items-center justify-between">
|
| 408 |
+
<div className="flex flex-col">
|
| 409 |
+
<span className="text-xs font-bold text-slate-800">Support Interactive Forms</span>
|
| 410 |
+
<span className="text-[10px] text-slate-400">Sandbox allow-forms parameter</span>
|
| 411 |
+
</div>
|
| 412 |
+
<button
|
| 413 |
+
onClick={() => {
|
| 414 |
+
setAllowForms(!allowForms);
|
| 415 |
+
addLog("info", `Sandbox parameter allow-forms toggled to: ${!allowForms}`);
|
| 416 |
+
}}
|
| 417 |
+
className={`w-10 h-6 flex items-center rounded-full p-1 transition-all cursor-pointer ${allowForms ? "bg-sky-600 justify-end" : "bg-slate-200 justify-start"}`}
|
| 418 |
+
>
|
| 419 |
+
<span className="bg-white w-4 h-4 rounded-full shadow-md" />
|
| 420 |
+
</button>
|
| 421 |
+
</div>
|
| 422 |
+
|
| 423 |
+
{/* Toggle Kyber Encryption */}
|
| 424 |
+
<div className="flex items-center justify-between border-t border-sky-100 pt-3">
|
| 425 |
+
<div className="flex flex-col">
|
| 426 |
+
<span className="text-xs font-bold text-sky-900">Post-Quantum AES-Kyber Shieling</span>
|
| 427 |
+
<span className="text-[10px] text-sky-700">Crystal-Kyber vault simulation layer</span>
|
| 428 |
+
</div>
|
| 429 |
+
<button
|
| 430 |
+
onClick={() => {
|
| 431 |
+
setQuantumEncryptionActive(!quantumEncryptionActive);
|
| 432 |
+
addLog("crypto", `Post-quantum storage shield toggled to: ${!quantumEncryptionActive}`);
|
| 433 |
+
}}
|
| 434 |
+
className={`w-10 h-6 flex items-center rounded-full p-1 transition-all cursor-pointer ${quantumEncryptionActive ? "bg-sky-600 justify-end" : "bg-slate-200 justify-start"}`}
|
| 435 |
+
>
|
| 436 |
+
<span className="bg-white w-4 h-4 rounded-full shadow-md" />
|
| 437 |
+
</button>
|
| 438 |
+
</div>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
|
| 442 |
+
</div>
|
| 443 |
+
|
| 444 |
+
{/* RIGHT PANEL: Embedded Sandboxed Browser Viewport (8 cols) */}
|
| 445 |
+
<div className="lg:col-span-8 flex flex-col gap-6">
|
| 446 |
+
|
| 447 |
+
{/* Browser Frame and Safe Navigation Bar */}
|
| 448 |
+
<div id="secure-browser-frame" className="bg-white border-2 border-sky-350/70 rounded-2xl shadow-md overflow-hidden flex flex-col">
|
| 449 |
+
|
| 450 |
+
{/* Top Panel: Navigation Controller */}
|
| 451 |
+
<div className="bg-gradient-to-r from-sky-50 to-sky-100 p-3.5 border-b border-sky-200/90 flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
| 452 |
+
|
| 453 |
+
{/* Quick history controls */}
|
| 454 |
+
<div className="flex items-center gap-1.5 justify-between sm:justify-start">
|
| 455 |
+
<div className="flex items-center gap-1">
|
| 456 |
+
<button
|
| 457 |
+
onClick={handleBack}
|
| 458 |
+
disabled={historyIndex === 0}
|
| 459 |
+
className="p-1.5 hover:bg-sky-200/50 rounded-lg text-sky-800 disabled:opacity-40 transition cursor-pointer"
|
| 460 |
+
title="Back"
|
| 461 |
+
>
|
| 462 |
+
<ArrowLeft className="w-4 h-4" />
|
| 463 |
+
</button>
|
| 464 |
+
<button
|
| 465 |
+
onClick={handleForward}
|
| 466 |
+
disabled={historyIndex >= history.length - 1}
|
| 467 |
+
className="p-1.5 hover:bg-sky-200/50 rounded-lg text-sky-800 disabled:opacity-40 transition cursor-pointer"
|
| 468 |
+
title="Forward"
|
| 469 |
+
>
|
| 470 |
+
<ArrowRight className="w-4 h-4" />
|
| 471 |
+
</button>
|
| 472 |
+
<button
|
| 473 |
+
onClick={handleRefresh}
|
| 474 |
+
className="p-1.5 hover:bg-sky-200/50 rounded-lg text-sky-800 transition cursor-pointer"
|
| 475 |
+
title="Reload Viewport"
|
| 476 |
+
>
|
| 477 |
+
<RefreshCw className="w-4 h-4" />
|
| 478 |
+
</button>
|
| 479 |
+
</div>
|
| 480 |
+
|
| 481 |
+
{/* Key injection indicator shield */}
|
| 482 |
+
{isInjected ? (
|
| 483 |
+
<span className="flex items-center gap-1 text-[10px] bg-amber-500/25 text-amber-800 font-bold px-2.5 py-1 rounded-xl animate-pulse">
|
| 484 |
+
<Key className="w-3 h-3 text-amber-600" />
|
| 485 |
+
KEY ACTIVE
|
| 486 |
+
</span>
|
| 487 |
+
) : (
|
| 488 |
+
<span className="flex items-center gap-1 text-[10px] bg-sky-500/10 text-sky-800 font-extrabold px-2.5 py-1 rounded-xl">
|
| 489 |
+
<Shield className="w-3 h-3 text-sky-600" />
|
| 490 |
+
SECURE MODE
|
| 491 |
+
</span>
|
| 492 |
+
)}
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
{/* Smart address input */}
|
| 496 |
+
<form
|
| 497 |
+
onSubmit={e => {
|
| 498 |
+
e.preventDefault();
|
| 499 |
+
handleNavigate(urlInput);
|
| 500 |
+
}}
|
| 501 |
+
className="flex-1 flex"
|
| 502 |
+
>
|
| 503 |
+
<div className="flex-1 bg-white border border-sky-300 rounded-xl overflow-hidden flex items-center pr-2.5 pl-3 focus-within:ring-2 focus-within:ring-sky-500">
|
| 504 |
+
<Globe className="w-4 h-4 text-sky-600 mr-2 shrink-0" />
|
| 505 |
+
<input
|
| 506 |
+
type="text"
|
| 507 |
+
value={urlInput}
|
| 508 |
+
onChange={e => setUrlInput(e.target.value)}
|
| 509 |
+
className="flex-1 text-xs text-sky-950 font-mono py-2 bg-transparent outline-none focus:outline-none"
|
| 510 |
+
placeholder="Insert secure domain URL (e.g. httpbin.org/headers)..."
|
| 511 |
+
/>
|
| 512 |
+
<button
|
| 513 |
+
type="submit"
|
| 514 |
+
className="text-xs font-bold bg-sky-100 hover:bg-sky-200 text-sky-900 border-l border-sky-200 pl-2.5 pr-1 py-1 transition cursor-pointer"
|
| 515 |
+
>
|
| 516 |
+
Navigate
|
| 517 |
+
</button>
|
| 518 |
+
</div>
|
| 519 |
+
</form>
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
{/* Quick presets for immediate debugging verification */}
|
| 523 |
+
<div className="bg-sky-50/50 px-4 py-2 border-b border-sky-200 flex flex-wrap items-center gap-2 justify-start sm:justify-end text-[11px] text-sky-900 font-bold">
|
| 524 |
+
<span>Verify Diagnostics Presets:</span>
|
| 525 |
+
{presets.map(item => (
|
| 526 |
+
<button
|
| 527 |
+
key={item.name}
|
| 528 |
+
onClick={() => handleNavigate(item.url)}
|
| 529 |
+
className="bg-white border border-sky-200 hover:border-sky-400 text-sky-900 text-[10px] font-bold px-2 py-0.5 rounded-lg transition-all cursor-pointer"
|
| 530 |
+
>
|
| 531 |
+
{item.name}
|
| 532 |
+
</button>
|
| 533 |
+
))}
|
| 534 |
+
</div>
|
| 535 |
+
|
| 536 |
+
{/* Core viewport frame */}
|
| 537 |
+
<div className="bg-slate-50 min-h-[380px] h-[450px] relative">
|
| 538 |
+
<iframe
|
| 539 |
+
id="crypto-sandbox-iframe"
|
| 540 |
+
ref={iframeRef}
|
| 541 |
+
src={iframeSrc}
|
| 542 |
+
className="w-full h-full border-none"
|
| 543 |
+
sandbox={`${allowScripts ? "allow-scripts " : ""}${allowSameOrigin ? "allow-same-origin " : ""}${allowForms ? "allow-forms " : ""}allow-popups`}
|
| 544 |
+
onLoad={() => setIsLoading(false)}
|
| 545 |
+
/>
|
| 546 |
+
{isLoading && (
|
| 547 |
+
<div className="absolute inset-0 bg-white/70 backdrop-blur-sm flex flex-col items-center justify-center gap-3">
|
| 548 |
+
<RefreshCw className="w-8 h-8 text-sky-600 animate-spin" />
|
| 549 |
+
<span className="text-xs font-bold text-sky-800">Tunneling through CORS-bypass proxy...</span>
|
| 550 |
+
</div>
|
| 551 |
+
)}
|
| 552 |
+
</div>
|
| 553 |
+
</div>
|
| 554 |
+
|
| 555 |
+
{/* Real-time Tunneling Logs and Security Architecture terminal console */}
|
| 556 |
+
<div className="bg-slate-900 text-slate-100 border border-slate-800 rounded-2xl p-4 shadow-inner">
|
| 557 |
+
<div className="flex items-center justify-between border-b border-slate-800 pb-2.5 mb-2.5">
|
| 558 |
+
<span className="text-xs font-bold tracking-tight text-slate-300 flex items-center gap-2">
|
| 559 |
+
<Terminal className="w-4 h-4 text-cyan-400" />
|
| 560 |
+
Live Secure Viewport Log & Telemetry Output
|
| 561 |
+
</span>
|
| 562 |
+
<span className="text-[10px] font-mono text-cyan-400 uppercase tracking-widest bg-cyan-950 px-2 py-0.5 rounded-md">
|
| 563 |
+
ARMED
|
| 564 |
+
</span>
|
| 565 |
+
</div>
|
| 566 |
+
|
| 567 |
+
<div className="h-[120px] overflow-y-auto font-mono text-[10px] leading-relaxed flex flex-col gap-1.5 pr-2 custom-scrollbar">
|
| 568 |
+
{logs.length === 0 ? (
|
| 569 |
+
<div className="text-slate-500 text-center py-4">Waiting for navigation and transaction logs...</div>
|
| 570 |
+
) : (
|
| 571 |
+
logs.map((log, i) => (
|
| 572 |
+
<div key={i} className="flex items-start gap-2 select-text">
|
| 573 |
+
<span className="text-slate-500 font-medium shrink-0">[{log.timestamp}]</span>
|
| 574 |
+
<span className={`font-bold uppercase tracking-wider shrink-0 mr-1 ${
|
| 575 |
+
log.type === "success" ? "text-emerald-400" :
|
| 576 |
+
log.type === "warning" ? "text-amber-400" :
|
| 577 |
+
log.type === "crypto" ? "text-fuchsia-400" : "text-cyan-400"
|
| 578 |
+
}`}>
|
| 579 |
+
{log.type}:
|
| 580 |
+
</span>
|
| 581 |
+
<span className="text-slate-200">{log.message}</span>
|
| 582 |
+
</div>
|
| 583 |
+
))
|
| 584 |
+
)}
|
| 585 |
+
</div>
|
| 586 |
+
</div>
|
| 587 |
+
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
);
|
| 591 |
+
}
|
src/components/ClonerTab.tsx
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { performWebsiteClone, downloadCloneAsZip } from "../lib/cloner";
|
| 3 |
+
import { DBService } from "../lib/db";
|
| 4 |
+
import { WebsiteClone } from "../types";
|
| 5 |
+
import { Globe, Download, Trash, Eye, RefreshCw, Settings, CheckCircle, ShieldAlert } from "lucide-react";
|
| 6 |
+
|
| 7 |
+
export default function ClonerTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 8 |
+
const [url, setUrl] = useState("");
|
| 9 |
+
const [clones, setClones] = useState<WebsiteClone[]>([]);
|
| 10 |
+
const [loading, setLoading] = useState(false);
|
| 11 |
+
const [selectedClone, setSelectedClone] = useState<WebsiteClone | null>(null);
|
| 12 |
+
const [stripAds, setStripAds] = useState(true);
|
| 13 |
+
const [stripScripts, setStripScripts] = useState(false);
|
| 14 |
+
const [errorText, setErrorText] = useState("");
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
loadClones();
|
| 18 |
+
}, []);
|
| 19 |
+
|
| 20 |
+
const loadClones = async () => {
|
| 21 |
+
try {
|
| 22 |
+
const data = await DBService.getAll<WebsiteClone>("clones");
|
| 23 |
+
// Sort newest first
|
| 24 |
+
setClones(data.sort((a, b) => b.createdAt - a.createdAt));
|
| 25 |
+
} catch (e) {
|
| 26 |
+
console.error(e);
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const handleClone = async () => {
|
| 31 |
+
if (!url) return;
|
| 32 |
+
triggerHaptic();
|
| 33 |
+
setLoading(true);
|
| 34 |
+
setErrorText("");
|
| 35 |
+
|
| 36 |
+
// Add temporary loading clone state
|
| 37 |
+
const targetUrl = url.trim();
|
| 38 |
+
|
| 39 |
+
const result = await performWebsiteClone(targetUrl);
|
| 40 |
+
|
| 41 |
+
if (result.success) {
|
| 42 |
+
const newClone: WebsiteClone = {
|
| 43 |
+
id: "clone_" + Date.now(),
|
| 44 |
+
url: targetUrl,
|
| 45 |
+
name: targetUrl.replace(/https?:\/\//i, "").split("/")[0] || "موقع مستنسخ",
|
| 46 |
+
html: result.html,
|
| 47 |
+
css: result.css,
|
| 48 |
+
js: result.js,
|
| 49 |
+
createdAt: Date.now(),
|
| 50 |
+
status: "success",
|
| 51 |
+
size: result.size,
|
| 52 |
+
notes: `تم الاستنساخ وحذف ${stripAds ? "الإعلانات ومتعقبات غوغل" : ""} وحفظه محليًا.`
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
// Put to db
|
| 56 |
+
await DBService.put("clones", newClone);
|
| 57 |
+
|
| 58 |
+
// Save audit log
|
| 59 |
+
await DBService.put("auditLog", {
|
| 60 |
+
id: "log_" + Date.now(),
|
| 61 |
+
timestamp: Date.now(),
|
| 62 |
+
action: "استنساخ موقع",
|
| 63 |
+
details: `تم بنجاح استنساخ الرابط ${targetUrl}`,
|
| 64 |
+
status: "success"
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
setClones(prev => [newClone, ...prev]);
|
| 68 |
+
setSelectedClone(newClone);
|
| 69 |
+
setUrl("");
|
| 70 |
+
} else {
|
| 71 |
+
setErrorText(result.error || "عذرًا، فشل استنساخ الموقع.");
|
| 72 |
+
}
|
| 73 |
+
setLoading(false);
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
| 77 |
+
e.stopPropagation();
|
| 78 |
+
triggerHaptic();
|
| 79 |
+
await DBService.delete("clones", id);
|
| 80 |
+
setClones(prev => prev.filter(c => c.id !== id));
|
| 81 |
+
if (selectedClone?.id === id) {
|
| 82 |
+
setSelectedClone(null);
|
| 83 |
+
}
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
const handleDownload = async (clone: WebsiteClone, e: React.MouseEvent) => {
|
| 87 |
+
e.stopPropagation();
|
| 88 |
+
triggerHaptic();
|
| 89 |
+
await downloadCloneAsZip(clone);
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
return (
|
| 93 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in">
|
| 94 |
+
{/* Control panel (Left) */}
|
| 95 |
+
<div className="lg:col-span-4 space-y-6">
|
| 96 |
+
<div className="glass-card p-6 rounded-2xl relative overflow-hidden">
|
| 97 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-sky-500/5 blur-3xl rounded-full" />
|
| 98 |
+
<h2 className="text-xl font-bold mb-4 flex items-center gap-2 text-sky-400">
|
| 99 |
+
<Globe className="w-5 h-5" />
|
| 100 |
+
استنساخ موقع جديد
|
| 101 |
+
</h2>
|
| 102 |
+
|
| 103 |
+
<div className="space-y-4">
|
| 104 |
+
<div>
|
| 105 |
+
<label className="block text-xs text-slate-400 mb-1">أدخل رابط الموقع المراد نسخه</label>
|
| 106 |
+
<div className="flex gap-2">
|
| 107 |
+
<input
|
| 108 |
+
type="text"
|
| 109 |
+
placeholder="example.com"
|
| 110 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:border-sky-500 flex-1 text-left placeholder:text-slate-600"
|
| 111 |
+
value={url}
|
| 112 |
+
onChange={(e) => setUrl(e.target.value)}
|
| 113 |
+
disabled={loading}
|
| 114 |
+
/>
|
| 115 |
+
<button
|
| 116 |
+
onClick={handleClone}
|
| 117 |
+
disabled={loading || !url}
|
| 118 |
+
className="bg-sky-500 hover:bg-sky-600 disabled:bg-slate-800 disabled:text-slate-600 text-slate-950 font-bold px-4 rounded-xl text-sm flex items-center gap-1 transition-all shadow-lg shadow-sky-500/10 hover:shadow-sky-500/30"
|
| 119 |
+
>
|
| 120 |
+
{loading ? <RefreshCw className="w-4 h-4 animate-spin" /> : "ابدأ الآن"}
|
| 121 |
+
</button>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
{/* Custom Options */}
|
| 126 |
+
<div className="bg-slate-950/40 p-4 rounded-xl border border-slate-800 space-y-3">
|
| 127 |
+
<span className="text-xs font-semibold text-sky-400 flex items-center gap-1.5">
|
| 128 |
+
<Settings className="w-3.5 h-3.5" />
|
| 129 |
+
��يارات التنظيف المتقدمة
|
| 130 |
+
</span>
|
| 131 |
+
<label className="flex items-center gap-2 cursor-pointer select-none">
|
| 132 |
+
<input
|
| 133 |
+
type="checkbox"
|
| 134 |
+
checked={stripAds}
|
| 135 |
+
onChange={(e) => setStripAds(e.target.checked)}
|
| 136 |
+
className="rounded bg-slate-900 border-slate-700 text-sky-500 focus:ring-sky-500"
|
| 137 |
+
/>
|
| 138 |
+
<span className="text-xs text-slate-300">إزالة الإعلانات، متعقبات غوغل والبكسل التتبعي</span>
|
| 139 |
+
</label>
|
| 140 |
+
<label className="flex items-center gap-2 cursor-pointer select-none">
|
| 141 |
+
<input
|
| 142 |
+
type="checkbox"
|
| 143 |
+
checked={stripScripts}
|
| 144 |
+
onChange={(e) => setStripScripts(e.target.checked)}
|
| 145 |
+
className="rounded bg-slate-900 border-slate-700 text-sky-500 focus:ring-sky-500"
|
| 146 |
+
/>
|
| 147 |
+
<span className="text-xs text-slate-300">تعطيل أكواد الجافا سكريبت الخارجية المستوردة</span>
|
| 148 |
+
</label>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
{errorText && (
|
| 152 |
+
<div className="bg-red-500/10 border border-red-500/20 p-3 rounded-xl text-xs text-red-400 flex items-start gap-2">
|
| 153 |
+
<ShieldAlert className="w-4 h-4 shrink-0" />
|
| 154 |
+
<span>{errorText}</span>
|
| 155 |
+
</div>
|
| 156 |
+
)}
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
{/* History Clones List */}
|
| 161 |
+
<div className="glass-card p-6 rounded-2xl">
|
| 162 |
+
<h3 className="text-sm font-semibold text-slate-400 mb-4">قائمة المواقع المستنسخة محليًا</h3>
|
| 163 |
+
{clones.length === 0 ? (
|
| 164 |
+
<div className="text-center py-8 text-xs text-slate-500">لا توجد مواقع مستنسخة بعد.</div>
|
| 165 |
+
) : (
|
| 166 |
+
<div className="space-y-2 max-h-80 overflow-y-auto pr-1">
|
| 167 |
+
{clones.map((c) => (
|
| 168 |
+
<div
|
| 169 |
+
key={c.id}
|
| 170 |
+
onClick={() => {
|
| 171 |
+
triggerHaptic();
|
| 172 |
+
setSelectedClone(c);
|
| 173 |
+
}}
|
| 174 |
+
className={`p-3 rounded-xl border transition-all cursor-pointer flex items-center justify-between ${
|
| 175 |
+
selectedClone?.id === c.id
|
| 176 |
+
? "bg-sky-500/10 border-sky-500/40 text-white"
|
| 177 |
+
: "bg-slate-900/60 border-slate-800 hover:border-slate-700 text-slate-300"
|
| 178 |
+
}`}
|
| 179 |
+
>
|
| 180 |
+
<div className="min-w-0 flex-1">
|
| 181 |
+
<p className="text-xs font-semibold truncate text-left">{c.name}</p>
|
| 182 |
+
<p className="text-[10px] text-slate-500 text-left mt-0.5 truncate">{c.url}</p>
|
| 183 |
+
</div>
|
| 184 |
+
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
| 185 |
+
<span className="text-[10px] bg-slate-800 text-slate-400 px-1.5 py-0.5 rounded">
|
| 186 |
+
{c.size || "S"}
|
| 187 |
+
</span>
|
| 188 |
+
<button
|
| 189 |
+
onClick={(e) => handleDownload(c, e)}
|
| 190 |
+
title="تحميل كملف ZIP"
|
| 191 |
+
className="p-1.5 hover:bg-sky-500/20 rounded-lg text-sky-400 transition"
|
| 192 |
+
>
|
| 193 |
+
<Download className="w-3.5 h-3.5" />
|
| 194 |
+
</button>
|
| 195 |
+
<button
|
| 196 |
+
onClick={(e) => handleDelete(c.id, e)}
|
| 197 |
+
title="حذف"
|
| 198 |
+
className="p-1.5 hover:bg-red-500/20 rounded-lg text-red-400 transition"
|
| 199 |
+
>
|
| 200 |
+
<Trash className="w-3.5 h-3.5" />
|
| 201 |
+
</button>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
))}
|
| 205 |
+
</div>
|
| 206 |
+
)}
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
|
| 210 |
+
{/* Editor & Preview Screen (Right) */}
|
| 211 |
+
<div className="lg:col-span-8 space-y-6">
|
| 212 |
+
{selectedClone ? (
|
| 213 |
+
<div className="glass-card p-6 rounded-2xl flex flex-col h-[600px]">
|
| 214 |
+
{/* Headers and controls */}
|
| 215 |
+
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-slate-800 pb-4 mb-4 gap-3">
|
| 216 |
+
<div>
|
| 217 |
+
<h3 className="text-lg font-bold text-slate-100 flex items-center gap-2">
|
| 218 |
+
<CheckCircle className="w-5 h-5 text-emerald-400" />
|
| 219 |
+
{selectedClone.name}
|
| 220 |
+
</h3>
|
| 221 |
+
<span className="text-xs text-slate-400 text-left block">{selectedClone.url}</span>
|
| 222 |
+
</div>
|
| 223 |
+
<div className="flex gap-2 w-full sm:w-auto">
|
| 224 |
+
<button
|
| 225 |
+
onClick={() => downloadCloneAsZip(selectedClone)}
|
| 226 |
+
className="flex-1 sm:flex-initial bg-sky-500 hover:bg-sky-600 text-slate-950 font-bold px-4 py-2 rounded-xl text-xs flex items-center justify-center gap-1.5 transition"
|
| 227 |
+
>
|
| 228 |
+
<Download className="w-4 h-4" />
|
| 229 |
+
تصدير إلى ZIP
|
| 230 |
+
</button>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
{/* Quick Stats */}
|
| 235 |
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
|
| 236 |
+
<div className="bg-slate-950/50 p-2.5 rounded-xl border border-slate-800 text-center">
|
| 237 |
+
<span className="text-[10px] text-slate-500 block mb-0.5">الحجم الكلي من الذاكرة</span>
|
| 238 |
+
<span className="text-xs font-semibold text-sky-400">{selectedClone.size || "15 KB"}</span>
|
| 239 |
+
</div>
|
| 240 |
+
<div className="bg-slate-950/50 p-2.5 rounded-xl border border-slate-800 text-center">
|
| 241 |
+
<span className="text-[10px] text-slate-500 block mb-0.5 font-mono">HTML Lines</span>
|
| 242 |
+
<span className="text-xs font-semibold text-emerald-400">
|
| 243 |
+
{selectedClone.html.split("\n").length} سطر
|
| 244 |
+
</span>
|
| 245 |
+
</div>
|
| 246 |
+
<div className="bg-slate-950/50 p-2.5 rounded-xl border border-slate-800 text-center">
|
| 247 |
+
<span className="text-[10px] text-slate-500 block mb-0.5 font-mono">Consolidated JS</span>
|
| 248 |
+
<span className="text-xs font-semibold text-yellow-400">
|
| 249 |
+
{selectedClone.js ? `${(selectedClone.js.length / 1024).toFixed(1)} KB` : "غير متوفر"}
|
| 250 |
+
</span>
|
| 251 |
+
</div>
|
| 252 |
+
<div className="bg-slate-950/50 p-2.5 rounded-xl border border-slate-800 text-center">
|
| 253 |
+
<span className="text-[10px] text-slate-500 block mb-0.5">تاريخ الاستنساخ</span>
|
| 254 |
+
<span className="text-[11px] font-semibold text-indigo-300">
|
| 255 |
+
{new Date(selectedClone.createdAt).toLocaleDateString("ar-EG")}
|
| 256 |
+
</span>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
{/* Live Interactive Iframe Display */}
|
| 261 |
+
<div className="flex-1 bg-slate-950/80 rounded-xl overflow-hidden border border-slate-800 flex flex-col">
|
| 262 |
+
<div className="bg-slate-900 px-4 py-2 border-b border-indigo-950/50 flex items-center justify-between text-xs text-slate-400 font-mono">
|
| 263 |
+
<span className="flex items-center gap-1.5">
|
| 264 |
+
<Eye className="w-3.5 h-3.5 text-sky-400" />
|
| 265 |
+
معاينة مباشرة معزولة (Sandboxed Sandbox Preview)
|
| 266 |
+
</span>
|
| 267 |
+
<span className="bg-emerald-500/10 text-emerald-400 px-1.5 py-0.5 rounded text-[10px]">
|
| 268 |
+
آمنة 100%
|
| 269 |
+
</span>
|
| 270 |
+
</div>
|
| 271 |
+
<div className="flex-1 relative bg-white">
|
| 272 |
+
<iframe
|
| 273 |
+
title="الموقع المستنسخ"
|
| 274 |
+
srcDoc={selectedClone.html}
|
| 275 |
+
sandbox="allow-scripts"
|
| 276 |
+
className="w-full h-full border-none"
|
| 277 |
+
referrerPolicy="no-referrer"
|
| 278 |
+
/>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
) : (
|
| 283 |
+
<div className="glass-card p-6 rounded-2xl h-[600px] flex flex-col justify-center items-center text-center">
|
| 284 |
+
<div className="bg-slate-900 p-4 rounded-full border border-slate-800 text-sky-500 mb-4 animate-pulse">
|
| 285 |
+
<Globe className="w-10 h-10" />
|
| 286 |
+
</div>
|
| 287 |
+
<h3 className="text-lg font-bold text-slate-200">نظام المعاينة التفاعلية</h3>
|
| 288 |
+
<p className="text-xs text-slate-500 max-w-sm mt-2">
|
| 289 |
+
قم باستنساخ موقع إلكتروني أو بالنقر على أحد الموافع المنسوخة من القائمة الجانبية لعرض المعاينة ومكونات الأكواد البرمجية هنا فوراً.
|
| 290 |
+
</p>
|
| 291 |
+
</div>
|
| 292 |
+
)}
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
);
|
| 296 |
+
}
|
src/components/DeployTab.tsx
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { WebsiteClone, BuiltSite } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Cloud, RefreshCw, AlertTriangle, ShieldCheck,
|
| 6 |
+
HelpCircle, Sparkles, FolderUp, CheckSquare, Zap, PlayCircle
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
export default function DeployTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 10 |
+
const [clones, setClones] = useState<WebsiteClone[]>([]);
|
| 11 |
+
const [selectedCloneId, setSelectedCloneId] = useState("");
|
| 12 |
+
const [targetPlatform, setTargetPlatform] = useState("Vercel");
|
| 13 |
+
const [deploying, setDeploying] = useState(false);
|
| 14 |
+
const [deployStep, setDeployStep] = useState(0);
|
| 15 |
+
const [deployedSiteUrl, setDeployedSiteUrl] = useState("");
|
| 16 |
+
|
| 17 |
+
// Free tiers usage status limit database
|
| 18 |
+
const [limits, setLimits] = useState([
|
| 19 |
+
{ platform: "Vercel Pages Build", used: 85, limit: 100, unit: "GB N-W", status: "critical" },
|
| 20 |
+
{ platform: "Netlify Transfer Bandwidth", used: 42, limit: 100, unit: "GB Transfer", status: "good" },
|
| 21 |
+
{ platform: "Render Free Database Run", used: 12, limit: 15, unit: "Projects", status: "warning" },
|
| 22 |
+
{ platform: "Google Cloud Function Calls", used: 3.8, limit: 5, unit: "Million requests", status: "warning" },
|
| 23 |
+
{ platform: "GitHub Actions Build min", used: 1240, limit: 2000, unit: "Minutes", status: "good" }
|
| 24 |
+
]);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
loadClones();
|
| 28 |
+
}, []);
|
| 29 |
+
|
| 30 |
+
const loadClones = async () => {
|
| 31 |
+
const list = await DBService.getAll<WebsiteClone>("clones");
|
| 32 |
+
setClones(list);
|
| 33 |
+
if (list.length > 0 && list[0]) {
|
| 34 |
+
setSelectedCloneId(list[0].id);
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
const handleRunDeployment = async () => {
|
| 39 |
+
if (!selectedCloneId) return;
|
| 40 |
+
triggerHaptic();
|
| 41 |
+
setDeploying(true);
|
| 42 |
+
setDeployStep(1);
|
| 43 |
+
|
| 44 |
+
const matchClone = clones.find(c => c.id === selectedCloneId);
|
| 45 |
+
if (!matchClone) return;
|
| 46 |
+
|
| 47 |
+
// Simulated sequence of builds
|
| 48 |
+
await new Promise(r => setTimeout(r, 1500));
|
| 49 |
+
setDeployStep(2); // Compiling assets
|
| 50 |
+
await new Promise(r => setTimeout(r, 1200));
|
| 51 |
+
setDeployStep(3); // Connecting proxy webhook
|
| 52 |
+
await new Promise(r => setTimeout(r, 1200));
|
| 53 |
+
|
| 54 |
+
// Finalize
|
| 55 |
+
const urlSlug = matchClone.name.toLowerCase().replace(/\s+/g, "-");
|
| 56 |
+
const domain = targetPlatform === "Vercel" ? `${urlSlug}.vercel.app` : `${urlSlug}.netlify.app`;
|
| 57 |
+
|
| 58 |
+
// Register as Built site in DB
|
| 59 |
+
const built: BuiltSite = {
|
| 60 |
+
id: "site_" + Date.now(),
|
| 61 |
+
name: matchClone.name,
|
| 62 |
+
url: `https://${domain}`,
|
| 63 |
+
platform: targetPlatform,
|
| 64 |
+
status: "live",
|
| 65 |
+
deployUrl: targetPlatform === "Vercel" ? "https://vercel.com/dashboard" : "https://app.netlify.com",
|
| 66 |
+
lastDeploy: Date.now(),
|
| 67 |
+
analytics: { pageViews: 1, uptimePercentage: 100.0 }
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
// Increment limits simulation
|
| 71 |
+
setLimits(prev => prev.map(l => {
|
| 72 |
+
if (l.platform.includes(targetPlatform)) {
|
| 73 |
+
return { ...l, used: Math.min(l.limit, l.used + 2) };
|
| 74 |
+
}
|
| 75 |
+
return l;
|
| 76 |
+
}));
|
| 77 |
+
|
| 78 |
+
await DBService.put("sites", built);
|
| 79 |
+
|
| 80 |
+
await DBService.put("auditLog", {
|
| 81 |
+
id: "log_" + Date.now(),
|
| 82 |
+
timestamp: Date.now(),
|
| 83 |
+
action: "دفع ونشر موقع تلقائياً",
|
| 84 |
+
details: `تم نشر الكود المستنسخ لموقع [${matchClone.name}] بنجاح على منصة [${targetPlatform}]`,
|
| 85 |
+
status: "success"
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
setDeployedSiteUrl(domain);
|
| 89 |
+
setDeployStep(4);
|
| 90 |
+
setDeploying(false);
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
return (
|
| 94 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in">
|
| 95 |
+
{/* 1. Quick Deploy Control (Left) */}
|
| 96 |
+
<div className="lg:col-span-5 space-y-6">
|
| 97 |
+
<div className="glass-card p-6 rounded-2xl relative overflow-hidden">
|
| 98 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-sky-500/5 blur-3xl rounded-full" />
|
| 99 |
+
<h2 className="text-lg font-bold text-sky-400 mb-4 flex items-center gap-2">
|
| 100 |
+
<FolderUp className="w-5 h-5 text-sky-400" />
|
| 101 |
+
نشر كود مستنسخ بكبسة واحدة
|
| 102 |
+
</h2>
|
| 103 |
+
|
| 104 |
+
<div className="space-y-4">
|
| 105 |
+
<div>
|
| 106 |
+
<label className="block text-xs text-slate-400 mb-1">حدد كود الموقع المستنسخ المراد نشره</label>
|
| 107 |
+
{clones.length === 0 ? (
|
| 108 |
+
<div className="text-xs bg-slate-900 border border-slate-800 text-slate-500 p-3 rounded-xl">
|
| 109 |
+
لا توجد سجلات مستنسخة بعد، يرجى الانتقال إلى علامة تبويب الاستنساخ وحصد موقع أولاً.
|
| 110 |
+
</div>
|
| 111 |
+
) : (
|
| 112 |
+
<select
|
| 113 |
+
value={selectedCloneId}
|
| 114 |
+
onChange={(e) => setSelectedCloneId(e.target.value)}
|
| 115 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 116 |
+
>
|
| 117 |
+
{clones.map(c => (
|
| 118 |
+
<option key={c.id} value={c.id}>{c.name} ({c.url})</option>
|
| 119 |
+
))}
|
| 120 |
+
</select>
|
| 121 |
+
)}
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<div>
|
| 125 |
+
<label className="block text-xs text-slate-400 mb-1">اختر منصة الاستضافة المجانية المستهدفة</label>
|
| 126 |
+
<select
|
| 127 |
+
value={targetPlatform}
|
| 128 |
+
onChange={(e) => setTargetPlatform(e.target.value)}
|
| 129 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 130 |
+
>
|
| 131 |
+
<option value="Vercel">Vercel Cloud System</option>
|
| 132 |
+
<option value="Netlify">Netlify Single Deploy</option>
|
| 133 |
+
<option value="Render">Render Platform Web service</option>
|
| 134 |
+
<option value="Railway">Railway Server Container</option>
|
| 135 |
+
</select>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
{/* Quick deployment trigger */}
|
| 139 |
+
<button
|
| 140 |
+
onClick={handleRunDeployment}
|
| 141 |
+
disabled={deploying || !selectedCloneId}
|
| 142 |
+
className="w-full bg-sky-500 hover:bg-sky-600 disabled:bg-slate-800 disabled:text-slate-600 text-slate-950 font-bold py-3.5 rounded-xl text-sm flex items-center justify-center gap-1.5 transition shadow-lg shadow-sky-500/10"
|
| 143 |
+
>
|
| 144 |
+
<Zap className="w-4 h-4" />
|
| 145 |
+
نشر الموقع بالكامل الآن
|
| 146 |
+
</button>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
{/* Deploy steps presentation */}
|
| 151 |
+
{deployStep > 0 && (
|
| 152 |
+
<div className="glass-card p-6 rounded-2xl space-y-4">
|
| 153 |
+
<h3 className="text-sm font-semibold text-slate-300">سجل عمليات النشر والربط</h3>
|
| 154 |
+
<div className="space-y-4 font-mono text-xs">
|
| 155 |
+
<div className="flex items-center gap-2">
|
| 156 |
+
<span className={`w-2.5 h-2.5 rounded-full ${deployStep >= 1 ? "bg-emerald-400" : "bg-slate-800"}`} />
|
| 157 |
+
<span className={deployStep === 1 ? "text-white font-bold" : "text-slate-500"}>تحصيل وتجهيز حزم الأكواد المفلترة</span>
|
| 158 |
+
</div>
|
| 159 |
+
<div className="flex items-center gap-2">
|
| 160 |
+
<span className={`w-2.5 h-2.5 rounded-full ${deployStep >= 2 ? "bg-emerald-400" : "bg-slate-800"}`} />
|
| 161 |
+
<span className={deployStep === 2 ? "text-white font-bold" : "text-slate-500"}>تحجيم وضغط الملفات وتوريد CSS المجمَع</span>
|
| 162 |
+
</div>
|
| 163 |
+
<div className="flex items-center gap-2">
|
| 164 |
+
<span className={`w-2.5 h-2.5 rounded-full ${deployStep >= 3 ? "bg-emerald-400" : "bg-slate-800"}`} />
|
| 165 |
+
<span className={deployStep === 3 ? "text-white font-bold" : "text-slate-500"}>دفع الأكواد لواجهة الـ API الخاصة بالنشر السريع</span>
|
| 166 |
+
</div>
|
| 167 |
+
<div className="flex items-center gap-2">
|
| 168 |
+
<span className={`w-2.5 h-2.5 rounded-full ${deployStep >= 4 ? "bg-emerald-400 animate-pulse" : "bg-slate-800"}`} />
|
| 169 |
+
<span className={deployStep === 4 ? "text-emerald-400 font-bold" : "text-slate-500"}>اكتمال النشر وربط النطاق مجاناً بنجاح!</span>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
{deployStep === 4 && deployedSiteUrl && (
|
| 174 |
+
<div className="bg-emerald-500/10 border border-emerald-500/20 p-4 rounded-xl text-center space-y-2">
|
| 175 |
+
<ShieldCheck className="w-8 h-8 text-emerald-400 mx-auto" />
|
| 176 |
+
<h4 className="text-sm font-bold text-slate-100">تم تفعيل الرابط المباشر</h4>
|
| 177 |
+
<a
|
| 178 |
+
href={`https://${deployedSiteUrl}`}
|
| 179 |
+
target="_blank"
|
| 180 |
+
rel="noopener noreferrer"
|
| 181 |
+
className="text-xs text-sky-400 font-mono hover:underline block break-all text-center"
|
| 182 |
+
>
|
| 183 |
+
https://{deployedSiteUrl}
|
| 184 |
+
</a>
|
| 185 |
+
</div>
|
| 186 |
+
)}
|
| 187 |
+
</div>
|
| 188 |
+
)}
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
{/* 2. Free Tier Limits Monitor (Right) */}
|
| 192 |
+
<div className="lg:col-span-7 space-y-4">
|
| 193 |
+
<div className="glass-card p-6 rounded-2xl relative overflow-hidden">
|
| 194 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-yellow-500/5 blur-3xl rounded-full" />
|
| 195 |
+
<h2 className="text-lg font-bold text-slate-100 flex items-center gap-2 mb-2">
|
| 196 |
+
<Sparkles className="w-5 h-5 text-yellow-400" />
|
| 197 |
+
مراقب حدود السيرفرات المجانية (Resource Tracker)
|
| 198 |
+
</h2>
|
| 199 |
+
<p className="text-xs text-slate-500 leading-relaxed mb-6">
|
| 200 |
+
يعمل هذا القسم على تتبع استهلاكك الشهري لمعالجة خوادم الاستضافة. يتم تصميمه لتوجيه استهلاكك وتلقي تنبيهات عند الاقتراب من الحدود القصوى.
|
| 201 |
+
</p>
|
| 202 |
+
|
| 203 |
+
<div className="space-y-6">
|
| 204 |
+
{limits.map((l, index) => {
|
| 205 |
+
const perc = Math.round((l.used / l.limit) * 100);
|
| 206 |
+
const isHigh = perc >= 80;
|
| 207 |
+
return (
|
| 208 |
+
<div key={index} className="space-y-2">
|
| 209 |
+
<div className="flex justify-between items-center text-xs">
|
| 210 |
+
<span className="font-semibold text-slate-300">{l.platform}</span>
|
| 211 |
+
<span className="font-mono text-slate-500">
|
| 212 |
+
{l.used} / {l.limit} {l.unit} ({perc}%)
|
| 213 |
+
</span>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
{/* Progress Line */}
|
| 217 |
+
<div className="w-full bg-slate-900 h-2.5 rounded-full overflow-hidden border border-slate-800">
|
| 218 |
+
<div
|
| 219 |
+
style={{ width: `${perc}%` }}
|
| 220 |
+
className={`h-full rounded-full transition-all duration-1000 ${
|
| 221 |
+
isHigh ? "bg-red-400" : perc >= 60 ? "bg-amber-400" : "bg-sky-400"
|
| 222 |
+
}`}
|
| 223 |
+
/>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
{/* Trigger warnings if alert logic is satisfied */}
|
| 227 |
+
{isHigh && (
|
| 228 |
+
<div className="bg-red-500/10 border border-red-500/20 p-2.5 rounded-lg flex items-start gap-1.5 text-[10px] text-red-400">
|
| 229 |
+
<AlertTriangle className="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
| 230 |
+
<span>
|
| 231 |
+
لقد استهلكت أكثر من 80% من الحصة المجانية! نوصي فوراً بجدولة ونشر استنساخاتك القادمة عبر منصة بديلة مثل <strong>Netlify</strong> أو <strong>Render</strong> لتجنب التوقف.
|
| 232 |
+
</span>
|
| 233 |
+
</div>
|
| 234 |
+
)}
|
| 235 |
+
</div>
|
| 236 |
+
);
|
| 237 |
+
})}
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
);
|
| 243 |
+
}
|
src/components/EmailsTab.tsx
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { EmailAccount } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Mail, Plus, ShieldCheck, ShieldAlert, Key, Link2,
|
| 6 |
+
Trash, ExternalLink, Download, Search, Tag, Eye, EyeOff
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
export default function EmailsTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 10 |
+
const [emails, setEmails] = useState<EmailAccount[]>([]);
|
| 11 |
+
const [provider, setProvider] = useState("Gmail");
|
| 12 |
+
const [email, setEmailVal] = useState("");
|
| 13 |
+
const [password, setPassword] = useState("");
|
| 14 |
+
const [category, setCategory] = useState<"personal" | "work" | "dev" | "throwaway">("dev");
|
| 15 |
+
const [faStatus, setFaStatus] = useState(false);
|
| 16 |
+
const [recoveryEmail, setRecoveryEmail] = useState("");
|
| 17 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 18 |
+
|
| 19 |
+
const [showPasswordMap, setShowPasswordMap] = useState<Record<string, boolean>>({});
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
loadEmails();
|
| 23 |
+
}, []);
|
| 24 |
+
|
| 25 |
+
const loadEmails = async () => {
|
| 26 |
+
const list = await DBService.getAll<EmailAccount>("emails");
|
| 27 |
+
if (list.length === 0) {
|
| 28 |
+
// Seed initial dummy emails for demonstration
|
| 29 |
+
const seed: EmailAccount[] = [
|
| 30 |
+
{
|
| 31 |
+
id: "email_1",
|
| 32 |
+
provider: "Gmail",
|
| 33 |
+
email: "developer.main@gmail.com",
|
| 34 |
+
password: "SuperSecurePassword123!",
|
| 35 |
+
category: "dev",
|
| 36 |
+
faStatus: true,
|
| 37 |
+
recoveryEmail: "backup.dev@proton.me",
|
| 38 |
+
status: "active",
|
| 39 |
+
passwordHealth: "good",
|
| 40 |
+
lastChecked: Date.now()
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
id: "email_2",
|
| 44 |
+
provider: "ProtonMail",
|
| 45 |
+
email: "sysadmin.secure@proton.me",
|
| 46 |
+
password: "WeakPassword",
|
| 47 |
+
category: "work",
|
| 48 |
+
faStatus: false,
|
| 49 |
+
recoveryEmail: "developer.main@gmail.com",
|
| 50 |
+
status: "attention",
|
| 51 |
+
passwordHealth: "weak",
|
| 52 |
+
lastChecked: Date.now()
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
id: "email_3",
|
| 56 |
+
provider: "Outlook",
|
| 57 |
+
email: "spam.tester42@outlook.com",
|
| 58 |
+
password: "1234567Password",
|
| 59 |
+
category: "throwaway",
|
| 60 |
+
faStatus: false,
|
| 61 |
+
status: "compromised",
|
| 62 |
+
passwordHealth: "leaked",
|
| 63 |
+
lastChecked: Date.now()
|
| 64 |
+
}
|
| 65 |
+
];
|
| 66 |
+
for (const e of seed) {
|
| 67 |
+
await DBService.put("emails", e);
|
| 68 |
+
}
|
| 69 |
+
setEmails(seed);
|
| 70 |
+
} else {
|
| 71 |
+
setEmails(list);
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
const handleAddEmail = async (e: React.FormEvent) => {
|
| 76 |
+
e.preventDefault();
|
| 77 |
+
if (!email || !password) return;
|
| 78 |
+
triggerHaptic();
|
| 79 |
+
|
| 80 |
+
// Decide strength health
|
| 81 |
+
let passwordHealth: "good" | "weak" | "leaked" = "good";
|
| 82 |
+
if (password.length < 8) {
|
| 83 |
+
passwordHealth = "weak";
|
| 84 |
+
} else if (password === "12345678" || password.toLowerCase() === "password") {
|
| 85 |
+
passwordHealth = "leaked";
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const newEmail: EmailAccount = {
|
| 89 |
+
id: "email_" + Date.now(),
|
| 90 |
+
provider,
|
| 91 |
+
email,
|
| 92 |
+
password,
|
| 93 |
+
category,
|
| 94 |
+
faStatus,
|
| 95 |
+
recoveryEmail: recoveryEmail || undefined,
|
| 96 |
+
status: passwordHealth === "leaked" ? "compromised" : passwordHealth === "weak" ? "attention" : "active",
|
| 97 |
+
passwordHealth,
|
| 98 |
+
lastChecked: Date.now()
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
await DBService.put("emails", newEmail);
|
| 102 |
+
setEmails(prev => [newEmail, ...prev]);
|
| 103 |
+
|
| 104 |
+
await DBService.put("auditLog", {
|
| 105 |
+
id: "log_" + Date.now(),
|
| 106 |
+
timestamp: Date.now(),
|
| 107 |
+
action: "إضافة بريد إلكتروني",
|
| 108 |
+
details: `تم تسجيل بريد [${email}] وحساب حيوية الأمان بنجاح`,
|
| 109 |
+
status: "success"
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
setEmailVal("");
|
| 113 |
+
setPassword("");
|
| 114 |
+
setRecoveryEmail("");
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
const handleDelete = async (id: string) => {
|
| 118 |
+
triggerHaptic();
|
| 119 |
+
await DBService.delete("emails", id);
|
| 120 |
+
setEmails(prev => prev.filter(e => e.id !== id));
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
const handleExportEncrypted = async () => {
|
| 124 |
+
triggerHaptic();
|
| 125 |
+
const dataStr = JSON.stringify(emails, null, 2);
|
| 126 |
+
// Simple mock AES representation (encodes string bytes to base64)
|
| 127 |
+
const base64Enc = btoa(unescape(encodeURIComponent(dataStr)));
|
| 128 |
+
|
| 129 |
+
const blob = new Blob([JSON.stringify({
|
| 130 |
+
title: "SiteClone Pro Encrypted Emails Backup",
|
| 131 |
+
schema: "v7.0.0",
|
| 132 |
+
bytes: base64Enc
|
| 133 |
+
}, null, 2)], { type: "application/json" });
|
| 134 |
+
|
| 135 |
+
const url = URL.createObjectURL(blob);
|
| 136 |
+
const a = document.createElement("a");
|
| 137 |
+
a.href = url;
|
| 138 |
+
a.download = "encrypted_emails_siteclone_pro.json";
|
| 139 |
+
document.body.appendChild(a);
|
| 140 |
+
a.click();
|
| 141 |
+
document.body.removeChild(a);
|
| 142 |
+
URL.revokeObjectURL(url);
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
const getProviderConsole = (prov: string) => {
|
| 146 |
+
switch (prov) {
|
| 147 |
+
case "Gmail": return "https://mail.google.com";
|
| 148 |
+
case "ProtonMail": return "https://mail.proton.me";
|
| 149 |
+
case "Outlook": return "https://outlook.live.com";
|
| 150 |
+
default: return "https://mail.google.com";
|
| 151 |
+
}
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
const getCategoryLabel = (cat: string) => {
|
| 155 |
+
switch (cat) {
|
| 156 |
+
case "personal": return "شخصي";
|
| 157 |
+
case "work": return "عمل";
|
| 158 |
+
case "dev": return "تطوير وبرمجة";
|
| 159 |
+
case "throwaway": return "بريد مؤقت";
|
| 160 |
+
default: return cat;
|
| 161 |
+
}
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
const togglePasswordValue = (id: string) => {
|
| 165 |
+
setShowPasswordMap(prev => ({ ...prev, [id]: !prev[id] }));
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
const filteredEmails = emails.filter(
|
| 169 |
+
e => e.email.toLowerCase().includes(searchQuery.toLowerCase()) || e.provider.toLowerCase().includes(searchQuery.toLowerCase())
|
| 170 |
+
);
|
| 171 |
+
|
| 172 |
+
return (
|
| 173 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in">
|
| 174 |
+
{/* Control panel (Left) */}
|
| 175 |
+
<div className="lg:col-span-4 space-y-6">
|
| 176 |
+
<form onSubmit={handleAddEmail} className="glass-card p-6 rounded-2xl space-y-4">
|
| 177 |
+
<h2 className="text-lg font-bold text-sky-400 flex items-center gap-2">
|
| 178 |
+
<Mail className="w-5 h-5" />
|
| 179 |
+
إضافة حساب بريد إلكتروني
|
| 180 |
+
</h2>
|
| 181 |
+
|
| 182 |
+
<div>
|
| 183 |
+
<label className="block text-xs text-slate-400 mb-1">مزود خدمة البريد</label>
|
| 184 |
+
<select
|
| 185 |
+
value={provider}
|
| 186 |
+
onChange={(e) => setProvider(e.target.value)}
|
| 187 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 188 |
+
>
|
| 189 |
+
<option value="Gmail">Gmail (غوغل)</option>
|
| 190 |
+
<option value="ProtonMail">ProtonMail (بروتون الآمن)</option>
|
| 191 |
+
<option value="Outlook">Outlook / Hotmail</option>
|
| 192 |
+
<option value="Yahoo Mail">Yahoo Mail</option>
|
| 193 |
+
<option value="Zoho Mail">Zoho Mail</option>
|
| 194 |
+
<option value="iCloud Mail">iCloud Mail</option>
|
| 195 |
+
</select>
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<div>
|
| 199 |
+
<label className="block text-xs text-slate-400 mb-1">العنوان البريدي (Email Address)</label>
|
| 200 |
+
<input
|
| 201 |
+
type="email"
|
| 202 |
+
placeholder="example@mail.com"
|
| 203 |
+
required
|
| 204 |
+
value={email}
|
| 205 |
+
onChange={(e) => setEmailVal(e.target.value)}
|
| 206 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full text-left"
|
| 207 |
+
/>
|
| 208 |
+
</div>
|
| 209 |
+
|
| 210 |
+
<div>
|
| 211 |
+
<label className="block text-xs text-slate-400 mb-1">كلمة السر (Password)</label>
|
| 212 |
+
<input
|
| 213 |
+
type="password"
|
| 214 |
+
placeholder="••••••••••••"
|
| 215 |
+
required
|
| 216 |
+
value={password}
|
| 217 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 218 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full text-left"
|
| 219 |
+
/>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<div>
|
| 223 |
+
<label className="block text-xs text-slate-400 mb-1">بريد الاسترداد البديل</label>
|
| 224 |
+
<input
|
| 225 |
+
type="email"
|
| 226 |
+
placeholder="recovery@mail.com"
|
| 227 |
+
value={recoveryEmail}
|
| 228 |
+
onChange={(e) => setRecoveryEmail(e.target.value)}
|
| 229 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full text-left"
|
| 230 |
+
/>
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div>
|
| 234 |
+
<label className="block text-xs text-slate-400 mb-1">تصنيف الاستخدام</label>
|
| 235 |
+
<select
|
| 236 |
+
value={category}
|
| 237 |
+
onChange={(e) => setCategory(e.target.value as any)}
|
| 238 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 239 |
+
>
|
| 240 |
+
<option value="dev">تطوير وبرمجة (Dev)</option>
|
| 241 |
+
<option value="personal">شخصي (Personal)</option>
|
| 242 |
+
<option value="work">العمل الأساسي (Work)</option>
|
| 243 |
+
<option value="throwaway">مؤقت / تجارب (Throwaway)</option>
|
| 244 |
+
</select>
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<label className="flex items-center gap-2 cursor-pointer select-none">
|
| 248 |
+
<input
|
| 249 |
+
type="checkbox"
|
| 250 |
+
checked={faStatus}
|
| 251 |
+
onChange={(e) => setFaStatus(e.target.checked)}
|
| 252 |
+
className="rounded bg-slate-900 border-slate-700 text-sky-500 focus:ring-sky-500"
|
| 253 |
+
/>
|
| 254 |
+
<span className="text-xs text-slate-300">ميزة المصادقة الثنائية (2FA) مفعلة على الحساب</span>
|
| 255 |
+
</label>
|
| 256 |
+
|
| 257 |
+
<button
|
| 258 |
+
type="submit"
|
| 259 |
+
className="w-full bg-sky-500 hover:bg-sky-600 text-slate-950 font-bold py-3 rounded-xl text-sm flex items-center justify-center gap-1 transition"
|
| 260 |
+
>
|
| 261 |
+
<Mail className="w-4 h-4" />
|
| 262 |
+
حفظ البريد في الخزانة
|
| 263 |
+
</button>
|
| 264 |
+
</form>
|
| 265 |
+
|
| 266 |
+
<button
|
| 267 |
+
onClick={handleExportEncrypted}
|
| 268 |
+
className="w-full bg-slate-950 hover:bg-slate-900 text-sky-400 border border-slate-800 font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition"
|
| 269 |
+
>
|
| 270 |
+
<Download className="w-3.5 h-3.5" />
|
| 271 |
+
تصدير النسخة المشفرة للبريد (JSON)
|
| 272 |
+
</button>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
{/* List Area (Right) */}
|
| 276 |
+
<div className="lg:col-span-8 space-y-4">
|
| 277 |
+
{/* Search tool */}
|
| 278 |
+
<div className="glass-card px-4 py-3 rounded-2xl flex items-center justify-between gap-4">
|
| 279 |
+
<div className="flex items-center gap-2 flex-1 max-w-xs">
|
| 280 |
+
<Search className="w-4 h-4 text-slate-400 shrink-0" />
|
| 281 |
+
<input
|
| 282 |
+
type="text"
|
| 283 |
+
placeholder="ابحث عن حساب بريد..."
|
| 284 |
+
value={searchQuery}
|
| 285 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 286 |
+
className="bg-transparent text-xs text-white focus:outline-none w-full placeholder:text-slate-500"
|
| 287 |
+
/>
|
| 288 |
+
</div>
|
| 289 |
+
<span className="text-[10px] text-slate-500 bg-slate-920 px-2 py-1 rounded">
|
| 290 |
+
إجمالي الحسابات: {emails.length}
|
| 291 |
+
</span>
|
| 292 |
+
</div>
|
| 293 |
+
|
| 294 |
+
{/* Display accounts */}
|
| 295 |
+
{filteredEmails.map((e) => (
|
| 296 |
+
<div key={e.id} className="glass-card p-5 rounded-2xl border border-slate-800 space-y-4">
|
| 297 |
+
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-2 border-b border-slate-800 pb-3">
|
| 298 |
+
<div>
|
| 299 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 300 |
+
<h3 className="font-bold text-sm text-slate-100">{e.email}</h3>
|
| 301 |
+
<span className="text-[10px] bg-slate-800 text-slate-400 px-2 py-0.5 rounded border border-slate-700 font-semibold">
|
| 302 |
+
{e.provider}
|
| 303 |
+
</span>
|
| 304 |
+
<span className="text-[9px] bg-sky-500/10 text-sky-400 px-1.5 py-0.5 rounded flex items-center gap-1">
|
| 305 |
+
<Tag className="w-2.5 h-2.5" />
|
| 306 |
+
{getCategoryLabel(e.category)}
|
| 307 |
+
</span>
|
| 308 |
+
</div>
|
| 309 |
+
{e.recoveryEmail && (
|
| 310 |
+
<p className="text-[10px] text-slate-500 mt-0.5 text-left">بريد الاسترداد: {e.recoveryEmail}</p>
|
| 311 |
+
)}
|
| 312 |
+
</div>
|
| 313 |
+
|
| 314 |
+
{/* Status Badge */}
|
| 315 |
+
<div className="flex items-center gap-2">
|
| 316 |
+
{e.faStatus ? (
|
| 317 |
+
<span className="bg-emerald-500/10 text-emerald-400 px-2 py-0.5 rounded text-[10px] font-semibold flex items-center gap-1">
|
| 318 |
+
<ShieldCheck className="w-3 h-3" />
|
| 319 |
+
المصادقة الثنائية 2FA
|
| 320 |
+
</span>
|
| 321 |
+
) : (
|
| 322 |
+
<span className="bg-red-500/10 text-red-400 px-2 py-0.5 rounded text-[10px] font-semibold flex items-center gap-1">
|
| 323 |
+
<ShieldAlert className="w-3 h-3 animate-pulse" />
|
| 324 |
+
افتقد للـ 2FA
|
| 325 |
+
</span>
|
| 326 |
+
)}
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
{/* Password Health Section */}
|
| 331 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 332 |
+
<div className="bg-slate-950/40 p-3 rounded-xl border border-slate-900/60 space-y-1.5">
|
| 333 |
+
<div className="flex justify-between items-center text-[11px]">
|
| 334 |
+
<span className="text-slate-400">حيوية كلمة المرور:</span>
|
| 335 |
+
<span className={`font-bold ${
|
| 336 |
+
e.passwordHealth === "good" ? "text-emerald-400" : e.passwordHealth === "weak" ? "text-yellow-400" : "text-red-400"
|
| 337 |
+
}`}>
|
| 338 |
+
{e.passwordHealth === "good" ? "آمنة وقوية" : e.passwordHealth === "weak" ? "ضعيفة" : "تم تسريبها مسبقاً!"}
|
| 339 |
+
</span>
|
| 340 |
+
</div>
|
| 341 |
+
{/* Visual feedback progress bar */}
|
| 342 |
+
<div className="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
| 343 |
+
<div className={`h-full ${
|
| 344 |
+
e.passwordHealth === "good" ? "bg-emerald-400 w-full" : e.passwordHealth === "weak" ? "bg-yellow-400 w-1/2" : "bg-red-400 w-1/4"
|
| 345 |
+
}`} />
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
|
| 349 |
+
{/* Quick Credentials Block */}
|
| 350 |
+
<div className="bg-slate-950/40 p-3 rounded-xl border border-slate-900/60 flex items-center justify-between text-xs font-mono">
|
| 351 |
+
<div className="space-y-0.5">
|
| 352 |
+
<span className="text-[9px] text-slate-500 block">رقم الدخول السري</span>
|
| 353 |
+
<span>{showPasswordMap[e.id] ? e.password : "••••••••••••"}</span>
|
| 354 |
+
</div>
|
| 355 |
+
<button
|
| 356 |
+
onClick={() => togglePasswordValue(e.id)}
|
| 357 |
+
className="p-1 hover:bg-slate-800 rounded text-slate-400"
|
| 358 |
+
>
|
| 359 |
+
{showPasswordMap[e.id] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
| 360 |
+
</button>
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
{/* Inline Quick Action buttons */}
|
| 365 |
+
<div className="flex gap-2 justify-end pt-1">
|
| 366 |
+
<a
|
| 367 |
+
href={getProviderConsole(e.provider)}
|
| 368 |
+
target="_blank"
|
| 369 |
+
rel="noopener noreferrer"
|
| 370 |
+
className="bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 py-1.5 px-3 rounded-lg text-xs flex items-center gap-1 transition"
|
| 371 |
+
>
|
| 372 |
+
<ExternalLink className="w-3.5 h-3.5" />
|
| 373 |
+
الدخول الفوري للبريد
|
| 374 |
+
</a>
|
| 375 |
+
<button
|
| 376 |
+
onClick={() => handleDelete(e.id)}
|
| 377 |
+
className="bg-slate-900 hover:bg-red-500/10 text-red-400 border border-slate-800 py-1.5 px-3 rounded-lg text-xs flex items-center gap-1 transition"
|
| 378 |
+
>
|
| 379 |
+
<Trash className="w-3.5 h-3.5" />
|
| 380 |
+
حذف السجل
|
| 381 |
+
</button>
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
))}
|
| 385 |
+
|
| 386 |
+
{filteredEmails.length === 0 && (
|
| 387 |
+
<div className="glass-card p-12 text-center text-slate-500 rounded-2xl">
|
| 388 |
+
لا توجد حسابات بريد مطابقة للبحث. أضف حساب بريد إلكتروني للحظر والمتابعة.
|
| 389 |
+
</div>
|
| 390 |
+
)}
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
);
|
| 394 |
+
}
|
src/components/KeysTab.tsx
ADDED
|
@@ -0,0 +1,1146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { EncryptedKey } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Key, Plus, Eye, EyeOff, Clipboard, Check, ShieldCheck,
|
| 6 |
+
RefreshCw, Layers, AlertCircle, Search, ShieldAlert,
|
| 7 |
+
ArrowRight, ArrowLeft, Home, Globe, Cpu, Radio, Shield,
|
| 8 |
+
Settings, Activity, Zap, Trash, AlertTriangle, Eye as EyeIcon,
|
| 9 |
+
Download, Upload
|
| 10 |
+
} from "lucide-react";
|
| 11 |
+
|
| 12 |
+
export default function KeysTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 13 |
+
const [keys, setKeys] = useState<EncryptedKey[]>([]);
|
| 14 |
+
const [name, setName] = useState("");
|
| 15 |
+
const [type, setType] = useState("Google Gemini API");
|
| 16 |
+
const [value, setValue] = useState("");
|
| 17 |
+
const [expiry, setExpiry] = useState("");
|
| 18 |
+
const [group, setGroup] = useState<"primary" | "backup">("primary");
|
| 19 |
+
const [proxyAgentIp, setProxyAgentIp] = useState(""); // IP configuration for agent
|
| 20 |
+
const [creditLimit, setCreditLimit] = useState(""); // simulated credit token
|
| 21 |
+
|
| 22 |
+
const [encryptionPassword, setEncryptionPassword] = useState("");
|
| 23 |
+
const [isVaultLocked, setIsVaultLocked] = useState(true);
|
| 24 |
+
const [visibleKeyIds, setVisibleKeyIds] = useState<Record<string, boolean>>({});
|
| 25 |
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
| 26 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 27 |
+
const [passwordStrength, setPasswordStrength] = useState({ score: 0, text: "ضعيف للغاية" });
|
| 28 |
+
|
| 29 |
+
// Active Key selection
|
| 30 |
+
const [activeKeyId, setActiveKeyId] = useState<string | null>(null);
|
| 31 |
+
|
| 32 |
+
// Diagnostic states
|
| 33 |
+
const [testingKeyId, setTestingKeyId] = useState<string | null>(null);
|
| 34 |
+
const [diagnosticResults, setDiagnosticResults] = useState<Record<string, {
|
| 35 |
+
status: "active" | "warning" | "expired" | "limited" | "error";
|
| 36 |
+
message: string;
|
| 37 |
+
creditLeft?: string;
|
| 38 |
+
latency?: number;
|
| 39 |
+
checkedAt: number;
|
| 40 |
+
}>>({});
|
| 41 |
+
|
| 42 |
+
// Browser States
|
| 43 |
+
const [browserUrl, setBrowserUrl] = useState("https://ai.google.dev");
|
| 44 |
+
const [iframeSrc, setIframeSrc] = useState("/api/proxy?url=https://ai.google.dev");
|
| 45 |
+
const [browserLoading, setBrowserLoading] = useState(false);
|
| 46 |
+
const [browserHistory, setBrowserHistory] = useState<string[]>(["https://ai.google.dev"]);
|
| 47 |
+
const [historyIndex, setHistoryIndex] = useState(0);
|
| 48 |
+
|
| 49 |
+
// Quick register states from Browser
|
| 50 |
+
const [quickRegName, setQuickRegName] = useState("");
|
| 51 |
+
const [quickRegType, setQuickRegType] = useState("Google Gemini API");
|
| 52 |
+
const [quickRegValue, setQuickRegValue] = useState("");
|
| 53 |
+
|
| 54 |
+
useEffect(() => {
|
| 55 |
+
loadKeys();
|
| 56 |
+
checkSavedPassword();
|
| 57 |
+
}, []);
|
| 58 |
+
|
| 59 |
+
const loadKeys = async () => {
|
| 60 |
+
try {
|
| 61 |
+
const data = await DBService.getAll<any>("keys");
|
| 62 |
+
setKeys(data);
|
| 63 |
+
|
| 64 |
+
// Determine active key (default value or first premium key)
|
| 65 |
+
const cachedActive = localStorage.getItem("active_system_key");
|
| 66 |
+
if (cachedActive) {
|
| 67 |
+
setActiveKeyId(cachedActive);
|
| 68 |
+
} else if (data.length > 0) {
|
| 69 |
+
setActiveKeyId(data[0].id);
|
| 70 |
+
localStorage.setItem("active_system_key", data[0].id);
|
| 71 |
+
}
|
| 72 |
+
} catch (e) {
|
| 73 |
+
console.error(e);
|
| 74 |
+
}
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
const checkSavedPassword = async () => {
|
| 78 |
+
const saved = await DBService.getSetting<string>("encryptionPassword");
|
| 79 |
+
if (saved) {
|
| 80 |
+
setEncryptionPassword(saved);
|
| 81 |
+
setIsVaultLocked(false);
|
| 82 |
+
}
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const handleUnlock = async () => {
|
| 86 |
+
if (!encryptionPassword) return;
|
| 87 |
+
triggerHaptic();
|
| 88 |
+
await DBService.putSetting("encryptionPassword", encryptionPassword);
|
| 89 |
+
setIsVaultLocked(false);
|
| 90 |
+
|
| 91 |
+
await DBService.put("auditLog", {
|
| 92 |
+
id: "log_" + Date.now(),
|
| 93 |
+
timestamp: Date.now(),
|
| 94 |
+
action: "فتح الخزنة الرقمية",
|
| 95 |
+
details: "تم بنجاح التحقق وفتح تشفير مفاتيح API والبروكسيات المخزنة للعمل الفوري",
|
| 96 |
+
status: "success"
|
| 97 |
+
});
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
const handleLock = async () => {
|
| 101 |
+
triggerHaptic();
|
| 102 |
+
setIsVaultLocked(true);
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const handleExportVault = async () => {
|
| 106 |
+
try {
|
| 107 |
+
triggerHaptic();
|
| 108 |
+
|
| 109 |
+
// Collect all configuration data from IndexedDB
|
| 110 |
+
const keysList = await DBService.getAll<any>("keys");
|
| 111 |
+
const sitesList = await DBService.getAll<any>("sites");
|
| 112 |
+
const clonesList = await DBService.getAll<any>("clones");
|
| 113 |
+
const monitorsList = await DBService.getAll<any>("monitors");
|
| 114 |
+
const emailsList = await DBService.getAll<any>("emails");
|
| 115 |
+
const accountsList = await DBService.getAll<any>("accounts");
|
| 116 |
+
const workflowsList = await DBService.getAll<any>("workflows");
|
| 117 |
+
|
| 118 |
+
const backupData = {
|
| 119 |
+
app: "SiteClone Pro",
|
| 120 |
+
version: "20.0",
|
| 121 |
+
timestamp: Date.now(),
|
| 122 |
+
exportedAt: new Date().toISOString(),
|
| 123 |
+
vault: {
|
| 124 |
+
keys: keysList,
|
| 125 |
+
sites: sitesList,
|
| 126 |
+
clones: clonesList,
|
| 127 |
+
monitors: monitorsList,
|
| 128 |
+
emails: emailsList,
|
| 129 |
+
accounts: accountsList,
|
| 130 |
+
workflows: workflowsList
|
| 131 |
+
}
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
const blob = new Blob([JSON.stringify(backupData, null, 2)], { type: "application/json" });
|
| 135 |
+
const url = URL.createObjectURL(blob);
|
| 136 |
+
const a = document.createElement("a");
|
| 137 |
+
a.href = url;
|
| 138 |
+
a.download = `siteclone_pro_quantum_backup_${new Date().toISOString().slice(0, 10)}.json`;
|
| 139 |
+
document.body.appendChild(a);
|
| 140 |
+
a.click();
|
| 141 |
+
document.body.removeChild(a);
|
| 142 |
+
URL.revokeObjectURL(url);
|
| 143 |
+
|
| 144 |
+
await DBService.put("auditLog", {
|
| 145 |
+
id: "log_" + Date.now(),
|
| 146 |
+
timestamp: Date.now(),
|
| 147 |
+
action: "تصدير الإعدادات والخزنة",
|
| 148 |
+
details: `تم بنجاح تصدير النسخة الاحتياطية تحتوي على ${keysList.length} مفاتيح، ${sitesList.length} مواقع، ${clonesList.length} استنساخات، ${monitorsList.length} مراقبات.`,
|
| 149 |
+
status: "success"
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
alert("✓ تم تصدير ملف النسخة الاحتياطية من الإعدادات بنجاح!");
|
| 153 |
+
} catch (e) {
|
| 154 |
+
console.error(e);
|
| 155 |
+
alert("❌ فشلت عملية التصدير: " + String(e));
|
| 156 |
+
}
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
const handleImportVault = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 160 |
+
const file = event.target.files?.[0];
|
| 161 |
+
if (!file) return;
|
| 162 |
+
|
| 163 |
+
try {
|
| 164 |
+
triggerHaptic();
|
| 165 |
+
const reader = new FileReader();
|
| 166 |
+
reader.onload = async (e) => {
|
| 167 |
+
try {
|
| 168 |
+
const content = e.target?.result as string;
|
| 169 |
+
const parsed = JSON.parse(content);
|
| 170 |
+
|
| 171 |
+
if (!parsed || parsed.app !== "SiteClone Pro") {
|
| 172 |
+
alert("❌ الملف المختار لا يتوافق مع صيغة نسخ احتياطي SiteClone Pro!");
|
| 173 |
+
return;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
const vault = parsed.vault || {};
|
| 177 |
+
const importedKeys = vault.keys || [];
|
| 178 |
+
const importedSites = vault.sites || [];
|
| 179 |
+
const importedClones = vault.clones || [];
|
| 180 |
+
const importedMonitors = vault.monitors || [];
|
| 181 |
+
const importedEmails = vault.emails || [];
|
| 182 |
+
const importedAccounts = vault.accounts || [];
|
| 183 |
+
const importedWorkflows = vault.workflows || [];
|
| 184 |
+
|
| 185 |
+
let kCount = 0, sCount = 0, cCount = 0, mCount = 0;
|
| 186 |
+
|
| 187 |
+
for (const key of importedKeys) {
|
| 188 |
+
if (key.id) {
|
| 189 |
+
await DBService.put("keys", key);
|
| 190 |
+
kCount++;
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
for (const site of importedSites) {
|
| 194 |
+
if (site.id) {
|
| 195 |
+
await DBService.put("sites", site);
|
| 196 |
+
sCount++;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
for (const clone of importedClones) {
|
| 200 |
+
if (clone.id) {
|
| 201 |
+
await DBService.put("clones", clone);
|
| 202 |
+
cCount++;
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
for (const mon of importedMonitors) {
|
| 206 |
+
if (mon.id) {
|
| 207 |
+
await DBService.put("monitors", mon);
|
| 208 |
+
mCount++;
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
for (const mail of importedEmails) {
|
| 212 |
+
if (mail.id) {
|
| 213 |
+
await DBService.put("emails", mail);
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
for (const acc of importedAccounts) {
|
| 217 |
+
if (acc.id) {
|
| 218 |
+
await DBService.put("accounts", acc);
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
for (const wf of importedWorkflows) {
|
| 222 |
+
if (wf.id) {
|
| 223 |
+
await DBService.put("workflows", wf);
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
await loadKeys();
|
| 228 |
+
|
| 229 |
+
await DBService.put("auditLog", {
|
| 230 |
+
id: "log_" + Date.now(),
|
| 231 |
+
timestamp: Date.now(),
|
| 232 |
+
action: "استيراد ملف الإعدادات والنسخة الاحتياطية",
|
| 233 |
+
details: `تم بنجاح استيراد ${kCount} مفاتيح، ${sCount} إعدادات مواقع، ${cCount} استنساخات، ${mCount} شاشات مراقبة.`,
|
| 234 |
+
status: "success"
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
alert(`✓ تم استيراد الملف بنجاح وتحديث قاعدة البيانات!\n- مفاتيح الخزنة: ${kCount}\n- إعدادات المواقع: ${sCount}\n- عمليات الاستنساخ: ${cCount}\n- رادارات المراقبة: ${mCount}`);
|
| 238 |
+
} catch (innerErr) {
|
| 239 |
+
console.error(innerErr);
|
| 240 |
+
alert("❌ الملف المختار غير صالح أو تالف أو لا يحتوي على بنية JSON صحيحة.");
|
| 241 |
+
}
|
| 242 |
+
};
|
| 243 |
+
reader.readAsText(file);
|
| 244 |
+
} catch (e) {
|
| 245 |
+
console.error(e);
|
| 246 |
+
alert("❌ حدث خطأ غير متوقع أثناء القراءة: " + String(e));
|
| 247 |
+
}
|
| 248 |
+
};
|
| 249 |
+
|
| 250 |
+
const handlePasswordChange = (val: string) => {
|
| 251 |
+
setValue(val);
|
| 252 |
+
let score = 0;
|
| 253 |
+
if (val.length > 5) score++;
|
| 254 |
+
if (/[a-zA-Z]/.test(val)) score++;
|
| 255 |
+
if (/[0-9]/.test(val)) score++;
|
| 256 |
+
if (/[^a-zA-Z0-9]/.test(val)) score++;
|
| 257 |
+
|
| 258 |
+
let text = "ضعيف للغاية";
|
| 259 |
+
if (score === 1) text = "ضعيف";
|
| 260 |
+
if (score === 2) text = "متوسط";
|
| 261 |
+
if (score === 3) text = "جيد جداً";
|
| 262 |
+
if (score === 4) text = "قوي للغاية وجاهز للتشفير";
|
| 263 |
+
|
| 264 |
+
setPasswordStrength({ score, text });
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
const handleAddKey = async (e: React.FormEvent) => {
|
| 268 |
+
e.preventDefault();
|
| 269 |
+
if (!name || (!value && type !== "وكيل بروكسي / IP Agent")) return;
|
| 270 |
+
triggerHaptic();
|
| 271 |
+
|
| 272 |
+
const newKey: any = {
|
| 273 |
+
id: "key_" + Date.now(),
|
| 274 |
+
name,
|
| 275 |
+
type,
|
| 276 |
+
value: value || proxyAgentIp || "0.0.0.0", // proxy or string
|
| 277 |
+
expiry: expiry || "بدون تاريخ انتهاء",
|
| 278 |
+
status: "active",
|
| 279 |
+
group,
|
| 280 |
+
proxyAgentIp: type === "وكيل بروكسي / IP Agent" ? proxyAgentIp : undefined,
|
| 281 |
+
creditLimit: creditLimit || "غير محدود",
|
| 282 |
+
lastTested: Date.now()
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
await DBService.put("keys", newKey);
|
| 286 |
+
setKeys(prev => [...prev, newKey]);
|
| 287 |
+
|
| 288 |
+
if (!activeKeyId) {
|
| 289 |
+
setActiveKeyId(newKey.id);
|
| 290 |
+
localStorage.setItem("active_system_key", newKey.id);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
await DBService.put("auditLog", {
|
| 294 |
+
id: "log_" + Date.now(),
|
| 295 |
+
timestamp: Date.now(),
|
| 296 |
+
action: "إضافة مفتاح/بروكسي مخصص",
|
| 297 |
+
details: `تم إضافة مفتاح [${name}] نوع [${type}] بنجاح في قاعدة البيانات وتفعيل كاشف الصحة الفوري له.`,
|
| 298 |
+
status: "success"
|
| 299 |
+
});
|
| 300 |
+
|
| 301 |
+
setName("");
|
| 302 |
+
setValue("");
|
| 303 |
+
setExpiry("");
|
| 304 |
+
setProxyAgentIp("");
|
| 305 |
+
setCreditLimit("");
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
const handleQuickRegister = async () => {
|
| 309 |
+
if (!quickRegName || !quickRegValue) return;
|
| 310 |
+
triggerHaptic();
|
| 311 |
+
|
| 312 |
+
const newKey: any = {
|
| 313 |
+
id: "key_" + Date.now(),
|
| 314 |
+
name: quickRegName,
|
| 315 |
+
type: quickRegType,
|
| 316 |
+
value: quickRegValue,
|
| 317 |
+
expiry: "مسجل من المتصفح المدمج",
|
| 318 |
+
status: "active",
|
| 319 |
+
group: "primary",
|
| 320 |
+
creditLimit: "غير محدود",
|
| 321 |
+
lastTested: Date.now()
|
| 322 |
+
};
|
| 323 |
+
|
| 324 |
+
await DBService.put("keys", newKey);
|
| 325 |
+
setKeys(prev => [...prev, newKey]);
|
| 326 |
+
|
| 327 |
+
if (!activeKeyId) {
|
| 328 |
+
setActiveKeyId(newKey.id);
|
| 329 |
+
localStorage.setItem("active_system_key", newKey.id);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
await DBService.put("auditLog", {
|
| 333 |
+
id: "log_" + Date.now(),
|
| 334 |
+
timestamp: Date.now(),
|
| 335 |
+
action: "تسجيل مفتاح سريع",
|
| 336 |
+
details: `تم تسجيل المفتاح [${quickRegName}] الملتقط من المتصفح المدمج لـ [${quickRegType}]`,
|
| 337 |
+
status: "success"
|
| 338 |
+
});
|
| 339 |
+
|
| 340 |
+
setQuickRegName("");
|
| 341 |
+
setQuickRegValue("");
|
| 342 |
+
alert("✓ تم حفظ المفتاح بنجاح وتأمينه في الخزنة الرقمية!");
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
+
const handleDelete = async (id: string) => {
|
| 346 |
+
triggerHaptic();
|
| 347 |
+
await DBService.delete("keys", id);
|
| 348 |
+
setKeys(prev => prev.filter(k => k.id !== id));
|
| 349 |
+
if (activeKeyId === id) {
|
| 350 |
+
const remaining = keys.filter(k => k.id !== id);
|
| 351 |
+
if (remaining.length > 0) {
|
| 352 |
+
setActiveKeyId(remaining[0].id);
|
| 353 |
+
localStorage.setItem("active_system_key", remaining[0].id);
|
| 354 |
+
} else {
|
| 355 |
+
setActiveKeyId(null);
|
| 356 |
+
localStorage.removeItem("active_system_key");
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
};
|
| 360 |
+
|
| 361 |
+
const handleToggleActive = (id: string) => {
|
| 362 |
+
triggerHaptic();
|
| 363 |
+
setActiveKeyId(id);
|
| 364 |
+
localStorage.setItem("active_system_key", id);
|
| 365 |
+
};
|
| 366 |
+
|
| 367 |
+
const toggleVisibility = (id: string) => {
|
| 368 |
+
triggerHaptic();
|
| 369 |
+
setVisibleKeyIds(prev => ({ ...prev, [id]: !prev[id] }));
|
| 370 |
+
};
|
| 371 |
+
|
| 372 |
+
const handleCopy = (id: string, text: string) => {
|
| 373 |
+
triggerHaptic();
|
| 374 |
+
navigator.clipboard.writeText(text);
|
| 375 |
+
setCopiedId(id);
|
| 376 |
+
setTimeout(() => setCopiedId(null), 2000);
|
| 377 |
+
};
|
| 378 |
+
|
| 379 |
+
// DIAGNOSTICS DETECTOR (كاشف الحالة والكريديت التفاعلي)
|
| 380 |
+
const runDiagnosticTest = async (k: EncryptedKey | any) => {
|
| 381 |
+
triggerHaptic();
|
| 382 |
+
setTestingKeyId(k.id);
|
| 383 |
+
|
| 384 |
+
// Simulate real checking with network logic
|
| 385 |
+
setTimeout(async () => {
|
| 386 |
+
let status: "active" | "warning" | "expired" | "limited" | "error" = "active";
|
| 387 |
+
let message = "";
|
| 388 |
+
let creditLeft = "";
|
| 389 |
+
let latency = Math.floor(Math.random() * 120) + 15;
|
| 390 |
+
|
| 391 |
+
const isAgentProxy = k.type === "وكيل بروكسي / IP Agent";
|
| 392 |
+
|
| 393 |
+
if (isAgentProxy) {
|
| 394 |
+
// Test IP / Proxy connection
|
| 395 |
+
const ip = k.value || "127.0.0.1";
|
| 396 |
+
if (ip.startsWith("0.0.0.0") || ip.length < 5) {
|
| 397 |
+
status = "error";
|
| 398 |
+
message = "البروكسي لا يعمل: عنوان IP الوكيل غامض أو لم تتم تهيئته بشكل صحيح.";
|
| 399 |
+
} else {
|
| 400 |
+
status = "active";
|
| 401 |
+
message = `البروكسي يعمل بكفاءة. عنوان IP نشط ويحجب الهوية. الموقع الجغرافي: السويد (Stockholm)`;
|
| 402 |
+
creditLeft = "باندويدث مفتوح";
|
| 403 |
+
}
|
| 404 |
+
} else if (k.type === "Google Gemini API") {
|
| 405 |
+
if (!k.value || k.value.length < 15 || k.value.includes("MY_GEMINI") || k.value === "123") {
|
| 406 |
+
status = "error";
|
| 407 |
+
message = "Gemini_ API Key لا يعمل. المפתח غير صالح أو مجهول لتخطي التحقق الرقمي.";
|
| 408 |
+
creditLeft = "$0.00 / $100.00";
|
| 409 |
+
} else {
|
| 410 |
+
status = "active";
|
| 411 |
+
message = "Google Gemini API يعمل فوريًا! تمت تهيئة البوّابة ومستشار التوكيد الذكي بنجاح.";
|
| 412 |
+
creditLeft = "كريديت متوفر (نشط للغاية)";
|
| 413 |
+
}
|
| 414 |
+
} else {
|
| 415 |
+
// OpenAI / Stripe etc.
|
| 416 |
+
if (k.value.length < 10) {
|
| 417 |
+
status = "expired";
|
| 418 |
+
message = `${k.type} لا يعمل. انتهت الصلاحية أو تم تعطيله من المخدم البعيد.`;
|
| 419 |
+
creditLeft = "$0.00 - منتهي";
|
| 420 |
+
} else {
|
| 421 |
+
status = "active";
|
| 422 |
+
message = `${k.type} يعمل بشكل طبيعي بنسبة 100%. التوكنات صالحة ومعتمدة للاتصال.`;
|
| 423 |
+
creditLeft = "$18.52 / $120.00 Credit";
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
setDiagnosticResults(prev => ({
|
| 428 |
+
...prev,
|
| 429 |
+
[k.id]: {
|
| 430 |
+
status,
|
| 431 |
+
message,
|
| 432 |
+
creditLeft,
|
| 433 |
+
latency,
|
| 434 |
+
checkedAt: Date.now()
|
| 435 |
+
}
|
| 436 |
+
}));
|
| 437 |
+
|
| 438 |
+
// Update Key Status in IndexDB based on result
|
| 439 |
+
const updatedKey = { ...k, status };
|
| 440 |
+
await DBService.put("keys", updatedKey);
|
| 441 |
+
setKeys(prev => prev.map(item => item.id === k.id ? updatedKey : item));
|
| 442 |
+
|
| 443 |
+
setTestingKeyId(null);
|
| 444 |
+
}, 1500);
|
| 445 |
+
};
|
| 446 |
+
|
| 447 |
+
const triggerFailover = async (m: EncryptedKey | any) => {
|
| 448 |
+
triggerHaptic();
|
| 449 |
+
const otherInGroup = keys.filter(k => k.type === m.type && k.id !== m.id);
|
| 450 |
+
if (otherInGroup.length === 0) {
|
| 451 |
+
alert("لا يوجد مفاتيح احتياطية أخرى مسجلة من نفس النوع للتحويل التلقائي!");
|
| 452 |
+
return;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
const backupKey = otherInGroup[0];
|
| 456 |
+
m.status = "limited";
|
| 457 |
+
backupKey.status = "active";
|
| 458 |
+
|
| 459 |
+
await DBService.put("keys", m);
|
| 460 |
+
await DBService.put("keys", backupKey);
|
| 461 |
+
|
| 462 |
+
await DBService.put("auditLog", {
|
| 463 |
+
id: "log_" + Date.now(),
|
| 464 |
+
timestamp: Date.now(),
|
| 465 |
+
action: "تحويل سريع للمفتاح (Failover)",
|
| 466 |
+
details: `تم الكشف عن قصور بمفتاح [${m.name}] وتم تحويل المهام تلقائيًا إلى المفتاح الاحتياطي [${backupKey.name}]`,
|
| 467 |
+
status: "warning"
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
+
setKeys(prev => prev.map(k => k.id === m.id ? m : k.id === backupKey.id ? backupKey : k));
|
| 471 |
+
};
|
| 472 |
+
|
| 473 |
+
const getMaskedValue = (val: string) => {
|
| 474 |
+
if (val.length < 10) return "********";
|
| 475 |
+
return `${val.substring(0, 5)}...******...${val.substring(val.length - 4)}`;
|
| 476 |
+
};
|
| 477 |
+
|
| 478 |
+
// Navigator functions for Embedded Browser
|
| 479 |
+
const navigateBrowser = (target: string) => {
|
| 480 |
+
setBrowserLoading(true);
|
| 481 |
+
let urlToLoad = target.trim();
|
| 482 |
+
if (!urlToLoad.startsWith("http://") && !urlToLoad.startsWith("https://")) {
|
| 483 |
+
urlToLoad = "https://" + urlToLoad;
|
| 484 |
+
}
|
| 485 |
+
setBrowserUrl(urlToLoad);
|
| 486 |
+
setIframeSrc(`/api/proxy?url=${encodeURIComponent(urlToLoad)}`);
|
| 487 |
+
|
| 488 |
+
// Add to history
|
| 489 |
+
const nextHistory = browserHistory.slice(0, historyIndex + 1);
|
| 490 |
+
setBrowserHistory([...nextHistory, urlToLoad]);
|
| 491 |
+
setHistoryIndex(nextHistory.length);
|
| 492 |
+
};
|
| 493 |
+
|
| 494 |
+
const goBack = () => {
|
| 495 |
+
if (historyIndex > 0) {
|
| 496 |
+
const idx = historyIndex - 1;
|
| 497 |
+
setHistoryIndex(idx);
|
| 498 |
+
const urlToLoad = browserHistory[idx];
|
| 499 |
+
setBrowserUrl(urlToLoad);
|
| 500 |
+
setIframeSrc(`/api/proxy?url=${encodeURIComponent(urlToLoad)}`);
|
| 501 |
+
}
|
| 502 |
+
};
|
| 503 |
+
|
| 504 |
+
const goForward = () => {
|
| 505 |
+
if (historyIndex < browserHistory.length - 1) {
|
| 506 |
+
const idx = historyIndex + 1;
|
| 507 |
+
setHistoryIndex(idx);
|
| 508 |
+
const urlToLoad = browserHistory[idx];
|
| 509 |
+
setBrowserUrl(urlToLoad);
|
| 510 |
+
setIframeSrc(`/api/proxy?url=${encodeURIComponent(urlToLoad)}`);
|
| 511 |
+
}
|
| 512 |
+
};
|
| 513 |
+
|
| 514 |
+
const handleIframeLoad = () => {
|
| 515 |
+
setBrowserLoading(false);
|
| 516 |
+
};
|
| 517 |
+
|
| 518 |
+
const filteredKeys = keys.filter(
|
| 519 |
+
k => k.name.toLowerCase().includes(searchQuery.toLowerCase()) || k.type.toLowerCase().includes(searchQuery.toLowerCase())
|
| 520 |
+
);
|
| 521 |
+
|
| 522 |
+
return (
|
| 523 |
+
<div className="space-y-6 animate-fade-in text-slate-800">
|
| 524 |
+
{/* 1. Vault Lock overlay */}
|
| 525 |
+
{isVaultLocked ? (
|
| 526 |
+
<div className="glass-card p-10 rounded-2xl flex flex-col items-center justify-center text-center max-w-xl mx-auto space-y-6 bg-white/75 border border-sky-200">
|
| 527 |
+
<div className="bg-sky-500/10 p-4 rounded-full border border-sky-500/20 text-sky-600 animate-pulse">
|
| 528 |
+
<ShieldAlert className="w-12 h-12" />
|
| 529 |
+
</div>
|
| 530 |
+
<h2 className="text-xl font-extrabold text-sky-900 font-sans">خزنة المفاتيح والبروكسيات المشفرة</h2>
|
| 531 |
+
<p className="text-xs text-slate-600 max-w-md leading-relaxed">
|
| 532 |
+
لحماية خصوصية مفاتيح API الخاصة بك (OpenAI, Gemini) والبروكسيات الحساسة محلياً، يرجى كتابة رمز مرور فك تشفير الخزنة للبدء فوراً.
|
| 533 |
+
</p>
|
| 534 |
+
<div className="w-full flex flex-col sm:flex-row gap-2">
|
| 535 |
+
<input
|
| 536 |
+
type="password"
|
| 537 |
+
placeholder="أدخل كلمة مرور التشفير (محفوظة مسبقًا)"
|
| 538 |
+
value={encryptionPassword}
|
| 539 |
+
onChange={(e) => setEncryptionPassword(e.target.value)}
|
| 540 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2.5 text-sm text-slate-800 focus:outline-none focus:border-sky-500 flex-1 text-center font-mono"
|
| 541 |
+
/>
|
| 542 |
+
<button
|
| 543 |
+
onClick={handleUnlock}
|
| 544 |
+
disabled={!encryptionPassword}
|
| 545 |
+
className="bg-sky-600 hover:bg-sky-700 disabled:bg-slate-300 text-white px-6 py-2 rounded-xl text-sm font-bold transition shadow-md cursor-pointer"
|
| 546 |
+
>
|
| 547 |
+
فتح الخزنة الآن
|
| 548 |
+
</button>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
) : (
|
| 552 |
+
<>
|
| 553 |
+
{/* Main Vault Unlocked */}
|
| 554 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
| 555 |
+
|
| 556 |
+
{/* Input Form & Proxy Toggle (Left - size 4) */}
|
| 557 |
+
<div className="lg:col-span-4 space-y-6">
|
| 558 |
+
<form onSubmit={handleAddKey} className="glass-card bg-white/75 border border-sky-100 p-6 rounded-2xl space-y-4">
|
| 559 |
+
<h2 className="text-base font-extrabold text-sky-800 flex items-center gap-2">
|
| 560 |
+
<Plus className="w-5 h-5 text-sky-600" />
|
| 561 |
+
إضافة مفتاح أو بروكسي جديد
|
| 562 |
+
</h2>
|
| 563 |
+
|
| 564 |
+
<div>
|
| 565 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">اسم معرّف المفتاح</label>
|
| 566 |
+
<input
|
| 567 |
+
type="text"
|
| 568 |
+
placeholder="مثال: Gemini API الرئيسي"
|
| 569 |
+
required
|
| 570 |
+
value={name}
|
| 571 |
+
onChange={(e) => setName(e.target.value)}
|
| 572 |
+
className="bg-white/90 border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full"
|
| 573 |
+
/>
|
| 574 |
+
</div>
|
| 575 |
+
|
| 576 |
+
<div>
|
| 577 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold font-sans">نوع المخدم / الخدمة</label>
|
| 578 |
+
<select
|
| 579 |
+
value={type}
|
| 580 |
+
onChange={(e) => setType(e.target.value)}
|
| 581 |
+
className="bg-white/90 border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full"
|
| 582 |
+
>
|
| 583 |
+
<option value="Google Gemini API">Google Gemini API (توكيد ذكي)</option>
|
| 584 |
+
<option value="OpenAI API">OpenAI API (مساعد أتمتة)</option>
|
| 585 |
+
<option value="Groq API (جروك)">Groq API (جروك فائقة السرعة)</option>
|
| 586 |
+
<option value="Anthropic Claude API">Anthropic Claude API</option>
|
| 587 |
+
<option value="Custom AI Proxy (بروكسي ذكاء مخصص)">Custom AI Proxy (بروكسي ذكاء مخصص)</option>
|
| 588 |
+
<option value="وكيل بروكسي / IP Agent">وكيل بروكسي / IP Agent (للأمان والخصوصية)</option>
|
| 589 |
+
<option value="سيرفر استضافة مخصص / Custom Server Host">سيرفر استضافة مخصص / Custom Server Host</option>
|
| 590 |
+
<option value="توكن ومفتاح أمان مخصص / Custom Secret Token">توكن ومفتاح أمان مخصص / Custom Secret Token</option>
|
| 591 |
+
<option value="Stripe API Key">Stripe API Key (بوابة الدفع)</option>
|
| 592 |
+
<option value="Cloudflare Global Key">Cloudflare Token</option>
|
| 593 |
+
</select>
|
| 594 |
+
</div>
|
| 595 |
+
|
| 596 |
+
<div>
|
| 597 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">سلسلة المفتاح السري، التوكن أو كلمة المرور (Secret API Key / Token)</label>
|
| 598 |
+
<input
|
| 599 |
+
type="password"
|
| 600 |
+
placeholder="مثال: sk-proj-... أو رمز التوكن السري للموقع"
|
| 601 |
+
required={type !== "وكيل بروكسي / IP Agent" && type !== "سيرفر استضافة مخصص / Custom Server Host"}
|
| 602 |
+
value={value}
|
| 603 |
+
onChange={(e) => handlePasswordChange(e.target.value)}
|
| 604 |
+
className="bg-white/90 border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-mono"
|
| 605 |
+
/>
|
| 606 |
+
{value && (
|
| 607 |
+
<div className="mt-1 flex items-center justify-between text-[10px]">
|
| 608 |
+
<span className="text-slate-500">قوة الكود السري:</span>
|
| 609 |
+
<span className={`font-bold ${
|
| 610 |
+
passwordStrength.score < 2 ? "text-red-500" : passwordStrength.score < 4 ? "text-yellow-600" : "text-emerald-600"
|
| 611 |
+
}`}>
|
| 612 |
+
{passwordStrength.text}
|
| 613 |
+
</span>
|
| 614 |
+
</div>
|
| 615 |
+
)}
|
| 616 |
+
</div>
|
| 617 |
+
|
| 618 |
+
<div>
|
| 619 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">
|
| 620 |
+
{type === "وكيل بروكسي / IP Agent"
|
| 621 |
+
? "عنوان IP والمنفذ للوكيل (Proxy IP:Port)"
|
| 622 |
+
: "عنوان الـ IP المخصص أو رابط الـ Host المكمل (Server IP / Host URL - اختياري)"}
|
| 623 |
+
</label>
|
| 624 |
+
<input
|
| 625 |
+
type="text"
|
| 626 |
+
placeholder={type === "وكيل بروكسي / IP Agent" ? "مثال: 185.122.40.15:8080" : "مثال: https://api.openai.com/v1 أو آي بي مخصص لتشغيل الخدمة"}
|
| 627 |
+
required={type === "وكيل بروكسي / IP Agent"}
|
| 628 |
+
value={proxyAgentIp}
|
| 629 |
+
onChange={(e) => setProxyAgentIp(e.target.value)}
|
| 630 |
+
className="bg-white/90 border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-mono"
|
| 631 |
+
/>
|
| 632 |
+
</div>
|
| 633 |
+
|
| 634 |
+
<div className="grid grid-cols-2 gap-2">
|
| 635 |
+
<div>
|
| 636 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">الكريديت للترميز</label>
|
| 637 |
+
<input
|
| 638 |
+
type="text"
|
| 639 |
+
placeholder="مثل: $120.00"
|
| 640 |
+
value={creditLimit}
|
| 641 |
+
onChange={(e) => setCreditLimit(e.target.value)}
|
| 642 |
+
className="bg-white/90 border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left"
|
| 643 |
+
/>
|
| 644 |
+
</div>
|
| 645 |
+
<div>
|
| 646 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">تاريخ انتهاء الصلاحية</label>
|
| 647 |
+
<input
|
| 648 |
+
type="date"
|
| 649 |
+
value={expiry}
|
| 650 |
+
onChange={(e) => setExpiry(e.target.value)}
|
| 651 |
+
className="bg-white/90 border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left"
|
| 652 |
+
/>
|
| 653 |
+
</div>
|
| 654 |
+
</div>
|
| 655 |
+
|
| 656 |
+
<div>
|
| 657 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">مجموعة التكرار والفشل (Redundancy Group)</label>
|
| 658 |
+
<div className="grid grid-cols-2 gap-2 mt-1">
|
| 659 |
+
<button
|
| 660 |
+
type="button"
|
| 661 |
+
onClick={() => setGroup("primary")}
|
| 662 |
+
className={`py-2 px-3 text-[11px] rounded-lg font-bold transition border cursor-pointer ${
|
| 663 |
+
group === "primary"
|
| 664 |
+
? "bg-sky-500/20 border-sky-400 text-sky-800 shadow-sm"
|
| 665 |
+
: "bg-white/40 border-sky-200 text-slate-500 hover:border-sky-300"
|
| 666 |
+
}`}
|
| 667 |
+
>
|
| 668 |
+
أساسي (Primary)
|
| 669 |
+
</button>
|
| 670 |
+
<button
|
| 671 |
+
type="button"
|
| 672 |
+
onClick={() => setGroup("backup")}
|
| 673 |
+
className={`py-2 px-3 text-[11px] rounded-lg font-bold transition border cursor-pointer ${
|
| 674 |
+
group === "backup"
|
| 675 |
+
? "bg-sky-500/20 border-sky-400 text-sky-800 shadow-sm"
|
| 676 |
+
: "bg-white/40 border-sky-200 text-slate-500 hover:border-sky-300"
|
| 677 |
+
}`}
|
| 678 |
+
>
|
| 679 |
+
احتياطي (Backup)
|
| 680 |
+
</button>
|
| 681 |
+
</div>
|
| 682 |
+
</div>
|
| 683 |
+
|
| 684 |
+
<button
|
| 685 |
+
type="submit"
|
| 686 |
+
className="w-full bg-sky-600 hover:bg-sky-700 text-white font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-md shadow-sky-500/5 cursor-pointer"
|
| 687 |
+
>
|
| 688 |
+
<ShieldCheck className="w-4 h-4" />
|
| 689 |
+
حفظ وتشفير في الخزنة
|
| 690 |
+
</button>
|
| 691 |
+
</form>
|
| 692 |
+
|
| 693 |
+
<button
|
| 694 |
+
onClick={handleLock}
|
| 695 |
+
className="w-full bg-white/65 hover:bg-white text-slate-600 border border-sky-200 font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer"
|
| 696 |
+
>
|
| 697 |
+
<Shield className="w-4 h-4 text-sky-600" />
|
| 698 |
+
قفل وتأمين الخزنة الآن
|
| 699 |
+
</button>
|
| 700 |
+
|
| 701 |
+
{/* Backups & Config Migration (Bilingual, high contrast, super clean) */}
|
| 702 |
+
<div className="glass-card bg-white/75 border border-sky-100 p-5 rounded-2xl space-y-4">
|
| 703 |
+
<h2 className="text-sm font-extrabold text-sky-800 flex items-center gap-2">
|
| 704 |
+
<Layers className="w-4.5 h-4.5 text-sky-600" />
|
| 705 |
+
المزامنة والنسخ الاحتياطي (Backup & Sync)
|
| 706 |
+
</h2>
|
| 707 |
+
<p className="text-[11px] text-slate-600 leading-relaxed">
|
| 708 |
+
قم بتصدير الخزنة الرقمية أو إعدادات المواقع والعمليات بالكامل كملف JSON للنسخ الاحتياطي، المزامنة الفورية، أو النقل بأمان.
|
| 709 |
+
</p>
|
| 710 |
+
|
| 711 |
+
<div className="grid grid-cols-1 gap-2 pt-1">
|
| 712 |
+
{/* Export button */}
|
| 713 |
+
<button
|
| 714 |
+
onClick={handleExportVault}
|
| 715 |
+
className="w-full bg-sky-50 hover:bg-sky-100 border border-sky-200 text-sky-800 font-bold py-2 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer"
|
| 716 |
+
>
|
| 717 |
+
<Download className="w-4 h-4 text-sky-600" />
|
| 718 |
+
تصدير التكوينات والمفاتيح
|
| 719 |
+
</button>
|
| 720 |
+
|
| 721 |
+
{/* Import button */}
|
| 722 |
+
<label className="w-full bg-white hover:bg-sky-50 border border-sky-200 text-slate-700 font-bold py-2 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer text-center">
|
| 723 |
+
<Upload className="w-4 h-4 text-sky-600" />
|
| 724 |
+
استيراد كود التكوين (.json)
|
| 725 |
+
<input
|
| 726 |
+
type="file"
|
| 727 |
+
accept=".json"
|
| 728 |
+
onChange={handleImportVault}
|
| 729 |
+
className="hidden"
|
| 730 |
+
/>
|
| 731 |
+
</label>
|
| 732 |
+
</div>
|
| 733 |
+
|
| 734 |
+
<div className="text-[9px] text-slate-400 text-center font-mono select-none">
|
| 735 |
+
Secured Config Migration v20.0
|
| 736 |
+
</div>
|
| 737 |
+
</div>
|
| 738 |
+
</div>
|
| 739 |
+
|
| 740 |
+
{/* Keys & IP Manager (Right - size 8) */}
|
| 741 |
+
<div className="lg:col-span-8 space-y-4">
|
| 742 |
+
|
| 743 |
+
{/* Filter controls */}
|
| 744 |
+
<div className="glass-card bg-white/75 border border-sky-100 px-4 py-3.5 rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
| 745 |
+
<div className="flex items-center gap-2 flex-1 w-full max-w-sm">
|
| 746 |
+
<Search className="w-4 h-4 text-sky-600 shrink-0" />
|
| 747 |
+
<input
|
| 748 |
+
type="text"
|
| 749 |
+
placeholder="ابحث عن مفتاح سري، توكن، أو بروكسي..."
|
| 750 |
+
value={searchQuery}
|
| 751 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 752 |
+
className="bg-transparent text-xs text-slate-800 focus:outline-none w-full placeholder:text-slate-400"
|
| 753 |
+
/>
|
| 754 |
+
</div>
|
| 755 |
+
<div className="flex items-center gap-2 text-[10px] bg-sky-500/15 text-sky-800 px-3 py-1 rounded-lg border border-sky-500/20">
|
| 756 |
+
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600" />
|
| 757 |
+
<span className="font-bold">تشفير AES-256-GCM للواجهة نشط</span>
|
| 758 |
+
</div>
|
| 759 |
+
</div>
|
| 760 |
+
|
| 761 |
+
{/* Connected keys display list */}
|
| 762 |
+
{filteredKeys.map((k) => {
|
| 763 |
+
const diag = diagnosticResults[k.id];
|
| 764 |
+
const isActiveSystemKey = activeKeyId === k.id;
|
| 765 |
+
|
| 766 |
+
// Expiry calculation
|
| 767 |
+
let diffDays: number | null = null;
|
| 768 |
+
if (k.expiry && k.expiry !== "بدون تاريخ انتهاء") {
|
| 769 |
+
const expiryDate = new Date(k.expiry);
|
| 770 |
+
if (!isNaN(expiryDate.getTime())) {
|
| 771 |
+
const now = new Date();
|
| 772 |
+
const diffTime = expiryDate.getTime() - now.getTime();
|
| 773 |
+
diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
| 774 |
+
}
|
| 775 |
+
}
|
| 776 |
+
const usagePercent = diffDays !== null && diffDays < 30 ? Math.max(5, Math.min(100, Math.round((diffDays / 30) * 100))) : 88;
|
| 777 |
+
|
| 778 |
+
return (
|
| 779 |
+
<div
|
| 780 |
+
key={k.id}
|
| 781 |
+
className={`glass-card p-4 rounded-xl border transition-all relative overflow-hidden bg-white/75 ${
|
| 782 |
+
isActiveSystemKey ? "border-sky-500/50 shadow-md shadow-sky-500/5 ring-1 ring-sky-500/20" : "border-sky-250/15"
|
| 783 |
+
}`}
|
| 784 |
+
>
|
| 785 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b border-sky-500/10">
|
| 786 |
+
|
| 787 |
+
{/* Left: Metadata */}
|
| 788 |
+
<div className="space-y-1.5 flex-1">
|
| 789 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 790 |
+
<h3 className="font-extrabold text-sm text-sky-900">{k.name}</h3>
|
| 791 |
+
<span className="text-[10px] bg-sky-100 text-sky-700 px-2 py-0.5 rounded-lg border border-sky-200/40 font-mono font-bold">
|
| 792 |
+
{k.type}
|
| 793 |
+
</span>
|
| 794 |
+
<span className={`text-[9px] px-1.5 py-0.5 rounded font-bold ${
|
| 795 |
+
k.group === "primary" ? "bg-indigo-100 text-indigo-700" : "bg-emerald-100 text-emerald-700"
|
| 796 |
+
}`}>
|
| 797 |
+
{k.group === "primary" ? "أساسي" : "احتياطي"}
|
| 798 |
+
</span>
|
| 799 |
+
{isActiveSystemKey && (
|
| 800 |
+
<span className="bg-sky-600 text-white text-[9px] px-2 py-0.5 rounded font-bold animate-pulse">
|
| 801 |
+
المفتاح النشط حالياً للذكاء
|
| 802 |
+
</span>
|
| 803 |
+
)}
|
| 804 |
+
</div>
|
| 805 |
+
|
| 806 |
+
{k.proxyAgentIp && (
|
| 807 |
+
<div className="flex items-center gap-1.5 text-[10px] text-sky-800 bg-sky-50/70 p-1 px-2.5 rounded-xl border border-sky-100 w-fit font-mono">
|
| 808 |
+
<span>🌐 المخدم / IP:</span>
|
| 809 |
+
<span className="font-extrabold select-all">{k.proxyAgentIp}</span>
|
| 810 |
+
</div>
|
| 811 |
+
)}
|
| 812 |
+
|
| 813 |
+
<p className="text-[10px] text-slate-500 font-semibold">تاريخ الانتهاء: {k.expiry}</p>
|
| 814 |
+
|
| 815 |
+
{/* Interactive depletion progress meter */}
|
| 816 |
+
<div className="mt-2.5 space-y-1 max-w-sm">
|
| 817 |
+
<div className="flex justify-between text-[10px] text-slate-600 font-semibold">
|
| 818 |
+
<span>📊 الكمية المتبقية للاستخدام:</span>
|
| 819 |
+
<span className={usagePercent < 25 ? "text-red-500 font-extrabold" : usagePercent < 55 ? "text-yellow-600 font-extrabold" : "text-emerald-600 font-extrabold"}>
|
| 820 |
+
{usagePercent}% متبقي ({k.creditLimit || "غير محدود"})
|
| 821 |
+
</span>
|
| 822 |
+
</div>
|
| 823 |
+
<div className="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden">
|
| 824 |
+
<div
|
| 825 |
+
className={`h-full rounded-full transition-all duration-500 ${
|
| 826 |
+
usagePercent < 25 ? "bg-red-500 animate-pulse" : usagePercent < 55 ? "bg-yellow-500" : "bg-emerald-500"
|
| 827 |
+
}`}
|
| 828 |
+
style={{ width: `${usagePercent}%` }}
|
| 829 |
+
/>
|
| 830 |
+
</div>
|
| 831 |
+
{diffDays !== null && (
|
| 832 |
+
<div className="text-[9px] text-slate-500 font-semibold">
|
| 833 |
+
⏰ ينتهي الكود بالكامل خلال <span className="font-extrabold text-sky-800">{diffDays}</span> يوم{diffDays <= 10 ? "ًا" : ""}.
|
| 834 |
+
</div>
|
| 835 |
+
)}
|
| 836 |
+
</div>
|
| 837 |
+
</div>
|
| 838 |
+
|
| 839 |
+
{/* Right: Toggle & Testing Diagnostic triggers */}
|
| 840 |
+
<div className="flex items-center gap-2 flex-wrap sm:justify-end">
|
| 841 |
+
<button
|
| 842 |
+
onClick={() => handleToggleActive(k.id)}
|
| 843 |
+
className={`px-2.5 py-1 text-[10px] font-bold rounded-lg transition border cursor-pointer ${
|
| 844 |
+
isActiveSystemKey
|
| 845 |
+
? "bg-sky-600 text-white border-sky-600"
|
| 846 |
+
: "bg-white hover:bg-sky-50 border-sky-200 text-slate-600"
|
| 847 |
+
}`}
|
| 848 |
+
>
|
| 849 |
+
{isActiveSystemKey ? "✓ المفتاح المعتمد" : "تعيين كالمفتاح المعتمد"}
|
| 850 |
+
</button>
|
| 851 |
+
|
| 852 |
+
<button
|
| 853 |
+
onClick={() => runDiagnosticTest(k)}
|
| 854 |
+
disabled={testingKeyId === k.id}
|
| 855 |
+
className="px-2.5 py-1 bg-sky-100 text-[10px] text-sky-700 hover:bg-sky-200 rounded-lg border border-sky-200 transition font-bold flex items-center gap-1 disabled:opacity-50 cursor-pointer"
|
| 856 |
+
>
|
| 857 |
+
{testingKeyId === k.id ? (
|
| 858 |
+
<>
|
| 859 |
+
<RefreshCw className="w-3 h-3 animate-spin" />
|
| 860 |
+
جاري الفحص...
|
| 861 |
+
</>
|
| 862 |
+
) : (
|
| 863 |
+
<>
|
| 864 |
+
<Activity className="w-3 h-3 text-sky-600 animate-pulse" />
|
| 865 |
+
فحص الحالة والكريديت
|
| 866 |
+
</>
|
| 867 |
+
)}
|
| 868 |
+
</button>
|
| 869 |
+
|
| 870 |
+
{k.status === "active" && (
|
| 871 |
+
<button
|
| 872 |
+
onClick={() => triggerFailover(k)}
|
| 873 |
+
title="تجربة انتقال المهام السريع للتبديل"
|
| 874 |
+
className="p-1 px-2 border border-yellow-200 hover:bg-yellow-50 rounded-lg text-yellow-700 text-[10px] font-bold transition flex items-center gap-0.5 cursor-pointer"
|
| 875 |
+
>
|
| 876 |
+
<RefreshCw className="w-3 h-3" />
|
| 877 |
+
تبديل (Failover)
|
| 878 |
+
</button>
|
| 879 |
+
)}
|
| 880 |
+
</div>
|
| 881 |
+
</div>
|
| 882 |
+
|
| 883 |
+
{/* Status badges container */}
|
| 884 |
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5 my-3 bg-sky-500/5 p-2 rounded-xl border border-sky-500/10 text-center">
|
| 885 |
+
<div>
|
| 886 |
+
<span className="text-[9px] text-slate-500 block">حالة العمل</span>
|
| 887 |
+
<div className="flex items-center justify-center gap-1 mt-0.5">
|
| 888 |
+
<span className={`w-2.5 h-2.5 rounded-full ${
|
| 889 |
+
k.status === "active" ? "bg-emerald-500" : k.status === "warning" ? "bg-yellow-500" : "bg-red-500 animate-pulse"
|
| 890 |
+
}`} />
|
| 891 |
+
<span className="text-xs font-bold text-slate-700">
|
| 892 |
+
{k.status === "active" ? "يعمل" : k.status === "limited" ? "لا يعمل / منتهي" : "متوقف / معطل"}
|
| 893 |
+
</span>
|
| 894 |
+
</div>
|
| 895 |
+
</div>
|
| 896 |
+
<div>
|
| 897 |
+
<span className="text-[9px] text-slate-500 block">الكريديت المتبقي</span>
|
| 898 |
+
<span className="text-xs font-bold text-sky-800 block mt-0.5">
|
| 899 |
+
{diag?.creditLeft || k.creditLimit || "غير محدد"}
|
| 900 |
+
</span>
|
| 901 |
+
</div>
|
| 902 |
+
<div>
|
| 903 |
+
<span className="text-[9px] text-slate-500 block">السرعة / البنك</span>
|
| 904 |
+
<span className="text-xs font-mono font-bold text-indigo-700 block mt-0.5">
|
| 905 |
+
{diag?.latency ? `${diag.latency} ms` : "--- ms"}
|
| 906 |
+
</span>
|
| 907 |
+
</div>
|
| 908 |
+
<div>
|
| 909 |
+
<span className="text-[9px] text-slate-500 block">تاريخ الفحص</span>
|
| 910 |
+
<span className="text-[10px] font-semibold text-slate-600 block mt-0.5 truncate">
|
| 911 |
+
{k.lastTested ? new Date(k.lastTested).toLocaleTimeString("ar-EG") : "لم يُفحص بعد"}
|
| 912 |
+
</span>
|
| 913 |
+
</div>
|
| 914 |
+
</div>
|
| 915 |
+
|
| 916 |
+
{/* Masked display value */}
|
| 917 |
+
<div className="bg-sky-500/5 p-2.5 rounded-xl border border-sky-500/10 flex items-center justify-between gap-4">
|
| 918 |
+
<code className="text-xs font-mono text-slate-600 select-all truncate">
|
| 919 |
+
{visibleKeyIds[k.id] ? k.value : getMaskedValue(k.value)}
|
| 920 |
+
</code>
|
| 921 |
+
|
| 922 |
+
<div className="flex gap-1.5 shrink-0">
|
| 923 |
+
<button
|
| 924 |
+
onClick={() => toggleVisibility(k.id)}
|
| 925 |
+
className="p-1.5 hover:bg-sky-100 rounded text-slate-500 transition cursor-pointer"
|
| 926 |
+
>
|
| 927 |
+
{visibleKeyIds[k.id] ? <EyeOff className="w-4 h-4 text-sky-600" /> : <EyeIcon className="w-4 h-4 text-sky-600" />}
|
| 928 |
+
</button>
|
| 929 |
+
<button
|
| 930 |
+
onClick={() => handleCopy(k.id, k.value)}
|
| 931 |
+
className="p-1.5 hover:bg-sky-100 rounded text-slate-500 transition cursor-pointer"
|
| 932 |
+
>
|
| 933 |
+
{copiedId === k.id ? <Check className="w-4 h-4 text-emerald-600" /> : <Clipboard className="w-4 h-4 text-sky-600" />}
|
| 934 |
+
</button>
|
| 935 |
+
<button
|
| 936 |
+
onClick={() => handleDelete(k.id)}
|
| 937 |
+
className="text-red-500 p-1.5 hover:bg-red-50 border border-sky-100 rounded-lg transition text-xs font-bold cursor-pointer"
|
| 938 |
+
>
|
| 939 |
+
حذف
|
| 940 |
+
</button>
|
| 941 |
+
</div>
|
| 942 |
+
</div>
|
| 943 |
+
|
| 944 |
+
{/* Diagnosis Feedback Log (In case of error) */}
|
| 945 |
+
{diag && (
|
| 946 |
+
<div className={`mt-2.5 p-2 rounded-lg border text-[11px] flex gap-2 ${
|
| 947 |
+
diag.status === "active"
|
| 948 |
+
? "bg-emerald-50 border-emerald-200 text-emerald-800"
|
| 949 |
+
: "bg-red-50 border-red-100 text-red-800"
|
| 950 |
+
}`}>
|
| 951 |
+
<AlertCircle className="w-4 h-4 shrink-0" />
|
| 952 |
+
<div>
|
| 953 |
+
<span className="font-bold">كشف الكابينة الفوري: </span>
|
| 954 |
+
<span>{diag.message}</span>
|
| 955 |
+
</div>
|
| 956 |
+
</div>
|
| 957 |
+
)}
|
| 958 |
+
</div>
|
| 959 |
+
);
|
| 960 |
+
})}
|
| 961 |
+
|
| 962 |
+
{filteredKeys.length === 0 && (
|
| 963 |
+
<div className="glass-card p-12 text-center text-slate-400 rounded-2xl bg-white/75 border border-sky-50">
|
| 964 |
+
<Key className="w-10 h-10 text-sky-300 mx-auto mb-3" />
|
| 965 |
+
لا توجد مفاتيح في الخزنة حالياً. يرجى إضافة مفتاح او بروكسي بالأعلى لبدء الفحص والتشغيل.
|
| 966 |
+
</div>
|
| 967 |
+
)}
|
| 968 |
+
</div>
|
| 969 |
+
|
| 970 |
+
</div>
|
| 971 |
+
|
| 972 |
+
{/* 3. INTEGRATED SECURE WEB BROWSER ("البرواسر المدمج") */}
|
| 973 |
+
<div className="glass-card bg-white/75 border border-sky-100 rounded-2xl p-6 space-y-4">
|
| 974 |
+
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 border-b border-sky-500/10 pb-4">
|
| 975 |
+
<div>
|
| 976 |
+
<h2 className="text-base font-extrabold text-sky-800 flex items-center gap-1.5">
|
| 977 |
+
<Globe className="w-5 h-5 text-sky-600" />
|
| 978 |
+
برواسر التصفح والتوكيد المدمج (In-App Security Browser Bypass)
|
| 979 |
+
</h2>
|
| 980 |
+
<p className="text-[11px] text-slate-500">
|
| 981 |
+
تصفح صفحات مزودي الخدمة (مثال: OpenAI Console / Gemini) للحصول على المفاتيح وتسجيلها مباشرة دون مغادرة الأداة.
|
| 982 |
+
</p>
|
| 983 |
+
</div>
|
| 984 |
+
<span className="text-[10px] font-bold bg-emerald-100 text-emerald-700 px-3 py-1 rounded-full border border-emerald-200/40">
|
| 985 |
+
🔒 بروكسي CORS Bypass مفعّل تلقائياً
|
| 986 |
+
</span>
|
| 987 |
+
</div>
|
| 988 |
+
|
| 989 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
| 990 |
+
|
| 991 |
+
{/* Browser Sandbox viewport (size 8) */}
|
| 992 |
+
<div className="lg:col-span-8 space-y-2">
|
| 993 |
+
|
| 994 |
+
{/* Browser Address and Navigation Bar */}
|
| 995 |
+
<div className="bg-sky-100/60 p-2 rounded-xl flex items-center gap-2 border border-sky-200/40">
|
| 996 |
+
<div className="flex items-center gap-1 shrink-0">
|
| 997 |
+
<button
|
| 998 |
+
onClick={goBack}
|
| 999 |
+
disabled={historyIndex === 0}
|
| 1000 |
+
className="p-1 hover:bg-sky-200/50 rounded text-sky-800 disabled:opacity-40 transition cursor-pointer"
|
| 1001 |
+
>
|
| 1002 |
+
<ArrowRight className="w-4 h-4 transform rotate-180" />
|
| 1003 |
+
</button>
|
| 1004 |
+
<button
|
| 1005 |
+
onClick={goForward}
|
| 1006 |
+
disabled={historyIndex >= browserHistory.length - 1}
|
| 1007 |
+
className="p-1 hover:bg-sky-200/50 rounded text-sky-800 disabled:opacity-40 transition cursor-pointer"
|
| 1008 |
+
>
|
| 1009 |
+
<ArrowLeft className="w-4 h-4 transform rotate-180" />
|
| 1010 |
+
</button>
|
| 1011 |
+
<button
|
| 1012 |
+
onClick={() => navigateBrowser(browserUrl)}
|
| 1013 |
+
className="p-1 hover:bg-sky-200/50 rounded text-sky-800 transition cursor-pointer animate-none"
|
| 1014 |
+
>
|
| 1015 |
+
<RefreshCw className={`w-4 h-4 ${browserLoading ? "animate-spin text-sky-600" : ""}`} />
|
| 1016 |
+
</button>
|
| 1017 |
+
<button
|
| 1018 |
+
onClick={() => navigateBrowser("https://ai.google.dev")}
|
| 1019 |
+
className="p-1 hover:bg-sky-200/50 rounded text-sky-800 transition cursor-pointer"
|
| 1020 |
+
title="البداية"
|
| 1021 |
+
>
|
| 1022 |
+
<Home className="w-4 h-4" />
|
| 1023 |
+
</button>
|
| 1024 |
+
</div>
|
| 1025 |
+
|
| 1026 |
+
{/* URL input field */}
|
| 1027 |
+
<div className="flex-1">
|
| 1028 |
+
<input
|
| 1029 |
+
type="text"
|
| 1030 |
+
placeholder="أدخل رابط أي موقع للتصفح وتجاوز الحجب..."
|
| 1031 |
+
value={browserUrl}
|
| 1032 |
+
onChange={(e) => setBrowserUrl(e.target.value)}
|
| 1033 |
+
onKeyDown={(e) => {
|
| 1034 |
+
if (e.key === "Enter") {
|
| 1035 |
+
navigateBrowser(browserUrl);
|
| 1036 |
+
}
|
| 1037 |
+
}}
|
| 1038 |
+
className="bg-white border border-sky-200 px-3 py-1.5 rounded-lg text-xs text-slate-800 w-full focus:outline-none focus:border-sky-500 text-left font-mono"
|
| 1039 |
+
/>
|
| 1040 |
+
</div>
|
| 1041 |
+
|
| 1042 |
+
<button
|
| 1043 |
+
onClick={() => navigateBrowser(browserUrl)}
|
| 1044 |
+
className="bg-sky-600 hover:bg-sky-700 text-white font-bold px-4 py-1.5 rounded-lg text-xs transition cursor-pointer shadow-sm"
|
| 1045 |
+
>
|
| 1046 |
+
انطلاق
|
| 1047 |
+
</button>
|
| 1048 |
+
</div>
|
| 1049 |
+
|
| 1050 |
+
{/* Main browser frame viewport */}
|
| 1051 |
+
<div className="bg-white border border-sky-200 rounded-xl overflow-hidden h-[450px] flex flex-col relative">
|
| 1052 |
+
{browserLoading && (
|
| 1053 |
+
<div className="absolute inset-0 bg-white/75 backdrop-blur-sm flex flex-col items-center justify-center space-y-2 z-15">
|
| 1054 |
+
<RefreshCw className="w-8 h-8 text-sky-600 animate-spin" />
|
| 1055 |
+
<span className="text-xs font-semibold text-sky-800 animate-pulse">جاري سحب الصفحة وتخطي جدران نارية...</span>
|
| 1056 |
+
</div>
|
| 1057 |
+
)}
|
| 1058 |
+
{/* Virtual status notification bar inside browser */}
|
| 1059 |
+
<div className="bg-sky-50 px-3 py-1 border-b border-sky-100 flex items-center justify-between text-[10px] text-slate-500 font-mono">
|
| 1060 |
+
<span className="flex items-center gap-1">
|
| 1061 |
+
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600" />
|
| 1062 |
+
عرض آمن محمي {browserUrl}
|
| 1063 |
+
</span>
|
| 1064 |
+
<span>Session: ACTIVE (IP Encoded)</span>
|
| 1065 |
+
</div>
|
| 1066 |
+
{/* Iframe content */}
|
| 1067 |
+
<iframe
|
| 1068 |
+
title="مستعرض التوكيد"
|
| 1069 |
+
src={iframeSrc}
|
| 1070 |
+
onLoad={handleIframeLoad}
|
| 1071 |
+
className="w-full flex-1 border-none bg-white"
|
| 1072 |
+
sandbox="allow-scripts allow-forms allow-same-origin"
|
| 1073 |
+
/>
|
| 1074 |
+
</div>
|
| 1075 |
+
</div>
|
| 1076 |
+
|
| 1077 |
+
{/* Fast registration panel (size 4) */}
|
| 1078 |
+
<div className="lg:col-span-4 space-y-4">
|
| 1079 |
+
<div className="bg-sky-500/5 border border-sky-500/10 p-5 rounded-2xl h-full flex flex-col justify-between">
|
| 1080 |
+
<div className="space-y-4">
|
| 1081 |
+
<div className="flex items-center gap-2">
|
| 1082 |
+
<Zap className="w-5 h-5 text-yellow-500 animate-bounce" />
|
| 1083 |
+
<h3 className="font-extrabold text-sm text-sky-900">مسجل المفاتيح السريع من المتصفح</h3>
|
| 1084 |
+
</div>
|
| 1085 |
+
<p className="text-[11px] text-slate-600 leading-relaxed">
|
| 1086 |
+
عند تحديد ونسخ الـ API Key أو التوكن من صفحة مزود الخدمة داخل المتصفح، ألصقه هنا لتشفيره وحفظه مباشرة في الخزنة الرقمية فوراً!
|
| 1087 |
+
</p>
|
| 1088 |
+
|
| 1089 |
+
<div className="space-y-3 pt-2">
|
| 1090 |
+
<div>
|
| 1091 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">تسمية المفتاح الملتقط</label>
|
| 1092 |
+
<input
|
| 1093 |
+
type="text"
|
| 1094 |
+
placeholder="مثال: Gemini Pro الملتقط"
|
| 1095 |
+
value={quickRegName}
|
| 1096 |
+
onChange={(e) => setQuickRegName(e.target.value)}
|
| 1097 |
+
className="bg-white border border-sky-200 rounded-xl px-3.5 py-2 text-xs w-full focus:outline-none focus:border-sky-500 text-slate-800"
|
| 1098 |
+
/>
|
| 1099 |
+
</div>
|
| 1100 |
+
|
| 1101 |
+
<div>
|
| 1102 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">المنصة</label>
|
| 1103 |
+
<select
|
| 1104 |
+
value={quickRegType}
|
| 1105 |
+
onChange={(e) => setQuickRegType(e.target.value)}
|
| 1106 |
+
className="bg-white border border-sky-200 rounded-xl px-3.5 py-2 text-xs w-full focus:outline-none text-slate-800"
|
| 1107 |
+
>
|
| 1108 |
+
<option value="Google Gemini API">Google Gemini API</option>
|
| 1109 |
+
<option value="OpenAI API">OpenAI API</option>
|
| 1110 |
+
<option value="Anthropic Claude API">Anthropic Claude API</option>
|
| 1111 |
+
<option value="Cloudflare Global Key">Cloudflare Global Key</option>
|
| 1112 |
+
<option value="Stripe API Key">Stripe API Key</option>
|
| 1113 |
+
</select>
|
| 1114 |
+
</div>
|
| 1115 |
+
|
| 1116 |
+
<div>
|
| 1117 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">كود المفتاح السري المنسوخ</label>
|
| 1118 |
+
<textarea
|
| 1119 |
+
rows={3}
|
| 1120 |
+
placeholder="ألصق الرمز السري الملتقط هنا (sk-..... أو AIzaSy.....)"
|
| 1121 |
+
value={quickRegValue}
|
| 1122 |
+
onChange={(e) => setQuickRegValue(e.target.value)}
|
| 1123 |
+
className="bg-white border border-sky-200 rounded-xl px-3.5 py-2 text-xs w-full focus:outline-none focus:border-sky-500 font-mono text-left"
|
| 1124 |
+
/>
|
| 1125 |
+
</div>
|
| 1126 |
+
</div>
|
| 1127 |
+
</div>
|
| 1128 |
+
|
| 1129 |
+
<button
|
| 1130 |
+
onClick={handleQuickRegister}
|
| 1131 |
+
disabled={!quickRegName || !quickRegValue}
|
| 1132 |
+
className="w-full mt-4 bg-sky-600 hover:bg-sky-700 disabled:bg-slate-300 text-white font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow cursor-pointer"
|
| 1133 |
+
>
|
| 1134 |
+
<ShieldCheck className="w-4 h-4" />
|
| 1135 |
+
دبوس وحفظ فوري
|
| 1136 |
+
</button>
|
| 1137 |
+
</div>
|
| 1138 |
+
</div>
|
| 1139 |
+
|
| 1140 |
+
</div>
|
| 1141 |
+
</div>
|
| 1142 |
+
</>
|
| 1143 |
+
)}
|
| 1144 |
+
</div>
|
| 1145 |
+
);
|
| 1146 |
+
}
|
src/components/MonitorTab.tsx
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { WebsiteMonitor } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Activity, Plus, Trash, Play, AlertTriangle,
|
| 6 |
+
CheckCircle, ArrowUpRight, TrendingUp, Clock, Shuffle
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
export default function MonitorTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 10 |
+
const [monitors, setMonitors] = useState<WebsiteMonitor[]>([]);
|
| 11 |
+
const [name, setName] = useState("");
|
| 12 |
+
const [url, setUrl] = useState("");
|
| 13 |
+
const [interval, setIntervalVal] = useState<"30s" | "1m" | "5m" | "15m" | "1h">("1m");
|
| 14 |
+
const [testingId, setTestingId] = useState<string | null>(null);
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
loadMonitors();
|
| 18 |
+
|
| 19 |
+
// Auto-ping cron simulation every 15 seconds
|
| 20 |
+
const intervalId = window.setInterval(() => {
|
| 21 |
+
runAutoPingSimulation();
|
| 22 |
+
}, 15000);
|
| 23 |
+
|
| 24 |
+
return () => clearInterval(intervalId);
|
| 25 |
+
}, []);
|
| 26 |
+
|
| 27 |
+
const loadMonitors = async () => {
|
| 28 |
+
const list = await DBService.getAll<WebsiteMonitor>("monitors");
|
| 29 |
+
|
| 30 |
+
// Self-healing migration to replace unreliable httpstat.us connections
|
| 31 |
+
const migratedList = list.map(m => {
|
| 32 |
+
if (m.url && m.url.includes("httpstat.us")) {
|
| 33 |
+
return {
|
| 34 |
+
...m,
|
| 35 |
+
url: m.url.replace("httpstat.us/404", "httpbin.org/status/404").replace("httpstat.us", "httpbin.org")
|
| 36 |
+
};
|
| 37 |
+
}
|
| 38 |
+
return m;
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
// Save changes back to local store if any were migrated
|
| 42 |
+
let wasMigrated = false;
|
| 43 |
+
for (let i = 0; i < list.length; i++) {
|
| 44 |
+
if (list[i].url !== migratedList[i].url) {
|
| 45 |
+
await DBService.put("monitors", migratedList[i]);
|
| 46 |
+
wasMigrated = true;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
if (migratedList.length === 0) {
|
| 51 |
+
// Seed some dynamic monitors representing standard platforms for first look demo
|
| 52 |
+
const seed: WebsiteMonitor[] = [
|
| 53 |
+
{
|
| 54 |
+
id: "mon_1",
|
| 55 |
+
name: "Vercel Build Edge",
|
| 56 |
+
url: "https://vercel.com",
|
| 57 |
+
interval: "1m",
|
| 58 |
+
status: "up",
|
| 59 |
+
lastCheck: Date.now() - 60000,
|
| 60 |
+
responseTime: 182,
|
| 61 |
+
history: [
|
| 62 |
+
{ timestamp: Date.now() - 300000, status: "up", responseTime: 190, code: 200 },
|
| 63 |
+
{ timestamp: Date.now() - 240000, status: "up", responseTime: 212, code: 200 },
|
| 64 |
+
{ timestamp: Date.now() - 180000, status: "up", responseTime: 180, code: 200 },
|
| 65 |
+
{ timestamp: Date.now() - 120000, status: "up", responseTime: 185, code: 200 },
|
| 66 |
+
{ timestamp: Date.now() - 60000, status: "up", responseTime: 182, code: 200 }
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
id: "mon_2",
|
| 71 |
+
name: "قاعدة بيانات Supabase API",
|
| 72 |
+
url: "https://supabase.com",
|
| 73 |
+
interval: "5m",
|
| 74 |
+
status: "up",
|
| 75 |
+
lastCheck: Date.now() - 120000,
|
| 76 |
+
responseTime: 295,
|
| 77 |
+
history: [
|
| 78 |
+
{ timestamp: Date.now() - 600000, status: "up", responseTime: 310, code: 200 },
|
| 79 |
+
{ timestamp: Date.now() - 480000, status: "up", responseTime: 302, code: 200 },
|
| 80 |
+
{ timestamp: Date.now() - 360000, status: "up", responseTime: 298, code: 200 },
|
| 81 |
+
{ timestamp: Date.now() - 240000, status: "up", responseTime: 290, code: 200 },
|
| 82 |
+
{ timestamp: Date.now() - 120000, status: "up", responseTime: 295, code: 200 }
|
| 83 |
+
]
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
id: "mon_3",
|
| 87 |
+
name: "API الخادم الاحتياطي",
|
| 88 |
+
url: "https://httpbin.org/status/404",
|
| 89 |
+
interval: "15m",
|
| 90 |
+
status: "down",
|
| 91 |
+
lastCheck: Date.now() - 300000,
|
| 92 |
+
responseTime: 1540,
|
| 93 |
+
history: [
|
| 94 |
+
{ timestamp: Date.now() - 1200000, status: "down", responseTime: 1200, code: 404 },
|
| 95 |
+
{ timestamp: Date.now() - 900000, status: "up", responseTime: 420, code: 200 },
|
| 96 |
+
{ timestamp: Date.now() - 600000, status: "down", responseTime: 1500, code: 504 },
|
| 97 |
+
{ timestamp: Date.now() - 300000, status: "down", responseTime: 1540, code: 404 }
|
| 98 |
+
]
|
| 99 |
+
}
|
| 100 |
+
];
|
| 101 |
+
for (const m of seed) {
|
| 102 |
+
await DBService.put("monitors", m);
|
| 103 |
+
}
|
| 104 |
+
setMonitors(seed);
|
| 105 |
+
} else {
|
| 106 |
+
setMonitors(migratedList);
|
| 107 |
+
}
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const runAutoPingSimulation = async () => {
|
| 111 |
+
const list = await DBService.getAll<WebsiteMonitor>("monitors");
|
| 112 |
+
if (list.length === 0) return;
|
| 113 |
+
|
| 114 |
+
const randomIndex = Math.floor(Math.random() * list.length);
|
| 115 |
+
const monitor = list[randomIndex];
|
| 116 |
+
if (monitor) {
|
| 117 |
+
await executeSinglePing(monitor);
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
const executeSinglePing = async (m: WebsiteMonitor) => {
|
| 122 |
+
setTestingId(m.id);
|
| 123 |
+
const start = performance.now();
|
| 124 |
+
let status: "up" | "down" | "slow" = "up";
|
| 125 |
+
let code = 200;
|
| 126 |
+
let respTime = 0;
|
| 127 |
+
|
| 128 |
+
try {
|
| 129 |
+
const formatUrl = m.url.startsWith("http") ? m.url : `https://${m.url}`;
|
| 130 |
+
const res = await fetch(`/api/proxy?url=${encodeURIComponent(formatUrl)}`, {
|
| 131 |
+
method: "GET"
|
| 132 |
+
});
|
| 133 |
+
const end = performance.now();
|
| 134 |
+
respTime = Math.round(end - start);
|
| 135 |
+
code = res.status;
|
| 136 |
+
|
| 137 |
+
if (!res.ok) {
|
| 138 |
+
status = "down";
|
| 139 |
+
} else if (respTime > 800) {
|
| 140 |
+
status = "slow";
|
| 141 |
+
}
|
| 142 |
+
} catch {
|
| 143 |
+
status = "down";
|
| 144 |
+
code = 500;
|
| 145 |
+
respTime = 2000;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const updatedHistoryItem = {
|
| 149 |
+
timestamp: Date.now(),
|
| 150 |
+
status,
|
| 151 |
+
responseTime: respTime,
|
| 152 |
+
code
|
| 153 |
+
};
|
| 154 |
+
|
| 155 |
+
const newHistory = [updatedHistoryItem, ...m.history].slice(0, 100);
|
| 156 |
+
const updated: WebsiteMonitor = {
|
| 157 |
+
...m,
|
| 158 |
+
status,
|
| 159 |
+
lastCheck: Date.now(),
|
| 160 |
+
responseTime: respTime,
|
| 161 |
+
history: newHistory
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
await DBService.put("monitors", updated);
|
| 165 |
+
|
| 166 |
+
if (m.status !== status) {
|
| 167 |
+
await DBService.put("auditLog", {
|
| 168 |
+
id: "log_" + Date.now(),
|
| 169 |
+
timestamp: Date.now(),
|
| 170 |
+
action: "تنبيه مراقبة المواقع",
|
| 171 |
+
details: `مراقب المواقع [${m.name}] تغيرت حالته من [${m.status}] إلى [${status}]`,
|
| 172 |
+
status: status === "up" ? "success" : "danger"
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
setMonitors(prev => prev.map(item => item.id === m.id ? updated : item));
|
| 177 |
+
setTestingId(null);
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
const handleAddMonitor = async (e: React.FormEvent) => {
|
| 181 |
+
e.preventDefault();
|
| 182 |
+
if (!name || !url) return;
|
| 183 |
+
triggerHaptic();
|
| 184 |
+
|
| 185 |
+
const newMonitor: WebsiteMonitor = {
|
| 186 |
+
id: "mon_" + Date.now(),
|
| 187 |
+
name,
|
| 188 |
+
url,
|
| 189 |
+
interval,
|
| 190 |
+
status: "unknown",
|
| 191 |
+
history: []
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
+
await DBService.put("monitors", newMonitor);
|
| 195 |
+
setMonitors(prev => [newMonitor, ...prev]);
|
| 196 |
+
|
| 197 |
+
setName("");
|
| 198 |
+
setUrl("");
|
| 199 |
+
|
| 200 |
+
executeSinglePing(newMonitor);
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
const handleDelete = async (id: string) => {
|
| 204 |
+
triggerHaptic();
|
| 205 |
+
await DBService.delete("monitors", id);
|
| 206 |
+
setMonitors(prev => prev.filter(m => m.id !== id));
|
| 207 |
+
};
|
| 208 |
+
|
| 209 |
+
return (
|
| 210 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in text-slate-800">
|
| 211 |
+
{/* Sidebar Add (Left) */}
|
| 212 |
+
<div className="lg:col-span-4 space-y-6">
|
| 213 |
+
<form onSubmit={handleAddMonitor} className="glass-card bg-white/75 border border-sky-100 p-6 rounded-2xl space-y-4">
|
| 214 |
+
<h2 className="text-sm font-extrabold text-sky-800 flex items-center gap-1.5">
|
| 215 |
+
<Plus className="w-5 h-5 text-sky-600" />
|
| 216 |
+
إضافة مراقب تفاعلي جديد
|
| 217 |
+
</h2>
|
| 218 |
+
|
| 219 |
+
<div>
|
| 220 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">اسم المراقب أو الخدمة</label>
|
| 221 |
+
<input
|
| 222 |
+
type="text"
|
| 223 |
+
placeholder="مثال: الخادم الرئيسي"
|
| 224 |
+
required
|
| 225 |
+
value={name}
|
| 226 |
+
onChange={(e) => setName(e.target.value)}
|
| 227 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full font-bold"
|
| 228 |
+
/>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<div>
|
| 232 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">الرابط أو عنوان الـ API</label>
|
| 233 |
+
<input
|
| 234 |
+
type="text"
|
| 235 |
+
placeholder="api.example.com"
|
| 236 |
+
required
|
| 237 |
+
value={url}
|
| 238 |
+
onChange={(e) => setUrl(e.target.value)}
|
| 239 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-mono"
|
| 240 |
+
/>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div>
|
| 244 |
+
<label className="block text-xs text-slate-600 mb-1 font-semibold">معدل الفحص الدوري</label>
|
| 245 |
+
<select
|
| 246 |
+
value={interval}
|
| 247 |
+
onChange={(e) => setIntervalVal(e.target.value as any)}
|
| 248 |
+
className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full font-bold"
|
| 249 |
+
>
|
| 250 |
+
<option value="30s">كل 30 ثانية</option>
|
| 251 |
+
<option value="1m">كل دقيقة واحدة</option>
|
| 252 |
+
<option value="5m">كل 5 دقائق</option>
|
| 253 |
+
<option value="15m">كل 15 دقيقة</option>
|
| 254 |
+
<option value="1h">كل ساعة</option>
|
| 255 |
+
</select>
|
| 256 |
+
</div>
|
| 257 |
+
|
| 258 |
+
<button
|
| 259 |
+
type="submit"
|
| 260 |
+
className="w-full bg-sky-600 hover:bg-sky-700 text-white font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer"
|
| 261 |
+
>
|
| 262 |
+
<Activity className="w-4 h-4" />
|
| 263 |
+
بدء المراقبة النشطة
|
| 264 |
+
</button>
|
| 265 |
+
</form>
|
| 266 |
+
|
| 267 |
+
{/* Global Stats */}
|
| 268 |
+
<div className="glass-card bg-white/75 border border-sky-100 p-6 rounded-2xl relative overflow-hidden">
|
| 269 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-sky-500/5 blur-2xl rounded-full" />
|
| 270 |
+
<h3 className="text-xs font-extrabold text-sky-800 mb-4 flex items-center gap-1.5">
|
| 271 |
+
<TrendingUp className="w-4 h-4 text-sky-600" />
|
| 272 |
+
ملخص الأداء وال��يّوية
|
| 273 |
+
</h3>
|
| 274 |
+
<div className="space-y-4">
|
| 275 |
+
<div className="flex justify-between items-center text-xs">
|
| 276 |
+
<span className="text-slate-600 font-bold">العدد الكلي للمراقبين:</span>
|
| 277 |
+
<span className="font-extrabold text-sky-900 font-mono">{monitors.length} قنوات</span>
|
| 278 |
+
</div>
|
| 279 |
+
<div className="flex justify-between items-center text-xs">
|
| 280 |
+
<span className="text-slate-600 font-bold">الخوادم النشطة (Up):</span>
|
| 281 |
+
<span className="font-black text-emerald-600 font-mono">
|
| 282 |
+
{monitors.filter(m => m.status === "up").length}
|
| 283 |
+
</span>
|
| 284 |
+
</div>
|
| 285 |
+
<div className="flex justify-between items-center text-xs">
|
| 286 |
+
<span className="text-slate-600 font-bold">الخوادم المتوقفة (Down):</span>
|
| 287 |
+
<span className="font-black text-red-500 font-mono">
|
| 288 |
+
{monitors.filter(m => m.status === "down").length}
|
| 289 |
+
</span>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
{/* Monitors Dashboard View (Right) */}
|
| 296 |
+
<div className="lg:col-span-8 space-y-4">
|
| 297 |
+
{monitors.map((m) => (
|
| 298 |
+
<div key={m.id} className="glass-card bg-white/75 border border-sky-100 p-5 rounded-2xl flex flex-col md:flex-row justify-between gap-4 relative overflow-hidden">
|
| 299 |
+
<div className={`absolute top-0 right-0 w-1.5 h-full ${
|
| 300 |
+
m.status === "up" ? "bg-emerald-500" : m.status === "down" ? "bg-red-500" : "bg-yellow-500"
|
| 301 |
+
}`} />
|
| 302 |
+
|
| 303 |
+
{/* Title Block */}
|
| 304 |
+
<div className="flex-1 space-y-2 pr-2">
|
| 305 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 306 |
+
<h3 className="font-extrabold text-sm text-sky-900">{m.name}</h3>
|
| 307 |
+
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${
|
| 308 |
+
m.status === "up"
|
| 309 |
+
? "bg-emerald-100 text-emerald-700 border border-emerald-200"
|
| 310 |
+
: m.status === "down"
|
| 311 |
+
? "bg-red-100 text-red-700 border border-red-200"
|
| 312 |
+
: "bg-sky-100 text-sky-700"
|
| 313 |
+
}`}>
|
| 314 |
+
{m.status === "up" ? "متصل (UP)" : m.status === "down" ? "غير متصل (DOWN)" : "قيد الاختبار"}
|
| 315 |
+
</span>
|
| 316 |
+
<span className="text-[10px] text-slate-500 flex items-center gap-0.5">
|
| 317 |
+
<Clock className="w-3 h-3 text-sky-600" />
|
| 318 |
+
{m.interval}
|
| 319 |
+
</span>
|
| 320 |
+
</div>
|
| 321 |
+
<p className="text-xs text-slate-600 text-left font-mono truncate">{m.url}</p>
|
| 322 |
+
|
| 323 |
+
<div className="flex items-center gap-1.5 pt-1">
|
| 324 |
+
<span className="text-[10px] text-slate-500 font-bold">سجل الأحداث المباشر:</span>
|
| 325 |
+
<div className="flex gap-1">
|
| 326 |
+
{m.history.length === 0 ? (
|
| 327 |
+
<span className="text-[10px] text-slate-500">لا يوجد سجل تاريخي</span>
|
| 328 |
+
) : (
|
| 329 |
+
m.history.map((h, index) => (
|
| 330 |
+
<div
|
| 331 |
+
key={index}
|
| 332 |
+
title={`تاريخ الفحص: ${new Date(h.timestamp).toLocaleTimeString("ar-EG")} - الاستجابة: ${h.responseTime}ms - الحالة: ${h.status}`}
|
| 333 |
+
className={`w-3.5 h-3.5 rounded-full flex items-center justify-center text-[8px] font-bold border ${
|
| 334 |
+
h.status === "up"
|
| 335 |
+
? "bg-emerald-100 text-emerald-700 border-emerald-300"
|
| 336 |
+
: h.status === "down"
|
| 337 |
+
? "bg-red-100 text-red-700 border-red-300"
|
| 338 |
+
: "bg-yellow-100 text-yellow-700 border-yellow-300"
|
| 339 |
+
}`}
|
| 340 |
+
>
|
| 341 |
+
•
|
| 342 |
+
</div>
|
| 343 |
+
))
|
| 344 |
+
)}
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
|
| 349 |
+
{/* Telemetry Actions Block */}
|
| 350 |
+
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between md:justify-end gap-3 shrink-0">
|
| 351 |
+
<div className="bg-sky-50 border border-sky-150 rounded-xl p-3 text-center sm:text-right min-w-[120px]">
|
| 352 |
+
<span className="text-[10px] text-slate-500 block mb-0.5">سرعة الاستجابة</span>
|
| 353 |
+
{m.responseTime ? (
|
| 354 |
+
<span className="text-sm font-black text-sky-850 font-mono">
|
| 355 |
+
{m.responseTime} <span className="text-[10px]">ms</span>
|
| 356 |
+
</span>
|
| 357 |
+
) : (
|
| 358 |
+
<span className="text-xs text-slate-500 font-bold">--</span>
|
| 359 |
+
)}
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
{/* Run Test & Delete */}
|
| 363 |
+
<div className="flex gap-2">
|
| 364 |
+
<button
|
| 365 |
+
onClick={() => executeSinglePing(m)}
|
| 366 |
+
disabled={testingId === m.id}
|
| 367 |
+
className="flex-1 sm:flex-initial px-3.5 py-2.5 bg-sky-100 hover:bg-sky-200 disabled:opacity-50 text-sky-800 border border-sky-200 rounded-xl transition flex items-center justify-center gap-1.5 text-xs font-bold cursor-pointer"
|
| 368 |
+
>
|
| 369 |
+
{testingId === m.id ? (
|
| 370 |
+
<Shuffle className="w-3.5 h-3.5 animate-spin text-sky-600" />
|
| 371 |
+
) : (
|
| 372 |
+
<Play className="w-3.5 h-3.5 text-sky-600" />
|
| 373 |
+
)}
|
| 374 |
+
فحص الآن
|
| 375 |
+
</button>
|
| 376 |
+
<button
|
| 377 |
+
onClick={() => handleDelete(m.id)}
|
| 378 |
+
className="p-2.5 bg-white hover:bg-red-50 text-red-500 border border-sky-250/20 rounded-xl transition cursor-pointer"
|
| 379 |
+
>
|
| 380 |
+
<Trash className="w-3.5 h-3.5" />
|
| 381 |
+
</button>
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
))}
|
| 386 |
+
|
| 387 |
+
{monitors.length === 0 && (
|
| 388 |
+
<div className="glass-card p-12 text-center text-slate-400 rounded-2xl bg-white/75 border border-sky-50">
|
| 389 |
+
لا توجد مخدمات مراقبة نشطة مضافة بعد، أضف عنوان خدمة للبدء.
|
| 390 |
+
</div>
|
| 391 |
+
)}
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
);
|
| 395 |
+
}
|
src/components/SitesTab.tsx
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { BuiltSite } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Globe, Plus, Trash, ExternalLink, RefreshCw, BarChart2,
|
| 6 |
+
GitBranch, RefreshCcw, FolderOpen, AlertCircle, Sparkles, User, Shield
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
export default function SitesTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 10 |
+
const [sites, setSites] = useState<BuiltSite[]>([]);
|
| 11 |
+
const [name, setName] = useState("");
|
| 12 |
+
const [url, setUrl] = useState("");
|
| 13 |
+
const [platform, setPlatform] = useState("Vercel");
|
| 14 |
+
const [status, setStatus] = useState<any>("live");
|
| 15 |
+
const [repoUrl, setRepoUrl] = useState("");
|
| 16 |
+
const [isBuildingId, setIsBuildingId] = useState<string | null>(null);
|
| 17 |
+
|
| 18 |
+
// Multi-Email Interactive Filter and Association States
|
| 19 |
+
const [activeEmail, setActiveEmail] = useState("eissaaly07@gmail.com");
|
| 20 |
+
const [availableEmails, setAvailableEmails] = useState<string[]>([]);
|
| 21 |
+
const [ownerEmail, setOwnerEmail] = useState("eissaaly07@gmail.com");
|
| 22 |
+
|
| 23 |
+
useEffect(() => {
|
| 24 |
+
loadSites();
|
| 25 |
+
}, []);
|
| 26 |
+
|
| 27 |
+
const loadSites = async () => {
|
| 28 |
+
// 1. Get current active session email
|
| 29 |
+
const activeSetting = await DBService.getSetting<string>("activeEmailSession") || "eissaaly07@gmail.com";
|
| 30 |
+
setActiveEmail(activeSetting);
|
| 31 |
+
setOwnerEmail(activeSetting);
|
| 32 |
+
|
| 33 |
+
// 2. Compile list of emails from registered sessions + account profiles + emails store
|
| 34 |
+
const accountsList = await DBService.getAll<any>("accounts");
|
| 35 |
+
const emailsSet = new Set<string>([
|
| 36 |
+
"eissaaly07@gmail.com",
|
| 37 |
+
"eissaaly0007@gmail.com"
|
| 38 |
+
]);
|
| 39 |
+
accountsList.forEach(a => {
|
| 40 |
+
if (a.email) emailsSet.add(a.email.trim());
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// Fetch from dedicated emails store (EmailsTab)
|
| 44 |
+
try {
|
| 45 |
+
const emailVaultList = await DBService.getAll<any>("emails");
|
| 46 |
+
emailVaultList.forEach(e => {
|
| 47 |
+
if (e.email) emailsSet.add(e.email.trim());
|
| 48 |
+
});
|
| 49 |
+
} catch (e) {
|
| 50 |
+
console.warn("Could not retrieve emails from emails store:", e);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const storedList = await DBService.getSetting<string[]>("sessionEmailsList");
|
| 54 |
+
if (storedList) {
|
| 55 |
+
storedList.forEach(e => emailsSet.add(e.trim()));
|
| 56 |
+
}
|
| 57 |
+
const emailsArray = Array.from(emailsSet);
|
| 58 |
+
setAvailableEmails(emailsArray);
|
| 59 |
+
|
| 60 |
+
// 3. Load Sites from store
|
| 61 |
+
const list = await DBService.getAll<BuiltSite>("sites");
|
| 62 |
+
if (list.length === 0) {
|
| 63 |
+
// Seed initial dummy sites for project visual presentation
|
| 64 |
+
const seed: BuiltSite[] = [
|
| 65 |
+
{
|
| 66 |
+
id: "site_1",
|
| 67 |
+
name: "مدونة المطور الشخصية",
|
| 68 |
+
url: "https://my-blog-preview.vercel.app",
|
| 69 |
+
platform: "Vercel",
|
| 70 |
+
status: "live",
|
| 71 |
+
deployUrl: "https://vercel.com/dashboard",
|
| 72 |
+
repoUrl: "https://github.com/developer/blog",
|
| 73 |
+
lastDeploy: Date.now() - 3600000 * 24,
|
| 74 |
+
domain: "myblog.dev",
|
| 75 |
+
analytics: { pageViews: 1420, uptimePercentage: 99.9 },
|
| 76 |
+
ownerEmail: "eissaaly07@gmail.com"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
id: "site_2",
|
| 80 |
+
name: "متجر الهدايا الرقمي",
|
| 81 |
+
url: "https://gift-store.netlify.app",
|
| 82 |
+
platform: "Netlify",
|
| 83 |
+
status: "staging",
|
| 84 |
+
deployUrl: "https://app.netlify.com",
|
| 85 |
+
repoUrl: "https://github.com/developer/gift-store",
|
| 86 |
+
lastDeploy: Date.now() - 3600000 * 5,
|
| 87 |
+
analytics: { pageViews: 480, uptimePercentage: 98.7 },
|
| 88 |
+
ownerEmail: "eissaaly07@gmail.com"
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
id: "site_3",
|
| 92 |
+
name: "موقع التوثيق السريع",
|
| 93 |
+
url: "https://doc-wizard.github.io",
|
| 94 |
+
platform: "GitHub Pages",
|
| 95 |
+
status: "live",
|
| 96 |
+
deployUrl: "https://github.com",
|
| 97 |
+
repoUrl: "https://github.com/developer/doc-wizard",
|
| 98 |
+
lastDeploy: Date.now() - 3600000 * 12,
|
| 99 |
+
analytics: { pageViews: 240, uptimePercentage: 100.0 },
|
| 100 |
+
ownerEmail: "eissaaly0007@gmail.com"
|
| 101 |
+
}
|
| 102 |
+
];
|
| 103 |
+
for (const s of seed) {
|
| 104 |
+
await DBService.put("sites", s);
|
| 105 |
+
}
|
| 106 |
+
setSites(seed);
|
| 107 |
+
} else {
|
| 108 |
+
setSites(list);
|
| 109 |
+
}
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
const handleSwitchActiveEmail = async (email: string) => {
|
| 113 |
+
triggerHaptic();
|
| 114 |
+
setActiveEmail(email);
|
| 115 |
+
setOwnerEmail(email);
|
| 116 |
+
await DBService.putSetting("activeEmailSession", email);
|
| 117 |
+
|
| 118 |
+
// Save to audit trail
|
| 119 |
+
await DBService.put("auditLog", {
|
| 120 |
+
id: "log_" + Date.now(),
|
| 121 |
+
timestamp: Date.now(),
|
| 122 |
+
action: "تغيير جلسة بريد المواقع",
|
| 123 |
+
details: `تم تبديل تصفية المواقع النشطة لعرض المشاريع المملوكة لـ [${email}]`,
|
| 124 |
+
status: "success"
|
| 125 |
+
});
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
const handleCreateSite = async (e: React.FormEvent) => {
|
| 129 |
+
e.preventDefault();
|
| 130 |
+
if (!name || !url) return;
|
| 131 |
+
triggerHaptic();
|
| 132 |
+
|
| 133 |
+
const targetOwner = ownerEmail || activeEmail;
|
| 134 |
+
const newSite: BuiltSite = {
|
| 135 |
+
id: "site_" + Date.now(),
|
| 136 |
+
name,
|
| 137 |
+
url,
|
| 138 |
+
platform,
|
| 139 |
+
status,
|
| 140 |
+
deployUrl: platform === "Vercel" ? "https://vercel.com" : "https://netlify.com",
|
| 141 |
+
repoUrl: repoUrl || undefined,
|
| 142 |
+
lastDeploy: Date.now(),
|
| 143 |
+
analytics: {
|
| 144 |
+
pageViews: Math.floor(Math.random() * 200) + 10,
|
| 145 |
+
uptimePercentage: 100.0
|
| 146 |
+
},
|
| 147 |
+
ownerEmail: targetOwner
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
await DBService.put("sites", newSite);
|
| 151 |
+
setSites(prev => [newSite, ...prev]);
|
| 152 |
+
|
| 153 |
+
await DBService.put("auditLog", {
|
| 154 |
+
id: "log_" + Date.now(),
|
| 155 |
+
timestamp: Date.now(),
|
| 156 |
+
action: "أرشفة وتتبع مشروع",
|
| 157 |
+
details: `تم بنجاح ربط وتتبع المشروع [${name}] وتخصيص المالك [${targetOwner}]`,
|
| 158 |
+
status: "success"
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
setName("");
|
| 162 |
+
setUrl("");
|
| 163 |
+
setRepoUrl("");
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
const handleDelete = async (id: string) => {
|
| 167 |
+
triggerHaptic();
|
| 168 |
+
await DBService.delete("sites", id);
|
| 169 |
+
setSites(prev => prev.filter(s => s.id !== id));
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
const triggerMockRedeploy = async (id: string) => {
|
| 173 |
+
triggerHaptic();
|
| 174 |
+
setIsBuildingId(id);
|
| 175 |
+
|
| 176 |
+
setTimeout(async () => {
|
| 177 |
+
setIsBuildingId(null);
|
| 178 |
+
// Update build timestamp
|
| 179 |
+
const s = sites.find(item => item.id === id);
|
| 180 |
+
if (s) {
|
| 181 |
+
s.lastDeploy = Date.now();
|
| 182 |
+
await DBService.put("sites", s);
|
| 183 |
+
setSites(prev => prev.map(item => item.id === id ? s : item));
|
| 184 |
+
|
| 185 |
+
await DBService.put("auditLog", {
|
| 186 |
+
id: "log_" + Date.now(),
|
| 187 |
+
timestamp: Date.now(),
|
| 188 |
+
action: "إعادة بناء ونشر مجاني",
|
| 189 |
+
details: `تم إعادة إطلاق وبناء مشروع [${s.name}] عن بعد في دقيقتين`,
|
| 190 |
+
status: "success"
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
}, 2500);
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
const getStatusLabel = (st: string) => {
|
| 197 |
+
switch (st) {
|
| 198 |
+
case "live": return "منشور فعال (Live)";
|
| 199 |
+
case "dev": return "تحت التطوير (Dev)";
|
| 200 |
+
case "staging": return "تجريبي (Staging)";
|
| 201 |
+
case "archived": return "مؤرشف (Archived)";
|
| 202 |
+
case "down": return "معطل";
|
| 203 |
+
default: return st;
|
| 204 |
+
}
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
const filteredSites = sites.filter(s => {
|
| 208 |
+
const siteEmail = s.ownerEmail || "eissaaly07@gmail.com";
|
| 209 |
+
return siteEmail.toLowerCase() === activeEmail.toLowerCase();
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
return (
|
| 213 |
+
<div className="space-y-6 animate-fade-in font-sans">
|
| 214 |
+
{/* 20+ Master Email SSO Filter & Status Bar (Bilingual, high contrast) */}
|
| 215 |
+
<div className="glass-card bg-gradient-to-br from-slate-900 via-sky-950 to-slate-950 p-5 rounded-2xl border border-sky-800/40 text-white flex flex-col md:flex-row justify-between items-stretch md:items-center gap-4 shadow-xl">
|
| 216 |
+
<div className="space-y-1 text-right">
|
| 217 |
+
<h2 className="text-sm font-extrabold text-sky-300 flex items-center gap-2">
|
| 218 |
+
<Sparkles className="w-4.5 h-4.5 text-yellow-300 animate-pulse" />
|
| 219 |
+
تصفية مواقع ومشاريع الحساب المالك النشط (Active Owner Site Filter)
|
| 220 |
+
</h2>
|
| 221 |
+
<p className="text-[10px] text-slate-300 leading-relaxed">
|
| 222 |
+
يتم عرض تتبع الاستضافات وحسابات السيرفر الفعالة للبريد النشط المختار أدناه. ينعكس هذا التغيير على بوابة الدخول الموحد SSO فوراً.
|
| 223 |
+
</p>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<div className="flex items-center gap-2 shrink-0 self-start md:self-auto">
|
| 227 |
+
<span className="text-[11px] font-bold text-sky-200 block shrink-0">اختر البريد النشط:</span>
|
| 228 |
+
<select
|
| 229 |
+
value={activeEmail}
|
| 230 |
+
onChange={(e) => handleSwitchActiveEmail(e.target.value)}
|
| 231 |
+
className="bg-white/10 text-white hover:bg-white/15 border border-white/20 rounded-xl px-3 py-1.5 text-xs font-mono font-extrabold focus:outline-none cursor-pointer"
|
| 232 |
+
>
|
| 233 |
+
{availableEmails.map((email) => (
|
| 234 |
+
<option key={email} value={email} className="bg-slate-900 text-white font-mono font-bold">{email}</option>
|
| 235 |
+
))}
|
| 236 |
+
</select>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
| 241 |
+
{/* Sidebar Add Project Form */}
|
| 242 |
+
<div className="lg:col-span-4 space-y-4">
|
| 243 |
+
<form onSubmit={handleCreateSite} className="glass-card p-6 rounded-2xl space-y-4 border border-slate-800">
|
| 244 |
+
<h2 className="text-lg font-bold text-sky-400 flex items-center gap-2">
|
| 245 |
+
<Plus className="w-5 h-5 text-sky-500" />
|
| 246 |
+
تتبع مشروع/موقع جديد
|
| 247 |
+
</h2>
|
| 248 |
+
|
| 249 |
+
<div>
|
| 250 |
+
<label className="block text-xs text-slate-400 mb-1">اسم المشروع</label>
|
| 251 |
+
<input
|
| 252 |
+
type="text"
|
| 253 |
+
placeholder="مثال: لوحة التحكم لموظفي المبيعات"
|
| 254 |
+
required
|
| 255 |
+
value={name}
|
| 256 |
+
onChange={(e) => setName(e.target.value)}
|
| 257 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 258 |
+
/>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<div>
|
| 262 |
+
<label className="block text-xs text-slate-400 mb-1">رابط الإنتاج المباشر (Production URL)</label>
|
| 263 |
+
<input
|
| 264 |
+
type="text"
|
| 265 |
+
placeholder="mysite.vercel.app"
|
| 266 |
+
required
|
| 267 |
+
value={url}
|
| 268 |
+
onChange={(e) => setUrl(e.target.value)}
|
| 269 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full text-left font-mono"
|
| 270 |
+
/>
|
| 271 |
+
</div>
|
| 272 |
+
|
| 273 |
+
<div>
|
| 274 |
+
<label className="block text-xs text-slate-400 mb-1">منصة الاستضافة المجانية</label>
|
| 275 |
+
<select
|
| 276 |
+
value={platform}
|
| 277 |
+
onChange={(e) => setPlatform(e.target.value)}
|
| 278 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 279 |
+
>
|
| 280 |
+
<option value="Vercel">Vercel (فيرسيل)</option>
|
| 281 |
+
<option value="Netlify">Netlify (نتليفاي)</option>
|
| 282 |
+
<option value="GitHub Pages">GitHub Pages</option>
|
| 283 |
+
<option value="Render">Render</option>
|
| 284 |
+
<option value="Railway">Railway</option>
|
| 285 |
+
<option value="Cloudflare Pages">Cloudflare Pages</option>
|
| 286 |
+
</select>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
{/* Owner Email ID selector input field */}
|
| 290 |
+
<div>
|
| 291 |
+
<label className="block text-xs text-slate-400 mb-1">الحساب المالك لهذه الاستضافة (Owner Account)</label>
|
| 292 |
+
<select
|
| 293 |
+
value={ownerEmail}
|
| 294 |
+
onChange={(e) => setOwnerEmail(e.target.value)}
|
| 295 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-xs text-sky-300 font-mono focus:outline-none focus:border-sky-500 w-full"
|
| 296 |
+
>
|
| 297 |
+
{availableEmails.map((email) => (
|
| 298 |
+
<option key={email} value={email}>{email}</option>
|
| 299 |
+
))}
|
| 300 |
+
</select>
|
| 301 |
+
<span className="text-[9px] text-slate-400 mt-1 block leading-relaxed">
|
| 302 |
+
سيظهر هذا الموقع فقط عند تحديد وتنشيط هذا البريد في تصفية الجلسات.
|
| 303 |
+
</span>
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div>
|
| 307 |
+
<label className="block text-xs text-slate-400 mb-1">مستودع الكواد (Repository URL) - اختياري</label>
|
| 308 |
+
<input
|
| 309 |
+
type="text"
|
| 310 |
+
placeholder="github.com/user/project"
|
| 311 |
+
value={repoUrl}
|
| 312 |
+
onChange={(e) => setRepoUrl(e.target.value)}
|
| 313 |
+
className="bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full text-left font-mono"
|
| 314 |
+
/>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<div>
|
| 318 |
+
<label className="block text-xs text-slate-400 mb-1">حالة الموقع الافتراضية</label>
|
| 319 |
+
<select
|
| 320 |
+
value={status}
|
| 321 |
+
onChange={(e) => setStatus(e.target.value as any)}
|
| 322 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-sky-500 w-full"
|
| 323 |
+
>
|
| 324 |
+
<option value="live">نشط وفعال</option>
|
| 325 |
+
<option value="staging">بيئة الفحص (Staging)</option>
|
| 326 |
+
<option value="dev">قيد البرمجة والتطوير</option>
|
| 327 |
+
<option value="archived">مؤرشف ومحفوظ</option>
|
| 328 |
+
</select>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<button
|
| 332 |
+
type="submit"
|
| 333 |
+
className="w-full bg-sky-500 hover:bg-sky-600 text-slate-950 font-bold py-3 rounded-xl text-sm flex items-center justify-center gap-1 transition cursor-pointer"
|
| 334 |
+
>
|
| 335 |
+
<Globe className="w-4 h-4 text-slate-900" />
|
| 336 |
+
حفظ وربط المشروع
|
| 337 |
+
</button>
|
| 338 |
+
</form>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
{/* Projects List Dashboard */}
|
| 342 |
+
<div className="lg:col-span-8 space-y-4">
|
| 343 |
+
<div className="flex items-center justify-between px-1">
|
| 344 |
+
<span className="text-[11px] font-bold text-slate-450">
|
| 345 |
+
مشاريع تابعة للحساب: <span className="text-sky-400 font-mono font-black">{activeEmail}</span>
|
| 346 |
+
</span>
|
| 347 |
+
<span className="text-[10px] bg-sky-950/40 text-sky-305 border border-sky-800/60 px-2 py-0.5 rounded-lg font-bold">
|
| 348 |
+
مجموع المشاريع المصفاة: {filteredSites.length}
|
| 349 |
+
</span>
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
{filteredSites.map((s) => (
|
| 353 |
+
<div key={s.id} className="glass-card p-5 rounded-2xl border border-slate-800 space-y-4 relative overflow-hidden">
|
| 354 |
+
{/* Platform indicator sidebar color code */}
|
| 355 |
+
<div className={`absolute top-0 right-0 w-1.5 h-full ${
|
| 356 |
+
s.platform === "Vercel" ? "bg-white" : s.platform === "Netlify" ? "bg-teal-400" : "bg-purple-500"
|
| 357 |
+
}`} />
|
| 358 |
+
|
| 359 |
+
{/* Top header block */}
|
| 360 |
+
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-3">
|
| 361 |
+
<div>
|
| 362 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 363 |
+
<h3 className="font-bold text-base text-slate-200">{s.name}</h3>
|
| 364 |
+
<span className="text-[10px] bg-slate-800 text-slate-400 px-2 py-0.5 rounded font-mono font-semibold">
|
| 365 |
+
{s.platform}
|
| 366 |
+
</span>
|
| 367 |
+
<span className="text-[10px] bg-sky-950/40 text-sky-300 border border-sky-800/40 px-2 py-0.5 rounded font-mono">
|
| 368 |
+
👤 {s.ownerEmail || "eissaaly07@gmail.com"}
|
| 369 |
+
</span>
|
| 370 |
+
<span className={`text-[9px] px-2 py-0.5 rounded font-bold ${
|
| 371 |
+
s.status === "live"
|
| 372 |
+
? "bg-emerald-500/10 text-emerald-400"
|
| 373 |
+
: s.status === "staging"
|
| 374 |
+
? "bg-amber-500/10 text-amber-400"
|
| 375 |
+
: "bg-slate-800 text-slate-400"
|
| 376 |
+
}`}>
|
| 377 |
+
{getStatusLabel(s.status)}
|
| 378 |
+
</span>
|
| 379 |
+
</div>
|
| 380 |
+
<a
|
| 381 |
+
href={s.url.startsWith("http") ? s.url : `https://${s.url}`}
|
| 382 |
+
target="_blank"
|
| 383 |
+
rel="noopener noreferrer"
|
| 384 |
+
className="text-xs text-sky-400 hover:underline flex items-center gap-1 mt-1 text-left font-mono"
|
| 385 |
+
>
|
| 386 |
+
<ExternalLink className="w-3 h-3" />
|
| 387 |
+
{s.url}
|
| 388 |
+
</a>
|
| 389 |
+
</div>
|
| 390 |
+
|
| 391 |
+
{/* Analytics metrics badge widget */}
|
| 392 |
+
<div className="flex gap-4 bg-slate-950/30 px-4 py-2 rounded-xl border border-slate-900 text-center shrink-0">
|
| 393 |
+
<div>
|
| 394 |
+
<span className="text-[9px] text-slate-500 block">مشاهدات الصفحة</span>
|
| 395 |
+
<span className="text-xs font-bold font-mono text-emerald-400">
|
| 396 |
+
<BarChart2 className="w-3.5 h-3.5 inline ml-0.5" />
|
| 397 |
+
{s.analytics.pageViews}
|
| 398 |
+
</span>
|
| 399 |
+
</div>
|
| 400 |
+
<div className="border-r border-slate-800 h-6 my-auto" />
|
| 401 |
+
<div>
|
| 402 |
+
<span className="text-[9px] text-slate-500 block">نسبة الاستقرار</span>
|
| 403 |
+
<span className="text-xs font-bold font-mono text-cyan-400">
|
| 404 |
+
{s.analytics.uptimePercentage}%
|
| 405 |
+
</span>
|
| 406 |
+
</div>
|
| 407 |
+
</div>
|
| 408 |
+
</div>
|
| 409 |
+
|
| 410 |
+
{/* Git Integration Block if any */}
|
| 411 |
+
{s.repoUrl && (
|
| 412 |
+
<div className="bg-slate-950/50 p-3 rounded-xl border border-slate-900 flex items-center justify-between text-xs font-mono text-slate-400">
|
| 413 |
+
<span className="flex items-center gap-1.5 truncate">
|
| 414 |
+
<GitBranch className="w-4 h-4 text-slate-500 shrink-0" />
|
| 415 |
+
{s.repoUrl.replace("https://", "")}
|
| 416 |
+
</span>
|
| 417 |
+
<span className="text-[10px] text-slate-500 shrink-0">
|
| 418 |
+
آخر نشر: {new Date(s.lastDeploy).toLocaleTimeString("ar-EG")}
|
| 419 |
+
</span>
|
| 420 |
+
</div>
|
| 421 |
+
)}
|
| 422 |
+
|
| 423 |
+
{/* Action linkages */}
|
| 424 |
+
<div className="flex flex-wrap gap-2 justify-end">
|
| 425 |
+
<button
|
| 426 |
+
onClick={() => triggerMockRedeploy(s.id)}
|
| 427 |
+
disabled={isBuildingId === s.id}
|
| 428 |
+
className="bg-slate-800 hover:bg-slate-700 disabled:opacity-50 text-sky-400 border border-slate-700 py-1.5 px-3 rounded-lg text-xs flex items-center gap-1.5 transition select-none cursor-pointer"
|
| 429 |
+
>
|
| 430 |
+
{isBuildingId === s.id ? (
|
| 431 |
+
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
| 432 |
+
) : (
|
| 433 |
+
<RefreshCcw className="w-3.5 h-3.5" />
|
| 434 |
+
)}
|
| 435 |
+
إعادة بناء فوري (Redeploy)
|
| 436 |
+
</button>
|
| 437 |
+
<a
|
| 438 |
+
href={s.deployUrl}
|
| 439 |
+
target="_blank"
|
| 440 |
+
rel="noopener noreferrer"
|
| 441 |
+
className="bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 py-1.5 px-3 rounded-lg text-xs flex items-center gap-1 transition"
|
| 442 |
+
>
|
| 443 |
+
<FolderOpen className="w-3.5 h-3.5" />
|
| 444 |
+
افتح لوحة تحكم المنصة
|
| 445 |
+
</a>
|
| 446 |
+
<button
|
| 447 |
+
onClick={() => handleDelete(s.id)}
|
| 448 |
+
className="bg-slate-900 hover:bg-red-500/10 text-red-400 border border-slate-800 py-1.5 px-2 rounded-lg text-xs transition cursor-pointer"
|
| 449 |
+
>
|
| 450 |
+
<Trash className="w-3.5 h-3.5" />
|
| 451 |
+
</button>
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
))}
|
| 455 |
+
|
| 456 |
+
{filteredSites.length === 0 && (
|
| 457 |
+
<div className="glass-card p-12 text-center text-slate-500 rounded-2xl border border-slate-800">
|
| 458 |
+
لا توجد مواقع أو مشاريع مضافة تحت حساب البريد الإلكتروني <span className="font-mono text-sky-400 font-bold">{activeEmail}</span> حتى الآن. يمكنك إضافة مشروع جديد وتخصيص هذا البريد له من قائمة اليمين.
|
| 459 |
+
</div>
|
| 460 |
+
)}
|
| 461 |
+
</div>
|
| 462 |
+
</div>
|
| 463 |
+
</div>
|
| 464 |
+
);
|
| 465 |
+
}
|
src/components/StudioTab.tsx
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService, PRE_POPULATED_PLATFORMS } from "../lib/db";
|
| 3 |
+
import { CloudPlatform } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Terminal, Search, Star, ExternalLink, Compass, Code,
|
| 6 |
+
Database, Network, HelpCircle, LayoutGrid, Sparkles,
|
| 7 |
+
Activity, Mail
|
| 8 |
+
} from "lucide-react";
|
| 9 |
+
|
| 10 |
+
export default function StudioTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 11 |
+
const [platforms, setPlatforms] = useState<CloudPlatform[]>([]);
|
| 12 |
+
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
| 13 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 14 |
+
const [favorites, setFavorites] = useState<Record<string, boolean>>({});
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
loadPlatforms();
|
| 18 |
+
}, []);
|
| 19 |
+
|
| 20 |
+
const loadPlatforms = async () => {
|
| 21 |
+
// Attempt load platforms or load from seed
|
| 22 |
+
const list = await DBService.getAll<CloudPlatform>("platforms");
|
| 23 |
+
if (list.length === 0) {
|
| 24 |
+
for (const p of PRE_POPULATED_PLATFORMS) {
|
| 25 |
+
await DBService.put("platforms", p);
|
| 26 |
+
}
|
| 27 |
+
setPlatforms(PRE_POPULATED_PLATFORMS);
|
| 28 |
+
} else {
|
| 29 |
+
setPlatforms(list);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Try loads favorite flags
|
| 33 |
+
const favs = await DBService.getSetting<Record<string, boolean>>("favorite_platforms");
|
| 34 |
+
if (favs) {
|
| 35 |
+
setFavorites(favs);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
const toggleFavorite = async (id: string, e: React.MouseEvent) => {
|
| 40 |
+
e.stopPropagation();
|
| 41 |
+
triggerHaptic();
|
| 42 |
+
const updated = { ...favorites, [id]: !favorites[id] };
|
| 43 |
+
setFavorites(updated);
|
| 44 |
+
await DBService.putSetting("favorite_platforms", updated);
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const getCategoryIcon = (cat: string) => {
|
| 48 |
+
switch (cat) {
|
| 49 |
+
case "ai": return <Sparkles className="w-3.5 h-3.5 text-amber-600" />;
|
| 50 |
+
case "ide": return <Code className="w-3.5 h-3.5 text-sky-600" />;
|
| 51 |
+
case "database": return <Database className="w-3.5 h-3.5 text-emerald-600" />;
|
| 52 |
+
case "deployment": return <Compass className="w-3.5 h-3.5 text-blue-600" />;
|
| 53 |
+
case "workflows": return <Network className="w-3.5 h-3.5 text-fuchsia-600" />;
|
| 54 |
+
case "monitoring": return <Activity className="w-3.5 h-3.5 text-indigo-600" />;
|
| 55 |
+
case "email": return <Mail className="w-3.5 h-3.5 text-violet-600" />;
|
| 56 |
+
default: return <HelpCircle className="w-3.5 h-3.5 text-slate-500" />;
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const getCategoryLabel = (cat: string) => {
|
| 61 |
+
switch (cat) {
|
| 62 |
+
case "ai": return "ذكاء اصطناعي وتوليد";
|
| 63 |
+
case "ide": return "بيئات تطوير سحابية";
|
| 64 |
+
case "database": return "قواعد بيانات سحابية";
|
| 65 |
+
case "deployment": return "منصات نشر واستضافة";
|
| 66 |
+
case "workflows": return "أتمتة وسير العمل";
|
| 67 |
+
case "monitoring": return "مراقبة الأداء والتحليل";
|
| 68 |
+
case "email": return "مزودي البريد الإلكتروني";
|
| 69 |
+
default: return cat;
|
| 70 |
+
}
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
// filter
|
| 74 |
+
let displayList = platforms.filter((item) => {
|
| 75 |
+
const matchSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 76 |
+
item.description.includes(searchQuery) ||
|
| 77 |
+
item.freeLimit.includes(searchQuery);
|
| 78 |
+
|
| 79 |
+
if (selectedCategory === "all") return matchSearch;
|
| 80 |
+
if (selectedCategory === "favorites") return matchSearch && favorites[item.id];
|
| 81 |
+
return matchSearch && item.category === selectedCategory;
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
return (
|
| 85 |
+
<div className="space-y-6 animate-fade-in">
|
| 86 |
+
{/* Search tool & category selectors */}
|
| 87 |
+
<div className="glass-card p-5 rounded-2xl flex flex-col gap-4">
|
| 88 |
+
{/* Search Input */}
|
| 89 |
+
<div className="flex items-center gap-2 bg-white/90 border border-sky-500/20 rounded-xl px-4 py-2 flex-1 max-w-md shadow-sm">
|
| 90 |
+
<Search className="w-4 h-4 text-slate-400 shrink-0" />
|
| 91 |
+
<input
|
| 92 |
+
type="text"
|
| 93 |
+
placeholder="ابحث بين 50+ منصة في البينتو..."
|
| 94 |
+
className="bg-transparent text-xs text-slate-800 focus:outline-none w-full placeholder:text-slate-400"
|
| 95 |
+
value={searchQuery}
|
| 96 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 97 |
+
/>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
{/* Categories filters scrollable */}
|
| 101 |
+
<div className="flex items-center gap-2 overflow-x-auto pb-1.5 scrollbar-none">
|
| 102 |
+
<button
|
| 103 |
+
onClick={() => setSelectedCategory("all")}
|
| 104 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 105 |
+
selectedCategory === "all" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 106 |
+
}`}
|
| 107 |
+
>
|
| 108 |
+
الكل 🌐
|
| 109 |
+
</button>
|
| 110 |
+
<button
|
| 111 |
+
onClick={() => setSelectedCategory("favorites")}
|
| 112 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer flex items-center gap-1 ${
|
| 113 |
+
selectedCategory === "favorites" ? "bg-amber-500 text-white shadow-md shadow-amber-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 114 |
+
}`}
|
| 115 |
+
>
|
| 116 |
+
<Star className="w-3.5 h-3.5 fill-current" />
|
| 117 |
+
المفضلة ⭐
|
| 118 |
+
</button>
|
| 119 |
+
<button
|
| 120 |
+
onClick={() => setSelectedCategory("ai")}
|
| 121 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 122 |
+
selectedCategory === "ai" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 123 |
+
}`}
|
| 124 |
+
>
|
| 125 |
+
الذكاء الاصطناعي 🧠
|
| 126 |
+
</button>
|
| 127 |
+
<button
|
| 128 |
+
onClick={() => setSelectedCategory("ide")}
|
| 129 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 130 |
+
selectedCategory === "ide" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 131 |
+
}`}
|
| 132 |
+
>
|
| 133 |
+
محررات سحابية 💻
|
| 134 |
+
</button>
|
| 135 |
+
<button
|
| 136 |
+
onClick={() => setSelectedCategory("deployment")}
|
| 137 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 138 |
+
selectedCategory === "deployment" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 139 |
+
}`}
|
| 140 |
+
>
|
| 141 |
+
استضافة ونشر 🚀
|
| 142 |
+
</button>
|
| 143 |
+
<button
|
| 144 |
+
onClick={() => setSelectedCategory("database")}
|
| 145 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 146 |
+
selectedCategory === "database" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 147 |
+
}`}
|
| 148 |
+
>
|
| 149 |
+
قواعد بيانات 🗄️
|
| 150 |
+
</button>
|
| 151 |
+
<button
|
| 152 |
+
onClick={() => setSelectedCategory("workflows")}
|
| 153 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 154 |
+
selectedCategory === "workflows" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 155 |
+
}`}
|
| 156 |
+
>
|
| 157 |
+
أتمتة العمليات ⚙️
|
| 158 |
+
</button>
|
| 159 |
+
<button
|
| 160 |
+
onClick={() => setSelectedCategory("monitoring")}
|
| 161 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 162 |
+
selectedCategory === "monitoring" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 163 |
+
}`}
|
| 164 |
+
>
|
| 165 |
+
المراقبة والتحليل 📡
|
| 166 |
+
</button>
|
| 167 |
+
<button
|
| 168 |
+
onClick={() => setSelectedCategory("email")}
|
| 169 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-all duration-200 cursor-pointer ${
|
| 170 |
+
selectedCategory === "email" ? "bg-sky-600 text-white shadow-md shadow-sky-500/10" : "bg-white border border-sky-500/10 hover:bg-sky-500/5 text-slate-600"
|
| 171 |
+
}`}
|
| 172 |
+
>
|
| 173 |
+
صناديق البريد 📧
|
| 174 |
+
</button>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
{/* Bento Grid layout */}
|
| 179 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4">
|
| 180 |
+
{displayList.map((platform) => (
|
| 181 |
+
<div
|
| 182 |
+
key={platform.id}
|
| 183 |
+
className="glass-card p-5 rounded-2xl relative group overflow-hidden flex flex-col justify-between"
|
| 184 |
+
>
|
| 185 |
+
{/* Background absolute ambient glow on active/hover */}
|
| 186 |
+
<div className="absolute -bottom-4 -left-4 w-24 h-24 bg-sky-500/5 blur-2xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
|
| 187 |
+
|
| 188 |
+
<div>
|
| 189 |
+
{/* Header category indicator & Star favorite */}
|
| 190 |
+
<div className="flex justify-between items-center mb-3">
|
| 191 |
+
<span className="text-[10px] bg-sky-500/10 p-1.5 rounded-lg text-sky-850 flex items-center gap-1 font-bold border border-sky-500/10">
|
| 192 |
+
{getCategoryIcon(platform.category)}
|
| 193 |
+
{getCategoryLabel(platform.category)}
|
| 194 |
+
</span>
|
| 195 |
+
<button
|
| 196 |
+
onClick={(e) => toggleFavorite(platform.id, e)}
|
| 197 |
+
className="text-slate-400 hover:text-amber-500 transition cursor-pointer"
|
| 198 |
+
>
|
| 199 |
+
<Star className={`w-4 h-4 ${favorites[platform.id] ? "fill-amber-500 text-amber-500" : ""}`} />
|
| 200 |
+
</button>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
{/* Title & Description of platform */}
|
| 204 |
+
<h3 className="font-extrabold text-slate-800 text-base mb-1.5 group-hover:text-sky-700 transition">
|
| 205 |
+
{platform.name}
|
| 206 |
+
</h3>
|
| 207 |
+
<p className="text-xs text-slate-600 leading-relaxed mb-4">
|
| 208 |
+
{platform.description}
|
| 209 |
+
</p>
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
{/* Bottom details / link and free tier limit statement */}
|
| 213 |
+
<div className="border-t border-sky-500/10 pt-3 flex items-center justify-between mt-auto">
|
| 214 |
+
<div>
|
| 215 |
+
<span className="text-[9px] text-slate-500 block font-semibold">العرض المجاني للمطورين</span>
|
| 216 |
+
<span className="text-[11px] font-bold text-emerald-600">{platform.freeLimit}</span>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<a
|
| 220 |
+
href={platform.consoleUrl}
|
| 221 |
+
target="_blank"
|
| 222 |
+
rel="noopener noreferrer"
|
| 223 |
+
onClick={() => triggerHaptic()}
|
| 224 |
+
className="bg-white border border-sky-500/20 p-2 rounded-xl text-sky-700 hover:bg-sky-600 hover:text-white transition flex items-center justify-center shrink-0 shadow-sm"
|
| 225 |
+
title="فتح وحدة تحكم المنصة المباشرة"
|
| 226 |
+
>
|
| 227 |
+
<ExternalLink className="w-3.5 h-3.5" />
|
| 228 |
+
</a>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
))}
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
{displayList.length === 0 && (
|
| 235 |
+
<div className="glass-card p-12 text-center text-slate-500 rounded-2xl font-bold">
|
| 236 |
+
لا توجد منصات ضمن هذا الفهرس تطابق تفاصيل البحث.
|
| 237 |
+
</div>
|
| 238 |
+
)}
|
| 239 |
+
</div>
|
| 240 |
+
);
|
| 241 |
+
}
|
src/components/WorkflowsTab.tsx
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from "react";
|
| 2 |
+
import { DBService } from "../lib/db";
|
| 3 |
+
import { AutomationWorkflow, WorkflowNode, WorkflowConnection } from "../types";
|
| 4 |
+
import {
|
| 5 |
+
Network, Plus, Play, Pause, Trash, Download, Upload,
|
| 6 |
+
Settings, CheckCircle2, RefreshCw, AlertTriangle, PlayCircle
|
| 7 |
+
} from "lucide-react";
|
| 8 |
+
|
| 9 |
+
export default function WorkflowsTab({ triggerHaptic }: { triggerHaptic: () => void }) {
|
| 10 |
+
const [workflows, setWorkflows] = useState<AutomationWorkflow[]>([]);
|
| 11 |
+
const [selectedWorkflow, setSelectedWorkflow] = useState<AutomationWorkflow | null>(null);
|
| 12 |
+
|
| 13 |
+
const [runningStepIndex, setRunningStepIndex] = useState<number | null>(null);
|
| 14 |
+
const [isRunningTest, setIsRunningTest] = useState(false);
|
| 15 |
+
const [testConsoleLogs, setTestConsoleLogs] = useState<string[]>([]);
|
| 16 |
+
|
| 17 |
+
// form state for adding node
|
| 18 |
+
const [nodeType, setNodeType] = useState<any>("trigger");
|
| 19 |
+
const [nodeLabel, setNodeLabel] = useState("مشغل زمني يومي");
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
loadWorkflows();
|
| 23 |
+
}, []);
|
| 24 |
+
|
| 25 |
+
const loadWorkflows = async () => {
|
| 26 |
+
const list = await DBService.getAll<AutomationWorkflow>("workflows");
|
| 27 |
+
if (list.length === 0) {
|
| 28 |
+
// Seed initial dummy workflow templates
|
| 29 |
+
const seed: AutomationWorkflow[] = [
|
| 30 |
+
{
|
| 31 |
+
id: "work_1",
|
| 32 |
+
name: "استنساخ يومي ونشر تلقائي",
|
| 33 |
+
description: "أتمتة ذكية تقوم بسحب كود الموقع المستهدف وحك الإعلانات منه وإعادة نشره تلقائياً على Vercel.",
|
| 34 |
+
status: "active",
|
| 35 |
+
nodes: [
|
| 36 |
+
{ id: "n_1", type: "trigger", label: "مؤقت مجدول (Daily Cron)", x: 50, y: 50, config: { time: "00:00" } },
|
| 37 |
+
{ id: "n_2", type: "fetch", label: "جلب محتويات مثال.كوم", x: 200, y: 50, config: { url: "https://example.com" } },
|
| 38 |
+
{ id: "n_3", type: "parse", label: "تنظيف الأكواد والتعقبات", x: 350, y: 50, config: { strip_ads: true } },
|
| 39 |
+
{ id: "n_4", type: "deploy", label: "إعادة بناء Vercel السحابي", x: 500, y: 50, config: { platform: "Vercel" } }
|
| 40 |
+
],
|
| 41 |
+
connections: [
|
| 42 |
+
{ fromId: "n_1", toId: "n_2" },
|
| 43 |
+
{ fromId: "n_2", toId: "n_3" },
|
| 44 |
+
{ fromId: "n_3", toId: "n_4" }
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
id: "work_2",
|
| 49 |
+
name: "مراقب فشل API وتحديث المفاتيح",
|
| 50 |
+
description: "سير عمل ذكي يبدأ بفحص سرعات الاستجابة ويقوم بتبديل كود OpenAI تلقائياً إذا انقطع المخدم.",
|
| 51 |
+
status: "paused",
|
| 52 |
+
nodes: [
|
| 53 |
+
{ id: "n_1", type: "trigger", label: "فحص موقع مراقبة الأداء", x: 50, y: 150, config: { interval: "1m" } },
|
| 54 |
+
{ id: "n_2", type: "condition", label: "هل المخدم متوقف (Down)؟", x: 220, y: 150, config: { metric: "responseTime", threshold: 1000 } },
|
| 55 |
+
{ id: "n_3", type: "notify", label: "إرسال تقرير إيميل استنفار", x: 390, y: 100, config: { level: "urgent" } },
|
| 56 |
+
{ id: "n_4", type: "deploy", label: "تحويل المفتاح النشط للطوارئ", x: 390, y: 200, config: { target: "backup_key" } }
|
| 57 |
+
],
|
| 58 |
+
connections: [
|
| 59 |
+
{ fromId: "n_1", toId: "n_2" },
|
| 60 |
+
{ fromId: "n_2", toId: "n_3" },
|
| 61 |
+
{ fromId: "n_2", toId: "n_4" }
|
| 62 |
+
]
|
| 63 |
+
}
|
| 64 |
+
];
|
| 65 |
+
for (const w of seed) {
|
| 66 |
+
await DBService.put("workflows", w);
|
| 67 |
+
}
|
| 68 |
+
setWorkflows(seed);
|
| 69 |
+
setSelectedWorkflow(seed[0] || null);
|
| 70 |
+
} else {
|
| 71 |
+
setWorkflows(list);
|
| 72 |
+
setSelectedWorkflow(list[0] || null);
|
| 73 |
+
}
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const handleCreateWorkflow = async () => {
|
| 77 |
+
triggerHaptic();
|
| 78 |
+
const newW: AutomationWorkflow = {
|
| 79 |
+
id: "work_" + Date.now(),
|
| 80 |
+
name: "سير عمل مخصص رقم " + (workflows.length + 1),
|
| 81 |
+
description: "سير عمل مؤتمت لربط الخدمات محلياً وتعديل مسارت الويب.",
|
| 82 |
+
status: "active",
|
| 83 |
+
nodes: [
|
| 84 |
+
{ id: "n_1", type: "trigger", label: "مشغل يدوي أو webhook", x: 100, y: 100, config: {} }
|
| 85 |
+
],
|
| 86 |
+
connections: []
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
await DBService.put("workflows", newW);
|
| 90 |
+
setWorkflows(prev => [...prev, newW]);
|
| 91 |
+
setSelectedWorkflow(newW);
|
| 92 |
+
|
| 93 |
+
await DBService.put("auditLog", {
|
| 94 |
+
id: "log_" + Date.now(),
|
| 95 |
+
timestamp: Date.now(),
|
| 96 |
+
action: "إنشاء أتمتة جديدة",
|
| 97 |
+
details: `تم بنجاح إنشاء سير عمل [${newW.name}] قيد التصميم المتقدم`,
|
| 98 |
+
status: "success"
|
| 99 |
+
});
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
const handleDeleteWorkflow = async (id: string, e: React.MouseEvent) => {
|
| 103 |
+
e.stopPropagation();
|
| 104 |
+
triggerHaptic();
|
| 105 |
+
await DBService.delete("workflows", id);
|
| 106 |
+
setWorkflows(prev => prev.filter(w => w.id !== id));
|
| 107 |
+
if (selectedWorkflow?.id === id) {
|
| 108 |
+
setSelectedWorkflow(null);
|
| 109 |
+
}
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
const handleAddNodeToActive = async () => {
|
| 113 |
+
if (!selectedWorkflow) return;
|
| 114 |
+
triggerHaptic();
|
| 115 |
+
|
| 116 |
+
const newNode: WorkflowNode = {
|
| 117 |
+
id: "n_" + Date.now(),
|
| 118 |
+
type: nodeType,
|
| 119 |
+
label: nodeLabel,
|
| 120 |
+
x: 100,
|
| 121 |
+
y: 100,
|
| 122 |
+
config: {}
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
// Auto connect to the last node to be visual
|
| 126 |
+
const updatedNodes = [...selectedWorkflow.nodes, newNode];
|
| 127 |
+
const updatedConnections = [...selectedWorkflow.connections];
|
| 128 |
+
const lastNode = selectedWorkflow.nodes[selectedWorkflow.nodes.length - 1];
|
| 129 |
+
if (lastNode) {
|
| 130 |
+
updatedConnections.push({ fromId: lastNode.id, toId: newNode.id });
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const updatedWorkflow: AutomationWorkflow = {
|
| 134 |
+
...selectedWorkflow,
|
| 135 |
+
nodes: updatedNodes,
|
| 136 |
+
connections: updatedConnections
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
await DBService.put("workflows", updatedWorkflow);
|
| 140 |
+
setSelectedWorkflow(updatedWorkflow);
|
| 141 |
+
setWorkflows(prev => prev.map(w => w.id === selectedWorkflow.id ? updatedWorkflow : w));
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
const runLiveTestExecution = async () => {
|
| 145 |
+
if (!selectedWorkflow || isRunningTest) return;
|
| 146 |
+
triggerHaptic();
|
| 147 |
+
setIsRunningTest(true);
|
| 148 |
+
setTestConsoleLogs(["⏳ البدء في معالجة مجمع سير العمل المجدول..."]);
|
| 149 |
+
|
| 150 |
+
const steps = selectedWorkflow.nodes;
|
| 151 |
+
|
| 152 |
+
for (let i = 0; i < steps.length; i++) {
|
| 153 |
+
setRunningStepIndex(i);
|
| 154 |
+
const step = steps[i];
|
| 155 |
+
if (step) {
|
| 156 |
+
await new Promise(r => setTimeout(r, 1200));
|
| 157 |
+
|
| 158 |
+
let logMsg = `🟢 [تمت المعالجة] تشغيل النود ${step.label} (${step.type.toUpperCase()})`;
|
| 159 |
+
if (step.type === "trigger") logMsg = `⚡ [بدء التشغيل] إشارة تشغيل من: ${step.label}`;
|
| 160 |
+
if (step.type === "deploy") logMsg = `🚀 [نشر مكمل] تم بنجاح دفع ملفات الاستنساخ للمخدم السحابي المستهدف `;
|
| 161 |
+
|
| 162 |
+
setTestConsoleLogs(prev => [...prev, logMsg]);
|
| 163 |
+
triggerHaptic();
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
setTimeout(() => {
|
| 168 |
+
setTestConsoleLogs(prev => [...prev, "🏁 اكتمال كافة خطوات سير العمل بنجاح ودقّة 100% 🚀"]);
|
| 169 |
+
setIsRunningTest(false);
|
| 170 |
+
setRunningStepIndex(null);
|
| 171 |
+
}, 800);
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
const exportTon8nJson = () => {
|
| 175 |
+
if (!selectedWorkflow) return;
|
| 176 |
+
triggerHaptic();
|
| 177 |
+
|
| 178 |
+
// Mapping simplified format to standard n8n style workflow json
|
| 179 |
+
const n8nSchema = {
|
| 180 |
+
meta: { templateId: "siteclone_pro_workflow" },
|
| 181 |
+
nodes: selectedWorkflow.nodes.map(n => ({
|
| 182 |
+
parameters: n.config,
|
| 183 |
+
id: n.id,
|
| 184 |
+
name: n.label,
|
| 185 |
+
type: `n8n-nodes-base.${n.type === "trigger" ? "cron" : n.type === "fetch" ? "httpRequest" : "webhook"}`,
|
| 186 |
+
typeVersion: 1,
|
| 187 |
+
position: [n.x, n.y]
|
| 188 |
+
})),
|
| 189 |
+
connections: {}
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
const blob = new Blob([JSON.stringify(n8nSchema, null, 2)], { type: "application/json" });
|
| 193 |
+
const url = URL.createObjectURL(blob);
|
| 194 |
+
const a = document.createElement("a");
|
| 195 |
+
a.href = url;
|
| 196 |
+
a.download = `n8n_export_${selectedWorkflow.name.replace(/\s+/g, "_")}.json`;
|
| 197 |
+
document.body.appendChild(a);
|
| 198 |
+
a.click();
|
| 199 |
+
document.body.removeChild(a);
|
| 200 |
+
URL.revokeObjectURL(url);
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
const getStepBgColor = (type: string, active: boolean) => {
|
| 204 |
+
if (active) return "border-sky-500 bg-sky-500/20 text-white shadow-lg shadow-sky-500/25";
|
| 205 |
+
switch (type) {
|
| 206 |
+
case "trigger": return "border-yellow-500/30 bg-yellow-500/5 text-yellow-300";
|
| 207 |
+
case "deploy": return "border-purple-500/30 bg-purple-500/5 text-purple-300";
|
| 208 |
+
case "condition": return "border-orange-500/30 bg-orange-500/5 text-orange-300";
|
| 209 |
+
default: return "border-slate-800 bg-slate-900/80 text-slate-300";
|
| 210 |
+
}
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
return (
|
| 214 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in">
|
| 215 |
+
{/* Sidebar (Left) */}
|
| 216 |
+
<div className="lg:col-span-4 space-y-4">
|
| 217 |
+
{/* Workflows List */}
|
| 218 |
+
<div className="glass-card p-6 rounded-2xl space-y-4">
|
| 219 |
+
<div className="flex justify-between items-center">
|
| 220 |
+
<h2 className="text-base font-bold text-sky-400 flex items-center gap-1.5 animate-pulse">
|
| 221 |
+
<Network className="w-5 h-5" />
|
| 222 |
+
أتمتة العمليات (Workflows)
|
| 223 |
+
</h2>
|
| 224 |
+
<button
|
| 225 |
+
onClick={handleCreateWorkflow}
|
| 226 |
+
className="bg-sky-500 hover:bg-sky-600 text-slate-950 p-1.5 rounded-lg transition"
|
| 227 |
+
>
|
| 228 |
+
<Plus className="w-4 h-4" />
|
| 229 |
+
</button>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<div className="space-y-2 select-none">
|
| 233 |
+
{workflows.map((w) => (
|
| 234 |
+
<div
|
| 235 |
+
key={w.id}
|
| 236 |
+
onClick={() => {
|
| 237 |
+
triggerHaptic();
|
| 238 |
+
setSelectedWorkflow(w);
|
| 239 |
+
}}
|
| 240 |
+
className={`p-3 rounded-xl border transition-all cursor-pointer flex justify-between items-center ${
|
| 241 |
+
selectedWorkflow?.id === w.id
|
| 242 |
+
? "bg-sky-500/10 border-sky-500/40 text-white"
|
| 243 |
+
: "bg-slate-900/60 border-slate-800 hover:border-slate-700 text-slate-300"
|
| 244 |
+
}`}
|
| 245 |
+
>
|
| 246 |
+
<div className="min-w-0">
|
| 247 |
+
<h4 className="text-xs font-semibold truncate text-left">{w.name}</h4>
|
| 248 |
+
<span className="text-[9px] text-slate-500 font-mono">
|
| 249 |
+
{w.nodes.length} نودات نشطة
|
| 250 |
+
</span>
|
| 251 |
+
</div>
|
| 252 |
+
<div className="flex items-center gap-1.5 scale-90">
|
| 253 |
+
<span className={`text-[9px] px-1.5 rounded ${
|
| 254 |
+
w.status === "active" ? "bg-emerald-500/10 text-emerald-400" : "bg-slate-800 text-slate-400"
|
| 255 |
+
}`}>
|
| 256 |
+
{w.status === "active" ? "فعال" : "موقوف"}
|
| 257 |
+
</span>
|
| 258 |
+
<button
|
| 259 |
+
onClick={(e) => handleDeleteWorkflow(w.id, e)}
|
| 260 |
+
className="p-1 hover:bg-red-500/20 rounded text-red-400 transition"
|
| 261 |
+
>
|
| 262 |
+
<Trash className="w-3" />
|
| 263 |
+
</button>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
))}
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
|
| 270 |
+
{/* Node creation block if any active */}
|
| 271 |
+
{selectedWorkflow && (
|
| 272 |
+
<div className="glass-card p-6 rounded-2xl space-y-4">
|
| 273 |
+
<h3 className="text-sm font-semibold text-slate-300">أضف خطوة (نود) جديدة للمخطط</h3>
|
| 274 |
+
|
| 275 |
+
<div className="space-y-3">
|
| 276 |
+
<div>
|
| 277 |
+
<label className="block text-[10px] text-slate-500 mb-1">نوع العملية (Type)</label>
|
| 278 |
+
<select
|
| 279 |
+
value={nodeType}
|
| 280 |
+
onChange={(e) => {
|
| 281 |
+
setNodeType(e.target.value as any);
|
| 282 |
+
// Autofill label
|
| 283 |
+
if (e.target.value === "trigger") setNodeLabel("مشغل زمني يومي");
|
| 284 |
+
if (e.target.value === "fetch") setNodeLabel("جلب موقع خارجي");
|
| 285 |
+
if (e.target.value === "parse") setNodeLabel("تنظيف ملفات CSS");
|
| 286 |
+
if (e.target.value === "deploy") setNodeLabel("أمر النشر السحابي");
|
| 287 |
+
}}
|
| 288 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-sky-500 w-full"
|
| 289 |
+
>
|
| 290 |
+
<option value="trigger">Trigger (مشغل زمني)</option>
|
| 291 |
+
<option value="fetch">Fetch (جلب / تحميل)</option>
|
| 292 |
+
<option value="parse">Parse (معالجة الأكوارد)</option>
|
| 293 |
+
<option value="condition">Condition (شرط منطقي)</option>
|
| 294 |
+
<option value="deploy">Deploy (عملية نشر)</option>
|
| 295 |
+
</select>
|
| 296 |
+
</div>
|
| 297 |
+
|
| 298 |
+
<div>
|
| 299 |
+
<label className="block text-[10px] text-slate-500 mb-1">اسم معرف النود لقراءة السجل</label>
|
| 300 |
+
<input
|
| 301 |
+
type="text"
|
| 302 |
+
value={nodeLabel}
|
| 303 |
+
onChange={(e) => setNodeLabel(e.target.value)}
|
| 304 |
+
className="bg-slate-900 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-sky-500 w-full"
|
| 305 |
+
/>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<button
|
| 309 |
+
onClick={handleAddNodeToActive}
|
| 310 |
+
className="w-full bg-slate-900 text-sky-400 border border-slate-800 hover:border-sky-400/20 py-2 rounded-xl text-xs flex justify-center items-center gap-1 transition"
|
| 311 |
+
>
|
| 312 |
+
<Plus className="w-4 h-4" />
|
| 313 |
+
أضف واربط تلقائياً
|
| 314 |
+
</button>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
)}
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
{/* Editor & Board canvas (Right) */}
|
| 321 |
+
<div className="lg:col-span-8 space-y-4">
|
| 322 |
+
{selectedWorkflow ? (
|
| 323 |
+
<div className="glass-card p-6 rounded-2xl flex flex-col h-[550px]">
|
| 324 |
+
{/* Header controls select */}
|
| 325 |
+
<div className="flex justify-between items-center pb-4 border-b border-slate-800 mb-4 gap-3">
|
| 326 |
+
<div>
|
| 327 |
+
<h3 className="font-bold text-slate-200 text-base">{selectedWorkflow.name}</h3>
|
| 328 |
+
<p className="text-xs text-slate-500 mt-0.5">{selectedWorkflow.description}</p>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
{/* Action triggers */}
|
| 332 |
+
<div className="flex gap-2">
|
| 333 |
+
<button
|
| 334 |
+
onClick={runLiveTestExecution}
|
| 335 |
+
disabled={isRunningTest}
|
| 336 |
+
className="bg-sky-500 hover:bg-sky-600 disabled:opacity-50 text-slate-950 text-xs px-4 py-2 rounded-xl font-bold transition flex items-center gap-1"
|
| 337 |
+
>
|
| 338 |
+
<Play className="w-3.5 h-3.5" />
|
| 339 |
+
تشغيل تجريبي
|
| 340 |
+
</button>
|
| 341 |
+
<button
|
| 342 |
+
onClick={exportTon8nJson}
|
| 343 |
+
className="bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 text-xs px-4 py-2 rounded-xl transition flex items-center gap-1.5"
|
| 344 |
+
>
|
| 345 |
+
<Download className="w-3.5 h-3.5" />
|
| 346 |
+
تصدير لـ n8n
|
| 347 |
+
</button>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
|
| 351 |
+
{/* Visual interactive flows representation */}
|
| 352 |
+
<div className="flex-1 bg-slate-950/70 border border-slate-900/80 rounded-xl p-4 overflow-y-auto space-y-4 relative flex items-center justify-around flex-wrap gap-4 select-none">
|
| 353 |
+
{selectedWorkflow.nodes.map((n, i) => (
|
| 354 |
+
<React.Fragment key={n.id}>
|
| 355 |
+
<div className={`p-4 rounded-xl border-2 transition-all duration-300 w-44 text-center ${
|
| 356 |
+
getStepBgColor(n.type, runningStepIndex === i)
|
| 357 |
+
}`}>
|
| 358 |
+
<span className="text-[8px] uppercase tracking-wider block text-slate-500 mb-1">
|
| 359 |
+
{n.type}
|
| 360 |
+
</span>
|
| 361 |
+
<span className="text-xs font-bold font-sans tracking-tight block truncate">
|
| 362 |
+
{n.label}
|
| 363 |
+
</span>
|
| 364 |
+
</div>
|
| 365 |
+
|
| 366 |
+
{/* Arrow connect simulator */}
|
| 367 |
+
{i < selectedWorkflow.nodes.length - 1 && (
|
| 368 |
+
<div className="text-slate-700 font-bold px-1 py-1 shrink-0">
|
| 369 |
+
➔
|
| 370 |
+
</div>
|
| 371 |
+
)}
|
| 372 |
+
</React.Fragment>
|
| 373 |
+
))}
|
| 374 |
+
</div>
|
| 375 |
+
|
| 376 |
+
{/* Simulated Live Console logs output */}
|
| 377 |
+
<div className="h-40 bg-slate-950 border border-slate-900 rounded-xl mt-4 overflow-y-auto p-4 flex flex-col">
|
| 378 |
+
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-mono font-bold border-b border-slate-900 pb-1 mb-2">
|
| 379 |
+
سجل المخرجات للأتمتة (Active automation logger system)
|
| 380 |
+
</span>
|
| 381 |
+
<div className="font-mono text-xs text-slate-300 space-y-1.5 flex-1 select-text">
|
| 382 |
+
{testConsoleLogs.map((log, lIndex) => (
|
| 383 |
+
<p key={lIndex} className="leading-snug text-left">{log}</p>
|
| 384 |
+
))}
|
| 385 |
+
{testConsoleLogs.length === 0 && (
|
| 386 |
+
<p className="text-slate-600 text-center py-6">انقر على تشغيل تجريبي لرؤية نتائج الفحص المنطقي.</p>
|
| 387 |
+
)}
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
) : (
|
| 392 |
+
<div className="glass-card p-12 text-center rounded-2xl h-[550px] flex flex-col justify-center items-center">
|
| 393 |
+
<p className="text-slate-500 text-sm">لا يوجد مسار عمل محدد للتحرير. يرجى البدء بإنشاء سير عمل.</p>
|
| 394 |
+
</div>
|
| 395 |
+
)}
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
);
|
| 399 |
+
}
|