@yuneta/gobj-ui 7.25.13 → 7.25.15

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.
@@ -94,6 +94,7 @@ import {
94
94
 
95
95
  import {
96
96
  apply_graphs_echo,
97
+ forget_refused_graphs_write,
97
98
  plan_graph_saves,
98
99
  topic_arrangement_changed,
99
100
  } from "./graph_save_plan.js";
@@ -3275,12 +3276,12 @@ function save_topic_graph_properties(gobj, topic_name)
3275
3276
  * the publish and as a copy, because the object below travels by
3276
3277
  * reference and goes on being arranged after the write.
3277
3278
  *
3278
- * Nothing answers an `EV_UPDATE_NODE` of `__graphs__` -- the view
3279
- * writes that topic itself and is deliberately not told about it
3280
- * (see the EV_RECORD_WRITTEN of C_YUI_TREEDB_GRAPH) -- so this is
3281
- * the write taken for granted. A refused one leaves the view
3282
- * believing the backend holds an arrangement it does not, until
3283
- * the next load reads the records again. */
3279
+ * Nothing answers an `EV_UPDATE_NODE` of `__graphs__` when it
3280
+ * lands -- the view writes that topic itself and is deliberately
3281
+ * not told about it (see the EV_RECORD_WRITTEN of
3282
+ * C_YUI_TREEDB_GRAPH) -- so this is the write taken for granted.
3283
+ * The host says when one does NOT land (EV_GRAPHS_WRITE_REFUSED),
3284
+ * and the next Save writes the topic again. */
3284
3285
  priv._saved_graph_properties[topic_name] = json_deep_copy(properties);
3285
3286
 
3286
3287
  let kw_update = {
@@ -10503,6 +10504,34 @@ function ac_save_graph(gobj, event, kw, src)
10503
10504
  return 0;
10504
10505
  }
10505
10506
 
10507
+ /************************************************************
10508
+ * The host says a write of `__graphs__` did not land: the
10509
+ * backend refused it, the transport refused it on the way out,
10510
+ * or there was no session to send it on. kw: {topic}
10511
+ *
10512
+ * save_topic_graph_properties() recorded it as saved before it
10513
+ * left. Forgotten now, and the Save is lit again: the next Save
10514
+ * writes the topic again. Before, the view believed the backend
10515
+ * held the arrangement, and the next Save had nothing to write.
10516
+ ************************************************************/
10517
+ function ac_graphs_write_refused(gobj, event, kw, src)
10518
+ {
10519
+ let priv = gobj.priv;
10520
+ let topic_name = kw && kw.topic;
10521
+
10522
+ if(!topic_name) {
10523
+ log_error(`${gobj_short_name(gobj)}: ${event} names no topic`);
10524
+ return -1;
10525
+ }
10526
+ log_warning(`${gobj_short_name(gobj)}: the arrangement of '${topic_name}' ` +
10527
+ `was not saved: the next Save writes it again`);
10528
+ forget_refused_graphs_write(priv._saved_graph_properties, topic_name);
10529
+ if(priv.edit_mode) {
10530
+ mark_graph_dirty(gobj);
10531
+ }
10532
+ return 0;
10533
+ }
10534
+
10506
10535
  /************************************************************
10507
10536
  * Node created, from subscription
10508
10537
  ************************************************************/
@@ -10582,8 +10611,8 @@ function ac_node_updated(gobj, event, kw, src)
10582
10611
  priv.__graphs__.push(node);
10583
10612
  }
10584
10613
  /* Only THIS record's topic: a rebuild of every topic took their
10585
- * saved copies from the live objects (M32 of the 2026-09-21
10586
- * review). build_graph_properties() is for the load. */
10614
+ * saved copies from the live objects.
10615
+ * build_graph_properties() is for the load. */
10587
10616
  apply_graphs_echo(
10588
10617
  priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node
10589
10618
  );
@@ -10637,7 +10666,7 @@ function ac_node_updated(gobj, event, kw, src)
10637
10666
  update_local_node(gobj, topic_name, node);
10638
10667
  /* `priv.graph`: this read an undeclared `graph`, the ReferenceError
10639
10668
  * fell into the catch, and a card on screen kept its old record after
10640
- * every UPDATED (M33 of the 2026-09-21 review). The catch is for G6,
10669
+ * every UPDATED. The catch is for G6,
10641
10670
  * which throws on an id it does not hold -- a folded card. */
10642
10671
  let on_screen = false;
10643
10672
  try {
@@ -11778,8 +11807,8 @@ function ac_confirmed(gobj, event, kw, src)
11778
11807
  }
11779
11808
  if(missing.length) {
11780
11809
  /* Gone while the question was open (another writer deleted
11781
- * them): said, to the log and to the person (a low of the
11782
- * 2026-09-22 review); the rest go on. */
11810
+ * them): said, to the log and to the person; the rest go
11811
+ * on. */
11783
11812
  log_error(`${gobj_short_name(gobj)}: ${event}: cards gone while ` +
11784
11813
  `confirming the delete, not deleted: ${missing.join(", ")}`);
11785
11814
  yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", {t: t});
@@ -12022,6 +12051,7 @@ function create_gclass(gclass_name)
12022
12051
  ["EV_NODE_CREATED", ac_node_created, null],
12023
12052
  ["EV_NODE_UPDATED", ac_node_updated, null],
12024
12053
  ["EV_NODE_DELETED", ac_node_deleted, null],
12054
+ ["EV_GRAPHS_WRITE_REFUSED", ac_graphs_write_refused, null],
12025
12055
 
12026
12056
  /*--- Graph interaction events ---*/
12027
12057
  ["EV_NODE_CLICK", ac_node_click, null],
@@ -12091,6 +12121,7 @@ function create_gclass(gclass_name)
12091
12121
  ["EV_NODE_CREATED", 0],
12092
12122
  ["EV_NODE_UPDATED", 0],
12093
12123
  ["EV_NODE_DELETED", 0],
12124
+ ["EV_GRAPHS_WRITE_REFUSED", 0],
12094
12125
 
12095
12126
  /*--- Graph interaction (internal) ---*/
12096
12127
  ["EV_NODE_CLICK", 0],
@@ -22,11 +22,10 @@
22
22
  * AN EDIT IS A DRAFT. A write here moves no version and reaches no
23
23
  * treedb: the host SAVES it (C_TREEDB's save-schema raises the
24
24
  * topic_version of what changed and the schema_version, once) and
25
- * APPLIES it (apply-schema + a restart of the owning yuno). Until
26
- * the owner's design of M36 (2026-09-21 review) every write here
27
- * raised both versions, so an edit half made was already the
28
- * schema of the next start. The topic list marks what this session
29
- * wrote and has not saved (EV_REFRESH forgets it: the view's own
25
+ * APPLIES it (apply-schema + a restart of the owning yuno). If a
26
+ * write here raised both versions, an edit half made would already
27
+ * be the schema of the next start. The topic list marks what this
28
+ * session wrote and has not saved (EV_REFRESH forgets it: the view's own
30
29
  * Refresh button sends it, and a host may after a save -- the
31
30
  * agent console's does not) and what the host says is not saved
32
31
  * (EV_DRAFTS, replaced whole each time it is sent).
@@ -188,6 +187,11 @@ const DIAGRAM_SEG = "diagram";
188
187
  * them), and each filters on its own purpose. */
189
188
  const PURPOSE = "schema_editor";
190
189
 
190
+ /* The dialogs that show what they were opened with and send nothing a
191
+ * screen declares: a move sent by the host leaves them up (see
192
+ * close_dialog_left_behind()). */
193
+ const DIALOGS_THAT_READ_NOTHING = ["SCHEMA_EXPORT_DIALOG", "SCHEMA_REPORT_DIALOG"];
194
+
191
195
 
192
196
  /***************************************************************
193
197
  * Attrs
@@ -230,6 +234,7 @@ let PRIVATE_DATA = {
230
234
  diagram_gobj: null,
231
235
  dialog: null,
232
236
  dialog_gen: 0, /* the model_gen the open dialog was built on */
237
+ dialog_kind: "", /* the logical class of the open dialog */
233
238
 
234
239
  /* A write, or a plan of them, in flight */
235
240
  save_queue: null,
@@ -460,9 +465,9 @@ function refuse_if_readonly(gobj, event)
460
465
  * dumped by the machine trace).
461
466
  *
462
467
  * Refused, and said. A column form sends every field it shows, so
463
- * its Save after a reload wrote the OLD record over the newer one
464
- * (fourth independent review, probe A3); a form whose column the
465
- * reload removed answered "names no column" for ever. end_load()
468
+ * its Save after a reload wrote the OLD record over the newer one;
469
+ * a form whose column the reload removed answered "names no column"
470
+ * for ever. end_load()
466
471
  * closes such a dialog when the load lands; this is the same rule
467
472
  * for whatever arrives anyway -- a stale screen, a confirmation
468
473
  * that was up, a keyboard.
@@ -489,8 +494,7 @@ function built_on_a_replaced_model(gobj, event, kw)
489
494
  * the model shown is kept, and it may be older than the store.
490
495
  * The session is up, so no reconnect will ask for it again -- it
491
496
  * was owed for as long as the session lasted, and a form opened
492
- * on that model wrote every field of it over the newer record
493
- * (fifth independent review; A3 of the fourth, by another path).
497
+ * on that model wrote every field of it over the newer record.
494
498
  *
495
499
  * So the operator's NEXT action asks it. An edit -- a form, a
496
500
  * write, a confirmation, what reads the model whole (check,
@@ -562,7 +566,7 @@ function reload_after_move(gobj)
562
566
  * none of the actions of a screen, so the screen it replaces goes
563
567
  * and the toolbar stops answering: left up, a click on a card, on
564
568
  * Back or on the drawing answered "Event NOT DEFINED in state
565
- * ST_LOADING" and was lost (third independent review). A dialog
569
+ * ST_LOADING" and was lost. A dialog
566
570
  * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
567
571
  * and a load that LANDS closes it (end_load()): it was built on the
568
572
  * model the load replaced, and its Save sends every field it shows.
@@ -969,8 +973,7 @@ function render_toolbar(gobj)
969
973
  * reload that no longer has them keeps the position (it says so,
970
974
  * and Back is the way out). Asked of the MODEL, not of the state:
971
975
  * gated on the state, Diagram, Check, Export, Import and New topic
972
- * stayed up there and each logged "with no treedb open" (fourth
973
- * independent review, probe D2). */
976
+ * stayed up there and each logged "with no treedb open". */
974
977
  let treedb_here = on_a_treedb && !!current_treedb(gobj);
975
978
  let topic_here = treedb_here && !!current_topic(gobj);
976
979
  /* Nothing is legal while the model is in the air: every action is
@@ -1745,6 +1748,7 @@ function close_dialog(gobj)
1745
1748
  }
1746
1749
  let dialog = priv.dialog;
1747
1750
  priv.dialog = null;
1751
+ priv.dialog_kind = "";
1748
1752
  priv.export_view = null;
1749
1753
  try {
1750
1754
  dialog.close();
@@ -1759,6 +1763,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1759
1763
 
1760
1764
  close_dialog(gobj);
1761
1765
  priv.dialog_gen = priv.model_gen;
1766
+ priv.dialog_kind = logical;
1762
1767
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1763
1768
  dialog: true,
1764
1769
  title: title,
@@ -1767,6 +1772,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1767
1772
  t: t,
1768
1773
  on_close: () => {
1769
1774
  priv.dialog = null;
1775
+ priv.dialog_kind = "";
1770
1776
  priv.export_view = null;
1771
1777
  }
1772
1778
  });
@@ -2674,11 +2680,11 @@ function transport_in_session(gobj)
2674
2680
  * The state is part of the question, and that is only true
2675
2681
  * because NOTHING moves the editor out of ST_LOADING or ST_SAVING
2676
2682
  * but the end of what is in flight: a position the host sends
2677
- * meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
2678
- * move a RELOAD out of ST_LOADING, and every answer of it was then
2679
- * "for a load that is over": the records it had emptied stayed
2680
- * empty, and the next write patched them into a model with no
2681
- * treedb at all.
2683
+ * meanwhile waits in `pending_seg` (ac_show()). If EV_SHOW moved a
2684
+ * RELOAD out of ST_LOADING, every answer of it would be "for a load
2685
+ * that is over": the records it had emptied would stay empty, and
2686
+ * the next write would patch them into a model with no treedb at
2687
+ * all.
2682
2688
  ***************************************************************/
2683
2689
  function is_current_load(gobj, md)
2684
2690
  {
@@ -2995,12 +3001,74 @@ function position_seg(gobj)
2995
3001
  * ...and the same in reverse. A tail naming a treedb or a topic
2996
3002
  * the store does not have lands one level up rather than on an
2997
3003
  * empty screen: a shared link outlives the schema it points at.
3004
+ *
3005
+ * Every position the HOST sends comes through here, at once
3006
+ * (ac_show()) or when what was in flight ends (end_load(),
3007
+ * end_writes()). A move to another position closes the dialog of
3008
+ * the screen the editor left (close_dialog_left_behind()).
2998
3009
  ***************************************************************/
2999
3010
  function apply_seg(gobj, seg)
3000
3011
  {
3012
+ let before = position_seg(gobj);
3001
3013
  let pos = seg_position(seg);
3002
3014
 
3003
- return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
3015
+ let ret = go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
3016
+ if(position_seg(gobj) !== before) {
3017
+ close_dialog_left_behind(gobj);
3018
+ }
3019
+ return ret;
3020
+ }
3021
+
3022
+ /***************************************************************
3023
+ * The host moved the editor while a dialog was up. C_YUI_SHELL
3024
+ * keeps the overlays open on a move that changes only the subpath
3025
+ * (a url typed in, a link), so the dialog outlives its screen.
3026
+ *
3027
+ * A form, the import and the orphans send events that only the
3028
+ * screen they were opened from declares, and they name their
3029
+ * topic under the treedb open at the time. Left up on another
3030
+ * screen, their Save answered "Event NOT DEFINED" and the edit was
3031
+ * lost with no word. So the dialog is closed and the operator told.
3032
+ * The import plan goes with its dialog: it was computed for the
3033
+ * treedb the editor left.
3034
+ *
3035
+ * The export and the check stay: they show the texts they were
3036
+ * opened with, and what they send is legal in every state.
3037
+ ***************************************************************/
3038
+ function close_dialog_left_behind(gobj)
3039
+ {
3040
+ let priv = gobj.priv;
3041
+
3042
+ if(!priv.dialog || DIALOGS_THAT_READ_NOTHING.includes(priv.dialog_kind)) {
3043
+ return;
3044
+ }
3045
+ log_warning(`${gobj_short_name(gobj)}: the host moved the view while the dialog ` +
3046
+ `${priv.dialog_kind} was open: closed`);
3047
+ close_dialog(gobj);
3048
+ forget_import_plan(gobj);
3049
+ yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", {t: t});
3050
+ }
3051
+
3052
+ /***************************************************************
3053
+ * A confirmation answered on another screen than the one it was
3054
+ * asked from. A confirmation is a shell modal, so a move sent by
3055
+ * the host does not close it. It names its topic and column, and
3056
+ * the actions look them up under the treedb open NOW: a Yes after
3057
+ * a move to another treedb deleted the topic or column of the same
3058
+ * name THERE.
3059
+ *
3060
+ * Refused, and said. A kw with no `seg` stamp is not a
3061
+ * confirmation's and is not asked.
3062
+ ***************************************************************/
3063
+ function asked_on_another_screen(gobj, event, kw)
3064
+ {
3065
+ if(!kw || kw.seg === undefined || kw.seg === position_seg(gobj)) {
3066
+ return false;
3067
+ }
3068
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, it was asked on ` +
3069
+ `'${kw.seg}' and the view is on '${position_seg(gobj)}'`);
3070
+ yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", {t: t});
3071
+ return true;
3004
3072
  }
3005
3073
 
3006
3074
  function seg_position(seg)
@@ -3111,10 +3179,10 @@ function ac_mt_command_answer(gobj, event, kw, src)
3111
3179
  * slow `nodes`): the model shown is what the store held a
3112
3180
  * moment ago, and blanking it took the screen and every open
3113
3181
  * form with it -- a form's Save then answered "Event NOT
3114
- * DEFINED in state ST_IDLE" (fourth independent review). It
3182
+ * DEFINED in state ST_IDLE". It
3115
3183
  * stays, as it does when the load cannot leave, and the
3116
3184
  * reload is owed: the operator's next action asks it
3117
- * (reload_first(), fifth independent review). */
3185
+ * (reload_first()). */
3118
3186
  if(priv.model) {
3119
3187
  log_warning(`${gobj_short_name(gobj)}: the schemas could not be read ` +
3120
3188
  `again (${priv.load_error}): the previous ones stay`);
@@ -3139,13 +3207,13 @@ function ac_mt_command_answer(gobj, event, kw, src)
3139
3207
  /* The store was read whole: whatever reload was owed is paid,
3140
3208
  * whoever asked for this one -- the operator's Refresh, a late
3141
3209
  * write, the reconnect. Left owed, the next edit was refused
3142
- * and read the store AGAIN (sixth independent review). */
3210
+ * and read the store AGAIN. */
3143
3211
  priv.reload_on_open = false;
3144
3212
  /* The plan was a list of writes computed on the model this load
3145
3213
  * replaces. Forgotten HERE, where the model goes, and not when
3146
3214
  * the requests leave: a load that fails replaces nothing, and
3147
3215
  * forgetting early left its dialog up with no plan and Import
3148
- * disabled (sixth independent review). */
3216
+ * disabled. */
3149
3217
  forget_import_plan(gobj);
3150
3218
  build_model(gobj);
3151
3219
  /* Same reason as order_undo above: this is the LOAD, and the
@@ -3191,7 +3259,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3191
3259
  * silence, a drag of five columns stopped at the first.
3192
3260
  * The reload is also the one a late write may have owed
3193
3261
  * (reload_after_write): left set, the next write loaded
3194
- * the whole model once more (fourth independent review). */
3262
+ * the whole model once more. */
3195
3263
  let not_sent = priv.save_queue.length - 1;
3196
3264
  log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record` +
3197
3265
  (not_sent > 0 ? `: ${not_sent} write(s) after it were not sent` : ""));
@@ -3284,8 +3352,7 @@ function late_write_done(gobj, command, topic_name, data)
3284
3352
  * The fkey is read with parse_fkey_ref(), because the store answers
3285
3353
  * in list_dict -- `[{topic_name, id, hook_name}]` -- and not in the
3286
3354
  * `topics^<topic id>^cols` strings this view writes. Split as a
3287
- * string, every production answer marked nothing (fourth independent
3288
- * review, probe C).
3355
+ * string, every production answer marked nothing.
3289
3356
  ***************************************************************/
3290
3357
  function mark_written_record(gobj, topic_name, record)
3291
3358
  {
@@ -3344,7 +3411,7 @@ function ac_show(gobj, event, kw, src)
3344
3411
  /* A move like the editor's own (ac_back() and the rest): it goes,
3345
3412
  * and pays the reload a refused load owes there. Without it a url
3346
3413
  * or the browser's Back left it owed on a screen that asked for
3347
- * nothing (sixth independent review). */
3414
+ * nothing. */
3348
3415
  apply_seg(gobj, seg);
3349
3416
  return reload_after_move(gobj);
3350
3417
  }
@@ -3368,7 +3435,7 @@ function ac_hide(gobj, event, kw, src)
3368
3435
  *
3369
3436
  * Nothing else ends a load or a write the drop cut: their answers
3370
3437
  * will never come, and waiting in ST_LOADING skipped the reload
3371
- * itself (M-1 of the independent review of 7.25.4).
3438
+ * itself.
3372
3439
  ***************************************************************/
3373
3440
  function ac_transport_state(gobj, event, kw, src)
3374
3441
  {
@@ -3417,12 +3484,11 @@ function ac_refresh(gobj, event, kw, src)
3417
3484
  * a model the answers in the air are about to replace.
3418
3485
  *
3419
3486
  * Refused, and SAID. Undeclared, it answered "Event NOT DEFINED
3420
- * in state ST_LOADING" and the edit was gone with no word (third
3421
- * independent review).
3487
+ * in state ST_LOADING" and the edit was gone with no word.
3422
3488
  *
3423
3489
  * And NOT "try again": the same Save, once the load was in, sent
3424
3490
  * the form built on the old model -- every field of the old record
3425
- * over the newer one (fourth independent review). The load that
3491
+ * over the newer one. The load that
3426
3492
  * lands closes such a dialog (end_load()); one that fails leaves it
3427
3493
  * up, on the model it was built on.
3428
3494
  ***************************************************************/
@@ -3439,8 +3505,7 @@ function ac_wait_for_the_load(gobj, event, kw, src)
3439
3505
  * reload landed on zero treedbs while it was up) or ST_IDLE (no
3440
3506
  * model). A confirmation is a shell modal, not this view's dialog,
3441
3507
  * so the load that lands does not close it, and its Yes arrives
3442
- * anyway. Undeclared, it answered "Event NOT DEFINED" (fifth
3443
- * independent review).
3508
+ * anyway. Undeclared, it answered "Event NOT DEFINED".
3444
3509
  *
3445
3510
  * Refused with the stamp's own words: whatever it was decided on
3446
3511
  * is not on screen any more.
@@ -3461,13 +3526,12 @@ function ac_confirmed_on_nothing(gobj, event, kw, src)
3461
3526
  * topics hold a draft in __system__ that is not saved (C_TREEDB's
3462
3527
  * saved-schema, `draft_changed`). The mark of a write lived in this
3463
3528
  * session's memory only, so a reload of the page, a reconnect or a
3464
- * refresh showed no draft while __system__ still differed (N13 of
3465
- * the 2026-09-22 review).
3529
+ * refresh showed no draft while __system__ still differed.
3466
3530
  *
3467
3531
  * REPLACES what the host said before -- the host is the truth after
3468
3532
  * a reload, and a Save is exactly the host saying "none any more".
3469
3533
  * It only ever added, and a topic stayed a draft after the Save that
3470
- * published it (M1 of the 2026-09-23 review). What this session wrote
3534
+ * published it. What this session wrote
3471
3535
  * stays marked: it is kept apart (see topic_is_draft()).
3472
3536
  ***************************************************************/
3473
3537
  function ac_drafts(gobj, event, kw, src)
@@ -3552,7 +3616,7 @@ function ac_show_orphans(gobj, event, kw, src)
3552
3616
  ***************************************************************/
3553
3617
  function confirm_then(gobj, message, detail, kw)
3554
3618
  {
3555
- kw = Object.assign({model_gen: gobj.priv.model_gen}, kw);
3619
+ kw = Object.assign({model_gen: gobj.priv.model_gen, seg: position_seg(gobj)}, kw);
3556
3620
  /* The answers are i18n KEYS: shell_modals names each button by its
3557
3621
  * label, and its defaults ("Delete", "Cancel") are not lower-case, so
3558
3622
  * no validated locale holds them -- they read English in every
@@ -4092,6 +4156,9 @@ function ac_confirmed(gobj, event, kw, src)
4092
4156
  if(built_on_a_replaced_model(gobj, event, kw)) {
4093
4157
  return -1;
4094
4158
  }
4159
+ if(asked_on_another_screen(gobj, event, kw)) {
4160
+ return -1;
4161
+ }
4095
4162
  if(kw.what === "import") {
4096
4163
  return run_import(gobj);
4097
4164
  }
@@ -1504,7 +1504,7 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1504
1504
 
1505
1505
  if(!priv.gobj_remote_yuno) {
1506
1506
  log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
1507
- return;
1507
+ return -1;
1508
1508
  }
1509
1509
 
1510
1510
  let command = "update-node";
@@ -1528,7 +1528,9 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1528
1528
  );
1529
1529
  if(ret) {
1530
1530
  log_error(ret);
1531
+ return -1;
1531
1532
  }
1533
+ return 0;
1532
1534
  }
1533
1535
 
1534
1536
  /************************************************************
@@ -2113,6 +2115,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
2113
2115
  * would tell a schema editor that the schema changed because
2114
2116
  * somebody dragged a node.
2115
2117
  */
2118
+ if(result < 0 && command === "update-node") {
2119
+ graphs_write_refused(gobj,
2120
+ kw_get_str(gobj, kw_command, "topic_name", "", 0),
2121
+ kw_get_dict(gobj, kw_command, "record", {}, 0));
2122
+ }
2116
2123
  if(result >= 0 &&
2117
2124
  kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
2118
2125
  gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
@@ -2473,22 +2480,62 @@ function ac_update_node(gobj, event, kw, src)
2473
2480
  {
2474
2481
  let priv = gobj.priv;
2475
2482
 
2476
- if(refuse_if_readonly(gobj, event)) {
2477
- return -1;
2478
- }
2479
-
2480
2483
  let treedb_name = priv.treedb_name;
2481
2484
  let topic_name = kw.topic_name;
2482
2485
  let record = kw.record;
2483
2486
  let options = kw.options || {};
2484
2487
 
2485
- return treedb_update_node(
2488
+ if(refuse_if_readonly(gobj, event)) {
2489
+ graphs_write_refused(gobj, topic_name, record);
2490
+ return -1;
2491
+ }
2492
+
2493
+ /* The engine's arrangement is sent with no way back when it lands,
2494
+ * so one that cannot leave must be said here: out of session the
2495
+ * transport may answer nothing at all. */
2496
+ if(topic_name === "__graphs__" && !is_connected(gobj)) {
2497
+ log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
2498
+ `was not sent`);
2499
+ graphs_write_refused(gobj, topic_name, record);
2500
+ return -1;
2501
+ }
2502
+
2503
+ let ret = treedb_update_node(
2486
2504
  gobj,
2487
2505
  treedb_name,
2488
2506
  topic_name,
2489
2507
  record,
2490
2508
  options
2491
2509
  );
2510
+ if(ret < 0) {
2511
+ graphs_write_refused(gobj, topic_name, record);
2512
+ }
2513
+ return ret;
2514
+ }
2515
+
2516
+ /************************************************************
2517
+ * A write of `__graphs__` did not land. The engine
2518
+ * (C_G6_NODES_TREE) took it for granted when it left -- nothing
2519
+ * answers it when it lands -- so it is told, and its next Save
2520
+ * writes the topic again. Other topics are the operator's data:
2521
+ * the answer's error says what happened to them.
2522
+ ************************************************************/
2523
+ function graphs_write_refused(gobj, topic_name, record)
2524
+ {
2525
+ let priv = gobj.priv;
2526
+
2527
+ if(topic_name !== "__graphs__") {
2528
+ return;
2529
+ }
2530
+ let topic = (record && typeof record === "object") ? (record.topic || record.id) : "";
2531
+ if(!topic) {
2532
+ log_error(`${gobj_short_name(gobj)}: a refused __graphs__ write names no topic`);
2533
+ return;
2534
+ }
2535
+ if(!priv.gobj_nodes_tree) {
2536
+ return; /* no engine: nothing believes it was saved */
2537
+ }
2538
+ gobj_send_event(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", {topic: topic}, gobj);
2492
2539
  }
2493
2540
 
2494
2541
  /********************************************
@@ -1500,8 +1500,8 @@ function create_tabulator(gobj)
1500
1500
  * what the ceiling costs is having to widen the column, or open
1501
1501
  * the record, to read the end of it.
1502
1502
  * `maxInitialWidth`, not `maxWidth`: the latter is a HARD ceiling
1503
- * that neither the drag handle nor the double click could pass
1504
- * (M29 of the 2026-09-21 review); this one caps only the width the
1503
+ * that neither the drag handle nor the double click could pass;
1504
+ * this one caps only the width the
1505
1505
  * layout gives, and leaves the column to the reader. */
1506
1506
  if(max_col_width > 0) {
1507
1507
  colDef.maxInitialWidth = max_col_width;
@@ -2153,8 +2153,8 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
2153
2153
  let items = treedb_hook_data_size(value);
2154
2154
 
2155
2155
  if(items > 0) {
2156
- /* DOM, not an HTML string: the row id is record data (M31
2157
- * of the 2026-09-21 review). A language change re-runs
2156
+ /* DOM, not an HTML string: the row id is record data. A
2157
+ * language change re-runs
2158
2158
  * setColumns(), which re-runs this. */
2159
2159
  value = createElement2(hook_cell_spec(
2160
2160
  row.id, col.id, items, t("show linked records"), "show linked records"
@@ -2250,7 +2250,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
2250
2250
  /*
2251
2251
  * A string is record data, and Tabulator would parse it as markup
2252
2252
  * (innerHTML): `a<b` lost its text, a field holding an `<img onerror>`
2253
- * ran it (M31 of the 2026-09-21 review). Shown as a text node.
2253
+ * ran it. Shown as a text node.
2254
2254
  */
2255
2255
  if(typeof value === "string" && value !== "") {
2256
2256
  value = cell_text(value);
@@ -4035,7 +4035,7 @@ function ac_new_row(gobj, event, kw, src)
4035
4035
  * What crosses the confirmation is the rows' IDENTITY, never their
4036
4036
  * position: the view applies EV_TREEDB_NODE_* of every writer while the
4037
4037
  * dialog is open, and a position names whatever row sits there when the
4038
- * person answers -- with force:true (A6 of the 2026-09-21 review).
4038
+ * person answers -- with force:true.
4039
4039
  ************************************************************/
4040
4040
  function ac_delete_rows(gobj, event, kw, src)
4041
4041
  {
@@ -4103,8 +4103,8 @@ function ac_confirmed(gobj, event, kw, src)
4103
4103
  * them): never replaced by the row that took their place. */
4104
4104
  log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while ` +
4105
4105
  `confirming the delete, not deleted: ${found.missing.join(", ")}`);
4106
- /* The person who answered "yes" is told, not only the log
4107
- * (a low of the 2026-09-22 review). */
4106
+ /* The person who answered "yes" is told, not only the
4107
+ * log. */
4108
4108
  yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", {t: t});
4109
4109
  }
4110
4110
  if(!found.rows.length) {
@@ -4374,8 +4374,8 @@ function ac_files_read(gobj, event, kw, src)
4374
4374
  * open and BUSY, and the serial it waits for is taken here.
4375
4375
  *
4376
4376
  * A refused write used to close the form all the same and throw
4377
- * away what was typed, although the README said it stayed open
4378
- * (M25 of the 2026-09-21 review): the close was posted with the
4377
+ * away what was typed, although the README said it stayed open:
4378
+ * the close was posted with the
4379
4379
  * publish, before the backend had answered anything.
4380
4380
  ************************************************************/
4381
4381
  function wait_for_write_answer(gobj)