@floless/app 0.8.0 → 0.9.1
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/dist/floless-server.cjs +125 -20
- package/dist/web/app.css +218 -0
- package/dist/web/aware.js +360 -62
- package/dist/web/index.html +20 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -72,7 +72,7 @@ var require_queue = __commonJS({
|
|
|
72
72
|
if (!(_concurrency >= 1)) {
|
|
73
73
|
throw new Error("fastqueue concurrency must be equal to or greater than 1");
|
|
74
74
|
}
|
|
75
|
-
var
|
|
75
|
+
var cache2 = reusify(Task);
|
|
76
76
|
var queueHead = null;
|
|
77
77
|
var queueTail = null;
|
|
78
78
|
var _running = 0;
|
|
@@ -151,7 +151,7 @@ var require_queue = __commonJS({
|
|
|
151
151
|
return _running === 0 && self.length() === 0;
|
|
152
152
|
}
|
|
153
153
|
function push(value, done) {
|
|
154
|
-
var current =
|
|
154
|
+
var current = cache2.get();
|
|
155
155
|
current.context = context;
|
|
156
156
|
current.release = release;
|
|
157
157
|
current.value = value;
|
|
@@ -172,7 +172,7 @@ var require_queue = __commonJS({
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
function unshift(value, done) {
|
|
175
|
-
var current =
|
|
175
|
+
var current = cache2.get();
|
|
176
176
|
current.context = context;
|
|
177
177
|
current.release = release;
|
|
178
178
|
current.value = value;
|
|
@@ -194,7 +194,7 @@ var require_queue = __commonJS({
|
|
|
194
194
|
}
|
|
195
195
|
function release(holder) {
|
|
196
196
|
if (holder) {
|
|
197
|
-
|
|
197
|
+
cache2.release(holder);
|
|
198
198
|
}
|
|
199
199
|
var next = queueHead;
|
|
200
200
|
if (next && _running <= _concurrency) {
|
|
@@ -6621,12 +6621,12 @@ var require_levels = __commonJS({
|
|
|
6621
6621
|
function genLsCache(instance) {
|
|
6622
6622
|
const formatter = instance[formattersSym].level;
|
|
6623
6623
|
const { labels } = instance.levels;
|
|
6624
|
-
const
|
|
6624
|
+
const cache2 = {};
|
|
6625
6625
|
for (const label in labels) {
|
|
6626
6626
|
const level = formatter(labels[label], Number(label));
|
|
6627
|
-
|
|
6627
|
+
cache2[label] = JSON.stringify(level).slice(0, -1);
|
|
6628
6628
|
}
|
|
6629
|
-
instance[lsCacheSym] =
|
|
6629
|
+
instance[lsCacheSym] = cache2;
|
|
6630
6630
|
return instance;
|
|
6631
6631
|
}
|
|
6632
6632
|
function isStandardLevel(level, useOnlyCustomLevels) {
|
|
@@ -25430,7 +25430,7 @@ var require_range = __commonJS({
|
|
|
25430
25430
|
parseRange(range) {
|
|
25431
25431
|
const memoOpts = (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | (this.options.loose && FLAG_LOOSE);
|
|
25432
25432
|
const memoKey = memoOpts + ":" + range;
|
|
25433
|
-
const cached =
|
|
25433
|
+
const cached = cache2.get(memoKey);
|
|
25434
25434
|
if (cached) {
|
|
25435
25435
|
return cached;
|
|
25436
25436
|
}
|
|
@@ -25464,7 +25464,7 @@ var require_range = __commonJS({
|
|
|
25464
25464
|
rangeMap.delete("");
|
|
25465
25465
|
}
|
|
25466
25466
|
const result = [...rangeMap.values()];
|
|
25467
|
-
|
|
25467
|
+
cache2.set(memoKey, result);
|
|
25468
25468
|
return result;
|
|
25469
25469
|
}
|
|
25470
25470
|
intersects(range, options) {
|
|
@@ -25503,7 +25503,7 @@ var require_range = __commonJS({
|
|
|
25503
25503
|
};
|
|
25504
25504
|
module2.exports = Range;
|
|
25505
25505
|
var LRU = require_lrucache();
|
|
25506
|
-
var
|
|
25506
|
+
var cache2 = new LRU();
|
|
25507
25507
|
var parseOptions = require_parse_options();
|
|
25508
25508
|
var Comparator = require_comparator();
|
|
25509
25509
|
var debug = require_debug2();
|
|
@@ -26445,12 +26445,12 @@ var require_plugin_utils = __commonJS({
|
|
|
26445
26445
|
if (display) {
|
|
26446
26446
|
return display;
|
|
26447
26447
|
}
|
|
26448
|
-
const
|
|
26449
|
-
if (
|
|
26450
|
-
const keys = Object.keys(
|
|
26448
|
+
const cache2 = require.cache;
|
|
26449
|
+
if (cache2) {
|
|
26450
|
+
const keys = Object.keys(cache2);
|
|
26451
26451
|
for (let i = 0; i < keys.length; i++) {
|
|
26452
26452
|
const key = keys[i];
|
|
26453
|
-
if (
|
|
26453
|
+
if (cache2[key].exports === func) {
|
|
26454
26454
|
return key;
|
|
26455
26455
|
}
|
|
26456
26456
|
}
|
|
@@ -42463,10 +42463,10 @@ var require_accept_negotiator = __commonJS({
|
|
|
42463
42463
|
}
|
|
42464
42464
|
const {
|
|
42465
42465
|
supportedValues = [],
|
|
42466
|
-
cache
|
|
42466
|
+
cache: cache2
|
|
42467
42467
|
} = options && typeof options === "object" && options || {};
|
|
42468
42468
|
this.supportedValues = supportedValues;
|
|
42469
|
-
this.cache =
|
|
42469
|
+
this.cache = cache2;
|
|
42470
42470
|
}
|
|
42471
42471
|
Negotiator.prototype.negotiate = function(header) {
|
|
42472
42472
|
if (typeof header !== "string") {
|
|
@@ -52093,6 +52093,9 @@ function mapUserStatus(u) {
|
|
|
52093
52093
|
function signInUrl() {
|
|
52094
52094
|
return `${env().webBase}/login?app=floless`;
|
|
52095
52095
|
}
|
|
52096
|
+
function webBaseUrl() {
|
|
52097
|
+
return env().webBase;
|
|
52098
|
+
}
|
|
52096
52099
|
function updateApiBase() {
|
|
52097
52100
|
return `${env().apiBase}/updates/releases`;
|
|
52098
52101
|
}
|
|
@@ -52354,7 +52357,7 @@ function appVersion() {
|
|
|
52354
52357
|
return resolveVersion({
|
|
52355
52358
|
isSea: isSea2(),
|
|
52356
52359
|
sqVersionXml: readSqVersionXml(),
|
|
52357
|
-
define: true ? "0.
|
|
52360
|
+
define: true ? "0.9.1" : void 0,
|
|
52358
52361
|
pkgVersion: readPkgVersion()
|
|
52359
52362
|
});
|
|
52360
52363
|
}
|
|
@@ -52364,7 +52367,7 @@ function resolveChannel(s) {
|
|
|
52364
52367
|
return "dev";
|
|
52365
52368
|
}
|
|
52366
52369
|
function appChannel() {
|
|
52367
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52370
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.9.1" : void 0 });
|
|
52368
52371
|
}
|
|
52369
52372
|
|
|
52370
52373
|
// oauth-presets.ts
|
|
@@ -52403,13 +52406,23 @@ function managedProfileYaml(preset) {
|
|
|
52403
52406
|
function oauthDir() {
|
|
52404
52407
|
return process.env.AWARE_HOME ? (0, import_node_path6.join)(process.env.AWARE_HOME, "oauth") : (0, import_node_path6.join)((0, import_node_os6.homedir)(), ".aware", "oauth");
|
|
52405
52408
|
}
|
|
52409
|
+
function isUpgradableLegacyProfile(existing, preset) {
|
|
52410
|
+
if (existing.startsWith(MANAGED_HEADER)) return false;
|
|
52411
|
+
const content = existing.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
52412
|
+
const only = content.length === 1 ? content[0] : void 0;
|
|
52413
|
+
if (only === void 0) return false;
|
|
52414
|
+
const m = /^client_id\s*:\s*(.+?)\s*$/.exec(only);
|
|
52415
|
+
return m !== null && m[1] === preset.clientId;
|
|
52416
|
+
}
|
|
52406
52417
|
function ensureManagedProfile(id) {
|
|
52407
52418
|
const preset = OAUTH_PRESETS[id];
|
|
52408
52419
|
if (!preset) return "not-managed";
|
|
52409
52420
|
const dir = oauthDir();
|
|
52410
52421
|
const file = (0, import_node_path6.join)(dir, `${id}.yaml`);
|
|
52411
52422
|
const existing = (0, import_node_fs7.existsSync)(file) ? (0, import_node_fs7.readFileSync)(file, "utf8") : null;
|
|
52412
|
-
if (existing !== null && !existing.startsWith(MANAGED_HEADER))
|
|
52423
|
+
if (existing !== null && !existing.startsWith(MANAGED_HEADER) && !isUpgradableLegacyProfile(existing, preset)) {
|
|
52424
|
+
return "skipped";
|
|
52425
|
+
}
|
|
52413
52426
|
const desired = managedProfileYaml(preset);
|
|
52414
52427
|
if (existing === desired) return "unchanged";
|
|
52415
52428
|
(0, import_node_fs7.mkdirSync)(dir, { recursive: true });
|
|
@@ -53924,6 +53937,87 @@ async function reportIssue(input) {
|
|
|
53924
53937
|
return { ok: true, ref };
|
|
53925
53938
|
}
|
|
53926
53939
|
|
|
53940
|
+
// release-notes.ts
|
|
53941
|
+
var FETCH_TIMEOUT_MS = 8e3;
|
|
53942
|
+
var AWARE_REPO = "aware-aeco/aware";
|
|
53943
|
+
var CHANGE_RE = /^[-*]\s+\*\*?(Added|Changed|Fixed|Removed|Security)\*\*?:\s+(.+)$/gim;
|
|
53944
|
+
function parseBulletChanges(body) {
|
|
53945
|
+
const out = [];
|
|
53946
|
+
let m;
|
|
53947
|
+
CHANGE_RE.lastIndex = 0;
|
|
53948
|
+
while ((m = CHANGE_RE.exec(body)) !== null) out.push({ type: m[1].toLowerCase(), description: m[2].trim() });
|
|
53949
|
+
return out;
|
|
53950
|
+
}
|
|
53951
|
+
var cache = /* @__PURE__ */ new Map();
|
|
53952
|
+
var CACHE_TTL_MS = 5 * 6e4;
|
|
53953
|
+
async function getJson(url, headers = {}) {
|
|
53954
|
+
try {
|
|
53955
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
53956
|
+
let json = null;
|
|
53957
|
+
try {
|
|
53958
|
+
json = await res.json();
|
|
53959
|
+
} catch {
|
|
53960
|
+
}
|
|
53961
|
+
return { status: res.status, json };
|
|
53962
|
+
} catch {
|
|
53963
|
+
return null;
|
|
53964
|
+
}
|
|
53965
|
+
}
|
|
53966
|
+
async function getAppReleaseNotes(version, deps = {}) {
|
|
53967
|
+
const useCache = !deps.changelogUrl;
|
|
53968
|
+
const key = `app:${version}`;
|
|
53969
|
+
if (useCache) {
|
|
53970
|
+
const hit = cache.get(key);
|
|
53971
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.notes;
|
|
53972
|
+
}
|
|
53973
|
+
const url = deps.changelogUrl ?? `${webBaseUrl()}/changelog.json`;
|
|
53974
|
+
const r = await getJson(url);
|
|
53975
|
+
let notes;
|
|
53976
|
+
if (!r || r.status >= 400 || !Array.isArray(r.json)) {
|
|
53977
|
+
notes = { ok: false, reason: "unavailable" };
|
|
53978
|
+
} else {
|
|
53979
|
+
const e = r.json.find(
|
|
53980
|
+
(x) => typeof x === "object" && x !== null && x.version === version
|
|
53981
|
+
);
|
|
53982
|
+
notes = e ? {
|
|
53983
|
+
ok: true,
|
|
53984
|
+
component: "app",
|
|
53985
|
+
version,
|
|
53986
|
+
title: String(e.title ?? `v${version}`),
|
|
53987
|
+
summary: String(e.description ?? ""),
|
|
53988
|
+
changes: Array.isArray(e.changes) ? e.changes.filter((c) => typeof c === "object" && c !== null).map((c) => ({ type: String(c.type ?? ""), description: String(c.description ?? "") })) : [],
|
|
53989
|
+
url: typeof e.url === "string" ? e.url : `${webBaseUrl()}/changelog#v${version}`
|
|
53990
|
+
} : { ok: false, reason: "not-found" };
|
|
53991
|
+
}
|
|
53992
|
+
if (useCache && (notes.ok || notes.reason === "not-found")) cache.set(key, { at: Date.now(), notes });
|
|
53993
|
+
return notes;
|
|
53994
|
+
}
|
|
53995
|
+
async function getAwareReleaseNotes(version, deps = {}) {
|
|
53996
|
+
const useCache = !deps.apiBase;
|
|
53997
|
+
const key = `aware:${version}`;
|
|
53998
|
+
if (useCache) {
|
|
53999
|
+
const hit = cache.get(key);
|
|
54000
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.notes;
|
|
54001
|
+
}
|
|
54002
|
+
const base = deps.apiBase ?? "https://api.github.com";
|
|
54003
|
+
const r = await getJson(`${base}/repos/${AWARE_REPO}/releases/tags/v${version}`, {
|
|
54004
|
+
Accept: "application/vnd.github+json",
|
|
54005
|
+
"User-Agent": "floless.app"
|
|
54006
|
+
});
|
|
54007
|
+
let notes;
|
|
54008
|
+
if (!r || r.status >= 400 || typeof r.json !== "object" || r.json === null) {
|
|
54009
|
+
notes = r && r.status === 404 ? { ok: false, reason: "not-found" } : { ok: false, reason: "unavailable" };
|
|
54010
|
+
} else {
|
|
54011
|
+
const rel = r.json;
|
|
54012
|
+
const body = typeof rel.body === "string" ? rel.body : "";
|
|
54013
|
+
const changes = parseBulletChanges(body);
|
|
54014
|
+
const summary = body.split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("#") && !l.startsWith("-") && !l.startsWith("*")) ?? "";
|
|
54015
|
+
notes = { ok: true, component: "aware", version, title: rel.name ?? `AWARE ${version}`, summary, changes, url: rel.html_url ?? `https://github.com/${AWARE_REPO}/releases/tag/v${version}` };
|
|
54016
|
+
}
|
|
54017
|
+
if (useCache && (notes.ok || notes.reason === "not-found")) cache.set(key, { at: Date.now(), notes });
|
|
54018
|
+
return notes;
|
|
54019
|
+
}
|
|
54020
|
+
|
|
53927
54021
|
// run-summary.ts
|
|
53928
54022
|
var clampMsg = (s) => {
|
|
53929
54023
|
const t = s.trim();
|
|
@@ -56432,7 +56526,9 @@ async function startServer() {
|
|
|
56432
56526
|
if (req.url.startsWith("/api/health") || req.url.startsWith("/api/license/") || req.url.startsWith("/api/bootstrap/") || req.url.startsWith("/api/autostart") || // Report-an-issue is a support lifeline: a user whose subscription lapsed must still be
|
|
56433
56527
|
// able to report "I got locked out". It touches no workspace data and the relay still
|
|
56434
56528
|
// requires a valid SESSION token (so it's not open abuse — see report-relay.ts).
|
|
56435
|
-
req.url.startsWith("/api/report-issue") || req.url.startsWith("/api/update") || req.url.startsWith("/api/aware/update")
|
|
56529
|
+
req.url.startsWith("/api/report-issue") || req.url.startsWith("/api/update") || req.url.startsWith("/api/aware/update") || // Release notes are a read-only display surface for the update pills — a local control,
|
|
56530
|
+
// no workspace data, never spawns `aware`. Exempt like the other update routes.
|
|
56531
|
+
req.url.startsWith("/api/release-notes")) return;
|
|
56436
56532
|
const { state: state2, signInUrl: signInUrl2 } = await getLicenseStatus();
|
|
56437
56533
|
if (state2 !== "valid" && state2 !== "offline-grace") {
|
|
56438
56534
|
return reply.status(402).send({ ok: false, error: "subscription required", state: state2, signInUrl: signInUrl2 });
|
|
@@ -56451,6 +56547,8 @@ async function startServer() {
|
|
|
56451
56547
|
ok: true,
|
|
56452
56548
|
appVersion: appVersion(),
|
|
56453
56549
|
// the installed build (sq.version), so it's scriptable
|
|
56550
|
+
webBase: webBaseUrl(),
|
|
56551
|
+
// channel-correct public site base, for the changelog deep-link
|
|
56454
56552
|
awareVersion: awareInstalledVersion() ?? bs.awareVersion,
|
|
56455
56553
|
// fresh (TTL-cached) install version; the boot snapshot is only a fallback
|
|
56456
56554
|
awareReady: bs.awareReady,
|
|
@@ -56540,6 +56638,13 @@ async function startServer() {
|
|
|
56540
56638
|
awareInstallInFlight = false;
|
|
56541
56639
|
}
|
|
56542
56640
|
});
|
|
56641
|
+
app.get("/api/release-notes", async (req, reply) => {
|
|
56642
|
+
const component = req.query.component === "aware" ? "aware" : "app";
|
|
56643
|
+
const version = typeof req.query.version === "string" ? req.query.version.trim().replace(/^v/, "") : "";
|
|
56644
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][\w.]+)?$/.test(version)) return reply.status(400).send({ ok: false, reason: "unavailable" });
|
|
56645
|
+
const notes = component === "aware" ? await getAwareReleaseNotes(version) : await getAppReleaseNotes(version);
|
|
56646
|
+
return notes;
|
|
56647
|
+
});
|
|
56543
56648
|
app.post("/api/bootstrap/retry", async () => {
|
|
56544
56649
|
const st = getBootstrapState().status;
|
|
56545
56650
|
if (st === "failed" || st === "idle") {
|
package/dist/web/app.css
CHANGED
|
@@ -2265,3 +2265,221 @@ body {
|
|
|
2265
2265
|
.req-thumb { width: 46px; height: 46px; padding: 0; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--bg); overflow: hidden; cursor: pointer; flex-shrink: 0; transition: border-color 0.15s; }
|
|
2266
2266
|
.req-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
2267
2267
|
.req-thumb:hover { border-color: var(--accent-dim); }
|
|
2268
|
+
|
|
2269
|
+
/* ============================================================================
|
|
2270
|
+
* Release notes — the click-to-open popover on the footer update pills, and the
|
|
2271
|
+
* relaunch-surviving "what's new" panel after a floless.app self-update.
|
|
2272
|
+
* Baseline tokens only (shadcn dark slate-blue): no new fonts/colours/aesthetics.
|
|
2273
|
+
* The popover's upward shadow IS the elevation cue (no decorative arrow); change
|
|
2274
|
+
* types are typeset, never coloured badges (only `.added` borrows --accent).
|
|
2275
|
+
* ==========================================================================*/
|
|
2276
|
+
|
|
2277
|
+
/* ── Popover (shared by #app-update and #aware-update) ───────────────────── */
|
|
2278
|
+
.relnotes-popover {
|
|
2279
|
+
position: fixed;
|
|
2280
|
+
width: 320px;
|
|
2281
|
+
background: var(--surface);
|
|
2282
|
+
border: 1px solid var(--border-strong);
|
|
2283
|
+
border-radius: 8px;
|
|
2284
|
+
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.55), 0 0 0 1px var(--border-strong);
|
|
2285
|
+
z-index: 1000;
|
|
2286
|
+
color: var(--text);
|
|
2287
|
+
animation: relnotes-in 0.14s ease-out;
|
|
2288
|
+
}
|
|
2289
|
+
.relnotes-popover[hidden] { display: none; }
|
|
2290
|
+
@keyframes relnotes-in {
|
|
2291
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
2292
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2293
|
+
}
|
|
2294
|
+
.relnotes-header { padding: 16px 16px 0; }
|
|
2295
|
+
.relnotes-title { font-size: 14px; font-weight: 700; color: var(--text); line-height: 1.3; }
|
|
2296
|
+
.relnotes-summary { font-size: 12px; color: var(--text-muted); margin-top: 4px; line-height: 1.45; }
|
|
2297
|
+
.relnotes-body {
|
|
2298
|
+
padding: 12px 16px;
|
|
2299
|
+
max-height: 260px;
|
|
2300
|
+
overflow-y: auto;
|
|
2301
|
+
border-top: 1px solid var(--border);
|
|
2302
|
+
border-bottom: 1px solid var(--border);
|
|
2303
|
+
}
|
|
2304
|
+
.relnotes-actions { padding: 12px 16px; display: flex; gap: 8px; align-items: center; }
|
|
2305
|
+
|
|
2306
|
+
/* Typed change bullets — REUSED verbatim by the what's-new panel (one renderer). */
|
|
2307
|
+
.relnotes-group { margin-bottom: 12px; }
|
|
2308
|
+
.relnotes-group:last-child { margin-bottom: 0; }
|
|
2309
|
+
.relnotes-type {
|
|
2310
|
+
font-family: var(--mono);
|
|
2311
|
+
font-size: 9px;
|
|
2312
|
+
text-transform: uppercase;
|
|
2313
|
+
letter-spacing: 0.16em;
|
|
2314
|
+
font-weight: 600;
|
|
2315
|
+
color: var(--text-dim);
|
|
2316
|
+
margin-bottom: 5px;
|
|
2317
|
+
}
|
|
2318
|
+
.relnotes-type.added { color: var(--accent); }
|
|
2319
|
+
.relnotes-list { list-style: none; display: flex; flex-direction: column; gap: 3px; }
|
|
2320
|
+
.relnotes-item {
|
|
2321
|
+
position: relative;
|
|
2322
|
+
font-size: 12px;
|
|
2323
|
+
color: var(--text-muted);
|
|
2324
|
+
line-height: 1.45;
|
|
2325
|
+
padding-left: 12px;
|
|
2326
|
+
}
|
|
2327
|
+
.relnotes-item::before { content: "–"; position: absolute; left: 0; color: var(--text-dim); }
|
|
2328
|
+
|
|
2329
|
+
/* Skeleton — renders instantly while notes load; the action row is live beneath it. */
|
|
2330
|
+
.relnotes-skel { display: flex; flex-direction: column; gap: 8px; }
|
|
2331
|
+
.relnotes-skel-line {
|
|
2332
|
+
height: 11px;
|
|
2333
|
+
border-radius: 3px;
|
|
2334
|
+
background: var(--surface-2);
|
|
2335
|
+
position: relative;
|
|
2336
|
+
overflow: hidden;
|
|
2337
|
+
}
|
|
2338
|
+
.relnotes-skel-line:nth-child(2) { width: 85%; }
|
|
2339
|
+
.relnotes-skel-line:nth-child(3) { width: 70%; }
|
|
2340
|
+
.relnotes-skel-line::after {
|
|
2341
|
+
content: "";
|
|
2342
|
+
position: absolute;
|
|
2343
|
+
inset: 0;
|
|
2344
|
+
transform: translateX(-100%);
|
|
2345
|
+
background: linear-gradient(90deg, transparent, var(--surface-3), transparent);
|
|
2346
|
+
animation: relnotes-shimmer 1.3s ease-in-out infinite;
|
|
2347
|
+
}
|
|
2348
|
+
@keyframes relnotes-shimmer { to { transform: translateX(100%); } }
|
|
2349
|
+
.relnotes-unavailable { font-size: 12px; color: var(--text-dim); font-style: italic; }
|
|
2350
|
+
|
|
2351
|
+
/* Action row — Update now reuses the modal .primary recipe; the changelog link is a ghost. */
|
|
2352
|
+
.relnotes-update {
|
|
2353
|
+
background: var(--accent);
|
|
2354
|
+
color: #fff;
|
|
2355
|
+
border: 1px solid var(--accent);
|
|
2356
|
+
font-weight: 600;
|
|
2357
|
+
padding: 7px 14px;
|
|
2358
|
+
border-radius: 4px;
|
|
2359
|
+
font-size: 12px;
|
|
2360
|
+
cursor: pointer;
|
|
2361
|
+
transition: background 0.15s, box-shadow 0.15s;
|
|
2362
|
+
}
|
|
2363
|
+
.relnotes-update:hover { background: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
|
|
2364
|
+
.relnotes-update[disabled] { opacity: 0.6; cursor: default; }
|
|
2365
|
+
.relnotes-update[disabled]:hover { background: var(--accent); box-shadow: none; }
|
|
2366
|
+
.relnotes-changelog {
|
|
2367
|
+
margin-left: auto;
|
|
2368
|
+
background: transparent;
|
|
2369
|
+
border: 1px solid transparent;
|
|
2370
|
+
color: var(--text-dim);
|
|
2371
|
+
padding: 7px 10px;
|
|
2372
|
+
border-radius: 4px;
|
|
2373
|
+
font-size: 12px;
|
|
2374
|
+
text-decoration: none;
|
|
2375
|
+
cursor: pointer;
|
|
2376
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
2377
|
+
}
|
|
2378
|
+
.relnotes-changelog:hover { background: var(--surface-2); border-color: var(--border-strong); color: var(--text-muted); }
|
|
2379
|
+
.relnotes-update:focus-visible,
|
|
2380
|
+
.relnotes-changelog:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2381
|
+
|
|
2382
|
+
/* The AWARE version label becomes a notes affordance after an in-place upgrade. */
|
|
2383
|
+
.aware-version.relnotes-reopen { cursor: pointer; }
|
|
2384
|
+
.aware-version.relnotes-reopen:hover { color: var(--accent-bright); }
|
|
2385
|
+
|
|
2386
|
+
/* ── What's-new panel (floless.app post-update) ─────────────────────────────
|
|
2387
|
+
A 4th grid row above the footer — NOT absolutely positioned. .has-whats-new
|
|
2388
|
+
inserts an `auto` row + a full-width `whats-new` area between the body and footer. */
|
|
2389
|
+
.app.has-whats-new {
|
|
2390
|
+
grid-template-rows: 60px 1fr auto 44px;
|
|
2391
|
+
grid-template-areas:
|
|
2392
|
+
"header header header"
|
|
2393
|
+
"chat canvas inspect"
|
|
2394
|
+
"whats-new whats-new whats-new"
|
|
2395
|
+
"footer footer footer";
|
|
2396
|
+
}
|
|
2397
|
+
.whats-new {
|
|
2398
|
+
grid-area: whats-new;
|
|
2399
|
+
background: var(--surface);
|
|
2400
|
+
border-top: 1px solid var(--border);
|
|
2401
|
+
border-bottom: 1px solid var(--border);
|
|
2402
|
+
border-left: 3px solid var(--accent);
|
|
2403
|
+
font-size: 12px;
|
|
2404
|
+
padding: 0 18px;
|
|
2405
|
+
overflow: hidden;
|
|
2406
|
+
}
|
|
2407
|
+
.whats-new[hidden] { display: none; }
|
|
2408
|
+
.whats-new-summary { display: flex; align-items: center; gap: 10px; height: 44px; }
|
|
2409
|
+
.whats-new-label {
|
|
2410
|
+
font-family: var(--mono);
|
|
2411
|
+
font-size: 10px;
|
|
2412
|
+
text-transform: uppercase;
|
|
2413
|
+
letter-spacing: 0.14em;
|
|
2414
|
+
color: var(--accent);
|
|
2415
|
+
font-weight: 600;
|
|
2416
|
+
flex-shrink: 0;
|
|
2417
|
+
}
|
|
2418
|
+
.whats-new-summary-text {
|
|
2419
|
+
font-size: 12px;
|
|
2420
|
+
color: var(--text-muted);
|
|
2421
|
+
flex: 1;
|
|
2422
|
+
overflow: hidden;
|
|
2423
|
+
text-overflow: ellipsis;
|
|
2424
|
+
white-space: nowrap;
|
|
2425
|
+
}
|
|
2426
|
+
.whats-new-toggle {
|
|
2427
|
+
flex-shrink: 0;
|
|
2428
|
+
background: transparent;
|
|
2429
|
+
border: 1px solid var(--border-strong);
|
|
2430
|
+
color: var(--text-muted);
|
|
2431
|
+
padding: 4px 10px;
|
|
2432
|
+
border-radius: 4px;
|
|
2433
|
+
font-size: 11px;
|
|
2434
|
+
cursor: pointer;
|
|
2435
|
+
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
2436
|
+
}
|
|
2437
|
+
.whats-new-toggle:hover { color: var(--accent); border-color: var(--accent-dim); }
|
|
2438
|
+
.whats-new-link {
|
|
2439
|
+
flex-shrink: 0;
|
|
2440
|
+
color: var(--text-dim);
|
|
2441
|
+
font-size: 11px;
|
|
2442
|
+
text-decoration: none;
|
|
2443
|
+
padding: 4px 6px;
|
|
2444
|
+
border-radius: 4px;
|
|
2445
|
+
transition: color 0.15s;
|
|
2446
|
+
}
|
|
2447
|
+
.whats-new-link:hover { color: var(--accent-bright); }
|
|
2448
|
+
.whats-new-dismiss {
|
|
2449
|
+
flex-shrink: 0;
|
|
2450
|
+
width: 26px;
|
|
2451
|
+
height: 26px;
|
|
2452
|
+
display: inline-flex;
|
|
2453
|
+
align-items: center;
|
|
2454
|
+
justify-content: center;
|
|
2455
|
+
padding: 0;
|
|
2456
|
+
background: transparent;
|
|
2457
|
+
border: 1px solid transparent;
|
|
2458
|
+
border-radius: 4px;
|
|
2459
|
+
color: var(--text-dim);
|
|
2460
|
+
font-size: 12px;
|
|
2461
|
+
line-height: 1;
|
|
2462
|
+
cursor: pointer;
|
|
2463
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
2464
|
+
}
|
|
2465
|
+
.whats-new-dismiss:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }
|
|
2466
|
+
.whats-new-toggle:focus-visible,
|
|
2467
|
+
.whats-new-link:focus-visible,
|
|
2468
|
+
.whats-new-dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2469
|
+
.whats-new-detail {
|
|
2470
|
+
display: grid;
|
|
2471
|
+
grid-template-rows: 0fr;
|
|
2472
|
+
transition: grid-template-rows 0.18s ease-out;
|
|
2473
|
+
overflow: hidden;
|
|
2474
|
+
}
|
|
2475
|
+
.whats-new-detail.open { grid-template-rows: 1fr; }
|
|
2476
|
+
.whats-new-detail-inner { overflow: hidden; padding-bottom: 14px; }
|
|
2477
|
+
|
|
2478
|
+
/* Reduced motion — kill the popover entry, the skeleton shimmer, and the
|
|
2479
|
+
what's-new expand transition; the surfaces still read via their static styling.
|
|
2480
|
+
Mirrors the .rtn-fired-flash / node-running reduced-motion precedents above. */
|
|
2481
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2482
|
+
.relnotes-popover { animation: none; }
|
|
2483
|
+
.relnotes-skel-line::after { animation: none; }
|
|
2484
|
+
.whats-new-detail { transition: none; }
|
|
2485
|
+
}
|
package/dist/web/aware.js
CHANGED
|
@@ -1585,11 +1585,194 @@
|
|
|
1585
1585
|
_handleMenuAction(action);
|
|
1586
1586
|
};
|
|
1587
1587
|
|
|
1588
|
+
// ── Release notes: shared state + popover ─────────────────────────────────────
|
|
1589
|
+
// The channel-correct public site base (e.g. https://floless.io), captured from
|
|
1590
|
+
// /api/health in the health poll. The changelog deep-link is omitted entirely when
|
|
1591
|
+
// this is empty — never hardcode floless.io (it would be wrong on stage/dev).
|
|
1592
|
+
let webBase = '';
|
|
1593
|
+
// The what's-new panel reveals at most once per session (guard against the 5s
|
|
1594
|
+
// health poll re-triggering it after the user expands/dismisses).
|
|
1595
|
+
let whatsNewShown = false;
|
|
1596
|
+
|
|
1597
|
+
const $notesPopover = document.getElementById('notes-popover');
|
|
1598
|
+
|
|
1599
|
+
// ONE renderer for typed change bullets — used by BOTH the popover body and the
|
|
1600
|
+
// what's-new panel detail. Groups bullets by change type (first-seen order); each
|
|
1601
|
+
// group is a typeset label + a bullet list. Only `.added` borrows --accent (CSS).
|
|
1602
|
+
function renderTypedChanges(container, changes) {
|
|
1603
|
+
if (!container) return;
|
|
1604
|
+
const groups = [];
|
|
1605
|
+
const byType = new Map();
|
|
1606
|
+
for (const c of (Array.isArray(changes) ? changes : [])) {
|
|
1607
|
+
const type = String((c && c.type) || '').trim() || 'changed';
|
|
1608
|
+
const desc = String((c && c.description) || '').trim();
|
|
1609
|
+
if (!desc) continue;
|
|
1610
|
+
if (!byType.has(type)) { byType.set(type, []); groups.push(type); }
|
|
1611
|
+
byType.get(type).push(desc);
|
|
1612
|
+
}
|
|
1613
|
+
if (!groups.length) { container.innerHTML = '<p class="relnotes-unavailable">No itemised changes</p>'; return; }
|
|
1614
|
+
container.innerHTML = groups.map((type) => {
|
|
1615
|
+
const items = byType.get(type).map((d) => `<li class="relnotes-item">${escapeHtml(d)}</li>`).join('');
|
|
1616
|
+
return `<div class="relnotes-group">`
|
|
1617
|
+
+ `<div class="relnotes-type ${escapeAttr(type)}">${escapeHtml(type)}</div>`
|
|
1618
|
+
+ `<ul class="relnotes-list">${items}</ul></div>`;
|
|
1619
|
+
}).join('');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// Open the shared release-notes popover for an update pill (or the AWARE version
|
|
1623
|
+
// label). The action row — Update now + the changelog deep-link — renders FIRST,
|
|
1624
|
+
// so "Update now" is live before (and regardless of whether) the notes arrive.
|
|
1625
|
+
// `onUpdate` is optional: the post-AWARE-upgrade label reopen passes none (read-only).
|
|
1626
|
+
let _notesEscHandler = null, _notesOutsideHandler = null, _notesOriginEl = null;
|
|
1627
|
+
function closeNotesPopover() {
|
|
1628
|
+
if (!$notesPopover || $notesPopover.hidden) return;
|
|
1629
|
+
$notesPopover.hidden = true;
|
|
1630
|
+
$notesPopover.innerHTML = '';
|
|
1631
|
+
if (_notesEscHandler) { document.removeEventListener('keydown', _notesEscHandler, true); _notesEscHandler = null; }
|
|
1632
|
+
if (_notesOutsideHandler) { document.removeEventListener('mousedown', _notesOutsideHandler, true); _notesOutsideHandler = null; }
|
|
1633
|
+
const origin = _notesOriginEl; _notesOriginEl = null;
|
|
1634
|
+
if (origin) {
|
|
1635
|
+
origin.setAttribute('aria-expanded', 'false');
|
|
1636
|
+
origin.removeAttribute('aria-controls');
|
|
1637
|
+
try { origin.focus(); } catch { /* origin may have hidden (pill → relaunch) */ }
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function openNotesPopover({ component, version, anchorEl, onUpdate }) {
|
|
1641
|
+
if (!$notesPopover || !version) return;
|
|
1642
|
+
// Clean up a prior open instance's document listeners (reopening from a different
|
|
1643
|
+
// pill) without the focus-restore — the new anchor takes over below.
|
|
1644
|
+
if (_notesEscHandler) { document.removeEventListener('keydown', _notesEscHandler, true); _notesEscHandler = null; }
|
|
1645
|
+
if (_notesOutsideHandler) { document.removeEventListener('mousedown', _notesOutsideHandler, true); _notesOutsideHandler = null; }
|
|
1646
|
+
if (_notesOriginEl && _notesOriginEl !== anchorEl) { _notesOriginEl.setAttribute('aria-expanded', 'false'); _notesOriginEl.removeAttribute('aria-controls'); }
|
|
1647
|
+
const compLabel = component === 'aware' ? 'AWARE' : 'floless.app';
|
|
1648
|
+
$notesPopover.setAttribute('aria-label', compLabel + ' v' + version + ' release notes');
|
|
1649
|
+
|
|
1650
|
+
// Build the "full notes" link, component-aware:
|
|
1651
|
+
// - app → the floless.io changelog (floless.app-only by design). Omit when webBase is empty.
|
|
1652
|
+
// - aware → the PUBLIC AWARE GitHub release. A stable URL built straight from `version`
|
|
1653
|
+
// (no webBase, no dependency on the notes fetch) — AWARE is NOT on floless.io.
|
|
1654
|
+
const changelogHtml = component === 'aware'
|
|
1655
|
+
? `<a class="relnotes-changelog" target="_blank" rel="noopener" href="${escapeAttr('https://github.com/aware-aeco/aware/releases/tag/v' + version)}">Full release notes ↗</a>`
|
|
1656
|
+
: (webBase
|
|
1657
|
+
? `<a class="relnotes-changelog" target="_blank" rel="noopener" href="${escapeAttr(webBase + '/changelog#v' + version)}">Full changelog ↗</a>`
|
|
1658
|
+
: '');
|
|
1659
|
+
const updateHtml = onUpdate ? `<button type="button" class="relnotes-update">Update now</button>` : '';
|
|
1660
|
+
|
|
1661
|
+
// Render the shell: header (skeleton title), body (skeleton bars), action row (LIVE).
|
|
1662
|
+
$notesPopover.innerHTML =
|
|
1663
|
+
`<div class="relnotes-header"><div class="relnotes-title">${compLabel} v${escapeHtml(version)}</div></div>`
|
|
1664
|
+
+ `<div class="relnotes-body" aria-busy="true">`
|
|
1665
|
+
+ `<div class="relnotes-skel"><div class="relnotes-skel-line"></div><div class="relnotes-skel-line"></div><div class="relnotes-skel-line"></div></div>`
|
|
1666
|
+
+ `</div>`
|
|
1667
|
+
+ `<div class="relnotes-actions">${updateHtml}${changelogHtml}</div>`;
|
|
1668
|
+
$notesPopover.hidden = false;
|
|
1669
|
+
|
|
1670
|
+
// Wire Update now — it never waits on the notes fetch.
|
|
1671
|
+
const $update = $notesPopover.querySelector('.relnotes-update');
|
|
1672
|
+
if ($update && onUpdate) {
|
|
1673
|
+
$update.onclick = () => { closeNotesPopover(); onUpdate(version); };
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Position relative to the anchor: hang above it (footer is at the bottom), and
|
|
1677
|
+
// clamp the right edge so a wide pill near the left can't push it off-screen.
|
|
1678
|
+
if (anchorEl) {
|
|
1679
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
1680
|
+
$notesPopover.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
|
|
1681
|
+
$notesPopover.style.right = Math.max(8, window.innerWidth - rect.right) + 'px';
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// a11y: mark the originating control expanded; move focus into the dialog.
|
|
1685
|
+
_notesOriginEl = anchorEl || null;
|
|
1686
|
+
if (anchorEl) { anchorEl.setAttribute('aria-expanded', 'true'); anchorEl.setAttribute('aria-controls', 'notes-popover'); }
|
|
1687
|
+
try { $notesPopover.focus(); } catch { /* container is tabindex=-1 */ }
|
|
1688
|
+
|
|
1689
|
+
// Tab cycles only the (up to 2) action controls; Escape closes + restores focus.
|
|
1690
|
+
const focusables = () => Array.from($notesPopover.querySelectorAll('.relnotes-update, .relnotes-changelog'));
|
|
1691
|
+
_notesEscHandler = (e) => {
|
|
1692
|
+
if (e.key === 'Escape') { e.preventDefault(); closeNotesPopover(); return; }
|
|
1693
|
+
if (e.key === 'Tab') {
|
|
1694
|
+
const f = focusables();
|
|
1695
|
+
if (!f.length) return;
|
|
1696
|
+
const first = f[0], last = f[f.length - 1];
|
|
1697
|
+
const active = document.activeElement;
|
|
1698
|
+
if (e.shiftKey && (active === first || active === $notesPopover)) { e.preventDefault(); last.focus(); }
|
|
1699
|
+
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
document.addEventListener('keydown', _notesEscHandler, true);
|
|
1703
|
+
// Click outside closes — but ignore the opening click on the anchor itself.
|
|
1704
|
+
_notesOutsideHandler = (e) => {
|
|
1705
|
+
if ($notesPopover.contains(e.target) || (anchorEl && anchorEl.contains(e.target))) return;
|
|
1706
|
+
closeNotesPopover();
|
|
1707
|
+
};
|
|
1708
|
+
document.addEventListener('mousedown', _notesOutsideHandler, true);
|
|
1709
|
+
|
|
1710
|
+
// Fetch the notes and fill the body (or the unavailable state). The action row
|
|
1711
|
+
// is untouched — Update now and the changelog link stay live throughout.
|
|
1712
|
+
(async () => {
|
|
1713
|
+
const $body = $notesPopover.querySelector('.relnotes-body');
|
|
1714
|
+
let notes = null;
|
|
1715
|
+
try {
|
|
1716
|
+
const r = await fetch('/api/release-notes?component=' + encodeURIComponent(component) + '&version=' + encodeURIComponent(version));
|
|
1717
|
+
notes = await r.json();
|
|
1718
|
+
} catch { notes = null; }
|
|
1719
|
+
// The popover may have been closed/reopened while the fetch was in flight.
|
|
1720
|
+
if ($notesPopover.hidden || !$body || !$body.isConnected) return;
|
|
1721
|
+
$body.removeAttribute('aria-busy');
|
|
1722
|
+
if (notes && notes.ok) {
|
|
1723
|
+
const title = $notesPopover.querySelector('.relnotes-title');
|
|
1724
|
+
if (title && notes.title) title.textContent = notes.title;
|
|
1725
|
+
const summaryHtml = notes.summary ? `<div class="relnotes-summary">${escapeHtml(notes.summary)}</div>` : '';
|
|
1726
|
+
const header = $notesPopover.querySelector('.relnotes-header');
|
|
1727
|
+
if (header && summaryHtml && !header.querySelector('.relnotes-summary')) header.insertAdjacentHTML('beforeend', summaryHtml);
|
|
1728
|
+
$body.innerHTML = '<div class="relnotes-changes"></div>';
|
|
1729
|
+
renderTypedChanges($body.querySelector('.relnotes-changes'), notes.changes);
|
|
1730
|
+
} else {
|
|
1731
|
+
$body.innerHTML = '<p class="relnotes-unavailable">Release notes unavailable</p>';
|
|
1732
|
+
}
|
|
1733
|
+
})();
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1588
1736
|
// ── App self-update tag ───────────────────────────────────────────────────────
|
|
1589
1737
|
// Surface updater.ts: poll GET /api/update (on load + every 6h); when a newer
|
|
1590
|
-
// installed build exists, show a footer
|
|
1591
|
-
//
|
|
1738
|
+
// installed build exists, show a footer pill → click opens the release-notes
|
|
1739
|
+
// popover → Update now → POST /api/update/apply, which downloads + relaunches into
|
|
1740
|
+
// the new version. Hidden in dev/npm (supported:false).
|
|
1592
1741
|
const $appUpdate = document.getElementById('app-update');
|
|
1742
|
+
// The apply body, extracted so the popover's "Update now" can drive it with the
|
|
1743
|
+
// polled target version (no textContent scrape). Preserves the npm-vs-desktop
|
|
1744
|
+
// branch verbatim: npm copies the command; desktop confirms → relaunch.
|
|
1745
|
+
async function applyAppUpdate(version) {
|
|
1746
|
+
if (!$appUpdate) return;
|
|
1747
|
+
// npm channel can't self-apply (no Update.exe) — copy the command for the user's terminal.
|
|
1748
|
+
if ($appUpdate.dataset.channel === 'npm') {
|
|
1749
|
+
const cmd = $appUpdate.dataset.command || 'npm i -g @floless/app@latest';
|
|
1750
|
+
// Best-effort copy — fire-and-forget so the toast never blocks on clipboard permission.
|
|
1751
|
+
if (navigator.clipboard) navigator.clipboard.writeText(cmd).catch(() => {});
|
|
1752
|
+
showToast('Update in your terminal — ' + cmd, 'info');
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
// desktop (Velopack): one-click download + relaunch
|
|
1756
|
+
const v = version || '';
|
|
1757
|
+
if (!window.confirm('Download v' + v + ' and relaunch floless.app now?')) return;
|
|
1758
|
+
// Record the pending version so the new build's first paint can reveal the
|
|
1759
|
+
// what's-new panel (survives the relaunch via localStorage; cleared once shown).
|
|
1760
|
+
try { localStorage.setItem('floless.whatsNew.pending', v); } catch { /* private mode */ }
|
|
1761
|
+
$appUpdate.disabled = true;
|
|
1762
|
+
$appUpdate.textContent = '↑ Updating…';
|
|
1763
|
+
try {
|
|
1764
|
+
const r = await fetch('/api/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' } });
|
|
1765
|
+
const d = await r.json().catch(() => ({}));
|
|
1766
|
+
if (!r.ok || !d.ok) throw new Error(d.error || 'update failed');
|
|
1767
|
+
// Success: the server is exiting + Update.exe relaunches the new build. The health
|
|
1768
|
+
// poll flips to offline (R1 overlay), then the new version reconnects on its own.
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
$appUpdate.disabled = false;
|
|
1771
|
+
try { localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ }
|
|
1772
|
+
showToast('Update failed — ' + String((e && e.message) || e).slice(0, 80), 'warn');
|
|
1773
|
+
refreshUpdate();
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1593
1776
|
async function refreshUpdate() {
|
|
1594
1777
|
if (!$appUpdate) return;
|
|
1595
1778
|
try {
|
|
@@ -1600,57 +1783,99 @@
|
|
|
1600
1783
|
$appUpdate.textContent = '↑ Update to v' + d.targetVersion;
|
|
1601
1784
|
$appUpdate.dataset.channel = d.channel || 'desktop';
|
|
1602
1785
|
$appUpdate.dataset.command = (npm && d.command) ? d.command : '';
|
|
1603
|
-
$appUpdate.dataset.
|
|
1604
|
-
|
|
1605
|
-
|
|
1786
|
+
$appUpdate.dataset.target = d.targetVersion;
|
|
1787
|
+
$appUpdate.dataset.tip = "What's new in v" + d.targetVersion;
|
|
1788
|
+
$appUpdate.setAttribute('aria-haspopup', 'dialog');
|
|
1606
1789
|
$appUpdate.hidden = false;
|
|
1607
1790
|
} else {
|
|
1608
|
-
|
|
1791
|
+
// Dev-override (ships inert): an E2E can force the pill visible deterministically
|
|
1792
|
+
// by setting localStorage['floless.dev.forceUpdate'] = {"component":"app","version":"x.y.z"}.
|
|
1793
|
+
// Read on every poll; absent by default → no behaviour change.
|
|
1794
|
+
if (!applyDevForcedUpdate('app', $appUpdate)) $appUpdate.hidden = true; // up-to-date / dev / registry-or-feed error → no tag
|
|
1609
1795
|
}
|
|
1610
|
-
} catch {
|
|
1796
|
+
} catch {
|
|
1797
|
+
if (!applyDevForcedUpdate('app', $appUpdate)) $appUpdate.hidden = true;
|
|
1798
|
+
}
|
|
1611
1799
|
}
|
|
1612
1800
|
if ($appUpdate) {
|
|
1613
|
-
$appUpdate.onclick =
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
const cmd = $appUpdate.dataset.command || 'npm i -g @floless/app@latest';
|
|
1617
|
-
// Best-effort copy — fire-and-forget so the toast never blocks on clipboard permission.
|
|
1618
|
-
if (navigator.clipboard) navigator.clipboard.writeText(cmd).catch(() => {});
|
|
1619
|
-
showToast('Update in your terminal — ' + cmd, 'info');
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
// desktop (Velopack): one-click download + relaunch
|
|
1623
|
-
const v = $appUpdate.textContent.replace(/^[^0-9]*/, ''); // "↑ Update to v0.5.2" → "0.5.2"
|
|
1624
|
-
if (!window.confirm('Download v' + v + ' and relaunch floless.app now?')) return;
|
|
1625
|
-
$appUpdate.disabled = true;
|
|
1626
|
-
$appUpdate.textContent = '↑ Updating…';
|
|
1627
|
-
try {
|
|
1628
|
-
const r = await fetch('/api/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' } });
|
|
1629
|
-
const d = await r.json().catch(() => ({}));
|
|
1630
|
-
if (!r.ok || !d.ok) throw new Error(d.error || 'update failed');
|
|
1631
|
-
// Success: the server is exiting + Update.exe relaunches the new build. The health
|
|
1632
|
-
// poll flips to offline (R1 overlay), then the new version reconnects on its own.
|
|
1633
|
-
} catch (e) {
|
|
1634
|
-
$appUpdate.disabled = false;
|
|
1635
|
-
showToast('Update failed — ' + String((e && e.message) || e).slice(0, 80), 'warn');
|
|
1636
|
-
refreshUpdate();
|
|
1637
|
-
}
|
|
1801
|
+
$appUpdate.onclick = () => {
|
|
1802
|
+
const version = $appUpdate.dataset.target || '';
|
|
1803
|
+
openNotesPopover({ component: 'app', version, anchorEl: $appUpdate, onUpdate: applyAppUpdate });
|
|
1638
1804
|
};
|
|
1639
1805
|
refreshUpdate();
|
|
1640
1806
|
setInterval(refreshUpdate, 6 * 60 * 60 * 1000); // re-check every 6h so a long-running window notices
|
|
1641
1807
|
}
|
|
1642
1808
|
|
|
1809
|
+
// Dev-override helper — the ONLY forced-visible path. Reveals a pill with a fixed
|
|
1810
|
+
// version when localStorage['floless.dev.forceUpdate'] matches the component, so an
|
|
1811
|
+
// E2E can exercise the popover without a real pending update. Returns true if forced.
|
|
1812
|
+
function applyDevForcedUpdate(component, pillEl) {
|
|
1813
|
+
if (!pillEl) return false;
|
|
1814
|
+
let forced = null;
|
|
1815
|
+
try { forced = JSON.parse(localStorage.getItem('floless.dev.forceUpdate') || 'null'); } catch { forced = null; }
|
|
1816
|
+
if (!forced || forced.component !== component || !forced.version) return false;
|
|
1817
|
+
const v = String(forced.version);
|
|
1818
|
+
pillEl.dataset.target = v;
|
|
1819
|
+
pillEl.dataset.tip = "What's new in v" + v;
|
|
1820
|
+
pillEl.setAttribute('aria-haspopup', 'dialog');
|
|
1821
|
+
if (component === 'aware') { pillEl.textContent = '↑ Upgrade AWARE to v' + v; }
|
|
1822
|
+
else { pillEl.textContent = '↑ Update to v' + v; pillEl.dataset.channel = pillEl.dataset.channel || 'desktop'; }
|
|
1823
|
+
pillEl.hidden = false;
|
|
1824
|
+
return true;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1643
1827
|
// ── AWARE runtime upgrade tag ─────────────────────────────────────────────────
|
|
1644
1828
|
// Surface aware-update.ts: poll GET /api/aware/update (on load + every 6h); when a
|
|
1645
|
-
// newer @aware-aeco/cli exists, show a footer pill →
|
|
1646
|
-
// reinstalls AWARE in place (no relaunch —
|
|
1647
|
-
// version live. Mirrors the app self-update
|
|
1648
|
-
// (the live version re-stamp IS the
|
|
1829
|
+
// newer @aware-aeco/cli exists, show a footer pill → click opens the release-notes
|
|
1830
|
+
// popover → Update now → POST apply, which reinstalls AWARE in place (no relaunch —
|
|
1831
|
+
// the app stays open) and re-stamps the version live. Mirrors the app self-update
|
|
1832
|
+
// pill; differs in that success is silent (the live version re-stamp IS the
|
|
1833
|
+
// confirmation) and there is no relaunch.
|
|
1649
1834
|
const $awareUpdate = document.getElementById('aware-update');
|
|
1650
1835
|
// After a successful upgrade, briefly trust the upgraded version: a /api/health request that
|
|
1651
1836
|
// was in flight before the reinstall finished could resolve with the OLD version and momentarily
|
|
1652
1837
|
// revert the footer. The live re-stamp (health poll) honors this short settle window.
|
|
1653
1838
|
let awareUpgradeFloor = null, awareUpgradeFloorUntil = 0;
|
|
1839
|
+
// Extracted apply body — driven by the popover's "Update now" with the polled
|
|
1840
|
+
// target version. Preserves the awareUpgradeFloor settle window + the live
|
|
1841
|
+
// #aware-version re-stamp verbatim; additionally makes the version label a notes
|
|
1842
|
+
// affordance and shows a brief "installed" confirmation before the pill hides.
|
|
1843
|
+
async function applyAwareUpdate(version) {
|
|
1844
|
+
if (!$awareUpdate) return;
|
|
1845
|
+
const v = version || '';
|
|
1846
|
+
if (!window.confirm('Upgrade AWARE runtime to v' + v + '? This reinstalls the npm package in place — the app stays open and the version restamps automatically.')) return;
|
|
1847
|
+
$awareUpdate.disabled = true;
|
|
1848
|
+
$awareUpdate.textContent = '↑ Upgrading…';
|
|
1849
|
+
try {
|
|
1850
|
+
// Cap the wait so the pill never sticks disabled if the global npm install wedges.
|
|
1851
|
+
const r = await fetch('/api/aware/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, signal: AbortSignal.timeout(120000) });
|
|
1852
|
+
const d = await r.json().catch(() => ({}));
|
|
1853
|
+
if (!r.ok || !d.ok) throw new Error(d.error || 'aware upgrade failed');
|
|
1854
|
+
// Success is silent: the pill disappears and the AWARE version re-stamps live.
|
|
1855
|
+
$awareUpdate.disabled = false;
|
|
1856
|
+
const wv = document.getElementById('aware-version');
|
|
1857
|
+
if (wv && d.version) {
|
|
1858
|
+
wv.textContent = 'AWARE ' + d.version;
|
|
1859
|
+
awareUpgradeFloor = d.version; awareUpgradeFloorUntil = Date.now() + 10000;
|
|
1860
|
+
// The version label becomes a notes affordance — click to reopen what changed.
|
|
1861
|
+
wv.classList.add('relnotes-reopen');
|
|
1862
|
+
wv.setAttribute('role', 'button');
|
|
1863
|
+
wv.setAttribute('tabindex', '0');
|
|
1864
|
+
wv.dataset.tip = "What's new in v" + d.version + ' — click for notes';
|
|
1865
|
+
wv.dataset.notesVersion = d.version;
|
|
1866
|
+
}
|
|
1867
|
+
// Brief confirmation (~2s) before the pill hides.
|
|
1868
|
+
$awareUpdate.textContent = 'AWARE v' + (d.version || v) + ' installed';
|
|
1869
|
+
setTimeout(() => { $awareUpdate.hidden = true; refreshAwareUpdate(); }, 2000);
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
$awareUpdate.disabled = false;
|
|
1872
|
+
const msg = (e && e.name === 'TimeoutError')
|
|
1873
|
+
? 'AWARE upgrade is taking a while — it may still be installing; the version updates when it finishes'
|
|
1874
|
+
: 'AWARE upgrade failed — ' + String((e && e.message) || e).slice(0, 80);
|
|
1875
|
+
showToast(msg, 'warn');
|
|
1876
|
+
refreshAwareUpdate(); // restore the pill text for retry
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1654
1879
|
async function refreshAwareUpdate() {
|
|
1655
1880
|
if (!$awareUpdate) return;
|
|
1656
1881
|
try {
|
|
@@ -1659,42 +1884,109 @@
|
|
|
1659
1884
|
if (r.ok && d.updateAvailable && d.targetVersion) {
|
|
1660
1885
|
$awareUpdate.textContent = '↑ Upgrade AWARE to v' + d.targetVersion;
|
|
1661
1886
|
$awareUpdate.dataset.target = d.targetVersion;
|
|
1662
|
-
$awareUpdate.dataset.tip = '
|
|
1887
|
+
$awareUpdate.dataset.tip = "What's new in v" + d.targetVersion;
|
|
1888
|
+
$awareUpdate.setAttribute('aria-haspopup', 'dialog');
|
|
1663
1889
|
$awareUpdate.hidden = false;
|
|
1664
1890
|
} else {
|
|
1665
|
-
|
|
1891
|
+
// Dev-override (ships inert) — see applyDevForcedUpdate. Read on every poll.
|
|
1892
|
+
if (!applyDevForcedUpdate('aware', $awareUpdate)) $awareUpdate.hidden = true; // up-to-date / absent / registry error → no pill
|
|
1666
1893
|
}
|
|
1667
|
-
} catch {
|
|
1894
|
+
} catch {
|
|
1895
|
+
if (!applyDevForcedUpdate('aware', $awareUpdate)) $awareUpdate.hidden = true;
|
|
1896
|
+
}
|
|
1668
1897
|
}
|
|
1669
1898
|
if ($awareUpdate) {
|
|
1670
|
-
$awareUpdate.onclick =
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
$awareUpdate.disabled = true;
|
|
1674
|
-
$awareUpdate.textContent = '↑ Upgrading…';
|
|
1675
|
-
try {
|
|
1676
|
-
// Cap the wait so the pill never sticks disabled if the global npm install wedges.
|
|
1677
|
-
const r = await fetch('/api/aware/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, signal: AbortSignal.timeout(120000) });
|
|
1678
|
-
const d = await r.json().catch(() => ({}));
|
|
1679
|
-
if (!r.ok || !d.ok) throw new Error(d.error || 'aware upgrade failed');
|
|
1680
|
-
// Success is silent: the pill disappears and the AWARE version re-stamps live.
|
|
1681
|
-
$awareUpdate.hidden = true;
|
|
1682
|
-
$awareUpdate.disabled = false;
|
|
1683
|
-
const wv = document.getElementById('aware-version');
|
|
1684
|
-
if (wv && d.version) { wv.textContent = 'AWARE ' + d.version; awareUpgradeFloor = d.version; awareUpgradeFloorUntil = Date.now() + 10000; }
|
|
1685
|
-
} catch (e) {
|
|
1686
|
-
$awareUpdate.disabled = false;
|
|
1687
|
-
const msg = (e && e.name === 'TimeoutError')
|
|
1688
|
-
? 'AWARE upgrade is taking a while — it may still be installing; the version updates when it finishes'
|
|
1689
|
-
: 'AWARE upgrade failed — ' + String((e && e.message) || e).slice(0, 80);
|
|
1690
|
-
showToast(msg, 'warn');
|
|
1691
|
-
refreshAwareUpdate(); // restore the pill text for retry
|
|
1692
|
-
}
|
|
1899
|
+
$awareUpdate.onclick = () => {
|
|
1900
|
+
const version = $awareUpdate.dataset.target || '';
|
|
1901
|
+
openNotesPopover({ component: 'aware', version, anchorEl: $awareUpdate, onUpdate: applyAwareUpdate });
|
|
1693
1902
|
};
|
|
1694
1903
|
refreshAwareUpdate();
|
|
1695
1904
|
setInterval(refreshAwareUpdate, 6 * 60 * 60 * 1000); // re-check every 6h
|
|
1696
1905
|
}
|
|
1697
1906
|
|
|
1907
|
+
// The AWARE version label, once it's a notes affordance (post in-place upgrade),
|
|
1908
|
+
// reopens the popover on click or Enter/Space.
|
|
1909
|
+
{
|
|
1910
|
+
const $awareVersion = document.getElementById('aware-version');
|
|
1911
|
+
if ($awareVersion) {
|
|
1912
|
+
const reopen = () => {
|
|
1913
|
+
const v = $awareVersion.dataset.notesVersion;
|
|
1914
|
+
if (v) openNotesPopover({ component: 'aware', version: v, anchorEl: $awareVersion });
|
|
1915
|
+
};
|
|
1916
|
+
$awareVersion.addEventListener('click', () => { if ($awareVersion.classList.contains('relnotes-reopen')) reopen(); });
|
|
1917
|
+
$awareVersion.addEventListener('keydown', (e) => {
|
|
1918
|
+
if (!$awareVersion.classList.contains('relnotes-reopen')) return;
|
|
1919
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); reopen(); }
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// ── What's-new panel (floless.app post-update, relaunch-surviving) ────────────
|
|
1925
|
+
// Called from the health poll AFTER #app-version is stamped (so the version
|
|
1926
|
+
// compare has a real value). Reveals once: if a pending self-update version was
|
|
1927
|
+
// recorded before the relaunch AND the now-running build matches it AND the user
|
|
1928
|
+
// hasn't already dismissed that version, fetch its notes and show the panel.
|
|
1929
|
+
function maybeShowWhatsNew() {
|
|
1930
|
+
if (whatsNewShown) return;
|
|
1931
|
+
let pending, seen;
|
|
1932
|
+
try { pending = localStorage.getItem('floless.whatsNew.pending'); seen = localStorage.getItem('floless.whatsNew.seen'); } catch { return; }
|
|
1933
|
+
if (!pending) return;
|
|
1934
|
+
const appVer = (document.getElementById('app-version')?.textContent || '').replace(/[^0-9.]/g, '');
|
|
1935
|
+
if (appVer !== pending || seen === pending) { try { localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ } return; }
|
|
1936
|
+
|
|
1937
|
+
const panel = document.getElementById('whats-new-panel');
|
|
1938
|
+
if (!panel) return;
|
|
1939
|
+
whatsNewShown = true; // guard: reveal at most once per session
|
|
1940
|
+
|
|
1941
|
+
const $summaryText = panel.querySelector('.whats-new-summary-text');
|
|
1942
|
+
const $toggle = panel.querySelector('.whats-new-toggle');
|
|
1943
|
+
const $detail = panel.querySelector('.whats-new-detail');
|
|
1944
|
+
const $detailInner = panel.querySelector('.whats-new-detail-inner');
|
|
1945
|
+
const $link = panel.querySelector('.whats-new-link');
|
|
1946
|
+
const $dismiss = panel.querySelector('.whats-new-dismiss');
|
|
1947
|
+
|
|
1948
|
+
// Changelog deep-link — channel-correct base only; omit (hide) when unknown.
|
|
1949
|
+
if ($link) {
|
|
1950
|
+
if (webBase) { $link.href = webBase + '/changelog#v' + pending; $link.hidden = false; }
|
|
1951
|
+
else { $link.removeAttribute('href'); $link.hidden = true; }
|
|
1952
|
+
}
|
|
1953
|
+
if ($summaryText) $summaryText.textContent = 'floless.app v' + pending + ' is now running.';
|
|
1954
|
+
|
|
1955
|
+
// Toggle expands/collapses the detail (grid-rows transition); label + aria flip.
|
|
1956
|
+
if ($toggle && $detail) {
|
|
1957
|
+
$toggle.onclick = () => {
|
|
1958
|
+
const open = $detail.classList.toggle('open');
|
|
1959
|
+
$toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1960
|
+
$toggle.textContent = open ? 'Hide' : "What's new";
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
// Dismiss — no confirm; persist the seen version so it never re-reveals.
|
|
1964
|
+
if ($dismiss) {
|
|
1965
|
+
$dismiss.onclick = () => {
|
|
1966
|
+
try { localStorage.setItem('floless.whatsNew.seen', pending); localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ }
|
|
1967
|
+
panel.hidden = true;
|
|
1968
|
+
document.getElementById('app')?.classList.remove('has-whats-new');
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Reveal the panel (adds the 4th grid row), then fill the detail with shared bullets.
|
|
1973
|
+
document.getElementById('app')?.classList.add('has-whats-new');
|
|
1974
|
+
panel.hidden = false;
|
|
1975
|
+
(async () => {
|
|
1976
|
+
let notes = null;
|
|
1977
|
+
try {
|
|
1978
|
+
const r = await fetch('/api/release-notes?component=app&version=' + encodeURIComponent(pending));
|
|
1979
|
+
notes = await r.json();
|
|
1980
|
+
} catch { notes = null; }
|
|
1981
|
+
if (notes && notes.ok) {
|
|
1982
|
+
if ($summaryText && notes.summary) $summaryText.textContent = notes.summary;
|
|
1983
|
+
renderTypedChanges($detailInner, notes.changes);
|
|
1984
|
+
} else if ($detailInner) {
|
|
1985
|
+
$detailInner.innerHTML = '<p class="relnotes-unavailable">Release notes unavailable</p>';
|
|
1986
|
+
}
|
|
1987
|
+
})();
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1698
1990
|
// ONE Run (the approved single-Run model). "▶ Run workflow" does a REAL run against
|
|
1699
1991
|
// the live host, using the app inputs. If the app has a report node, it drives
|
|
1700
1992
|
// the in-app HTML Viewer (renders + caches the returned HTML); otherwise it
|
|
@@ -2245,8 +2537,14 @@
|
|
|
2245
2537
|
// version CAN change (an out-of-band `npm i -g`, or the in-app upgrade), so
|
|
2246
2538
|
// re-stamp it whenever /api/health reports a different value — never show a
|
|
2247
2539
|
// stale runtime version in the footer.
|
|
2540
|
+
// Capture the channel-correct public site base for the changelog deep-links
|
|
2541
|
+
// (popover + what's-new). Empty until /api/health reports it → links omitted.
|
|
2542
|
+
if (h && h.webBase) webBase = h.webBase;
|
|
2248
2543
|
const av = document.getElementById('app-version');
|
|
2249
2544
|
if (av && h && h.appVersion && !shownVersion) { av.textContent = 'v' + h.appVersion; shownVersion = true; }
|
|
2545
|
+
// After the build version is stamped, reveal the relaunch-surviving what's-new
|
|
2546
|
+
// panel iff this is the build we just self-updated into (guarded to once).
|
|
2547
|
+
maybeShowWhatsNew();
|
|
2250
2548
|
const wv = document.getElementById('aware-version');
|
|
2251
2549
|
if (wv && h && h.awareVersion) {
|
|
2252
2550
|
const next = 'AWARE ' + h.awareVersion;
|
package/dist/web/index.html
CHANGED
|
@@ -133,6 +133,21 @@
|
|
|
133
133
|
<div class="inspect-body" id="inspect-body"></div>
|
|
134
134
|
</aside>
|
|
135
135
|
|
|
136
|
+
<!-- What's-new panel — a relaunch-surviving, calm "what changed" strip shown
|
|
137
|
+
once after a floless.app self-update. A direct grid child of .app (sibling
|
|
138
|
+
to header/body/footer); occupies the extra `whats-new` row that .app.has-whats-new
|
|
139
|
+
inserts above the footer. Dismiss persists in localStorage so it never nags. -->
|
|
140
|
+
<div id="whats-new-panel" class="whats-new" hidden>
|
|
141
|
+
<div class="whats-new-summary">
|
|
142
|
+
<span class="whats-new-label">Updated</span>
|
|
143
|
+
<span class="whats-new-summary-text"></span>
|
|
144
|
+
<button class="whats-new-toggle" type="button" aria-expanded="false">What's new</button>
|
|
145
|
+
<a class="whats-new-link" target="_blank" rel="noopener">Full changelog ↗</a>
|
|
146
|
+
<button class="whats-new-dismiss" type="button" aria-label="Dismiss">✕</button>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="whats-new-detail"><div class="whats-new-detail-inner"></div></div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
136
151
|
<footer>
|
|
137
152
|
<div class="status">
|
|
138
153
|
<span class="stat"><span class="dot"></span><span class="stat-val">runtime online</span></span>
|
|
@@ -155,6 +170,11 @@
|
|
|
155
170
|
</footer>
|
|
156
171
|
</div>
|
|
157
172
|
|
|
173
|
+
<!-- Single reusable release-notes popover — anchored to the clicked update pill
|
|
174
|
+
(or the AWARE version label) at open time. Shared by #app-update and
|
|
175
|
+
#aware-update; its body is rendered per-open in aware.js. -->
|
|
176
|
+
<div id="notes-popover" class="relnotes-popover" role="dialog" aria-modal="true" aria-label="Release notes" tabindex="-1" hidden></div>
|
|
177
|
+
|
|
158
178
|
<div class="menu" id="menu" role="menu">
|
|
159
179
|
<button class="menu-item" data-action="open" role="menuitem">
|
|
160
180
|
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg></span>
|