index.html 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  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. // If a file was passed in via double-click, open it once availability is confirmed.
  651. (function checkSQLiteAvailability() {
  652. api({action: 'available'}, function(err, data) {
  653. var unavailable = (err) ||
  654. (data && data.error &&
  655. data.error.indexOf('not available') !== -1);
  656. if (unavailable) {
  657. document.getElementById('not-supported-banner').classList.add('show');
  658. document.getElementById('btn-open-db').disabled = true;
  659. document.getElementById('btn-open-db').title =
  660. 'SQLite is not supported on this platform';
  661. return;
  662. }
  663. var inputFiles = ao_module_loadInputFiles();
  664. if (inputFiles && inputFiles.length > 0) {
  665. var vpath = inputFiles[0].filepath || inputFiles[0];
  666. if (vpath) openDatabase(vpath);
  667. }
  668. });
  669. }());
  670. function escapeHtml(s) {
  671. if (s === null || s === undefined) return '';
  672. return String(s)
  673. .replace(/&/g,'&amp;').replace(/</g,'&lt;')
  674. .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  675. }
  676. function pkCol() {
  677. for (var i = 0; i < App.schema.length; i++) {
  678. if (App.schema[i].pk > 0) return App.schema[i].name;
  679. }
  680. // fallback: use rowid or first column
  681. return App.schema.length > 0 ? App.schema[0].name : null;
  682. }
  683. // ─── Database open ────────────────────────────────────────────────────────────
  684. // Named global so virtual-desktop mode can locate the callback by name
  685. window.onSQLiteFileSelected = function(files) {
  686. if (!files || files.length === 0) return;
  687. var vpath = files[0].filepath || files[0];
  688. openDatabase(vpath);
  689. };
  690. document.getElementById('btn-open-db').addEventListener('click', function() {
  691. ao_module_openFileSelector(
  692. window.onSQLiteFileSelected,
  693. "user:/", "file", false,
  694. {filter: ["sqlite", "sqlite3", "db"], fnameOverride: "onSQLiteFileSelected"}
  695. );
  696. });
  697. function openDatabase(vpath) {
  698. loading(true);
  699. api({action: 'tables', db: vpath}, function(err, data) {
  700. loading(false);
  701. if (err || data.error) {
  702. toast(err ? err.message : data.error, true);
  703. return;
  704. }
  705. App.db = vpath;
  706. App.table = null;
  707. App.page = 1;
  708. document.getElementById('db-path-label').textContent = vpath;
  709. document.getElementById('welcome').style.display = 'none';
  710. document.getElementById('main-area').style.display = 'flex';
  711. renderTableList(data.tables);
  712. // Auto-select first table
  713. if (data.tables && data.tables.length > 0) {
  714. selectTable(data.tables[0]);
  715. }
  716. });
  717. }
  718. // ─── Table list ───────────────────────────────────────────────────────────────
  719. function renderTableList(tables) {
  720. var list = document.getElementById('table-list');
  721. list.innerHTML = '';
  722. if (!tables || tables.length === 0) {
  723. list.innerHTML = '<div id="sidebar-empty">Database has no tables.</div>';
  724. return;
  725. }
  726. tables.forEach(function(name) {
  727. var isInternal = name.indexOf('sqlite_') === 0;
  728. var item = document.createElement('div');
  729. item.className = 'table-item';
  730. item.dataset.table = name;
  731. var dropBtnHtml = isInternal ? '' :
  732. '<button class="tbl-drop-btn" title="Drop table">' +
  733. '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
  734. '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/>' +
  735. '<path d="M10 11v6"/><path d="M14 11v6"/>' +
  736. '<path d="M9 6V4h6v2"/>' +
  737. '</svg>' +
  738. '</button>';
  739. item.innerHTML =
  740. '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
  741. '<rect x="3" y="3" width="18" height="18" rx="2"/>' +
  742. '<line x1="3" y1="9" x2="21" y2="9"/>' +
  743. '<line x1="3" y1="15" x2="21" y2="15"/>' +
  744. '<line x1="9" y1="3" x2="9" y2="21"/>' +
  745. '</svg>' +
  746. '<span class="tbl-name">' + escapeHtml(name) + '</span>' +
  747. dropBtnHtml;
  748. item.addEventListener('click', function(e) {
  749. if (e.target.closest('.tbl-drop-btn')) return;
  750. selectTable(name);
  751. });
  752. if (!isInternal) {
  753. item.querySelector('.tbl-drop-btn').addEventListener('click', function(e) {
  754. e.stopPropagation();
  755. openDropTableConfirm(name);
  756. });
  757. }
  758. list.appendChild(item);
  759. });
  760. }
  761. function selectTable(name) {
  762. App.table = name;
  763. App.page = 1;
  764. App.sortCol = null;
  765. App.sortDir = 'ASC';
  766. // Highlight sidebar item
  767. document.querySelectorAll('.table-item').forEach(function(el) {
  768. el.classList.toggle('active', el.dataset.table === name);
  769. });
  770. document.getElementById('tab-title').textContent = name;
  771. var activeTab = document.querySelector('.tab-btn.active');
  772. var tab = activeTab ? activeTab.dataset.tab : 'browse';
  773. if (tab === 'browse') loadBrowse();
  774. else if (tab === 'structure') loadStructure();
  775. }
  776. // ─── Tab switching ────────────────────────────────────────────────────────────
  777. document.querySelectorAll('.tab-btn').forEach(function(btn) {
  778. btn.addEventListener('click', function() {
  779. document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
  780. btn.classList.add('active');
  781. var tab = btn.dataset.tab;
  782. document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('visible'); });
  783. document.getElementById('tab-' + tab).classList.add('visible');
  784. if (!App.table) return;
  785. if (tab === 'browse') loadBrowse();
  786. else if (tab === 'structure') loadStructure();
  787. });
  788. });
  789. // ─── Browse ───────────────────────────────────────────────────────────────────
  790. document.getElementById('btn-refresh').addEventListener('click', function() {
  791. if (App.table) loadBrowse();
  792. });
  793. document.getElementById('limit-select').addEventListener('change', function() {
  794. App.limit = parseInt(this.value) || 50;
  795. App.page = 1;
  796. if (App.table) loadBrowse();
  797. });
  798. function loadBrowse() {
  799. if (!App.db || !App.table) return;
  800. loading(true);
  801. var params = {
  802. action: 'query', db: App.db,
  803. table: App.table, page: App.page, limit: App.limit
  804. };
  805. if (App.sortCol) {
  806. params.sort_col = App.sortCol;
  807. params.sort_dir = App.sortDir;
  808. }
  809. api(params, function(err, data) {
  810. loading(false);
  811. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  812. App.total = data.total;
  813. App.schema = data.schema || [];
  814. renderTable(data.rows, data.schema);
  815. renderPagination(data.page, data.total, data.limit);
  816. document.getElementById('row-count').textContent =
  817. data.total + ' row' + (data.total === 1 ? '' : 's') + ' total';
  818. });
  819. }
  820. function renderTable(rows, schema) {
  821. var placeholder = document.getElementById('browse-placeholder');
  822. var table = document.getElementById('data-table');
  823. var thead = document.getElementById('data-thead');
  824. var tbody = document.getElementById('data-tbody');
  825. if (!rows || rows.length === 0 && (!schema || schema.length === 0)) {
  826. placeholder.textContent = 'No data in this table.';
  827. placeholder.style.display = '';
  828. table.style.display = 'none';
  829. return;
  830. }
  831. placeholder.style.display = 'none';
  832. table.style.display = '';
  833. // Column order from schema
  834. var cols = schema.map(function(c) { return c.name; });
  835. var pkName = pkColFromSchema(schema);
  836. // Header
  837. thead.innerHTML = '';
  838. var hr = document.createElement('tr');
  839. cols.forEach(function(c) {
  840. var th = document.createElement('th');
  841. var classes = ['sortable'];
  842. if (c === pkName) classes.push('pk');
  843. if (c === App.sortCol) classes.push('sort-active');
  844. th.className = classes.join(' ');
  845. var label = document.createElement('span');
  846. label.textContent = c;
  847. th.appendChild(label);
  848. var icon = document.createElement('i');
  849. icon.className = 'sort-icon';
  850. if (c === App.sortCol) {
  851. icon.textContent = App.sortDir === 'DESC' ? '▼' : '▲';
  852. } else {
  853. icon.textContent = '⇅';
  854. }
  855. th.appendChild(icon);
  856. (function(col) {
  857. th.addEventListener('click', function() {
  858. if (App.sortCol === col) {
  859. App.sortDir = App.sortDir === 'ASC' ? 'DESC' : 'ASC';
  860. } else {
  861. App.sortCol = col;
  862. App.sortDir = 'ASC';
  863. }
  864. App.page = 1;
  865. loadBrowse();
  866. });
  867. })(c);
  868. hr.appendChild(th);
  869. });
  870. var thAct = document.createElement('th');
  871. thAct.className = 'actions';
  872. thAct.textContent = 'Actions';
  873. hr.appendChild(thAct);
  874. thead.appendChild(hr);
  875. // Rows
  876. tbody.innerHTML = '';
  877. rows.forEach(function(row) {
  878. var tr = document.createElement('tr');
  879. cols.forEach(function(c) {
  880. var td = document.createElement('td');
  881. var val = row[c];
  882. if (val === null || val === undefined) {
  883. td.innerHTML = '<span class="null">NULL</span>';
  884. } else {
  885. td.textContent = String(val);
  886. td.title = String(val);
  887. }
  888. if (c === pkName) td.className = 'pk';
  889. tr.appendChild(td);
  890. });
  891. // Actions
  892. var tdAct = document.createElement('td');
  893. tdAct.className = 'actions';
  894. var pkVal = pkName ? row[pkName] : null;
  895. var editBtn = document.createElement('button');
  896. editBtn.className = 'btn-edit';
  897. editBtn.textContent = 'Edit';
  898. editBtn.addEventListener('click', (function(r) {
  899. return function() { openEditModal(r, schema, pkName); };
  900. })(row));
  901. var delBtn = document.createElement('button');
  902. delBtn.className = 'btn-del';
  903. delBtn.textContent = 'Del';
  904. delBtn.addEventListener('click', (function(pv) {
  905. return function() { openDeleteConfirm(pkName, pv); };
  906. })(pkVal));
  907. tdAct.appendChild(editBtn);
  908. tdAct.appendChild(document.createTextNode(' '));
  909. tdAct.appendChild(delBtn);
  910. tr.appendChild(tdAct);
  911. tbody.appendChild(tr);
  912. });
  913. }
  914. function pkColFromSchema(schema) {
  915. if (!schema) return null;
  916. for (var i = 0; i < schema.length; i++) {
  917. if (schema[i].pk > 0) return schema[i].name;
  918. }
  919. return schema.length > 0 ? schema[0].name : null;
  920. }
  921. // ─── Pagination ───────────────────────────────────────────────────────────────
  922. function renderPagination(page, total, limit) {
  923. var pages = Math.max(1, Math.ceil(total / limit));
  924. var el = document.getElementById('pagination');
  925. el.innerHTML = '';
  926. if (pages <= 1) return;
  927. var prev = document.createElement('button');
  928. prev.className = 'page-btn';
  929. prev.textContent = '‹ Prev';
  930. prev.disabled = (page <= 1);
  931. prev.addEventListener('click', function() { App.page = page - 1; loadBrowse(); });
  932. el.appendChild(prev);
  933. var start = Math.max(1, page - 2);
  934. var end = Math.min(pages, start + 4);
  935. start = Math.max(1, end - 4);
  936. for (var p = start; p <= end; p++) {
  937. var pb = document.createElement('button');
  938. pb.className = 'page-btn' + (p === page ? ' current' : '');
  939. pb.textContent = p;
  940. (function(pp) {
  941. pb.addEventListener('click', function() { App.page = pp; loadBrowse(); });
  942. })(p);
  943. el.appendChild(pb);
  944. }
  945. var info = document.createElement('span');
  946. info.id = 'page-info';
  947. info.textContent = 'of ' + pages;
  948. el.appendChild(info);
  949. var next = document.createElement('button');
  950. next.className = 'page-btn';
  951. next.textContent = 'Next ›';
  952. next.disabled = (page >= pages);
  953. next.addEventListener('click', function() { App.page = page + 1; loadBrowse(); });
  954. el.appendChild(next);
  955. }
  956. // ─── Structure ────────────────────────────────────────────────────────────────
  957. function loadStructure() {
  958. if (!App.db || !App.table) return;
  959. loading(true);
  960. api({action: 'schema', db: App.db, table: App.table}, function(err, data) {
  961. loading(false);
  962. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  963. renderStructure(data.schema);
  964. });
  965. }
  966. function renderStructure(schema) {
  967. var placeholder = document.getElementById('structure-placeholder');
  968. var table = document.getElementById('schema-table');
  969. var tbody = document.getElementById('schema-tbody');
  970. if (!schema || schema.length === 0) {
  971. placeholder.textContent = 'No schema information.';
  972. placeholder.style.display = '';
  973. table.style.display = 'none';
  974. return;
  975. }
  976. placeholder.style.display = 'none';
  977. table.style.display = '';
  978. tbody.innerHTML = '';
  979. schema.forEach(function(col) {
  980. var tr = document.createElement('tr');
  981. var badges = '';
  982. if (col.pk > 0) badges += '<span class="badge badge-pk">PK</span> ';
  983. if (col.notnull > 0) badges += '<span class="badge badge-nn">NOT NULL</span> ';
  984. var dflt = (col.dflt_value !== null && col.dflt_value !== undefined)
  985. ? escapeHtml(String(col.dflt_value))
  986. : '<span style="color:var(--null-col);font-style:italic">NULL</span>';
  987. tr.innerHTML =
  988. '<td style="color:var(--muted);font-size:11px;">' + col.cid + '</td>' +
  989. '<td><strong>' + escapeHtml(col.name) + '</strong></td>' +
  990. '<td><span class="badge badge-type">' + escapeHtml(col.type || 'ANY') + '</span></td>' +
  991. '<td>' + badges + '</td>' +
  992. '<td style="font-size:12px;">' + dflt + '</td>';
  993. tbody.appendChild(tr);
  994. });
  995. }
  996. // ─── Add Row ──────────────────────────────────────────────────────────────────
  997. document.getElementById('btn-add-row').addEventListener('click', function() {
  998. if (!App.table) { toast('Select a table first.', true); return; }
  999. openInsertModal(App.schema);
  1000. });
  1001. function openInsertModal(schema) {
  1002. App.pendingEditRow = null;
  1003. document.getElementById('modal-title').textContent = 'Add Row — ' + App.table;
  1004. var fields = document.getElementById('modal-fields');
  1005. fields.innerHTML = '';
  1006. schema.forEach(function(col) {
  1007. var div = document.createElement('div');
  1008. div.className = 'modal-field';
  1009. var label = document.createElement('label');
  1010. label.textContent = col.name;
  1011. if (col.pk > 0) {
  1012. label.textContent += ' (PK)';
  1013. }
  1014. var input = document.createElement('input');
  1015. input.type = 'text';
  1016. input.name = col.name;
  1017. input.placeholder = col.type || '';
  1018. if (col.pk > 0 && col.type && col.type.toUpperCase().indexOf('INTEGER') >= 0) {
  1019. input.placeholder = 'auto-increment';
  1020. }
  1021. div.appendChild(label);
  1022. div.appendChild(input);
  1023. fields.appendChild(div);
  1024. });
  1025. document.getElementById('modal-save').onclick = doInsert;
  1026. openModal();
  1027. }
  1028. function doInsert() {
  1029. var inputs = document.querySelectorAll('#modal-fields input');
  1030. var row = {};
  1031. inputs.forEach(function(inp) {
  1032. if (inp.value !== '') row[inp.name] = inp.value;
  1033. });
  1034. if (Object.keys(row).length === 0) { toast('Enter at least one value.', true); return; }
  1035. loading(true);
  1036. api({action: 'insert', db: App.db, table: App.table, row: JSON.stringify(row)},
  1037. function(err, data) {
  1038. loading(false);
  1039. closeModal();
  1040. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  1041. toast('Row inserted.');
  1042. loadBrowse();
  1043. });
  1044. }
  1045. // ─── Edit Row ─────────────────────────────────────────────────────────────────
  1046. function openEditModal(row, schema, pkName) {
  1047. App.pendingEditRow = {row: row, pkName: pkName};
  1048. document.getElementById('modal-title').textContent = 'Edit Row — ' + App.table;
  1049. var fields = document.getElementById('modal-fields');
  1050. fields.innerHTML = '';
  1051. schema.forEach(function(col) {
  1052. var div = document.createElement('div');
  1053. div.className = 'modal-field';
  1054. var label = document.createElement('label');
  1055. label.textContent = col.name;
  1056. if (col.pk > 0) label.textContent += ' (PK)';
  1057. var input = document.createElement('input');
  1058. input.type = 'text';
  1059. input.name = col.name;
  1060. var val = row[col.name];
  1061. input.value = (val !== null && val !== undefined) ? String(val) : '';
  1062. if (col.pk > 0) {
  1063. input.readOnly = true;
  1064. input.style.opacity = '.6';
  1065. var note = document.createElement('div');
  1066. note.className = 'pk-note';
  1067. note.textContent = 'Primary key — read-only';
  1068. div.appendChild(label);
  1069. div.appendChild(input);
  1070. div.appendChild(note);
  1071. } else {
  1072. div.appendChild(label);
  1073. div.appendChild(input);
  1074. }
  1075. fields.appendChild(div);
  1076. });
  1077. document.getElementById('modal-save').onclick = doUpdate;
  1078. openModal();
  1079. }
  1080. function doUpdate() {
  1081. if (!App.pendingEditRow) return;
  1082. var pkName = App.pendingEditRow.pkName;
  1083. var pkVal = App.pendingEditRow.row[pkName];
  1084. var inputs = document.querySelectorAll('#modal-fields input:not([readonly])');
  1085. var promises = [];
  1086. inputs.forEach(function(inp) {
  1087. promises.push({col: inp.name, val: inp.value});
  1088. });
  1089. if (promises.length === 0) { closeModal(); return; }
  1090. var done = 0;
  1091. var errors = [];
  1092. loading(true);
  1093. function finish() {
  1094. done++;
  1095. if (done === promises.length) {
  1096. loading(false);
  1097. closeModal();
  1098. if (errors.length > 0) toast(errors[0], true);
  1099. else { toast('Row updated.'); loadBrowse(); }
  1100. }
  1101. }
  1102. promises.forEach(function(p) {
  1103. api({action: 'update', db: App.db, table: App.table,
  1104. pk_col: pkName, pk_val: pkVal, col: p.col, val: p.val},
  1105. function(err, data) {
  1106. if (err || data.error) errors.push(err ? err.message : data.error);
  1107. finish();
  1108. });
  1109. });
  1110. }
  1111. // ─── Delete Row ───────────────────────────────────────────────────────────────
  1112. function openDeleteConfirm(pkCol, pkVal) {
  1113. if (!pkCol || pkVal === null || pkVal === undefined) {
  1114. toast('Cannot identify row to delete (no primary key).', true);
  1115. return;
  1116. }
  1117. App.pendingDelete = {pkCol: pkCol, pkVal: pkVal};
  1118. App.pendingDrop = null;
  1119. document.getElementById('confirm-title').textContent = 'Delete Row';
  1120. document.getElementById('confirm-body').textContent =
  1121. 'Are you sure you want to delete this row? This action cannot be undone.';
  1122. document.getElementById('del-confirm').textContent = 'Delete';
  1123. document.getElementById('confirm-overlay').style.display = 'flex';
  1124. }
  1125. function openDropTableConfirm(tableName) {
  1126. App.pendingDrop = tableName;
  1127. App.pendingDelete = null;
  1128. document.getElementById('confirm-title').textContent = 'Drop Table';
  1129. document.getElementById('confirm-body').innerHTML =
  1130. 'Are you sure you want to drop <strong>' + escapeHtml(tableName) + '</strong>? ' +
  1131. 'All data in this table will be permanently deleted and cannot be recovered.';
  1132. document.getElementById('del-confirm').textContent = 'Drop Table';
  1133. document.getElementById('confirm-overlay').style.display = 'flex';
  1134. }
  1135. function closeConfirm() {
  1136. App.pendingDelete = null;
  1137. App.pendingDrop = null;
  1138. document.getElementById('confirm-overlay').style.display = 'none';
  1139. }
  1140. document.getElementById('del-cancel').addEventListener('click', closeConfirm);
  1141. document.getElementById('del-confirm').addEventListener('click', function() {
  1142. if (App.pendingDrop) {
  1143. var tbl = App.pendingDrop;
  1144. closeConfirm();
  1145. loading(true);
  1146. api({action: 'droptable', db: App.db, table: tbl}, function(err, data) {
  1147. loading(false);
  1148. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  1149. toast('Table "' + tbl + '" dropped.');
  1150. // Clear selection if the dropped table was active
  1151. if (App.table === tbl) {
  1152. App.table = null;
  1153. document.getElementById('tab-title').textContent = '';
  1154. document.getElementById('data-table').style.display = 'none';
  1155. document.getElementById('browse-placeholder').textContent = 'Select a table from the sidebar.';
  1156. document.getElementById('browse-placeholder').style.display = '';
  1157. document.getElementById('pagination').innerHTML = '';
  1158. document.getElementById('row-count').textContent = '';
  1159. }
  1160. // Refresh sidebar table list
  1161. api({action: 'tables', db: App.db}, function(err2, d2) {
  1162. if (!err2 && !d2.error) renderTableList(d2.tables);
  1163. });
  1164. });
  1165. } else if (App.pendingDelete) {
  1166. var d = App.pendingDelete;
  1167. closeConfirm();
  1168. loading(true);
  1169. api({action: 'delete', db: App.db, table: App.table,
  1170. pk_col: d.pkCol, pk_val: d.pkVal},
  1171. function(err, data) {
  1172. loading(false);
  1173. if (err || data.error) { toast(err ? err.message : data.error, true); return; }
  1174. toast('Row deleted.');
  1175. loadBrowse();
  1176. });
  1177. }
  1178. });
  1179. // ─── SQL Editor ───────────────────────────────────────────────────────────────
  1180. document.getElementById('btn-exec-sql').addEventListener('click', executeSql);
  1181. document.getElementById('sql-editor').addEventListener('keydown', function(e) {
  1182. if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); executeSql(); }
  1183. });
  1184. function executeSql() {
  1185. var sql = document.getElementById('sql-editor').value.trim();
  1186. if (!sql || !App.db) return;
  1187. loading(true);
  1188. api({action: 'exec', db: App.db, sql: sql}, function(err, data) {
  1189. loading(false);
  1190. var out = document.getElementById('sql-results');
  1191. if (err) {
  1192. out.innerHTML = '<div class="sql-error">Error: ' + escapeHtml(err.message) + '</div>';
  1193. return;
  1194. }
  1195. if (data.error) {
  1196. out.innerHTML = '<div class="sql-error">Error: ' + escapeHtml(data.error) + '</div>';
  1197. return;
  1198. }
  1199. if (data.rows && data.rows.length > 0) {
  1200. renderSqlResultTable(data.rows, out);
  1201. } else if (data.result) {
  1202. out.innerHTML = '<div class="sql-ok-msg">✓ OK — ' +
  1203. data.result.rowsAffected + ' row(s) affected, last insert ID: ' +
  1204. data.result.lastInsertId + '</div>';
  1205. // Refresh table list and current table if schema might have changed
  1206. refreshAfterExec(sql);
  1207. } else {
  1208. out.innerHTML = '<div class="sql-ok-msg">✓ Query executed, no rows returned.</div>';
  1209. refreshAfterExec(sql);
  1210. }
  1211. });
  1212. }
  1213. function renderSqlResultTable(rows, container) {
  1214. if (!rows || rows.length === 0) {
  1215. container.innerHTML = '<div class="sql-ok-msg">No rows returned.</div>';
  1216. return;
  1217. }
  1218. var cols = Object.keys(rows[0]);
  1219. var html = '<table class="data-table" style="margin:0;border-radius:0;box-shadow:none;">';
  1220. html += '<thead><tr>' + cols.map(function(c) {
  1221. return '<th>' + escapeHtml(c) + '</th>';
  1222. }).join('') + '</tr></thead><tbody>';
  1223. rows.forEach(function(row) {
  1224. html += '<tr>' + cols.map(function(c) {
  1225. var v = row[c];
  1226. if (v === null || v === undefined) return '<td><span class="null">NULL</span></td>';
  1227. return '<td title="' + escapeHtml(String(v)) + '">' + escapeHtml(String(v)) + '</td>';
  1228. }).join('') + '</tr>';
  1229. });
  1230. html += '</tbody></table>';
  1231. container.innerHTML = html;
  1232. }
  1233. function refreshAfterExec(sql) {
  1234. var upper = sql.trim().toUpperCase();
  1235. // Reload table list if DDL was likely executed
  1236. if (upper.indexOf('CREATE') === 0 || upper.indexOf('DROP') === 0 ||
  1237. upper.indexOf('ALTER') === 0) {
  1238. api({action: 'tables', db: App.db}, function(err, data) {
  1239. if (!err && !data.error) renderTableList(data.tables);
  1240. });
  1241. }
  1242. // Refresh current browse view if a table is selected
  1243. if (App.table) loadBrowse();
  1244. }
  1245. // ─── Modal helpers ────────────────────────────────────────────────────────────
  1246. function openModal() {
  1247. document.getElementById('modal-overlay').classList.add('open');
  1248. var first = document.querySelector('#modal-fields input');
  1249. if (first) setTimeout(function() { first.focus(); }, 50);
  1250. }
  1251. function closeModal() {
  1252. document.getElementById('modal-overlay').classList.remove('open');
  1253. }
  1254. document.getElementById('modal-cancel').addEventListener('click', closeModal);
  1255. document.getElementById('modal-overlay').addEventListener('click', function(e) {
  1256. if (e.target === this) closeModal();
  1257. });
  1258. document.getElementById('confirm-overlay').addEventListener('click', function(e) {
  1259. if (e.target === this) closeConfirm();
  1260. });
  1261. </script>
  1262. </body>
  1263. </html>