local-kanban 1.6.3 → 1.7.1
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 +27 -0
- package/package.json +1 -1
- package/public/js/core.js +4 -0
- package/public/js/settings.js +16 -5
- package/src/routes/system.js +44 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
Notable changes, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
4
4
|
and the project uses [semantic versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.7.1]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **The update check asked the registry in a format it answers with an error.** The request carried
|
|
11
|
+
npm's abbreviated media type, which is meant for a package document and not for the `/latest`
|
|
12
|
+
address: the registry replies 406 with an empty body, so an npm install was told "cannot check"
|
|
13
|
+
again. A cached response let it through often enough to look like a flaky network — it was not.
|
|
14
|
+
Caught on a clean install of 1.7.0 minutes after it was published.
|
|
15
|
+
|
|
16
|
+
## [1.7.0]
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **An npm install can finally see that a new version is out.** The update check asked git for the
|
|
21
|
+
current commit, and a package installed from npm has no git repository — so it answered "cannot
|
|
22
|
+
check" every single time, and the About section showed the date of a frontend file instead of a
|
|
23
|
+
version number. The board now reports its own version and compares it against the registry:
|
|
24
|
+
*update available 1.6.3 → 1.7.0*, next to the command that installs it. A clone keeps the git
|
|
25
|
+
check, which says more: the commit, the tag, and how many commits are waiting for a release.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **A failed update check no longer sticks around for an hour.** The answer was cached for an hour
|
|
30
|
+
whether it worked or not, so a single lost request kept the board saying "cannot check" long after
|
|
31
|
+
the network came back. A failure is now remembered for a minute; a real answer still for an hour.
|
|
32
|
+
|
|
6
33
|
## [1.6.3]
|
|
7
34
|
|
|
8
35
|
### Changed
|
package/package.json
CHANGED
package/public/js/core.js
CHANGED
|
@@ -86,6 +86,7 @@ const I18N_RU = {
|
|
|
86
86
|
'Appearance': 'Оформление',
|
|
87
87
|
'applies after page reload': 'применится после перезагрузки страницы',
|
|
88
88
|
'Archive': 'Архивировать',
|
|
89
|
+
'ask the npm registry again': 'спросить реестр npm заново',
|
|
89
90
|
'at': 'от',
|
|
90
91
|
'Attach a screenshot': 'Приложить скриншот',
|
|
91
92
|
'attachment': 'вложение',
|
|
@@ -116,6 +117,7 @@ const I18N_RU = {
|
|
|
116
117
|
'Cancel the task? The issue will be closed as not planned.': 'Отменить задачу? Issue закроется как not planned.',
|
|
117
118
|
'Cancelled': 'Отменено',
|
|
118
119
|
'cannot check (no network, or the repo is private/not a git checkout)': 'не проверить (нет сети, либо репо приватный/без git)',
|
|
120
|
+
'cannot check — the npm registry did not answer': 'не удалось проверить — реестр npm не ответил',
|
|
119
121
|
'Card actions': 'Управление карточкой',
|
|
120
122
|
'change status': 'сменить статус',
|
|
121
123
|
'changed — not written to the file yet': 'изменено — в файл ещё не записано',
|
|
@@ -330,6 +332,7 @@ const I18N_RU = {
|
|
|
330
332
|
'm ago': 'м назад',
|
|
331
333
|
'mark as done': 'отметить выполненным',
|
|
332
334
|
'matches GitHub': 'совпадает с GitHub',
|
|
335
|
+
'matches the registry': 'совпадает с реестром',
|
|
333
336
|
'matches main ✓': 'совпадает с main ✓',
|
|
334
337
|
'max': 'макс',
|
|
335
338
|
'MB': 'МБ',
|
|
@@ -592,6 +595,7 @@ const I18N_RU = {
|
|
|
592
595
|
'Upload': 'Загрузить',
|
|
593
596
|
'used by': 'используют',
|
|
594
597
|
'Using the board at work': 'Пользуешься доской в работе',
|
|
598
|
+
'version': 'версия',
|
|
595
599
|
'Waiting for you': 'Ждёт тебя',
|
|
596
600
|
'Wed': 'Ср',
|
|
597
601
|
'Week': 'Неделя',
|
package/public/js/settings.js
CHANGED
|
@@ -411,7 +411,7 @@ async function renderSection(sec) {
|
|
|
411
411
|
+ `<div class="set-row"><span class="set-lab">${tr('Code version')}<small id="about-ver">…</small></span><span class="about-fresh" id="about-fresh"></span></div>`
|
|
412
412
|
+ `<div class="set-row"><span class="set-lab"><span id="about-branch-lab">${tr('Board updates')}</span><small id="about-upd-sub">…</small></span><span class="about-fresh" id="about-upd"></span></div>`
|
|
413
413
|
+ `<div class="set-row hidden" id="about-dev-row"><span class="set-lab">${tr('dev branch')}<small id="about-dev-sub">…</small></span><span class="about-fresh" id="about-dev"></span></div>`
|
|
414
|
-
+ `<div class="set-row"><span class="set-lab">${tr('Check for updates')}<small>${tr('re-read branch state from GitHub')}</small></span><button class="btn-ghost" id="about-recheck">${tr('Check')}</button></div>`
|
|
414
|
+
+ `<div class="set-row"><span class="set-lab">${tr('Check for updates')}<small id="about-recheck-sub">${tr('re-read branch state from GitHub')}</small></span><button class="btn-ghost" id="about-recheck">${tr('Check')}</button></div>`
|
|
415
415
|
+ `<div class="set-row"><span class="set-lab">${tr('Reload the page')}<small>${tr('re-read the board code right now')}</small></span><button class="btn-ghost" id="about-reload">${tr('Reload now')}</button></div>`
|
|
416
416
|
+ `<div class="set-row"><span class="set-lab">${tr('Using the board at work')}<small>${tr('free for everyone, nothing switched off — if it earns you money, $12 a year per person keeps it maintained')}</small></span>`
|
|
417
417
|
+ `<a class="btn-ghost" href="https://github.com/lamaohub/local-kanban/blob/main/docs/COMMERCIAL.md" target="_blank" rel="noopener noreferrer">${tr('Read')}</a></div>`
|
|
@@ -432,12 +432,21 @@ async function renderSection(sec) {
|
|
|
432
432
|
const el = $('about-upd'); const sub = $('about-upd-sub'); const lab = $('about-branch-lab');
|
|
433
433
|
if (!el || !sub) return;
|
|
434
434
|
if (lab && u.branch) lab.textContent = `${tr('Branch')} ${u.branch}`;
|
|
435
|
+
const rs = $('about-recheck-sub');
|
|
436
|
+
if (rs && about?.packaged) rs.textContent = tr('ask the npm registry again');
|
|
437
|
+
const what = about?.packaged ? tr('version') : tr('commit');
|
|
438
|
+
const same = about?.packaged ? tr('matches the registry') : tr('matches GitHub');
|
|
435
439
|
const tag = u.tag ? ` · ${tr('tag')} ${u.tag}` : '';
|
|
436
440
|
el.className = 'about-fresh';
|
|
437
|
-
if (u.update_available === null) {
|
|
438
|
-
|
|
441
|
+
if (u.update_available === null) {
|
|
442
|
+
el.textContent = '';
|
|
443
|
+
sub.textContent = about?.packaged
|
|
444
|
+
? tr('cannot check — the npm registry did not answer')
|
|
445
|
+
: tr('cannot check (no network, or the repo is private/not a git checkout)');
|
|
446
|
+
}
|
|
447
|
+
else if (u.update_available) { el.textContent = `⟳ ${tr('update available')} ${u.local} → ${u.remote}`; el.classList.add('stale-ver'); sub.textContent = `${what} ${u.local}${tag} · ${updateHint()}`; }
|
|
439
448
|
else if (u.dev?.ahead) { el.textContent = `${tr('in dev')} ${u.dev.ahead} ${commitsWord(u.dev.ahead)} ${tr('awaiting release')}`; el.classList.add('stale-ver'); sub.textContent = `${tr('commit')} ${u.local}${tag} · ${tr('matches GitHub')}`; }
|
|
440
|
-
else { el.textContent = tr('up to date ✓'); sub.textContent = `${
|
|
449
|
+
else { el.textContent = tr('up to date ✓'); sub.textContent = `${what} ${u.local}${tag} · ${same}`; }
|
|
441
450
|
const row = $('about-dev-row'); const dEl = $('about-dev'); const dSub = $('about-dev-sub');
|
|
442
451
|
if (!row || !u.dev) return;
|
|
443
452
|
row.classList.remove('hidden');
|
|
@@ -461,7 +470,9 @@ async function renderSection(sec) {
|
|
|
461
470
|
try {
|
|
462
471
|
const a = await api('GET', '/api/about');
|
|
463
472
|
about = a;
|
|
464
|
-
$('about-ver').textContent =
|
|
473
|
+
$('about-ver').textContent = (a.packaged && a.version) ? `v${a.version}`
|
|
474
|
+
: a.commit ? `${tr('commit')} ${a.commit}`
|
|
475
|
+
: (a.app_mtime ? `app.js ${tr('from')} ${fmtDbTime(a.app_mtime.slice(0, 19).replace('T', ' '))}` : '—');
|
|
465
476
|
const up = a.uptime;
|
|
466
477
|
const upStr = up >= 86400 ? `${Math.floor(up / 86400)}${tr('d')} ${Math.floor((up % 86400) / 3600)}${tr('h')}` : up >= 3600 ? `${Math.floor(up / 3600)}${tr('h')} ${Math.floor((up % 3600) / 60)}${tr('m')}` : `${Math.floor(up / 60)}${tr('m')}`;
|
|
467
478
|
$('about-diag').innerHTML = [
|
package/src/routes/system.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { statSync, readdirSync, createReadStream, createWriteStream, existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { statSync, readdirSync, createReadStream, createWriteStream, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join, dirname, sep } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -14,6 +14,41 @@ import { snapshot, TASK_SELECT } from './tasks.js';
|
|
|
14
14
|
|
|
15
15
|
const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
16
16
|
|
|
17
|
+
const PACKAGED = ROOT_DIR.split(sep).includes('node_modules');
|
|
18
|
+
|
|
19
|
+
let selfPkgCache;
|
|
20
|
+
function selfPkg() {
|
|
21
|
+
if (selfPkgCache === undefined) {
|
|
22
|
+
try {
|
|
23
|
+
const { name, version } = JSON.parse(readFileSync(join(ROOT_DIR, 'package.json'), 'utf8'));
|
|
24
|
+
selfPkgCache = name && version ? { name, version } : null;
|
|
25
|
+
} catch { selfPkgCache = null; }
|
|
26
|
+
}
|
|
27
|
+
return selfPkgCache;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isNewer(candidate, current) {
|
|
31
|
+
const nums = (v) => String(v).split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);
|
|
32
|
+
const [a, b] = [nums(candidate), nums(current)];
|
|
33
|
+
for (let i = 0; i < 3; i++) if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function registryCheck() {
|
|
38
|
+
const pkg = selfPkg();
|
|
39
|
+
if (!pkg) return null;
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`, {
|
|
42
|
+
headers: { 'User-Agent': 'local-kanban', Accept: 'application/json' },
|
|
43
|
+
signal: AbortSignal.timeout(8000),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return null;
|
|
46
|
+
const remote = (await res.json())?.version;
|
|
47
|
+
if (!remote) return null;
|
|
48
|
+
return { update_available: isNewer(remote, pkg.version), local: pkg.version, remote, branch: null, tag: null, dev: null };
|
|
49
|
+
} catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
17
52
|
const UPLOAD_FLOOR = 64 * 1024 * 1024;
|
|
18
53
|
function uploadLimit() {
|
|
19
54
|
try { return Math.max(UPLOAD_FLOOR, statSync(join(DATA_DIR, 'kanban.db')).size * 4); } catch { return UPLOAD_FLOOR; }
|
|
@@ -169,17 +204,20 @@ export default async function systemRoutes(app) {
|
|
|
169
204
|
tasks: db.prepare('SELECT COUNT(*) AS n FROM tasks').get().n,
|
|
170
205
|
projects: db.prepare('SELECT COUNT(*) AS n FROM projects').get().n,
|
|
171
206
|
node: process.version,
|
|
172
|
-
packaged:
|
|
207
|
+
packaged: PACKAGED,
|
|
208
|
+
version: selfPkg()?.version || null,
|
|
173
209
|
};
|
|
174
210
|
});
|
|
175
211
|
|
|
176
|
-
|
|
212
|
+
const CACHE_OK_MS = 3600000;
|
|
213
|
+
const CACHE_FAIL_MS = 60000;
|
|
214
|
+
let updCache = { at: 0, ttl: 0, data: null };
|
|
177
215
|
let lastForced = 0;
|
|
178
216
|
const FORCE_EVERY = 15000;
|
|
179
217
|
app.get('/api/update-check', async (req) => {
|
|
180
218
|
const forced = Boolean(req.query?.refresh) && Date.now() - lastForced >= FORCE_EVERY;
|
|
181
219
|
if (forced) lastForced = Date.now();
|
|
182
|
-
if (!forced && updCache.data && Date.now() - updCache.at <
|
|
220
|
+
if (!forced && updCache.data && Date.now() - updCache.at < updCache.ttl) return updCache.data;
|
|
183
221
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
184
222
|
const git = (args) => new Promise((resolve) => {
|
|
185
223
|
execFile('git', args, { cwd: ROOT, timeout: 8000 }, (err, out) => resolve(err ? null : out.trim()));
|
|
@@ -224,7 +262,8 @@ export default async function systemRoutes(app) {
|
|
|
224
262
|
}
|
|
225
263
|
}
|
|
226
264
|
} catch { }
|
|
227
|
-
|
|
265
|
+
if (data.update_available === null) data = (await registryCheck()) || data;
|
|
266
|
+
updCache = { at: Date.now(), ttl: data.update_available === null ? CACHE_FAIL_MS : CACHE_OK_MS, data };
|
|
228
267
|
return data;
|
|
229
268
|
});
|
|
230
269
|
|