index.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <!DOCTYPE html>
  2. <html lang="en" class="dark">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Raft KV Admin</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
  9. <script>
  10. tailwind.config = {
  11. darkMode: 'class',
  12. theme: {
  13. extend: {
  14. colors: {
  15. dark: {
  16. 900: '#0f172a',
  17. 800: '#1e293b',
  18. 700: '#334155',
  19. },
  20. brand: {
  21. 500: '#3b82f6',
  22. 600: '#2563eb',
  23. }
  24. }
  25. }
  26. }
  27. }
  28. </script>
  29. <style>
  30. [x-cloak] { display: none !important; }
  31. </style>
  32. </head>
  33. <body class="bg-dark-900 text-slate-300 font-sans antialiased" x-data="app()">
  34. <!-- Login Modal -->
  35. <div x-show="!isLoggedIn" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
  36. <div class="bg-dark-800 p-8 rounded-2xl shadow-2xl w-full max-w-md border border-dark-700">
  37. <h2 class="text-2xl font-bold text-white mb-6 text-center">Raft KV Admin</h2>
  38. <form @submit.prevent="login">
  39. <div class="mb-4">
  40. <label class="block text-sm font-medium mb-2">Username</label>
  41. <input type="text" x-model="loginForm.user" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-brand-500 outline-none">
  42. </div>
  43. <div class="mb-4">
  44. <label class="block text-sm font-medium mb-2">Password</label>
  45. <input type="password" x-model="loginForm.pass" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-brand-500 outline-none">
  46. </div>
  47. <div class="mb-6">
  48. <label class="block text-sm font-medium mb-2">OTP (Optional)</label>
  49. <input type="text" x-model="loginForm.otp" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-brand-500 outline-none">
  50. </div>
  51. <button type="submit" class="w-full bg-brand-600 hover:bg-brand-500 text-white font-bold py-3 rounded-lg transition shadow-lg shadow-brand-500/20">
  52. <span x-show="!loading">Login</span>
  53. <span x-show="loading">Authenticating...</span>
  54. </button>
  55. <p x-show="errorMsg" x-text="errorMsg" class="mt-4 text-red-400 text-center text-sm"></p>
  56. </form>
  57. </div>
  58. </div>
  59. <!-- Main Layout -->
  60. <div x-show="isLoggedIn" x-cloak class="flex h-screen overflow-hidden">
  61. <!-- Sidebar -->
  62. <aside class="w-64 bg-dark-800 border-r border-dark-700 flex flex-col">
  63. <div class="p-6 flex items-center gap-3">
  64. <div class="w-8 h-8 rounded bg-brand-500 flex items-center justify-center font-bold text-white">R</div>
  65. <span class="text-xl font-bold text-white tracking-tight">Raft Admin</span>
  66. </div>
  67. <nav class="flex-1 px-4 space-y-2 mt-4">
  68. <a @click="tab = 'dashboard'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'dashboard', 'hover:bg-dark-700': tab !== 'dashboard'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
  69. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
  70. Dashboard
  71. </a>
  72. <a @click="tab = 'data'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'data', 'hover:bg-dark-700': tab !== 'data'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
  73. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
  74. KV Explorer
  75. </a>
  76. <div class="pt-4 pb-2 px-2 text-xs font-bold text-slate-500 uppercase">Access Control</div>
  77. <a @click="fetchUsers(); tab = 'users'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'users', 'hover:bg-dark-700': tab !== 'users'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
  78. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
  79. Users
  80. </a>
  81. <a @click="fetchRoles(); tab = 'roles'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'roles', 'hover:bg-dark-700': tab !== 'roles'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
  82. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
  83. Roles
  84. </a>
  85. </nav>
  86. <div class="p-4 border-t border-dark-700">
  87. <button @click="logout" class="flex items-center gap-3 w-full px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">
  88. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
  89. Logout
  90. </button>
  91. </div>
  92. </aside>
  93. <!-- Main Content -->
  94. <main class="flex-1 flex flex-col overflow-hidden bg-dark-900">
  95. <!-- Topbar -->
  96. <header class="h-16 bg-dark-800/50 backdrop-blur border-b border-dark-700 flex items-center justify-between px-8">
  97. <h2 class="text-xl font-semibold text-white capitalize" x-text="tab"></h2>
  98. <div class="flex items-center gap-4">
  99. <span class="text-sm text-slate-400">Connected as <span class="text-white font-medium" x-text="username"></span></span>
  100. <div class="w-8 h-8 rounded-full bg-gradient-to-r from-brand-500 to-purple-500"></div>
  101. </div>
  102. </header>
  103. <!-- Scrollable Area -->
  104. <div class="flex-1 overflow-y-auto p-8">
  105. <!-- Dashboard Tab -->
  106. <div x-show="tab === 'dashboard'" class="space-y-6">
  107. <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
  108. <!-- Stat Card -->
  109. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
  110. <div class="text-slate-400 mb-2">Total Users</div>
  111. <div class="text-3xl font-bold text-white" x-text="users.length || '-'"></div>
  112. </div>
  113. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
  114. <div class="text-slate-400 mb-2">Cluster Status</div>
  115. <div class="text-3xl font-bold text-green-400">Healthy</div>
  116. </div>
  117. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
  118. <div class="text-slate-400 mb-2">Keys Checked</div>
  119. <div class="text-3xl font-bold text-blue-400" x-text="kvHistory.length"></div>
  120. </div>
  121. </div>
  122. <div class="bg-dark-800 rounded-2xl border border-dark-700 p-6">
  123. <h3 class="text-lg font-bold text-white mb-4">Quick Command</h3>
  124. <div class="flex gap-2">
  125. <input type="text" x-model="quickCmd" placeholder="GET system.config" class="flex-1 bg-dark-900 border border-dark-700 rounded-lg p-3 text-white outline-none focus:border-brand-500">
  126. <button @click="runQuickCmd" class="bg-brand-600 px-6 rounded-lg text-white font-bold hover:bg-brand-500">Run</button>
  127. </div>
  128. <pre x-show="quickResult" class="mt-4 bg-black/30 p-4 rounded-lg text-sm text-green-400 font-mono" x-text="quickResult"></pre>
  129. </div>
  130. </div>
  131. <!-- KV Explorer Tab -->
  132. <div x-show="tab === 'data'" class="space-y-6">
  133. <div class="bg-dark-800 rounded-2xl border border-dark-700 overflow-hidden">
  134. <div class="p-6 border-b border-dark-700 flex justify-between items-center">
  135. <h3 class="text-lg font-bold text-white">Key-Value Operations</h3>
  136. </div>
  137. <div class="p-6 space-y-4">
  138. <!-- Operation Form -->
  139. <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
  140. <select x-model="kvOp" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  141. <option value="GET">GET</option>
  142. <option value="SET">SET</option>
  143. <option value="DEL">DEL</option>
  144. </select>
  145. <input x-model="kvKey" type="text" placeholder="Key" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  146. <input x-show="kvOp === 'SET'" x-model="kvVal" type="text" placeholder="Value" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white md:col-span-1">
  147. <button @click="executeKV" class="bg-brand-600 text-white rounded-lg p-3 font-bold hover:bg-brand-500">Execute</button>
  148. </div>
  149. <!-- Result Display -->
  150. <div x-show="kvResult" class="mt-4 p-4 rounded-lg border border-dark-700" :class="kvError ? 'bg-red-900/20 border-red-800 text-red-200' : 'bg-green-900/20 border-green-800 text-green-200'">
  151. <span class="font-bold block mb-1">Result:</span>
  152. <span class="font-mono" x-text="kvResult"></span>
  153. </div>
  154. </div>
  155. </div>
  156. </div>
  157. <!-- Users Tab -->
  158. <div x-show="tab === 'users'" class="space-y-6">
  159. <!-- Actions -->
  160. <div class="flex justify-end">
  161. <button @click="showAddUserModal = true" class="bg-brand-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-brand-500 flex items-center gap-2">
  162. <span>+ Add User</span>
  163. </button>
  164. </div>
  165. <!-- User Table -->
  166. <div class="bg-dark-800 rounded-2xl border border-dark-700 overflow-hidden">
  167. <table class="w-full text-left">
  168. <thead class="bg-dark-700 text-slate-300">
  169. <tr>
  170. <th class="p-4">Username</th>
  171. <th class="p-4">Roles</th>
  172. <th class="p-4">Status</th>
  173. <th class="p-4 text-right">Actions</th>
  174. </tr>
  175. </thead>
  176. <tbody class="divide-y divide-dark-700">
  177. <template x-for="u in users" :key="u.username">
  178. <tr class="hover:bg-dark-700/50 transition">
  179. <td class="p-4 font-medium text-white" x-text="u.username"></td>
  180. <td class="p-4">
  181. <div class="flex gap-2">
  182. <template x-for="r in (u.roles || [])">
  183. <span class="px-2 py-1 bg-blue-500/20 text-blue-300 rounded text-xs" x-text="r"></span>
  184. </template>
  185. </div>
  186. </td>
  187. <td class="p-4">
  188. <span class="inline-flex items-center gap-1 text-green-400 text-sm">
  189. <span class="w-2 h-2 rounded-full bg-green-400"></span> Active
  190. </span>
  191. </td>
  192. <td class="p-4 text-right">
  193. <button @click="unlockUser(u.username)" class="text-yellow-500 hover:text-yellow-400 text-sm mr-3">Reset Lock</button>
  194. <!-- <button class="text-red-500 hover:text-red-400 text-sm">Delete</button> -->
  195. </td>
  196. </tr>
  197. </template>
  198. </tbody>
  199. </table>
  200. </div>
  201. </div>
  202. <!-- Roles Tab -->
  203. <div x-show="tab === 'roles'" class="space-y-6">
  204. <div class="flex justify-end">
  205. <button @click="showAddRoleModal = true" class="bg-brand-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-brand-500">
  206. + Create Role
  207. </button>
  208. </div>
  209. <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  210. <template x-for="r in roles" :key="r.name">
  211. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700 hover:border-brand-500 transition group">
  212. <div class="flex justify-between items-start mb-4">
  213. <h3 class="text-lg font-bold text-white" x-text="r.name"></h3>
  214. <span class="text-xs text-slate-500 bg-dark-900 px-2 py-1 rounded" x-text="(r.permissions || []).length + ' Rules'"></span>
  215. </div>
  216. <div class="space-y-2 mb-4">
  217. <template x-for="p in (r.permissions || []).slice(0, 3)">
  218. <div class="text-sm text-slate-400 font-mono bg-dark-900/50 p-1 rounded px-2">
  219. <span x-text="p.key"></span>
  220. <span class="text-brand-500" x-text="'[' + p.actions.join(',') + ']'"></span>
  221. </div>
  222. </template>
  223. <div x-show="(r.permissions || []).length > 3" class="text-xs text-slate-500 italic">...and more</div>
  224. </div>
  225. </div>
  226. </template>
  227. </div>
  228. </div>
  229. </div>
  230. </main>
  231. </div>
  232. <!-- Modals -->
  233. <!-- Add User Modal -->
  234. <div x-show="showAddUserModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  235. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  236. <h3 class="text-xl font-bold text-white mb-4">Create New User</h3>
  237. <div class="space-y-4">
  238. <input x-model="newUser.username" type="text" placeholder="Username" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  239. <input x-model="newUser.password" type="password" placeholder="Password" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  240. <input x-model="newUser.roles" type="text" placeholder="Roles (comma separated)" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  241. <div class="flex gap-3 pt-2">
  242. <button @click="createUser" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
  243. <button @click="showAddUserModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  244. </div>
  245. </div>
  246. </div>
  247. </div>
  248. <!-- Add Role Modal -->
  249. <div x-show="showAddRoleModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  250. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  251. <h3 class="text-xl font-bold text-white mb-4">Create New Role</h3>
  252. <div class="space-y-4">
  253. <input x-model="newRoleName" type="text" placeholder="Role Name" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  254. <div class="flex gap-3 pt-2">
  255. <button @click="createRole" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
  256. <button @click="showAddRoleModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  257. </div>
  258. </div>
  259. </div>
  260. </div>
  261. <script>
  262. function app() {
  263. return {
  264. isLoggedIn: false,
  265. token: localStorage.getItem('raft_token') || '',
  266. username: localStorage.getItem('raft_user') || '',
  267. tab: 'dashboard',
  268. loading: false,
  269. errorMsg: '',
  270. // Login Form
  271. loginForm: { user: '', pass: '', otp: '' },
  272. // Data
  273. users: [],
  274. roles: [],
  275. kvHistory: [], // Track KV ops
  276. // KV Op
  277. kvOp: 'GET',
  278. kvKey: '',
  279. kvVal: '',
  280. kvResult: '',
  281. kvError: false,
  282. // Quick Command
  283. quickCmd: '',
  284. quickResult: '',
  285. // Modals
  286. showAddUserModal: false,
  287. newUser: { username: '', password: '', roles: '' },
  288. showAddRoleModal: false,
  289. newRoleName: '',
  290. init() {
  291. if (this.token) {
  292. this.isLoggedIn = true;
  293. this.fetchUsers();
  294. }
  295. },
  296. async api(command, args = []) {
  297. try {
  298. const res = await fetch('/api/proxy', {
  299. method: 'POST',
  300. headers: {
  301. 'Content-Type': 'application/json',
  302. 'X-Session-Token': this.token
  303. },
  304. body: JSON.stringify({ command, args })
  305. });
  306. const json = await res.json();
  307. if (json.status === 'error') {
  308. if (json.error === 'Session Expired') {
  309. this.logout();
  310. }
  311. throw new Error(json.error);
  312. }
  313. return json.data;
  314. } catch (e) {
  315. throw e;
  316. }
  317. },
  318. async login() {
  319. this.loading = true;
  320. this.errorMsg = '';
  321. try {
  322. // Args: user pass [otp]
  323. const args = [this.loginForm.user, this.loginForm.pass];
  324. if (this.loginForm.otp) args.push(this.loginForm.otp);
  325. const token = await this.api('LOGIN', args);
  326. // If success, token is returned string
  327. // Note: My TCP API returns "OK <token>" string or JSON if modified.
  328. // The proxy handles "OK " stripping. So 'token' variable here holds the raw token string.
  329. this.token = token.trim();
  330. this.username = this.loginForm.user;
  331. localStorage.setItem('raft_token', this.token);
  332. localStorage.setItem('raft_user', this.username);
  333. this.isLoggedIn = true;
  334. this.fetchUsers();
  335. } catch (e) {
  336. this.errorMsg = e.message;
  337. } finally {
  338. this.loading = false;
  339. }
  340. },
  341. logout() {
  342. this.api('LOGOUT');
  343. this.token = '';
  344. this.username = '';
  345. this.isLoggedIn = false;
  346. localStorage.removeItem('raft_token');
  347. },
  348. async fetchUsers() {
  349. try {
  350. this.users = await this.api('USER_LIST');
  351. if (typeof this.users === 'string') {
  352. // If proxy didn't parse JSON (edge case), try parse
  353. this.users = JSON.parse(this.users);
  354. }
  355. } catch (e) {
  356. console.error("Failed to fetch users", e);
  357. // Show error if it's "unknown command" suggesting outdated server
  358. if (e.message.includes("unknown command")) {
  359. alert("Server error: USER_LIST command not found. Please restart your Raft node to apply latest changes.");
  360. } else {
  361. this.errorMsg = "Failed to load users: " + e.message;
  362. }
  363. }
  364. },
  365. async fetchRoles() {
  366. try {
  367. this.roles = await this.api('ROLE_LIST');
  368. } catch (e) {
  369. console.error(e);
  370. if (e.message.includes("unknown command")) {
  371. alert("Server error: ROLE_LIST command not found. Please restart your Raft node to apply latest changes.");
  372. }
  373. }
  374. },
  375. async executeKV() {
  376. this.kvResult = 'Processing...';
  377. this.kvError = false;
  378. try {
  379. const args = [this.kvKey];
  380. if (this.kvOp === 'SET') args.push(this.kvVal);
  381. const res = await this.api(this.kvOp, args);
  382. this.kvResult = typeof res === 'object' ? JSON.stringify(res) : res;
  383. this.kvHistory.push({ op: this.kvOp, key: this.kvKey, time: new Date().toLocaleTimeString() });
  384. } catch (e) {
  385. this.kvError = true;
  386. this.kvResult = e.message;
  387. }
  388. },
  389. async runQuickCmd() {
  390. if(!this.quickCmd) return;
  391. const parts = this.quickCmd.split(' ');
  392. const cmd = parts[0].toUpperCase();
  393. const args = parts.slice(1);
  394. try {
  395. const res = await this.api(cmd, args);
  396. this.quickResult = typeof res === 'object' ? JSON.stringify(res, null, 2) : res;
  397. } catch (e) {
  398. this.quickResult = "Error: " + e.message;
  399. }
  400. },
  401. async createUser() {
  402. try {
  403. await this.api('USER_CREATE', [this.newUser.username, this.newUser.password, this.newUser.roles]);
  404. this.showAddUserModal = false;
  405. this.newUser = { username: '', password: '', roles: '' };
  406. this.fetchUsers();
  407. } catch (e) {
  408. alert(e.message);
  409. }
  410. },
  411. async createRole() {
  412. try {
  413. await this.api('ROLE_CREATE', [this.newRoleName]);
  414. this.showAddRoleModal = false;
  415. this.newRoleName = '';
  416. this.fetchRoles();
  417. } catch (e) {
  418. alert(e.message);
  419. }
  420. },
  421. async unlockUser(username) {
  422. if(!confirm("Unlock user " + username + "?")) return;
  423. try {
  424. await this.api('USER_UNLOCK', [username]);
  425. alert("Unlocked");
  426. } catch (e) { alert(e.message); }
  427. }
  428. }
  429. }
  430. </script>
  431. </body>
  432. </html>