@yuneta/gobj-ui 7.23.170 → 7.23.172

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.
@@ -1,5 +1,4 @@
1
- import * as gobj_js from "@yuneta/gobj-js";
2
- import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
1
+ import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
3
2
  import i18next, { t } from "i18next";
4
3
  import TomSelect from "tom-select";
5
4
  import { createJSONEditor } from "vanilla-jsoneditor";
@@ -23046,13 +23045,455 @@ function nodes_answer(data) {
23046
23045
  };
23047
23046
  }
23048
23047
  //#endregion
23048
+ //#region src/schema_model.js
23049
+ /***********************************************************************
23050
+ * schema_model.js
23051
+ *
23052
+ * A SCHEMA out of the three meta-topics that store it.
23053
+ *
23054
+ * `treedb_system_schema` keeps every schema of a yuno as DATA, in
23055
+ * three flat topics — `treedbs`, `topics`, `cols` — linked by
23056
+ * fkeys. That is the right STORAGE and the wrong thing to put in
23057
+ * front of an operator: adding one column to one topic means
23058
+ * finding it in a table holding every column of every topic of
23059
+ * every treedb the yuno has.
23060
+ *
23061
+ * This module turns those three lists back into what they
23062
+ * describe: treedb -> topics -> columns, each in its declared
23063
+ * `order`. Pure, so the editor's model can be tested with no DOM
23064
+ * and no backend — the gclass fetches and draws, this decides
23065
+ * what the records MEAN.
23066
+ *
23067
+ * TWO THINGS IT EXISTS TO STATE ONCE:
23068
+ *
23069
+ * 1. A record is grouped by its FKEY, not by its id. The id is
23070
+ * qualified (`<treedb>.<topic>.<column>`) and reading it back
23071
+ * with a split on '.' works right up to the first treedb or
23072
+ * topic whose name carries one. The fkey is the link the store
23073
+ * itself uses; the id is only the fallback for a record whose
23074
+ * fkey never arrived.
23075
+ *
23076
+ * 2. A fkey reaches this code in FOUR shapes, because `nodes`
23077
+ * answers differently depending on the options it was called
23078
+ * with: the bare ref string, the expanded object, a list of
23079
+ * either, or a dict keyed by the ref. All four are the same
23080
+ * fact and parse_fkey_ref() flattens them to it.
23081
+ *
23082
+ * Copyright (c) 2026, ArtGins.
23083
+ * All Rights Reserved.
23084
+ ***********************************************************************/
23085
+ var DEFAULT_ORDER = 9999;
23086
+ /***************************************************************
23087
+ * parse_fkey_ref(value) -> [{topic_name, id, hook_name}, ...]
23088
+ *
23089
+ * Every shape a fkey column can arrive in, flattened to the
23090
+ * refs it names. Anything unrecognizable contributes nothing —
23091
+ * a schema drawn without one link beats a screen that throws.
23092
+ ***************************************************************/
23093
+ function parse_fkey_ref(value) {
23094
+ let refs = [];
23095
+ let push_one = (v) => {
23096
+ if (typeof v === "string") {
23097
+ let parts = v.split("^");
23098
+ if (parts.length !== 3) return;
23099
+ refs.push({
23100
+ topic_name: parts[0],
23101
+ id: parts[1],
23102
+ hook_name: parts[2]
23103
+ });
23104
+ return;
23105
+ }
23106
+ if (v && typeof v === "object") {
23107
+ let id = v.id;
23108
+ if (typeof id !== "string" || id.length === 0) return;
23109
+ refs.push({
23110
+ topic_name: v.topic_name || "",
23111
+ id,
23112
+ hook_name: v.hook_name || ""
23113
+ });
23114
+ }
23115
+ };
23116
+ if (value === null || value === void 0) return refs;
23117
+ if (typeof value === "string") {
23118
+ push_one(value);
23119
+ return refs;
23120
+ }
23121
+ if (Array.isArray(value)) {
23122
+ for (let v of value) push_one(v);
23123
+ return refs;
23124
+ }
23125
+ if (typeof value === "object") for (let [key, v] of Object.entries(value)) if (v && typeof v === "object" && typeof v.id === "string") push_one(v);
23126
+ else push_one(key);
23127
+ return refs;
23128
+ }
23129
+ /***************************************************************
23130
+ * The id of the FIRST parent a record names, or "" when it
23131
+ * names none. A record has one parent here — a column belongs to
23132
+ * one topic, a topic to one treedb — and a second ref would be a
23133
+ * store this editor cannot draw anyway.
23134
+ ***************************************************************/
23135
+ function parent_id(record, fkey_col) {
23136
+ let refs = parse_fkey_ref(record ? record[fkey_col] : null);
23137
+ return refs.length > 0 ? refs[0].id : "";
23138
+ }
23139
+ /***************************************************************
23140
+ * The name a record is KNOWN by, which is not what it is keyed
23141
+ * by: `topics` and `cols` key by the qualified id and carry the
23142
+ * bare name in `value` (its pkey2). An older store that still
23143
+ * keyed by rowid has no `value` worth showing, so the id stands
23144
+ * in — the same fallback as node_label().
23145
+ ***************************************************************/
23146
+ function record_name(record) {
23147
+ if (!record) return "";
23148
+ if (typeof record.value === "string" && record.value.length > 0) return record.value;
23149
+ return typeof record.id === "string" ? record.id : "";
23150
+ }
23151
+ /***************************************************************
23152
+ * The `order` of a record as a NUMBER, whatever the store put
23153
+ * there: an integer column read back from JSON may arrive as a
23154
+ * string, and comparing "10" with 9 sorts the schema wrong.
23155
+ ***************************************************************/
23156
+ function record_order(record) {
23157
+ let order = record ? record.order : void 0;
23158
+ if (typeof order === "number" && isFinite(order)) return order;
23159
+ if (typeof order === "string" && order.trim().length > 0) {
23160
+ let n = Number(order);
23161
+ if (isFinite(n)) return n;
23162
+ }
23163
+ return DEFAULT_ORDER;
23164
+ }
23165
+ /***************************************************************
23166
+ * Sort in place by `order`, ties broken by name so a schema
23167
+ * whose records all default to 9999 still draws the same way
23168
+ * twice.
23169
+ ***************************************************************/
23170
+ function sort_by_order(list) {
23171
+ list.sort((a, b) => {
23172
+ if (a.order !== b.order) return a.order - b.order;
23173
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
23174
+ });
23175
+ return list;
23176
+ }
23177
+ /***************************************************************
23178
+ * build_schema_model({treedbs, topics, cols}) -> model
23179
+ *
23180
+ * model = {
23181
+ * treedbs: [{
23182
+ * id, name, record,
23183
+ * schema_version, c_schema_version, system_schema_version,
23184
+ * topics: [{
23185
+ * id, name, order, record,
23186
+ * pkey, topic_version, system_topic, main_topic,
23187
+ * cols: [{id, name, order, record}]
23188
+ * }]
23189
+ * }],
23190
+ * orphan_topics: [...], topics whose treedb is not in the store
23191
+ * orphan_cols: [...] columns whose topic is not in the store
23192
+ * }
23193
+ *
23194
+ * An orphan is NOT dropped. A column whose topic was deleted is
23195
+ * exactly the kind of leftover an operator opens this editor to
23196
+ * find, and a model that hides it makes the editor lie.
23197
+ ***************************************************************/
23198
+ function build_schema_model(records) {
23199
+ let r = records || {};
23200
+ let treedb_rows = Array.isArray(r.treedbs) ? r.treedbs : [];
23201
+ let topic_rows = Array.isArray(r.topics) ? r.topics : [];
23202
+ let col_rows = Array.isArray(r.cols) ? r.cols : [];
23203
+ let treedbs = [];
23204
+ let by_treedb_id = {};
23205
+ for (let record of treedb_rows) {
23206
+ if (!record || typeof record.id !== "string") continue;
23207
+ let entry = {
23208
+ id: record.id,
23209
+ name: record.id,
23210
+ record,
23211
+ schema_version: record.schema_version,
23212
+ c_schema_version: record.c_schema_version,
23213
+ system_schema_version: record.system_schema_version,
23214
+ topics: []
23215
+ };
23216
+ treedbs.push(entry);
23217
+ by_treedb_id[entry.id] = entry;
23218
+ }
23219
+ let orphan_topics = [];
23220
+ let by_topic_id = {};
23221
+ for (let record of topic_rows) {
23222
+ if (!record || typeof record.id !== "string") continue;
23223
+ let entry = {
23224
+ id: record.id,
23225
+ name: record_name(record),
23226
+ order: record_order(record),
23227
+ record,
23228
+ treedb_id: parent_id(record, "treedbs"),
23229
+ pkey: record.pkey || "",
23230
+ pkey2s: record.pkey2s,
23231
+ system_flag: record.system_flag || "",
23232
+ tkey: record.tkey || "",
23233
+ topic_version: record.topic_version,
23234
+ system_topic: !!record.system_topic,
23235
+ main_topic: !!record.main_topic,
23236
+ cols: []
23237
+ };
23238
+ by_topic_id[entry.id] = entry;
23239
+ let parent = by_treedb_id[entry.treedb_id];
23240
+ if (parent) parent.topics.push(entry);
23241
+ else orphan_topics.push(entry);
23242
+ }
23243
+ let orphan_cols = [];
23244
+ for (let record of col_rows) {
23245
+ if (!record || typeof record.id !== "string") continue;
23246
+ let entry = {
23247
+ id: record.id,
23248
+ name: record_name(record),
23249
+ order: record_order(record),
23250
+ record,
23251
+ topic_id: parent_id(record, "topics")
23252
+ };
23253
+ let parent = by_topic_id[entry.topic_id];
23254
+ if (parent) parent.cols.push(entry);
23255
+ else orphan_cols.push(entry);
23256
+ }
23257
+ for (let treedb of treedbs) {
23258
+ sort_by_order(treedb.topics);
23259
+ for (let topic of treedb.topics) sort_by_order(topic.cols);
23260
+ }
23261
+ for (let topic of orphan_topics) sort_by_order(topic.cols);
23262
+ treedbs.sort((a, b) => {
23263
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
23264
+ });
23265
+ sort_by_order(orphan_topics);
23266
+ sort_by_order(orphan_cols);
23267
+ return {
23268
+ treedbs,
23269
+ orphan_topics,
23270
+ orphan_cols
23271
+ };
23272
+ }
23273
+ /***************************************************************
23274
+ * Is there a value here at all?
23275
+ *
23276
+ * The store answers a `blob` column that was never set with an
23277
+ * EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
23278
+ * `default: {}`. Read as "a value", those turn every export into
23279
+ * a schema full of empty objects — and `'hook': {}` in a literal
23280
+ * is a hook with no mapping, which is a link the treedb builds
23281
+ * and nothing writes.
23282
+ ***************************************************************/
23283
+ function is_empty_value(value) {
23284
+ if (value === null || value === void 0 || value === "") return true;
23285
+ if (Array.isArray(value)) return value.length === 0;
23286
+ if (typeof value === "object") return Object.keys(value).length === 0;
23287
+ return false;
23288
+ }
23289
+ /***************************************************************
23290
+ * The fields of a column that are DECLARED as one thing and
23291
+ * STORED as another.
23292
+ *
23293
+ * `hook`, `enum`, `default`, `pkey2s` and `properties` are `blob`
23294
+ * columns of the `cols` topic (treedb_system_schema.c), so what
23295
+ * comes back may be the value or the JSON text of the value,
23296
+ * depending on how it was written — a projection writes the
23297
+ * value, a hand edit through a plain text field writes the text.
23298
+ * `flag` is an array that a single-flag edit can leave as a bare
23299
+ * string. Reading these without saying so is how a hook drawn
23300
+ * from `{"users":"department"}` becomes no hook at all.
23301
+ ***************************************************************/
23302
+ function as_json(value) {
23303
+ if (typeof value !== "string") return value;
23304
+ let text = value.trim();
23305
+ if (text.length === 0) return null;
23306
+ try {
23307
+ return JSON.parse(text);
23308
+ } catch (e) {
23309
+ return value;
23310
+ }
23311
+ }
23312
+ function col_flags(record) {
23313
+ let flag = record ? record.flag : null;
23314
+ if (Array.isArray(flag)) return flag.filter((f) => typeof f === "string" && f.length > 0);
23315
+ if (typeof flag === "string" && flag.length > 0) {
23316
+ let parsed = as_json(flag);
23317
+ if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string" && f.length > 0);
23318
+ return [flag];
23319
+ }
23320
+ return [];
23321
+ }
23322
+ function col_hook(record) {
23323
+ let hook = as_json(record ? record.hook : null);
23324
+ if (hook && typeof hook === "object" && !Array.isArray(hook)) return hook;
23325
+ return null;
23326
+ }
23327
+ function col_enum(record) {
23328
+ let e = as_json(record ? record.enum : null);
23329
+ if (Array.isArray(e)) return e;
23330
+ return [];
23331
+ }
23332
+ function topic_pkey2s(record) {
23333
+ let pkey2s = as_json(record ? record.pkey2s : null);
23334
+ if (Array.isArray(pkey2s)) return pkey2s.filter((k) => typeof k === "string" && k.length > 0);
23335
+ if (typeof pkey2s === "string" && pkey2s.length > 0) return [pkey2s];
23336
+ return [];
23337
+ }
23338
+ /***************************************************************
23339
+ * Lookups by id. The editor addresses a position by NAME (the
23340
+ * url carries `<treedb>/<topic>`), so both are offered.
23341
+ ***************************************************************/
23342
+ function find_treedb(model, treedb_id) {
23343
+ if (!model || !Array.isArray(model.treedbs)) return null;
23344
+ for (let treedb of model.treedbs) if (treedb.id === treedb_id) return treedb;
23345
+ return null;
23346
+ }
23347
+ function find_topic(model, treedb_id, topic_name) {
23348
+ let treedb = find_treedb(model, treedb_id);
23349
+ if (!treedb) return null;
23350
+ for (let topic of treedb.topics) if (topic.name === topic_name || topic.id === topic_name) return topic;
23351
+ return null;
23352
+ }
23353
+ function find_col(model, treedb_id, topic_name, col_name) {
23354
+ let topic = find_topic(model, treedb_id, topic_name);
23355
+ if (!topic) return null;
23356
+ for (let col of topic.cols) if (col.name === col_name || col.id === col_name) return col;
23357
+ return null;
23358
+ }
23359
+ /***************************************************************
23360
+ * The fkey a NEW record must carry to belong to `parent_id`.
23361
+ * Written here because the editor is the only writer that has to
23362
+ * compose one, and getting the hook name wrong links the record
23363
+ * to nothing while the write still succeeds.
23364
+ ***************************************************************/
23365
+ function fkey_ref(parent_topic, parent_id_, hook_name) {
23366
+ return `${parent_topic}^${parent_id_}^${hook_name}`;
23367
+ }
23368
+ /***************************************************************
23369
+ * Where a new sibling goes: after the last one. The store's own
23370
+ * default (9999) would put every hand-made column in the same
23371
+ * place and let their names decide the schema's order.
23372
+ ***************************************************************/
23373
+ function next_order(siblings) {
23374
+ let max = 0;
23375
+ if (Array.isArray(siblings)) for (let s of siblings) {
23376
+ let order = s && typeof s.order === "number" ? s.order : DEFAULT_ORDER;
23377
+ if (order < 9999 && order > max) max = order;
23378
+ }
23379
+ return max + 1;
23380
+ }
23381
+ /***************************************************************
23382
+ * Renumber a list AFTER a move, as the writes it takes: only the
23383
+ * records whose order actually changed, so a drag of one row in a
23384
+ * 40-column topic is not 40 writes.
23385
+ *
23386
+ * moved_orders(list) -> [{id, order}, ...]
23387
+ ***************************************************************/
23388
+ function moved_orders(list) {
23389
+ let writes = [];
23390
+ if (!Array.isArray(list)) return writes;
23391
+ for (let i = 0; i < list.length; i++) {
23392
+ let entry = list[i];
23393
+ let order = i + 1;
23394
+ if (!entry || typeof entry.id !== "string") continue;
23395
+ if (entry.order !== order) writes.push({
23396
+ id: entry.id,
23397
+ order
23398
+ });
23399
+ }
23400
+ return writes;
23401
+ }
23402
+ //#endregion
23403
+ //#region src/treedb_topic_keys.js
23404
+ /***********************************************************************
23405
+ * treedb_topic_keys.js
23406
+ *
23407
+ * The KEYS of a treedb topic, read from its desc: the pkey, the
23408
+ * secondary keys (`pkey2s`) and the time key (`tkey`), plus the
23409
+ * names behind its `system_flag` number.
23410
+ *
23411
+ * Every view that describes a topic -- the schema diagram, the
23412
+ * topic-info panel, the topic cards -- must say them, and say them
23413
+ * the same way. A topic with `pkey2s` is not a topic with one more
23414
+ * column: one id holds several INSTANCES, an update has to carry
23415
+ * the pkey2 or it lands on the primary, and `delete-instance`
23416
+ * exists. `tkey` names where the time of every record comes from.
23417
+ * The views said neither, so this is where they ask.
23418
+ *
23419
+ * Pure, so it is tested with no DOM.
23420
+ *
23421
+ * Copyright (c) 2026, ArtGins.
23422
+ * All Rights Reserved.
23423
+ ***********************************************************************/
23424
+ var SF_NAMES = [
23425
+ "sf_string_key",
23426
+ "sf_rowid_key",
23427
+ "sf_int_key",
23428
+ "",
23429
+ "sf_zip_record",
23430
+ "sf_cipher_record",
23431
+ "",
23432
+ "",
23433
+ "sf_t_ms",
23434
+ "sf_tm_ms",
23435
+ "sf_deleted_instance",
23436
+ "sf_immutable_record",
23437
+ "sf_loading_from_disk",
23438
+ "",
23439
+ "",
23440
+ ""
23441
+ ];
23442
+ /************************************************************
23443
+ * The secondary keys of a topic, always a list. The desc
23444
+ * carries a list; a schema literal may declare a bare
23445
+ * string.
23446
+ ************************************************************/
23447
+ function desc_pkey2s(desc) {
23448
+ return topic_pkey2s(desc);
23449
+ }
23450
+ /************************************************************
23451
+ * The time key of a topic, "" when it has none -- and then
23452
+ * the time of a record is the time it was appended.
23453
+ ************************************************************/
23454
+ function desc_tkey(desc) {
23455
+ let tkey = desc ? desc.tkey : "";
23456
+ return typeof tkey === "string" ? tkey : "";
23457
+ }
23458
+ /************************************************************
23459
+ * Which keys of its topic a column IS, in the order the
23460
+ * views print them: "pkey", "pkey2", "tkey". Empty for a
23461
+ * column that is none.
23462
+ ************************************************************/
23463
+ function col_key_roles(desc, col_id) {
23464
+ let roles = [];
23465
+ if (!desc || typeof col_id !== "string" || col_id.length === 0) return roles;
23466
+ if (col_id === (desc.pkey || "id")) roles.push("pkey");
23467
+ if (desc_pkey2s(desc).includes(col_id)) roles.push("pkey2");
23468
+ if (desc_tkey(desc) === col_id) roles.push("tkey");
23469
+ return roles;
23470
+ }
23471
+ /************************************************************
23472
+ * The names of a system_flag. The desc carries the NUMBER
23473
+ * (`1`), a schema literal carries the names
23474
+ * ("sf_string_key|sf_t_ms"); both come back as a list. A
23475
+ * set bit with no name is shown by its value, never
23476
+ * dropped.
23477
+ ************************************************************/
23478
+ function system_flag_names(system_flag) {
23479
+ if (Array.isArray(system_flag)) return system_flag.filter((n) => typeof n === "string" && n.length > 0);
23480
+ if (typeof system_flag === "string") return system_flag.split(/[|,]/).map((n) => n.trim()).filter((n) => n.length > 0);
23481
+ if (typeof system_flag !== "number" || !Number.isInteger(system_flag) || system_flag <= 0) return [];
23482
+ let names = [];
23483
+ for (let bit = 0; bit < SF_NAMES.length; bit++) {
23484
+ let mask = 1 << bit;
23485
+ if (system_flag & mask) names.push(SF_NAMES[bit] || `0x${mask.toString(16).padStart(4, "0")}`);
23486
+ }
23487
+ return names;
23488
+ }
23489
+ //#endregion
23049
23490
  //#region src/c_yui_treedb_topics.js
23050
23491
  /***********************************************************************
23051
23492
  * c_yui_treedb_topics.js
23052
23493
  *
23053
23494
  * Management of TreeDB's topics with Bulma tabs
23054
23495
  *
23055
- * Copyright (c) 2025, ArtGins.
23496
+ * Copyright (c) 2025-2026, ArtGins.
23056
23497
  * All Rights Reserved.
23057
23498
  ***********************************************************************/
23058
23499
  /***************************************************************
@@ -23566,6 +24007,29 @@ function topic_card_meta(desc) {
23566
24007
  { class: "TREEDB_TOPIC_CARD_META" },
23567
24008
  head
23568
24009
  ]);
24010
+ let pkey2s = desc_pkey2s(desc);
24011
+ if (pkey2s.length) {
24012
+ let list = pkey2s.join(", ");
24013
+ meta.push([
24014
+ "div",
24015
+ {
24016
+ class: "TREEDB_TOPIC_CARD_REL TREEDB_TOPIC_CARD_PKEY2S",
24017
+ title: list
24018
+ },
24019
+ [[
24020
+ "span",
24021
+ {
24022
+ class: "TREEDB_TOPIC_CARD_REL_LABEL",
24023
+ i18n: "pkey2s"
24024
+ },
24025
+ "pkey2s"
24026
+ ], [
24027
+ "span",
24028
+ { class: "TREEDB_TOPIC_CARD_REL_LIST" },
24029
+ list
24030
+ ]]
24031
+ ]);
24032
+ }
23569
24033
  let relation = (cls, key, arrow, names) => {
23570
24034
  let list = names.join(", ");
23571
24035
  return [
@@ -23829,33 +24293,26 @@ function show_topic_info(gobj, topic) {
23829
24293
  }
23830
24294
  /************************************************************
23831
24295
  * Build the read-only topic-info panel from a topic desc: the
23832
- * topic name, its pkey, and a table of columns (name / type /
24296
+ * topic name, its KEYS, and a table of columns (name / type /
23833
24297
  * key relationship). Everything is guarded — a malformed desc
23834
24298
  * renders a shorter panel, never throws.
24299
+ *
24300
+ * The keys are what an operator has to know before touching a
24301
+ * record, so they are all said, and the empty ones too: a
24302
+ * topic with `pkey2s` keeps several instances under one id,
24303
+ * and an empty `tkey` means the time of a record is the time
24304
+ * it was appended -- a row missing is not the same as "none".
23835
24305
  ************************************************************/
23836
24306
  function build_topic_info_panel(gobj, $info, topic, desc) {
23837
24307
  while ($info.firstChild) $info.removeChild($info.firstChild);
23838
24308
  let pkey = desc.pkey || "";
23839
- let fmt_flag = (v) => {
23840
- if (is_array(v)) return v.join(", ");
23841
- if (is_object(v)) return Object.keys(v).join(", ");
23842
- return v === void 0 || v === null ? "" : String(v);
23843
- };
24309
+ let pkey2s = desc_pkey2s(desc);
24310
+ let tkey = desc_tkey(desc);
23844
24311
  let $meta = [];
23845
- let push_meta = (key, value, highlight) => {
23846
- if (value === void 0 || value === null || value === "") return;
23847
- let $val = highlight ? [
23848
- "span",
23849
- { class: "tag is-info" },
23850
- `${value}`
23851
- ] : [
23852
- "code",
23853
- {},
23854
- `${value}`
23855
- ];
24312
+ let push_meta = (key, $val) => {
23856
24313
  $meta.push([
23857
24314
  "tr",
23858
- {},
24315
+ { class: `TREEDB_TOPIC_INFO_${key.toUpperCase()}` },
23859
24316
  [[
23860
24317
  "th",
23861
24318
  {
@@ -23870,29 +24327,64 @@ function build_topic_info_panel(gobj, $info, topic, desc) {
23870
24327
  ]]
23871
24328
  ]);
23872
24329
  };
23873
- push_meta("version", desc.topic_version, true);
23874
- push_meta("system", fmt_flag(desc.system_flag));
23875
- push_meta("pkey", pkey);
23876
- push_meta("tkey", desc.tkey);
24330
+ if (desc.topic_version !== void 0 && desc.topic_version !== null && desc.topic_version !== "") push_meta("version", [
24331
+ "span",
24332
+ { class: "tag is-info" },
24333
+ `${desc.topic_version}`
24334
+ ]);
24335
+ let sf_names = system_flag_names(desc.system_flag);
24336
+ if (sf_names.length > 0) push_meta("system", [
24337
+ "code",
24338
+ { title: `${desc.system_flag}` },
24339
+ sf_names.join(" | ")
24340
+ ]);
24341
+ if (pkey) push_meta("pkey", [
24342
+ "code",
24343
+ {},
24344
+ pkey
24345
+ ]);
24346
+ if (pkey2s.length > 0) push_meta("pkey2s", [
24347
+ "span",
24348
+ { class: "tags" },
24349
+ pkey2s.map((name) => [
24350
+ "span",
24351
+ { class: "tag is-warning" },
24352
+ name
24353
+ ])
24354
+ ]);
24355
+ push_meta("tkey", tkey ? [
24356
+ "code",
24357
+ {},
24358
+ tkey
24359
+ ] : [
24360
+ "span",
24361
+ {
24362
+ class: "has-text-grey",
24363
+ i18n: "append time"
24364
+ },
24365
+ "append time"
24366
+ ]);
23877
24367
  let $rows = [];
23878
24368
  let cols = is_array(desc.cols) ? desc.cols : [];
23879
24369
  for (let col of cols) {
23880
24370
  if (!col || !col.id || col.id.charAt(0) === "_") continue;
23881
24371
  let type = col.type || (is_array(col.flag) ? col.flag.join(", ") : col.flag || "");
23882
- let rel = "";
23883
- if (col.id === pkey) rel = "pkey";
23884
- else if (is_object(col.fkey)) rel = "→ " + Object.keys(col.fkey).join(", ");
23885
- else if (is_object(col.hook)) rel = "hook → " + Object.keys(col.hook).join(", ");
24372
+ let roles = col_key_roles(desc, col.id);
24373
+ let parts = roles.slice();
24374
+ if (is_object(col.fkey)) parts.push("→ " + Object.keys(col.fkey).join(", "));
24375
+ else if (is_object(col.hook)) parts.push("hook → " + Object.keys(col.hook).join(", "));
24376
+ let rel = parts.join(" · ");
24377
+ let is_key = roles.length > 0;
23886
24378
  $rows.push([
23887
24379
  "tr",
23888
- {},
24380
+ is_key ? { class: "TREEDB_TOPIC_INFO_KEY_COL" } : {},
23889
24381
  [
23890
24382
  [
23891
24383
  "td",
23892
24384
  {},
23893
24385
  [[
23894
24386
  "code",
23895
- {},
24387
+ is_key ? { class: "has-text-weight-bold" } : {},
23896
24388
  `${col.id}`
23897
24389
  ]]
23898
24390
  ],
@@ -23903,7 +24395,7 @@ function build_topic_info_panel(gobj, $info, topic, desc) {
23903
24395
  ],
23904
24396
  [
23905
24397
  "td",
23906
- {},
24398
+ is_key ? { class: "has-text-weight-semibold" } : {},
23907
24399
  `${rel}`
23908
24400
  ]
23909
24401
  ]
@@ -25638,361 +26130,6 @@ function row_matches(row, term) {
25638
26130
  });
25639
26131
  }
25640
26132
  //#endregion
25641
- //#region src/schema_model.js
25642
- /***********************************************************************
25643
- * schema_model.js
25644
- *
25645
- * A SCHEMA out of the three meta-topics that store it.
25646
- *
25647
- * `treedb_system_schema` keeps every schema of a yuno as DATA, in
25648
- * three flat topics — `treedbs`, `topics`, `cols` — linked by
25649
- * fkeys. That is the right STORAGE and the wrong thing to put in
25650
- * front of an operator: adding one column to one topic means
25651
- * finding it in a table holding every column of every topic of
25652
- * every treedb the yuno has.
25653
- *
25654
- * This module turns those three lists back into what they
25655
- * describe: treedb -> topics -> columns, each in its declared
25656
- * `order`. Pure, so the editor's model can be tested with no DOM
25657
- * and no backend — the gclass fetches and draws, this decides
25658
- * what the records MEAN.
25659
- *
25660
- * TWO THINGS IT EXISTS TO STATE ONCE:
25661
- *
25662
- * 1. A record is grouped by its FKEY, not by its id. The id is
25663
- * qualified (`<treedb>.<topic>.<column>`) and reading it back
25664
- * with a split on '.' works right up to the first treedb or
25665
- * topic whose name carries one. The fkey is the link the store
25666
- * itself uses; the id is only the fallback for a record whose
25667
- * fkey never arrived.
25668
- *
25669
- * 2. A fkey reaches this code in FOUR shapes, because `nodes`
25670
- * answers differently depending on the options it was called
25671
- * with: the bare ref string, the expanded object, a list of
25672
- * either, or a dict keyed by the ref. All four are the same
25673
- * fact and parse_fkey_ref() flattens them to it.
25674
- *
25675
- * Copyright (c) 2026, ArtGins.
25676
- * All Rights Reserved.
25677
- ***********************************************************************/
25678
- var DEFAULT_ORDER = 9999;
25679
- /***************************************************************
25680
- * parse_fkey_ref(value) -> [{topic_name, id, hook_name}, ...]
25681
- *
25682
- * Every shape a fkey column can arrive in, flattened to the
25683
- * refs it names. Anything unrecognizable contributes nothing —
25684
- * a schema drawn without one link beats a screen that throws.
25685
- ***************************************************************/
25686
- function parse_fkey_ref(value) {
25687
- let refs = [];
25688
- let push_one = (v) => {
25689
- if (typeof v === "string") {
25690
- let parts = v.split("^");
25691
- if (parts.length !== 3) return;
25692
- refs.push({
25693
- topic_name: parts[0],
25694
- id: parts[1],
25695
- hook_name: parts[2]
25696
- });
25697
- return;
25698
- }
25699
- if (v && typeof v === "object") {
25700
- let id = v.id;
25701
- if (typeof id !== "string" || id.length === 0) return;
25702
- refs.push({
25703
- topic_name: v.topic_name || "",
25704
- id,
25705
- hook_name: v.hook_name || ""
25706
- });
25707
- }
25708
- };
25709
- if (value === null || value === void 0) return refs;
25710
- if (typeof value === "string") {
25711
- push_one(value);
25712
- return refs;
25713
- }
25714
- if (Array.isArray(value)) {
25715
- for (let v of value) push_one(v);
25716
- return refs;
25717
- }
25718
- if (typeof value === "object") for (let [key, v] of Object.entries(value)) if (v && typeof v === "object" && typeof v.id === "string") push_one(v);
25719
- else push_one(key);
25720
- return refs;
25721
- }
25722
- /***************************************************************
25723
- * The id of the FIRST parent a record names, or "" when it
25724
- * names none. A record has one parent here — a column belongs to
25725
- * one topic, a topic to one treedb — and a second ref would be a
25726
- * store this editor cannot draw anyway.
25727
- ***************************************************************/
25728
- function parent_id(record, fkey_col) {
25729
- let refs = parse_fkey_ref(record ? record[fkey_col] : null);
25730
- return refs.length > 0 ? refs[0].id : "";
25731
- }
25732
- /***************************************************************
25733
- * The name a record is KNOWN by, which is not what it is keyed
25734
- * by: `topics` and `cols` key by the qualified id and carry the
25735
- * bare name in `value` (its pkey2). An older store that still
25736
- * keyed by rowid has no `value` worth showing, so the id stands
25737
- * in — the same fallback as node_label().
25738
- ***************************************************************/
25739
- function record_name(record) {
25740
- if (!record) return "";
25741
- if (typeof record.value === "string" && record.value.length > 0) return record.value;
25742
- return typeof record.id === "string" ? record.id : "";
25743
- }
25744
- /***************************************************************
25745
- * The `order` of a record as a NUMBER, whatever the store put
25746
- * there: an integer column read back from JSON may arrive as a
25747
- * string, and comparing "10" with 9 sorts the schema wrong.
25748
- ***************************************************************/
25749
- function record_order(record) {
25750
- let order = record ? record.order : void 0;
25751
- if (typeof order === "number" && isFinite(order)) return order;
25752
- if (typeof order === "string" && order.trim().length > 0) {
25753
- let n = Number(order);
25754
- if (isFinite(n)) return n;
25755
- }
25756
- return DEFAULT_ORDER;
25757
- }
25758
- /***************************************************************
25759
- * Sort in place by `order`, ties broken by name so a schema
25760
- * whose records all default to 9999 still draws the same way
25761
- * twice.
25762
- ***************************************************************/
25763
- function sort_by_order(list) {
25764
- list.sort((a, b) => {
25765
- if (a.order !== b.order) return a.order - b.order;
25766
- return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
25767
- });
25768
- return list;
25769
- }
25770
- /***************************************************************
25771
- * build_schema_model({treedbs, topics, cols}) -> model
25772
- *
25773
- * model = {
25774
- * treedbs: [{
25775
- * id, name, record,
25776
- * schema_version, c_schema_version, system_schema_version,
25777
- * topics: [{
25778
- * id, name, order, record,
25779
- * pkey, topic_version, system_topic, main_topic,
25780
- * cols: [{id, name, order, record}]
25781
- * }]
25782
- * }],
25783
- * orphan_topics: [...], topics whose treedb is not in the store
25784
- * orphan_cols: [...] columns whose topic is not in the store
25785
- * }
25786
- *
25787
- * An orphan is NOT dropped. A column whose topic was deleted is
25788
- * exactly the kind of leftover an operator opens this editor to
25789
- * find, and a model that hides it makes the editor lie.
25790
- ***************************************************************/
25791
- function build_schema_model(records) {
25792
- let r = records || {};
25793
- let treedb_rows = Array.isArray(r.treedbs) ? r.treedbs : [];
25794
- let topic_rows = Array.isArray(r.topics) ? r.topics : [];
25795
- let col_rows = Array.isArray(r.cols) ? r.cols : [];
25796
- let treedbs = [];
25797
- let by_treedb_id = {};
25798
- for (let record of treedb_rows) {
25799
- if (!record || typeof record.id !== "string") continue;
25800
- let entry = {
25801
- id: record.id,
25802
- name: record.id,
25803
- record,
25804
- schema_version: record.schema_version,
25805
- c_schema_version: record.c_schema_version,
25806
- system_schema_version: record.system_schema_version,
25807
- topics: []
25808
- };
25809
- treedbs.push(entry);
25810
- by_treedb_id[entry.id] = entry;
25811
- }
25812
- let orphan_topics = [];
25813
- let by_topic_id = {};
25814
- for (let record of topic_rows) {
25815
- if (!record || typeof record.id !== "string") continue;
25816
- let entry = {
25817
- id: record.id,
25818
- name: record_name(record),
25819
- order: record_order(record),
25820
- record,
25821
- treedb_id: parent_id(record, "treedbs"),
25822
- pkey: record.pkey || "",
25823
- pkey2s: record.pkey2s,
25824
- system_flag: record.system_flag || "",
25825
- tkey: record.tkey || "",
25826
- topic_version: record.topic_version,
25827
- system_topic: !!record.system_topic,
25828
- main_topic: !!record.main_topic,
25829
- cols: []
25830
- };
25831
- by_topic_id[entry.id] = entry;
25832
- let parent = by_treedb_id[entry.treedb_id];
25833
- if (parent) parent.topics.push(entry);
25834
- else orphan_topics.push(entry);
25835
- }
25836
- let orphan_cols = [];
25837
- for (let record of col_rows) {
25838
- if (!record || typeof record.id !== "string") continue;
25839
- let entry = {
25840
- id: record.id,
25841
- name: record_name(record),
25842
- order: record_order(record),
25843
- record,
25844
- topic_id: parent_id(record, "topics")
25845
- };
25846
- let parent = by_topic_id[entry.topic_id];
25847
- if (parent) parent.cols.push(entry);
25848
- else orphan_cols.push(entry);
25849
- }
25850
- for (let treedb of treedbs) {
25851
- sort_by_order(treedb.topics);
25852
- for (let topic of treedb.topics) sort_by_order(topic.cols);
25853
- }
25854
- for (let topic of orphan_topics) sort_by_order(topic.cols);
25855
- treedbs.sort((a, b) => {
25856
- return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
25857
- });
25858
- sort_by_order(orphan_topics);
25859
- sort_by_order(orphan_cols);
25860
- return {
25861
- treedbs,
25862
- orphan_topics,
25863
- orphan_cols
25864
- };
25865
- }
25866
- /***************************************************************
25867
- * Is there a value here at all?
25868
- *
25869
- * The store answers a `blob` column that was never set with an
25870
- * EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
25871
- * `default: {}`. Read as "a value", those turn every export into
25872
- * a schema full of empty objects — and `'hook': {}` in a literal
25873
- * is a hook with no mapping, which is a link the treedb builds
25874
- * and nothing writes.
25875
- ***************************************************************/
25876
- function is_empty_value(value) {
25877
- if (value === null || value === void 0 || value === "") return true;
25878
- if (Array.isArray(value)) return value.length === 0;
25879
- if (typeof value === "object") return Object.keys(value).length === 0;
25880
- return false;
25881
- }
25882
- /***************************************************************
25883
- * The fields of a column that are DECLARED as one thing and
25884
- * STORED as another.
25885
- *
25886
- * `hook`, `enum`, `default`, `pkey2s` and `properties` are `blob`
25887
- * columns of the `cols` topic (treedb_system_schema.c), so what
25888
- * comes back may be the value or the JSON text of the value,
25889
- * depending on how it was written — a projection writes the
25890
- * value, a hand edit through a plain text field writes the text.
25891
- * `flag` is an array that a single-flag edit can leave as a bare
25892
- * string. Reading these without saying so is how a hook drawn
25893
- * from `{"users":"department"}` becomes no hook at all.
25894
- ***************************************************************/
25895
- function as_json(value) {
25896
- if (typeof value !== "string") return value;
25897
- let text = value.trim();
25898
- if (text.length === 0) return null;
25899
- try {
25900
- return JSON.parse(text);
25901
- } catch (e) {
25902
- return value;
25903
- }
25904
- }
25905
- function col_flags(record) {
25906
- let flag = record ? record.flag : null;
25907
- if (Array.isArray(flag)) return flag.filter((f) => typeof f === "string" && f.length > 0);
25908
- if (typeof flag === "string" && flag.length > 0) {
25909
- let parsed = as_json(flag);
25910
- if (Array.isArray(parsed)) return parsed.filter((f) => typeof f === "string" && f.length > 0);
25911
- return [flag];
25912
- }
25913
- return [];
25914
- }
25915
- function col_hook(record) {
25916
- let hook = as_json(record ? record.hook : null);
25917
- if (hook && typeof hook === "object" && !Array.isArray(hook)) return hook;
25918
- return null;
25919
- }
25920
- function col_enum(record) {
25921
- let e = as_json(record ? record.enum : null);
25922
- if (Array.isArray(e)) return e;
25923
- return [];
25924
- }
25925
- function topic_pkey2s(record) {
25926
- let pkey2s = as_json(record ? record.pkey2s : null);
25927
- if (Array.isArray(pkey2s)) return pkey2s.filter((k) => typeof k === "string" && k.length > 0);
25928
- if (typeof pkey2s === "string" && pkey2s.length > 0) return [pkey2s];
25929
- return [];
25930
- }
25931
- /***************************************************************
25932
- * Lookups by id. The editor addresses a position by NAME (the
25933
- * url carries `<treedb>/<topic>`), so both are offered.
25934
- ***************************************************************/
25935
- function find_treedb(model, treedb_id) {
25936
- if (!model || !Array.isArray(model.treedbs)) return null;
25937
- for (let treedb of model.treedbs) if (treedb.id === treedb_id) return treedb;
25938
- return null;
25939
- }
25940
- function find_topic(model, treedb_id, topic_name) {
25941
- let treedb = find_treedb(model, treedb_id);
25942
- if (!treedb) return null;
25943
- for (let topic of treedb.topics) if (topic.name === topic_name || topic.id === topic_name) return topic;
25944
- return null;
25945
- }
25946
- function find_col(model, treedb_id, topic_name, col_name) {
25947
- let topic = find_topic(model, treedb_id, topic_name);
25948
- if (!topic) return null;
25949
- for (let col of topic.cols) if (col.name === col_name || col.id === col_name) return col;
25950
- return null;
25951
- }
25952
- /***************************************************************
25953
- * The fkey a NEW record must carry to belong to `parent_id`.
25954
- * Written here because the editor is the only writer that has to
25955
- * compose one, and getting the hook name wrong links the record
25956
- * to nothing while the write still succeeds.
25957
- ***************************************************************/
25958
- function fkey_ref(parent_topic, parent_id_, hook_name) {
25959
- return `${parent_topic}^${parent_id_}^${hook_name}`;
25960
- }
25961
- /***************************************************************
25962
- * Where a new sibling goes: after the last one. The store's own
25963
- * default (9999) would put every hand-made column in the same
25964
- * place and let their names decide the schema's order.
25965
- ***************************************************************/
25966
- function next_order(siblings) {
25967
- let max = 0;
25968
- if (Array.isArray(siblings)) for (let s of siblings) {
25969
- let order = s && typeof s.order === "number" ? s.order : DEFAULT_ORDER;
25970
- if (order < 9999 && order > max) max = order;
25971
- }
25972
- return max + 1;
25973
- }
25974
- /***************************************************************
25975
- * Renumber a list AFTER a move, as the writes it takes: only the
25976
- * records whose order actually changed, so a drag of one row in a
25977
- * 40-column topic is not 40 writes.
25978
- *
25979
- * moved_orders(list) -> [{id, order}, ...]
25980
- ***************************************************************/
25981
- function moved_orders(list) {
25982
- let writes = [];
25983
- if (!Array.isArray(list)) return writes;
25984
- for (let i = 0; i < list.length; i++) {
25985
- let entry = list[i];
25986
- let order = i + 1;
25987
- if (!entry || typeof entry.id !== "string") continue;
25988
- if (entry.order !== order) writes.push({
25989
- id: entry.id,
25990
- order
25991
- });
25992
- }
25993
- return writes;
25994
- }
25995
- //#endregion
25996
26133
  //#region src/treedb_write_plan.js
25997
26134
  /***********************************************************************
25998
26135
  * treedb_write_plan.js
@@ -68838,12 +68975,20 @@ function hook_mark(type) {
68838
68975
  * (↖) 1 fkey (1 parent)
68839
68976
  * [↖] n fkeys (n parents)
68840
68977
  * {↖} N fkeys (N parents)
68978
+ * (2) a secondary key (pkey2)
68979
+ * (t) the time key (tkey)
68841
68980
  * * required
68842
68981
  * # the primary key
68843
68982
  *
68844
- * A column can be BOTH hook and fkey, so the flags are read
68845
- * here rather than through treedb_get_field_desc's single
68846
- * `type`, which keeps only the last flag it saw.
68983
+ * The key marks lead the mark column, and a pkey2 is drawn in
68984
+ * bold like the pkey: a topic with secondary keys keeps several
68985
+ * instances under one id, which is the first thing to know
68986
+ * about it. `(t)` is new to the notation: the literals' legend
68987
+ * carries it too.
68988
+ *
68989
+ * The flags are read here rather than through
68990
+ * treedb_get_field_desc's single `type`, which keeps only the
68991
+ * last flag it saw.
68847
68992
  ************************************************************/
68848
68993
  function schema_rows(desc) {
68849
68994
  let rows = [];
@@ -68853,19 +68998,25 @@ function schema_rows(desc) {
68853
68998
  let flags = Array.isArray(col.flag) ? col.flag : [];
68854
68999
  let is_hook = flags.indexOf("hook") >= 0;
68855
69000
  let is_fkey = flags.indexOf("fkey") >= 0;
69001
+ let roles = col_key_roles(desc, col.id);
69002
+ let key_marks = [];
69003
+ if (roles.includes("pkey2")) key_marks.push("(2)");
69004
+ if (roles.includes("tkey")) key_marks.push("(t)");
68856
69005
  let mark = "";
68857
69006
  if (is_hook) mark = hook_mark(col.type);
68858
69007
  if (is_fkey) {
68859
69008
  let fmark = hook_mark(col.type).replace("}", "↖}").replace("]", "↖]").replace(")", "↖)");
68860
69009
  mark = mark ? mark + " " + fmark : fmark;
68861
69010
  }
69011
+ if (key_marks.length) mark = mark ? key_marks.join(" ") + " " + mark : key_marks.join(" ");
68862
69012
  rows.push({
68863
69013
  name: col.id,
68864
69014
  type: col.type || "",
68865
69015
  mark,
68866
69016
  is_hook,
68867
69017
  is_fkey,
68868
- is_pkey: col.id === desc.pkey,
69018
+ is_pkey: roles.includes("pkey"),
69019
+ is_pkey2: roles.includes("pkey2"),
68869
69020
  required: flags.indexOf("required") >= 0 || flags.indexOf("notnull") >= 0
68870
69021
  });
68871
69022
  }
@@ -68912,7 +69063,7 @@ function build_card_innerHTML(topic, rows, color, dark) {
68912
69063
  let rows_html = "";
68913
69064
  for (let row of rows) {
68914
69065
  let lead = row.is_pkey ? "#" : row.required ? "*" : "";
68915
- let name_weight = row.is_pkey || row.required ? "600" : "400";
69066
+ let name_weight = row.is_pkey || row.is_pkey2 ? "700" : row.required ? "600" : "400";
68916
69067
  let name_color = row.is_hook || row.is_fkey ? title_color : text_color;
68917
69068
  rows_html += ` <div class="TREEDB_SCHEMA_FIELD" style="
68918
69069
  display: flex; align-items: center; gap: 6px;
@@ -76000,86 +76151,6 @@ function register_c_yui_period() {
76000
76151
  return create_gclass(GCLASS_NAME);
76001
76152
  }
76002
76153
  //#endregion
76003
- //#region src/dev_machine_trace.js
76004
- /***********************************************************************
76005
- * dev_machine_trace.js
76006
- *
76007
- * Reading a MACHINE trace line back.
76008
- *
76009
- * The developer window mirrors the runtime's log, and the
76010
- * `machine` trace is most of what lands there once it is on.
76011
- * Those lines arrive as text, so to the window they were just
76012
- * "a debug log" -- which is why its `Periodic` filter could
76013
- * not touch them: the filter works on a signature, and every
76014
- * mirrored log carried the same one (`log:debug`).
76015
- *
76016
- * This gives a machine line back its EVENT, which is the only
76017
- * thing that says whether it is noise. Both formats gobj-js
76018
- * emits (`gobj_set_trace_machine_format`):
76019
- *
76020
- * 0 verbose "🔄 mach(C_TIMER^t), st: ST_IDLE, ev: EV_TIMEOUT, ac: …"
76021
- * "<- mach(C_TIMER^t), st: ST_IDLE, ev: EV_TIMEOUT, ret: 0"
76022
- * 1 compact "🔄 EV_TIMEOUT C_TIMER^t ST_IDLE from C_TIMER^t"
76023
- *
76024
- * Copyright (c) 2026, ArtGins.
76025
- * All Rights Reserved.
76026
- ***********************************************************************/
76027
- var MACHINE_MARK_RE = /\bmach\(/;
76028
- var EVENT_FIELD_RE = /\bev:\s*([A-Za-z0-9_]+)/;
76029
- var EVENT_HEAD_RE = /^\s*\S{0,4}\s*(EV_[A-Za-z0-9_]+)(?=\s|$)/;
76030
- var PERIODIC_EVENT_RE = /PERIODIC|TIMEOUT|HEARTBEAT|PING|POLL/i;
76031
- /************************************************************
76032
- * The event of a machine trace line, or "" when the line is
76033
- * not one.
76034
- *
76035
- * "" is also the answer for a machine line that carries no
76036
- * event -- a state change ("new st(…), prev st(…)") -- and
76037
- * that is right: what it reports is not repetitive.
76038
- ************************************************************/
76039
- function machine_event_of(text) {
76040
- if (typeof text !== "string" || text.length === 0) return "";
76041
- if (MACHINE_MARK_RE.test(text)) {
76042
- let m = EVENT_FIELD_RE.exec(text);
76043
- return m ? m[1] : "";
76044
- }
76045
- let m = EVENT_HEAD_RE.exec(text);
76046
- return m ? m[1] : "";
76047
- }
76048
- /************************************************************
76049
- * Is this event one of the ones that arrive by themselves,
76050
- * for ever, and say nothing about what the app is doing?
76051
- ************************************************************/
76052
- function is_periodic_event(event_name) {
76053
- if (!event_name) return false;
76054
- return PERIODIC_EVENT_RE.test(event_name);
76055
- }
76056
- /************************************************************
76057
- * The signature of a mirrored log line: what the window
76058
- * counts it under and what its filters match on.
76059
- *
76060
- * A machine line is signed by its EVENT, so a hundred
76061
- * EV_TIMEOUTs are one recurring thing rather than a hundred
76062
- * anonymous debug lines. Everything else is signed by its
76063
- * level, as before.
76064
- ************************************************************/
76065
- function log_signature(level, text) {
76066
- let event = machine_event_of(text);
76067
- if (event) return "mach:" + event;
76068
- return "log:" + (level || "debug");
76069
- }
76070
- /************************************************************
76071
- * May the periodic filter hide this log line?
76072
- *
76073
- * Only a machine transition, and only below warning level.
76074
- * An ERROR that names EV_TIMEOUT -- "Event NOT DEFINED in
76075
- * state", which is exactly the loud one the framework wants
76076
- * seen -- must never disappear because of the word in it.
76077
- ************************************************************/
76078
- function log_is_periodic(level, text) {
76079
- if (level === "error" || level === "warning") return false;
76080
- return is_periodic_event(machine_event_of(text));
76081
- }
76082
- //#endregion
76083
76154
  //#region src/yui_dev.js
76084
76155
  /***********************************************************************
76085
76156
  * ui_dev.js
@@ -76097,10 +76168,7 @@ function log_is_periodic(level, text) {
76097
76168
  * losing history. Reopening the window repaints the buffer.
76098
76169
  ***********************************************************************/
76099
76170
  var TRAFFIC_MAX = 600;
76100
- var PERIODIC_THRESHOLD = 5;
76101
- var PERIODIC_RE = /PERIODIC|TIMEOUT|HEARTBEAT|PING/i;
76102
76171
  var TRAFFIC_LOG = [];
76103
- var TRAFFIC_COUNTS = /* @__PURE__ */ new Map();
76104
76172
  var SEARCH_TEXT = "";
76105
76173
  var LOG_ERR_COUNT = 0;
76106
76174
  var LOG_WARN_COUNT = 0;
@@ -76117,39 +76185,53 @@ var TRAFFIC_TS_FIELDS = {
76117
76185
  };
76118
76186
  var TRACE_DEFS = [
76119
76187
  [
76120
- "trace_automata",
76121
76188
  "automata",
76122
- trace_automata
76189
+ "automata",
76190
+ "trace every event of every automaton",
76191
+ automata_state,
76192
+ toggle_automata
76123
76193
  ],
76124
76194
  [
76125
- "trace_creation",
76126
76195
  "creation",
76127
- trace_creation
76196
+ "creation",
76197
+ "trace the creation and destruction of gobjs",
76198
+ () => global_bit(trace_level_t.TRACE_CREATE_DELETE),
76199
+ () => toggle_global("create_delete")
76128
76200
  ],
76129
76201
  [
76130
- "trace_start_stop",
76202
+ "start_stop",
76131
76203
  "start / stop",
76132
- trace_start_stop
76204
+ "trace the start and stop of gobjs",
76205
+ () => global_bit(trace_level_t.TRACE_START_STOP),
76206
+ () => toggle_global("start_stop")
76133
76207
  ],
76134
76208
  [
76135
- "trace_subscriptions",
76136
76209
  "subscriptions",
76137
- trace_subscriptions
76210
+ "subscriptions",
76211
+ "trace subscriptions and publications",
76212
+ () => global_bit(trace_level_t.TRACE_SUBSCRIPTIONS),
76213
+ () => toggle_global("subscriptions")
76138
76214
  ],
76139
76215
  [
76140
- "trace_i18n",
76141
76216
  "i18n",
76142
- trace_i18n
76217
+ "i18n",
76218
+ "i18next debug output",
76219
+ i18n_state,
76220
+ toggle_i18n
76143
76221
  ],
76144
76222
  [
76145
- "trace_traffic",
76146
76223
  "traffic",
76147
- trace_traffic
76224
+ "traffic",
76225
+ "trace the messages to and from the backend",
76226
+ traffic_state,
76227
+ toggle_traffic
76148
76228
  ],
76149
76229
  [
76150
- "no_poll",
76151
- "no poll",
76152
- set_no_poll
76230
+ "periodic",
76231
+ "periodic",
76232
+ "trace the periodic timer event",
76233
+ periodic_state,
76234
+ toggle_periodic
76153
76235
  ]
76154
76236
  ];
76155
76237
  /******************************
@@ -76216,45 +76298,24 @@ function dev_view() {
76216
76298
  let v = kw_get_local_storage_value("dev_view_mode", "detailed", false);
76217
76299
  return v === "compact" || v === "name" || v === "full" ? v : "detailed";
76218
76300
  }
76219
- var HIDE_PERIODIC_DEFAULT = 1;
76220
76301
  var SIMPLE_MACH_DEFAULT = 1;
76221
76302
  var SIMPLE_MACH_KEY = "dev_mach_simple";
76222
76303
  function dev_simple_mach() {
76223
76304
  return dev_num(SIMPLE_MACH_KEY, SIMPLE_MACH_DEFAULT) ? 1 : 0;
76224
76305
  }
76225
- function dev_hide_periodic() {
76226
- return dev_num("dev_hide_periodic", HIDE_PERIODIC_DEFAULT) ? true : false;
76227
- }
76228
76306
  function dev_route() {
76229
76307
  let v = kw_get_local_storage_value("dev_output_route", "both", false);
76230
76308
  return v === "window" || v === "console" ? v : "both";
76231
76309
  }
76232
76310
  function apply_console_route() {
76233
76311
  set_console_log_enabled(dev_route() !== "window");
76234
- if (typeof gobj_js.set_console_log_filter === "function") gobj_js.set_console_log_filter((level, msg) => {
76235
- if (!dev_hide_periodic()) return true;
76236
- return !log_is_periodic(level, is_string(msg) ? msg : String(msg));
76237
- });
76238
- else if (!__console_filter_warned__) {
76239
- __console_filter_warned__ = true;
76240
- console.warn("yui_dev: gobj-js has no set_console_log_filter (needs >= 7.13.6): the Periodic filter applies to this window only, not to the console");
76241
- }
76242
76312
  }
76243
- var __console_filter_warned__ = false;
76244
76313
  function console_traffic(title, jn, direction, size) {
76245
76314
  let event = jn && jn.event ? String(jn.event) : "(no event)";
76246
76315
  let kw = jn && jn.kw && typeof jn.kw === "object" ? jn.kw : jn;
76247
76316
  let color = direction === 2 ? "#16a34a" : direction === 3 ? "#dc2626" : "#2563eb";
76248
76317
  window.console.log("%c" + dir_arrow(direction) + " " + (title ? "[" + title + "] " : "") + event + (size ? " (" + size + "b)" : ""), "color:" + color, kw);
76249
76318
  }
76250
- function dev_muted() {
76251
- let a = kw_get_local_storage_value("dev_muted_events", [], false);
76252
- if (!Array.isArray(a)) a = [];
76253
- return new Set(a);
76254
- }
76255
- function dev_set_muted(set) {
76256
- kw_set_local_storage_value("dev_muted_events", Array.from(set));
76257
- }
76258
76319
  function set_view(v) {
76259
76320
  kw_set_local_storage_value("dev_view_mode", v);
76260
76321
  rerender_all();
@@ -76270,54 +76331,25 @@ function set_output_route(v) {
76270
76331
  function toggle_pref(key, def) {
76271
76332
  let v = dev_num(key, def) ? 0 : 1;
76272
76333
  kw_set_local_storage_value(key, v);
76273
- apply_console_route();
76274
- rerender_all();
76275
- refresh_dev_chrome();
76276
- }
76277
- function mute_signature(sig) {
76278
- let set = dev_muted();
76279
- set.add(sig);
76280
- dev_set_muted(set);
76281
- rerender_all();
76282
- refresh_dev_chrome();
76283
- }
76284
- function unmute_signature(sig) {
76285
- let set = dev_muted();
76286
- set.delete(sig);
76287
- dev_set_muted(set);
76288
76334
  rerender_all();
76289
76335
  refresh_dev_chrome();
76290
76336
  }
76291
76337
  /******************************
76292
76338
  * Filtering
76293
76339
  ******************************/
76294
- function traffic_signature(event, command) {
76295
- return command ? event + " · " + command : event;
76296
- }
76297
- function traffic_is_periodic(sig) {
76298
- if (PERIODIC_RE.test(sig)) return true;
76299
- return (TRAFFIC_COUNTS.get(sig) || 0) >= PERIODIC_THRESHOLD;
76300
- }
76301
76340
  function build_filter_ctx() {
76302
76341
  return {
76303
76342
  out: dev_num("dev_filter_out", 1),
76304
76343
  inc: dev_num("dev_filter_in", 1),
76305
76344
  err: dev_num("dev_filter_err", 1),
76306
- muted: dev_muted(),
76307
- hide_periodic: dev_hide_periodic(),
76308
76345
  search: SEARCH_TEXT
76309
76346
  };
76310
76347
  }
76311
76348
  function entry_hidden(e, ctx) {
76312
- if (e.kind === "log") {
76313
- if (ctx.hide_periodic && e.periodic) return true;
76314
- return !!(ctx.search && e.hay.indexOf(ctx.search) < 0);
76315
- }
76349
+ if (e.kind === "log") return !!(ctx.search && e.hay.indexOf(ctx.search) < 0);
76316
76350
  if (e.dir === 1 && !ctx.out) return true;
76317
76351
  if (e.dir === 2 && !ctx.inc) return true;
76318
76352
  if (e.dir === 3 && !ctx.err) return true;
76319
- if (ctx.muted.has(e.sig)) return true;
76320
- if (ctx.hide_periodic && traffic_is_periodic(e.sig)) return true;
76321
76353
  if (ctx.search && e.hay.indexOf(ctx.search) < 0) return true;
76322
76354
  return false;
76323
76355
  }
@@ -76341,11 +76373,6 @@ function ensure_dev_style() {
76341
76373
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
76342
76374
  }
76343
76375
  .YDEV_LOG { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 6px 10px; }
76344
- .YDEV_MUTED {
76345
- display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
76346
- padding: 4px 10px; border-bottom: 1px solid rgba(0,0,0,0.08); font-size: 12px;
76347
- }
76348
- .YDEV_MUTED:empty { display: none; }
76349
76376
  .YDEV_STATS {
76350
76377
  flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 14px;
76351
76378
  padding: 6px 10px; border-top: 1px solid rgba(0,0,0,0.1);
@@ -76377,7 +76404,6 @@ function ensure_dev_style() {
76377
76404
  .YDEV_CHIP.s-in.is-active { background: rgba(5,150,105,0.16); border-color: #059669; color: #059669; }
76378
76405
  .YDEV_CHIP.s-err.is-active { background: rgba(220,38,38,0.16); border-color: #dc2626; color: #dc2626; }
76379
76406
  .YDEV_CHIP[data-dir]:not(.is-active) { opacity: 0.4; text-decoration: line-through; }
76380
- .YDEV_CHIP[data-toggle="periodic"].is-active { background: rgba(217,119,6,0.16); border-color: #d97706; color: #b45309; font-weight: 600; }
76381
76407
  .YDEV_SEG { display: inline-flex; border: 1px solid rgba(0,0,0,0.18); border-radius: 7px; overflow: hidden; }
76382
76408
  .YDEV_SEG_BTN {
76383
76409
  font: inherit; font-size: 12px; padding: 4px 10px; border: 0;
@@ -76391,12 +76417,6 @@ function ensure_dev_style() {
76391
76417
  border: 1px solid rgba(0,0,0,0.18); border-radius: 7px;
76392
76418
  background: transparent; color: inherit;
76393
76419
  }
76394
- .YDEV_MUTED_CHIP {
76395
- font: inherit; font-family: "DejaVu Sans Mono", monospace; font-size: 12px;
76396
- display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px;
76397
- border: 1px solid rgba(217,119,6,0.5); border-radius: 999px;
76398
- background: rgba(217,119,6,0.12); color: #b45309; cursor: pointer;
76399
- }
76400
76420
  .YDEV_STAT.s-out { color: #2563eb; } .YDEV_STAT.s-in { color: #059669; } .YDEV_STAT.s-err { color: #dc2626; }
76401
76421
  .YDEV_STAT.s-warn { color: #d97706; }
76402
76422
  .YDEV_STAT.is-strong { font-weight: 700; }
@@ -76424,10 +76444,7 @@ function ensure_dev_style() {
76424
76444
  .dir-err .TRAFFIC_ARROW, .dir-err .TRAFFIC_EVENT { color: #dc2626; }
76425
76445
  .TRAFFIC_SUMMARY { opacity: 0.6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
76426
76446
  .TRAFFIC_META { margin-left: auto; opacity: 0.6; font-size: 11px; white-space: nowrap; }
76427
- .TRAFFIC_MUTE { border: 0; background: transparent; color: inherit; cursor: pointer; opacity: 0; font-size: 12px; padding: 0 2px; flex: 0 0 auto; }
76428
- .TRAFFIC_ENTRY:hover .TRAFFIC_MUTE, .TRAFFIC_LINE:hover .TRAFFIC_MUTE, .TRAFFIC_NAME:hover .TRAFFIC_MUTE { opacity: 0.5; }
76429
- .TRAFFIC_MUTE:hover { opacity: 1 !important; color: #d97706; }
76430
- .TRAFFIC_KW { margin: 2px 0 0 16px; }
76447
+ .TRAFFIC_ENTRY:hover .TRAFFIC_KW { margin: 2px 0 0 16px; }
76431
76448
  .TRAFFIC_FULL { margin: 4px 0 0 16px; padding: 6px 8px; font-family: monospace; font-size: 11px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; background: rgba(0,0,0,0.04); border-radius: 4px; overflow-x: auto; }
76432
76449
  .TRAFFIC_ROW { display: flex; gap: 6px; align-items: baseline; }
76433
76450
  .TRAFFIC_BULLET { opacity: 0.45; flex: 0 0 auto; }
@@ -76471,9 +76488,7 @@ details.TRAFFIC_NEST > summary::-webkit-details-marker { display: none; }
76471
76488
  :root[data-theme="dark"] .TRAFFIC_VAL.t-bool, :root[data-theme="dark"] .TRAFFIC_VAL.t-null { color: #c084fc; }
76472
76489
  :root[data-theme="dark"] .YDEV_CHIP.is-active { background: rgba(96,165,250,0.2); border-color: #60a5fa; color: #93c5fd; }
76473
76490
  :root[data-theme="dark"] .YDEV_SEG_BTN.is-active { background: #2563eb; color: #fff; }
76474
- :root[data-theme="dark"] .YDEV_CHIP[data-toggle="periodic"].is-active { background: rgba(217,119,6,0.24); border-color: #f59e0b; color: #fbbf24; }
76475
- :root[data-theme="dark"] .YDEV_MUTED_CHIP { border-color: rgba(245,158,11,0.5); background: rgba(245,158,11,0.16); color: #fbbf24; }
76476
- `;
76491
+ :root[data-theme="dark"] `;
76477
76492
  let $style = document.createElement("style");
76478
76493
  $style.id = "yui-dev-style";
76479
76494
  $style.textContent = css;
@@ -76616,23 +76631,6 @@ function traffic_bullets(obj) {
76616
76631
  /******************************
76617
76632
  * Entry rendering (per view)
76618
76633
  ******************************/
76619
- function mute_button(sig) {
76620
- return [
76621
- "button",
76622
- {
76623
- class: "TRAFFIC_MUTE",
76624
- type: "button",
76625
- title: t("mute this message"),
76626
- "data-i18n-title": "mute this message"
76627
- },
76628
- "⊘",
76629
- { click: (ev) => {
76630
- ev.stopPropagation();
76631
- ev.preventDefault();
76632
- mute_signature(sig);
76633
- } }
76634
- ];
76635
- }
76636
76634
  function event_spans(e) {
76637
76635
  let spans = [[
76638
76636
  "span",
@@ -76670,7 +76668,6 @@ function compact_summary(kw) {
76670
76668
  }
76671
76669
  function render_detailed(e) {
76672
76670
  let head = event_spans(e);
76673
- head.push(mute_button(e.sig));
76674
76671
  head.push([
76675
76672
  "span",
76676
76673
  { class: "TRAFFIC_META" },
@@ -76729,7 +76726,6 @@ function full_sections(payload) {
76729
76726
  }
76730
76727
  function render_full(e) {
76731
76728
  let head = event_spans(e);
76732
- head.push(mute_button(e.sig));
76733
76729
  head.push([
76734
76730
  "span",
76735
76731
  { class: "TRAFFIC_META" },
@@ -76767,7 +76763,6 @@ function render_compact(e) {
76767
76763
  { class: "TRAFFIC_SUMMARY" },
76768
76764
  compact_summary(e.kw)
76769
76765
  ]);
76770
- kids.push(mute_button(e.sig));
76771
76766
  kids.push([
76772
76767
  "span",
76773
76768
  { class: "TRAFFIC_META" },
@@ -76784,7 +76779,6 @@ function render_compact(e) {
76784
76779
  }
76785
76780
  function render_name(e) {
76786
76781
  let kids = event_spans(e);
76787
- kids.push(mute_button(e.sig));
76788
76782
  kids.push([
76789
76783
  "span",
76790
76784
  { class: "TRAFFIC_META" },
@@ -76841,7 +76835,6 @@ function render_entry(e) {
76841
76835
  ******************************/
76842
76836
  function clear_traffic() {
76843
76837
  TRAFFIC_LOG.length = 0;
76844
- TRAFFIC_COUNTS.clear();
76845
76838
  LOG_ERR_COUNT = 0;
76846
76839
  LOG_WARN_COUNT = 0;
76847
76840
  let logger = document.getElementById("developer-traffic-logger");
@@ -76953,7 +76946,6 @@ function info_traffic(title, msg, direction, size) {
76953
76946
  let event = jn && jn.event ? String(jn.event) : "(no event)";
76954
76947
  let kw = jn && jn.kw && typeof jn.kw === "object" ? jn.kw : null;
76955
76948
  let command = kw && typeof kw.command === "string" ? kw.command : "";
76956
- let sig = traffic_signature(event, command);
76957
76949
  let hay = "";
76958
76950
  try {
76959
76951
  hay = (event + " " + command + " " + (kw ? JSON.stringify(kw) : "")).toLowerCase();
@@ -76964,7 +76956,6 @@ function info_traffic(title, msg, direction, size) {
76964
76956
  title: title || "",
76965
76957
  event,
76966
76958
  command,
76967
- sig,
76968
76959
  dir: direction,
76969
76960
  size,
76970
76961
  ts: traffic_now(),
@@ -76974,17 +76965,11 @@ function info_traffic(title, msg, direction, size) {
76974
76965
  $node: null
76975
76966
  };
76976
76967
  TRAFFIC_LOG.push(entry);
76977
- TRAFFIC_COUNTS.set(sig, (TRAFFIC_COUNTS.get(sig) || 0) + 1);
76978
- let crossed = dev_hide_periodic() && TRAFFIC_COUNTS.get(sig) === PERIODIC_THRESHOLD;
76979
76968
  if (TRAFFIC_LOG.length > TRAFFIC_MAX) {
76980
76969
  let old = TRAFFIC_LOG.shift();
76981
- let c = (TRAFFIC_COUNTS.get(old.sig) || 0) - 1;
76982
- if (c <= 0) TRAFFIC_COUNTS.delete(old.sig);
76983
- else TRAFFIC_COUNTS.set(old.sig, c);
76984
76970
  if (old.$node && old.$node.parentNode) old.$node.parentNode.removeChild(old.$node);
76985
76971
  }
76986
- if (crossed) rerender_all();
76987
- else if (!entry_hidden(entry, build_filter_ctx())) {
76972
+ if (!entry_hidden(entry, build_filter_ctx())) {
76988
76973
  let node = render_entry(entry);
76989
76974
  entry.$node = node;
76990
76975
  let ph = logger.querySelector(".YDEV_EMPTY");
@@ -77030,19 +77015,12 @@ function info_log(level, msg, hora) {
77030
77015
  dir: 0,
77031
77016
  size: 0,
77032
77017
  ts: traffic_now(),
77033
- sig: log_signature(lvl, text),
77034
- periodic: log_is_periodic(lvl, text),
77035
77018
  hay: (lvl + " " + text).toLowerCase(),
77036
77019
  $node: null
77037
77020
  };
77038
77021
  TRAFFIC_LOG.push(entry);
77039
77022
  if (TRAFFIC_LOG.length > TRAFFIC_MAX) {
77040
77023
  let old = TRAFFIC_LOG.shift();
77041
- if (old.kind !== "log") {
77042
- let c = (TRAFFIC_COUNTS.get(old.sig) || 0) - 1;
77043
- if (c <= 0) TRAFFIC_COUNTS.delete(old.sig);
77044
- else TRAFFIC_COUNTS.set(old.sig, c);
77045
- }
77046
77024
  if (old.$node && old.$node.parentNode) old.$node.parentNode.removeChild(old.$node);
77047
77025
  }
77048
77026
  if (!entry_hidden(entry, build_filter_ctx())) {
@@ -77060,72 +77038,97 @@ function info_log(level, msg, hora) {
77060
77038
  /******************************
77061
77039
  * Trace toggles
77062
77040
  ******************************/
77063
- function trace_traffic() {
77064
- let v = Number(kw_get_local_storage_value("trace_traffic"));
77065
- if (v) {
77066
- gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
77067
- v = 0;
77068
- } else {
77069
- gobj_write_attr(gobj_yuno(), "trace_inter_event", true);
77070
- gobj_write_attr(gobj_yuno(), "trace_ievent_callback", info_traffic);
77071
- v = 1;
77072
- }
77073
- kw_set_local_storage_value("trace_traffic", v);
77041
+ function yuno_trace_command(command, kw) {
77042
+ let r = gobj_command(gobj_yuno(), command, kw, gobj_yuno());
77043
+ if (!r || r.result < 0) log_error(`yui_dev: ${command} ${JSON.stringify(kw)}: ${r ? r.comment : "no answer"}`);
77074
77044
  refresh_dev_chrome();
77075
77045
  }
77076
- function trace_automata() {
77077
- let v = Number(kw_get_local_storage_value("trace_automata"));
77078
- if (v === 0) v = 1;
77079
- else if (v === 1) v = 2;
77080
- else v = 0;
77081
- gobj_write_attr(gobj_yuno(), "tracing", v);
77082
- kw_set_local_storage_value("trace_automata", v);
77083
- refresh_dev_chrome();
77046
+ function global_bit(bit) {
77047
+ return gobj_global_trace_level() & bit ? 1 : 0;
77084
77048
  }
77085
- function trace_creation() {
77086
- let v = Number(kw_get_local_storage_value("trace_creation"));
77087
- v = v === 0 ? 1 : 0;
77088
- gobj_write_attr(gobj_yuno(), "trace_creation", v);
77089
- kw_set_local_storage_value("trace_creation", v);
77090
- refresh_dev_chrome();
77049
+ var GLOBAL_TRACE_BITS = {
77050
+ "machine": trace_level_t.TRACE_MACHINE,
77051
+ "ev_kw": trace_level_t.TRACE_EV_KW,
77052
+ "create_delete": trace_level_t.TRACE_CREATE_DELETE,
77053
+ "start_stop": trace_level_t.TRACE_START_STOP,
77054
+ "subscriptions": trace_level_t.TRACE_SUBSCRIPTIONS
77055
+ };
77056
+ function toggle_global(level) {
77057
+ yuno_trace_command("set-global-trace", {
77058
+ level,
77059
+ set: global_bit(GLOBAL_TRACE_BITS[level]) ? 0 : 1
77060
+ });
77091
77061
  }
77092
- function trace_start_stop() {
77093
- let v = Number(kw_get_local_storage_value("trace_start_stop"));
77094
- v = v === 0 ? 1 : 0;
77095
- gobj_write_attr(gobj_yuno(), "trace_start_stop", v);
77096
- kw_set_local_storage_value("trace_start_stop", v);
77097
- refresh_dev_chrome();
77062
+ function automata_state() {
77063
+ if (!global_bit(trace_level_t.TRACE_MACHINE)) return 0;
77064
+ return global_bit(trace_level_t.TRACE_EV_KW) ? 2 : 1;
77098
77065
  }
77099
- function trace_subscriptions() {
77100
- let v = Number(kw_get_local_storage_value("trace_subscriptions"));
77101
- v = v === 0 ? 1 : 0;
77102
- gobj_write_attr(gobj_yuno(), "trace_subscriptions", v);
77103
- kw_set_local_storage_value("trace_subscriptions", v);
77104
- refresh_dev_chrome();
77066
+ function toggle_automata() {
77067
+ let v = automata_state();
77068
+ if (v === 0) yuno_trace_command("set-global-trace", {
77069
+ level: "machine",
77070
+ set: 1
77071
+ });
77072
+ else if (v === 1) yuno_trace_command("set-global-trace", {
77073
+ level: "ev_kw",
77074
+ set: 1
77075
+ });
77076
+ else {
77077
+ yuno_trace_command("set-global-trace", {
77078
+ level: "ev_kw",
77079
+ set: 0
77080
+ });
77081
+ yuno_trace_command("set-global-trace", {
77082
+ level: "machine",
77083
+ set: 0
77084
+ });
77085
+ }
77086
+ }
77087
+ function traffic_state() {
77088
+ return gobj_get_gclass_trace_level2("C_IEVENT_CLI").includes("ievents") ? 1 : 0;
77105
77089
  }
77106
- function trace_i18n() {
77107
- let v = Number(kw_get_local_storage_value("trace_i18n"));
77108
- v = v === 0 ? 1 : 0;
77090
+ function toggle_traffic() {
77091
+ yuno_trace_command("set-gclass-trace", {
77092
+ gclass_name: "C_IEVENT_CLI",
77093
+ level: "ievents",
77094
+ set: traffic_state() ? 0 : 1
77095
+ });
77096
+ }
77097
+ function periodic_state() {
77098
+ let on = gobj_global_trace_level() & trace_level_t.TRACE_TIMER_PERIODIC;
77099
+ let off = gobj_global_trace_no_level() & trace_level_t.TRACE_TIMER_PERIODIC;
77100
+ return on && !off ? 1 : 0;
77101
+ }
77102
+ function toggle_periodic() {
77103
+ let v = periodic_state() ? 0 : 1;
77104
+ yuno_trace_command("set-global-trace", {
77105
+ level: "timer_periodic",
77106
+ set: v
77107
+ });
77108
+ yuno_trace_command("set-global-no-trace", {
77109
+ level: "timer_periodic",
77110
+ set: v ? 0 : 1
77111
+ });
77112
+ }
77113
+ function i18n_state() {
77114
+ return Number(kw_get_local_storage_value("trace_i18n", 0, false)) ? 1 : 0;
77115
+ }
77116
+ function toggle_i18n() {
77117
+ let v = i18n_state() ? 0 : 1;
77109
77118
  i18next.options.debug = v ? true : false;
77110
77119
  kw_set_local_storage_value("trace_i18n", v);
77111
77120
  refresh_dev_chrome();
77112
77121
  }
77113
- function set_no_poll() {
77114
- let v = Number(kw_get_local_storage_value("no_poll"));
77115
- v = v ? 0 : 1;
77116
- gobj_write_attr(gobj_yuno(), "no_poll", v);
77117
- kw_set_local_storage_value("no_poll", v);
77118
- refresh_dev_chrome();
77119
- }
77120
77122
  /******************************
77121
77123
  * Chrome (controls)
77122
77124
  ******************************/
77123
77125
  function refresh_dev_chrome() {
77124
77126
  document.querySelectorAll(".YDEV_CHIP[data-trace]").forEach(($b) => {
77125
77127
  let key = $b.getAttribute("data-trace");
77128
+ let def = TRACE_DEFS.find((d) => d[0] === key);
77126
77129
  let label = t($b.getAttribute("data-label") || "");
77127
- let v = dev_num(key, 0);
77128
- $b.textContent = key === "trace_automata" && v > 0 ? label + " " + v : label;
77130
+ let v = def ? def[3]() : 0;
77131
+ $b.textContent = key === "automata" && v > 0 ? label + " " + v : label;
77129
77132
  $b.classList.toggle("is-active", v > 0);
77130
77133
  });
77131
77134
  let view = dev_view();
@@ -77144,40 +77147,9 @@ function refresh_dev_chrome() {
77144
77147
  document.querySelectorAll(".YDEV_CHIP[data-dir]").forEach(($b) => {
77145
77148
  $b.classList.toggle("is-active", !!dev_num($b.getAttribute("data-dir"), 1));
77146
77149
  });
77147
- document.querySelectorAll(".YDEV_CHIP[data-toggle=\"periodic\"]").forEach(($b) => {
77148
- $b.classList.toggle("is-active", dev_hide_periodic());
77149
- });
77150
77150
  document.querySelectorAll(".YDEV_CHIP[data-toggle=\"automata-simple\"]").forEach(($b) => {
77151
77151
  $b.classList.toggle("is-active", !!dev_simple_mach());
77152
77152
  });
77153
- let $m = document.getElementById("ydev-muted");
77154
- if ($m) {
77155
- $m.replaceChildren();
77156
- let set = dev_muted();
77157
- if (set.size) {
77158
- let $lbl = document.createElement("span");
77159
- $lbl.className = "YDEV_LABEL";
77160
- $lbl.textContent = t("muted");
77161
- $lbl.setAttribute("data-i18n", "muted");
77162
- $m.appendChild($lbl);
77163
- set.forEach((sig) => {
77164
- $m.appendChild(createElement2([
77165
- "button",
77166
- {
77167
- class: "YDEV_MUTED_CHIP",
77168
- type: "button",
77169
- title: t("unmute"),
77170
- "data-i18n-title": "unmute"
77171
- },
77172
- "⊘ " + sig + " ✕",
77173
- { click: (ev) => {
77174
- ev.stopPropagation();
77175
- unmute_signature(sig);
77176
- } }
77177
- ]));
77178
- });
77179
- }
77180
- }
77181
77153
  update_stats();
77182
77154
  }
77183
77155
  function traffic_to_text() {
@@ -77222,19 +77194,23 @@ function dev_fallback_copy(text) {
77222
77194
  document.body.removeChild(ta);
77223
77195
  }
77224
77196
  function build_control_bar() {
77225
- let trace_chips = TRACE_DEFS.map(([key, label_key, fn]) => [
77197
+ let trace_chips = TRACE_DEFS.map(([key, label_key, title_key, state, toggle]) => [
77226
77198
  "button",
77227
77199
  {
77228
77200
  class: "YDEV_CHIP",
77229
77201
  "data-trace": key,
77230
77202
  "data-label": label_key,
77231
77203
  "data-i18n": label_key,
77232
- type: "button"
77204
+ type: "button",
77205
+ title: t(title_key),
77206
+ "data-i18n-title": title_key,
77207
+ "aria-label": t(label_key),
77208
+ "data-i18n-aria-label": label_key
77233
77209
  },
77234
77210
  t(label_key),
77235
77211
  { click: (ev) => {
77236
77212
  ev.stopPropagation();
77237
- fn();
77213
+ toggle();
77238
77214
  } }
77239
77215
  ]);
77240
77216
  let simple_mach = [
@@ -77372,29 +77348,6 @@ function build_control_bar() {
77372
77348
  mk_dir("in", "⇠", "dev_filter_in", "incoming"),
77373
77349
  mk_dir("err", "⚠", "dev_filter_err", "errors")
77374
77350
  ];
77375
- let periodic_chip = [
77376
- "button",
77377
- {
77378
- class: "YDEV_CHIP",
77379
- "data-toggle": "periodic",
77380
- type: "button",
77381
- title: t("hide the timers and the recurring traffic"),
77382
- "data-i18n-title": "hide the timers and the recurring traffic"
77383
- },
77384
- [[
77385
- "span",
77386
- { class: "YDEV_GLYPH" },
77387
- "⊘"
77388
- ], [
77389
- "span",
77390
- { "data-i18n": "periodic" },
77391
- t("periodic")
77392
- ]],
77393
- { click: (ev) => {
77394
- ev.stopPropagation();
77395
- toggle_pref("dev_hide_periodic", HIDE_PERIODIC_DEFAULT);
77396
- } }
77397
- ];
77398
77351
  let search = [
77399
77352
  "input",
77400
77353
  {
@@ -77475,7 +77428,7 @@ function build_control_bar() {
77475
77428
  grp("view", [view_seg]),
77476
77429
  expand_grp,
77477
77430
  sep(),
77478
- grp("show", [...dir_chips, periodic_chip]),
77431
+ grp("show", dir_chips),
77479
77432
  sep(),
77480
77433
  grp("find", [search]),
77481
77434
  sep(),
@@ -77510,14 +77463,6 @@ function build_dev_body() {
77510
77463
  { class: "YDEV_BODY" },
77511
77464
  [
77512
77465
  build_control_bar(),
77513
- [
77514
- "div",
77515
- {
77516
- class: "YDEV_MUTED",
77517
- id: "ydev-muted"
77518
- },
77519
- []
77520
- ],
77521
77466
  [
77522
77467
  "div",
77523
77468
  {
@@ -77550,28 +77495,16 @@ function dev_window_was_open() {
77550
77495
  return Number(kw_get_local_storage_value("open_developer_window", 0, false)) ? true : false;
77551
77496
  }
77552
77497
  /************************************************************
77553
- * Apply ALL persisted developer-trace flags to the running
77554
- * yuno. Independent of the dev window — call it once at app
77555
- * startup so a refresh keeps logging whatever was enabled.
77498
+ * Wire the developer window's sinks and the browser-side prefs.
77499
+ * Call it once at app startup.
77500
+ *
77501
+ * The TRACE LEVELS are not here: the yuno restores the ones the user
77502
+ * persisted in its own mt_create (`trace_levels` / `no_trace_levels`),
77503
+ * as a C yuno does. What is left is where the output goes.
77556
77504
  ************************************************************/
77557
77505
  function apply_dev_traces() {
77558
- let traffic = Number(kw_get_local_storage_value("trace_traffic", 0, false));
77559
- let trace = Number(kw_get_local_storage_value("trace_automata", 0, false));
77560
- let creation = Number(kw_get_local_storage_value("trace_creation", 0, false));
77561
- let start_stop = Number(kw_get_local_storage_value("trace_start_stop", 0, false));
77562
- let subscriptions = Number(kw_get_local_storage_value("trace_subscriptions", 0, false));
77563
- let i18n = Number(kw_get_local_storage_value("trace_i18n", 0, false));
77564
- let no_poll = Number(kw_get_local_storage_value("no_poll", 0, false));
77565
- if (traffic) {
77566
- gobj_write_attr(gobj_yuno(), "trace_inter_event", true);
77567
- gobj_write_attr(gobj_yuno(), "trace_ievent_callback", info_traffic);
77568
- } else gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
77569
- gobj_write_attr(gobj_yuno(), "tracing", trace);
77570
- gobj_write_attr(gobj_yuno(), "trace_creation", creation);
77571
- gobj_write_attr(gobj_yuno(), "trace_start_stop", start_stop);
77572
- gobj_write_attr(gobj_yuno(), "trace_subscriptions", subscriptions);
77573
- gobj_write_attr(gobj_yuno(), "no_poll", no_poll);
77574
- i18next.options.debug = i18n ? true : false;
77506
+ gobj_write_attr(gobj_yuno(), "trace_ievent_callback", info_traffic);
77507
+ i18next.options.debug = i18n_state() ? true : false;
77575
77508
  gobj_set_trace_machine_format(dev_simple_mach());
77576
77509
  set_log_callback(info_log);
77577
77510
  apply_console_route();
@@ -77622,8 +77555,8 @@ function setup_dev(self, show) {
77622
77555
  *
77623
77556
  * Returns { $el, dispose }:
77624
77557
  * - $el: the panel element (control bar + log + stats).
77625
- * - dispose: stops the inter-event traffic trace; call it from
77626
- * the modal's on_close.
77558
+ * - dispose: call it from the modal's on_close (kept for the
77559
+ * contract; the traces outlive the panel).
77627
77560
  ************************************************************/
77628
77561
  function build_dev_panel() {
77629
77562
  ensure_dev_style();
@@ -77644,9 +77577,7 @@ function build_dev_panel() {
77644
77577
  refresh_dev_chrome();
77645
77578
  rerender_all();
77646
77579
  }, 0);
77647
- let dispose = function() {
77648
- gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
77649
- };
77580
+ let dispose = function() {};
77650
77581
  return {
77651
77582
  $el,
77652
77583
  dispose