@silicaclaw/cli 2026.3.19-6 → 2026.3.19-7

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.
@@ -0,0 +1,325 @@
1
+ export function createOverviewController({
2
+ ago,
3
+ api,
4
+ describeCurrentMode,
5
+ escapeHtml,
6
+ shortId,
7
+ t,
8
+ writeUiCache,
9
+ }) {
10
+ function renderOverviewGuide(overview, profile) {
11
+ const hasDisplayName = Boolean(String(profile?.display_name || overview?.display_name || "").trim());
12
+ const hasBio = Boolean(String(profile?.bio || "").trim());
13
+ const hasTags = Array.isArray(profile?.tags) && profile.tags.length > 0;
14
+ const profileComplete = hasDisplayName || hasBio || hasTags;
15
+ const publicEnabled = Boolean(profile?.public_enabled ?? overview?.public_enabled);
16
+ const announcedRecently = Boolean(overview?.last_broadcast_at);
17
+
18
+ const guideStatusEl = document.getElementById("overviewGuideStatus");
19
+ const profileStepEl = document.getElementById("overviewStepProfile");
20
+ const publicStepEl = document.getElementById("overviewStepPublic");
21
+ const broadcastStepEl = document.getElementById("overviewStepBroadcast");
22
+
23
+ profileStepEl.classList.toggle("is-done", profileComplete);
24
+ publicStepEl.classList.toggle("is-done", publicEnabled);
25
+ broadcastStepEl.classList.toggle("is-done", announcedRecently);
26
+
27
+ document.getElementById("overviewStepProfileStatus").textContent = profileComplete
28
+ ? t("overview.stepDone")
29
+ : t("overview.stepIncomplete");
30
+ document.getElementById("overviewStepProfileBody").textContent = profileComplete
31
+ ? t("overview.stepProfileDone")
32
+ : t("overview.stepProfileBody");
33
+
34
+ document.getElementById("overviewStepPublicStatus").textContent = publicEnabled
35
+ ? t("overview.stepDone")
36
+ : t("overview.stepIncomplete");
37
+ document.getElementById("overviewStepPublicBody").textContent = publicEnabled
38
+ ? t("overview.stepPublicDone")
39
+ : t("overview.stepPublicBody");
40
+
41
+ document.getElementById("overviewStepBroadcastStatus").textContent = announcedRecently
42
+ ? t("overview.stepDone")
43
+ : t("overview.stepWaiting");
44
+ document.getElementById("overviewStepBroadcastBody").textContent = announcedRecently
45
+ ? t("overview.stepBroadcastDone")
46
+ : t("overview.stepBroadcastBody");
47
+
48
+ if (publicEnabled && announcedRecently) {
49
+ guideStatusEl.textContent = t("overview.guideLive");
50
+ guideStatusEl.className = "pill ok";
51
+ } else if (profileComplete && publicEnabled) {
52
+ guideStatusEl.textContent = t("overview.guideReadyToAnnounce");
53
+ guideStatusEl.className = "pill warn";
54
+ } else {
55
+ guideStatusEl.textContent = t("overview.guideNeedSetup");
56
+ guideStatusEl.className = "pill warn";
57
+ }
58
+ }
59
+
60
+ async function refreshOverview({
61
+ getAgentsPage,
62
+ getOnlyShowOnline,
63
+ onPageChange,
64
+ setOverviewMode,
65
+ setVisibleRemotePublicCount,
66
+ }) {
67
+ const [overviewRes, discoveredRes, peerRes, profileRes, bridgeRes, networkCfgRes, networkStatsRes] = await Promise.allSettled([
68
+ api("/api/overview"),
69
+ api("/api/search?q="),
70
+ api("/api/peers"),
71
+ api("/api/profile"),
72
+ api("/api/openclaw/bridge"),
73
+ api("/api/network/config"),
74
+ api("/api/network/stats"),
75
+ ]);
76
+ if (overviewRes.status !== "fulfilled") {
77
+ throw overviewRes.reason;
78
+ }
79
+ const o = overviewRes.value.data;
80
+ const allProfiles = discoveredRes.status === "fulfilled" ? discoveredRes.value.data || [] : [];
81
+ const peers = peerRes.status === "fulfilled" ? peerRes.value.data || {} : {};
82
+ const currentProfile = profileRes.status === "fulfilled" ? profileRes.value.data || {} : {};
83
+ const bridge = bridgeRes.status === "fulfilled" ? bridgeRes.value.data || {} : {};
84
+ const networkCfg = networkCfgRes.status === "fulfilled" ? networkCfgRes.value.data || {} : {};
85
+ const networkStats = networkStatsRes.status === "fulfilled" ? networkStatsRes.value.data || {} : {};
86
+ const peerItems = peers.items || [];
87
+ const mergedById = new Map();
88
+
89
+ for (const agent of allProfiles) mergedById.set(agent.agent_id, agent);
90
+
91
+ for (const peer of peerItems) {
92
+ const existing = mergedById.get(peer.peer_id);
93
+ if (existing) {
94
+ if (peer.status && !existing.online) existing.online = peer.status === "online";
95
+ if (peer.last_seen_at && (!existing.updated_at || peer.last_seen_at > existing.updated_at)) {
96
+ existing.updated_at = peer.last_seen_at;
97
+ }
98
+ continue;
99
+ }
100
+ mergedById.set(peer.peer_id, {
101
+ agent_id: peer.peer_id,
102
+ display_name: shortId(peer.peer_id),
103
+ online: peer.status === "online",
104
+ updated_at: peer.last_seen_at || peer.first_seen_at || 0,
105
+ });
106
+ }
107
+
108
+ const all = Array.from(mergedById.values());
109
+ const filtered = getOnlyShowOnline() ? all.filter((agent) => agent.online) : all;
110
+ setVisibleRemotePublicCount(all.filter((agent) => !agent.is_self && agent.online).length);
111
+ const totalAgentPages = Math.max(1, Math.ceil(filtered.length / 10));
112
+ let agentsPage = Math.min(Math.max(1, getAgentsPage()), totalAgentPages);
113
+ onPageChange(agentsPage);
114
+ const pagedAgents = filtered.slice((agentsPage - 1) * 10, agentsPage * 10);
115
+ const discoveredCount = Math.max(Number(o.discovered_count || 0), Number(peers.total || 0), all.length);
116
+ const onlineCount = Math.max(Number(o.online_count || 0), Number(peers.online || 0), all.filter((agent) => agent.online).length);
117
+ const offlineCount = Math.max(0, discoveredCount - onlineCount);
118
+
119
+ const overviewCardsHtml = [
120
+ [t("overview.discovered"), discoveredCount],
121
+ [t("overview.online"), onlineCount],
122
+ [t("overview.offline"), offlineCount],
123
+ [t("overview.presenceTtl"), `${Math.floor(o.presence_ttl_ms / 1000)}s`],
124
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value">${v}</div></div>`).join("");
125
+ document.getElementById("overviewCards").innerHTML = overviewCardsHtml;
126
+
127
+ const brandVersionText = o.app_version ? `v${o.app_version}` : "-";
128
+ document.getElementById("brandVersion").textContent = brandVersionText;
129
+
130
+ const snapshotHtml = `
131
+ <div class="snapshot-card">
132
+ <div class="snapshot-card__identity">
133
+ <div class="snapshot-card__label">Current Node</div>
134
+ <div class="snapshot-card__title">${escapeHtml(o.display_name || t("overview.unnamed"))}</div>
135
+ <div class="snapshot-card__subtle mono">${escapeHtml(o.agent_id || "-")}</div>
136
+ </div>
137
+ <div class="snapshot-card__grid">
138
+ <div class="snapshot-card__item"><div class="label">Version</div><span class="value-inline">${escapeHtml(o.app_version || "-")}</span></div>
139
+ <div class="snapshot-card__item"><div class="label">Public</div><span class="value-inline">${o.public_enabled ? t("common.on") : t("common.off")}</span></div>
140
+ <div class="snapshot-card__item"><div class="label">Broadcast</div><span class="value-inline">${o.broadcast_enabled ? t("common.on") : t("common.off")}</span></div>
141
+ <div class="snapshot-card__item"><div class="label">Last Broadcast</div><span class="value-inline">${escapeHtml(ago(o.last_broadcast_at))}</span></div>
142
+ </div>
143
+ </div>
144
+ `;
145
+ document.getElementById("snapshot").innerHTML = snapshotHtml;
146
+ const heroModeText = o.social?.network_mode || "-";
147
+ setOverviewMode(heroModeText);
148
+ document.getElementById("heroMode").textContent = heroModeText;
149
+ document.getElementById("overviewModeHint").textContent = t("overview.modeCurrentSource", {
150
+ mode: describeCurrentMode(t, heroModeText),
151
+ hint: t("overview.modeCacheHint"),
152
+ });
153
+
154
+ const pillBroadcastText = o.broadcast_enabled ? t("overview.pillRunning") : t("overview.pillPaused");
155
+ const pillBroadcastClassName = `pill ${o.broadcast_enabled ? "ok" : "warn"}`;
156
+ document.getElementById("pillBroadcast").textContent = pillBroadcastText;
157
+ document.getElementById("pillBroadcast").className = pillBroadcastClassName;
158
+
159
+ const openclawRunning = !!bridge.openclaw_runtime?.running;
160
+ const openclawDetected = !!bridge.openclaw_installation?.detected || openclawRunning || !!bridge.openclaw_runtime?.gateway_reachable;
161
+ const skillInstalled = !!bridge.skill_learning?.installed;
162
+ const globalMode = heroModeText === "global-preview";
163
+ const networkDiag = networkStats.adapter_diagnostics_summary || {};
164
+ const lastNetworkError = String(networkDiag.last_error || o.last_broadcast_error || "").trim();
165
+ const broadcastHealthy = o.broadcast_enabled && !lastNetworkError;
166
+ const roleKey = openclawRunning
167
+ ? "overview.homeRoleListener"
168
+ : openclawDetected
169
+ ? "overview.homeRoleStandby"
170
+ : "overview.homeRoleBroadcaster";
171
+ const titleKey = openclawRunning
172
+ ? "overview.homeTitleListener"
173
+ : openclawDetected
174
+ ? "overview.homeTitleOffline"
175
+ : "overview.homeTitleBroadcaster";
176
+ const bodyKey = openclawRunning
177
+ ? "overview.homeBodyListener"
178
+ : openclawDetected
179
+ ? "overview.homeBodyOffline"
180
+ : "overview.homeBodyBroadcaster";
181
+
182
+ document.getElementById("homeMissionTitle").textContent = t(titleKey);
183
+ document.getElementById("homeMissionBody").textContent = t(bodyKey);
184
+ document.getElementById("homeMissionStatus").innerHTML = [
185
+ `<span class="mission-status-pill"><strong>${t("overview.homeRole")}</strong> ${t(roleKey)}</span>`,
186
+ `<span class="mission-status-pill"><strong>${t("overview.homeOpenClaw")}</strong> ${openclawRunning ? t("overview.homeRunning") : openclawDetected ? t("overview.homeInstalledOnly") : t("overview.homeStopped")}</span>`,
187
+ `<span class="mission-status-pill"><strong>${t("overview.homeGlobalMode")}</strong> ${globalMode ? t("overview.homeGlobalReady") : t("overview.homeNotGlobal")}</span>`,
188
+ ].join("");
189
+ document.getElementById("homePriorityGrid").innerHTML = [
190
+ [t("overview.homeOpenClaw"), openclawRunning ? t("overview.homeRunning") : openclawDetected ? t("overview.homeInstalledOnly") : t("overview.homeStopped"), openclawRunning ? t("overview.homeMetaRunning") : t("overview.homeMetaNotRunning")],
191
+ [t("overview.homeGlobalMode"), globalMode ? t("overview.homeGlobalReady") : t("overview.homeNotGlobal"), globalMode ? t("overview.homeMetaGlobal") : t("overview.homeMetaNotGlobal")],
192
+ [t("overview.homeBroadcastHealth"), broadcastHealthy ? t("overview.homeHealthy") : t("overview.homeDegraded"), lastNetworkError || `Last broadcast ${ago(o.last_broadcast_at)}`],
193
+ [t("overview.homePeers"), String(all.filter((agent) => !agent.is_self && agent.online).length), t("overview.homeMetaPeers", { online: String(onlineCount), discovered: String(discoveredCount) })],
194
+ ].map(([label, value, meta]) => `
195
+ <div class="priority-card">
196
+ <div class="priority-card__label">${label}</div>
197
+ <div class="priority-card__value">${escapeHtml(value)}</div>
198
+ <div class="priority-card__meta">${escapeHtml(meta)}</div>
199
+ </div>
200
+ `).join("");
201
+ document.getElementById("homeBriefList").innerHTML = [
202
+ [t("overview.homeBriefNetwork"), t("overview.homeBriefNetworkValue", {
203
+ mode: describeCurrentMode(t, heroModeText),
204
+ relay: String(networkDiag.signaling_url || networkCfg.adapter_extra?.signaling_url || "-"),
205
+ room: String(networkDiag.room || networkCfg.adapter_extra?.room || "-"),
206
+ })],
207
+ [t("overview.homeBriefBridge"), t("overview.homeBriefBridgeValue", {
208
+ runtime: openclawRunning ? t("overview.homeRunning") : openclawDetected ? t("overview.homeInstalledOnly") : t("overview.homeStopped"),
209
+ skill: skillInstalled ? t("common.yes") : t("common.no"),
210
+ })],
211
+ [t("overview.homeBriefNextAction"), !broadcastHealthy ? t("overview.homeBriefActionStabilize") : openclawRunning ? t("overview.homeBriefActionBroadcast") : t("overview.homeBriefActionLearn")],
212
+ ].map(([label, value]) => `
213
+ <div class="home-brief__item">
214
+ <div class="home-brief__label">${label}</div>
215
+ <div class="home-brief__value">${escapeHtml(value)}</div>
216
+ </div>
217
+ `).join("");
218
+
219
+ const init = o.init_state || {};
220
+ const onboarding = o.onboarding || {};
221
+ const notice = document.getElementById("initNotice");
222
+ const discoveryHint = document.getElementById("publicDiscoveryHint");
223
+ if (onboarding.first_run || init.identity_auto_created || init.profile_auto_created || init.social_auto_created) {
224
+ notice.classList.add("show");
225
+ notice.textContent = t("overview.onboardingNotice", {
226
+ mode: onboarding.mode || "-",
227
+ discoverable: onboarding.discoverable ? t("common.yes") : t("common.no"),
228
+ });
229
+ } else {
230
+ notice.classList.remove("show");
231
+ }
232
+ discoveryHint.style.display = onboarding.can_enable_public_discovery || onboarding.public_enabled ? "block" : "none";
233
+ renderOverviewGuide(o, currentProfile);
234
+
235
+ if (!filtered.length) {
236
+ const agentsCountHintText = t("overview.agentsZero");
237
+ const agentsWrapHtml = `<div class="label">${t("overview.noDiscoveredAgents")}</div>`;
238
+ document.getElementById("agentsCountHint").textContent = agentsCountHintText;
239
+ document.getElementById("agentsWrap").innerHTML = agentsWrapHtml;
240
+ writeUiCache("silicaclaw_ui_overview", {
241
+ overviewCardsHtml,
242
+ brandVersionText,
243
+ snapshotText: snapshotHtml,
244
+ heroModeText,
245
+ pillBroadcastText,
246
+ pillBroadcastClassName,
247
+ agentsCountHintText,
248
+ agentsWrapHtml,
249
+ });
250
+ return;
251
+ }
252
+
253
+ const agentsCountHintText = getOnlyShowOnline()
254
+ ? t("overview.agentsOnlineFilter", { shown: String(filtered.length), total: String(all.length) })
255
+ : t("overview.agentsDiscovered", { count: String(filtered.length) });
256
+ const renderAvatar = (agent) => {
257
+ const avatar = String(agent.avatar_url || "").trim();
258
+ if (avatar) {
259
+ return `<img class="agent-card__avatar" src="${avatar}" alt="${escapeHtml(agent.display_name || "agent")}" loading="lazy" />`;
260
+ }
261
+ const label = String(agent.display_name || agent.agent_id || "?").trim();
262
+ const initial = escapeHtml((label[0] || "?").toUpperCase());
263
+ return `<div class="agent-card__avatar-fallback">${initial}</div>`;
264
+ };
265
+ const renderTags = (agent) => {
266
+ const tags = Array.isArray(agent.tags) ? agent.tags.slice(0, 4) : [];
267
+ if (!tags.length) return "";
268
+ return `<div class="agent-card__tags">${tags.map((tag) => `<span class="tag-chip">${escapeHtml(String(tag))}</span>`).join("")}</div>`;
269
+ };
270
+ const agentsWrapHtml = `
271
+ <div class="agent-list">
272
+ ${pagedAgents.map((a) => `
273
+ <div class="agent-card">
274
+ ${renderAvatar(a)}
275
+ <div class="agent-card__main">
276
+ <div class="agent-card__row">
277
+ <div class="agent-card__name">${escapeHtml(a.display_name || t("overview.unnamed"))}</div>
278
+ <div class="agent-card__id mono">${shortId(a.agent_id)}</div>
279
+ </div>
280
+ <div class="agent-card__bio">${escapeHtml(a.bio || t("preview.noBioYet"))}</div>
281
+ ${renderTags(a)}
282
+ </div>
283
+ <div class="${a.online ? "online" : "offline"}">${a.online ? t("overview.online") : t("overview.offline")}</div>
284
+ <div class="agent-card__meta"><div class="agent-card__updated">${ago(a.updated_at)}</div></div>
285
+ </div>
286
+ `).join("")}
287
+ <div class="agent-list__footer">
288
+ <div class="agent-list__page">${t("overview.pageStatus", { page: String(agentsPage), total: String(totalAgentPages) })}</div>
289
+ <div class="agent-list__pager">
290
+ <button class="secondary" type="button" id="agentsPrevPageBtn" ${agentsPage <= 1 ? "disabled" : ""}>${t("overview.prevPage")}</button>
291
+ <button class="secondary" type="button" id="agentsNextPageBtn" ${agentsPage >= totalAgentPages ? "disabled" : ""}>${t("overview.nextPage")}</button>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ `;
296
+ document.getElementById("agentsCountHint").textContent = agentsCountHintText;
297
+ document.getElementById("agentsWrap").innerHTML = agentsWrapHtml;
298
+ document.getElementById("agentsPrevPageBtn")?.addEventListener("click", async () => {
299
+ if (agentsPage <= 1) return;
300
+ agentsPage -= 1;
301
+ onPageChange(agentsPage);
302
+ await refreshOverview({ getAgentsPage, getOnlyShowOnline, onPageChange, setOverviewMode, setVisibleRemotePublicCount });
303
+ });
304
+ document.getElementById("agentsNextPageBtn")?.addEventListener("click", async () => {
305
+ if (agentsPage >= totalAgentPages) return;
306
+ agentsPage += 1;
307
+ onPageChange(agentsPage);
308
+ await refreshOverview({ getAgentsPage, getOnlyShowOnline, onPageChange, setOverviewMode, setVisibleRemotePublicCount });
309
+ });
310
+ writeUiCache("silicaclaw_ui_overview", {
311
+ overviewCardsHtml,
312
+ brandVersionText,
313
+ snapshotText: snapshotHtml,
314
+ heroModeText,
315
+ pillBroadcastText,
316
+ pillBroadcastClassName,
317
+ agentsCountHintText,
318
+ agentsWrapHtml,
319
+ });
320
+ }
321
+
322
+ return {
323
+ refreshOverview,
324
+ };
325
+ }
@@ -0,0 +1,234 @@
1
+ export function createProfileController({
2
+ api,
3
+ field,
4
+ normalizeTagsInput,
5
+ parseTags,
6
+ setFeedback,
7
+ setProfileNextStepVisible,
8
+ t,
9
+ toast,
10
+ toPrettyJson,
11
+ }) {
12
+ const state = {
13
+ baseline: "",
14
+ dirty: false,
15
+ saving: false,
16
+ };
17
+
18
+ function profileSnapshot(form) {
19
+ const displayNameEl = field(form, "display_name");
20
+ const bioEl = field(form, "bio");
21
+ const avatarEl = field(form, "avatar_url");
22
+ const tagsEl = field(form, "tags");
23
+ const publicEl = field(form, "public_enabled");
24
+ return JSON.stringify({
25
+ display_name: String(displayNameEl?.value || "").trim(),
26
+ bio: String(bioEl?.value || "").trim(),
27
+ avatar_url: String(avatarEl?.value || "").trim(),
28
+ tags: parseTags(tagsEl?.value || ""),
29
+ public_enabled: !!publicEl?.checked,
30
+ });
31
+ }
32
+
33
+ function updateDirtyState(form, fromUserInput = false) {
34
+ const now = profileSnapshot(form);
35
+ state.dirty = now !== state.baseline;
36
+ if (fromUserInput && !state.saving) {
37
+ setFeedback(
38
+ "profileFeedback",
39
+ state.dirty ? t("feedback.unsavedChanges") : t("feedback.allChangesSaved"),
40
+ state.dirty ? "warn" : "info",
41
+ );
42
+ }
43
+ }
44
+
45
+ function setProfileBaseline(form) {
46
+ state.baseline = profileSnapshot(form);
47
+ state.dirty = false;
48
+ }
49
+
50
+ function setInputError(inputEl, errorElId, message) {
51
+ const errEl = document.getElementById(errorElId);
52
+ errEl.textContent = message || "";
53
+ if (inputEl) {
54
+ inputEl.classList.toggle("input-invalid", Boolean(message));
55
+ }
56
+ }
57
+
58
+ function validateProfileForm(form) {
59
+ const displayNameEl = field(form, "display_name");
60
+ const avatarEl = field(form, "avatar_url");
61
+ const tagsEl = field(form, "tags");
62
+ const displayName = String(displayNameEl?.value || "").trim();
63
+ const avatarUrl = String(avatarEl?.value || "").trim();
64
+ const tags = parseTags(tagsEl?.value || "");
65
+
66
+ let ok = true;
67
+ let err = "";
68
+ if (displayName.length > 0 && displayName.length < 2) {
69
+ err = t("validation.displayNameShort");
70
+ ok = false;
71
+ } else if (displayName.length > 48) {
72
+ err = t("validation.displayNameLong");
73
+ ok = false;
74
+ }
75
+ setInputError(displayNameEl, "errDisplayName", err);
76
+
77
+ err = "";
78
+ if (avatarUrl && !/^https?:\/\//i.test(avatarUrl)) {
79
+ err = t("validation.avatarProtocol");
80
+ ok = false;
81
+ }
82
+ setInputError(avatarEl, "errAvatarUrl", err);
83
+
84
+ err = "";
85
+ if (tags.length > 8) {
86
+ err = t("validation.tooManyTags");
87
+ ok = false;
88
+ } else if (tags.some((tag) => tag.length > 20)) {
89
+ err = t("validation.tagTooLong");
90
+ ok = false;
91
+ }
92
+ setInputError(tagsEl, "errTags", err);
93
+ return { ok, tags };
94
+ }
95
+
96
+ function renderProfilePreview(form) {
97
+ const displayName = String(field(form, "display_name")?.value || "").trim();
98
+ const bio = String(field(form, "bio")?.value || "").trim();
99
+ const tags = parseTags(field(form, "tags")?.value || "");
100
+ const enabled = !!field(form, "public_enabled")?.checked;
101
+
102
+ document.getElementById("previewName").textContent = displayName || t("preview.unnamedAgent");
103
+ document.getElementById("previewBio").textContent = bio || t("preview.noBioYet");
104
+ document.getElementById("previewPublish").textContent = enabled ? t("preview.publishPublicState") : t("preview.publishPrivateState");
105
+ document.getElementById("previewPublishHint").textContent = enabled ? t("hints.previewPublic") : t("hints.previewPrivate");
106
+ document.getElementById("publishLaunchStatus").textContent = enabled ? t("hints.publishPublic") : t("hints.publishPrivate");
107
+ document.getElementById("publishLaunchCard").classList.toggle("is-public", enabled);
108
+ document.getElementById("bioCount").textContent = String(bio.length);
109
+
110
+ const tagBox = document.getElementById("previewTags");
111
+ if (!tags.length) {
112
+ tagBox.innerHTML = `<span class="tag-chip muted">${t("preview.noTags")}</span>`;
113
+ return;
114
+ }
115
+ tagBox.innerHTML = tags.map((tag) => `<span class="tag-chip">${tag}</span>`).join("");
116
+ }
117
+
118
+ function setSaveBusy(busy) {
119
+ state.saving = busy;
120
+ const btn = document.getElementById("saveProfileBtn");
121
+ btn.disabled = busy;
122
+ btn.classList.toggle("save-busy", busy);
123
+ btn.textContent = busy ? t("common.saving") : t("actions.saveProfile");
124
+ }
125
+
126
+ async function refreshPublicProfilePreview() {
127
+ const summary = (await api("/api/public-profile/preview")).data;
128
+ document.getElementById("publicProfilePreviewWrap").textContent = toPrettyJson(summary || null);
129
+ const visibleFields = summary?.public_visibility?.visible_fields || [];
130
+ const hiddenFields = summary?.public_visibility?.hidden_fields || [];
131
+ const visible = visibleFields.join(", ") || "-";
132
+ const hidden = hiddenFields.join(", ") || "-";
133
+ document.getElementById("publicVisibilityHint").textContent = t("preview.visibleFields", { visible, hidden });
134
+ const rows = [
135
+ ...visibleFields.map((item) => t("preview.visible", { field: item })),
136
+ ...hiddenFields.map((item) => t("preview.hidden", { field: item })),
137
+ ];
138
+ document.getElementById("publicVisibilityList").textContent = rows.length ? rows.join("\n") : "-";
139
+ }
140
+
141
+ async function refreshProfile() {
142
+ const profile = (await api("/api/profile")).data;
143
+ if (!profile) return;
144
+ const form = document.getElementById("profileForm");
145
+ field(form, "display_name").value = profile.display_name || "";
146
+ field(form, "bio").value = profile.bio || "";
147
+ field(form, "tags").value = (profile.tags || []).join(", ");
148
+ field(form, "avatar_url").value = profile.avatar_url || "";
149
+ field(form, "public_enabled").checked = !!profile.public_enabled;
150
+ form.dataset.lastSavedPublicEnabled = String(!!profile.public_enabled);
151
+ renderProfilePreview(form);
152
+ setProfileBaseline(form);
153
+ setProfileNextStepVisible(false);
154
+ await refreshPublicProfilePreview();
155
+ }
156
+
157
+ function bindProfileForm({ refreshAll }) {
158
+ document.getElementById("profileForm").addEventListener("submit", async (event) => {
159
+ event.preventDefault();
160
+ const form = event.currentTarget;
161
+ const result = validateProfileForm(form);
162
+ if (!result.ok) {
163
+ setFeedback("profileFeedback", t("feedback.fixValidation"), "warn");
164
+ return;
165
+ }
166
+
167
+ const tags = result.tags;
168
+ const wasPublicEnabled = String(form.dataset.lastSavedPublicEnabled || "false") === "true";
169
+ const willBePublicEnabled = !!field(form, "public_enabled").checked;
170
+ setFeedback("profileFeedback", t("feedback.savingProfile"));
171
+ setSaveBusy(true);
172
+ try {
173
+ const response = await api("/api/profile", {
174
+ method: "PUT",
175
+ body: JSON.stringify({
176
+ display_name: field(form, "display_name").value,
177
+ bio: field(form, "bio").value,
178
+ tags,
179
+ avatar_url: field(form, "avatar_url").value,
180
+ public_enabled: !!field(form, "public_enabled").checked,
181
+ }),
182
+ });
183
+ setFeedback("profileFeedback", response.meta?.message || t("common.saved"));
184
+ toast(t("feedback.profileSaved"));
185
+ field(form, "tags").value = normalizeTagsInput(field(form, "tags").value);
186
+ form.dataset.lastSavedPublicEnabled = String(willBePublicEnabled);
187
+ await refreshAll();
188
+ setProfileNextStepVisible(!wasPublicEnabled && willBePublicEnabled);
189
+ } catch (error) {
190
+ setFeedback("profileFeedback", error instanceof Error ? error.message : t("feedback.failed"), "error");
191
+ } finally {
192
+ setSaveBusy(false);
193
+ }
194
+ });
195
+
196
+ document.getElementById("refreshProfileBtn").addEventListener("click", async () => {
197
+ await refreshProfile();
198
+ toast(t("feedback.profileReloaded"));
199
+ });
200
+
201
+ const profileFormEl = document.getElementById("profileForm");
202
+ ["input", "change"].forEach((evt) => {
203
+ profileFormEl.addEventListener(evt, () => {
204
+ renderProfilePreview(profileFormEl);
205
+ validateProfileForm(profileFormEl);
206
+ updateDirtyState(profileFormEl, true);
207
+ });
208
+ });
209
+
210
+ const tagsInputEl = field(profileFormEl, "tags");
211
+ tagsInputEl.addEventListener("keydown", (event) => {
212
+ if (event.key !== "Enter") return;
213
+ event.preventDefault();
214
+ tagsInputEl.value = normalizeTagsInput(tagsInputEl.value);
215
+ renderProfilePreview(profileFormEl);
216
+ validateProfileForm(profileFormEl);
217
+ updateDirtyState(profileFormEl, true);
218
+ });
219
+ tagsInputEl.addEventListener("blur", () => {
220
+ tagsInputEl.value = normalizeTagsInput(tagsInputEl.value);
221
+ renderProfilePreview(profileFormEl);
222
+ validateProfileForm(profileFormEl);
223
+ updateDirtyState(profileFormEl, true);
224
+ });
225
+ }
226
+
227
+ return {
228
+ bindProfileForm,
229
+ isDirty: () => state.dirty,
230
+ isSaving: () => state.saving,
231
+ refreshProfile,
232
+ refreshPublicProfilePreview,
233
+ };
234
+ }