scenri 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/tonygorb/Scenri/compare/v0.12.0...v0.13.0) (2026-09-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * **studio:** rebuild transient alerts as a quiet toast stack ([7b44141](https://github.com/tonygorb/Scenri/commit/7b44141b57cc6748942d97bc6acc27a022260207))
9
+ * **studio:** turn first-use on, with a tutor that can go back ([df6b799](https://github.com/tonygorb/Scenri/commit/df6b799708d65cf31130233dabe2fd0ee1095a28))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **cli:** an install that has made a shot is not offered the first-use welcome ([4dae292](https://github.com/tonygorb/Scenri/commit/4dae292f3e833180448b2310a81573b641408bcc))
15
+ * **create:** keep the feed still when a shot archives or the rail toggles ([b3276cd](https://github.com/tonygorb/Scenri/commit/b3276cd1f68315455e8b29d7dfdf13c768a0438a))
16
+ * **studio:** a lesson on its first step has not begun, and Learn cannot take the app down ([cf0540b](https://github.com/tonygorb/Scenri/commit/cf0540b1ecd1e8fbdde2e3d203538f9e13f46d8c))
17
+ * **studio:** a lesson owns its progress, and only the screen is shared ([8c58bdb](https://github.com/tonygorb/Scenri/commit/8c58bdbc06124d2f9fda0775a41f84fa156e10ea))
18
+ * **studio:** a library lesson keeps its button in sight on narrow screens ([93869f4](https://github.com/tonygorb/Scenri/commit/93869f4f1f1fa8791c15ff893ef8ac536deda721))
19
+ * **studio:** a step scrolled out of sight is not gone ([49ceccb](https://github.com/tonygorb/Scenri/commit/49ceccb8d7dc41daf8327ecefb8353f98ed94e70))
20
+ * **studio:** alerts stand above a docked composer instead of over its buttons ([b6756c7](https://github.com/tonygorb/Scenri/commit/b6756c79c63419ca2ed146fc2e3c6d868eeb6f79))
21
+ * **studio:** Learn gives the keyboard back to whichever control opened it ([e9c5945](https://github.com/tonygorb/Scenri/commit/e9c5945d60e6ceb55704da1130fe23a988c6044f))
22
+ * **studio:** no empty alert on every page, and a held toast stays held ([3305f75](https://github.com/tonygorb/Scenri/commit/3305f7542fb38f604c9b4087c59aa7cfe0d4cf6b))
23
+ * **studio:** refining teaches how to find a shot, instead of picking one for you ([228938f](https://github.com/tonygorb/Scenri/commit/228938f2440e17b48d9bab836e7c4aeeb0a74cef))
24
+
25
+ ## [0.12.0](https://github.com/tonygorb/Scenri/compare/v0.11.1...v0.12.0) (2026-09-20)
26
+
27
+
28
+ ### Features
29
+
30
+ * archive a selection, and make the selection dock a toolbar ([a2697a2](https://github.com/tonygorb/Scenri/commit/a2697a26201314185e609125d04a2bf6391c4e6e))
31
+ * **create:** archive a whole selection from the dock ([d5c2181](https://github.com/tonygorb/Scenri/commit/d5c2181210f0f92b1df528b0113a773a049e916d))
32
+ * **studio:** make the selection dock a toolbar ([362112e](https://github.com/tonygorb/Scenri/commit/362112e9d1bc2be6bf171f0b0702e61ca8845720))
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * ask the browser whether the page is still here ([2800f8d](https://github.com/tonygorb/Scenri/commit/2800f8d72feb294b7a54da090242ecb2a66e6929))
38
+ * **brand:** a deleted brand does not reopen its dialogs on the next one ([dbe69eb](https://github.com/tonygorb/Scenri/commit/dbe69eb2e6679dc068c426721a0a0ebbfd39c3ca))
39
+ * **brand:** brand saves, renames, deletes and shot wipes reach the screen ([020c905](https://github.com/tonygorb/Scenri/commit/020c905c6700d8e909cef025d437bc7d4566b663))
40
+ * **create:** a shot restored from the archive comes back to the feed ([131d132](https://github.com/tonygorb/Scenri/commit/131d1329dac03a4d43299636ecf94cb90a26c0e1))
41
+ * **create:** keep a phone's toast clear of the tab bar ([c42f947](https://github.com/tonygorb/Scenri/commit/c42f94791f226879d15f460f2e054ddd79529a9b))
42
+ * **create:** the selection dock is a shelf on a phone, not the pill squeezed ([ffac597](https://github.com/tonygorb/Scenri/commit/ffac5970779d28dfabd52d7655d7469a993417e6))
43
+ * make every saved change reach every surface without a reload ([fb1e517](https://github.com/tonygorb/Scenri/commit/fb1e517170d1327eb4b1ce8af5a6e93d42c9ed7e))
44
+ * **presenters:** a saved person is on every surface the moment they land ([dd6ed16](https://github.com/tonygorb/Scenri/commit/dd6ed16ba1e687c1bf8848bb1f070349663ddb81))
45
+ * **products:** a product edit or delete reaches every surface at once ([2e34a76](https://github.com/tonygorb/Scenri/commit/2e34a765fe038e26d440a2ee586ea7844b6c35b9))
46
+ * **scenes:** a deleted scene leaves every surface at once ([1d132cf](https://github.com/tonygorb/Scenri/commit/1d132cff749437f1ed6ec5ea7635eaa92125763e))
47
+ * **studio:** ask the browser, not the last render, whether the page is still here ([0c4d874](https://github.com/tonygorb/Scenri/commit/0c4d8742a7358dad11fc5d6a2d137f8bb9d11448))
48
+ * **studio:** hand focus on after a delete, and write the rule down ([07efb5c](https://github.com/tonygorb/Scenri/commit/07efb5c653af482e4725587a731dceb1701de130))
49
+ * **studio:** let the selection bar undo Select all ([da21207](https://github.com/tonygorb/Scenri/commit/da21207c9d1215e85b10670b7af8c3b5631462b3))
50
+ * **studio:** order brand answers so a late read never undoes a newer write ([abac146](https://github.com/tonygorb/Scenri/commit/abac146d5fa8ff559bb1105589d2c4418c5df94b))
51
+
3
52
  ## [0.11.1](https://github.com/tonygorb/Scenri/compare/v0.11.0...v0.11.1) (2026-09-18)
4
53
 
5
54
 
@@ -865,7 +865,7 @@ function createStore(db) {
865
865
  }));
866
866
  },
867
867
  updateBrand(id, json) {
868
- db.prepare("UPDATE brands SET json=?, slug=?, updated_at=datetime('now') WHERE id=?").run(
868
+ db.prepare("UPDATE brands SET json=?, slug=?, updated_at=strftime('%Y-%m-%d %H:%M:%f','now') WHERE id=?").run(
869
869
  JSON.stringify(json),
870
870
  uniqueSlug(db, json.meta.name, id),
871
871
  id
@@ -2224,5 +2224,5 @@ function createCore(homeDir = defaultHome()) {
2224
2224
  }
2225
2225
 
2226
2226
  export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug };
2227
- //# sourceMappingURL=chunk-K6IELFL3.js.map
2228
- //# sourceMappingURL=chunk-K6IELFL3.js.map
2227
+ //# sourceMappingURL=chunk-MHTA6O3A.js.map
2228
+ //# sourceMappingURL=chunk-MHTA6O3A.js.map
@@ -463,7 +463,7 @@ async function addOrRepair(ownEntry, say, over) {
463
463
  }
464
464
  async function rememberDecline(home) {
465
465
  try {
466
- const { createCore } = await import('./src-TC4UQ7H6.js');
466
+ const { createCore } = await import('./src-INKSTJNC.js');
467
467
  const core = createCore(home);
468
468
  try {
469
469
  core.store.setSetting("desktop.prompt", "declined");
@@ -530,5 +530,5 @@ function tail(path, bytes = 4096) {
530
530
  }
531
531
 
532
532
  export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand };
533
- //# sourceMappingURL=cli-GMRPOISO.js.map
534
- //# sourceMappingURL=cli-GMRPOISO.js.map
533
+ //# sourceMappingURL=cli-4A3QWRSC.js.map
534
+ //# sourceMappingURL=cli-4A3QWRSC.js.map
package/dist/index.js CHANGED
@@ -86,12 +86,12 @@ try {
86
86
  break;
87
87
  }
88
88
  case "desktop": {
89
- const { runDesktopCommand } = await import('./cli-GMRPOISO.js');
89
+ const { runDesktopCommand } = await import('./cli-4A3QWRSC.js');
90
90
  process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
91
91
  break;
92
92
  }
93
93
  case "open": {
94
- const { runOpenCommand } = await import('./cli-GMRPOISO.js');
94
+ const { runOpenCommand } = await import('./cli-4A3QWRSC.js');
95
95
  process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
96
96
  break;
97
97
  }
package/dist/serve.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createUpdateChecker, classify, findNpm, isReleaseTriplet, stageVersion } from './chunk-YK37H6DM.js';
2
2
  import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-CMLJGPYC.js';
3
- import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-K6IELFL3.js';
3
+ import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-MHTA6O3A.js';
4
4
  import { detectInstallKind } from './chunk-PAIAXRAC.js';
5
5
  export { detectInstallKind } from './chunk-PAIAXRAC.js';
6
6
  import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
@@ -8007,6 +8007,7 @@ async function runSceneBuild(deps, job, hashes, instruction, signal) {
8007
8007
  commit(core, job.brandId, (json) => {
8008
8008
  const rows = brandScenes(json);
8009
8009
  const at = rows.findIndex((s) => s?.id === scene.id);
8010
+ if (at < 0 && job.sceneId) throw new Error("This scene was deleted while it was being read again.");
8010
8011
  json.scenes = at >= 0 ? rows.map((s, i) => i === at ? scene : s) : [...rows, scene];
8011
8012
  });
8012
8013
  patch(job, {
@@ -8052,6 +8053,11 @@ function sceneBuildRunning(brandId, sceneId) {
8052
8053
  }
8053
8054
  return false;
8054
8055
  }
8056
+ function cancelSceneBuilds(brandId, sceneId) {
8057
+ for (const b of builds.values()) {
8058
+ if (b.brandId === brandId && b.sceneId === sceneId && !b.finished) cancelAssetBuild(b.id);
8059
+ }
8060
+ }
8055
8061
  var LOOPBACK = ["localhost", "127.0.0.1", "::1"];
8056
8062
  var ACCESS_COOKIE = "sc_access";
8057
8063
  var LEGACY_ACCESS_COOKIE = "bt_access";
@@ -10947,10 +10953,16 @@ function registerAssetBuildRoutes(app, deps) {
10947
10953
  if (!brand) return;
10948
10954
  const id = String(req.params.sceneId);
10949
10955
  if (!brandScenes(brand.json).some((s) => s.id === id)) return reply.status(404).send({ error: "scene not found" });
10950
- commit(core, brand.id, (json) => {
10951
- json.scenes = brandScenes(json).filter((s) => s.id !== id);
10952
- });
10953
- return { ok: true };
10956
+ cancelSceneBuilds(brand.id, id);
10957
+ let row;
10958
+ try {
10959
+ row = commit(core, brand.id, (json) => {
10960
+ json.scenes = brandScenes(json).filter((s) => s.id !== id);
10961
+ });
10962
+ } catch (err) {
10963
+ return reply.status(err.statusCode ?? 500).send({ error: err.message });
10964
+ }
10965
+ return { ok: true, brand: row };
10954
10966
  });
10955
10967
  app.post("/api/brands/:id/scenes/:sceneId/reread", async (req, reply) => {
10956
10968
  const brand = brandOr404(req, reply);
@@ -11704,6 +11716,44 @@ function registerImageRoutes(app, deps) {
11704
11716
 
11705
11717
  // src/release/notes.data.ts
11706
11718
  var RELEASES = [
11719
+ {
11720
+ version: "0.13.0",
11721
+ date: "2026-09-21",
11722
+ title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still.",
11723
+ sections: [
11724
+ {
11725
+ heading: "Learn",
11726
+ body: "Learn sits in the top bar, or under Help on a smaller screen, and teaches Scenri in six short lessons: your first shot, adding a product, a presenter, a scene, using a product again, and refining a shot. Each lesson keeps its own place, so you can set one down, take up another and come back to it. A new install is offered a guided first shot, and the guide can step back when you change your mind."
11727
+ },
11728
+ {
11729
+ heading: "Alerts",
11730
+ body: "Alerts are smaller and stack at the bottom of the screen, above the composer rather than over it. The same event twice shares one card, an error stays until you close it, and an alert holds still while you point at it or tab into it."
11731
+ },
11732
+ {
11733
+ heading: "Create",
11734
+ body: "Archiving a shot no longer makes the pictures around it blink, and opening or closing the side panel rearranges the feed in one step."
11735
+ }
11736
+ ]
11737
+ },
11738
+ {
11739
+ version: "0.12.0",
11740
+ date: "2026-09-20",
11741
+ title: "Saved work shows up everywhere at once, and a selection has its own toolbar.",
11742
+ sections: [
11743
+ {
11744
+ heading: "Create",
11745
+ body: "Pick several shots and the dock can archive the lot in one go, with a single Undo that brings them all back. That dock is a toolbar now: how many are picked, Select all, and the verbs as tools that name themselves. It is the same bar on a phone, at a size a thumb can hit."
11746
+ },
11747
+ {
11748
+ heading: "Brand",
11749
+ body: "A brand you save, rename or delete, and a workspace you wipe, show up on every screen straight away. A brand you have just deleted no longer reopens its dialogs on the brand you land in next."
11750
+ },
11751
+ {
11752
+ heading: "Fixes",
11753
+ body: "A product edit, a new presenter and a deleted scene each reach every surface that shows them, without a reload, and a slow answer can no longer undo a newer change. Walking to another page while a delete is still out leaves you where you went, instead of pulling you back when it answers. A shot restored from the archive comes back to the feed where it belongs. After deleting a record, focus lands somewhere sensible instead of at the top of the page. On a phone a toast no longer sits on the tab bar."
11754
+ }
11755
+ ]
11756
+ },
11707
11757
  {
11708
11758
  version: "0.11.1",
11709
11759
  date: "2026-09-18",
@@ -12804,14 +12854,24 @@ function registerSystemRoutes(app, deps) {
12804
12854
  }
12805
12855
 
12806
12856
  // src/routes/guide.ts
12807
- var TASKS = ["first-shot", "refine", "product", "presenter", "scene"];
12857
+ var TASKS = ["first-shot", "refine", "product", "presenter", "scene", "reuse"];
12808
12858
  var MILESTONES = ["shot", "refine", "product", "presenter", "scene"];
12809
12859
  var KEY = "guide";
12810
12860
  var isTask = (t) => TASKS.includes(t);
12811
12861
  var isMilestone = (m) => MILESTONES.includes(m);
12812
12862
  var num2 = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
12813
12863
  function blank(eligible) {
12814
- return { v: 2, eligible, welcome: null, hidden: null, done: {}, dismissed: [], active: null };
12864
+ return {
12865
+ v: 2,
12866
+ eligible,
12867
+ welcome: null,
12868
+ hidden: null,
12869
+ done: {},
12870
+ lessons: {},
12871
+ dismissed: [],
12872
+ active: null,
12873
+ progress: {}
12874
+ };
12815
12875
  }
12816
12876
  function parse2(raw) {
12817
12877
  if (!raw) return null;
@@ -12823,9 +12883,47 @@ function parse2(raw) {
12823
12883
  if (j.done && typeof j.done === "object") {
12824
12884
  for (const [k, at] of Object.entries(j.done)) if (isMilestone(k) && typeof at === "string") done[k] = at;
12825
12885
  }
12886
+ const lessons = {};
12887
+ if (j.lessons && typeof j.lessons === "object") {
12888
+ for (const [k, at] of Object.entries(j.lessons)) if (isTask(k) && typeof at === "string") lessons[k] = at;
12889
+ }
12890
+ const asTask = (a2) => {
12891
+ const b2 = a2?.baseline;
12892
+ return a2 && isTask(a2.task) && typeof a2.brandId === "string" && typeof a2.since === "string" ? {
12893
+ task: a2.task,
12894
+ brandId: a2.brandId,
12895
+ since: a2.since,
12896
+ baseline: { products: num2(b2?.products), presenters: num2(b2?.presenters), scenes: num2(b2?.scenes) },
12897
+ ...a2.paused === true ? { paused: true } : {}
12898
+ } : null;
12899
+ };
12900
+ const asProgress = (v) => {
12901
+ const p = v;
12902
+ const b2 = p?.baseline;
12903
+ return p && typeof p.brandId === "string" && typeof p.since === "string" ? {
12904
+ brandId: p.brandId,
12905
+ since: p.since,
12906
+ baseline: { products: num2(b2?.products), presenters: num2(b2?.presenters), scenes: num2(b2?.scenes) },
12907
+ reached: Array.isArray(p.reached) ? p.reached.filter((m) => typeof m === "string") : [],
12908
+ ...p.paused === true ? { paused: true } : {}
12909
+ } : null;
12910
+ };
12911
+ const progress = {};
12912
+ if (j.progress && typeof j.progress === "object") {
12913
+ for (const [k, v] of Object.entries(j.progress)) {
12914
+ const p = asProgress(v);
12915
+ if (isTask(k) && p) progress[k] = p;
12916
+ }
12917
+ }
12918
+ if (j.parked && typeof j.parked === "object") {
12919
+ for (const [k, v] of Object.entries(j.parked)) {
12920
+ const t = asTask(v);
12921
+ if (isTask(k) && t && !progress[k]) progress[k] = { ...t, reached: [], paused: true };
12922
+ }
12923
+ }
12826
12924
  const a = j.active;
12827
12925
  const b = a?.baseline;
12828
- const active = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12926
+ const held = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12829
12927
  task: a.task,
12830
12928
  brandId: a.brandId,
12831
12929
  since: a.since,
@@ -12838,8 +12936,11 @@ function parse2(raw) {
12838
12936
  welcome: j.welcome === "taken" || j.welcome === "declined" ? j.welcome : null,
12839
12937
  hidden: typeof j.hidden === "boolean" ? j.hidden : null,
12840
12938
  done,
12939
+ lessons,
12841
12940
  dismissed: Array.isArray(j.dismissed) ? j.dismissed.filter(isTask) : [],
12842
- active
12941
+ // an old record's one task in hand is that lesson's progress now
12942
+ active: typeof j.active === "string" && isTask(j.active) ? j.active : held?.task ?? null,
12943
+ progress: held && !progress[held.task] ? { ...progress, [held.task]: { ...held, reached: [] } } : progress
12843
12944
  };
12844
12945
  } catch {
12845
12946
  return null;
@@ -12898,21 +12999,33 @@ function readGuide(core, env) {
12898
12999
  changed = true;
12899
13000
  }
12900
13001
  }
12901
- if (r.active && !core.store.getBrand(r.active.brandId)) {
12902
- r.active = null;
13002
+ for (const [t, p] of Object.entries(r.progress)) {
13003
+ if (core.store.getBrand(p.brandId)) continue;
13004
+ delete r.progress[t];
13005
+ if (r.active === t) r.active = null;
12903
13006
  changed = true;
12904
13007
  }
12905
13008
  if (changed) write(core.store, r);
12906
- const a = r.active;
12907
- const nodeKind = a?.task === "first-shot" ? "generation" : a?.task === "refine" ? "edit" : null;
12908
- const eligible = r.eligible && env.SCENRI_NO_GUIDE !== "1";
13009
+ const on = r.active ? r.progress[r.active] : null;
13010
+ const a = r.active && on ? {
13011
+ task: r.active,
13012
+ brandId: on.brandId,
13013
+ since: on.since,
13014
+ baseline: on.baseline,
13015
+ ...on.paused ? { paused: true } : {}
13016
+ } : null;
13017
+ const nodeKind = a?.task === "first-shot" || a?.task === "reuse" ? "generation" : a?.task === "refine" ? "edit" : null;
13018
+ const settled = r.welcome === null && !!r.done.shot;
13019
+ const eligible = r.eligible && !settled && env.SCENRI_NO_GUIDE !== "1";
12909
13020
  return {
12910
13021
  eligible,
12911
13022
  welcome: r.welcome,
12912
13023
  hidden: r.hidden ?? !eligible,
12913
13024
  done: r.done,
13025
+ lessons: r.lessons,
12914
13026
  dismissed: r.dismissed,
12915
13027
  active: a,
13028
+ progress: r.progress,
12916
13029
  activeNodes: a && nodeKind ? core.store.nodesSince(a.brandId, nodeKind, a.since) : [],
12917
13030
  activeDraftId: a?.task === "presenter" ? core.store.presenterDraftSince(a.brandId, a.since) : null,
12918
13031
  counts: a ? countsOf(core, a.brandId) : null
@@ -12930,19 +13043,34 @@ function applyIntent(core, body) {
12930
13043
  if (typeof s.brandId !== "string") return "brandId required";
12931
13044
  const baseline = countsOf(core, s.brandId);
12932
13045
  if (!baseline) return "brand not found";
12933
- const held = r.active;
12934
- if (held && held.paused && held.task === s.task && held.brandId === s.brandId) {
12935
- const { paused: _, ...going } = held;
12936
- r.active = going;
12937
- } else r.active = { task: s.task, brandId: s.brandId, since: core.store.now(), baseline };
13046
+ const guiding = r.active ? r.progress[r.active] : null;
13047
+ if (r.active && r.active !== s.task && guiding) r.progress[r.active] = { ...guiding, paused: true };
13048
+ const mine = r.progress[s.task];
13049
+ if (mine && mine.brandId === s.brandId) {
13050
+ const { paused: _, ...going } = mine;
13051
+ r.progress[s.task] = going;
13052
+ } else r.progress[s.task] = { brandId: s.brandId, since: core.store.now(), baseline, reached: [] };
13053
+ r.active = s.task;
12938
13054
  r.dismissed = r.dismissed.filter((t) => t !== s.task);
12939
13055
  } else if ("finish" in b) {
12940
13056
  if (!isTask(b.finish)) return "unknown task";
12941
- if (r.active?.task === b.finish) r.active = null;
13057
+ if (!r.lessons[b.finish]) r.lessons[b.finish] = (/* @__PURE__ */ new Date()).toISOString();
13058
+ if (r.active === b.finish) r.active = null;
13059
+ delete r.progress[b.finish];
12942
13060
  } else if ("dismiss" in b) {
12943
13061
  if (!isTask(b.dismiss)) return "unknown task";
12944
13062
  if (!r.dismissed.includes(b.dismiss)) r.dismissed.push(b.dismiss);
12945
- if (r.active?.task === b.dismiss) r.active = { ...r.active, paused: true };
13063
+ const shut = r.progress[b.dismiss];
13064
+ if (shut) r.progress[b.dismiss] = { ...shut, paused: true };
13065
+ if (r.active === b.dismiss) r.active = null;
13066
+ } else if ("reached" in b) {
13067
+ const w = b.reached ?? {};
13068
+ if (!isTask(w.task)) return "unknown task";
13069
+ if (typeof w.moment !== "string" || !w.moment) return "moment required";
13070
+ const p = r.progress[w.task];
13071
+ if (!p) return null;
13072
+ if (p.reached.includes(w.moment)) return null;
13073
+ r.progress[w.task] = { ...p, reached: [...p.reached, w.moment].slice(-24) };
12946
13074
  } else if ("hidden" in b) {
12947
13075
  if (typeof b.hidden !== "boolean") return "hidden is true or false";
12948
13076
  r.hidden = b.hidden;
@@ -12976,7 +13104,7 @@ function registerDesktopRoutes(app, deps) {
12976
13104
  record: null
12977
13105
  };
12978
13106
  }
12979
- const { installDeps } = await import('./cli-GMRPOISO.js');
13107
+ const { installDeps } = await import('./cli-4A3QWRSC.js');
12980
13108
  const { desktopStatus } = await import('./install-VXJPLYWM.js');
12981
13109
  return desktopStatus(installDeps(runtime.entry));
12982
13110
  });
@@ -12984,7 +13112,7 @@ function registerDesktopRoutes(app, deps) {
12984
13112
  if (!runtime.entry) {
12985
13113
  return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
12986
13114
  }
12987
- const { addToDesktop } = await import('./cli-GMRPOISO.js');
13115
+ const { addToDesktop } = await import('./cli-4A3QWRSC.js');
12988
13116
  return addToDesktop(runtime.entry);
12989
13117
  });
12990
13118
  app.get("/api/desktop", async () => {
@@ -13092,7 +13220,12 @@ function buildServer(opts) {
13092
13220
  return { ...row, warnings, report };
13093
13221
  });
13094
13222
  app.put("/api/brands/:id", async (req, reply) => {
13095
- const json = req.body?.brand;
13223
+ const body = req.body ?? {};
13224
+ let json = body.brand;
13225
+ if (body.keepAssets === true && json && typeof json === "object") {
13226
+ const stored = core.store.getBrand(req.params.id);
13227
+ if (stored) json = withStoredAssets(json, stored.json);
13228
+ }
13096
13229
  const v = validateBrand(json);
13097
13230
  if (!v.valid) return reply.status(400).send({ error: "invalid .brand", details: v.errors });
13098
13231
  const row = core.store.updateBrand(req.params.id, json);
@@ -14474,6 +14607,16 @@ function buildServer(opts) {
14474
14607
  }
14475
14608
  return app;
14476
14609
  }
14610
+ var ASSET_COLLECTIONS = ["products", "scenes", "characters"];
14611
+ function withStoredAssets(sent, stored) {
14612
+ const out = { ...sent };
14613
+ const held = stored ?? {};
14614
+ for (const key of ASSET_COLLECTIONS) {
14615
+ if (key in held) out[key] = held[key];
14616
+ else delete out[key];
14617
+ }
14618
+ return out;
14619
+ }
14477
14620
 
14478
14621
  // src/serve.ts
14479
14622
  var PORT = Number(process.env.SCENRI_PORT || 4747);
@@ -14625,7 +14768,7 @@ async function run() {
14625
14768
  }
14626
14769
  }
14627
14770
  const ownEntry = fileURLToPath(import.meta.url);
14628
- const { addToDesktop, installDeps } = await import('./cli-GMRPOISO.js');
14771
+ const { addToDesktop, installDeps } = await import('./cli-4A3QWRSC.js');
14629
14772
  const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
14630
14773
  const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
14631
14774
  const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
@@ -1,3 +1,3 @@
1
- export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug } from './chunk-K6IELFL3.js';
2
- //# sourceMappingURL=src-TC4UQ7H6.js.map
3
- //# sourceMappingURL=src-TC4UQ7H6.js.map
1
+ export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug } from './chunk-MHTA6O3A.js';
2
+ //# sourceMappingURL=src-INKSTJNC.js.map
3
+ //# sourceMappingURL=src-INKSTJNC.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",