@summerscar/dsh-mnemon-sync 0.1.0 → 0.1.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/client.js CHANGED
@@ -56,6 +56,7 @@ window.__ModuleLoader__.load({
56
56
  componentSpaces: '{count} 个空间',
57
57
  backups: '备份列表',
58
58
  listBackups: '从目标列出备份',
59
+ refresh: '刷新',
59
60
  backupsHint: '列出远端已存在的备份包,可查看明细并恢复。',
60
61
  noBackups: '目标上还没有备份包',
61
62
  backupName: '文件名',
@@ -118,7 +119,6 @@ window.__ModuleLoader__.load({
118
119
  backingUp: '备份中…',
119
120
  backupDone: '备份完成',
120
121
  backupFailed: '备份失败',
121
- never: '从未',
122
122
  history: '运行记录',
123
123
  noHistory: '还没有运行记录',
124
124
  ok: '成功',
@@ -129,6 +129,9 @@ window.__ModuleLoader__.load({
129
129
  duration: '耗时',
130
130
  size: '体积',
131
131
  componentsCount: '组件',
132
+ clearHistory: '清空记录',
133
+ historyCleared: '运行记录已清空',
134
+ clearFailed: '清空失败',
132
135
  error: '错误',
133
136
  loading: '读取中…',
134
137
  loadFailed: '读取失败',
@@ -157,6 +160,7 @@ window.__ModuleLoader__.load({
157
160
  componentSpaces: '{count} spaces',
158
161
  backups: 'Backups',
159
162
  listBackups: 'List from target',
163
+ refresh: 'Refresh',
160
164
  backupsHint: 'List the archives already on the target, inspect one, and restore it.',
161
165
  noBackups: 'No archive on the target yet',
162
166
  backupName: 'File',
@@ -229,6 +233,9 @@ window.__ModuleLoader__.load({
229
233
  duration: 'Duration',
230
234
  size: 'Size',
231
235
  componentsCount: 'Components',
236
+ clearHistory: 'Clear log',
237
+ historyCleared: 'Run history cleared',
238
+ clearFailed: 'Could not clear',
232
239
  error: 'Error',
233
240
  loading: 'Loading\u2026',
234
241
  loadFailed: 'Could not load',
@@ -657,6 +664,24 @@ window.__ModuleLoader__.load({
657
664
  }
658
665
  }
659
666
 
667
+ /**
668
+ * Drop the run log. Archives on the target are not touched — this is a
669
+ * display reset, and the page says so before the button is pressed.
670
+ */
671
+ const clearHistory = async () => {
672
+ setBusy(true)
673
+ setNotice(null)
674
+ try {
675
+ await api('/clear-history', { method: 'POST', body: '{}' })
676
+ await load()
677
+ setNotice({ ok: true, text: t('historyCleared') })
678
+ } catch (cause) {
679
+ setNotice({ ok: false, text: `${t('clearFailed')}: ${cause.message}` })
680
+ } finally {
681
+ setBusy(false)
682
+ }
683
+ }
684
+
660
685
  const loadArchives = async () => {
661
686
  setArchivesBusy(true)
662
687
  setArchivesError(null)
@@ -1082,7 +1107,15 @@ window.__ModuleLoader__.load({
1082
1107
 
1083
1108
  // history
1084
1109
  h('section', { style: styles.card },
1085
- h('h3', { style: styles.cardTitle }, t('history')),
1110
+ h('div', { style: styles.row },
1111
+ h('h3', { style: { ...styles.cardTitle, marginRight: 'auto' } }, t('history')),
1112
+ // The list keeps the newest 20 on its own; this clears what is left.
1113
+ history.length === 0 ? null : h('button', {
1114
+ style: styles.button,
1115
+ disabled: busy,
1116
+ onClick: () => void clearHistory(),
1117
+ }, t('clearHistory')),
1118
+ ),
1086
1119
  history.length === 0
1087
1120
  ? h('p', { style: styles.note }, t('noHistory'))
1088
1121
  : h('table', { style: styles.table },
package/index.js CHANGED
@@ -199,19 +199,8 @@ export function apply(ctx, config = {}) {
199
199
 
200
200
  if (req.method === 'PUT' && route === '/settings') {
201
201
  const body = await readJsonBody(req)
202
- const previous = settings()
203
202
  const incoming = body?.settings ?? body
204
- // A masked secret coming back from the UI means "unchanged".
205
- if (incoming !== null && typeof incoming === 'object' && incoming.target !== null && typeof incoming.target === 'object') {
206
- const target = incoming.target
207
- if (target.kind === 'webdav' && isMasked(target.password)) {
208
- target.password = previous.target.kind === 'webdav' ? previous.target.password : ''
209
- }
210
- if (target.kind === 's3' && isMasked(target.secretAccessKey)) {
211
- target.secretAccessKey = previous.target.kind === 's3' ? previous.target.secretAccessKey : ''
212
- }
213
- }
214
- const saved = writeSettings(home, normalizeSettings(incoming))
203
+ const saved = writeSettings(home, normalizeSettings(restoreMaskedSecrets(incoming, settings())))
215
204
  scheduler.refresh()
216
205
  sendJson(res, 200, { settings: maskSettings(saved) })
217
206
  return
@@ -225,7 +214,10 @@ export function apply(ctx, config = {}) {
225
214
  const body = await readJsonBody(req)
226
215
  try {
227
216
  if (body?.settings !== undefined) {
228
- const saved = writeSettings(home, normalizeSettings(body.settings))
217
+ // The same masked-secret rule as saving settings: the page always
218
+ // sends the mask it was shown, and writing that mask as the real
219
+ // password would silently break the next upload with a 401.
220
+ const saved = writeSettings(home, normalizeSettings(restoreMaskedSecrets(body.settings, settings())))
229
221
  scheduler.refresh()
230
222
  const result = await scheduler.runNow('manual')
231
223
  sendJson(res, 200, { result, settings: maskSettings(saved) })
@@ -239,6 +231,12 @@ export function apply(ctx, config = {}) {
239
231
  return
240
232
  }
241
233
 
234
+ if (req.method === 'POST' && route === '/clear-history') {
235
+ // Only the local run log is cleared; archives on the target are untouched.
236
+ sendJson(res, 200, { history: history.clear() })
237
+ return
238
+ }
239
+
242
240
  if (req.method === 'POST' && route === '/refresh-schedule') {
243
241
  scheduler.refresh()
244
242
  sendJson(res, 200, { scheduler: scheduler.snapshot(settings()) })
@@ -557,6 +555,33 @@ function isMasked(value) {
557
555
  return typeof value === 'string' && value !== '' && /^•+$/u.test(value)
558
556
  }
559
557
 
558
+ /**
559
+ * Put the real secrets back into a settings payload that arrived masked.
560
+ *
561
+ * The page never receives a secret — `maskSettings` replaces it with bullets —
562
+ * and echoes those bullets back on every save. Treating them as a value would
563
+ * overwrite the real credential with eight bullet characters, which is exactly
564
+ * what made a correctly configured WebDAV target answer 401: the stored
565
+ * password really was `••••••••`.
566
+ *
567
+ * Any write path that accepts settings from the browser must run this first.
568
+ */
569
+ function restoreMaskedSecrets(incoming, previous) {
570
+ if (incoming === null || typeof incoming !== 'object') return incoming
571
+ const target = incoming.target
572
+ if (target === null || typeof target !== 'object') return incoming
573
+ if (target.kind === 'webdav' && isMasked(target.password)) {
574
+ target.password = previous.target.kind === 'webdav' ? previous.target.password : ''
575
+ }
576
+ if (target.kind === 's3' && isMasked(target.secretAccessKey)) {
577
+ target.secretAccessKey = previous.target.kind === 's3' ? previous.target.secretAccessKey : ''
578
+ }
579
+ if (target.kind === 's3' && isMasked(target.sessionToken)) {
580
+ target.sessionToken = previous.target.kind === 's3' ? previous.target.sessionToken : ''
581
+ }
582
+ return incoming
583
+ }
584
+
560
585
  /**
561
586
  * Resolve the dsh-mnemon extension SDK.
562
587
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@summerscar/dsh-mnemon-sync",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Back up and restore dsh-mnemon memory components (memory spaces, project documents, runtime memory) to S3-compatible storage or WebDAV.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/scheduler.js CHANGED
@@ -72,6 +72,22 @@ export class SyncHistory {
72
72
  latest() {
73
73
  return this.read()[0]
74
74
  }
75
+
76
+ /**
77
+ * Drop the whole run history.
78
+ *
79
+ * History is a display convenience, not a record of truth: the archives
80
+ * themselves live on the target, and retention keys off the same list, so
81
+ * clearing it simply stops the page from showing past runs. Retention then
82
+ * has nothing to delete, which is the honest outcome — after clearing, this
83
+ * plugin no longer knows which keys it uploaded.
84
+ */
85
+ clear() {
86
+ const path = this.path()
87
+ mkdirSync(join(this.root, STATE_DIRECTORY), { recursive: true, mode: 0o700 })
88
+ writeFileSync(path, `${JSON.stringify({ version: 1, runs: [] }, null, 2)}\n`, { mode: 0o600 })
89
+ return []
90
+ }
75
91
  }
76
92
 
77
93
  /**