wiki-plugin-pod 1.0.0 → 1.2.3
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/ReadMe.md +61 -36
- package/api/handlers.js +2 -2
- package/api/openapi.json +6 -18
- package/client/pod.js +9 -8
- package/client/pod.js.map +4 -4
- package/package.json +29 -4
- package/pages/about-pod-plugin +103 -5
- package/src/client/pod.js +132 -286
- package/src/pod/commands.js +49 -102
- package/src/pod/kinship.js +39 -0
- package/src/pod/roll.js +35 -66
- package/src/pod/vocabulary.js +20 -0
- package/server/package.json +0 -3
- package/server/server.js +0 -47
package/src/client/pod.js
CHANGED
|
@@ -1,326 +1,172 @@
|
|
|
1
1
|
// wiki-plugin-pod — gather configurable pods of related wiki sites.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// search and lineage
|
|
7
|
-
//
|
|
3
|
+
// The item text chooses WHICH pods of sites to gather — sisters, parent,
|
|
4
|
+
// children, descendants, the farm, the neighbourhood — and draws them as a
|
|
5
|
+
// roster of flags. Say FORCE and the gathered sites also join your neighbourhood
|
|
6
|
+
// on load, so their pages join your search and lineage; without it the roster's
|
|
7
|
+
// own » does that when you ask, which is how any other roster behaves.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// descendants all sub-domains, any depth (server)
|
|
14
|
-
// farm every wiki in the farm (server)
|
|
15
|
-
// neighbourhood the sites currently in your neighborhood (client)
|
|
16
|
-
// snapshot freeze the current neighborhood (client, v0.1 == neighbourhood)
|
|
9
|
+
// The commands an author may write are NOT listed here. They live in
|
|
10
|
+
// ../pod/commands.js, which the plugin's API declaration also names, so what an
|
|
11
|
+
// author may type and what the mounted operation accepts come from one table.
|
|
12
|
+
// Restating that table in a comment is how the two drift apart; read the table.
|
|
17
13
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// it, leaving even padding around the flags.
|
|
25
|
-
// TWIN show a roster of the pod members that hold a page with
|
|
26
|
-
// THIS page's slug — existence-only, exactly like the
|
|
27
|
-
// wiki-client Twins strip: a slug match in the neighbour's
|
|
28
|
-
// sitemap, no journal or lineage check.
|
|
29
|
-
// WATCH stricter TWIN: show only the members whose copy is actually a
|
|
30
|
-
// fork of this page — its journal carries a `fork` event — i.e.
|
|
31
|
-
// who is watching (tracking) your page. Fetches each candidate.
|
|
14
|
+
// Where each pod comes from: this plugin ships NO server. It declares a
|
|
15
|
+
// specification and a module of plain functions (api/, src/pod/), and the Farm
|
|
16
|
+
// Plugin mounts them — so the disk-derived pods are read from that mount,
|
|
17
|
+
// /system/api/pod/roll.json, the same address an agent uses. NEIGHBOURHOOD and
|
|
18
|
+
// SNAPSHOT never leave the browser at all, being the neighbourhood the client
|
|
19
|
+
// has already assembled.
|
|
32
20
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// the
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
// The vocabulary is not defined here. It lives in ../pod/commands.js, which
|
|
40
|
-
// the plugin's API declaration also names — so what an author may type and what
|
|
41
|
-
// the mounted operation accepts come from one table and cannot drift apart.
|
|
42
|
-
import { valuesFor, clientValues, labels, parseKinds, hasCommand, parseTitle } from '../pod/commands.js'
|
|
21
|
+
// How it draws: it doesn't. A gather IS a roster — a heading over the members'
|
|
22
|
+
// flags — and wiki-plugin-roster already draws one, on every wiki there is. So
|
|
23
|
+
// pod builds the roster text and hands it to that plugin. The text it builds is
|
|
24
|
+
// also exactly what the ❄ icon saves as a page, so the drawn thing and the saved
|
|
25
|
+
// thing are one string rather than two code paths that must agree.
|
|
26
|
+
import { valuesFor, fieldByValue, parseKinds, problems, hasCommand, argumentOf, retirementOf } from '../pod/vocabulary.js'
|
|
43
27
|
|
|
44
28
|
const SERVER_KINDS = valuesFor('kinds')
|
|
45
|
-
const
|
|
46
|
-
const LABEL = labels()
|
|
29
|
+
const HEADING = fieldByValue('heading')
|
|
47
30
|
|
|
48
|
-
const
|
|
31
|
+
const escapeHtml = text =>
|
|
49
32
|
(text || '')
|
|
50
33
|
.replace(/&/g, '&')
|
|
51
34
|
.replace(/</g, '<')
|
|
52
35
|
.replace(/>/g, '>')
|
|
53
|
-
.replace(/\*(.+?)\*/g, '<i>$1</i>')
|
|
54
|
-
|
|
55
|
-
const elapsed = ms => {
|
|
56
|
-
const s = Math.floor(ms / 1000)
|
|
57
|
-
const f = 1.9
|
|
58
|
-
let out = 'a minute ago'
|
|
59
|
-
if (s > f * 60) out = `${Math.round(s / 60)} minutes ago`
|
|
60
|
-
if (s > f * 3600) out = `${Math.round(s / 3600)} hours ago`
|
|
61
|
-
if (s > f * 86400) out = `${Math.round(s / 86400)} days ago`
|
|
62
|
-
if (s > f * 604800) out = `${Math.round(s / 604800)} weeks ago`
|
|
63
|
-
if (s > f * 2592000) out = `${Math.round(s / 2592000)} months ago`
|
|
64
|
-
if (s > f * 31536000) out = `${Math.round(s / 31536000)} years ago`
|
|
65
|
-
return out
|
|
66
|
-
}
|
|
67
36
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
37
|
+
// A mistyped command (CHIKDREN for CHILDREN) is parsed as data, so the item
|
|
38
|
+
// falls back to sisters and draws a pod nobody asked for. Say so above the
|
|
39
|
+
// gather, since nothing else on the panel would show a trace of it.
|
|
40
|
+
const problemsHtml = found =>
|
|
41
|
+
found.length
|
|
42
|
+
? `<p class=pod-problem style="color:#a00;font-size:80%;text-align:center;margin:0 0 6px">` +
|
|
43
|
+
found
|
|
44
|
+
.map(p =>
|
|
45
|
+
p.suggestion
|
|
46
|
+
? `${escapeHtml(p.word)} — did you mean <b>${p.suggestion}</b>?`
|
|
47
|
+
: `${escapeHtml(p.word)} — not a pod command`,
|
|
48
|
+
)
|
|
49
|
+
.join('<br>') +
|
|
50
|
+
`</p>`
|
|
51
|
+
: ''
|
|
52
|
+
|
|
53
|
+
// A command that has moved says where it went, rather than silently doing
|
|
54
|
+
// nothing on a page that still asks for it.
|
|
55
|
+
const retiredHtml = text => {
|
|
56
|
+
const moved = ['TWIN', 'WATCH', 'ROSTER']
|
|
57
|
+
.filter(word => hasCommand(text, word))
|
|
58
|
+
.map(word => [word, retirementOf(word)])
|
|
59
|
+
return moved.length
|
|
60
|
+
? `<p class=pod-retired style="color:#666;font-size:80%;text-align:center;margin:0 0 6px">` +
|
|
61
|
+
moved.map(([word, became]) => `${word} — now ${escapeHtml(became)}`).join('<br>') +
|
|
62
|
+
`</p>`
|
|
63
|
+
: ''
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
const portSuffix = () => ([80, '80', '', null].includes(location.port) ? '' : `:${location.port}`)
|
|
75
67
|
|
|
76
|
-
// parseKinds, hasCommand and parseTitle come from the shared vocabulary above.
|
|
77
|
-
// ROSTER, TWIN and WATCH are bare keywords on their own line, so they never
|
|
78
|
-
// pollute the gathered kinds: ROSTER draws the whole gather compactly (with a
|
|
79
|
-
// button to save it as a roster page); TWIN and WATCH switch the panel to a
|
|
80
|
-
// roster of the pod members that hold this page (TWIN by slug, WATCH by an
|
|
81
|
-
// actual fork event in the copy's journal).
|
|
82
|
-
const hasAction = hasCommand
|
|
83
|
-
|
|
84
|
-
// Display name for a site row. Descendants of the viewing site keep their
|
|
85
|
-
// whole relative name (david.pod.peoplepowered.money seen from
|
|
86
|
-
// peoplepowered.money → "david.pod"); everything else shows its first label.
|
|
87
|
-
const shortName = site =>
|
|
88
|
-
site.endsWith('.' + location.hostname)
|
|
89
|
-
? site.slice(0, -(location.hostname.length + 1))
|
|
90
|
-
: site.split('.')[0]
|
|
91
|
-
|
|
92
|
-
const rowHtml = (site, pages, sitemap) => {
|
|
93
|
-
const suffix = portSuffix()
|
|
94
|
-
const short = shortName(site)
|
|
95
|
-
const img =
|
|
96
|
-
`<img class=remote title="${site}${suffix}" src="//${site}${suffix}/favicon.png" ` +
|
|
97
|
-
`data-site="${site}${suffix}" data-slug=welcome-visitors>`
|
|
98
|
-
return (
|
|
99
|
-
`<tr><td align=right>${short} ${img}` +
|
|
100
|
-
`<td data-site="${site}${suffix}">${pages} pages ${freshness(sitemap)}`
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const groupHtml = (kind, rows) =>
|
|
105
|
-
`<p class=caption><b>${LABEL[kind]}</b></p>` +
|
|
106
|
-
`<table width=100% class=pod-${kind}>${rows.join('\n')}</table>`
|
|
107
|
-
|
|
108
68
|
export const emit = (div, item) => {
|
|
109
69
|
const suffix = portSuffix()
|
|
110
70
|
const kinds = parseKinds(item.text)
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const rosterCmd = hasAction(item.text, 'ROSTER') // compact display of the whole gather
|
|
115
|
-
const showTitle = parseTitle(item.text) // TITLE yes/no — show the ROSTER title
|
|
116
|
-
// Title for the ROSTER caption and the saved roster page — the commands
|
|
117
|
-
// present, e.g. "SISTERS Rosters" / "PARENT SISTERS Rosters".
|
|
118
|
-
const rostersTitle = `${kinds.map(k => k.toUpperCase()).join(' ')} Rosters`
|
|
119
|
-
// slug of the page hosting this item — what TWIN/WATCH match against, the way
|
|
120
|
-
// Twins matches the viewing page's slug across the neighbourhood. The page
|
|
121
|
-
// div's id IS the slug (possibly with a _rev… suffix on a historical view);
|
|
122
|
-
// .data('key') is the lineup key, NOT the slug, so don't use it here.
|
|
123
|
-
const slug = (div.closest('.page').attr('id') || '').split('_rev')[0]
|
|
124
|
-
// gathered pod, captured during render so saveRosters() can reuse it:
|
|
125
|
-
// kind -> ordered list of full domain names
|
|
126
|
-
const gathered = {}
|
|
127
|
-
|
|
128
|
-
// TWIN test — existence-only: does this neighbour's already-loaded sitemap hold
|
|
129
|
-
// a page with our slug? (Twins' inner test — no journal, no lineage.)
|
|
130
|
-
const hasTwin = key => {
|
|
131
|
-
const sm = wiki.neighborhood[key]?.sitemap
|
|
132
|
-
return Array.isArray(sm) && sm.some(p => p.slug === slug)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// WATCH test — is this member watching us: their copy of this page carries a
|
|
136
|
-
// `fork` event in its journal (fedwiki's term for a copy taken from ours).
|
|
137
|
-
// Needs the page JSON, so results are fetched lazily and cached (true/false
|
|
138
|
-
// once resolved, `in` even while the request is in flight). We only fetch where
|
|
139
|
-
// the sitemap already shows the slug, so it costs one request per actual twin.
|
|
140
|
-
const watchStatus = {}
|
|
141
|
-
const ensureWatchChecked = (key, onDone) => {
|
|
142
|
-
if (key in watchStatus || !hasTwin(key)) return
|
|
143
|
-
watchStatus[key] = undefined // in-flight marker
|
|
144
|
-
wiki.site(key).get(`${slug}.json`, (err, page) => {
|
|
145
|
-
watchStatus[key] =
|
|
146
|
-
!err && Array.isArray(page?.journal) && page.journal.some(a => a?.type === 'fork')
|
|
147
|
-
onDone()
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
const matches = key => (watch ? watchStatus[key] === true : hasTwin(key))
|
|
71
|
+
const showHeadings = argumentOf(item.text, 'TITLE')
|
|
72
|
+
const force = argumentOf(item.text, 'FORCE')
|
|
73
|
+
const notes = problemsHtml(problems(item.text)) + retiredHtml(item.text)
|
|
151
74
|
|
|
152
75
|
if (div.closest('.page').hasClass('remote')) {
|
|
153
76
|
div.html(
|
|
154
|
-
`<div class=pod style="background-color:#eee;padding:15px"><center
|
|
77
|
+
`<div class=pod style="background-color:#eee;padding:15px"><center>` +
|
|
155
78
|
`<p class=caption>Pod is only available when viewed on a page's home wiki.</p></div>`,
|
|
156
79
|
)
|
|
157
80
|
return
|
|
158
81
|
}
|
|
159
82
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// TWIN / WATCH render like a roster item: the matching members' flags flowing
|
|
192
|
-
// inline, deduplicated across kinds, each linking to that member's copy of
|
|
193
|
-
// THIS page. No titles are shown (every copy is the same page) — the
|
|
194
|
-
// (sub)domain lives in the hover tooltip. Sitemaps (and, for WATCH, the
|
|
195
|
-
// fetched page journals) arrive asynchronously, so this is re-run as they
|
|
196
|
-
// resolve, which is when matches reveal themselves.
|
|
197
|
-
const buildRoster = () => {
|
|
198
|
-
const seen = new Set()
|
|
199
|
-
const flags = []
|
|
83
|
+
div.html(`${notes}<div class=pod-gather><p class=caption>gathering…</p></div>`)
|
|
84
|
+
|
|
85
|
+
// The roster text: a heading, a BLANK LINE, then one domain per line, and a
|
|
86
|
+
// blank line again between groups. That is wiki-plugin-roster's own markup —
|
|
87
|
+
// categories are the lines that are not domains, and a blank line starts a new
|
|
88
|
+
// display line, which is what puts the heading on a line of its own with the
|
|
89
|
+
// flags in a row beneath it. Without the blank line roster runs them together:
|
|
90
|
+
// "Sisters of demo.localhost [flag] [flag]" on one line.
|
|
91
|
+
//
|
|
92
|
+
// This same string draws the item and, saved, becomes an ordinary roster item
|
|
93
|
+
// on a page any wiki can read.
|
|
94
|
+
const rosterText = (groups, origin) =>
|
|
95
|
+
kinds
|
|
96
|
+
.map(kind => {
|
|
97
|
+
const sites = groups[kind] || []
|
|
98
|
+
if (!sites.length) return ''
|
|
99
|
+
const heading = showHeadings ? (HEADING[kind] || kind).replace('{site}', origin) + '\n\n' : ''
|
|
100
|
+
return heading + sites.join('\n')
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join('\n\n')
|
|
104
|
+
|
|
105
|
+
const render = (groups, origin) => {
|
|
106
|
+
// FORCE — register the gathered sites as neighbours, so their pages join
|
|
107
|
+
// your search and lineage. Off by default: a pod is a roster, and a roster
|
|
108
|
+
// does not reach into your neighbourhood unasked. The roster's own » does it
|
|
109
|
+
// on demand instead, and the flags are drawn either way — wiki.site().flag()
|
|
110
|
+
// never consults the neighbourhood.
|
|
111
|
+
if (force) {
|
|
200
112
|
for (const kind of kinds) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (seen.has(key) || !matches(key)) continue
|
|
204
|
-
seen.add(key)
|
|
205
|
-
flags.push(
|
|
206
|
-
`<img class="remote" src="${wiki.site(key).flag()}" ` +
|
|
207
|
-
`title="${key}" data-site="${key}" data-slug="${slug}">`,
|
|
208
|
-
)
|
|
209
|
-
}
|
|
113
|
+
if (!SERVER_KINDS.includes(kind)) continue
|
|
114
|
+
for (const site of groups[kind] || []) wiki.neighborhoodObject.registerNeighbor(site + suffix)
|
|
210
115
|
}
|
|
211
|
-
return flags
|
|
212
116
|
}
|
|
213
117
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// Non-roster: the full pod tables (favicon + name + page count + freshness).
|
|
236
|
-
const buildGroups = () => {
|
|
237
|
-
const html = []
|
|
238
|
-
for (const kind of kinds) {
|
|
239
|
-
const rows = candidates[kind].map(key => {
|
|
240
|
-
const bare = suffix && key.endsWith(suffix) ? key.slice(0, -suffix.length) : key
|
|
241
|
-
const sm = wiki.neighborhood[key]?.sitemap
|
|
242
|
-
return rowHtml(bare, sm ? sm.length : 0, sm)
|
|
243
|
-
})
|
|
244
|
-
const empty = SERVER_KINDS.includes(kind) ? '<tr><td><i>none</i>' : '<tr><td><i>empty</i>'
|
|
245
|
-
html.push(groupHtml(kind, rows.length ? rows : [empty]))
|
|
246
|
-
}
|
|
247
|
-
return html.join('\n')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const paint = () => {
|
|
251
|
-
if (roster) {
|
|
252
|
-
// TWIN / WATCH — flat, filtered flags
|
|
253
|
-
const flags = buildRoster()
|
|
254
|
-
div.find('.groups').html(flags.join(' '))
|
|
255
|
-
div.find('.caption').first().text(
|
|
256
|
-
flags.length ? '' : watch ? 'no pod watchers yet' : 'no pod twins yet',
|
|
257
|
-
)
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
if (rosterCmd) {
|
|
261
|
-
// Mirror a roster item: an optional left-aligned title, then a <br>, then
|
|
262
|
-
// the flags — all in the panel's grey box. TITLE off → flags only, even
|
|
263
|
-
// padding all round.
|
|
264
|
-
const title = showTitle ? `${rostersTitle} <br> ` : ''
|
|
265
|
-
div.find('.groups').html(title + buildRosterDisplay())
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
div.find('.groups').html(buildGroups())
|
|
269
|
-
div.find('.caption').first().text('just updated')
|
|
270
|
-
}
|
|
271
|
-
paint()
|
|
272
|
-
|
|
273
|
-
// Save the gathered pod as a roster ghost page titled after the commands
|
|
274
|
-
// (e.g. "SISTERS Rosters"), one roster item per kind. No FREEZE command any
|
|
275
|
-
// more: it's reached from the ❄ icon tucked into the corner of the ROSTER
|
|
276
|
-
// and wide-table views.
|
|
277
|
-
const saveRosters = () => {
|
|
278
|
-
const hexId = () =>
|
|
279
|
-
Math.floor(Math.random() * 0xffffffffffff).toString(16).padStart(12, '0')
|
|
280
|
-
const story = [{ type: 'markdown', id: hexId(), text: `# ${rostersTitle}` }]
|
|
281
|
-
for (const kind of kinds) {
|
|
282
|
-
const domains = gathered[kind] || []
|
|
283
|
-
story.push({
|
|
284
|
-
type: 'roster',
|
|
285
|
-
id: hexId(),
|
|
286
|
-
text: `${LABEL[kind]} Wikis\n\n${domains.join('\n') || '(none)'}`,
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
|
-
const page = wiki.newPage({ title: rostersTitle, story })
|
|
290
|
-
wiki.showResult(page, { $page: div.parents('.page') })
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Freeze affordance — a subtle ❄ icon in the panel corner, on the ROSTER and
|
|
294
|
-
// the wide-table views (TWIN / WATCH are monitoring views, so they get none).
|
|
295
|
-
if (rosterCmd || !roster) {
|
|
118
|
+
const text = rosterText(groups, origin)
|
|
119
|
+
const target = div.find('.pod-gather')
|
|
120
|
+
target.empty()
|
|
121
|
+
if (!text) return target.html('<p class=caption><i>none</i></p>')
|
|
122
|
+
|
|
123
|
+
wiki.getPlugin('roster', roster => {
|
|
124
|
+
target.empty()
|
|
125
|
+
const rosterItem = { type: 'roster', id: `${item.id}-roster`, text }
|
|
126
|
+
roster.emit(target, rosterItem)
|
|
127
|
+
// bind() too, or the » roster draws beside each row — "add these N sites
|
|
128
|
+
// to neighborhood" — is inert markup. With FORCE off that link is the only
|
|
129
|
+
// way to take the pod into your neighbourhood, so it has to work.
|
|
130
|
+
//
|
|
131
|
+
// Its other binding is a dblclick opening a text editor on this synthetic
|
|
132
|
+
// roster item, which belongs to nothing and would edit a string we
|
|
133
|
+
// generated. Drop that one; pod binds its own dblclick, on the item, to
|
|
134
|
+
// edit the pod text an author actually wrote.
|
|
135
|
+
roster.bind(target, rosterItem)
|
|
136
|
+
target.off('dblclick')
|
|
137
|
+
// Freeze — the same text, saved as a real roster item on a new page.
|
|
296
138
|
const icon = $(
|
|
297
|
-
'<span class=pod-freeze-icon title="
|
|
139
|
+
'<span class=pod-freeze-icon title="Freeze — save this gather as a roster page" ' +
|
|
298
140
|
'style="position:absolute;top:6px;right:9px;cursor:pointer;font-size:14px;opacity:.55">❄</span>',
|
|
299
141
|
)
|
|
300
|
-
icon.on('click',
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (roster) return paint()
|
|
310
|
-
const cell = div.find(`td[data-site="${site}"]`)
|
|
311
|
-
if (!cell.length) return
|
|
312
|
-
const sm = wiki.neighborhood[site]?.sitemap
|
|
313
|
-
if (sm) cell.text(`${sm.length} pages ${freshness(sm)}`)
|
|
142
|
+
icon.on('click', () => {
|
|
143
|
+
const title = kinds.map(k => (HEADING[k] || k).replace('{site}', origin)).join(', ')
|
|
144
|
+
const page = wiki.newPage({
|
|
145
|
+
title,
|
|
146
|
+
story: [{ type: 'roster', id: Math.floor(Math.random() * 0xffffffffffff).toString(16).padStart(12, '0'), text }],
|
|
147
|
+
})
|
|
148
|
+
wiki.showResult(page, { $page: div.parents('.page') })
|
|
149
|
+
})
|
|
150
|
+
div.css('position', 'relative').append(icon)
|
|
314
151
|
})
|
|
315
152
|
}
|
|
316
153
|
|
|
154
|
+
// Only the disk-derived pods need asking; NEIGHBOURHOOD and SNAPSHOT are
|
|
155
|
+
// already in the browser.
|
|
156
|
+
const neighbourhood = () => Object.keys(wiki.neighborhood).map(key => (suffix && key.endsWith(suffix) ? key.slice(0, -suffix.length) : key))
|
|
157
|
+
const localGroups = () => {
|
|
158
|
+
const groups = {}
|
|
159
|
+
for (const kind of kinds) if (!SERVER_KINDS.includes(kind)) groups[kind] = neighbourhood()
|
|
160
|
+
return groups
|
|
161
|
+
}
|
|
162
|
+
|
|
317
163
|
const serverKinds = kinds.filter(k => SERVER_KINDS.includes(k))
|
|
318
164
|
if (serverKinds.length === 0) {
|
|
319
|
-
render(
|
|
165
|
+
render(localGroups(), location.hostname)
|
|
320
166
|
return
|
|
321
167
|
}
|
|
322
168
|
|
|
323
|
-
fetch(`/
|
|
169
|
+
fetch(`/system/api/pod/roll.json?kinds=${encodeURIComponent(serverKinds.join(','))}`)
|
|
324
170
|
.then(res => {
|
|
325
171
|
if (!res.ok) {
|
|
326
172
|
const err = new Error(`HTTP ${res.status}`)
|
|
@@ -329,15 +175,15 @@ export const emit = (div, item) => {
|
|
|
329
175
|
}
|
|
330
176
|
return res.json()
|
|
331
177
|
})
|
|
332
|
-
.then(data => render(data.groups || {}))
|
|
178
|
+
.then(data => render({ ...(data.groups || {}), ...localGroups() }, data.origin || location.hostname))
|
|
333
179
|
.catch(err => {
|
|
334
|
-
// A 404 means
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
180
|
+
// A 404 means nothing is mounted at that address: this farm has no
|
|
181
|
+
// wiki-plugin-farm, or it has not been restarted since pod was installed.
|
|
182
|
+
// Name it, rather than saying "server error", so the operator knows what
|
|
183
|
+
// to install.
|
|
338
184
|
const msg =
|
|
339
185
|
err && err.status === 404
|
|
340
|
-
? 'pod
|
|
186
|
+
? 'pod needs wiki-plugin-farm on this wiki to mount its API — install it and restart'
|
|
341
187
|
: 'server error'
|
|
342
188
|
div.find('.caption').first().text(msg)
|
|
343
189
|
})
|