diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7df4ff8..013901f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,26 @@
+## 2.2.0
+
+### Improvement: show the file path under the name in the IDE explorer
+
+Pages and scripts in the IDE file tree now show their **path** (`s3key`) in a muted second line under the file name. Same-named files (common for pages) are now distinguishable at a glance without having to open each one. The full path is also available as a hover tooltip.
+
+### Feature: "Republish all pages" + per-page republish result (KYTE-#181)
+
+Pairs with kyte-php v4.8.1, which makes the republish hook fault-isolated and returns a `republish_summary`. On the app **configuration page**:
+
+- A **"Republish all pages"** button (on the Authentication Mode card) — an explicit recovery action that re-stamps and re-deploys every published page with the current Kyte Connect code, then reports the result. Useful when an auto-republish half-completed.
+- The **per-page result is now surfaced**: the auth-mode flip / kyte-connect update and the new button all read `republish_summary` and show how many pages succeeded/failed (failures are listed and logged to the console) — instead of a blind "it worked" toast.
+
+### Feature: Publish action in the IDE (KYTE-#189)
+
+The in-app IDE could only Save (which persists content); there was no way to publish from it. Added a **Publish** action for publishable file types (**pages** and **scripts** — the ones that deploy to S3):
+
+- A green **rocket** button in the IDE tab bar, shown only when the active file is a page or script.
+- **Ctrl+Shift+S** keyboard shortcut (Save remains Ctrl+S); added to the welcome-screen shortcut list.
+- Publishing sends `state=1` alongside the content, so the backend runs its normal publish path (page → `publishPage` → S3 + CloudFront; script → `handleScriptPublication` → S3) and creates a version with the change summary. Unlike Save, Publish is allowed even when the file isn't dirty (re-deploy current content), and it persists any pending edits in the same request.
+
+Pairs with kyte-php v4.8.1 (`block_layout` partial-save guard). The "IDE save didn't persist" issue from #189 was already fixed by kyte-php v4.8.0.
+
## 2.1.0
### Change: remove JavaScript obfuscation (pairs with kyte-php v4.7.0 — KYTE-#191)
diff --git a/app/configuration.html b/app/configuration.html
index e257593..ff4d15a 100644
--- a/app/configuration.html
+++ b/app/configuration.html
@@ -376,8 +376,12 @@
JWT Bearer (Recommended — rotating tokens)
-
-
+
+
+
+
+ Ctrl+Shift+S
+ Publish current file (pages & scripts)
+
Ctrl+WClose current tab
diff --git a/assets/css/kyte-ide.css b/assets/css/kyte-ide.css
index 74c3602..f544608 100644
--- a/assets/css/kyte-ide.css
+++ b/assets/css/kyte-ide.css
@@ -261,6 +261,23 @@ body, html {
text-overflow: ellipsis;
}
+/* Stacked name + path (pages & scripts) so same-named files are distinguishable */
+.tree-item .file-meta {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.tree-item .file-path {
+ font-size: 0.68rem;
+ color: #6a6a6a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.tree-item .dirty-indicator {
margin-left: auto;
width: 8px;
diff --git a/assets/js/source/kyte-shipyard-application-configuration.js b/assets/js/source/kyte-shipyard-application-configuration.js
index 2c9d3e8..89e8465 100644
--- a/assets/js/source/kyte-shipyard-application-configuration.js
+++ b/assets/js/source/kyte-shipyard-application-configuration.js
@@ -250,6 +250,12 @@ function setupFormEventHandlers(_ks, idx) {
e.preventDefault();
saveAuthModeSettings(_ks, idx);
});
+
+ // Republish all pages handler (KYTE-#181)
+ $("#republishAllPages").click(function(e) {
+ e.preventDefault();
+ republishAllPages(_ks, idx);
+ });
}
// Handle user model change
@@ -633,13 +639,21 @@ function updateKyteConnectCode(_ks) {
// Update local app object
app.kyte_connect = systemKyteCode;
app.kyte_connect_obfuscated = '';
-
+
// Update current code and refresh comparison
currentKyteCode = systemKyteCode;
displayKyteCodes();
compareKyteCodes();
-
- showSuccess('Kyte Connect code updated successfully!');
+
+ // Surface the per-page republish result (KYTE-#181)
+ const formatted = formatRepublishSummary(r.data[0] && r.data[0].republish_summary);
+ if (formatted && !formatted.ok) {
+ showError('Kyte Connect updated, but some pages failed to republish. ' + formatted.message);
+ } else if (formatted) {
+ showSuccess('Kyte Connect code updated. ' + formatted.message);
+ } else {
+ showSuccess('Kyte Connect code updated successfully!');
+ }
} else {
showError('Failed to update Kyte Connect code. Please try again.');
}
@@ -653,6 +667,46 @@ function updateKyteConnectCode(_ks) {
});
}
+// Format the backend's republish_summary (KYTE-#181) into a user-facing result.
+// Returns { ok: bool, message: string } or null when no summary is present.
+function formatRepublishSummary(summary) {
+ if (!summary) return null;
+ const ok = summary.succeeded || 0;
+ const failed = summary.failed || 0;
+ const total = ok + failed;
+ if (failed === 0) {
+ return { ok: true, message: `Republished ${ok} page${ok === 1 ? '' : 's'}.` };
+ }
+ console.warn('Republish failures:', summary.failures);
+ const detail = (summary.failures || [])
+ .map(f => `page ${f.page}${f.s3key ? ' (' + f.s3key + ')' : ''}`)
+ .join(', ');
+ return { ok: false, message: `Republished ${ok} of ${total} pages — ${failed} failed: ${detail}. See console for details.` };
+}
+
+// Explicitly re-stamp and re-deploy every published page with the CURRENT Kyte
+// Connect code, and report a per-page result. Recovery action for a half-completed
+// auto-republish. See KYTE-#181.
+function republishAllPages(_ks, idx) {
+ const btn = $("#republishAllPages");
+ const original = btn.html();
+ btn.html('Republishing...').prop('disabled', true);
+
+ _ks.put('Application', 'id', idx, { 'republish_kyte_connect': 1 }, null, [], function(r) {
+ const summary = (r.data && r.data[0]) ? r.data[0].republish_summary : null;
+ const formatted = formatRepublishSummary(summary);
+ if (formatted && !formatted.ok) {
+ showError(formatted.message);
+ } else {
+ showSuccess(formatted ? formatted.message : 'All pages republished.');
+ }
+ btn.html(original).prop('disabled', false);
+ }, function(err) {
+ showError('Republish failed: ' + err);
+ btn.html(original).prop('disabled', false);
+ });
+}
+
// Show detailed diff in a modal
function showDetailedDiff() {
const modal = document.createElement('div');
diff --git a/assets/js/source/kyte-shipyard-ide.js b/assets/js/source/kyte-shipyard-ide.js
index 8a74719..a5ff9c0 100644
--- a/assets/js/source/kyte-shipyard-ide.js
+++ b/assets/js/source/kyte-shipyard-ide.js
@@ -65,6 +65,7 @@ const FILE_TYPES = {
iconClass: 'html',
sectionIcon: 'fas fa-sitemap',
model: 'KytePage',
+ publishable: true, // can be published to S3 (state=1 → publishPage)
loadModel: 'KytePageData', // Special loader: uses KytePageData to get content
loadField: 'page', // KytePageData is keyed by 'page' field
listField: 'site',
@@ -119,6 +120,7 @@ const FILE_TYPES = {
iconClass: 'js',
sectionIcon: 'fas fa-scroll',
model: 'KyteScript',
+ publishable: true, // can be published to S3 (state=1 → handleScriptPublication)
listField: 'site',
parts: [
{ key: 'content', label: 'JavaScript', language: 'javascript', field: 'content' }
@@ -405,6 +407,7 @@ function renderPagesSection() {
html += renderGroup(site.name || 'Unnamed Site', 'fas fa-globe', site._pages.map(page => ({
fileId: generateFileId(FILE_TYPES.PAGE, page.id),
name: FILE_TYPES.PAGE.displayName(page),
+ path: page.s3key || '',
iconClass: FILE_TYPES.PAGE.iconClass,
icon: FILE_TYPES.PAGE.icon
})));
@@ -437,6 +440,7 @@ function renderScriptsSection() {
return {
fileId: generateFileId(FILE_TYPES.SCRIPT, script.id),
name: FILE_TYPES.SCRIPT.displayName(script),
+ path: script.s3key || '',
iconClass: isCss ? 'css' : 'js',
icon: isCss ? 'fab fa-css3-alt' : 'fab fa-js'
};
@@ -473,9 +477,12 @@ function renderGroup(label, icon, items) {