@bongos/core 1.19.1011 → 1.19.1013
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +53 -43
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
- package/docs/copy-inventory.md +99 -56
- package/docs/copy-registry.json +830 -465
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/idea-objects.js +4 -1
- package/modules/hall-ui/public/sky-composer.js +155 -0
- package/modules/hall-ui/public/sky-data.js +287 -0
- package/modules/hall-ui/public/sky-draw.js +244 -0
- package/modules/hall-ui/public/sky-interaction.js +137 -0
- package/modules/hall-ui/public/sky-layout.js +81 -0
- package/modules/hall-ui/public/sky-panel.js +141 -0
- package/modules/hall-ui/public/sky-tour.js +80 -0
- package/modules/hall-ui/public/thinking.css +203 -267
- package/modules/hall-ui/public/thinking.html +119 -58
- package/modules/hall-ui/public/thinking.states.json +33 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/gen-file-map.js +29 -12
- package/src/bongos/routes/me.js +4 -1
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +1 -1
- package/tests/file_map.mjs +35 -3
- package/tests/hall_audit.mjs +27 -0
- package/tests/hall_page_gate_map.mjs +11 -5
- package/tests/hall_sky.mjs +393 -405
- package/tests/hall_thinking_room.mjs +274 -296
- package/tests/helpers.mjs +185 -217
- package/tests/ideator_large_dead_thought_proof.mjs +33 -25
- package/tests/ideator_traceable_proof.mjs +61 -39
- package/modules/hall-ui/public/sky.css +0 -365
- package/modules/hall-ui/public/sky.html +0 -79
- package/modules/hall-ui/public/sky.js +0 -580
- package/modules/hall-ui/public/sky.states.json +0 -20
- package/modules/hall-ui/public/thinking.js +0 -418
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
// modules/hall-ui/public/thinking.js — THE ROOM (task 1004098 / BV2.R03 of
|
|
2
|
-
// goal 1000095, criterion `ideator-nothing-is-lost`; spec
|
|
3
|
-
// docs/specs/bongos-v2-ideator-surface.md, the agreed shape is
|
|
4
|
-
// docs/design/mocks/ideator-surface/Room.dc.html).
|
|
5
|
-
//
|
|
6
|
-
// "A plain overview of one's own thinking: what was filed, what it became, its
|
|
7
|
-
// size, and whether it still burns." It is deliberately unglamorous — rows, a
|
|
8
|
-
// size, a state — because it is the page a person opens daily and the sky is
|
|
9
|
-
// the ambient one.
|
|
10
|
-
//
|
|
11
|
-
// IT IS THE AUTHOR'S OWN VIEW, and that is spec decision 19 rather than a
|
|
12
|
-
// convenience: the profile's idea trail (profile.js, `activity-ideas`) stays
|
|
13
|
-
// exactly as it is, because it is what SOMEONE ELSE sees on you — three columns,
|
|
14
|
-
// capped, inside an activity log about a person. This page is about the
|
|
15
|
-
// thinking, not about the thinker, so it reads your own trail whole and spends
|
|
16
|
-
// its width on what the profile has no room for: the size and the state. The
|
|
17
|
-
// two surfaces share a READ and nothing else, which is why neither is built out
|
|
18
|
-
// of the other.
|
|
19
|
-
//
|
|
20
|
-
// EVERY NUMBER ON THIS PAGE IS THE SERVER'S. `GET /inbox/by-builder/:id`
|
|
21
|
-
// composes four attachers over each trail row — the descent, what it paid
|
|
22
|
-
// (`size`), which of the five objects it is (`state`) — and this file draws
|
|
23
|
-
// them. It derives no state, compresses no size and counts no brightness:
|
|
24
|
-
// modules/ideas/idea-state.js exists precisely so that the room, the descent
|
|
25
|
-
// and the sky cannot answer the same question three ways (ADR 0322), and the
|
|
26
|
-
// word for each object comes from the one shared table in ./idea-objects.js.
|
|
27
|
-
//
|
|
28
|
-
// WHY IT LOADS THE WHOLE TRAIL AND NOT ONE PAGE OF IT. The four figures over
|
|
29
|
-
// the list — filed / ignited / still burning / burned out — are a claim about a
|
|
30
|
-
// person's whole body of thinking, and computed over a 20-row window they would
|
|
31
|
-
// be a claim about a window while looking exactly like a claim about a life.
|
|
32
|
-
// So the page renders its first window immediately (the read is not cheap: a
|
|
33
|
-
// descent walk and a ledger read per page) and then keeps fetching in the
|
|
34
|
-
// background, re-rendering as it goes, until the trail is exhausted or the cap
|
|
35
|
-
// below is reached. If the cap is reached the foot SAYS the figures cover the
|
|
36
|
-
// newest N — an honest partial beats a silent one.
|
|
37
|
-
|
|
38
|
-
(() => {
|
|
39
|
-
'use strict';
|
|
40
|
-
|
|
41
|
-
const API = '/api/bongos';
|
|
42
|
-
// baseUrl:'' — this page builds full paths itself through api.request, the
|
|
43
|
-
// way work.js and copy-desk.js do, because the trail read is reached by a
|
|
44
|
-
// path the generated namespaced client does not carry a method for. The two
|
|
45
|
-
// idioms must never be mixed on one page (see idea.js's note on what happens
|
|
46
|
-
// when they are).
|
|
47
|
-
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
48
|
-
const { escapeHtml, $, fmtDate } = window.OTB;
|
|
49
|
-
const objects = window.OTBIdeaObjects;
|
|
50
|
-
|
|
51
|
-
const hallBase = () => (location.hostname.startsWith('builders.') ? '' : '/builders');
|
|
52
|
-
const ideaHref = (id) => `${hallBase()}/idea/${encodeURIComponent(id)}`;
|
|
53
|
-
const taskHref = (id) => `${hallBase()}/task/${encodeURIComponent(id)}`;
|
|
54
|
-
|
|
55
|
-
// The server's own ceiling (modules/ideas/trail.js IDEA_TRAIL_MAX_LIMIT), so
|
|
56
|
-
// the background load takes the fewest round trips the route allows.
|
|
57
|
-
const PAGE = 100;
|
|
58
|
-
// Three pages. A trail longer than this exists — and when it does the foot
|
|
59
|
-
// says so rather than the figures quietly describing the newest 300 as though
|
|
60
|
-
// they described everything.
|
|
61
|
-
const CAP = 300;
|
|
62
|
-
|
|
63
|
-
const state = {
|
|
64
|
-
ideas: [],
|
|
65
|
-
total: 0,
|
|
66
|
-
capped: false,
|
|
67
|
-
loading: false,
|
|
68
|
-
filter: 'everything',
|
|
69
|
-
tabs: null,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// ---- what a row says it became -------------------------------------------
|
|
73
|
-
//
|
|
74
|
-
// The OUTCOME column, and the one place this file composes a sentence rather
|
|
75
|
-
// than printing a field. It is built from the ledger facts the row already
|
|
76
|
-
// carries and never from the state: a satellite says it was folded in because
|
|
77
|
-
// `merged_into_idea_id` is set, not because the resolver called it a
|
|
78
|
-
// satellite — so a disagreement between the two shows up as a visibly odd row
|
|
79
|
-
// instead of being smoothed over into agreement.
|
|
80
|
-
//
|
|
81
|
-
// `descent_tasks` is what the roots went on to cause, and it deliberately
|
|
82
|
-
// EXCLUDES those roots (attachDescentToTrail leaves them out so the first hop
|
|
83
|
-
// is not paid twice), which is why the two counts are added rather than one
|
|
84
|
-
// being read as the whole.
|
|
85
|
-
function becameHtml(idea) {
|
|
86
|
-
if (idea.merged_into_idea_id) {
|
|
87
|
-
const title = idea.merged_into_idea_title || `idea ${idea.merged_into_idea_id}`;
|
|
88
|
-
return `folded into <a href="${ideaHref(idea.merged_into_idea_id)}">${escapeHtml(title)}</a>`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const lineage = Array.isArray(idea.lineage_tasks) ? idea.lineage_tasks : [];
|
|
92
|
-
const roots = new Set();
|
|
93
|
-
for (const t of lineage) if (t && t.task_id != null) roots.add(String(t.task_id));
|
|
94
|
-
// The legacy column is the only answer for a promotion that predates
|
|
95
|
-
// ideas_task_lineage (the same guard idea.js's descentRows() carries).
|
|
96
|
-
if (roots.size === 0 && idea.promoted_to_task_id != null) roots.add(String(idea.promoted_to_task_id));
|
|
97
|
-
|
|
98
|
-
if (roots.size === 0) {
|
|
99
|
-
if (idea.status === 'discarded') {
|
|
100
|
-
return idea.triage_note
|
|
101
|
-
? `declined — ${escapeHtml(idea.triage_note)}`
|
|
102
|
-
: 'declined at triage';
|
|
103
|
-
}
|
|
104
|
-
return '<span class="tr-quiet">nothing yet</span>';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const bits = [];
|
|
108
|
-
// A single root gets its name and a link, because with one task the title
|
|
109
|
-
// IS the outcome. Several get a count — five titles in a cell is not an
|
|
110
|
-
// overview, which is what this page is.
|
|
111
|
-
if (roots.size === 1) {
|
|
112
|
-
const id = [...roots][0];
|
|
113
|
-
const named = lineage.find((t) => t && String(t.task_id) === id);
|
|
114
|
-
const title = (named && named.title) || idea.promoted_task_title || `task ${id}`;
|
|
115
|
-
bits.push(`<a href="${taskHref(id)}">${escapeHtml(title)}</a>`);
|
|
116
|
-
} else {
|
|
117
|
-
bits.push(`${roots.size} tasks`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const descent = Array.isArray(idea.descent_tasks) ? idea.descent_tasks : [];
|
|
121
|
-
if (descent.length > 0) {
|
|
122
|
-
bits.push(`<span class="tr-quiet">and ${descent.length} more below ${descent.length === 1 ? 'it' : 'them'}</span>`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// The nod is the one ACTIONABLE thing this column can carry: a task sitting
|
|
126
|
-
// at backlog inside a goal is one Metic+ promote from claimable, and the
|
|
127
|
-
// whole point of surfacing position (idea-state.js) was that the queue
|
|
128
|
-
// becomes something a person can see rather than remember to open.
|
|
129
|
-
const pos = objects.positionWord(idea.state);
|
|
130
|
-
if (pos === 'waiting for a nod') bits.push('<span class="tr-nod">waiting for a nod</span>');
|
|
131
|
-
|
|
132
|
-
return bits.join(' · ');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---- size ----------------------------------------------------------------
|
|
136
|
-
//
|
|
137
|
-
// The bar's width is the server's `scale` and nothing else. Size is
|
|
138
|
-
// long-tailed, so economy compresses it ONCE at the read
|
|
139
|
-
// (modules/economy/idea-size.js compressIdeaSize) and the direction is
|
|
140
|
-
// explicit that the compressed value is what a drawing uses while the true
|
|
141
|
-
// figure is shown beside it. A second curve here — a log of a log — is
|
|
142
|
-
// exactly how the sky and the room would end up drawing the same thought at
|
|
143
|
-
// two different sizes.
|
|
144
|
-
function sizeHtml(idea) {
|
|
145
|
-
const size = idea.size;
|
|
146
|
-
if (!size || typeof size.credits !== 'number') {
|
|
147
|
-
// `size: null` is "the reward port could not be asked", not "it earned
|
|
148
|
-
// nothing" (trail-size.js). Drawing it as a zero would be a claim.
|
|
149
|
-
return '<span class="tr-quiet" title="the ledger could not be read">unknown</span>';
|
|
150
|
-
}
|
|
151
|
-
if (size.credits <= 0) return '<span class="tr-quiet">—</span>';
|
|
152
|
-
const pct = Math.max(4, Math.round(Number(size.scale || 0) * 100));
|
|
153
|
-
// A size computed from the descent an idea already had, rather than from
|
|
154
|
-
// payouts actually made (spec decision 17). The marker rides wherever the
|
|
155
|
-
// number is shown or the number quietly claims to be money that was spent.
|
|
156
|
-
const mark = size.historical === true
|
|
157
|
-
? '<abbr class="tr-hist" title="computed from the descent this idea already had — not payouts that were made">part historical</abbr>'
|
|
158
|
-
: '';
|
|
159
|
-
// THE BAR KEEPS ITS WIDTH AND LOSES ITS COLOUR WHEN THE WORK IS GONE, which
|
|
160
|
-
// is the agreed shape (Room.dc.html draws the bar in the accent only while
|
|
161
|
-
// `alive > 0`) and the picture the whole two-channel model exists for: a
|
|
162
|
-
// thought that caused a great deal of work the project then tore out reads
|
|
163
|
-
// LARGE AND DEAD, not small. Width is size and never falls; colour is the
|
|
164
|
-
// live channel. Set from the same `state` the object column draws, so the
|
|
165
|
-
// two cells of a row cannot disagree — and only when the removal facts were
|
|
166
|
-
// actually READABLE, because `brightness: null` is "we could not look" and
|
|
167
|
-
// greying a bar on that basis would state something nobody checked.
|
|
168
|
-
const st = idea.state;
|
|
169
|
-
const out = st && st.attributable === true && st.hasWork === true && st.brightness === 0;
|
|
170
|
-
return `<span class="tr-size"${out ? ' data-lit="out"' : ''}>
|
|
171
|
-
<span class="tr-size__bar" style="--w:${pct}%"></span>
|
|
172
|
-
<span class="tr-size__n">${escapeHtml(String(size.credits))}</span>
|
|
173
|
-
</span>${mark}`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---- the object ----------------------------------------------------------
|
|
177
|
-
//
|
|
178
|
-
// The mark restates in SHAPE what the cell also says in words — the sheet's
|
|
179
|
-
// own rule for the grade badge, and the reason the glyph is not decoration: a
|
|
180
|
-
// mark distinguished only by colour tells a colour-blind reader nothing.
|
|
181
|
-
// Brightness rides the glyph's opacity AND the sentence beside it, so dimness
|
|
182
|
-
// is never the only channel either.
|
|
183
|
-
function objectHtml(idea) {
|
|
184
|
-
const st = idea.state;
|
|
185
|
-
const o = objects.objectFor(st && st.state);
|
|
186
|
-
const bright = objects.brightnessWord(st);
|
|
187
|
-
const pos = objects.positionWord(st);
|
|
188
|
-
// `brightness: null` on a thought that HAS work means the removal facts
|
|
189
|
-
// could not be read. It must not be drawn as a dim star — the glyph stays
|
|
190
|
-
// full and the words carry the doubt.
|
|
191
|
-
const lit = (st && st.hasWork === true && st.attributable === true && typeof st.brightness === 'number')
|
|
192
|
-
? st.brightness
|
|
193
|
-
: 1;
|
|
194
|
-
const sub = bright || pos || o.blurb.split('.')[0];
|
|
195
|
-
return `<span class="tr-obj" data-object="${escapeHtml(String((st && st.state) || 'unknown'))}" style="--lit:${lit.toFixed(2)}">
|
|
196
|
-
<span class="tr-obj__glyph" aria-hidden="true"></span>
|
|
197
|
-
<span class="tr-obj__body">
|
|
198
|
-
<span class="tr-obj__label">${escapeHtml(o.label)}</span>
|
|
199
|
-
<span class="tr-obj__sub">${escapeHtml(sub)}</span>
|
|
200
|
-
</span>
|
|
201
|
-
</span>`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function rowHtml(idea) {
|
|
205
|
-
const st = idea.state;
|
|
206
|
-
// NO ID COLUMN, which is the agreed shape (the mock's five columns are the
|
|
207
|
-
// thought, filed, became, size, object) and not only a saving of width: an
|
|
208
|
-
// id is a lookup key, and on the one page where every row is YOURS there is
|
|
209
|
-
// nothing to look up — you know which thought it is by reading it. The id
|
|
210
|
-
// is still one click away, on the record the title links to, and it rides
|
|
211
|
-
// the link's title so it can be copied without leaving.
|
|
212
|
-
return `<tr data-object="${escapeHtml(String((st && st.state) || 'unknown'))}">
|
|
213
|
-
<td class="tr-thought"><a href="${ideaHref(idea.id)}" title="Idea ${escapeHtml(String(idea.id))}">${escapeHtml(idea.title || `Idea ${idea.id}`)}</a></td>
|
|
214
|
-
<td class="ledger__date">${escapeHtml(fmtDate(idea.captured_at))}</td>
|
|
215
|
-
<td class="tr-became">${becameHtml(idea)}</td>
|
|
216
|
-
<td class="tr-sizecell">${sizeHtml(idea)}</td>
|
|
217
|
-
<td class="tr-objcell">${objectHtml(idea)}</td>
|
|
218
|
-
</tr>`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ---- the figures ---------------------------------------------------------
|
|
222
|
-
//
|
|
223
|
-
// Four counts, each one a plain tally over the rows in hand — no thresholds
|
|
224
|
-
// and no judgement. "filed" is the SERVER's total rather than the loaded
|
|
225
|
-
// length, because that one is knowable in full from the first response even
|
|
226
|
-
// when the rest is still arriving.
|
|
227
|
-
//
|
|
228
|
-
// "ignited" is `hasWork`, which is the resolver's own word for "this has been
|
|
229
|
-
// paid for something" — not a status. An idea can be promoted, sit in a goal,
|
|
230
|
-
// and have ignited nothing; that is the distinction the direction draws and
|
|
231
|
-
// counting statuses here would erase it.
|
|
232
|
-
function figuresHtml() {
|
|
233
|
-
const rows = state.ideas;
|
|
234
|
-
const by = (fn) => rows.filter(fn).length;
|
|
235
|
-
const figs = [
|
|
236
|
-
{ n: state.total, label: 'filed', tone: '' },
|
|
237
|
-
{ n: by((i) => i.state && i.state.hasWork === true), label: 'ignited', tone: '' },
|
|
238
|
-
{ n: by((i) => i.state && i.state.state === 'star'), label: 'still burning', tone: 'warm' },
|
|
239
|
-
{ n: by((i) => i.state && i.state.state === 'remnant'), label: 'burned out', tone: 'dim' },
|
|
240
|
-
];
|
|
241
|
-
return figs.map((f) => `<div class="tr-fig"${f.tone ? ` data-tone="${f.tone}"` : ''}>
|
|
242
|
-
<span class="tr-fig__n">${escapeHtml(String(f.n))}</span>
|
|
243
|
-
<span class="tr-fig__label">${escapeHtml(f.label)}</span>
|
|
244
|
-
</div>`).join('');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ---- the filter strip ----------------------------------------------------
|
|
248
|
-
//
|
|
249
|
-
// The tab ids ARE the five state values, so a deep link is `#remnant` and the
|
|
250
|
-
// strip cannot drift from the vocabulary: the order comes from
|
|
251
|
-
// idea-objects.js ORDER and the labels from its table.
|
|
252
|
-
//
|
|
253
|
-
// A tab whose count is zero is HIDDEN rather than shown empty or disabled.
|
|
254
|
-
// Five dead pills over a new builder's two ideas is noise about objects they
|
|
255
|
-
// do not have; the strip grows as their thinking does. It is done by
|
|
256
|
-
// visibility and not by rebuilding the strip, so the hash of a filter that
|
|
257
|
-
// later empties still resolves instead of throwing.
|
|
258
|
-
function buildTabs() {
|
|
259
|
-
const kit = window.OTBKit;
|
|
260
|
-
const mount = $('#tr-filters');
|
|
261
|
-
if (!kit || !mount || state.tabs) return;
|
|
262
|
-
const tabs = [{ id: 'everything', label: 'everything' }].concat(
|
|
263
|
-
objects.ORDER.map((id) => ({ id, label: objects.objectFor(id).label }))
|
|
264
|
-
);
|
|
265
|
-
state.tabs = kit.makeTabs({
|
|
266
|
-
mount,
|
|
267
|
-
tabs,
|
|
268
|
-
hash: true,
|
|
269
|
-
// syncTabs BEFORE the rows, and not only on a load: the "never hide the
|
|
270
|
-
// filter the reader is looking through" rule is a property of the
|
|
271
|
-
// CURRENT filter, so it has to be re-applied whenever that changes. A
|
|
272
|
-
// filter can be selected without a load — the strip syncs its hash, so
|
|
273
|
-
// `#remnant` on a trail with no remnants arrives here directly — and
|
|
274
|
-
// painting rows alone would leave the reader on an empty list whose
|
|
275
|
-
// control had just been hidden.
|
|
276
|
-
onChange: (id) => { state.filter = id; syncTabs(); paintRows(); },
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function syncTabs() {
|
|
281
|
-
if (!state.tabs) return;
|
|
282
|
-
const counts = { everything: state.ideas.length };
|
|
283
|
-
for (const id of objects.ORDER) counts[id] = 0;
|
|
284
|
-
for (const i of state.ideas) {
|
|
285
|
-
const k = i.state && i.state.state;
|
|
286
|
-
if (k in counts) counts[k] += 1;
|
|
287
|
-
}
|
|
288
|
-
for (const id of Object.keys(counts)) {
|
|
289
|
-
state.tabs.setCount(id, counts[id]);
|
|
290
|
-
const btn = state.tabs.el.querySelector(`[data-tab="${window.CSS.escape(id)}"]`);
|
|
291
|
-
// The active filter is never hidden, even at zero: hiding the control a
|
|
292
|
-
// reader is currently looking through would leave them staring at an
|
|
293
|
-
// empty list with no way back to everything.
|
|
294
|
-
if (btn) btn.hidden = counts[id] === 0 && id !== 'everything' && id !== state.filter;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ---- painting ------------------------------------------------------------
|
|
299
|
-
|
|
300
|
-
function visibleIdeas() {
|
|
301
|
-
if (state.filter === 'everything') return state.ideas;
|
|
302
|
-
return state.ideas.filter((i) => i.state && i.state.state === state.filter);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function paintRows() {
|
|
306
|
-
const rows = visibleIdeas();
|
|
307
|
-
const body = $('#tr-body');
|
|
308
|
-
if (rows.length === 0) {
|
|
309
|
-
body.innerHTML = state.ideas.length === 0
|
|
310
|
-
? `<p class="tr-empty">You have not filed anything yet. <a href="${hallBase()}/ideas">File a thought</a> — it takes one line.</p>`
|
|
311
|
-
: `<p class="tr-empty">Nothing of yours is ${escapeHtml(objects.objectFor(state.filter).label)} right now.</p>`;
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
body.innerHTML = `<table class="ledger tr-table">
|
|
315
|
-
<thead><tr>
|
|
316
|
-
<th>The thought</th>
|
|
317
|
-
<th class="ledger__date">Filed</th>
|
|
318
|
-
<th>Became</th>
|
|
319
|
-
<th>Size</th>
|
|
320
|
-
<th>Object</th>
|
|
321
|
-
</tr></thead>
|
|
322
|
-
<tbody>${rows.map(rowHtml).join('')}</tbody>
|
|
323
|
-
</table>`;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function paint() {
|
|
327
|
-
$('#tr-figs').innerHTML = figuresHtml();
|
|
328
|
-
syncTabs();
|
|
329
|
-
paintRows();
|
|
330
|
-
const foot = $('#tr-foot');
|
|
331
|
-
const bits = [];
|
|
332
|
-
if (state.loading) bits.push(`Reading the rest of your trail — ${state.ideas.length} of ${state.total} so far.`);
|
|
333
|
-
else if (state.capped) bits.push(`Showing your newest ${state.ideas.length} of ${state.total}; the figures above count those.`);
|
|
334
|
-
if (!state.loading && !state.capped && state.ideas.length > 0) {
|
|
335
|
-
bits.push('Size never falls. Brightness is how much of the work a thought caused is still alive, and that is the part that can go out.');
|
|
336
|
-
}
|
|
337
|
-
foot.innerHTML = bits.map((b) => escapeHtml(b)).join(' ');
|
|
338
|
-
foot.hidden = bits.length === 0;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function subLine() {
|
|
342
|
-
const sub = $('#tr-sub');
|
|
343
|
-
if (!sub) return;
|
|
344
|
-
sub.textContent = state.total === 0
|
|
345
|
-
? 'Nothing filed yet'
|
|
346
|
-
: `${state.total} thought${state.total === 1 ? '' : 's'} filed`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ---- the read ------------------------------------------------------------
|
|
350
|
-
|
|
351
|
-
async function fetchPage(builderId, offset) {
|
|
352
|
-
const r = await api.request('GET',
|
|
353
|
-
`${API}/inbox/by-builder/${encodeURIComponent(builderId)}?limit=${PAGE}&offset=${offset}`);
|
|
354
|
-
if (r.status === 401) { window.location.assign(`${API}/auth/web/start`); return null; }
|
|
355
|
-
if (!r.ok || !r.data) throw new Error(`idea trail → ${r.status}`);
|
|
356
|
-
return r.data;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function fail(msg) {
|
|
360
|
-
$('#tr-body').innerHTML = `<p class="tr-empty">${escapeHtml(msg)}</p>`;
|
|
361
|
-
$('#tr-foot').hidden = true;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function main() {
|
|
365
|
-
// Who is looking. The page is the AUTHOR's own view, so the id is the
|
|
366
|
-
// viewer's and never a query parameter: /thinking?builder=41 would be a
|
|
367
|
-
// second, worse profile page, and the profile already exists.
|
|
368
|
-
const me = await api.request('GET', `${API}/me`);
|
|
369
|
-
if (me.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
|
|
370
|
-
if (!me.ok || !me.data || !me.data.builder) return fail('Could not work out who you are — try signing in again.');
|
|
371
|
-
const builderId = me.data.builder.id;
|
|
372
|
-
|
|
373
|
-
let first;
|
|
374
|
-
try {
|
|
375
|
-
first = await fetchPage(builderId, 0);
|
|
376
|
-
} catch (_) {
|
|
377
|
-
return fail('Could not read your idea trail.');
|
|
378
|
-
}
|
|
379
|
-
if (!first) return;
|
|
380
|
-
|
|
381
|
-
state.ideas = Array.isArray(first.ideas) ? first.ideas : [];
|
|
382
|
-
state.total = Number(first.total) || state.ideas.length;
|
|
383
|
-
state.loading = state.ideas.length < state.total && state.ideas.length < CAP;
|
|
384
|
-
subLine();
|
|
385
|
-
buildTabs();
|
|
386
|
-
paint();
|
|
387
|
-
|
|
388
|
-
// The rest, in the background. A failure here leaves the first window
|
|
389
|
-
// rendered and says so in the foot rather than blanking a page that is
|
|
390
|
-
// already useful — the same posture idea.js takes with its /me read.
|
|
391
|
-
while (state.loading) {
|
|
392
|
-
let next;
|
|
393
|
-
try {
|
|
394
|
-
next = await fetchPage(builderId, state.ideas.length);
|
|
395
|
-
} catch (_) {
|
|
396
|
-
state.loading = false;
|
|
397
|
-
state.capped = true;
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
if (!next) return;
|
|
401
|
-
const batch = Array.isArray(next.ideas) ? next.ideas : [];
|
|
402
|
-
state.total = Number(next.total) || state.total;
|
|
403
|
-
if (batch.length === 0) { state.loading = false; break; }
|
|
404
|
-
state.ideas = state.ideas.concat(batch);
|
|
405
|
-
if (state.ideas.length >= CAP && state.ideas.length < state.total) {
|
|
406
|
-
state.capped = true;
|
|
407
|
-
state.loading = false;
|
|
408
|
-
} else if (state.ideas.length >= state.total) {
|
|
409
|
-
state.loading = false;
|
|
410
|
-
}
|
|
411
|
-
subLine();
|
|
412
|
-
paint();
|
|
413
|
-
}
|
|
414
|
-
paint();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
main();
|
|
418
|
-
})();
|