@lchase/kiln 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/serve.js CHANGED
@@ -248,9 +248,819 @@
248
248
  }
249
249
  }
250
250
 
251
+ var titleDirty = false;
252
+ var issueDirty = false;
253
+ var sourceKind = "prompt";
254
+ var specPath = "";
255
+ var ticketsDir = "";
256
+ var fsPath = "";
257
+ var sending = false;
258
+ var FS_KEY = "kiln-fs-dir";
259
+
260
+ function selectedRepo() {
261
+ var pressed = document.querySelector("#repo-pick .pick[aria-pressed='true']");
262
+ return pressed ? pressed.getAttribute("data-repo") || "" : "";
263
+ }
264
+
265
+ function intakeRepoPath() {
266
+ var pressed = document.querySelector("#repo-pick .pick[aria-pressed='true']");
267
+ return pressed ? pressed.getAttribute("data-path") || "" : "";
268
+ }
269
+
270
+ function showIntakeError(message) {
271
+ var el = document.getElementById("intake-error");
272
+ if (!el) return;
273
+ el.hidden = !message;
274
+ el.textContent = message || "";
275
+ }
276
+
277
+ function titleFromPrompt(text) {
278
+ var lines = String(text).split(/\n/);
279
+ for (var i = 0; i < lines.length; i++) {
280
+ var line = lines[i].trim();
281
+ if (!line) continue;
282
+ if (line.length <= 80) return line;
283
+ return line.slice(0, 77) + "...";
284
+ }
285
+ return "";
286
+ }
287
+
288
+ function titleFromMarkdown(text) {
289
+ var lines = String(text).split(/\n/);
290
+ for (var i = 0; i < lines.length; i++) {
291
+ var match = /^#\s+(.+?)\s*$/.exec(lines[i]);
292
+ if (match) return match[1];
293
+ }
294
+ return "";
295
+ }
296
+
297
+ function setPicked(text) {
298
+ var el = document.getElementById("picked");
299
+ if (el) el.textContent = text || "";
300
+ }
301
+
302
+ function renderListing(data) {
303
+ fsPath = data.path || "";
304
+ try { localStorage.setItem(FS_KEY, fsPath); } catch (err) { /* private mode */ }
305
+ var host = document.getElementById("browser");
306
+ if (!host) return;
307
+ var html = '<div class="branch">' + esc(fsPath) + "</div>";
308
+ if (data.parent) html += '<button type="button" class="fs-up" data-path="' + esc(data.parent) + '">Up</button>';
309
+ var entries = data.entries || [];
310
+ html += '<div class="fs-list">';
311
+ if (!entries.length) html += '<div class="empty">Nothing here.</div>';
312
+ for (var i = 0; i < entries.length; i++) {
313
+ var entry = entries[i];
314
+ html += '<button type="button" class="fs-item" data-kind="' + esc(entry.kind) + '" data-path="' + esc(entry.path) + '">' + esc(entry.name) + "</button>";
315
+ }
316
+ html += "</div>";
317
+ if (sourceKind === "tickets") html += '<button type="button" class="fs-use" data-path="' + esc(fsPath) + '">Use this folder</button>';
318
+ host.innerHTML = html;
319
+ if (sourceKind === "tickets") setPicked(fsPath);
320
+ }
321
+
322
+ function chooseSpec(data) {
323
+ specPath = data.path || "";
324
+ setPicked(specPath);
325
+ if (!titleDirty) {
326
+ var heading = titleFromMarkdown(data.preview || "");
327
+ var input = document.getElementById("job-title");
328
+ if (input && heading) input.value = heading;
329
+ }
330
+ }
331
+
332
+ function loadFs(path, fallback) {
333
+ var query = path ? "?path=" + encodeURIComponent(path) : "";
334
+ fetch("/api/fs" + query, { cache: "no-store", headers: { Accept: "application/json" } })
335
+ .then(function (response) {
336
+ return response.json().then(function (data) { return { ok: response.ok, data: data }; });
337
+ })
338
+ .then(function (result) {
339
+ if (!result.ok) {
340
+ if (fallback && path) {
341
+ loadFs("", false);
342
+ return;
343
+ }
344
+ showIntakeError((result.data && result.data.error) || "Could not list that directory.");
345
+ return;
346
+ }
347
+ showIntakeError("");
348
+ if (result.data.kind === "file") chooseSpec(result.data);
349
+ else renderListing(result.data);
350
+ })
351
+ .catch(function () { showIntakeError("Could not list that directory."); });
352
+ }
353
+
354
+ function openBrowser() {
355
+ var start = "";
356
+ try { start = localStorage.getItem(FS_KEY) || ""; } catch (err) { start = ""; }
357
+ if (!start) start = intakeRepoPath();
358
+ loadFs(start, true);
359
+ }
360
+
361
+ function setSource(kind) {
362
+ sourceKind = kind || "prompt";
363
+ var buttons = document.querySelectorAll("#add-form [data-source]");
364
+ for (var i = 0; i < buttons.length; i++) {
365
+ buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-source") === sourceKind ? "true" : "false");
366
+ }
367
+ var panes = document.querySelectorAll("#add-form [data-pane]");
368
+ for (var j = 0; j < panes.length; j++) panes[j].hidden = panes[j].getAttribute("data-pane") !== sourceKind;
369
+ var browserField = document.getElementById("browser-field");
370
+ if (browserField) browserField.hidden = sourceKind !== "spec" && sourceKind !== "tickets";
371
+ var label = document.getElementById("browser-label");
372
+ if (label) label.textContent = sourceKind === "tickets" ? "Tickets folder" : "Spec file";
373
+ var titleField = document.getElementById("title-field");
374
+ var issueField = document.getElementById("issue-field");
375
+ var afterField = document.getElementById("after-field");
376
+ var preview = document.getElementById("preview-job");
377
+ if (titleField) titleField.hidden = sourceKind === "tickets";
378
+ if (issueField) issueField.hidden = sourceKind === "tickets";
379
+ if (afterField) afterField.hidden = sourceKind === "tickets";
380
+ if (preview) preview.hidden = sourceKind !== "tickets";
381
+ var pre = document.getElementById("intake-preview");
382
+ if (pre) { pre.hidden = true; pre.textContent = ""; }
383
+ showIntakeError("");
384
+ if (sourceKind === "prompt" && !titleDirty) {
385
+ var promptBox = document.getElementById("prompt-text");
386
+ var title = document.getElementById("job-title");
387
+ if (promptBox && title) title.value = titleFromPrompt(promptBox.value);
388
+ }
389
+ syncIssueDefault();
390
+ if (sourceKind === "spec" || sourceKind === "tickets") openBrowser();
391
+ }
392
+
393
+ function syncIssueDefault() {
394
+ var input = document.getElementById("job-issue");
395
+ if (!input || issueDirty) return;
396
+ if (sourceKind !== "jira") {
397
+ input.value = "";
398
+ return;
399
+ }
400
+ var key = document.getElementById("jira-key");
401
+ input.value = key ? key.value.trim().toUpperCase() : "";
402
+ }
403
+
404
+ function openIntake() {
405
+ var sheets = document.querySelectorAll(".sheet");
406
+ for (var i = 0; i < sheets.length; i++) sheets[i].hidden = sheets[i].id !== "intake";
407
+ body.classList.add("open");
408
+ try { localStorage.removeItem("kiln-drawer"); } catch (err) { /* private mode */ }
409
+ setSource(sourceKind || "prompt");
410
+ }
411
+
412
+ function sourceBody() {
413
+ if (sourceKind === "prompt") {
414
+ var promptBox = document.getElementById("prompt-text");
415
+ return { kind: "prompt", text: promptBox ? promptBox.value : "" };
416
+ }
417
+ if (sourceKind === "spec") return { kind: "spec", path: specPath };
418
+ if (sourceKind === "tickets") return { kind: "tickets", dir: ticketsDir || fsPath };
419
+ var key = document.getElementById("jira-key");
420
+ return { kind: "jira", key: key ? key.value.trim() : "" };
421
+ }
422
+
423
+ function submitIntake(dryRun) {
424
+ if (sending) return;
425
+ var repo = selectedRepo();
426
+ var workflow = document.getElementById("workflow");
427
+ var source = sourceBody();
428
+ if (!repo) { showIntakeError("Name a repo."); return; }
429
+ if (source.kind === "prompt" && !String(source.text || "").trim()) { showIntakeError("Prompt is empty."); return; }
430
+ if (source.kind === "spec" && !source.path) { showIntakeError("Choose a spec file."); return; }
431
+ if (source.kind === "tickets" && !source.dir) { showIntakeError("Choose a tickets folder."); return; }
432
+ if (source.kind === "jira" && !source.key) { showIntakeError("Enter a Jira key."); return; }
433
+ var payload = { repo: repo, workflow: workflow ? workflow.value : "", source: source };
434
+ if (dryRun) payload.dryRun = true;
435
+ if (sourceKind !== "tickets") {
436
+ var issue = document.getElementById("job-issue");
437
+ if (issue && issue.value.trim()) payload.issue = issue.value.trim();
438
+ var title = document.getElementById("job-title");
439
+ if (title && titleDirty) payload.title = title.value;
440
+ var checked = document.querySelectorAll("#after-list input:checked");
441
+ if (checked.length) {
442
+ payload.after = [];
443
+ for (var i = 0; i < checked.length; i++) payload.after.push(checked[i].value);
444
+ }
445
+ }
446
+ sending = true;
447
+ showIntakeError("");
448
+ fetch("/api/jobs", {
449
+ method: "POST",
450
+ headers: {
451
+ Accept: "application/json",
452
+ "Content-Type": "application/json",
453
+ "X-Kiln-CSRF": body.getAttribute("data-csrf") || "",
454
+ },
455
+ body: JSON.stringify(payload),
456
+ })
457
+ .then(function (response) {
458
+ return response.json().then(function (data) { return { ok: response.ok, data: data }; });
459
+ })
460
+ .then(function (result) {
461
+ sending = false;
462
+ if (!result.ok) {
463
+ showIntakeError((result.data && result.data.error) || "Could not add the job.");
464
+ return;
465
+ }
466
+ if (dryRun) {
467
+ var pre = document.getElementById("intake-preview");
468
+ if (pre) {
469
+ pre.hidden = false;
470
+ pre.textContent = (result.data && result.data.report) || "";
471
+ }
472
+ return;
473
+ }
474
+ var ids = (result.data && result.data.ids) || [];
475
+ try { sessionStorage.setItem("kiln-fresh", JSON.stringify(ids)); } catch (err) { /* private mode */ }
476
+ closeDrawer();
477
+ location.reload();
478
+ })
479
+ .catch(function () {
480
+ sending = false;
481
+ showIntakeError("Could not add the job.");
482
+ });
483
+ }
484
+
485
+ function applyLiveConfig(data) {
486
+ var el = document.getElementById("config-error");
487
+ if (el) {
488
+ if (data && data.configError) {
489
+ el.hidden = false;
490
+ el.textContent = data.configError;
491
+ } else {
492
+ el.hidden = true;
493
+ el.textContent = "";
494
+ }
495
+ }
496
+ if (!data || !Array.isArray(data.repos)) return;
497
+ var box = document.getElementById("repo-pick");
498
+ if (!box) return;
499
+ var current = selectedRepo();
500
+ var still = false;
501
+ var html = "";
502
+ if (!data.repos.length) html = '<p class="empty">No repos are registered.</p>';
503
+ for (var i = 0; i < data.repos.length; i++) {
504
+ var repo = data.repos[i];
505
+ if (!repo || !repo.name) continue;
506
+ if (repo.name === current) still = true;
507
+ }
508
+ if (data.repos.length) {
509
+ html = "";
510
+ for (var j = 0; j < data.repos.length; j++) {
511
+ var item = data.repos[j];
512
+ var pressed = still ? item.name === current : j === 0;
513
+ html += '<button type="button" class="pick" data-repo="' + esc(item.name) + '" data-path="' + esc(item.path || "") + '" aria-pressed="' + (pressed ? "true" : "false") + '">' + esc(item.name) + "</button>";
514
+ }
515
+ }
516
+ if (box.getAttribute("data-repos") === html) return;
517
+ box.setAttribute("data-repos", html);
518
+ box.innerHTML = html;
519
+ }
520
+
521
+ function showFresh() {
522
+ var raw = null;
523
+ try { raw = sessionStorage.getItem("kiln-fresh"); } catch (err) { raw = null; }
524
+ if (!raw) return;
525
+ try { sessionStorage.removeItem("kiln-fresh"); } catch (err) { /* private mode */ }
526
+ var ids = [];
527
+ try { ids = JSON.parse(raw); } catch (err) { return; }
528
+ if (!ids || !ids.length) return;
529
+ for (var i = 0; i < ids.length; i++) {
530
+ var card = document.getElementById("job-" + ids[i]);
531
+ if (!card) continue;
532
+ card.classList.add("fresh");
533
+ if (i === 0 && card.scrollIntoView) card.scrollIntoView({ block: "nearest", inline: "center" });
534
+ }
535
+ }
536
+
537
+ var settingsDoc = null;
538
+ var repoDraft = {};
539
+ var editing = null;
540
+ var settingsSaving = false;
541
+ var settingsDefaults = {};
542
+
543
+ function showSettingsError(message) {
544
+ var el = document.getElementById("settings-error");
545
+ if (!el) return;
546
+ el.hidden = !message;
547
+ el.textContent = message || "";
548
+ }
549
+
550
+ function markSettingsDirty() {
551
+ var saved = document.getElementById("settings-saved");
552
+ if (saved) saved.hidden = true;
553
+ }
554
+
555
+ function clearSettingsErrors() {
556
+ var nodes = document.querySelectorAll("#settings .field-error");
557
+ for (var i = 0; i < nodes.length; i++) {
558
+ nodes[i].hidden = true;
559
+ nodes[i].textContent = "";
560
+ }
561
+ showSettingsError("");
562
+ }
563
+
564
+ function showSlot(name, message) {
565
+ var slot = document.querySelector('#settings [data-error="' + name + '"]');
566
+ if (!slot) return;
567
+ slot.hidden = !message;
568
+ slot.textContent = message || "";
569
+ if (message && slot.scrollIntoView) slot.scrollIntoView({ block: "nearest" });
570
+ }
571
+
572
+ function setSettingsTab(tab) {
573
+ var repos = document.getElementById("settings-repos");
574
+ var globalPane = document.getElementById("settings-global");
575
+ if (repos) repos.hidden = tab !== "repos";
576
+ if (globalPane) globalPane.hidden = tab !== "global";
577
+ var buttons = document.querySelectorAll("#settings-form [data-settings-tab]");
578
+ for (var i = 0; i < buttons.length; i++) {
579
+ buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-settings-tab") === tab ? "true" : "false");
580
+ }
581
+ }
582
+
583
+ function setPressed(selector, attr, value) {
584
+ var buttons = document.querySelectorAll(selector);
585
+ for (var i = 0; i < buttons.length; i++) {
586
+ buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute(attr) === value ? "true" : "false");
587
+ }
588
+ }
589
+
590
+ function selectedAttr(selector, attr, fallback) {
591
+ var pressed = document.querySelector(selector + '[aria-pressed="true"]');
592
+ return pressed ? pressed.getAttribute(attr) || fallback : fallback;
593
+ }
594
+
595
+ function cloneRepos(repos) {
596
+ var out = {};
597
+ var names = Object.keys(repos || {});
598
+ for (var i = 0; i < names.length; i++) {
599
+ var name = names[i];
600
+ var repo = repos[name] || {};
601
+ out[name] = {
602
+ path: repo.path || "",
603
+ test: repo.test || "",
604
+ deliver: repo.deliver === "pr" ? "pr" : "local",
605
+ base: repo.base || "",
606
+ prTitle: repo.prTitle || "",
607
+ branch: repo.branch || "",
608
+ };
609
+ }
610
+ return out;
611
+ }
612
+
613
+ function fillHints(defaults) {
614
+ settingsDefaults = defaults || {};
615
+ var pr = document.getElementById("hint-pr-title");
616
+ var branch = document.getElementById("hint-branch");
617
+ var title = settingsDefaults.prTitle || "[{issue}] {title}";
618
+ var template = settingsDefaults.branch || "{issue}-{slug}";
619
+ if (pr) pr.textContent = "Default " + title + ", which renders as [ABC-123] Title.";
620
+ if (branch) branch.textContent = "Default " + template + ", which renders as ABC-123-slug.";
621
+ var prInput = document.getElementById("repo-pr-title");
622
+ var branchInput = document.getElementById("repo-branch");
623
+ if (prInput) prInput.placeholder = title;
624
+ if (branchInput) branchInput.placeholder = template;
625
+ var attempts = document.getElementById("hint-attempts");
626
+ var stall = document.getElementById("hint-stall");
627
+ var rounds = document.getElementById("hint-fix");
628
+ if (attempts) attempts.textContent = "Default " + (settingsDefaults.maxAttempts != null ? settingsDefaults.maxAttempts : 3) + ". Transient failures before the job is dead.";
629
+ if (stall) stall.textContent = "Default " + (settingsDefaults.stallTimeoutMs != null ? settingsDefaults.stallTimeoutMs : 900000) + " (15 minutes). 0 disables stall detection.";
630
+ if (rounds) rounds.textContent = "Default " + (settingsDefaults.maxFixRounds != null ? settingsDefaults.maxFixRounds : 2) + ". Fix rounds before the job needs a person.";
631
+ var attemptInput = document.getElementById("max-attempts");
632
+ var stallInput = document.getElementById("stall-timeout");
633
+ var roundInput = document.getElementById("max-fix-rounds");
634
+ if (attemptInput) attemptInput.placeholder = String(settingsDefaults.maxAttempts != null ? settingsDefaults.maxAttempts : 3);
635
+ if (stallInput) stallInput.placeholder = String(settingsDefaults.stallTimeoutMs != null ? settingsDefaults.stallTimeoutMs : 900000);
636
+ if (roundInput) roundInput.placeholder = String(settingsDefaults.maxFixRounds != null ? settingsDefaults.maxFixRounds : 2);
637
+ }
638
+
639
+ function fillGlobal(data) {
640
+ fillHints(data.defaults || {});
641
+ var attempts = document.getElementById("max-attempts");
642
+ var stall = document.getElementById("stall-timeout");
643
+ var rounds = document.getElementById("max-fix-rounds");
644
+ if (attempts) attempts.value = data.maxAttempts != null ? String(data.maxAttempts) : "";
645
+ if (stall) stall.value = data.stallTimeoutMs != null ? String(data.stallTimeoutMs) : "";
646
+ if (rounds) rounds.value = data.maxFixRounds != null ? String(data.maxFixRounds) : "";
647
+ setPressed("#sandbox-mode [data-sandbox]", "data-sandbox", data.sandbox && data.sandbox.mode === "docker" ? "docker" : "off");
648
+ var domains = document.getElementById("sandbox-domains");
649
+ if (domains) domains.value = data.sandbox && data.sandbox.allowDomains ? data.sandbox.allowDomains.join("\n") : "";
650
+ }
651
+
652
+ function renderRepoList() {
653
+ var host = document.getElementById("repo-list");
654
+ if (!host) return;
655
+ var names = Object.keys(repoDraft).sort();
656
+ var html = "";
657
+ for (var i = 0; i < names.length; i++) {
658
+ var name = names[i];
659
+ if (editing && editing.original === name) continue;
660
+ var repo = repoDraft[name];
661
+ var bits = [repo.deliver || "local"];
662
+ if (repo.base) bits.push("base " + repo.base);
663
+ if (repo.test) bits.push(repo.test);
664
+ html += '<div class="repo-row"><div><div class="num">' + esc(name) + '</div><div class="branch">' + esc(repo.path || "") + '</div><div class="meta">' + esc(bits.join(" · ")) + '</div></div><div class="d-actions"><button type="button" data-repo-edit="' + esc(name) + '">Edit</button><button type="button" data-repo-remove="' + esc(name) + '">Remove</button></div></div>';
665
+ }
666
+ if (!html && !editing) html = '<p class="empty">No repos yet.</p>';
667
+ host.innerHTML = html;
668
+ }
669
+
670
+ function fillEditor(name, repo) {
671
+ var nameInput = document.getElementById("repo-name");
672
+ var pathInput = document.getElementById("repo-path");
673
+ var testInput = document.getElementById("repo-test");
674
+ var baseInput = document.getElementById("repo-base");
675
+ var titleInput = document.getElementById("repo-pr-title");
676
+ var branchInput = document.getElementById("repo-branch");
677
+ if (nameInput) nameInput.value = name || "";
678
+ if (pathInput) pathInput.value = repo.path || "";
679
+ if (testInput) testInput.value = repo.test || "";
680
+ if (baseInput) baseInput.value = repo.base || "";
681
+ if (titleInput) titleInput.value = repo.prTitle || "";
682
+ if (branchInput) branchInput.value = repo.branch || "";
683
+ setPressed("#repo-deliver [data-deliver]", "data-deliver", repo.deliver === "pr" ? "pr" : "local");
684
+ var browser = document.getElementById("repo-browser");
685
+ if (browser) { browser.hidden = true; browser.innerHTML = ""; }
686
+ }
687
+
688
+ function readEditor() {
689
+ function value(id) {
690
+ var el = document.getElementById(id);
691
+ return el ? el.value.trim() : "";
692
+ }
693
+ return {
694
+ name: value("repo-name"),
695
+ path: value("repo-path"),
696
+ test: value("repo-test"),
697
+ deliver: selectedAttr("#repo-deliver [data-deliver]", "data-deliver", "local"),
698
+ base: value("repo-base"),
699
+ prTitle: value("repo-pr-title"),
700
+ branch: value("repo-branch"),
701
+ };
702
+ }
703
+
704
+ function flushEditor() {
705
+ if (!editing) return true;
706
+ var repo = readEditor();
707
+ showSlot("name", "");
708
+ if (!repo.name) {
709
+ showSlot("name", "Name is required.");
710
+ return false;
711
+ }
712
+ if (repo.name !== editing.original && Object.prototype.hasOwnProperty.call(repoDraft, repo.name)) {
713
+ showSlot("name", "A repo named " + repo.name + " is already in the list.");
714
+ return false;
715
+ }
716
+ if (editing.original && editing.original !== repo.name) delete repoDraft[editing.original];
717
+ repoDraft[repo.name] = repo;
718
+ editing.original = repo.name;
719
+ return true;
720
+ }
721
+
722
+ function openEditor(name, repo) {
723
+ editing = { original: name };
724
+ fillEditor(name, repo);
725
+ var editor = document.getElementById("repo-editor");
726
+ if (editor) editor.hidden = false;
727
+ renderRepoList();
728
+ var nameInput = document.getElementById("repo-name");
729
+ if (nameInput && nameInput.focus) nameInput.focus();
730
+ }
731
+
732
+ function closeEditor() {
733
+ editing = null;
734
+ var editor = document.getElementById("repo-editor");
735
+ if (editor) editor.hidden = true;
736
+ renderRepoList();
737
+ }
738
+
739
+ function startEdit(name) {
740
+ if (!Object.prototype.hasOwnProperty.call(repoDraft, name)) return;
741
+ if (editing && editing.original !== name && !flushEditor()) return;
742
+ if (editing && editing.original === name) return;
743
+ openEditor(name, repoDraft[name]);
744
+ }
745
+
746
+ function startAdd() {
747
+ if (editing && !flushEditor()) return;
748
+ openEditor(null, { path: "", test: "", deliver: "local", base: "", prTitle: "", branch: "" });
749
+ }
750
+
751
+ function removeRepo(name) {
752
+ var ok = window.confirm("Remove " + name + " from the kiln config? This only edits the config. The checkout on disk is left as it is.");
753
+ if (!ok) return;
754
+ if (editing && editing.original === name) {
755
+ editing = null;
756
+ var editor = document.getElementById("repo-editor");
757
+ if (editor) editor.hidden = true;
758
+ } else if (editing && !flushEditor()) {
759
+ return;
760
+ }
761
+ delete repoDraft[name];
762
+ markSettingsDirty();
763
+ renderRepoList();
764
+ }
765
+
766
+ function optionalInt(raw) {
767
+ var text = String(raw == null ? "" : raw).trim();
768
+ if (!text) return undefined;
769
+ if (!/^-?\d+$/.test(text)) return text;
770
+ return Number(text);
771
+ }
772
+
773
+ function domainList(raw) {
774
+ return String(raw || "").split(/[\s,]+/).filter(function (part) { return part.trim(); }).map(function (part) { return part.trim(); });
775
+ }
776
+
777
+ function inputValue(id) {
778
+ var el = document.getElementById(id);
779
+ return el ? el.value : "";
780
+ }
781
+
782
+ function settingsPayload() {
783
+ var repos = {};
784
+ var names = Object.keys(repoDraft);
785
+ for (var i = 0; i < names.length; i++) {
786
+ var name = names[i];
787
+ var repo = repoDraft[name];
788
+ var entry = { path: repo.path || "" };
789
+ if (repo.test) entry.test = repo.test;
790
+ if (repo.deliver === "pr" || repo.deliver === "local") entry.deliver = repo.deliver;
791
+ if (repo.base) entry.base = repo.base;
792
+ if (repo.prTitle) entry.prTitle = repo.prTitle;
793
+ if (repo.branch) entry.branch = repo.branch;
794
+ repos[name] = entry;
795
+ }
796
+ var body = { revision: settingsDoc ? settingsDoc.revision : "", repos: repos };
797
+ var attempts = optionalInt(inputValue("max-attempts"));
798
+ var stall = optionalInt(inputValue("stall-timeout"));
799
+ var rounds = optionalInt(inputValue("max-fix-rounds"));
800
+ if (attempts !== undefined) body.maxAttempts = attempts;
801
+ if (stall !== undefined) body.stallTimeoutMs = stall;
802
+ if (rounds !== undefined) body.maxFixRounds = rounds;
803
+ var mode = selectedAttr("#sandbox-mode [data-sandbox]", "data-sandbox", "off");
804
+ var domains = domainList(inputValue("sandbox-domains"));
805
+ var sandbox = {};
806
+ if (mode) sandbox.mode = mode;
807
+ if (domains.length) sandbox.allowDomains = domains;
808
+ if (mode || domains.length) body.sandbox = sandbox;
809
+ return body;
810
+ }
811
+
812
+ function applyFieldErrors(fields) {
813
+ var keys = Object.keys(fields || {});
814
+ var lines = [];
815
+ for (var i = 0; i < keys.length; i++) {
816
+ var key = keys[i];
817
+ var message = fields[key];
818
+ lines.push(message);
819
+ var repoField = /^repos\.([^.]+)\.([^.]+)$/.exec(key);
820
+ if (repoField) {
821
+ setSettingsTab("repos");
822
+ if (Object.prototype.hasOwnProperty.call(repoDraft, repoField[1])) startEdit(repoField[1]);
823
+ showSlot(repoField[2], message);
824
+ continue;
825
+ }
826
+ if (key.indexOf("sandbox.allowDomains") === 0) {
827
+ setSettingsTab("global");
828
+ showSlot("sandbox.allowDomains", message);
829
+ continue;
830
+ }
831
+ if (key === "sandbox.mode" || key === "maxAttempts" || key === "stallTimeoutMs" || key === "maxFixRounds") {
832
+ setSettingsTab("global");
833
+ showSlot(key, message);
834
+ continue;
835
+ }
836
+ if (key === "name" || key.indexOf("repos.") === 0) {
837
+ setSettingsTab("repos");
838
+ showSlot("name", message);
839
+ }
840
+ }
841
+ if (lines.length) showSettingsError(lines.join("\n"));
842
+ }
843
+
844
+ function applySettingsDocument(data) {
845
+ settingsDoc = data;
846
+ repoDraft = cloneRepos(data.repos || {});
847
+ var pathEl = document.getElementById("settings-path");
848
+ if (pathEl) pathEl.textContent = data.path || "";
849
+ fillGlobal(data);
850
+ if (editing && editing.original && Object.prototype.hasOwnProperty.call(repoDraft, editing.original)) {
851
+ fillEditor(editing.original, repoDraft[editing.original]);
852
+ } else {
853
+ closeEditor();
854
+ }
855
+ renderRepoList();
856
+ }
857
+
858
+ function loadSettings() {
859
+ clearSettingsErrors();
860
+ var saved = document.getElementById("settings-saved");
861
+ if (saved) saved.hidden = true;
862
+ fetch("/api/settings", { cache: "no-store", headers: { Accept: "application/json" } })
863
+ .then(function (response) {
864
+ return response.json().then(function (data) { return { ok: response.ok, status: response.status, data: data }; });
865
+ })
866
+ .then(function (result) {
867
+ if (!result.ok) {
868
+ showSettingsError((result.data && result.data.error) || "Could not load settings.");
869
+ return;
870
+ }
871
+ editing = null;
872
+ var editor = document.getElementById("repo-editor");
873
+ if (editor) editor.hidden = true;
874
+ applySettingsDocument(result.data);
875
+ setSettingsTab("repos");
876
+ })
877
+ .catch(function () { showSettingsError("Could not load settings."); });
878
+ }
879
+
880
+ function saveSettings() {
881
+ if (settingsSaving) return;
882
+ if (!flushEditor()) return;
883
+ clearSettingsErrors();
884
+ var payload = settingsPayload();
885
+ settingsSaving = true;
886
+ fetch("/api/settings", {
887
+ method: "PUT",
888
+ headers: {
889
+ Accept: "application/json",
890
+ "Content-Type": "application/json",
891
+ "X-Kiln-CSRF": body.getAttribute("data-csrf") || "",
892
+ },
893
+ body: JSON.stringify(payload),
894
+ })
895
+ .then(function (response) {
896
+ return response.json().then(function (data) { return { ok: response.ok, status: response.status, data: data }; });
897
+ })
898
+ .then(function (result) {
899
+ settingsSaving = false;
900
+ if (result.status === 400) {
901
+ applyFieldErrors((result.data && result.data.fields) || {});
902
+ if (!document.getElementById("settings-error") || document.getElementById("settings-error").hidden) {
903
+ showSettingsError((result.data && result.data.error) || "Settings are invalid.");
904
+ }
905
+ return;
906
+ }
907
+ if (!result.ok) {
908
+ showSettingsError((result.data && result.data.error) || "Could not save settings.");
909
+ return;
910
+ }
911
+ applySettingsDocument(result.data);
912
+ var saved = document.getElementById("settings-saved");
913
+ if (saved) {
914
+ saved.hidden = false;
915
+ saved.textContent = "Saved";
916
+ }
917
+ })
918
+ .catch(function () {
919
+ settingsSaving = false;
920
+ showSettingsError("Could not save settings.");
921
+ });
922
+ }
923
+
924
+ function renderRepoListing(data) {
925
+ var host = document.getElementById("repo-browser");
926
+ if (!host) return;
927
+ host.hidden = false;
928
+ var html = '<div class="branch">' + esc(data.path || "") + "</div>";
929
+ if (data.parent) html += '<button type="button" class="fs-up" data-path="' + esc(data.parent) + '">Up</button>';
930
+ html += '<div class="fs-list">';
931
+ var entries = data.entries || [];
932
+ var dirs = 0;
933
+ for (var i = 0; i < entries.length; i++) {
934
+ if (entries[i].kind !== "dir") continue;
935
+ dirs += 1;
936
+ html += '<button type="button" class="fs-item" data-kind="dir" data-path="' + esc(entries[i].path) + '">' + esc(entries[i].name) + "</button>";
937
+ }
938
+ if (!dirs) html += '<div class="empty">No folders here.</div>';
939
+ html += '</div><button type="button" class="fs-use" data-path="' + esc(data.path || "") + '">Use this folder</button>';
940
+ host.innerHTML = html;
941
+ }
942
+
943
+ function loadRepoFs(path, fallback) {
944
+ var query = path ? "?path=" + encodeURIComponent(path) : "";
945
+ fetch("/api/fs" + query, { cache: "no-store", headers: { Accept: "application/json" } })
946
+ .then(function (response) {
947
+ return response.json().then(function (data) { return { ok: response.ok, data: data }; });
948
+ })
949
+ .then(function (result) {
950
+ if (!result.ok) {
951
+ if (fallback && path) {
952
+ loadRepoFs("", false);
953
+ return;
954
+ }
955
+ showSlot("path", (result.data && result.data.error) || "Could not list that directory.");
956
+ return;
957
+ }
958
+ showSlot("path", "");
959
+ if (result.data.kind === "file") {
960
+ var parent = String(result.data.path || "").replace(/\/[^/]+$/, "");
961
+ loadRepoFs(parent, false);
962
+ return;
963
+ }
964
+ renderRepoListing(result.data);
965
+ })
966
+ .catch(function () { showSlot("path", "Could not list that directory."); });
967
+ }
968
+
969
+ function openSettings() {
970
+ var sheets = document.querySelectorAll(".sheet");
971
+ for (var i = 0; i < sheets.length; i++) sheets[i].hidden = sheets[i].id !== "settings";
972
+ body.classList.add("open");
973
+ try { localStorage.removeItem("kiln-drawer"); } catch (err) { /* private mode */ }
974
+ loadSettings();
975
+ }
976
+
251
977
  document.addEventListener("click", function (event) {
252
978
  var target = event.target;
253
979
  if (!target || !target.closest) return;
980
+ if (target.closest("#open-settings")) {
981
+ openSettings();
982
+ return;
983
+ }
984
+ if (target.closest("#add-job")) {
985
+ openIntake();
986
+ return;
987
+ }
988
+ var sourceBtn = target.closest("#add-form [data-source]");
989
+ if (sourceBtn) {
990
+ setSource(sourceBtn.getAttribute("data-source") || "prompt");
991
+ return;
992
+ }
993
+ var pick = target.closest("#repo-pick .pick");
994
+ if (pick) {
995
+ var picks = document.querySelectorAll("#repo-pick .pick");
996
+ for (var p = 0; p < picks.length; p++) picks[p].setAttribute("aria-pressed", picks[p] === pick ? "true" : "false");
997
+ return;
998
+ }
999
+ var settingsTab = target.closest("#settings-form [data-settings-tab]");
1000
+ if (settingsTab) {
1001
+ setSettingsTab(settingsTab.getAttribute("data-settings-tab") || "repos");
1002
+ return;
1003
+ }
1004
+ var deliverBtn = target.closest("#repo-deliver [data-deliver]");
1005
+ if (deliverBtn) {
1006
+ setPressed("#repo-deliver [data-deliver]", "data-deliver", deliverBtn.getAttribute("data-deliver") || "local");
1007
+ markSettingsDirty();
1008
+ return;
1009
+ }
1010
+ var sandboxBtn = target.closest("#sandbox-mode [data-sandbox]");
1011
+ if (sandboxBtn) {
1012
+ setPressed("#sandbox-mode [data-sandbox]", "data-sandbox", sandboxBtn.getAttribute("data-sandbox") || "off");
1013
+ markSettingsDirty();
1014
+ return;
1015
+ }
1016
+ if (target.closest("#repo-add")) {
1017
+ startAdd();
1018
+ return;
1019
+ }
1020
+ if (target.closest("#repo-done")) {
1021
+ if (flushEditor()) closeEditor();
1022
+ return;
1023
+ }
1024
+ if (target.closest("#repo-browse")) {
1025
+ var currentPath = document.getElementById("repo-path");
1026
+ loadRepoFs(currentPath ? currentPath.value.trim() : "", true);
1027
+ return;
1028
+ }
1029
+ var editBtn = target.closest("[data-repo-edit]");
1030
+ if (editBtn) {
1031
+ startEdit(editBtn.getAttribute("data-repo-edit") || "");
1032
+ return;
1033
+ }
1034
+ var removeBtn = target.closest("[data-repo-remove]");
1035
+ if (removeBtn) {
1036
+ removeRepo(removeBtn.getAttribute("data-repo-remove") || "");
1037
+ return;
1038
+ }
1039
+ var repoFs = target.closest("#repo-browser .fs-item, #repo-browser .fs-up, #repo-browser .fs-use");
1040
+ if (repoFs) {
1041
+ var nextPath = repoFs.getAttribute("data-path") || "";
1042
+ if (repoFs.classList.contains("fs-use")) {
1043
+ var pathInput = document.getElementById("repo-path");
1044
+ if (pathInput) pathInput.value = nextPath;
1045
+ markSettingsDirty();
1046
+ var browser = document.getElementById("repo-browser");
1047
+ if (browser) browser.hidden = true;
1048
+ return;
1049
+ }
1050
+ loadRepoFs(nextPath, false);
1051
+ return;
1052
+ }
1053
+ var fsButton = target.closest(".fs-item, .fs-up, .fs-use");
1054
+ if (fsButton) {
1055
+ var next = fsButton.getAttribute("data-path") || "";
1056
+ if (fsButton.classList.contains("fs-use")) {
1057
+ ticketsDir = next;
1058
+ setPicked(next);
1059
+ return;
1060
+ }
1061
+ loadFs(next, false);
1062
+ return;
1063
+ }
254
1064
  if (target.closest("#compact")) {
255
1065
  compact = !compact;
256
1066
  try { localStorage.setItem("kiln-compact", compact ? "on" : "off"); } catch (err) { /* private mode */ }
@@ -337,6 +1147,7 @@
337
1147
  return response.json();
338
1148
  })
339
1149
  .then(function (data) {
1150
+ applyLiveConfig(data);
340
1151
  var rev = data && data.attention && data.attention.revision;
341
1152
  var current = body.getAttribute("data-revision");
342
1153
  if (rev && current && rev !== current) {
@@ -348,6 +1159,39 @@
348
1159
  .catch(function () { stamp(false); });
349
1160
  }
350
1161
 
1162
+ var promptBox = document.getElementById("prompt-text");
1163
+ if (promptBox) {
1164
+ promptBox.addEventListener("input", function () {
1165
+ if (titleDirty || sourceKind !== "prompt") return;
1166
+ var input = document.getElementById("job-title");
1167
+ if (input) input.value = titleFromPrompt(promptBox.value);
1168
+ });
1169
+ }
1170
+ var titleInput = document.getElementById("job-title");
1171
+ if (titleInput) titleInput.addEventListener("input", function () { titleDirty = true; });
1172
+ var issueInput = document.getElementById("job-issue");
1173
+ if (issueInput) issueInput.addEventListener("input", function () { issueDirty = true; });
1174
+ var jiraKey = document.getElementById("jira-key");
1175
+ if (jiraKey) jiraKey.addEventListener("input", syncIssueDefault);
1176
+ var addForm = document.getElementById("add-form");
1177
+ if (addForm) addForm.addEventListener("submit", function (event) {
1178
+ event.preventDefault();
1179
+ submitIntake(false);
1180
+ });
1181
+ var settingsForm = document.getElementById("settings-form");
1182
+ if (settingsForm) {
1183
+ settingsForm.addEventListener("submit", function (event) {
1184
+ event.preventDefault();
1185
+ saveSettings();
1186
+ });
1187
+ settingsForm.addEventListener("input", function () {
1188
+ markSettingsDirty();
1189
+ clearSettingsErrors();
1190
+ });
1191
+ }
1192
+ var previewBtn = document.getElementById("preview-job");
1193
+ if (previewBtn) previewBtn.addEventListener("click", function () { submitIntake(true); });
1194
+ showFresh();
351
1195
  renderCompact();
352
1196
  notifyLabel();
353
1197
  applyRepo();