pmtiles-swarm 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -3
- package/docs/tile-stacks.md +14 -0
- package/package.json +1 -1
- package/src/bake-jobs.js +61 -6
- package/src/bake.js +21 -1
- package/src/index.js +16 -0
- package/src/web/index.html +41 -15
- package/src/web/preview.html +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.71.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **The preview comes back to where it was opened from.** The link out of a preview said
|
|
13
|
+
"console" and went to the archive list, whatever had been previewed - so previewing a stack and
|
|
14
|
+
coming back meant finding the Stacks tab again. The console now names its view in the address
|
|
15
|
+
and honours one it is given, and a preview links to the view it belongs to.
|
|
16
|
+
|
|
17
|
+
- **A category offers terrain where its newest build is terrain.** Archives and stacks got the
|
|
18
|
+
second button; categories did not, on either page. Same rule and the same reason: the raster
|
|
19
|
+
keeps the plain name because it is the view that shows a hole.
|
|
20
|
+
|
|
21
|
+
### 🐞 Bug fixes
|
|
22
|
+
- **A stopped export resumed; a restarted service did not.** Both were called "stopping keeps the
|
|
23
|
+
work", and only one of them did. **Stop export** cancels the job, which writes a checkpoint. A
|
|
24
|
+
service restart tells it nothing at all: the process is torn down, and whatever had been merged
|
|
25
|
+
since the last checkpoint was merged again.
|
|
26
|
+
|
|
27
|
+
Two things were wrong. Nothing asked a running export to stop when the node did, so exports are
|
|
28
|
+
now a shutdown step of their own - cancelled early in the sequence, before the pieces they read
|
|
29
|
+
through are taken away, and waited for so each writes its checkpoint.
|
|
30
|
+
|
|
31
|
+
And a checkpoint fired on a tile count alone, every five thousand. That is the wrong measure for
|
|
32
|
+
a bake merging slowly, which can run for an hour without reaching it - an export that had done a
|
|
33
|
+
few hundred tiles had never checkpointed at all, so a restart lost everything it had done. There
|
|
34
|
+
is a clock now as well, thirty seconds, whichever comes first. A checkpoint costs about eleven
|
|
35
|
+
milliseconds.
|
|
36
|
+
|
|
37
|
+
## 0.70.0
|
|
38
|
+
### ✨ Features and improvements
|
|
39
|
+
- **An export works on the disk the archive is going to.** It worked under the data directory and
|
|
40
|
+
moved the finished file afterwards, which is the wrong disk twice over. The bytes have to go
|
|
41
|
+
where there is room for them, and a 700 GiB archive is not something a data directory is sized
|
|
42
|
+
for - while the disk chosen to hold the finished archive is, by definition.
|
|
43
|
+
|
|
44
|
+
The second problem was the move. `publish` renames within a filesystem and copies across one, so
|
|
45
|
+
a bake that worked elsewhere ended with a full copy of the whole archive - and until that
|
|
46
|
+
finished, the buffered tile data and the finished file both sat on the data directory's disk.
|
|
47
|
+
Working at the destination makes the last step a rename and removes the second full-size write.
|
|
48
|
+
|
|
49
|
+
Nothing appears at the destination until the end either way: the archive is assembled in one
|
|
50
|
+
pass at finalize, so until then `<destination>/bakes/<stack>/` holds the buffered tiles and the
|
|
51
|
+
checkpoint and no `.pmtiles` at all.
|
|
52
|
+
|
|
53
|
+
### 🐞 Bug fixes
|
|
54
|
+
- **A stack said "without sharp installed, its tiles answer 501" whether or not sharp was
|
|
55
|
+
installed.** It is a conditional the console can resolve and the reader cannot: nothing on the
|
|
56
|
+
page said which way it fell, so the line told somebody with a working stack that it might not
|
|
57
|
+
be, and somebody with a broken one that it might be fine.
|
|
58
|
+
|
|
59
|
+
`/api/stacks` has always reported the codec. Now the stack row uses it. With one installed there
|
|
60
|
+
is nothing to say, so nothing is said - and the "needs a codec" badge is gone too, because a
|
|
61
|
+
warning on something that works teaches people to ignore warnings. Without one it says the tiles
|
|
62
|
+
answer 501 *here*, names the field responsible, and says what to install.
|
|
63
|
+
|
|
10
64
|
## 0.69.0
|
|
11
65
|
### ✨ Features and improvements
|
|
12
66
|
- **An export merges several tiles at once.** It did one at a time, start to finish, before
|
|
@@ -40,9 +94,6 @@
|
|
|
40
94
|
while it ran was never looked at again, and the export completed as though nobody had asked it
|
|
41
95
|
to stop. Checked around each batch as well now.
|
|
42
96
|
|
|
43
|
-
### 🐞 Bug fixes
|
|
44
|
-
- _...Add new stuff here..._
|
|
45
|
-
|
|
46
97
|
## 0.68.3
|
|
47
98
|
### 🐞 Bug fixes
|
|
48
99
|
- **Exporting a stack failed on its first merged tile.** "Cannot transfer object of unsupported
|
package/docs/tile-stacks.md
CHANGED
|
@@ -789,6 +789,20 @@ One bake per stack at a time. Two runs of one recipe write the same checkpoint
|
|
|
789
789
|
files over each other, and the second would resume the first's work believing it
|
|
790
790
|
were its own.
|
|
791
791
|
|
|
792
|
+
The work happens **on the filesystem the archive is going to** — under
|
|
793
|
+
`<destination>/bakes/<stack>/` — not under the data directory. The bytes have
|
|
794
|
+
to go where there is room for them, and a 700 GiB archive is not something a
|
|
795
|
+
data directory is sized for, while the disk chosen to hold the finished archive
|
|
796
|
+
is by definition. It also turns the last step of a long job from a copy of the
|
|
797
|
+
whole archive into a rename, and means the buffered tile data and the finished
|
|
798
|
+
archive never both have to fit on the data directory's disk.
|
|
799
|
+
|
|
800
|
+
Nothing appears at the destination itself until the end. The archive is
|
|
801
|
+
assembled in one pass at finalize, so until then the directory holds the
|
|
802
|
+
buffered tiles and the checkpoint and no `.pmtiles` at all. Choosing a different
|
|
803
|
+
location for a resumed bake looks for the checkpoint in the new place and finds
|
|
804
|
+
none, so it starts over.
|
|
805
|
+
|
|
792
806
|
### What exists now
|
|
793
807
|
|
|
794
808
|
`src/pmtiles-write.js` writes archives, `src/pmtiles-scan.js` reads back what one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/bake-jobs.js
CHANGED
|
@@ -24,9 +24,36 @@ import { outputFormat } from './stack-tile.js';
|
|
|
24
24
|
* first's work as if it were its own.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/** What the working directory is called, wherever it sits. */
|
|
28
28
|
const WORK_DIR = 'bakes';
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Where a bake does its work.
|
|
32
|
+
*
|
|
33
|
+
* On the filesystem the archive is going to, not under the data directory.
|
|
34
|
+
* Two reasons, and the first is the one that decides it: the bytes have to go
|
|
35
|
+
* somewhere there is room for them, and a 700 GiB archive is not something a
|
|
36
|
+
* data directory is sized for -- while the disk chosen to hold the finished
|
|
37
|
+
* archive is, by definition.
|
|
38
|
+
*
|
|
39
|
+
* The second falls out of the first. `publish` renames the finished file into
|
|
40
|
+
* place where it can and copies it where it cannot, so working on the
|
|
41
|
+
* destination's filesystem turns the last step of a long job from a full copy
|
|
42
|
+
* of the whole archive into a rename. That also removes the second full-size
|
|
43
|
+
* write, since the buffered tile data and the finished archive no longer have
|
|
44
|
+
* to exist on the data directory's disk at the same time.
|
|
45
|
+
*
|
|
46
|
+
* The directory is removed when the bake finishes. A cancelled one keeps it,
|
|
47
|
+
* which is the point of it.
|
|
48
|
+
* @param {object} job - The job, carrying where it will publish to.
|
|
49
|
+
* @param {object} config - The node's configuration.
|
|
50
|
+
* @returns {string} - The working directory.
|
|
51
|
+
*/
|
|
52
|
+
export function workDirFor(job, config = {}) {
|
|
53
|
+
const root = job.publishDir ?? config.savePath ?? config.dataDir ?? './data';
|
|
54
|
+
return path.join(root, WORK_DIR, job.stackId);
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
/** A bake that has finished is kept this long, so the console can report it. */
|
|
31
58
|
const KEEP_FINISHED_MS = 10 * 60 * 1000;
|
|
32
59
|
|
|
@@ -81,6 +108,38 @@ export class BakeManager {
|
|
|
81
108
|
return true;
|
|
82
109
|
}
|
|
83
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Stops every running bake and waits for each to write its checkpoint.
|
|
113
|
+
*
|
|
114
|
+
* For a service stopping. Without this a restart kills a bake where it
|
|
115
|
+
* stands: the merging half is cancellable and keeps its work, but only if
|
|
116
|
+
* something tells it to stop, and a process being torn down does not. What
|
|
117
|
+
* was merged since the last checkpoint would be merged again.
|
|
118
|
+
* @param {object} [options] - `timeoutMs` to stop waiting.
|
|
119
|
+
* @returns {Promise<number>} - How many were stopped.
|
|
120
|
+
*/
|
|
121
|
+
async stopAll(options = {}) {
|
|
122
|
+
const running = [...this.#jobs.values()].filter((job) => !job.finishedAt);
|
|
123
|
+
if (running.length === 0) return 0;
|
|
124
|
+
|
|
125
|
+
for (const job of running) {
|
|
126
|
+
job.cancelling = true;
|
|
127
|
+
job.controller.abort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Bounded, because a shutdown that waits for ever is a shutdown that gets
|
|
131
|
+
// killed harder. A checkpoint is milliseconds; what takes time is the tile
|
|
132
|
+
// in flight noticing it has been abandoned.
|
|
133
|
+
const timeout = new Promise((resolve) =>
|
|
134
|
+
setTimeout(resolve, options.timeoutMs ?? 10000).unref?.(),
|
|
135
|
+
);
|
|
136
|
+
await Promise.race([
|
|
137
|
+
Promise.all(running.map((job) => job.promise?.catch(() => {}))),
|
|
138
|
+
timeout,
|
|
139
|
+
]);
|
|
140
|
+
return running.length;
|
|
141
|
+
}
|
|
142
|
+
|
|
84
143
|
/**
|
|
85
144
|
* Starts a bake, and answers as soon as it is running rather than when it
|
|
86
145
|
* finishes.
|
|
@@ -173,11 +232,7 @@ export class BakeManager {
|
|
|
173
232
|
* @returns {Promise<void>} - Resolves when the archive is in the catalog.
|
|
174
233
|
*/
|
|
175
234
|
async #run(job, resolved, codec, options) {
|
|
176
|
-
const workDir =
|
|
177
|
-
this.#config.dataDir ?? './data',
|
|
178
|
-
WORK_DIR,
|
|
179
|
-
job.stackId,
|
|
180
|
-
);
|
|
235
|
+
const workDir = workDirFor(job, this.#config);
|
|
181
236
|
const destination = path.join(workDir, job.name);
|
|
182
237
|
const format = outputFormat(resolved);
|
|
183
238
|
|
package/src/bake.js
CHANGED
|
@@ -25,6 +25,17 @@ import { Compression, PMTilesWriter, TileType } from './pmtiles-write.js';
|
|
|
25
25
|
/** Tiles merged between checkpoints. */
|
|
26
26
|
const DEFAULT_CHECKPOINT_EVERY = 5000;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Seconds between checkpoints, however few tiles have been done.
|
|
30
|
+
*
|
|
31
|
+
* A count alone is the wrong measure. A bake merging slowly -- a big tile, a
|
|
32
|
+
* cache-mode source, a stack with several layers -- can run for an hour
|
|
33
|
+
* without reaching five thousand, and a process killed before its first
|
|
34
|
+
* checkpoint has nothing to resume from at all. A checkpoint costs about
|
|
35
|
+
* eleven milliseconds, so doing one on the clock is close to free.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_CHECKPOINT_SECONDS = 30;
|
|
38
|
+
|
|
28
39
|
/** Tiles merged at once. */
|
|
29
40
|
const DEFAULT_CONCURRENCY = 4;
|
|
30
41
|
|
|
@@ -454,6 +465,7 @@ export async function bakeStack(options) {
|
|
|
454
465
|
header = {},
|
|
455
466
|
deduplicate = true,
|
|
456
467
|
checkpointEvery = DEFAULT_CHECKPOINT_EVERY,
|
|
468
|
+
checkpointSeconds = DEFAULT_CHECKPOINT_SECONDS,
|
|
457
469
|
pauseMs = 0,
|
|
458
470
|
concurrency = DEFAULT_CONCURRENCY,
|
|
459
471
|
} = options;
|
|
@@ -486,6 +498,7 @@ export async function bakeStack(options) {
|
|
|
486
498
|
let skipped = found?.skipped ?? 0;
|
|
487
499
|
let lastTileId = found?.lastTileId ?? -1;
|
|
488
500
|
let sinceCheckpoint = 0;
|
|
501
|
+
let checkpointedAt = Date.now();
|
|
489
502
|
let persisted = found?.entries.length ?? 0;
|
|
490
503
|
|
|
491
504
|
/**
|
|
@@ -508,6 +521,7 @@ export async function bakeStack(options) {
|
|
|
508
521
|
persisted,
|
|
509
522
|
);
|
|
510
523
|
sinceCheckpoint = 0;
|
|
524
|
+
checkpointedAt = Date.now();
|
|
511
525
|
};
|
|
512
526
|
|
|
513
527
|
try {
|
|
@@ -563,7 +577,13 @@ export async function bakeStack(options) {
|
|
|
563
577
|
}
|
|
564
578
|
|
|
565
579
|
batch = [];
|
|
566
|
-
|
|
580
|
+
// Whichever comes first. The count keeps a fast bake from checkpointing
|
|
581
|
+
// constantly; the clock keeps a slow one from never checkpointing at
|
|
582
|
+
// all.
|
|
583
|
+
const due =
|
|
584
|
+
sinceCheckpoint >= checkpointEvery ||
|
|
585
|
+
Date.now() - checkpointedAt >= checkpointSeconds * 1000;
|
|
586
|
+
if (due) await checkpoint();
|
|
567
587
|
signal?.throwIfAborted();
|
|
568
588
|
|
|
569
589
|
// Handing time back, where the operator asked for that. A bake on a node
|
package/src/index.js
CHANGED
|
@@ -474,6 +474,22 @@ PMTILES_SWARM_PUBLIC_URL
|
|
|
474
474
|
cutlines,
|
|
475
475
|
});
|
|
476
476
|
|
|
477
|
+
// Early in the sequence, so an export is told to stop before the pieces it
|
|
478
|
+
// reads through are taken away. Its checkpoint is the hours already spent.
|
|
479
|
+
stoppers.unshift({
|
|
480
|
+
label: 'stack exports',
|
|
481
|
+
stop: async () => {
|
|
482
|
+
const stopped = await bakes.stopAll();
|
|
483
|
+
if (stopped > 0) {
|
|
484
|
+
console.log(
|
|
485
|
+
`[shutdown] stopped ${stopped} stack export(s); each kept its ` +
|
|
486
|
+
'checkpoint, so exporting again carries on from there',
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
ms: 12000,
|
|
491
|
+
});
|
|
492
|
+
|
|
477
493
|
// Reads the head of anything joined but not yet understood — the header,
|
|
478
494
|
// then the root directory and metadata it points at. Without this an archive
|
|
479
495
|
// being mirrored is unservable until the download happens to reach byte
|
package/src/web/index.html
CHANGED
|
@@ -4019,7 +4019,12 @@ Every piece is hashed against the ` +
|
|
|
4019
4019
|
? `<div class="links">
|
|
4020
4020
|
${copyable(ends.tileJson, 'TileJSON')}
|
|
4021
4021
|
${copyable(ends.xyz, 'XYZ')}
|
|
4022
|
-
${
|
|
4022
|
+
${
|
|
4023
|
+
drawsAsTerrain(newest)
|
|
4024
|
+
? `${link(`${ends.preview}?raw=1`, 'preview')}
|
|
4025
|
+
${link(ends.preview, 'terrain')}`
|
|
4026
|
+
: link(ends.preview, 'preview')
|
|
4027
|
+
}
|
|
4023
4028
|
${link(ends.torrent, '.torrent', true)}
|
|
4024
4029
|
${copyable(ends.magnet, 'magnet', true)}
|
|
4025
4030
|
${copyable(ends.feed, 'RSS')}
|
|
@@ -5225,7 +5230,7 @@ Every piece is hashed against the ` +
|
|
|
5225
5230
|
},
|
|
5226
5231
|
{
|
|
5227
5232
|
key: 'stacks.bakePauseMs',
|
|
5228
|
-
label: 'Pause between
|
|
5233
|
+
label: 'Pause between batches while exporting',
|
|
5229
5234
|
type: 'number',
|
|
5230
5235
|
placeholder: '0',
|
|
5231
5236
|
unit: 'ms',
|
|
@@ -6995,17 +7000,19 @@ Every piece is hashed against the ` +
|
|
|
6995
7000
|
}
|
|
6996
7001
|
|
|
6997
7002
|
// ── Tabs ──────────────────────────────────────────────────────────────
|
|
7003
|
+
/** Views a URL may name, so a link can come back to the right one. */
|
|
7004
|
+
const TABS = ['archives', 'categories', 'stacks', 'traffic', 'settings'];
|
|
7005
|
+
|
|
6998
7006
|
const showTab = (name) => {
|
|
6999
|
-
for (const tab of
|
|
7000
|
-
'archives',
|
|
7001
|
-
'categories',
|
|
7002
|
-
'stacks',
|
|
7003
|
-
'traffic',
|
|
7004
|
-
'settings',
|
|
7005
|
-
]) {
|
|
7007
|
+
for (const tab of TABS) {
|
|
7006
7008
|
$(`view-${tab}`).hidden = tab !== name;
|
|
7007
7009
|
$(`tab-${tab}`).classList.toggle('on', tab === name);
|
|
7008
7010
|
}
|
|
7011
|
+
// Written into the address, so a page that sends somebody here can say
|
|
7012
|
+
// where to land -- a preview of a stack belongs back at Stacks, not at
|
|
7013
|
+
// whatever the console opens on. `replaceState` rather than assigning
|
|
7014
|
+
// the hash, which would scroll and add a history entry per tab.
|
|
7015
|
+
history.replaceState(null, '', name === 'archives' ? '#' : `#${name}`);
|
|
7009
7016
|
if (name === 'settings') loadSettings().catch((e) => toast(e.message));
|
|
7010
7017
|
if (name === 'categories') loadCategories().catch((e) => toast(e.message));
|
|
7011
7018
|
if (name === 'stacks') loadStacks().catch((e) => toast(e.message));
|
|
@@ -7014,6 +7021,13 @@ Every piece is hashed against the ` +
|
|
|
7014
7021
|
loadSwarmTraffic();
|
|
7015
7022
|
}
|
|
7016
7023
|
};
|
|
7024
|
+
// A view named in the address wins over the one this opens on. Ignored
|
|
7025
|
+
// when it names nothing, so an old bookmark is not a blank console.
|
|
7026
|
+
const tabFromUrl = () => {
|
|
7027
|
+
const named = location.hash.replace('#', '');
|
|
7028
|
+
return TABS.includes(named) ? named : null;
|
|
7029
|
+
};
|
|
7030
|
+
|
|
7017
7031
|
// The footer's year, set from the clock rather than typed into a file
|
|
7018
7032
|
// nobody will remember to edit. The version beside it arrives with the
|
|
7019
7033
|
// first status, and reads "…" until then.
|
|
@@ -7042,11 +7056,15 @@ Every piece is hashed against the ` +
|
|
|
7042
7056
|
// The shapes this node has, so a source can be clipped to one by name
|
|
7043
7057
|
// rather than by remembering a filename.
|
|
7044
7058
|
let stackCutlines = [];
|
|
7059
|
+
// Which codec is installed, or null. A stack row needs it to say whether
|
|
7060
|
+
// its pixel work is a problem here or merely a fact about the recipe.
|
|
7061
|
+
let stackCodec = null;
|
|
7045
7062
|
|
|
7046
7063
|
const loadStacks = async () => {
|
|
7047
7064
|
const body = await api('/api/stacks');
|
|
7048
7065
|
const { stacks, codec } = body;
|
|
7049
7066
|
stackCutlines = body.cutlines ?? [];
|
|
7067
|
+
stackCodec = codec ?? null;
|
|
7050
7068
|
// A category resolving to a new build changes this, which is the
|
|
7051
7069
|
// reason to poll at all -- but most polls change nothing, and
|
|
7052
7070
|
// repainting a list somebody is about to click is worse than not.
|
|
@@ -7108,8 +7126,12 @@ Every piece is hashed against the ` +
|
|
|
7108
7126
|
if (unresolved.length) {
|
|
7109
7127
|
badges.push('<span class="pill warn">sources missing</span>');
|
|
7110
7128
|
}
|
|
7111
|
-
|
|
7112
|
-
|
|
7129
|
+
// Only where it is actually a problem. With a codec installed the
|
|
7130
|
+
// recipe asking for pixel work is a fact about the recipe, not
|
|
7131
|
+
// something wrong with it, and a warning badge on a working stack
|
|
7132
|
+
// teaches people to ignore warning badges.
|
|
7133
|
+
if (stack.needsCodec && !stackCodec) {
|
|
7134
|
+
badges.push('<span class="pill warn">no codec for this</span>');
|
|
7113
7135
|
}
|
|
7114
7136
|
if (stack.pinned) badges.push('<span class="pill">pinned</span>');
|
|
7115
7137
|
|
|
@@ -7191,10 +7213,10 @@ Every piece is hashed against the ` +
|
|
|
7191
7213
|
}
|
|
7192
7214
|
${bakeLine}
|
|
7193
7215
|
${
|
|
7194
|
-
stack.needsCodec
|
|
7195
|
-
? `<p class="
|
|
7196
|
-
(<code>${escapeHtml(stack.needsCodec)}</code>)
|
|
7197
|
-
<code>sharp</code
|
|
7216
|
+
stack.needsCodec && !stackCodec
|
|
7217
|
+
? `<p class="bad">Its tiles answer 501 here: serving this means
|
|
7218
|
+
decoding pixels (<code>${escapeHtml(stack.needsCodec)}</code>)
|
|
7219
|
+
and this node has no codec. <code>npm install sharp</code>.</p>`
|
|
7198
7220
|
: ''
|
|
7199
7221
|
}
|
|
7200
7222
|
<table class="tight">
|
|
@@ -8304,6 +8326,10 @@ Every piece is hashed against the ` +
|
|
|
8304
8326
|
// An unreachable server is reported by refresh() in the status line.
|
|
8305
8327
|
}
|
|
8306
8328
|
refresh();
|
|
8329
|
+
// After the archives have been asked for, so the tab that opens is the
|
|
8330
|
+
// one asked for and not the one this happens to start on.
|
|
8331
|
+
const named = tabFromUrl();
|
|
8332
|
+
if (named && named !== 'archives') showTab(named);
|
|
8307
8333
|
}
|
|
8308
8334
|
|
|
8309
8335
|
start();
|
package/src/web/preview.html
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
<code id="endpoint"></code>
|
|
70
70
|
<span class="links">
|
|
71
71
|
<a id="mode" href="#" hidden></a>
|
|
72
|
-
<a href="/">← console</a>
|
|
72
|
+
<a id="back" href="/">← console</a>
|
|
73
73
|
</span>
|
|
74
74
|
</header>
|
|
75
75
|
<div id="note" hidden></div>
|
|
@@ -96,6 +96,18 @@
|
|
|
96
96
|
);
|
|
97
97
|
const $ = (id) => document.getElementById(id);
|
|
98
98
|
|
|
99
|
+
// Back to the view this was opened from rather than to whatever the
|
|
100
|
+
// console opens on. The path already says which: a stack was reached
|
|
101
|
+
// from Stacks, a category from Categories, an archive from the list.
|
|
102
|
+
const back = $('back');
|
|
103
|
+
if (back) {
|
|
104
|
+
back.href = location.pathname.startsWith('/stacks/')
|
|
105
|
+
? '/#stacks'
|
|
106
|
+
: location.pathname.startsWith('/latest/')
|
|
107
|
+
? '/#categories'
|
|
108
|
+
: '/';
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
const fail = (message) => {
|
|
100
112
|
document.body.innerHTML = `<div class="banner">${message}</div>`;
|
|
101
113
|
};
|