index.html 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="apple-mobile-web-app-capable" content="yes">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <title>SQLite Admin</title>
  8. <link rel="stylesheet" href="../script/semantic/semantic.min.css">
  9. <link rel="stylesheet" href="../script/ao.css">
  10. <script src="../script/jquery.min.js"></script>
  11. <script src="../script/ao_module.js"></script>
  12. <script src="../script/semantic/semantic.min.js"></script>
  13. <style>
  14. :root {
  15. --accent: #4a6cf7;
  16. --accent-h: #3557e0;
  17. --sidebar: #1e2233;
  18. --side-txt: #c8cfdf;
  19. --side-muted: #6b7490;
  20. --bg: #f4f6fb;
  21. --surface: #ffffff;
  22. --border: #e2e6f0;
  23. --text: #1a1f36;
  24. --muted: #6b7a99;
  25. --danger: #e84444;
  26. --success: #2ecc71;
  27. --null-col: #a0a8bf;
  28. --pk-col: #f0f4ff;
  29. --row-hover:#f7f9ff;
  30. }
  31. * { box-sizing: border-box; }
  32. body {
  33. margin: 0; padding: 0;
  34. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  35. font-size: 13px;
  36. background: var(--bg);
  37. color: var(--text);
  38. height: 100vh;
  39. display: flex;
  40. flex-direction: column;
  41. overflow: hidden;
  42. }
  43. /* ── Toolbar ─────────────────────────────────────── */
  44. #toolbar {
  45. display: flex;
  46. align-items: center;
  47. gap: 10px;
  48. padding: 0 14px;
  49. height: 44px;
  50. background: var(--surface);
  51. border-bottom: 1px solid var(--border);
  52. flex-shrink: 0;
  53. }
  54. #toolbar .logo {
  55. display: flex; align-items: center; gap: 7px;
  56. font-weight: 700; font-size: 14px; color: var(--text);
  57. }
  58. #toolbar .logo svg { opacity: .85; }
  59. #toolbar .db-path {
  60. flex: 1;
  61. font-size: 12px; color: var(--muted);
  62. white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  63. padding: 0 8px;
  64. }
  65. #toolbar button {
  66. padding: 5px 12px;
  67. border-radius: 6px; border: 1px solid var(--border);
  68. background: var(--surface); color: var(--text);
  69. cursor: pointer; font-size: 12px; font-weight: 500;
  70. transition: background .15s;
  71. }
  72. #toolbar button:hover { background: var(--bg); }
  73. #toolbar button.primary {
  74. background: var(--accent); color: #fff; border-color: var(--accent);
  75. }
  76. #toolbar button.primary:hover { background: var(--accent-h); }
  77. /* ── Layout ──────────────────────────────────────── */
  78. #layout {
  79. display: flex;
  80. flex: 1;
  81. overflow: hidden;
  82. }
  83. /* ── Sidebar ─────────────────────────────────────── */
  84. #sidebar {
  85. width: 200px;
  86. flex-shrink: 0;
  87. background: var(--sidebar);
  88. display: flex;
  89. flex-direction: column;
  90. overflow: hidden;
  91. }
  92. #sidebar-header {
  93. padding: 12px 14px 8px;
  94. font-size: 10px;
  95. font-weight: 700;
  96. letter-spacing: .08em;
  97. text-transform: uppercase;
  98. color: var(--side-muted);
  99. border-bottom: 1px solid rgba(255,255,255,.06);
  100. }
  101. #table-list {
  102. flex: 1;
  103. overflow-y: auto;
  104. padding: 6px 0;
  105. }
  106. #table-list::-webkit-scrollbar { width: 4px; }
  107. #table-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.12); border-radius: 2px; }
  108. .table-item {
  109. display: flex; align-items: center; gap: 7px;
  110. padding: 7px 14px;
  111. color: var(--side-txt);
  112. cursor: pointer;
  113. border-radius: 0;
  114. transition: background .12s;
  115. white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  116. }
  117. .table-item:hover { background: rgba(255,255,255,.07); }
  118. .table-item.active {
  119. background: var(--accent);
  120. color: #fff;
  121. }
  122. .table-item svg { flex-shrink: 0; opacity: .7; }
  123. .table-item.active svg { opacity: 1; }
  124. .table-item .tbl-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
  125. .tbl-drop-btn {
  126. display: none;
  127. flex-shrink: 0;
  128. background: none; border: none; padding: 2px 4px;
  129. color: rgba(255,255,255,.45); cursor: pointer; border-radius: 3px;
  130. line-height: 1; font-size: 13px;
  131. transition: color .12s, background .12s;
  132. }
  133. .table-item:hover .tbl-drop-btn { display: flex; align-items: center; }
  134. .tbl-drop-btn:hover { color: #ff7070; background: rgba(255,100,100,.15); }
  135. .table-item.active .tbl-drop-btn { color: rgba(255,255,255,.5); }
  136. .table-item.active .tbl-drop-btn:hover { color: #fff; background: rgba(255,255,255,.2); }
  137. #sidebar-empty {
  138. padding: 20px 14px;
  139. color: var(--side-muted);
  140. font-size: 12px;
  141. line-height: 1.5;
  142. }
  143. /* ── Main content ────────────────────────────────── */
  144. #content {
  145. flex: 1;
  146. display: flex;
  147. flex-direction: column;
  148. overflow: hidden;
  149. background: var(--bg);
  150. }
  151. /* ── Tab bar ─────────────────────────────────────── */
  152. #tab-bar {
  153. display: flex;
  154. align-items: center;
  155. padding: 0 16px;
  156. gap: 2px;
  157. background: var(--surface);
  158. border-bottom: 1px solid var(--border);
  159. flex-shrink: 0;
  160. height: 40px;
  161. }
  162. .tab-btn {
  163. padding: 6px 14px;
  164. border: none; background: none;
  165. cursor: pointer; font-size: 12px; font-weight: 500;
  166. color: var(--muted);
  167. border-bottom: 2px solid transparent;
  168. margin-bottom: -1px;
  169. transition: color .12s, border-color .12s;
  170. }
  171. .tab-btn:hover { color: var(--text); }
  172. .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
  173. #tab-title {
  174. margin-left: auto;
  175. font-size: 12px; color: var(--muted);
  176. }
  177. /* ── Tab panels ──────────────────────────────────── */
  178. .tab-panel {
  179. display: none;
  180. flex: 1;
  181. overflow: hidden;
  182. flex-direction: column;
  183. }
  184. .tab-panel.visible { display: flex; }
  185. /* ── Welcome screen ──────────────────────────────── */
  186. #welcome {
  187. flex: 1;
  188. display: flex;
  189. flex-direction: column;
  190. align-items: center;
  191. justify-content: center;
  192. color: var(--muted);
  193. gap: 12px;
  194. }
  195. #welcome svg { opacity: .25; }
  196. #welcome h2 { margin: 0; font-size: 18px; font-weight: 600; color: var(--muted); }
  197. #welcome p { margin: 0; font-size: 13px; }
  198. /* ── Browse tab ──────────────────────────────────── */
  199. #browse-toolbar {
  200. display: flex; align-items: center; gap: 8px;
  201. padding: 10px 16px;
  202. flex-shrink: 0;
  203. }
  204. #browse-toolbar .row-count {
  205. font-size: 12px; color: var(--muted);
  206. margin-right: auto;
  207. }
  208. #browse-toolbar button {
  209. padding: 5px 11px;
  210. border-radius: 6px; border: 1px solid var(--border);
  211. background: var(--surface); color: var(--text);
  212. cursor: pointer; font-size: 12px; font-weight: 500;
  213. transition: background .12s;
  214. }
  215. #browse-toolbar button:hover { background: var(--bg); }
  216. #browse-toolbar button.primary {
  217. background: var(--accent); color: #fff; border-color: var(--accent);
  218. }
  219. #browse-toolbar button.primary:hover { background: var(--accent-h); }
  220. #table-wrapper {
  221. flex: 1;
  222. overflow: auto;
  223. padding: 0 16px;
  224. }
  225. #table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
  226. #table-wrapper::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
  227. table.data-table {
  228. width: 100%;
  229. border-collapse: collapse;
  230. font-size: 12px;
  231. background: var(--surface);
  232. border-radius: 8px;
  233. overflow: hidden;
  234. box-shadow: 0 1px 3px rgba(0,0,0,.06);
  235. }
  236. table.data-table thead th {
  237. background: #f8f9fc;
  238. padding: 9px 12px;
  239. text-align: left;
  240. font-weight: 600;
  241. color: var(--muted);
  242. font-size: 11px;
  243. letter-spacing: .04em;
  244. text-transform: uppercase;
  245. border-bottom: 1px solid var(--border);
  246. white-space: nowrap;
  247. user-select: none;
  248. }
  249. table.data-table thead th.sortable {
  250. cursor: pointer;
  251. }
  252. table.data-table thead th.sortable:hover { color: var(--text); }
  253. table.data-table thead th.sort-active { color: var(--accent); }
  254. .sort-icon { margin-left: 4px; font-style: normal; opacity: .6; }
  255. th.sort-active .sort-icon { opacity: 1; }
  256. table.data-table thead th.pk { background: var(--pk-col); }
  257. #browse-toolbar select.limit-select {
  258. padding: 5px 8px;
  259. border-radius: 6px; border: 1px solid var(--border);
  260. background: var(--surface); color: var(--text);
  261. cursor: pointer; font-size: 12px;
  262. }
  263. table.data-table thead th.actions { width: 90px; text-align: center; }
  264. table.data-table tbody tr { border-bottom: 1px solid var(--border); }
  265. table.data-table tbody tr:last-child { border-bottom: none; }
  266. table.data-table tbody tr:hover { background: var(--row-hover); }
  267. table.data-table td {
  268. padding: 8px 12px;
  269. max-width: 260px;
  270. overflow: hidden;
  271. white-space: nowrap;
  272. text-overflow: ellipsis;
  273. vertical-align: middle;
  274. }
  275. table.data-table td.pk { background: var(--pk-col); font-weight: 600; }
  276. table.data-table td .null { color: var(--null-col); font-style: italic; }
  277. table.data-table td.actions {
  278. text-align: center; white-space: nowrap; padding: 4px 8px;
  279. }
  280. .btn-edit, .btn-del {
  281. padding: 3px 8px;
  282. border-radius: 4px; border: 1px solid var(--border);
  283. cursor: pointer; font-size: 11px; font-weight: 500;
  284. background: var(--surface);
  285. transition: background .12s;
  286. }
  287. .btn-edit:hover { background: var(--bg); color: var(--accent); border-color: var(--accent); }
  288. .btn-del { color: var(--danger); }
  289. .btn-del:hover { background: #fff0f0; border-color: var(--danger); }
  290. #pagination {
  291. display: flex; align-items: center; justify-content: center;
  292. gap: 6px; padding: 12px 16px;
  293. flex-shrink: 0;
  294. }
  295. .page-btn {
  296. padding: 4px 10px;
  297. border-radius: 5px; border: 1px solid var(--border);
  298. background: var(--surface); color: var(--text);
  299. cursor: pointer; font-size: 12px;
  300. transition: background .12s;
  301. }
  302. .page-btn:hover:not(:disabled) { background: var(--bg); }
  303. .page-btn:disabled { opacity: .4; cursor: default; }
  304. .page-btn.current { background: var(--accent); color: #fff; border-color: var(--accent); }
  305. #page-info { font-size: 12px; color: var(--muted); padding: 0 4px; }
  306. /* ── Structure tab ───────────────────────────────── */
  307. #structure-wrap {
  308. flex: 1; overflow: auto; padding: 16px;
  309. }
  310. table.schema-table {
  311. width: 100%; border-collapse: collapse;
  312. font-size: 12px;
  313. background: var(--surface);
  314. border-radius: 8px; overflow: hidden;
  315. box-shadow: 0 1px 3px rgba(0,0,0,.06);
  316. }
  317. table.schema-table thead th {
  318. background: #f8f9fc; padding: 9px 14px;
  319. text-align: left; font-weight: 600; color: var(--muted);
  320. font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  321. border-bottom: 1px solid var(--border);
  322. }
  323. table.schema-table tbody tr { border-bottom: 1px solid var(--border); }
  324. table.schema-table tbody tr:last-child { border-bottom: none; }
  325. table.schema-table td { padding: 9px 14px; vertical-align: middle; }
  326. .badge {
  327. display: inline-block; padding: 2px 6px;
  328. border-radius: 3px; font-size: 10px; font-weight: 600;
  329. letter-spacing: .04em; text-transform: uppercase;
  330. }
  331. .badge-pk { background: #e8eeff; color: var(--accent); }
  332. .badge-nn { background: #fff3e0; color: #e67e22; }
  333. .badge-type { background: #f0f4ff; color: #5566aa; }
  334. /* ── SQL Editor tab ──────────────────────────────── */
  335. #sql-wrap {
  336. flex: 1; display: flex; flex-direction: column;
  337. padding: 14px 16px; gap: 10px; overflow: hidden;
  338. }
  339. #sql-editor {
  340. flex: 0 0 140px;
  341. resize: vertical;
  342. border: 1px solid var(--border);
  343. border-radius: 7px;
  344. padding: 10px 12px;
  345. font-family: "Fira Code", "Cascadia Code", "Consolas", monospace;
  346. font-size: 12.5px;
  347. line-height: 1.6;
  348. background: var(--surface);
  349. color: var(--text);
  350. outline: none;
  351. transition: border-color .15s;
  352. min-height: 80px;
  353. max-height: 300px;
  354. }
  355. #sql-editor:focus { border-color: var(--accent); }
  356. #sql-exec-bar {
  357. display: flex; align-items: center; gap: 8px; flex-shrink: 0;
  358. }
  359. #sql-exec-bar button {
  360. padding: 6px 16px;
  361. border-radius: 6px; border: none;
  362. background: var(--accent); color: #fff;
  363. cursor: pointer; font-size: 12px; font-weight: 600;
  364. transition: background .12s;
  365. }
  366. #sql-exec-bar button:hover { background: var(--accent-h); }
  367. #sql-exec-bar .hint { font-size: 11px; color: var(--muted); }
  368. #sql-results {
  369. flex: 1; overflow: auto; border-radius: 7px;
  370. border: 1px solid var(--border);
  371. background: var(--surface);
  372. }
  373. #sql-results::-webkit-scrollbar { width: 6px; height: 6px; }
  374. #sql-results::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
  375. .sql-error {
  376. padding: 12px 14px;
  377. color: var(--danger);
  378. font-family: monospace; font-size: 12px; line-height: 1.5;
  379. background: #fff5f5; border-radius: 7px;
  380. }
  381. .sql-ok-msg {
  382. padding: 12px 14px;
  383. color: var(--success); font-size: 12px;
  384. }
  385. /* ── Modal ───────────────────────────────────────── */
  386. #modal-overlay {
  387. display: none;
  388. position: fixed; inset: 0;
  389. background: rgba(0,0,0,.45);
  390. z-index: 900;
  391. align-items: center; justify-content: center;
  392. }
  393. #modal-overlay.open { display: flex; }
  394. .modal-box {
  395. background: var(--surface);
  396. border-radius: 10px;
  397. padding: 22px 24px;
  398. width: 400px; max-width: 95vw;
  399. max-height: 85vh; overflow-y: auto;
  400. box-shadow: 0 12px 40px rgba(0,0,0,.2);
  401. }
  402. .modal-box h3 { margin: 0 0 16px; font-size: 15px; font-weight: 700; }
  403. .modal-field { margin-bottom: 12px; }
  404. .modal-field label {
  405. display: block; font-size: 11px; font-weight: 600;
  406. color: var(--muted); margin-bottom: 4px;
  407. text-transform: uppercase; letter-spacing: .04em;
  408. }
  409. .modal-field input, .modal-field textarea {
  410. width: 100%; padding: 7px 10px;
  411. border: 1px solid var(--border); border-radius: 6px;
  412. font-size: 13px; background: var(--bg);
  413. color: var(--text); outline: none;
  414. transition: border-color .15s;
  415. }
  416. .modal-field input:focus, .modal-field textarea:focus {
  417. border-color: var(--accent); background: var(--surface);
  418. }
  419. .modal-field .pk-note { font-size: 11px; color: var(--muted); margin-top: 3px; }
  420. .modal-actions {
  421. display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;
  422. }
  423. .modal-actions button {
  424. padding: 7px 16px; border-radius: 6px; border: 1px solid var(--border);
  425. cursor: pointer; font-size: 12px; font-weight: 600;
  426. transition: background .12s;
  427. }
  428. .modal-actions .btn-cancel { background: var(--surface); color: var(--text); }
  429. .modal-actions .btn-cancel:hover { background: var(--bg); }
  430. .modal-actions .btn-save {
  431. background: var(--accent); color: #fff; border-color: var(--accent);
  432. }
  433. .modal-actions .btn-save:hover { background: var(--accent-h); }
  434. .modal-actions .btn-danger {
  435. background: var(--danger); color: #fff; border-color: var(--danger);
  436. }
  437. /* ── Loading spinner ─────────────────────────────── */
  438. .spinner {
  439. display: inline-block; width: 16px; height: 16px;
  440. border: 2px solid var(--border); border-top-color: var(--accent);
  441. border-radius: 50%; animation: spin .6s linear infinite;
  442. }
  443. @keyframes spin { to { transform: rotate(360deg); } }
  444. #loading-overlay {
  445. display: none; position: absolute; inset: 0;
  446. background: rgba(244,246,251,.6);
  447. align-items: center; justify-content: center;
  448. z-index: 50;
  449. }
  450. #loading-overlay.show { display: flex; }
  451. #content { position: relative; }
  452. /* ── Notification toast ──────────────────────────── */
  453. #toast {
  454. position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%);
  455. background: #1e2233; color: #fff;
  456. padding: 9px 18px; border-radius: 7px;
  457. font-size: 12px; font-weight: 500;
  458. opacity: 0; pointer-events: none;
  459. transition: opacity .2s;
  460. z-index: 999;
  461. }
  462. #toast.show { opacity: 1; }
  463. #toast.error { background: var(--danger); }
  464. /* ── SQLite-unavailable banner ───────────────────── */
  465. #not-supported-banner {
  466. display: none;
  467. margin: 0 0 12px 0;
  468. padding: 12px 16px;
  469. background: #fff3cd; color: #7d5800;
  470. border: 1px solid #ffc107;
  471. border-radius: 8px;
  472. font-size: 13px;
  473. line-height: 1.5;
  474. text-align: center;
  475. }
  476. #not-supported-banner.show { display: block; }
  477. #not-supported-banner strong { display: block; margin-bottom: 4px; font-size: 14px; }
  478. </style>
  479. </head>
  480. <body>
  481. <!-- Toolbar -->
  482. <div id="toolbar">
  483. <div class="logo">
  484. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4a6cf7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  485. <ellipse cx="12" cy="5" rx="9" ry="3"/>
  486. <path d="M21 12c0 1.657-4.03 3-9 3s-9-1.343-9-3"/>
  487. <path d="M3 5v14c0 1.657 4.03 3 9 3s9-1.343 9-3V5"/>
  488. </svg>
  489. SQLite Admin
  490. </div>
  491. <span id="db-path-label" class="db-path">No database selected</span>
  492. <button id="btn-open-db" class="primary">Open Database&hellip;</button>
  493. </div>
  494. <!-- Layout -->
  495. <div id="layout">
  496. <!-- Sidebar -->
  497. <div id="sidebar">
  498. <div id="sidebar-header">Tables</div>
  499. <div id="table-list">
  500. <div id="sidebar-empty">Open a <code>.sqlite</code> file to begin.</div>
  501. </div>
  502. </div>
  503. <!-- Content -->
  504. <div id="content">
  505. <div id="loading-overlay"><div class="spinner"></div></div>
  506. <!-- Welcome screen (shown before any DB is open) -->
  507. <div id="welcome">
  508. <div id="not-supported-banner">
  509. <strong>SQLite not available on this platform</strong>
  510. The SQLite library could not be loaded on this server. This feature requires a platform supported by the SQLite backend (linux/amd64, linux/arm64, darwin, windows/amd64, windows/arm64, and others). OpenWRT (linux/mipsle) and other embedded targets are not supported.
  511. </div>
  512. <svg width="72" height="72" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
  513. <ellipse cx="12" cy="5" rx="9" ry="3"/>
  514. <path d="M21 12c0 1.657-4.03 3-9 3s-9-1.343-9-3"/>
  515. <path d="M3 5v14c0 1.657 4.03 3 9 3s9-1.343 9-3V5"/>
  516. </svg>
  517. <h2>SQLite Admin</h2>
  518. <p>Click <strong>Open Database&hellip;</strong> to select a <code>.sqlite</code> file.</p>
  519. </div>
  520. <!-- Main area (hidden until DB is open) -->
  521. <div id="main-area" style="display:none; flex-direction:column; flex:1; overflow:hidden;">
  522. <!-- Tab bar -->
  523. <div id="tab-bar">
  524. <button class="tab-btn active" data-tab="browse">Browse</button>
  525. <button class="tab-btn" data-tab="structure">Structure</button>
  526. <button class="tab-btn" data-tab="sql">SQL Editor</button>
  527. <span id="tab-title"></span>
  528. </div>
  529. <!-- Browse panel -->
  530. <div id="tab-browse" class="tab-panel visible">
  531. <div id="browse-toolbar">
  532. <span id="row-count" class="row-count"></span>
  533. <select id="limit-select" class="limit-select" title="Rows per page">
  534. <option value="25">25 rows</option>
  535. <option value="50" selected>50 rows</option>
  536. <option value="100">100 rows</option>
  537. <option value="200">200 rows</option>
  538. </select>
  539. <button id="btn-refresh">&#8635; Refresh</button>
  540. <button id="btn-add-row" class="primary">+ Add Row</button>
  541. </div>
  542. <div id="table-wrapper">
  543. <div id="browse-placeholder" style="padding:40px;text-align:center;color:var(--muted);">
  544. Select a table from the sidebar.
  545. </div>
  546. <table id="data-table" class="data-table" style="display:none;">
  547. <thead id="data-thead"></thead>
  548. <tbody id="data-tbody"></tbody>
  549. </table>
  550. </div>
  551. <div id="pagination"></div>
  552. </div>
  553. <!-- Structure panel -->
  554. <div id="tab-structure" class="tab-panel">
  555. <div id="structure-wrap">
  556. <div id="structure-placeholder" style="padding:40px;text-align:center;color:var(--muted);">
  557. Select a table from the sidebar.
  558. </div>
  559. <table id="schema-table" class="schema-table" style="display:none;">
  560. <thead>
  561. <tr>
  562. <th>#</th>
  563. <th>Column</th>
  564. <th>Type</th>
  565. <th>Flags</th>
  566. <th>Default</th>
  567. </tr>
  568. </thead>
  569. <tbody id="schema-tbody"></tbody>
  570. </table>
  571. </div>
  572. </div>
  573. <!-- SQL Editor panel -->
  574. <div id="tab-sql" class="tab-panel">
  575. <div id="sql-wrap">
  576. <textarea id="sql-editor" placeholder="Enter SQL statements…&#10;&#10;Examples:&#10; SELECT * FROM tablename LIMIT 10;&#10; CREATE TABLE test (id INTEGER PRIMARY KEY, val TEXT);&#10; INSERT INTO test VALUES (1, 'hello');"></textarea>
  577. <div id="sql-exec-bar">
  578. <button id="btn-exec-sql">&#9654; Execute</button>
  579. <span class="hint">Ctrl+Enter to run &bull; SELECT returns rows &bull; Other statements return row count</span>
  580. </div>
  581. <div id="sql-results"></div>
  582. </div>
  583. </div>
  584. </div>
  585. </div>
  586. </div>
  587. <!-- Edit / Insert Modal -->
  588. <div id="modal-overlay">
  589. <div class="modal-box">
  590. <h3 id="modal-title">Edit Row</h3>
  591. <div id="modal-fields"></div>
  592. <div class="modal-actions">
  593. <button class="btn-cancel" id="modal-cancel">Cancel</button>
  594. <button class="btn-save" id="modal-save">Save</button>
  595. </div>
  596. </div>
  597. </div>
  598. <!-- Confirm Dialog (shared for row delete and table drop) -->
  599. <div id="confirm-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:900;align-items:center;justify-content:center;">
  600. <div class="modal-box" style="width:360px;">
  601. <h3 id="confirm-title">Delete Row</h3>
  602. <p id="confirm-body" style="font-size:13px;color:var(--muted);margin:0 0 16px;">Are you sure you want to delete this row? This action cannot be undone.</p>
  603. <div class="modal-actions">
  604. <button class="btn-cancel" id="del-cancel">Cancel</button>
  605. <button class="btn-danger" id="del-confirm">Delete</button>
  606. </div>
  607. </div>
  608. </div>
  609. <!-- Toast -->
  610. <div id="toast"></div>
  611. <script>
  612. // ─── State ────────────────────────────────────────────────────────────────────
  613. var App = {
  614. db: null, // current db virtual path
  615. table: null, // current table name
  616. schema: [], // current table schema (array of col info)
  617. page: 1,
  618. limit: 50,
  619. total: 0,
  620. sortCol: null, // active sort column name or null
  621. sortDir: 'ASC', // 'ASC' or 'DESC'
  622. pendingDelete: null, // {pkCol, pkVal}
  623. pendingDrop: null, // table name to drop
  624. pendingEditRow: null // row data for modal
  625. };
  626. // ─── Utilities ────────────────────────────────────────────────────────────────
  627. function toast(msg, isError) {
  628. var el = document.getElementById('toast');
  629. el.textContent = msg;
  630. el.className = 'show' + (isError ? ' error' : '');
  631. clearTimeout(el._t);
  632. el._t = setTimeout(function() { el.className = ''; }, 2800);
  633. }
  634. function loading(on) {
  635. document.getElementById('loading-overlay').className = on ? 'show' : '';
  636. }
  637. function api(params, cb) {
  638. ao_module_agirun("SQLite Admin/backend/api.agi", params, function(raw) {
  639. try {
  640. var data = (typeof raw === 'string') ? JSON.parse(raw) : raw;
  641. cb(null, data);
  642. } catch(e) {
  643. cb(e, null);
  644. }
  645. }, function(xhr) {
  646. cb(new Error("Request failed: " + xhr.status), null);
  647. });
  648. }
  649. // Probe SQLite availability on load; show banner and disable open button if unsupported
  650. (function checkSQLiteAvailability() {
  651. api({action: 'available'}, function(err, data) {
  652. var unavailable = (err) ||
  653. (data && data.error &&
  654. data.error.indexOf('not available') !== -1);
  655. if (unavailable) {
  656. document.getElementById('not-supported-banner').classList.add('show');
  657. document.getElementById('btn-open-db').disabled = true;
  658. document.getElementById('btn-open-db').title =
  659. 'SQLite is not supported on this platform';
  660. }
  661. });
  662. }());
  663. function escapeHtml(s) {
  664. if (s === null || s === undefined) return '';
  665. return String(s)
  666. .replace(/&/g,'&amp;').replace(/</g,'&lt;')
  667. .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  668. }
  669. function pkCol() {
  670. for (var i = 0; i < App.schema.length; i++) {
  671. if (App.schema[i].pk > 0) return App.schema[i].name;
  672. }
  673. // fallback: use rowid or first column
  674. return App.schema.length > 0 ? App.schema[0].name : null;
  675. }
  676. // ─── Database open ────────────────────────────────────────────────────────────
  677. // Named global so virtual-desktop mode can locate the callback by name
  678. window.onSQLiteFileSelected = function(files) {
  679. if (!files || files.length === 0) return;
  680. var vpath = files[0].filepath || files[0];
  681. openDatabase(vpath);
  682. };
  683. document.getElementById('btn-open-db').addEventListener('click', function() {
  684. ao_module_openFileSelector(
  685. window.onSQLiteFileSelected,
  686. "user:/", "file", false,
  687. {filter: ["sqlite", "sqlite3", "db"], fnameOverride: "onSQLiteFileSelected"}
  688. );
  689. });
  690. function openDatabase(vpath) {
  691. loading(true);
  692. api({action: 'tables', db: vpath}, function(err, data) {
  693. loading(false);
  694. if (err || data.error) {
  695. toast(err ? err.message : data.error, true);
  696. return;
  697. }
  698. App.db = vpath;
  699. App.table = null;
  700. App.page = 1;
  701. document.getElementById('db-path-label').textContent = vpath;
  702. document.getElementById('welcome').style.display = 'none';
  703. document.getElementById('main-area').style.display = 'flex';
  704. renderTableList(data.tables);
  705. // Auto-select first table
  706. if (data.tables && data.tables.length > 0) {
  707. selectTable(data.tables[0]);
  708. }
  709. });
  710. }
  711. // ─── Table list ───────────────────────────────────────────────────────────────
  712. function renderTableList(tables) {
  713. var list = document.getElementById('table-list');
  714. list.innerHTML = '';
  715. if (!tables || tables.length === 0) {
  716. list.innerHTML = '<div id="sidebar-empty">Database has no tables.</div>';
  717. return;
  718. }
  719. tables.forEach(function(name) {
  720. var isInternal = name.indexOf('sqlite_') === 0;
  721. var item = document.createElement('div');
  722. item.className = 'table-item';
  723. item.dataset.table = name;
  724. var dropBtnHtml = isInternal ? '' :
  725. '<button class="tbl-drop-btn" title="Drop table">' +
  726. '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
  727. '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/>' +
  728. '<path d="M10 11v6"/><path d="M14 11v6"/>' +
  729. '<path d="M9 6V4h6v2"/>' +
  730. '</svg>' +
  731. '</button>';
  732. item.innerHTML =
  733. '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
  734. '<rect x="3" y="3" width="18" height="18" rx="2"/>' +
  735. '<line x1="3" y1="9" x2="21" y2="9"/>' +
  736. '<line x1="3" y1="15" x2="21" y2="15"/>' +
  737. '<line x1="9" y1="3" x2="9" y2="21"/>' +
  738. '</svg>' +
  739. '<span class="tbl-name">' + escapeHtml(name) + '</span>' +
  740. dropBtnHtml;
  741. item.addEventListener('click', function(e) {
  742. if (e.target.closest('.tbl-drop-btn')) return;
  743. selectTable(name);
  744. });
  745. if (!isInternal) {
  746. item.querySelector('.tbl-drop-btn').addEventListener('click', function(e) {
  747. e.stopPropagation();
  748. openDropTableConfirm(name);
  749. });
  750. }
  751. list.appendChild(item);
  752. });
  753. }
  754. function selectTable(name) {
  755. App.table = name;
  756. App.page = 1;
  757. App.sortCol = null;
  758. App.sortDir = 'ASC';
  759. // Highlight sidebar item
  760. document.querySelectorAll('.table-item').forEach(function(el) {
  761. el.classList.toggle('active', el.dataset.table === name);
  762. });
  763. document.getElementById('tab-title').textContent = name;
  764. var activeTab = document.querySelector('.tab-btn.active');
  765. var tab = activeTab ? activeTab.dataset.tab : 'browse';
  766. if (tab === 'browse') loadBrowse();
  767. else if (tab === 'structure') loadStructure();
  768. }
  769. // ─── Tab switching ────────────────────────────────────────────────────────────
  770. document.querySelectorAll('.tab-btn').forEach(function(btn) {
  771. btn.addEventListener('click', function() {
  772. document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
  773. btn.classList.add('active');
  774. var tab = btn.dataset.tab;
  775. document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('visible'); });
  776. document.getElementById('tab-' + tab).classList.add('visible');
  777. if (!App.table) return;
  778. if (tab === 'browse') loadBrowse();
  779. else if (tab === 'structure') loadStructure();
  780. });
  781. });
  782. // ─── Browse ───────────────────────────────────────────────────────────────────
  783. document.getElementById('btn-refresh').addEventListener('click', function() {
  784. if (App.table) loadBrowse();
  785. });
  786. document.getElementById('limit-select').addEventListener('change', function() {
  787. App.limit = parseInt(this.value) || 50;
  788. App.page = 1;
  789. if (App.table) loadBrowse();
  790. });
  791. function loadBrowse() {
  792. if (!App.db || !App.table) return;
  793. loading(true);
  794. var params = {
  795. action: 'query', db: App.db,
  796. table: App.table, page: App.page, limit: App.limit
  797. };
  798. if (App.sortCol) {
  799. params.sort_col = App.sortCol;
  800. params.sort_dir = App.sortDir;
  801. }
  802. api(params, function(err, data) {
  803. loading(false);
  804. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  805. App.total = data.total;
  806. App.schema = data.schema || [];
  807. renderTable(data.rows, data.schema);
  808. renderPagination(data.page, data.total, data.limit);
  809. document.getElementById('row-count').textContent =
  810. data.total + ' row' + (data.total === 1 ? '' : 's') + ' total';
  811. });
  812. }
  813. function renderTable(rows, schema) {
  814. var placeholder = document.getElementById('browse-placeholder');
  815. var table = document.getElementById('data-table');
  816. var thead = document.getElementById('data-thead');
  817. var tbody = document.getElementById('data-tbody');
  818. if (!rows || rows.length === 0 && (!schema || schema.length === 0)) {
  819. placeholder.textContent = 'No data in this table.';
  820. placeholder.style.display = '';
  821. table.style.display = 'none';
  822. return;
  823. }
  824. placeholder.style.display = 'none';
  825. table.style.display = '';
  826. // Column order from schema
  827. var cols = schema.map(function(c) { return c.name; });
  828. var pkName = pkColFromSchema(schema);
  829. // Header
  830. thead.innerHTML = '';
  831. var hr = document.createElement('tr');
  832. cols.forEach(function(c) {
  833. var th = document.createElement('th');
  834. var classes = ['sortable'];
  835. if (c === pkName) classes.push('pk');
  836. if (c === App.sortCol) classes.push('sort-active');
  837. th.className = classes.join(' ');
  838. var label = document.createElement('span');
  839. label.textContent = c;
  840. th.appendChild(label);
  841. var icon = document.createElement('i');
  842. icon.className = 'sort-icon';
  843. if (c === App.sortCol) {
  844. icon.textContent = App.sortDir === 'DESC' ? '▼' : '▲';
  845. } else {
  846. icon.textContent = '⇅';
  847. }
  848. th.appendChild(icon);
  849. (function(col) {
  850. th.addEventListener('click', function() {
  851. if (App.sortCol === col) {
  852. App.sortDir = App.sortDir === 'ASC' ? 'DESC' : 'ASC';
  853. } else {
  854. App.sortCol = col;
  855. App.sortDir = 'ASC';
  856. }
  857. App.page = 1;
  858. loadBrowse();
  859. });
  860. })(c);
  861. hr.appendChild(th);
  862. });
  863. var thAct = document.createElement('th');
  864. thAct.className = 'actions';
  865. thAct.textContent = 'Actions';
  866. hr.appendChild(thAct);
  867. thead.appendChild(hr);
  868. // Rows
  869. tbody.innerHTML = '';
  870. rows.forEach(function(row) {
  871. var tr = document.createElement('tr');
  872. cols.forEach(function(c) {
  873. var td = document.createElement('td');
  874. var val = row[c];
  875. if (val === null || val === undefined) {
  876. td.innerHTML = '<span class="null">NULL</span>';
  877. } else {
  878. td.textContent = String(val);
  879. td.title = String(val);
  880. }
  881. if (c === pkName) td.className = 'pk';
  882. tr.appendChild(td);
  883. });
  884. // Actions
  885. var tdAct = document.createElement('td');
  886. tdAct.className = 'actions';
  887. var pkVal = pkName ? row[pkName] : null;
  888. var editBtn = document.createElement('button');
  889. editBtn.className = 'btn-edit';
  890. editBtn.textContent = 'Edit';
  891. editBtn.addEventListener('click', (function(r) {
  892. return function() { openEditModal(r, schema, pkName); };
  893. })(row));
  894. var delBtn = document.createElement('button');
  895. delBtn.className = 'btn-del';
  896. delBtn.textContent = 'Del';
  897. delBtn.addEventListener('click', (function(pv) {
  898. return function() { openDeleteConfirm(pkName, pv); };
  899. })(pkVal));
  900. tdAct.appendChild(editBtn);
  901. tdAct.appendChild(document.createTextNode(' '));
  902. tdAct.appendChild(delBtn);
  903. tr.appendChild(tdAct);
  904. tbody.appendChild(tr);
  905. });
  906. }
  907. function pkColFromSchema(schema) {
  908. if (!schema) return null;
  909. for (var i = 0; i < schema.length; i++) {
  910. if (schema[i].pk > 0) return schema[i].name;
  911. }
  912. return schema.length > 0 ? schema[0].name : null;
  913. }
  914. // ─── Pagination ───────────────────────────────────────────────────────────────
  915. function renderPagination(page, total, limit) {
  916. var pages = Math.max(1, Math.ceil(total / limit));
  917. var el = document.getElementById('pagination');
  918. el.innerHTML = '';
  919. if (pages <= 1) return;
  920. var prev = document.createElement('button');
  921. prev.className = 'page-btn';
  922. prev.textContent = '‹ Prev';
  923. prev.disabled = (page <= 1);
  924. prev.addEventListener('click', function() { App.page = page - 1; loadBrowse(); });
  925. el.appendChild(prev);
  926. var start = Math.max(1, page - 2);
  927. var end = Math.min(pages, start + 4);
  928. start = Math.max(1, end - 4);
  929. for (var p = start; p <= end; p++) {
  930. var pb = document.createElement('button');
  931. pb.className = 'page-btn' + (p === page ? ' current' : '');
  932. pb.textContent = p;
  933. (function(pp) {
  934. pb.addEventListener('click', function() { App.page = pp; loadBrowse(); });
  935. })(p);
  936. el.appendChild(pb);
  937. }
  938. var info = document.createElement('span');
  939. info.id = 'page-info';
  940. info.textContent = 'of ' + pages;
  941. el.appendChild(info);
  942. var next = document.createElement('button');
  943. next.className = 'page-btn';
  944. next.textContent = 'Next ›';
  945. next.disabled = (page >= pages);
  946. next.addEventListener('click', function() { App.page = page + 1; loadBrowse(); });
  947. el.appendChild(next);
  948. }
  949. // ─── Structure ────────────────────────────────────────────────────────────────
  950. function loadStructure() {
  951. if (!App.db || !App.table) return;
  952. loading(true);
  953. api({action: 'schema', db: App.db, table: App.table}, function(err, data) {
  954. loading(false);
  955. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  956. renderStructure(data.schema);
  957. });
  958. }
  959. function renderStructure(schema) {
  960. var placeholder = document.getElementById('structure-placeholder');
  961. var table = document.getElementById('schema-table');
  962. var tbody = document.getElementById('schema-tbody');
  963. if (!schema || schema.length === 0) {
  964. placeholder.textContent = 'No schema information.';
  965. placeholder.style.display = '';
  966. table.style.display = 'none';
  967. return;
  968. }
  969. placeholder.style.display = 'none';
  970. table.style.display = '';
  971. tbody.innerHTML = '';
  972. schema.forEach(function(col) {
  973. var tr = document.createElement('tr');
  974. var badges = '';
  975. if (col.pk > 0) badges += '<span class="badge badge-pk">PK</span> ';
  976. if (col.notnull > 0) badges += '<span class="badge badge-nn">NOT NULL</span> ';
  977. var dflt = (col.dflt_value !== null && col.dflt_value !== undefined)
  978. ? escapeHtml(String(col.dflt_value))
  979. : '<span style="color:var(--null-col);font-style:italic">NULL</span>';
  980. tr.innerHTML =
  981. '<td style="color:var(--muted);font-size:11px;">' + col.cid + '</td>' +
  982. '<td><strong>' + escapeHtml(col.name) + '</strong></td>' +
  983. '<td><span class="badge badge-type">' + escapeHtml(col.type || 'ANY') + '</span></td>' +
  984. '<td>' + badges + '</td>' +
  985. '<td style="font-size:12px;">' + dflt + '</td>';
  986. tbody.appendChild(tr);
  987. });
  988. }
  989. // ─── Add Row ──────────────────────────────────────────────────────────────────
  990. document.getElementById('btn-add-row').addEventListener('click', function() {
  991. if (!App.table) { toast('Select a table first.', true); return; }
  992. openInsertModal(App.schema);
  993. });
  994. function openInsertModal(schema) {
  995. App.pendingEditRow = null;
  996. document.getElementById('modal-title').textContent = 'Add Row — ' + App.table;
  997. var fields = document.getElementById('modal-fields');
  998. fields.innerHTML = '';
  999. schema.forEach(function(col) {
  1000. var div = document.createElement('div');
  1001. div.className = 'modal-field';
  1002. var label = document.createElement('label');
  1003. label.textContent = col.name;
  1004. if (col.pk > 0) {
  1005. label.textContent += ' (PK)';
  1006. }
  1007. var input = document.createElement('input');
  1008. input.type = 'text';
  1009. input.name = col.name;
  1010. input.placeholder = col.type || '';
  1011. if (col.pk > 0 && col.type && col.type.toUpperCase().indexOf('INTEGER') >= 0) {
  1012. input.placeholder = 'auto-increment';
  1013. }
  1014. div.appendChild(label);
  1015. div.appendChild(input);
  1016. fields.appendChild(div);
  1017. });
  1018. document.getElementById('modal-save').onclick = doInsert;
  1019. openModal();
  1020. }
  1021. function doInsert() {
  1022. var inputs = document.querySelectorAll('#modal-fields input');
  1023. var row = {};
  1024. inputs.forEach(function(inp) {
  1025. if (inp.value !== '') row[inp.name] = inp.value;
  1026. });
  1027. if (Object.keys(row).length === 0) { toast('Enter at least one value.', true); return; }
  1028. loading(true);
  1029. api({action: 'insert', db: App.db, table: App.table, row: JSON.stringify(row)},
  1030. function(err, data) {
  1031. loading(false);
  1032. closeModal();
  1033. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  1034. toast('Row inserted.');
  1035. loadBrowse();
  1036. });
  1037. }
  1038. // ─── Edit Row ─────────────────────────────────────────────────────────────────
  1039. function openEditModal(row, schema, pkName) {
  1040. App.pendingEditRow = {row: row, pkName: pkName};
  1041. document.getElementById('modal-title').textContent = 'Edit Row — ' + App.table;
  1042. var fields = document.getElementById('modal-fields');
  1043. fields.innerHTML = '';
  1044. schema.forEach(function(col) {
  1045. var div = document.createElement('div');
  1046. div.className = 'modal-field';
  1047. var label = document.createElement('label');
  1048. label.textContent = col.name;
  1049. if (col.pk > 0) label.textContent += ' (PK)';
  1050. var input = document.createElement('input');
  1051. input.type = 'text';
  1052. input.name = col.name;
  1053. var val = row[col.name];
  1054. input.value = (val !== null && val !== undefined) ? String(val) : '';
  1055. if (col.pk > 0) {
  1056. input.readOnly = true;
  1057. input.style.opacity = '.6';
  1058. var note = document.createElement('div');
  1059. note.className = 'pk-note';
  1060. note.textContent = 'Primary key — read-only';
  1061. div.appendChild(label);
  1062. div.appendChild(input);
  1063. div.appendChild(note);
  1064. } else {
  1065. div.appendChild(label);
  1066. div.appendChild(input);
  1067. }
  1068. fields.appendChild(div);
  1069. });
  1070. document.getElementById('modal-save').onclick = doUpdate;
  1071. openModal();
  1072. }
  1073. function doUpdate() {
  1074. if (!App.pendingEditRow) return;
  1075. var pkName = App.pendingEditRow.pkName;
  1076. var pkVal = App.pendingEditRow.row[pkName];
  1077. var inputs = document.querySelectorAll('#modal-fields input:not([readonly])');
  1078. var promises = [];
  1079. inputs.forEach(function(inp) {
  1080. promises.push({col: inp.name, val: inp.value});
  1081. });
  1082. if (promises.length === 0) { closeModal(); return; }
  1083. var done = 0;
  1084. var errors = [];
  1085. loading(true);
  1086. function finish() {
  1087. done++;
  1088. if (done === promises.length) {
  1089. loading(false);
  1090. closeModal();
  1091. if (errors.length > 0) toast(errors[0], true);
  1092. else { toast('Row updated.'); loadBrowse(); }
  1093. }
  1094. }
  1095. promises.forEach(function(p) {
  1096. api({action: 'update', db: App.db, table: App.table,
  1097. pk_col: pkName, pk_val: pkVal, col: p.col, val: p.val},
  1098. function(err, data) {
  1099. if (err || data.error) errors.push(err ? err.message : data.error);
  1100. finish();
  1101. });
  1102. });
  1103. }
  1104. // ─── Delete Row ───────────────────────────────────────────────────────────────
  1105. function openDeleteConfirm(pkCol, pkVal) {
  1106. if (!pkCol || pkVal === null || pkVal === undefined) {
  1107. toast('Cannot identify row to delete (no primary key).', true);
  1108. return;
  1109. }
  1110. App.pendingDelete = {pkCol: pkCol, pkVal: pkVal};
  1111. App.pendingDrop = null;
  1112. document.getElementById('confirm-title').textContent = 'Delete Row';
  1113. document.getElementById('confirm-body').textContent =
  1114. 'Are you sure you want to delete this row? This action cannot be undone.';
  1115. document.getElementById('del-confirm').textContent = 'Delete';
  1116. document.getElementById('confirm-overlay').style.display = 'flex';
  1117. }
  1118. function openDropTableConfirm(tableName) {
  1119. App.pendingDrop = tableName;
  1120. App.pendingDelete = null;
  1121. document.getElementById('confirm-title').textContent = 'Drop Table';
  1122. document.getElementById('confirm-body').innerHTML =
  1123. 'Are you sure you want to drop <strong>' + escapeHtml(tableName) + '</strong>? ' +
  1124. 'All data in this table will be permanently deleted and cannot be recovered.';
  1125. document.getElementById('del-confirm').textContent = 'Drop Table';
  1126. document.getElementById('confirm-overlay').style.display = 'flex';
  1127. }
  1128. function closeConfirm() {
  1129. App.pendingDelete = null;
  1130. App.pendingDrop = null;
  1131. document.getElementById('confirm-overlay').style.display = 'none';
  1132. }
  1133. document.getElementById('del-cancel').addEventListener('click', closeConfirm);
  1134. document.getElementById('del-confirm').addEventListener('click', function() {
  1135. if (App.pendingDrop) {
  1136. var tbl = App.pendingDrop;
  1137. closeConfirm();
  1138. loading(true);
  1139. api({action: 'droptable', db: App.db, table: tbl}, function(err, data) {
  1140. loading(false);
  1141. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  1142. toast('Table "' + tbl + '" dropped.');
  1143. // Clear selection if the dropped table was active
  1144. if (App.table === tbl) {
  1145. App.table = null;
  1146. document.getElementById('tab-title').textContent = '';
  1147. document.getElementById('data-table').style.display = 'none';
  1148. document.getElementById('browse-placeholder').textContent = 'Select a table from the sidebar.';
  1149. document.getElementById('browse-placeholder').style.display = '';
  1150. document.getElementById('pagination').innerHTML = '';
  1151. document.getElementById('row-count').textContent = '';
  1152. }
  1153. // Refresh sidebar table list
  1154. api({action: 'tables', db: App.db}, function(err2, d2) {
  1155. if (!err2 && !d2.error) renderTableList(d2.tables);
  1156. });
  1157. });
  1158. } else if (App.pendingDelete) {
  1159. var d = App.pendingDelete;
  1160. closeConfirm();
  1161. loading(true);
  1162. api({action: 'delete', db: App.db, table: App.table,
  1163. pk_col: d.pkCol, pk_val: d.pkVal},
  1164. function(err, data) {
  1165. loading(false);
  1166. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  1167. toast('Row deleted.');
  1168. loadBrowse();
  1169. });
  1170. }
  1171. });
  1172. // ─── SQL Editor ───────────────────────────────────────────────────────────────
  1173. document.getElementById('btn-exec-sql').addEventListener('click', executeSql);
  1174. document.getElementById('sql-editor').addEventListener('keydown', function(e) {
  1175. if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); executeSql(); }
  1176. });
  1177. function executeSql() {
  1178. var sql = document.getElementById('sql-editor').value.trim();
  1179. if (!sql || !App.db) return;
  1180. loading(true);
  1181. api({action: 'exec', db: App.db, sql: sql}, function(err, data) {
  1182. loading(false);
  1183. var out = document.getElementById('sql-results');
  1184. if (err) {
  1185. out.innerHTML = '<div class="sql-error">Error: ' + escapeHtml(err.message) + '</div>';
  1186. return;
  1187. }
  1188. if (data.error) {
  1189. out.innerHTML = '<div class="sql-error">Error: ' + escapeHtml(data.error) + '</div>';
  1190. return;
  1191. }
  1192. if (data.rows && data.rows.length > 0) {
  1193. renderSqlResultTable(data.rows, out);
  1194. } else if (data.result) {
  1195. out.innerHTML = '<div class="sql-ok-msg">✓ OK — ' +
  1196. data.result.rowsAffected + ' row(s) affected, last insert ID: ' +
  1197. data.result.lastInsertId + '</div>';
  1198. // Refresh table list and current table if schema might have changed
  1199. refreshAfterExec(sql);
  1200. } else {
  1201. out.innerHTML = '<div class="sql-ok-msg">✓ Query executed, no rows returned.</div>';
  1202. refreshAfterExec(sql);
  1203. }
  1204. });
  1205. }
  1206. function renderSqlResultTable(rows, container) {
  1207. if (!rows || rows.length === 0) {
  1208. container.innerHTML = '<div class="sql-ok-msg">No rows returned.</div>';
  1209. return;
  1210. }
  1211. var cols = Object.keys(rows[0]);
  1212. var html = '<table class="data-table" style="margin:0;border-radius:0;box-shadow:none;">';
  1213. html += '<thead><tr>' + cols.map(function(c) {
  1214. return '<th>' + escapeHtml(c) + '</th>';
  1215. }).join('') + '</tr></thead><tbody>';
  1216. rows.forEach(function(row) {
  1217. html += '<tr>' + cols.map(function(c) {
  1218. var v = row[c];
  1219. if (v === null || v === undefined) return '<td><span class="null">NULL</span></td>';
  1220. return '<td title="' + escapeHtml(String(v)) + '">' + escapeHtml(String(v)) + '</td>';
  1221. }).join('') + '</tr>';
  1222. });
  1223. html += '</tbody></table>';
  1224. container.innerHTML = html;
  1225. }
  1226. function refreshAfterExec(sql) {
  1227. var upper = sql.trim().toUpperCase();
  1228. // Reload table list if DDL was likely executed
  1229. if (upper.indexOf('CREATE') === 0 || upper.indexOf('DROP') === 0 ||
  1230. upper.indexOf('ALTER') === 0) {
  1231. api({action: 'tables', db: App.db}, function(err, data) {
  1232. if (!err && !data.error) renderTableList(data.tables);
  1233. });
  1234. }
  1235. // Refresh current browse view if a table is selected
  1236. if (App.table) loadBrowse();
  1237. }
  1238. // ─── Modal helpers ────────────────────────────────────────────────────────────
  1239. function openModal() {
  1240. document.getElementById('modal-overlay').classList.add('open');
  1241. var first = document.querySelector('#modal-fields input');
  1242. if (first) setTimeout(function() { first.focus(); }, 50);
  1243. }
  1244. function closeModal() {
  1245. document.getElementById('modal-overlay').classList.remove('open');
  1246. }
  1247. document.getElementById('modal-cancel').addEventListener('click', closeModal);
  1248. document.getElementById('modal-overlay').addEventListener('click', function(e) {
  1249. if (e.target === this) closeModal();
  1250. });
  1251. document.getElementById('confirm-overlay').addEventListener('click', function(e) {
  1252. if (e.target === this) closeConfirm();
  1253. });
  1254. </script>
  1255. </body>
  1256. </html>