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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-kanban",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "license": "MIT",
5
5
  "author": "Dannylamao",
6
6
  "repository": {
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': 'Неделя',
@@ -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) { el.textContent = ''; sub.textContent = tr('cannot check (no network, or the repo is private/not a git checkout)'); }
438
- else if (u.update_available) { el.textContent = `⟳ ${tr('update available')} ${u.local} → ${u.remote}`; el.classList.add('stale-ver'); sub.textContent = `${tr('commit')} ${u.local}${tag} · ${updateHint()}`; }
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 = `${tr('commit')} ${u.local}${tag} · ${tr('matches GitHub')}`; }
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 = a.commit ? `${tr('commit')} ${a.commit}` : (a.app_mtime ? `app.js ${tr('from')} ${fmtDbTime(a.app_mtime.slice(0, 19).replace('T', ' '))}` : '—');
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 = [
@@ -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: ROOT.split(sep).includes('node_modules'),
207
+ packaged: PACKAGED,
208
+ version: selfPkg()?.version || null,
173
209
  };
174
210
  });
175
211
 
176
- let updCache = { at: 0, data: null };
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 < 3600000) return updCache.data;
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
- updCache = { at: Date.now(), data };
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