Skip to content

Commit c041661

Browse files
committed
Release v3.7.0 - In-app update notifications + summary OS toasts (Phase 12.10)
applyUpdate no longer fires a per-script OS notification. Previously a 10- script auto-update cycle would trigger 10 back-to-back "Script Updated" toasts; now autoUpdate aggregates cycle results and fires at most one summary notification gated by notifyOnUpdate: "3 scripts updated: A v1.0 -> v1.1, B v2.0 -> v2.1, C v0.4 -> v0.5" New UpdateSystem._recentUpdates ring buffer (cap 20, newest first) plus getRecentUpdates / clearRecentUpdates background message handlers. Dashboard init() calls showRecentUpdatesBanner() which renders a dismissible banner at the top of the Scripts tab listing what auto-updated since the last dashboard visit. Dismiss button clears the ring on the background side so the banner stays gone next visit. Manual single-script flows (popup "Check for Update", dashboard force-update) keep their inline feedback path - they don't push onto the ring or fire a summary notification.
1 parent c3d18c7 commit c041661

8 files changed

Lines changed: 197 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to ScriptVault will be documented in this file.
44

5+
## [v3.7.0] — In-app update notifications + summary OS notifications (Phase 12.10)
6+
7+
- Changed: `applyUpdate` no longer fires a per-script OS notification. Previously a 10-script auto-update cycle would trigger 10 OS-level "Script Updated" toasts back-to-back; now `autoUpdate` aggregates the cycle's successful updates and fires at most one summary notification (`"3 scripts updated: A v1.0 → v1.1, B v2.0 → v2.1, C v0.4 → v0.5"`).
8+
- Added: in-app dashboard banner that lists scripts auto-updated since the last visit. Lands at the top of the Scripts tab on dashboard load. Dismiss button clears the ring on the background side so the banner stays gone next visit.
9+
- Added: `UpdateSystem._recentUpdates` ring buffer (cap 20, newest first) plus `getRecentUpdates` / `clearRecentUpdates` background message handlers.
10+
- Manual single-script flows (popup "Check for Update", dashboard force-update) keep their inline feedback path — they don't push onto the ring or fire a summary notification.
11+
512
## [v3.6.3] — Beautify preserves cursor + scroll (Phase 7.5)
613

714
- Fixed: `beautifyCode` (editor toolbar "Beautify" button) used to slam the cursor to line 0, char 0 after every reformat. On a long file you'd lose your place every time you hit it. The cursor + vertical scroll position now stay where they were.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</p>
1010

1111
<p align="center">
12-
<img src="https://img.shields.io/badge/version-3.6.3-22c55e?style=flat-square" alt="Version">
12+
<img src="https://img.shields.io/badge/version-3.7.0-22c55e?style=flat-square" alt="Version">
1313
<img src="https://img.shields.io/badge/manifest-v3-60a5fa?style=flat-square" alt="Manifest V3">
1414
<img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
1515
<img src="https://img.shields.io/badge/chrome-120%2B-blue?style=flat-square" alt="Chrome 120+">

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,8 @@ TM issue [#2748](https://github.com/Tampermonkey/tampermonkey/issues/2748) and [
808808
- Dashboard shows an "Updates available" banner that lists pending updates
809809
- Only use OS notifications for: install errors, sync failures, and security warnings (new `@connect` domain added)
810810

811+
**Status (v3.7.0, 2026-05-02):** Mostly shipped. `applyUpdate` no longer fires a per-script OS notification`autoUpdate` aggregates cycle results into a single summary notification ("3 scripts updated: A v1.0 → v1.1, B v2.0 → v2.1, ..."), still gated by `notifyOnUpdate`. New `UpdateSystem._recentUpdates` ring buffer + `getRecentUpdates`/`clearRecentUpdates` background handlers feed a dismissible dashboard banner that lists scripts auto-updated since the last visit. Yellow-badge indicator for "updates available" still pendingcan land alongside Phase 6.2 (staged updates) when those land.
812+
811813
### 12.11 Per-Site Enable/Disable Toggle
812814

813815
VM issue [#2410](https://github.com/violentmonkey/violentmonkey/issues/2410). Allow enabling or disabling a script for only the current domain without globally disabling it or editing `@match`. No other manager has this; it fills the gap between "script off everywhere" and "script on everywhere."

background.core.js

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -419,19 +419,20 @@ const UpdateSystem = {
419419
// Persist to storage after registration attempt
420420
await ScriptStorage.set(scriptId, script);
421421

422-
const settings = await SettingsManager.get();
423-
if (settings.notifyOnUpdate) {
424-
chrome.notifications.create({
425-
type: 'basic',
426-
iconUrl: 'images/icon128.png',
427-
title: 'Script Updated',
428-
message: `${script.meta.name} updated to v${script.meta.version}`
429-
});
430-
}
431-
422+
// Phase 12.10 — applyUpdate no longer fires a per-script OS notification.
423+
// Instead, the autoUpdate caller aggregates successful updates into a
424+
// single summary notification (or none, when notifyOnUpdate is off), and
425+
// pushes them onto the recent-updates ring so the dashboard can surface
426+
// an in-app banner. Manual single-script updates (popup "Check for
427+
// Update", dashboard force-update) get their feedback inline via the
428+
// returned { success, script } payload.
432429
return { success: true, script };
433430
},
434-
431+
432+
// Phase 12.10 — recently-applied updates ring buffer surfaced to the
433+
// dashboard via the `getRecentUpdates` background message. Capped at 20.
434+
_recentUpdates: [],
435+
435436
async autoUpdate() {
436437
const settings = await SettingsManager.get();
437438
if (!settings.autoUpdate) return;
@@ -444,7 +445,56 @@ const UpdateSystem = {
444445
console.error('[ScriptVault] Auto-update failures:', failed.map(r => r.reason?.message || r.reason));
445446
}
446447

448+
// Aggregate successful updates for in-app surfacing + a single summary
449+
// OS notification (only fired when the user opted in via notifyOnUpdate).
450+
const successful = [];
451+
results.forEach((r, idx) => {
452+
if (r.status === 'fulfilled' && r.value?.success && updates[idx]) {
453+
successful.push({
454+
id: updates[idx].id,
455+
name: updates[idx].name,
456+
previousVersion: updates[idx].currentVersion,
457+
newVersion: updates[idx].newVersion,
458+
appliedAt: Date.now()
459+
});
460+
}
461+
});
462+
if (successful.length > 0) {
463+
// Push onto the ring (newest first), cap at 20.
464+
this._recentUpdates = [...successful, ...this._recentUpdates].slice(0, 20);
465+
466+
if (settings.notifyOnUpdate) {
467+
const title = successful.length === 1
468+
? 'Script Updated'
469+
: `${successful.length} scripts updated`;
470+
// Compact message body: list up to 3 names then "+N more".
471+
const names = successful.slice(0, 3).map(s => `${s.name} v${s.newVersion}`);
472+
const overflow = successful.length - names.length;
473+
const message = overflow > 0
474+
? `${names.join(', ')} (+${overflow} more)`
475+
: names.join(', ');
476+
try {
477+
chrome.notifications.create({
478+
type: 'basic',
479+
iconUrl: 'images/icon128.png',
480+
title,
481+
message
482+
});
483+
} catch (_e) { /* notifications may be disabled; non-fatal */ }
484+
}
485+
}
486+
447487
await SettingsManager.set('lastUpdateCheck', Date.now());
488+
},
489+
490+
/** Return the most recent successful auto-updates (newest first). */
491+
getRecentUpdates() {
492+
return this._recentUpdates.slice();
493+
},
494+
495+
/** Clear the recent-updates ring (called when the dashboard banner is dismissed). */
496+
clearRecentUpdates() {
497+
this._recentUpdates = [];
448498
}
449499
};
450500

@@ -1532,6 +1582,14 @@ async function handleMessage(message, sender) {
15321582
case 'checkUpdates':
15331583
return await UpdateSystem.checkForUpdates(data?.scriptId);
15341584

1585+
// Phase 12.10 — recently-applied updates for the in-app dashboard banner.
1586+
case 'getRecentUpdates':
1587+
return UpdateSystem.getRecentUpdates();
1588+
1589+
case 'clearRecentUpdates':
1590+
UpdateSystem.clearRecentUpdates();
1591+
return { success: true };
1592+
15351593
case 'forceUpdate': {
15361594
// Force re-download bypassing HTTP cache
15371595
const scriptId = data.scriptId;

background.js

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault v3.6.3 - Background Service Worker
1+
// ScriptVault v3.7.0 - Background Service Worker
22
// Comprehensive userscript manager with cloud sync and auto-updates
33
// NOTE: This file is built from source modules. Edit the individual files in
44
// shared/, modules/, and lib/, then run `npm run build` to regenerate.
@@ -11074,19 +11074,20 @@ const UpdateSystem = {
1107411074
// Persist to storage after registration attempt
1107511075
await ScriptStorage.set(scriptId, script);
1107611076

11077-
const settings = await SettingsManager.get();
11078-
if (settings.notifyOnUpdate) {
11079-
chrome.notifications.create({
11080-
type: 'basic',
11081-
iconUrl: 'images/icon128.png',
11082-
title: 'Script Updated',
11083-
message: `${script.meta.name} updated to v${script.meta.version}`
11084-
});
11085-
}
11086-
11077+
// Phase 12.10 — applyUpdate no longer fires a per-script OS notification.
11078+
// Instead, the autoUpdate caller aggregates successful updates into a
11079+
// single summary notification (or none, when notifyOnUpdate is off), and
11080+
// pushes them onto the recent-updates ring so the dashboard can surface
11081+
// an in-app banner. Manual single-script updates (popup "Check for
11082+
// Update", dashboard force-update) get their feedback inline via the
11083+
// returned { success, script } payload.
1108711084
return { success: true, script };
1108811085
},
11089-
11086+
11087+
// Phase 12.10 — recently-applied updates ring buffer surfaced to the
11088+
// dashboard via the `getRecentUpdates` background message. Capped at 20.
11089+
_recentUpdates: [],
11090+
1109011091
async autoUpdate() {
1109111092
const settings = await SettingsManager.get();
1109211093
if (!settings.autoUpdate) return;
@@ -11099,7 +11100,56 @@ const UpdateSystem = {
1109911100
console.error('[ScriptVault] Auto-update failures:', failed.map(r => r.reason?.message || r.reason));
1110011101
}
1110111102

11103+
// Aggregate successful updates for in-app surfacing + a single summary
11104+
// OS notification (only fired when the user opted in via notifyOnUpdate).
11105+
const successful = [];
11106+
results.forEach((r, idx) => {
11107+
if (r.status === 'fulfilled' && r.value?.success && updates[idx]) {
11108+
successful.push({
11109+
id: updates[idx].id,
11110+
name: updates[idx].name,
11111+
previousVersion: updates[idx].currentVersion,
11112+
newVersion: updates[idx].newVersion,
11113+
appliedAt: Date.now()
11114+
});
11115+
}
11116+
});
11117+
if (successful.length > 0) {
11118+
// Push onto the ring (newest first), cap at 20.
11119+
this._recentUpdates = [...successful, ...this._recentUpdates].slice(0, 20);
11120+
11121+
if (settings.notifyOnUpdate) {
11122+
const title = successful.length === 1
11123+
? 'Script Updated'
11124+
: `${successful.length} scripts updated`;
11125+
// Compact message body: list up to 3 names then "+N more".
11126+
const names = successful.slice(0, 3).map(s => `${s.name} v${s.newVersion}`);
11127+
const overflow = successful.length - names.length;
11128+
const message = overflow > 0
11129+
? `${names.join(', ')} (+${overflow} more)`
11130+
: names.join(', ');
11131+
try {
11132+
chrome.notifications.create({
11133+
type: 'basic',
11134+
iconUrl: 'images/icon128.png',
11135+
title,
11136+
message
11137+
});
11138+
} catch (_e) { /* notifications may be disabled; non-fatal */ }
11139+
}
11140+
}
11141+
1110211142
await SettingsManager.set('lastUpdateCheck', Date.now());
11143+
},
11144+
11145+
/** Return the most recent successful auto-updates (newest first). */
11146+
getRecentUpdates() {
11147+
return this._recentUpdates.slice();
11148+
},
11149+
11150+
/** Clear the recent-updates ring (called when the dashboard banner is dismissed). */
11151+
clearRecentUpdates() {
11152+
this._recentUpdates = [];
1110311153
}
1110411154
};
1110511155

@@ -12187,6 +12237,14 @@ async function handleMessage(message, sender) {
1218712237
case 'checkUpdates':
1218812238
return await UpdateSystem.checkForUpdates(data?.scriptId);
1218912239

12240+
// Phase 12.10 — recently-applied updates for the in-app dashboard banner.
12241+
case 'getRecentUpdates':
12242+
return UpdateSystem.getRecentUpdates();
12243+
12244+
case 'clearRecentUpdates':
12245+
UpdateSystem.clearRecentUpdates();
12246+
return { success: true };
12247+
1219012248
case 'forceUpdate': {
1219112249
// Force re-download bypassing HTTP cache
1219212250
const scriptId = data.scriptId;

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "__MSG_extName__",
4-
"version": "3.6.3",
4+
"version": "3.7.0",
55
"description": "__MSG_extDescription__",
66
"default_locale": "en",
77
"minimum_chrome_version": "120",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scriptvault",
3-
"version": "3.6.3",
3+
"version": "3.7.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

pages/dashboard.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,11 @@
17121712
// Lazy-init the scripts tab (default active tab) so CardView etc. are available
17131713
lazyInitTab('scripts');
17141714

1715+
// Phase 12.10 — show an in-app banner if scripts auto-updated since
1716+
// the last dashboard visit. Replaces the per-script OS notification
1717+
// spam.
1718+
showRecentUpdatesBanner();
1719+
17151720
const route = getDashboardRoute();
17161721
if (route.type === 'new') {
17171722
createNewScript();
@@ -1942,6 +1947,47 @@
19421947
});
19431948
}
19441949

1950+
// Phase 12.10 — In-app banner replacing the per-script update OS
1951+
// notification spam. Pulls the recent-updates ring from background;
1952+
// dismiss → clearRecentUpdates so the banner stays gone next visit.
1953+
async function showRecentUpdatesBanner() {
1954+
try {
1955+
const updates = await chrome.runtime.sendMessage({ action: 'getRecentUpdates' });
1956+
if (!Array.isArray(updates) || updates.length === 0) return;
1957+
1958+
const panel = document.getElementById('scriptsPanel');
1959+
if (!panel) return;
1960+
1961+
// Compose a compact summary — list up to 3 names with old → new
1962+
// versions, then "+N more" overflow.
1963+
const head = updates.slice(0, 3).map(u =>
1964+
`${escapeHtml(u.name || u.id || 'Unnamed')} <span style="opacity:0.7">v${escapeHtml(u.previousVersion || '?')} → v${escapeHtml(u.newVersion || '?')}</span>`
1965+
);
1966+
const overflow = updates.length - head.length;
1967+
const list = head.join(' · ') + (overflow > 0 ? ` <span style="opacity:0.7">(+${overflow} more)</span>` : '');
1968+
1969+
const banner = document.createElement('div');
1970+
banner.className = 'recent-updates-banner';
1971+
banner.innerHTML = `
1972+
<div style="display:flex;align-items:center;gap:12px;padding:10px 16px;background:linear-gradient(135deg,rgba(34,197,94,0.12),rgba(96,165,250,0.12));border:1px solid rgba(34,197,94,0.25);border-radius:8px;margin:8px 12px;font-size:13px">
1973+
<div style="flex:1;min-width:0;color:var(--text-primary)">
1974+
<strong>${updates.length === 1 ? 'Script updated' : `${updates.length} scripts updated`}:</strong>
1975+
${list}
1976+
</div>
1977+
<button id="btnRecentUpdatesDismiss" type="button" aria-label="Dismiss" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;padding:4px;line-height:1">&times;</button>
1978+
</div>
1979+
`;
1980+
panel.insertBefore(banner, panel.firstChild);
1981+
1982+
document.getElementById('btnRecentUpdatesDismiss')?.addEventListener('click', async () => {
1983+
banner.remove();
1984+
try { await chrome.runtime.sendMessage({ action: 'clearRecentUpdates' }); } catch (_e) {}
1985+
});
1986+
} catch (_e) {
1987+
// Background message failure isn't fatal — banner just won't show.
1988+
}
1989+
}
1990+
19451991
// Check if userScripts API is available and enabled
19461992
async function checkUserScriptsAvailability() {
19471993
const banner = document.getElementById('setupWarningBanner');

0 commit comments

Comments
 (0)