poi-plugin-remodel-assistant 1.2.3-beta.6 → 1.2.3

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/README.md CHANGED
@@ -1,34 +1,42 @@
1
1
  # 改修助手
2
2
 
3
- Poi 用舰队 Collection 装备改修规划插件。结合当前库存与随包数据,把长期改修目标拆成“今天改什么”、具体起点、完整升级路线、秘书舰条件和材料消耗。
3
+ Poi plugin for planning Kantai Collection equipment improvements.
4
4
 
5
- Beta preview channel. Beta and Stable share the npm package name `poi-plugin-remodel-assistant`; Beta uses the `beta` dist-tag and Stable uses `latest`.
5
+ Tell the plugin the equipment you ultimately want, the target improvement level, and quantity. It will combine that goal with your current inventory to organize routes, intermediate equipment, material requirements, and what you can improve today.
6
6
 
7
7
  ## Features
8
8
 
9
- - Use **Today** as the execution list for unfinished plans, with current equipment, assistant ships, available days, current-step materials, and the remaining equipment-star route in one detail view.
10
- - Use **Equipment library** to search equipment, review owned copies, add explicit target-star/quantity plans, compare total cost, and manage each plan route.
11
- - Keep route choices explicit: one valid complete route is used directly, while multiple valid routes wait for the user to choose instead of being silently auto-ranked.
12
- - Review acquisition sources when a route requires equipment you do not own, including development, quest, and ship initial/remodel sources when available.
13
- - Compare expected and guaranteed material needs, with development materials and improvement materials presented separately.
14
- - Customize ordinary detail sections by visibility and order, while required unresolved/action sections remain visible.
15
- - Use category filters, sorting, and low-priority equipment to narrow recommendations without removing valid plans or routes.
9
+ - Create improvement plans directly from **Equipment library**.
10
+ - Match plans against your current inventory to determine the practical starting point.
11
+ - Manage complete improvement routes, with explicit user choice when more than one valid route exists.
12
+ - Review acquisition sources for missing prerequisite equipment, including development, quests, and ship initial/remodel equipment when available.
13
+ - Use **Today** to see plans that can be advanced now, together with assistant ships, available days, materials, and remaining routes.
14
+ - Filter and sort equipment, manage low-priority equipment, and customize ordinary detail sections.
15
+ - Back up and restore plans, route choices, and interface settings.
16
16
 
17
17
  ## Quick Start
18
18
 
19
- 1. Open **Equipment library**, select an equipment, and add a plan with the target improvement level and quantity.
20
- 2. If the plan has more than one complete valid route, choose the route you want; if the starting equipment is missing, open its acquisition details.
21
- 3. Open **Today** and follow the current equipment, assistant ship, available day, material requirement, and remaining route shown in the detail pane.
19
+ 1. Open **Equipment library** and select your target equipment.
20
+ 2. Set the target improvement level and quantity, then create a plan.
21
+ 3. If more than one route is available, choose the route you want.
22
+ 4. Open **Today** and follow the current guidance.
22
23
 
23
- The in-plugin **Help** entry contains this workflow, detailed category/low-priority guides, data-source information, and user-facing release notes.
24
+ The in-plugin **Help** entry contains illustrated guides.
24
25
 
25
- ## Installation
26
+ ## Data Sources
26
27
 
27
- Install `poi-plugin-remodel-assistant` from the Poi plugin manager.
28
+ Improvement information is based on:
29
+
30
+ - Akashi-List
31
+ - wikiwiki.jp
32
+ - KcWiki-Api
33
+ - KC3
28
34
 
29
- ## Data
35
+ The plugin uses the latest available data and shows the active data version and update time in **Help**.
30
36
 
31
- Improvement data is based on Akashi-List, wikiwiki.jp, KcWiki-Api, and KC3. Data is updated with the packaged plugin data and is not fetched in real time while the plugin is running.
37
+ ## Installation
38
+
39
+ Install `poi-plugin-remodel-assistant` from the Poi plugin manager.
32
40
 
33
41
  ## License
34
42
 
@@ -4,8 +4,10 @@
4
4
  {
5
5
  "version": "1.2.3",
6
6
  "items": [
7
- "changelog_1_2_3_latent_issue_fixes",
8
- "changelog_1_2_3_runtime_data_auto_update_fix"
7
+ "changelog_1_2_3_runtime_data_auto_update_fix",
8
+ "changelog_1_2_3_equipment_planning_ui",
9
+ "changelog_1_2_3_problem_diagnostics",
10
+ "changelog_1_2_3_host_compatibility_and_stability"
9
11
  ]
10
12
  },
11
13
  {
package/i18n/en-US.json CHANGED
@@ -1465,7 +1465,6 @@
1465
1465
  "app.changelog_1_2_0_backup": "Back up and restore plans, route choices, interface settings, and related user data.",
1466
1466
  "app.changelog_1_2_0_help_and_data": "Illustrated Help is included with the active data version and update time, and interface/help language follows Poi i18n settings.",
1467
1467
  "app.Data package updated, refreshing decision chain...": "Data package updated. Refreshing decision chain…",
1468
- "app.changelog_1_2_3_latent_issue_fixes": "Fixed several latent issues.",
1469
1468
  "app.changelog_1_2_3_runtime_data_auto_update_fix": "Fixed an issue that could prevent the data package from updating automatically as expected.",
1470
1469
  "app.Diagnostics": "Diagnostics",
1471
1470
  "diagnostics.All known plugin-owned Poi config keys, including compatibility keys": "All known plugin-owned Poi config keys, including compatibility keys",
@@ -1503,5 +1502,12 @@
1503
1502
  "diagnostics.Canonical plugin backup with plans, route choices, assignments, blacklist, and UI preferences": "Canonical plugin backup with plans, route choices, assignments, blacklist, and UI preferences",
1504
1503
  "equipment.Upgrade chain terminal": "Upgrade endpoint",
1505
1504
  "equipment.Improvement route": "Improvement route",
1506
- "equipment.Improvement endpoint": "Improvement endpoint"
1505
+ "equipment.Improvement endpoint": "Improvement endpoint",
1506
+ "app.changelog_1_2_3_equipment_planning_ui": "Improved plan and owned-equipment information in Today and the Equipment Library, refined upgrade-route filters, and now remembers search and sort choices.",
1507
+ "app.changelog_1_2_3_problem_diagnostics": "Added problem diagnostics: clear caches and reload the plugin first, then export diagnostic data if the problem remains.",
1508
+ "app.changelog_1_2_3_host_compatibility_and_stability": "Improved Poi 12 / React 18 compatibility and fixed several latent issues.",
1509
+ "diagnostics.Exports plugin diagnostic data and the personal inventory data stored by Poi under Local Storage/leveldb.": "Packages plugin diagnostic data and the personal inventory state stored by Poi under Local Storage/leveldb.",
1510
+ "diagnostics.Plugin diagnostic data file": "Plugin diagnostic data",
1511
+ "diagnostics.Poi personal inventory data file": "Poi personal inventory data",
1512
+ "diagnostics.Some Poi personal inventory files were in use and could not be copied. The skipped files are listed in manifest.json.": "Some Poi personal inventory files were in use and could not be copied. The diagnostic bundle was saved and the skipped files are listed in manifest.json."
1507
1513
  }
package/i18n/ja-JP.json CHANGED
@@ -1465,7 +1465,6 @@
1465
1465
  "app.changelog_1_2_0_backup": "計画、ルート選択、画面設定などのデータをバックアップ/復元できます。",
1466
1466
  "app.changelog_1_2_0_help_and_data": "図解ヘルプを内蔵し、現在のデータ版と更新時刻を表示します。画面とヘルプの言語は Poi の i18n 設定に連動します。",
1467
1467
  "app.Data package updated, refreshing decision chain...": "データパッケージを更新しました。判断チェーンを更新しています…",
1468
- "app.changelog_1_2_3_latent_issue_fixes": "いくつかの潜在的な問題を修正しました。",
1469
1468
  "app.changelog_1_2_3_runtime_data_auto_update_fix": "データパッケージが想定どおり自動更新されないことがある問題を修正しました。",
1470
1469
  "app.Diagnostics": "診断",
1471
1470
  "diagnostics.All known plugin-owned Poi config keys, including compatibility keys": "互換キーを含む、このプラグインの既知の Poi 設定一式",
@@ -1503,5 +1502,12 @@
1503
1502
  "diagnostics.Canonical plugin backup with plans, route choices, assignments, blacklist, and UI preferences": "計画、ルート選択、装備割り当て、低優先装備、UI設定を含む標準プラグインバックアップ",
1504
1503
  "equipment.Upgrade chain terminal": "改修終点",
1505
1504
  "equipment.Improvement route": "改修ルート",
1506
- "equipment.Improvement endpoint": "改修終点"
1505
+ "equipment.Improvement endpoint": "改修終点",
1506
+ "app.changelog_1_2_3_equipment_planning_ui": "「今日の改修」と装備ライブラリの計画・所持装備情報の表示を改善し、改修ルートのフィルターを整理して、検索・並び順を記憶するようにしました。",
1507
+ "app.changelog_1_2_3_problem_diagnostics": "問題診断を追加しました。まずキャッシュを消去してプラグインを再読み込みし、問題が残る場合は診断データを出力できます。",
1508
+ "app.changelog_1_2_3_host_compatibility_and_stability": "Poi 12 / React 18 との互換性を改善し、いくつかの潜在的な問題を修正しました。",
1509
+ "diagnostics.Exports plugin diagnostic data and the personal inventory data stored by Poi under Local Storage/leveldb.": "プラグインの診断データと、Poi の Local Storage/leveldb にある個人インベントリ状態のデータをまとめます。",
1510
+ "diagnostics.Plugin diagnostic data file": "プラグイン診断データ",
1511
+ "diagnostics.Poi personal inventory data file": "Poi 個人インベントリデータ",
1512
+ "diagnostics.Some Poi personal inventory files were in use and could not be copied. The skipped files are listed in manifest.json.": "一部の Poi 個人インベントリファイルは使用中のためコピーできませんでした。診断 ZIP は保存され、スキップしたファイルは manifest.json に記録されています。"
1507
1513
  }
package/i18n/zh-CN.json CHANGED
@@ -1465,7 +1465,6 @@
1465
1465
  "app.changelog_1_2_0_backup": "支持计划、路线选择和界面设置等数据的备份与恢复。",
1466
1466
  "app.changelog_1_2_0_help_and_data": "内置图文帮助,并显示当前数据版本和更新时间;界面与帮助语言对接 Poi 的 i18n 设置。",
1467
1467
  "app.Data package updated, refreshing decision chain...": "数据包已更新,正在刷新决策链路…",
1468
- "app.changelog_1_2_3_latent_issue_fixes": "修复一些潜在问题。",
1469
1468
  "app.changelog_1_2_3_runtime_data_auto_update_fix": "修复数据包无法按预期自动更新的问题。",
1470
1469
  "app.Diagnostics": "诊断",
1471
1470
  "diagnostics.All known plugin-owned Poi config keys, including compatibility keys": "本插件全部已知 Poi 配置项(含兼容键)",
@@ -1503,5 +1502,12 @@
1503
1502
  "diagnostics.Canonical plugin backup with plans, route choices, assignments, blacklist, and UI preferences": "插件标准备份,包含计划、路线选择、装备分配、低优先装备和界面设置",
1504
1503
  "equipment.Upgrade chain terminal": "改修终点",
1505
1504
  "equipment.Improvement route": "改修路线",
1506
- "equipment.Improvement endpoint": "改修终点"
1505
+ "equipment.Improvement endpoint": "改修终点",
1506
+ "app.changelog_1_2_3_equipment_planning_ui": "优化“今天改什么”和装备库的计划与持有装备信息显示,改进改修路线筛选,并记住搜索与排序。",
1507
+ "app.changelog_1_2_3_problem_diagnostics": "新增问题诊断,可先清理缓存并重载插件;问题仍存在时可导出诊断数据。",
1508
+ "app.changelog_1_2_3_host_compatibility_and_stability": "提升 Poi 12 / React 18 兼容性并修复若干潜在问题。",
1509
+ "diagnostics.Exports plugin diagnostic data and the personal inventory data stored by Poi under Local Storage/leveldb.": "打包插件诊断数据,以及 Poi 的 Local Storage/leveldb 中用于恢复个人库存现场的数据。",
1510
+ "diagnostics.Plugin diagnostic data file": "插件诊断数据",
1511
+ "diagnostics.Poi personal inventory data file": "Poi 个人库存数据",
1512
+ "diagnostics.Some Poi personal inventory files were in use and could not be copied. The skipped files are listed in manifest.json.": "部分 Poi 个人库存文件正在被占用,未能复制;诊断包已保存,跳过的文件记录在 manifest.json 中。"
1507
1513
  }
package/i18n/zh-TW.json CHANGED
@@ -1465,7 +1465,6 @@
1465
1465
  "app.changelog_1_2_0_backup": "支援計畫、路線選擇與介面設定等資料的備份與還原。",
1466
1466
  "app.changelog_1_2_0_help_and_data": "內建圖文幫助,並顯示目前資料版本與更新時間;介面與幫助語言對接 Poi 的 i18n 設定。",
1467
1467
  "app.Data package updated, refreshing decision chain...": "資料包已更新,正在重新整理決策鏈路…",
1468
- "app.changelog_1_2_3_latent_issue_fixes": "修復一些潛在問題。",
1469
1468
  "app.changelog_1_2_3_runtime_data_auto_update_fix": "修復資料包無法按預期自動更新的問題。",
1470
1469
  "app.Diagnostics": "診斷",
1471
1470
  "diagnostics.All known plugin-owned Poi config keys, including compatibility keys": "此外掛全部已知 Poi 設定項(含相容鍵)",
@@ -1503,5 +1502,12 @@
1503
1502
  "diagnostics.Canonical plugin backup with plans, route choices, assignments, blacklist, and UI preferences": "外掛標準備份,包含計畫、路線選擇、裝備分配、低優先裝備與介面設定",
1504
1503
  "equipment.Upgrade chain terminal": "改修終點",
1505
1504
  "equipment.Improvement route": "改修路線",
1506
- "equipment.Improvement endpoint": "改修終點"
1505
+ "equipment.Improvement endpoint": "改修終點",
1506
+ "app.changelog_1_2_3_equipment_planning_ui": "最佳化「今天改什麼」與裝備庫的計畫及持有裝備資訊顯示,改進改修路線篩選,並記住搜尋與排序。",
1507
+ "app.changelog_1_2_3_problem_diagnostics": "新增問題診斷,可先清理快取並重新載入外掛;問題仍存在時可匯出診斷資料。",
1508
+ "app.changelog_1_2_3_host_compatibility_and_stability": "提升 Poi 12 / React 18 相容性並修復若干潛在問題。",
1509
+ "diagnostics.Exports plugin diagnostic data and the personal inventory data stored by Poi under Local Storage/leveldb.": "打包外掛診斷資料,以及 Poi 的 Local Storage/leveldb 中用於還原個人庫存現場的資料。",
1510
+ "diagnostics.Plugin diagnostic data file": "外掛診斷資料",
1511
+ "diagnostics.Poi personal inventory data file": "Poi 個人庫存資料",
1512
+ "diagnostics.Some Poi personal inventory files were in use and could not be copied. The skipped files are listed in manifest.json.": "部分 Poi 個人庫存檔案正在被佔用,未能複製;診斷包已儲存,略過的檔案記錄在 manifest.json 中。"
1507
1513
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-remodel-assistant",
3
- "version": "1.2.3-beta.6",
3
+ "version": "1.2.3",
4
4
  "description": "View enhanced KanColle improvement data, manage improvement plans, and receive improvement recommendations.",
5
5
  "main": "index.js",
6
6
  "author": {
@@ -27,7 +27,7 @@
27
27
  "icon": "fa/wrench",
28
28
  "id": "poi-plugin-remodel-assistant",
29
29
  "priority": 50,
30
- "releaseChannel": "beta",
30
+ "releaseChannel": "stable",
31
31
  "title": "改修助手",
32
32
  "runtimeFlags": {
33
33
  "forceFullDecisionRebuild": false
@@ -428,12 +428,12 @@ class NewInterfaceArea extends _react.Component {
428
428
  bsSize: "small",
429
429
  onClick: this.handleShowDataTransfer
430
430
  }, __('Data')), _react.default.createElement(_reactBootstrap.Button, {
431
- bsSize: "small",
432
- onClick: this.handleShowProblemDiagnostics
433
- }, __('Diagnostics')), _react.default.createElement(_reactBootstrap.Button, {
434
431
  bsSize: "small",
435
432
  onClick: onShowHelp
436
- }, __('Help')))), runtimeDataDecisionRefreshPending && _react.default.createElement(_reactBootstrap.Alert, {
433
+ }, __('Help')), _react.default.createElement(_reactBootstrap.Button, {
434
+ bsSize: "small",
435
+ onClick: this.handleShowProblemDiagnostics
436
+ }, __('Diagnostics')))), runtimeDataDecisionRefreshPending && _react.default.createElement(_reactBootstrap.Alert, {
437
437
  bsStyle: "info",
438
438
  className: "decision-runtime-data-refresh-note"
439
439
  }, __('Data package updated, refreshing decision chain...')), _react.default.createElement("main", {
@@ -33,10 +33,6 @@ const {
33
33
 
34
34
  const __ = (key, ...args) => translate(`diagnostics.${key}`, ...args);
35
35
 
36
- const {
37
- config
38
- } = window;
39
-
40
36
  const downloadBinary = (filename, buffer) => {
41
37
  const blob = new Blob([buffer], {
42
38
  type: 'application/zip'
@@ -65,6 +61,7 @@ class ProblemDiagnosticsModal extends _react.Component {
65
61
  busy: false,
66
62
  confirmingReset: false,
67
63
  message: '',
64
+ warning: '',
68
65
  error: ''
69
66
  }
70
67
  }), Object.defineProperty(this, "refreshPreview", {
@@ -72,18 +69,12 @@ class ProblemDiagnosticsModal extends _react.Component {
72
69
  enumerable: true,
73
70
  writable: true,
74
71
  value: () => {
75
- const {
76
- inventory
77
- } = this.props;
78
72
  this.setState({
79
- bundleFiles: (0, _problemDiagnostics.previewDiagnosticBundleContents)({
80
- config,
81
- inventory,
82
- hostWindow: window
83
- }),
73
+ bundleFiles: (0, _problemDiagnostics.previewDiagnosticBundleContents)(),
84
74
  busy: false,
85
75
  confirmingReset: false,
86
76
  message: '',
77
+ warning: '',
87
78
  error: ''
88
79
  });
89
80
  }
@@ -100,14 +91,12 @@ class ProblemDiagnosticsModal extends _react.Component {
100
91
  enumerable: true,
101
92
  writable: true,
102
93
  value: async event => {
103
- const {
104
- inventory
105
- } = this.props;
106
94
  const hostWindow = event.currentTarget.ownerDocument.defaultView;
107
95
  const filename = (0, _problemDiagnostics.makeDiagnosticBundleFilename)();
108
96
  this.setState({
109
97
  busy: true,
110
98
  message: '',
99
+ warning: '',
111
100
  error: ''
112
101
  });
113
102
  let selection;
@@ -130,18 +119,15 @@ class ProblemDiagnosticsModal extends _react.Component {
130
119
  }
131
120
 
132
121
  try {
133
- const bundle = (0, _problemDiagnostics.buildDiagnosticBundle)({
134
- config,
135
- inventory,
136
- hostWindow
137
- });
122
+ const bundle = await (0, _problemDiagnostics.buildDiagnosticBundle)();
138
123
 
139
124
  if (!selection.supported) {
140
125
  downloadBinary(filename, bundle.buffer);
141
126
  this.setState({
142
127
  busy: false,
143
128
  bundleFiles: bundle.files,
144
- message: __('Diagnostic bundle exported')
129
+ message: __('Diagnostic bundle exported'),
130
+ warning: bundle.partial ? __('Some Poi personal inventory files were in use and could not be copied. The skipped files are listed in manifest.json.') : ''
145
131
  });
146
132
  return;
147
133
  }
@@ -151,7 +137,8 @@ class ProblemDiagnosticsModal extends _react.Component {
151
137
  this.setState({
152
138
  busy: false,
153
139
  bundleFiles: bundle.files,
154
- message: __('Diagnostic bundle saved as %s').replace('%s', _path.default.basename(selection.filePath))
140
+ message: __('Diagnostic bundle saved as %s').replace('%s', _path.default.basename(selection.filePath)),
141
+ warning: bundle.partial ? __('Some Poi personal inventory files were in use and could not be copied. The skipped files are listed in manifest.json.') : ''
155
142
  });
156
143
  } catch (error) {
157
144
  this.setState({
@@ -168,6 +155,7 @@ class ProblemDiagnosticsModal extends _react.Component {
168
155
  this.setState({
169
156
  confirmingReset: true,
170
157
  message: '',
158
+ warning: '',
171
159
  error: ''
172
160
  });
173
161
  }
@@ -189,6 +177,7 @@ class ProblemDiagnosticsModal extends _react.Component {
189
177
  this.setState({
190
178
  busy: true,
191
179
  message: '',
180
+ warning: '',
192
181
  error: ''
193
182
  });
194
183
 
@@ -236,7 +225,8 @@ class ProblemDiagnosticsModal extends _react.Component {
236
225
  busy,
237
226
  confirmingReset,
238
227
  error,
239
- message
228
+ message,
229
+ warning
240
230
  } = this.state;
241
231
  return _react.default.createElement(_poiDialog.PoiDialog, {
242
232
  show: show,
@@ -254,21 +244,9 @@ class ProblemDiagnosticsModal extends _react.Component {
254
244
  bsStyle: "danger"
255
245
  }, error), message && _react.default.createElement(_reactBootstrap.Alert, {
256
246
  bsStyle: "success"
257
- }, message), _react.default.createElement("section", {
258
- className: "decision-diagnostics-section"
259
- }, _react.default.createElement("div", {
260
- className: "decision-diagnostics-section-copy"
261
- }, _react.default.createElement("h4", null, __('Export diagnostic bundle')), _react.default.createElement("p", null, __('Exports the inventory and all plugin configuration needed to reproduce a problem.')), _react.default.createElement("p", {
262
- className: "decision-diagnostics-warning"
263
- }, __('The bundle contains inventory and plugin settings. Review it before sharing.')), _react.default.createElement("strong", null, __('Files included')), this.renderFileList()), _react.default.createElement(_reactBootstrap.Button, {
264
- className: "decision-diagnostics-primary-action",
265
- bsStyle: "primary",
266
- disabled: busy,
267
- onClick: this.handleExport
268
- }, _react.default.createElement("span", {
269
- className: "glyphicon glyphicon-save",
270
- "aria-hidden": "true"
271
- }), __('Choose save location'))), _react.default.createElement("section", {
247
+ }, message), warning && _react.default.createElement(_reactBootstrap.Alert, {
248
+ bsStyle: "warning"
249
+ }, warning), _react.default.createElement("section", {
272
250
  className: "decision-diagnostics-section is-danger"
273
251
  }, _react.default.createElement("div", {
274
252
  className: "decision-diagnostics-section-copy"
@@ -288,7 +266,19 @@ class ProblemDiagnosticsModal extends _react.Component {
288
266
  bsStyle: "danger",
289
267
  disabled: busy,
290
268
  onClick: this.handleConfirmReset
291
- }, busy ? __('Clearing caches...') : __('Confirm clear and restart'))))), _react.default.createElement(_poiDialog.PoiDialog.Footer, null, _react.default.createElement(_reactBootstrap.Button, {
269
+ }, busy ? __('Clearing caches...') : __('Confirm clear and restart')))), _react.default.createElement("section", {
270
+ className: "decision-diagnostics-section"
271
+ }, _react.default.createElement("div", {
272
+ className: "decision-diagnostics-section-copy"
273
+ }, _react.default.createElement("h4", null, __('Export diagnostic bundle')), _react.default.createElement("p", null, __('Exports plugin diagnostic data and the personal inventory data stored by Poi under Local Storage/leveldb.')), _react.default.createElement("strong", null, __('Files included')), this.renderFileList()), _react.default.createElement(_reactBootstrap.Button, {
274
+ className: "decision-diagnostics-primary-action",
275
+ bsStyle: "primary",
276
+ disabled: busy,
277
+ onClick: this.handleExport
278
+ }, _react.default.createElement("span", {
279
+ className: "glyphicon glyphicon-save",
280
+ "aria-hidden": "true"
281
+ }), __('Choose save location')))), _react.default.createElement(_poiDialog.PoiDialog.Footer, null, _react.default.createElement(_reactBootstrap.Button, {
292
282
  disabled: busy,
293
283
  onClick: this.handleClose
294
284
  }, __('Close'))), _react.default.createElement(_modalResizeHandle.ModalResizeHandle, {
@@ -5,6 +5,8 @@ exports.showZipSaveDialog = exports.showJsonSaveDialog = exports.showJsonOpenDia
5
5
 
6
6
  var _fs = _interopRequireDefault(require("fs"));
7
7
 
8
+ var _os = _interopRequireDefault(require("os"));
9
+
8
10
  var _path = _interopRequireDefault(require("path"));
9
11
 
10
12
  var _personalProfileStorage = require("./personal-profile-storage");
@@ -42,6 +44,18 @@ const ensurePluginDataDirectory = () => {
42
44
  return directory;
43
45
  };
44
46
 
47
+ const diagnosticExportDirectory = remote => {
48
+ try {
49
+ if (remote && remote.app && typeof remote.app.getPath === 'function') {
50
+ const downloads = remote.app.getPath('downloads');
51
+ if (downloads) return downloads;
52
+ }
53
+ } catch (error) {// Fall back to the user's Downloads directory when host path lookup is unavailable.
54
+ }
55
+
56
+ return _path.default.join(_os.default.homedir(), 'Downloads');
57
+ };
58
+
45
59
  const focusRenderer = (hostWindow, remote) => {
46
60
  try {
47
61
  if (hostWindow && typeof hostWindow.focus === 'function') hostWindow.focus();
@@ -168,7 +182,7 @@ const showZipSaveDialog = async (hostWindow, filename, title = 'Export diagnosti
168
182
  try {
169
183
  const result = await runDialog(remote.dialog, 'showSaveDialog', dialogParent(remote), {
170
184
  title,
171
- defaultPath: _path.default.join(ensurePluginDataDirectory(), filename),
185
+ defaultPath: _path.default.join(diagnosticExportDirectory(remote), filename),
172
186
  filters: [{
173
187
  name: 'ZIP',
174
188
  extensions: ['zip']
@@ -11,8 +11,6 @@ var _os = _interopRequireDefault(require("os"));
11
11
 
12
12
  var _path = _interopRequireDefault(require("path"));
13
13
 
14
- var _changelog = require("../app/changelog");
15
-
16
14
  var _decisionFoundationService = require("../new-interface/app/decision-foundation-service");
17
15
 
18
16
  var _decisionSnapshotCache = require("../new-interface/app/decision-snapshot-cache");
@@ -25,10 +23,6 @@ var _acquisitionDomain = require("../new-interface/equipment/domain/acquisition-
25
23
 
26
24
  var _inventoryDomain = require("../new-interface/equipment/domain/inventory-domain");
27
25
 
28
- var _currentStartSelectionDomain = require("../new-interface/plan/domain/current-start-selection-domain");
29
-
30
- var _currentStartAssignmentDomain = require("../new-interface/plan/domain/current-start-assignment-domain");
31
-
32
26
  var _dataPackageDomain = require("./domain/data-package-domain");
33
27
 
34
28
  var _improvementCostCache = require("./improvement-cost-cache");
@@ -37,26 +31,18 @@ var _personalProfileStorage = require("./personal-profile-storage");
37
31
 
38
32
  var _pluginMeta = require("./plugin-meta");
39
33
 
40
- var _planStorage = require("./plan-storage");
41
-
42
34
  var _runtimeDataPackage = require("./runtime-data-package");
43
35
 
44
- var _dataTransfer = require("./data-transfer");
45
-
46
- var _uiPreferences = require("./ui-preferences");
47
-
48
- var _uiZoom = require("./ui-zoom");
49
-
50
- var _windowBounds = require("./window-bounds");
51
-
52
36
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
53
37
 
54
38
  function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
55
39
 
56
40
  const DIAGNOSTIC_BUNDLE_FORMAT = `${_pluginMeta.PLUGIN_ID}.diagnostic-bundle`;
57
41
  exports.DIAGNOSTIC_BUNDLE_FORMAT = DIAGNOSTIC_BUNDLE_FORMAT;
58
- const DIAGNOSTIC_BUNDLE_SCHEMA_VERSION = 1;
42
+ const DIAGNOSTIC_BUNDLE_SCHEMA_VERSION = 3;
59
43
  exports.DIAGNOSTIC_BUNDLE_SCHEMA_VERSION = DIAGNOSTIC_BUNDLE_SCHEMA_VERSION;
44
+ const DIAGNOSTIC_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
45
+ const DIAGNOSTIC_COPY_RETRY_DELAYS_MS = [0, 40, 120];
60
46
 
61
47
  const json = value => `${JSON.stringify(value, null, 2)}\n`;
62
48
 
@@ -70,237 +56,116 @@ const portablePath = value => {
70
56
  return home && value.startsWith(home) ? `~${value.slice(home.length)}` : value;
71
57
  };
72
58
 
73
- const statSummary = filePath => {
74
- try {
75
- const stat = _fs.default.statSync(filePath);
76
-
77
- return {
78
- exists: true,
79
- type: stat.isDirectory() ? 'directory' : 'file',
80
- bytes: stat.isFile() ? stat.size : null,
81
- modifiedAt: stat.mtime.toISOString()
82
- };
83
- } catch (error) {
84
- return {
85
- exists: false,
86
- type: null,
87
- bytes: null,
88
- modifiedAt: null
89
- };
90
- }
91
- };
92
-
93
- const safeRead = filePath => {
94
- try {
95
- return _fs.default.existsSync(filePath) ? _fs.default.readFileSync(filePath) : null;
96
- } catch (error) {
97
- return null;
98
- }
59
+ const toArchivePath = value => value.split(_path.default.sep).join('/');
60
+
61
+ const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
62
+
63
+ const diagnosticSourceSpecs = () => {
64
+ const pluginDataDirectory = (0, _personalProfileStorage.getPluginDataDirectory)();
65
+ const runtimeDataPaths = (0, _runtimeDataPackage.getRuntimeDataPackagePaths)();
66
+ const pluginArchiveRoot = `poi/plugin-data/${_pluginMeta.PLUGIN_ID}`;
67
+ return [{
68
+ kind: 'directory',
69
+ sourcePath: _path.default.join((0, _personalProfileStorage.resolvePoiAppDataPath)(), 'Local Storage', 'leveldb'),
70
+ archiveRoot: 'poi/Local Storage/leveldb',
71
+ description: 'Poi personal inventory data file',
72
+ bestEffort: true
73
+ }, {
74
+ kind: 'file',
75
+ sourcePath: _path.default.join(pluginDataDirectory, 'personal-profile.json'),
76
+ archivePath: `${pluginArchiveRoot}/personal-profile.json`,
77
+ description: 'Plugin diagnostic data file',
78
+ bestEffort: false
79
+ }, {
80
+ kind: 'directory',
81
+ sourcePath: runtimeDataPaths.current,
82
+ archiveRoot: `${pluginArchiveRoot}/runtime-data-package/current`,
83
+ description: 'Plugin diagnostic data file',
84
+ bestEffort: false
85
+ }, {
86
+ kind: 'file',
87
+ sourcePath: runtimeDataPaths.status,
88
+ archivePath: `${pluginArchiveRoot}/runtime-data-package/status.json`,
89
+ description: 'Plugin diagnostic data file',
90
+ bestEffort: false
91
+ }];
99
92
  };
100
93
 
101
- const currentConfigKeys = [_planStorage.keyPlans, _uiPreferences.NEW_INTERFACE_CONFIG_KEY, _uiPreferences.FONT_SCALE_CONFIG_KEY, _uiPreferences.DECISION_CHAIN_SELECTIONS_CONFIG_KEY, _uiPreferences.WORKBENCH_SORT_CONFIG_KEY, _uiPreferences.SELECTED_ITEM_ID_CONFIG_KEY, _uiPreferences.EQUIPMENT_CATEGORY_FILTER_CONFIG_KEY, _uiPreferences.EQUIPMENT_WORKSPACE_SORT_CONFIG_KEY, _uiPreferences.SEARCH_QUERY_CONFIG_KEY, _uiPreferences.PERSONAL_PROFILE_DATA_SOURCE_CONFIG_KEY, _uiPreferences.DECISION_ROUTE_VIEW_MODE_CONFIG_KEY, _uiPreferences.EQUIPMENT_SPLIT_RATIO_CONFIG_KEY, _uiPreferences.WORKBENCH_SPLIT_RATIO_CONFIG_KEY, _uiPreferences.EQUIPMENT_PANE_WIDTH_CONFIG_KEY, _uiPreferences.WORKBENCH_PANE_WIDTH_CONFIG_KEY, _uiPreferences.DETAIL_PANEL_LAYOUTS_CONFIG_KEY, _uiZoom.UI_ZOOM_CONFIG_KEY, _windowBounds.WINDOW_BOUNDS_CONFIG_KEY, _equipmentBlacklistDomain.EQUIPMENT_BLACKLIST_CONFIG_KEY, _currentStartSelectionDomain.CURRENT_START_SELECTIONS_CONFIG_KEY, _currentStartAssignmentDomain.PLAN_EQUIPMENT_ASSIGNMENTS_CONFIG_KEY, _changelog.CHANGELOG_CONFIG_KEY, _changelog.HELP_GUIDE_CONFIG_KEY];
102
- const legacySuffixes = ['useNewInterface', 'fontScale', 'decisionChainSelections', 'workbenchSort', 'selectedItemId', 'equipmentCategoryFilter', 'personalProfileDataSource', 'decisionRouteViewMode', 'equipmentSplitRatio', 'workbenchSplitRatio', 'equipmentPaneWidth', 'workbenchPaneWidth', 'detailPanelLayouts', 'zoom', 'windowBounds', 'equipmentBlacklist', 'planRoutePreferences', 'planEquipmentAssignments', 'lastSeenChangelogVersion', 'lastSeenHelpGuideVersion'];
103
- const pluginConfigKeys = Array.from(new Set([...currentConfigKeys, ..._planStorage.LEGACY_PLAN_STORAGE_KEYS, ..._currentStartSelectionDomain.LEGACY_CURRENT_START_SELECTION_KEYS, ..._pluginMeta.LEGACY_PLUGIN_IDS.flatMap(pluginId => legacySuffixes.map(suffix => `${pluginId}.${suffix}`))])).sort();
104
-
105
- const readConfigSubset = (config, keys) => {
106
- if (!config || typeof config.get !== 'function') return {};
107
- const missing = {};
108
- return keys.reduce((result, key) => {
109
- const value = config.get(key, missing);
110
- if (value !== missing) result[key] = value;
111
- return result;
112
- }, {});
113
- };
114
-
115
- const readPluginLocalStorage = hostWindow => {
116
- const storage = hostWindow && hostWindow.localStorage;
117
- if (!storage) return {};
118
-
119
- const prefixes = _pluginMeta.COMPATIBLE_PLUGIN_IDS.map(pluginId => `${pluginId}.`);
120
-
121
- const result = {};
122
-
123
- try {
124
- for (let index = 0; index < storage.length; index += 1) {
125
- const key = storage.key(index);
126
- if (!key || !prefixes.some(prefix => key.startsWith(prefix))) continue;
127
- result[key] = storage.getItem(key);
128
- }
129
- } catch (error) {
130
- return {
131
- __error: error && error.message ? error.message : String(error)
132
- };
133
- }
134
-
135
- return result;
136
- };
94
+ const enumerateDirectoryFiles = spec => {
95
+ const entries = [];
137
96
 
138
- const runtimeDataStatus = () => {
139
- const paths = (0, _runtimeDataPackage.getRuntimeDataPackagePaths)();
140
- const statusBuffer = safeRead(paths.status);
141
- let status = null;
97
+ const visit = (directory, relativeDirectory = '') => {
98
+ let names;
142
99
 
143
- if (statusBuffer) {
144
100
  try {
145
- status = JSON.parse(statusBuffer.toString('utf8'));
101
+ names = _fs.default.readdirSync(directory).sort();
146
102
  } catch (error) {
147
- status = {
148
- invalidJson: true,
149
- raw: statusBuffer.toString('utf8')
150
- };
103
+ return;
151
104
  }
152
- }
153
105
 
154
- return {
155
- status,
156
- files: Object.keys(paths).reduce((result, key) => _extends({}, result, {
157
- [key]: statSummary(paths[key])
158
- }), {})
106
+ names.forEach(name => {
107
+ const sourcePath = _path.default.join(directory, name);
108
+
109
+ let stat;
110
+
111
+ try {
112
+ stat = _fs.default.lstatSync(sourcePath);
113
+ } catch (error) {
114
+ return;
115
+ }
116
+
117
+ if (stat.isSymbolicLink()) return;
118
+ const relativePath = relativeDirectory ? _path.default.join(relativeDirectory, name) : name;
119
+
120
+ if (stat.isDirectory()) {
121
+ visit(sourcePath, relativePath);
122
+ return;
123
+ }
124
+
125
+ if (!stat.isFile()) return;
126
+ entries.push({
127
+ sourcePath,
128
+ archivePath: `${spec.archiveRoot}/${toArchivePath(relativePath)}`,
129
+ description: spec.description,
130
+ bestEffort: spec.bestEffort,
131
+ bytes: stat.size
132
+ });
133
+ });
159
134
  };
160
- };
161
135
 
162
- const improvementCostCacheStatus = () => {
163
- const paths = (0, _improvementCostCache.getImprovementCostCachePaths)();
164
- return Object.keys(paths).reduce((result, key) => _extends({}, result, {
165
- [key]: statSummary(paths[key])
166
- }), {});
136
+ if (_fs.default.existsSync(spec.sourcePath)) visit(spec.sourcePath);
137
+ return entries;
167
138
  };
168
139
 
169
- const diagnosticEnvironment = inventory => {
170
- let dataPackage = null;
140
+ const enumerateSourceFiles = (specs = diagnosticSourceSpecs()) => specs.reduce((entries, spec) => {
141
+ if (spec.kind === 'directory') return entries.concat(enumerateDirectoryFiles(spec));
142
+ if (!_fs.default.existsSync(spec.sourcePath)) return entries;
143
+ let stat;
171
144
 
172
145
  try {
173
- const snapshot = (0, _dataPackageDomain.getDataPackageSnapshot)();
174
- dataPackage = {
175
- identity: (0, _dataPackageDomain.getDataPackageIdentity)(),
176
- packageVersion: snapshot && snapshot.packageVersion,
177
- root: portablePath(snapshot && snapshot.root),
178
- manifest: snapshot && snapshot.manifest
179
- };
146
+ stat = _fs.default.lstatSync(spec.sourcePath);
180
147
  } catch (error) {
181
- dataPackage = {
182
- error: error && error.message ? error.message : String(error)
183
- };
184
- }
185
-
186
- return {
187
- plugin: {
188
- id: _pluginMeta.PLUGIN_ID,
189
- version: _pluginMeta.PLUGIN_VERSION,
190
- releaseChannel: _pluginMeta.RELEASE_CHANNEL
191
- },
192
- runtime: {
193
- platform: process.platform,
194
- arch: process.arch,
195
- versions: process.versions
196
- },
197
- poi: {
198
- appDataPath: portablePath((0, _personalProfileStorage.resolvePoiAppDataPath)()),
199
- pluginDataPath: portablePath((0, _personalProfileStorage.getPluginDataDirectory)())
200
- },
201
- inventorySource: inventory && inventory.source,
202
- dataPackage
203
- };
204
- };
205
-
206
- const virtualEntries = ({
207
- config,
208
- inventory,
209
- hostWindow
210
- }) => [{
211
- path: 'inventory/equipment.json',
212
- description: 'Current effective equipment inventory used by the plugin',
213
- content: json(inventory && inventory.equips || {})
214
- }, {
215
- path: 'inventory/ships.json',
216
- description: 'Current effective ship inventory used by the plugin',
217
- content: json(inventory && inventory.ships || {})
218
- }, {
219
- path: 'inventory/resources.json',
220
- description: 'Current effective resources used by the plugin',
221
- content: json(inventory && inventory.resources || [])
222
- }, {
223
- path: 'inventory/useitems.json',
224
- description: 'Current effective useitem inventory used by the plugin',
225
- content: json(inventory && inventory.useitems || {})
226
- }, {
227
- path: 'config/plugin-backup.json',
228
- description: 'Canonical plugin backup with plans, route choices, assignments, blacklist, and UI preferences',
229
- content: (0, _dataTransfer.serializeExportPayload)(config)
230
- }, {
231
- path: 'config/plugin-config.json',
232
- description: 'All known plugin-owned Poi config keys, including compatibility keys',
233
- content: json({
234
- pluginIds: _pluginMeta.COMPATIBLE_PLUGIN_IDS,
235
- values: readConfigSubset(config, pluginConfigKeys)
236
- })
237
- }, {
238
- path: 'config/host-config.json',
239
- description: 'Small Poi host config subset consumed by this plugin',
240
- content: json(readConfigSubset(config, ['poi.misc.language', 'packageManager.mirrorName', 'poi.window.x', 'poi.window.y']))
241
- }, {
242
- path: 'config/plugin-local-storage.json',
243
- description: 'Plugin-owned renderer localStorage values such as modal bounds',
244
- content: json(readPluginLocalStorage(hostWindow))
245
- }, {
246
- path: 'runtime/environment.json',
247
- description: 'Plugin/runtime/data-package identity used for reproduction',
248
- content: json(diagnosticEnvironment(inventory))
249
- }, {
250
- path: 'runtime/runtime-data-status.json',
251
- description: 'Runtime data package status and cache metadata',
252
- content: json(runtimeDataStatus())
253
- }, {
254
- path: 'runtime/improvement-cost-cache-status.json',
255
- description: 'Improvement-cost cache metadata',
256
- content: json(improvementCostCacheStatus())
257
- }];
258
-
259
- const optionalFileEntries = () => {
260
- const entries = [];
261
- const profilePath = (0, _personalProfileStorage.getPersonalProfilePath)();
262
- const profile = safeRead(profilePath);
263
-
264
- if (profile) {
265
- entries.push({
266
- path: 'inventory/personal-profile.json',
267
- description: 'Persisted personal profile when present',
268
- content: profile
269
- });
148
+ return entries;
270
149
  }
271
150
 
272
- const costPaths = (0, _improvementCostCache.getImprovementCostCachePaths)();
273
- [['cache/improvement-cost-current.json', costPaths.runtime, 'Current persisted improvement-cost cache'], ['cache/improvement-cost-previous.json', costPaths.previous, 'Previous persisted improvement-cost cache']].forEach(([archivePath, filePath, description]) => {
274
- const content = safeRead(filePath);
275
- if (content) entries.push({
276
- path: archivePath,
277
- description,
278
- content
279
- });
151
+ if (!stat.isFile() || stat.isSymbolicLink()) return entries;
152
+ return entries.concat({
153
+ sourcePath: spec.sourcePath,
154
+ archivePath: spec.archivePath,
155
+ description: spec.description,
156
+ bestEffort: spec.bestEffort,
157
+ bytes: stat.size
280
158
  });
281
- return entries;
282
- };
283
-
284
- const normalizeEntries = entries => entries.map(entry => _extends({}, entry, {
285
- content: Buffer.isBuffer(entry.content) ? entry.content : Buffer.from(String(entry.content), 'utf8')
286
- }));
159
+ }, []);
287
160
 
288
- const previewDiagnosticBundleContents = ({
289
- config,
290
- inventory,
291
- hostWindow
292
- } = {}) => [{
161
+ const previewDiagnosticBundleContents = () => [{
293
162
  path: 'manifest.json',
294
163
  description: 'Diagnostic bundle manifest',
295
164
  bytes: null
296
- }, ...normalizeEntries([...virtualEntries({
297
- config,
298
- inventory,
299
- hostWindow
300
- }), ...optionalFileEntries()]).map(entry => ({
301
- path: entry.path,
165
+ }, ...enumerateSourceFiles().map(entry => ({
166
+ path: entry.archivePath,
302
167
  description: entry.description,
303
- bytes: entry.content.length
168
+ bytes: entry.bytes
304
169
  }))];
305
170
 
306
171
  exports.previewDiagnosticBundleContents = previewDiagnosticBundleContents;
@@ -395,43 +260,193 @@ const zipBuffer = entries => {
395
260
  return Buffer.concat([...localParts, centralDirectory, end]);
396
261
  };
397
262
 
398
- const buildDiagnosticBundle = ({
399
- config,
400
- inventory,
401
- hostWindow
402
- } = {}) => {
403
- const baseEntries = normalizeEntries([...virtualEntries({
404
- config,
405
- inventory,
406
- hostWindow
407
- }), ...optionalFileEntries()]);
408
- const manifest = {
409
- format: DIAGNOSTIC_BUNDLE_FORMAT,
410
- schemaVersion: DIAGNOSTIC_BUNDLE_SCHEMA_VERSION,
411
- generatedAt: new Date().toISOString(),
412
- pluginVersion: _pluginMeta.PLUGIN_VERSION,
413
- releaseChannel: _pluginMeta.RELEASE_CHANNEL,
414
- notice: 'Contains inventory and plugin configuration for troubleshooting. Review before sharing.',
415
- files: baseEntries.map(entry => ({
416
- path: entry.path,
417
- description: entry.description,
418
- bytes: entry.content.length,
419
- sha256: sha256(entry.content)
420
- }))
263
+ const diagnosticTempRoot = () => _path.default.join((0, _personalProfileStorage.getPluginDataDirectory)(), 'temp', 'diagnostics');
264
+
265
+ const cleanupStaleDiagnosticTemp = () => {
266
+ const root = diagnosticTempRoot();
267
+ if (!_fs.default.existsSync(root)) return;
268
+ const now = Date.now();
269
+ let names;
270
+
271
+ try {
272
+ names = _fs.default.readdirSync(root);
273
+ } catch (error) {
274
+ return;
275
+ }
276
+
277
+ names.forEach(name => {
278
+ const target = _path.default.join(root, name);
279
+
280
+ try {
281
+ const stat = _fs.default.lstatSync(target);
282
+
283
+ if (stat.isDirectory() && now - stat.mtimeMs > DIAGNOSTIC_TEMP_MAX_AGE_MS) {
284
+ _fs.default.rmSync(target, {
285
+ recursive: true,
286
+ force: true
287
+ });
288
+ }
289
+ } catch (error) {// Stale temp cleanup is best effort and must never block diagnostics.
290
+ }
291
+ });
292
+ };
293
+
294
+ const createDiagnosticStage = () => {
295
+ cleanupStaleDiagnosticTemp();
296
+ const root = diagnosticTempRoot();
297
+
298
+ _fs.default.mkdirSync(root, {
299
+ recursive: true
300
+ });
301
+
302
+ const session = `${Date.now()}-${_crypto.default.randomBytes(6).toString('hex')}`;
303
+
304
+ const stage = _path.default.join(root, session);
305
+
306
+ _fs.default.mkdirSync(stage, {
307
+ recursive: true
308
+ });
309
+
310
+ return stage;
311
+ };
312
+
313
+ const copyFileWithRetry = async entry => {
314
+ let lastError = null;
315
+
316
+ for (let attempt = 0; attempt < DIAGNOSTIC_COPY_RETRY_DELAYS_MS.length; attempt += 1) {
317
+ const delay = DIAGNOSTIC_COPY_RETRY_DELAYS_MS[attempt];
318
+ if (delay > 0) await wait(delay);
319
+
320
+ try {
321
+ await _fs.default.promises.copyFile(entry.sourcePath, entry.stagePath);
322
+ return {
323
+ copied: true,
324
+ attempts: attempt + 1
325
+ };
326
+ } catch (error) {
327
+ lastError = error;
328
+ }
329
+ }
330
+
331
+ return {
332
+ copied: false,
333
+ attempts: DIAGNOSTIC_COPY_RETRY_DELAYS_MS.length,
334
+ error: lastError
335
+ };
336
+ };
337
+
338
+ const snapshotDiagnosticSources = async stage => {
339
+ const sources = diagnosticSourceSpecs();
340
+ const candidates = enumerateSourceFiles(sources);
341
+ const copied = [];
342
+ const failures = [];
343
+
344
+ for (const entry of candidates) {
345
+ const stagePath = _path.default.join(stage, ...entry.archivePath.split('/'));
346
+
347
+ _fs.default.mkdirSync(_path.default.dirname(stagePath), {
348
+ recursive: true
349
+ });
350
+
351
+ const result = await copyFileWithRetry(_extends({}, entry, {
352
+ stagePath
353
+ }));
354
+
355
+ if (result.copied) {
356
+ copied.push(_extends({}, entry, {
357
+ stagePath,
358
+ attempts: result.attempts
359
+ }));
360
+ continue;
361
+ }
362
+
363
+ const failure = {
364
+ path: entry.archivePath,
365
+ sourcePath: portablePath(entry.sourcePath),
366
+ errorCode: result.error && result.error.code ? result.error.code : 'UNKNOWN',
367
+ attempts: result.attempts
368
+ };
369
+ failures.push(failure);
370
+
371
+ if (!entry.bestEffort) {
372
+ const error = new Error('plugin-diagnostic-snapshot-copy-failed');
373
+ error.cause = result.error;
374
+ error.diagnosticFailure = failure;
375
+ throw error;
376
+ }
377
+ }
378
+
379
+ return {
380
+ sources,
381
+ copied,
382
+ failures
421
383
  };
422
- const entries = normalizeEntries([{
423
- path: 'manifest.json',
424
- description: 'Diagnostic bundle manifest',
425
- content: json(manifest)
426
- }, ...baseEntries]);
384
+ };
385
+
386
+ const stagedEntries = copied => copied.map(entry => {
387
+ const content = _fs.default.readFileSync(entry.stagePath);
388
+
427
389
  return {
428
- buffer: zipBuffer(entries),
429
- files: entries.map(entry => ({
430
- path: entry.path,
431
- description: entry.description,
432
- bytes: entry.content.length
433
- }))
390
+ path: entry.archivePath,
391
+ description: entry.description,
392
+ sourcePath: entry.sourcePath,
393
+ content,
394
+ attempts: entry.attempts
434
395
  };
396
+ });
397
+
398
+ const buildDiagnosticBundle = async () => {
399
+ const stage = createDiagnosticStage();
400
+
401
+ try {
402
+ const snapshot = await snapshotDiagnosticSources(stage);
403
+ const baseEntries = stagedEntries(snapshot.copied);
404
+ const manifest = {
405
+ format: DIAGNOSTIC_BUNDLE_FORMAT,
406
+ schemaVersion: DIAGNOSTIC_BUNDLE_SCHEMA_VERSION,
407
+ generatedAt: new Date().toISOString(),
408
+ pluginVersion: _pluginMeta.PLUGIN_VERSION,
409
+ releaseChannel: _pluginMeta.RELEASE_CHANNEL,
410
+ snapshotStatus: snapshot.failures.length > 0 ? 'partial' : 'complete',
411
+ notice: 'Contains whitelisted plugin diagnostic data and a best-effort Poi Local Storage snapshot used to reproduce personal inventory state.',
412
+ sources: snapshot.sources.map(source => ({
413
+ kind: source.kind,
414
+ sourcePath: portablePath(source.sourcePath),
415
+ archiveRoot: source.archiveRoot || null,
416
+ archivePath: source.archivePath || null,
417
+ exists: _fs.default.existsSync(source.sourcePath),
418
+ bestEffort: source.bestEffort
419
+ })),
420
+ files: baseEntries.map(entry => ({
421
+ path: entry.path,
422
+ description: entry.description,
423
+ sourcePath: portablePath(entry.sourcePath),
424
+ bytes: entry.content.length,
425
+ sha256: sha256(entry.content),
426
+ copyAttempts: entry.attempts
427
+ })),
428
+ failures: snapshot.failures
429
+ };
430
+ const entries = [{
431
+ path: 'manifest.json',
432
+ description: 'Diagnostic bundle manifest',
433
+ content: Buffer.from(json(manifest), 'utf8')
434
+ }, ...baseEntries];
435
+ return {
436
+ buffer: zipBuffer(entries),
437
+ partial: snapshot.failures.length > 0,
438
+ files: entries.map(entry => ({
439
+ path: entry.path,
440
+ description: entry.description,
441
+ bytes: entry.content.length
442
+ }))
443
+ };
444
+ } finally {
445
+ _fs.default.rmSync(stage, {
446
+ recursive: true,
447
+ force: true
448
+ });
449
+ }
435
450
  };
436
451
 
437
452
  exports.buildDiagnosticBundle = buildDiagnosticBundle;