@trops/dash-core 0.1.132 → 0.1.133
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/dist/electron/index.js +1485 -1345
- package/dist/electron/index.js.map +1 -1
- package/dist/index.esm.js +30 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -1289,7 +1289,7 @@ const { getFileContents: getFileContents$6 } = file;
|
|
|
1289
1289
|
const configFilename$4 = "themes.json";
|
|
1290
1290
|
const appName$6 = "Dashboard";
|
|
1291
1291
|
|
|
1292
|
-
const themeController$
|
|
1292
|
+
const themeController$3 = {
|
|
1293
1293
|
/**
|
|
1294
1294
|
* saveTheme
|
|
1295
1295
|
* Create a new Theme that can be used in the application
|
|
@@ -1420,7 +1420,7 @@ const themeController$2 = {
|
|
|
1420
1420
|
},
|
|
1421
1421
|
};
|
|
1422
1422
|
|
|
1423
|
-
var themeController_1 = themeController$
|
|
1423
|
+
var themeController_1 = themeController$3;
|
|
1424
1424
|
|
|
1425
1425
|
/**
|
|
1426
1426
|
* Utils/tranaform
|
|
@@ -7991,6 +7991,25 @@ var properties = {
|
|
|
7991
7991
|
description: "Originating app package name (from package.json). Informational — indicates which app this dashboard was built for.",
|
|
7992
7992
|
maxLength: 200
|
|
7993
7993
|
},
|
|
7994
|
+
theme: {
|
|
7995
|
+
type: "object",
|
|
7996
|
+
description: "Optional bundled theme for the dashboard",
|
|
7997
|
+
properties: {
|
|
7998
|
+
key: {
|
|
7999
|
+
type: "string",
|
|
8000
|
+
description: "Theme key in the app"
|
|
8001
|
+
},
|
|
8002
|
+
data: {
|
|
8003
|
+
type: "object",
|
|
8004
|
+
description: "Full raw theme source object"
|
|
8005
|
+
},
|
|
8006
|
+
registryPackage: {
|
|
8007
|
+
type: "string",
|
|
8008
|
+
description: "Scoped registry package name (e.g. 'username/ocean-depth')"
|
|
8009
|
+
}
|
|
8010
|
+
},
|
|
8011
|
+
additionalProperties: false
|
|
8012
|
+
},
|
|
7994
8013
|
eventWiring: {
|
|
7995
8014
|
type: "array",
|
|
7996
8015
|
description: "Pre-configured event connections between widgets",
|
|
@@ -10702,765 +10721,435 @@ var registryApiController$2 = {
|
|
|
10702
10721
|
};
|
|
10703
10722
|
|
|
10704
10723
|
/**
|
|
10705
|
-
*
|
|
10706
|
-
*
|
|
10707
|
-
* Handles export and import of dashboard configuration files.
|
|
10708
|
-
* Runs in the Electron main process.
|
|
10709
|
-
*
|
|
10710
|
-
* Export: serializes a workspace into a .dashboard.json config,
|
|
10711
|
-
* resolving widget dependencies, extracting event wiring from
|
|
10712
|
-
* layout listeners, and aggregating provider requirements.
|
|
10724
|
+
* themeRegistryController.js
|
|
10713
10725
|
*
|
|
10714
|
-
*
|
|
10715
|
-
*
|
|
10716
|
-
*
|
|
10726
|
+
* Handles publishing themes to and installing themes from the Dash registry.
|
|
10727
|
+
* Mirrors dashboardConfigController patterns for ZIP creation, manifest generation,
|
|
10728
|
+
* and registry interaction.
|
|
10717
10729
|
*/
|
|
10718
|
-
|
|
10719
|
-
const { app: app$2, dialog: dialog$1 } = require$$0$1;
|
|
10720
10730
|
const path$2 = require$$1$1;
|
|
10731
|
+
const { app: app$2, dialog: dialog$1 } = require$$0$1;
|
|
10721
10732
|
const AdmZip$1 = require$$3$3;
|
|
10722
|
-
const { getFileContents: getFileContents$1 } = file;
|
|
10723
|
-
const {
|
|
10724
|
-
validateDashboardConfig,
|
|
10725
|
-
applyDefaults,
|
|
10726
|
-
CURRENT_SCHEMA_VERSION,
|
|
10727
|
-
} = dashboardConfigValidator$1;
|
|
10728
|
-
const {
|
|
10729
|
-
collectComponentNames,
|
|
10730
|
-
extractEventWiring,
|
|
10731
|
-
buildWidgetDependencies,
|
|
10732
|
-
buildProviderRequirements,
|
|
10733
|
-
applyEventWiringToLayout,
|
|
10734
|
-
} = dashboardConfigUtils$1;
|
|
10735
|
-
const { searchRegistry, getPackage } = registryController$2;
|
|
10736
10733
|
|
|
10737
|
-
const
|
|
10738
|
-
const
|
|
10734
|
+
const themeController$2 = themeController_1;
|
|
10735
|
+
const registryController$1 = registryController$2;
|
|
10736
|
+
const registryApiController$1 = registryApiController$2;
|
|
10737
|
+
const { getAuthStatus } = registryAuthController$1;
|
|
10739
10738
|
|
|
10740
10739
|
/**
|
|
10741
|
-
*
|
|
10740
|
+
* Sanitize a name for use as a filename (lowercase, hyphens only).
|
|
10741
|
+
*/
|
|
10742
|
+
function sanitizeName(name) {
|
|
10743
|
+
return (name || "theme")
|
|
10744
|
+
.toLowerCase()
|
|
10745
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10746
|
+
.replace(/^-|-$/g, "");
|
|
10747
|
+
}
|
|
10748
|
+
|
|
10749
|
+
/**
|
|
10750
|
+
* Generate a registry manifest for a theme package.
|
|
10742
10751
|
*
|
|
10743
|
-
* @param {
|
|
10752
|
+
* @param {Object} themeData - The raw theme object
|
|
10753
|
+
* @param {string} themeKey - The theme key/name
|
|
10754
|
+
* @param {Object} options - Publish options { authorName, description, tags, scope }
|
|
10755
|
+
* @returns {Object} Registry manifest
|
|
10756
|
+
*/
|
|
10757
|
+
function generateThemeRegistryManifest(themeData, themeKey, options = {}) {
|
|
10758
|
+
const sanitizedName = sanitizeName(themeKey);
|
|
10759
|
+
const colors = extractColors(themeData);
|
|
10760
|
+
|
|
10761
|
+
return {
|
|
10762
|
+
scope: options.scope || "",
|
|
10763
|
+
name: sanitizedName,
|
|
10764
|
+
displayName: themeKey,
|
|
10765
|
+
author: options.authorName || "",
|
|
10766
|
+
description: options.description || "",
|
|
10767
|
+
version: "1.0.0",
|
|
10768
|
+
type: "theme",
|
|
10769
|
+
category: "general",
|
|
10770
|
+
tags: options.tags || [],
|
|
10771
|
+
icon: "palette",
|
|
10772
|
+
colors,
|
|
10773
|
+
appOrigin: options.appOrigin || "",
|
|
10774
|
+
publishedAt: new Date().toISOString(),
|
|
10775
|
+
};
|
|
10776
|
+
}
|
|
10777
|
+
|
|
10778
|
+
/**
|
|
10779
|
+
* Extract primary/secondary/tertiary/neutral colors from a theme object.
|
|
10780
|
+
* Theme objects store colors in various structures; this normalizes them.
|
|
10781
|
+
*/
|
|
10782
|
+
function extractColors(themeData) {
|
|
10783
|
+
const colors = {
|
|
10784
|
+
primary: "",
|
|
10785
|
+
secondary: "",
|
|
10786
|
+
tertiary: "",
|
|
10787
|
+
neutral: "",
|
|
10788
|
+
};
|
|
10789
|
+
|
|
10790
|
+
if (!themeData) return colors;
|
|
10791
|
+
|
|
10792
|
+
// Direct color fields
|
|
10793
|
+
if (themeData.primary) colors.primary = themeData.primary;
|
|
10794
|
+
if (themeData.secondary) colors.secondary = themeData.secondary;
|
|
10795
|
+
if (themeData.tertiary) colors.tertiary = themeData.tertiary;
|
|
10796
|
+
if (themeData.neutral) colors.neutral = themeData.neutral;
|
|
10797
|
+
|
|
10798
|
+
// Nested under "colors" key
|
|
10799
|
+
if (themeData.colors) {
|
|
10800
|
+
if (themeData.colors.primary) colors.primary = themeData.colors.primary;
|
|
10801
|
+
if (themeData.colors.secondary)
|
|
10802
|
+
colors.secondary = themeData.colors.secondary;
|
|
10803
|
+
if (themeData.colors.tertiary) colors.tertiary = themeData.colors.tertiary;
|
|
10804
|
+
if (themeData.colors.neutral) colors.neutral = themeData.colors.neutral;
|
|
10805
|
+
}
|
|
10806
|
+
|
|
10807
|
+
return colors;
|
|
10808
|
+
}
|
|
10809
|
+
|
|
10810
|
+
/**
|
|
10811
|
+
* Prepare a theme for publishing to the registry.
|
|
10812
|
+
*
|
|
10813
|
+
* Reads the theme from themes.json, generates a manifest, creates a ZIP,
|
|
10814
|
+
* and publishes via the registry API.
|
|
10815
|
+
*
|
|
10816
|
+
* @param {BrowserWindow} win - The sender window
|
|
10744
10817
|
* @param {string} appId - Application identifier
|
|
10745
|
-
* @param {
|
|
10746
|
-
* @param {Object} options -
|
|
10747
|
-
* @
|
|
10748
|
-
* @param {string} options.authorId - Dashboard author ID
|
|
10749
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
10750
|
-
* @returns {Promise<Object>} Result with success flag and file path
|
|
10818
|
+
* @param {string} themeKey - Key of the theme to publish
|
|
10819
|
+
* @param {Object} options - { authorName, description, tags }
|
|
10820
|
+
* @returns {Object} Result with success, manifest, registryResult
|
|
10751
10821
|
*/
|
|
10752
|
-
async function
|
|
10753
|
-
win,
|
|
10754
|
-
appId,
|
|
10755
|
-
workspaceId,
|
|
10756
|
-
options = {},
|
|
10757
|
-
widgetRegistry = null,
|
|
10758
|
-
) {
|
|
10822
|
+
async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
|
|
10759
10823
|
try {
|
|
10760
|
-
//
|
|
10761
|
-
const
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
configFilename,
|
|
10766
|
-
);
|
|
10767
|
-
const workspacesArray = getFileContents$1(filename);
|
|
10768
|
-
const workspace = workspacesArray.find(
|
|
10769
|
-
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
10770
|
-
);
|
|
10824
|
+
// Read the theme data
|
|
10825
|
+
const themesResult = themeController$2.listThemesForApplication(win, appId);
|
|
10826
|
+
if (themesResult.error) {
|
|
10827
|
+
return { success: false, error: "Failed to read themes: " + themesResult.message };
|
|
10828
|
+
}
|
|
10771
10829
|
|
|
10772
|
-
|
|
10830
|
+
const themeData = themesResult.themes[themeKey];
|
|
10831
|
+
if (!themeData) {
|
|
10832
|
+
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
10833
|
+
}
|
|
10834
|
+
|
|
10835
|
+
// Get auth status for scope
|
|
10836
|
+
const auth = getAuthStatus();
|
|
10837
|
+
const scope = auth.profile?.username || options.scope || "";
|
|
10838
|
+
if (!scope) {
|
|
10773
10839
|
return {
|
|
10774
10840
|
success: false,
|
|
10775
|
-
error:
|
|
10841
|
+
error: "Not authenticated with registry",
|
|
10842
|
+
authRequired: true,
|
|
10776
10843
|
};
|
|
10777
10844
|
}
|
|
10778
10845
|
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
10785
|
-
const providers = buildProviderRequirements(componentNames, widgetRegistry);
|
|
10786
|
-
|
|
10787
|
-
// 3. Build the dashboard config
|
|
10788
|
-
const dashboardConfig = applyDefaults({
|
|
10789
|
-
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
10790
|
-
name: workspace.name || workspace.label || "Exported Dashboard",
|
|
10791
|
-
description: options.description || "",
|
|
10792
|
-
...(options.authorName
|
|
10793
|
-
? { author: { name: options.authorName, id: options.authorId || "" } }
|
|
10794
|
-
: {}),
|
|
10795
|
-
shareable: true,
|
|
10796
|
-
tags: options.tags || [],
|
|
10797
|
-
icon: options.icon || "grip",
|
|
10798
|
-
workspace: {
|
|
10799
|
-
id: workspace.id,
|
|
10800
|
-
name: workspace.name,
|
|
10801
|
-
type: workspace.type || "workspace",
|
|
10802
|
-
label: workspace.label || workspace.name,
|
|
10803
|
-
version: workspace.version || 1,
|
|
10804
|
-
layout,
|
|
10805
|
-
menuId: workspace.menuId || 1,
|
|
10806
|
-
},
|
|
10807
|
-
widgets,
|
|
10808
|
-
providers,
|
|
10809
|
-
eventWiring,
|
|
10846
|
+
// Generate manifest
|
|
10847
|
+
const manifest = generateThemeRegistryManifest(themeData, themeKey, {
|
|
10848
|
+
...options,
|
|
10849
|
+
scope,
|
|
10850
|
+
appOrigin: appId,
|
|
10810
10851
|
});
|
|
10811
10852
|
|
|
10812
|
-
//
|
|
10813
|
-
|
|
10814
|
-
if (!validation.valid) {
|
|
10853
|
+
// Validate colors
|
|
10854
|
+
if (!manifest.colors.primary || !manifest.colors.secondary || !manifest.colors.tertiary) {
|
|
10815
10855
|
return {
|
|
10816
10856
|
success: false,
|
|
10817
|
-
error:
|
|
10857
|
+
error: "Theme must have primary, secondary, and tertiary colors defined",
|
|
10818
10858
|
};
|
|
10819
10859
|
}
|
|
10820
10860
|
|
|
10821
|
-
//
|
|
10822
|
-
const sanitizedName = (
|
|
10823
|
-
|
|
10824
|
-
.replace(/\s+/g, "-")
|
|
10825
|
-
.toLowerCase();
|
|
10861
|
+
// Show save dialog
|
|
10862
|
+
const sanitizedName = sanitizeName(themeKey);
|
|
10863
|
+
const defaultFilename = `theme-${sanitizedName}-v${manifest.version}.zip`;
|
|
10826
10864
|
|
|
10827
|
-
const
|
|
10828
|
-
title: "
|
|
10829
|
-
defaultPath:
|
|
10830
|
-
|
|
10831
|
-
`dashboard-${sanitizedName}.zip`,
|
|
10832
|
-
),
|
|
10833
|
-
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
10865
|
+
const saveResult = await dialog$1.showSaveDialog(win, {
|
|
10866
|
+
title: "Save Theme Package",
|
|
10867
|
+
defaultPath: defaultFilename,
|
|
10868
|
+
filters: [{ name: "ZIP Files", extensions: ["zip"] }],
|
|
10834
10869
|
});
|
|
10835
10870
|
|
|
10836
|
-
if (canceled || !filePath) {
|
|
10837
|
-
return { success: false,
|
|
10871
|
+
if (saveResult.canceled || !saveResult.filePath) {
|
|
10872
|
+
return { success: false, error: "Save canceled" };
|
|
10838
10873
|
}
|
|
10839
10874
|
|
|
10840
|
-
|
|
10875
|
+
const filePath = saveResult.filePath;
|
|
10876
|
+
|
|
10877
|
+
// Create ZIP with manifest.json + {name}.theme.json
|
|
10841
10878
|
const zip = new AdmZip$1();
|
|
10842
|
-
const configJson = JSON.stringify(dashboardConfig, null, 2);
|
|
10843
10879
|
zip.addFile(
|
|
10844
|
-
|
|
10845
|
-
Buffer.from(
|
|
10880
|
+
"manifest.json",
|
|
10881
|
+
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
10882
|
+
);
|
|
10883
|
+
zip.addFile(
|
|
10884
|
+
`${sanitizedName}.theme.json`,
|
|
10885
|
+
Buffer.from(JSON.stringify(themeData, null, 2), "utf-8"),
|
|
10846
10886
|
);
|
|
10847
|
-
|
|
10848
10887
|
zip.writeZip(filePath);
|
|
10849
10888
|
|
|
10850
|
-
console.log(
|
|
10851
|
-
|
|
10852
|
-
|
|
10889
|
+
console.log("[ThemeRegistryController] ZIP created at:", filePath);
|
|
10890
|
+
|
|
10891
|
+
// Attempt to publish to registry
|
|
10892
|
+
let registryResult = null;
|
|
10893
|
+
if (auth.authenticated) {
|
|
10894
|
+
registryResult = await registryApiController$1.publishToRegistry(
|
|
10895
|
+
filePath,
|
|
10896
|
+
manifest,
|
|
10897
|
+
);
|
|
10898
|
+
console.log("[ThemeRegistryController] Registry publish result:", registryResult);
|
|
10899
|
+
}
|
|
10853
10900
|
|
|
10854
10901
|
return {
|
|
10855
10902
|
success: true,
|
|
10903
|
+
manifest,
|
|
10856
10904
|
filePath,
|
|
10857
|
-
|
|
10858
|
-
};
|
|
10859
|
-
} catch (error) {
|
|
10860
|
-
console.error(
|
|
10861
|
-
"[DashboardConfigController] Error exporting dashboard:",
|
|
10862
|
-
error,
|
|
10863
|
-
);
|
|
10864
|
-
return {
|
|
10865
|
-
success: false,
|
|
10866
|
-
error: error.message,
|
|
10905
|
+
registryResult,
|
|
10867
10906
|
};
|
|
10907
|
+
} catch (err) {
|
|
10908
|
+
console.error("[ThemeRegistryController] Error preparing theme for publish:", err);
|
|
10909
|
+
return { success: false, error: err.message };
|
|
10868
10910
|
}
|
|
10869
10911
|
}
|
|
10870
10912
|
|
|
10871
10913
|
/**
|
|
10872
|
-
*
|
|
10873
|
-
* Opens the file picker, extracts and validates the .dashboard.json,
|
|
10874
|
-
* and returns a preview of the config + the file path for later import.
|
|
10914
|
+
* Install a theme from the registry.
|
|
10875
10915
|
*
|
|
10876
|
-
*
|
|
10877
|
-
*
|
|
10916
|
+
* Looks up the theme package, downloads the ZIP, extracts the .theme.json,
|
|
10917
|
+
* and saves it via themeController.
|
|
10918
|
+
*
|
|
10919
|
+
* @param {BrowserWindow} win - The sender window
|
|
10920
|
+
* @param {string} appId - Application identifier
|
|
10921
|
+
* @param {string} packageName - Registry package name (e.g., "username/ocean-depth")
|
|
10922
|
+
* @returns {Object} Result with success, themeKey, theme
|
|
10878
10923
|
*/
|
|
10879
|
-
async function
|
|
10924
|
+
async function installThemeFromRegistry$1(win, appId, packageName) {
|
|
10880
10925
|
try {
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
});
|
|
10886
|
-
|
|
10887
|
-
if (canceled || !filePaths || !filePaths.length) {
|
|
10888
|
-
return { success: false, canceled: true };
|
|
10926
|
+
// Look up the package
|
|
10927
|
+
const pkg = await registryController$1.getPackage(packageName);
|
|
10928
|
+
if (!pkg) {
|
|
10929
|
+
return { success: false, error: `Theme package "${packageName}" not found in registry` };
|
|
10889
10930
|
}
|
|
10890
10931
|
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
const { validateZipEntries } = widgetRegistryExports;
|
|
10897
|
-
validateZipEntries(zip, tempDir);
|
|
10932
|
+
// Resolve download URL
|
|
10933
|
+
let downloadUrl = pkg.downloadUrl;
|
|
10934
|
+
if (!downloadUrl) {
|
|
10935
|
+
return { success: false, error: "Package has no download URL" };
|
|
10936
|
+
}
|
|
10898
10937
|
|
|
10899
|
-
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10938
|
+
// Resolve template variables
|
|
10939
|
+
downloadUrl = downloadUrl
|
|
10940
|
+
.replace("{version}", pkg.version || "1.0.0")
|
|
10941
|
+
.replace("{name}", pkg.name || "");
|
|
10903
10942
|
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
error: "No .dashboard.json file found in ZIP archive",
|
|
10908
|
-
};
|
|
10943
|
+
// Enforce HTTPS
|
|
10944
|
+
if (!downloadUrl.startsWith("https://")) {
|
|
10945
|
+
return { success: false, error: "Download URL must use HTTPS" };
|
|
10909
10946
|
}
|
|
10910
10947
|
|
|
10911
|
-
|
|
10912
|
-
let dashboardConfig;
|
|
10913
|
-
try {
|
|
10914
|
-
dashboardConfig = JSON.parse(configJson);
|
|
10915
|
-
} catch (parseError) {
|
|
10916
|
-
return {
|
|
10917
|
-
success: false,
|
|
10918
|
-
error: `Invalid JSON: ${parseError.message}`,
|
|
10919
|
-
};
|
|
10920
|
-
}
|
|
10948
|
+
console.log("[ThemeRegistryController] Downloading theme from:", downloadUrl);
|
|
10921
10949
|
|
|
10922
|
-
|
|
10923
|
-
|
|
10950
|
+
// Download the ZIP
|
|
10951
|
+
const response = await fetch(downloadUrl);
|
|
10952
|
+
if (!response.ok) {
|
|
10924
10953
|
return {
|
|
10925
10954
|
success: false,
|
|
10926
|
-
error: `
|
|
10955
|
+
error: `Failed to download theme: ${response.status} ${response.statusText}`,
|
|
10927
10956
|
};
|
|
10928
10957
|
}
|
|
10929
10958
|
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
return {
|
|
10933
|
-
success: true,
|
|
10934
|
-
filePath: zipPath,
|
|
10935
|
-
dashboardConfig: {
|
|
10936
|
-
name: dashboardConfig.name,
|
|
10937
|
-
description: dashboardConfig.description,
|
|
10938
|
-
author: dashboardConfig.author,
|
|
10939
|
-
workspace: dashboardConfig.workspace,
|
|
10940
|
-
widgets: dashboardConfig.widgets || [],
|
|
10941
|
-
providers: dashboardConfig.providers || [],
|
|
10942
|
-
},
|
|
10943
|
-
};
|
|
10944
|
-
} catch (error) {
|
|
10945
|
-
console.error(
|
|
10946
|
-
"[DashboardConfigController] Error selecting dashboard file:",
|
|
10947
|
-
error,
|
|
10948
|
-
);
|
|
10949
|
-
return { success: false, error: error.message };
|
|
10950
|
-
}
|
|
10951
|
-
}
|
|
10952
|
-
|
|
10953
|
-
/**
|
|
10954
|
-
* Import a dashboard from a ZIP file containing a .dashboard.json config.
|
|
10955
|
-
*
|
|
10956
|
-
* Steps:
|
|
10957
|
-
* 1. Show native file picker for .zip selection (or use options.filePath)
|
|
10958
|
-
* 2. Extract and validate .dashboard.json
|
|
10959
|
-
* 3. Auto-install missing widgets from registry
|
|
10960
|
-
* 4. Create workspace in workspaces.json
|
|
10961
|
-
* 5. Apply event wiring to layout
|
|
10962
|
-
* 6. Mark imported dashboard shareable: false
|
|
10963
|
-
*
|
|
10964
|
-
* @param {BrowserWindow} win - The main window (for dialog)
|
|
10965
|
-
* @param {string} appId - Application identifier
|
|
10966
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets(), downloadWidget())
|
|
10967
|
-
* @param {Object} options - Import options
|
|
10968
|
-
* @param {string} options.filePath - Skip file picker, use this path directly
|
|
10969
|
-
* @param {string} options.name - Override workspace name
|
|
10970
|
-
* @param {number} options.menuId - Override workspace menuId (folder)
|
|
10971
|
-
* @param {string} options.themeKey - Override workspace themeKey
|
|
10972
|
-
* @returns {Promise<Object>} Result with success, workspace, and import summary
|
|
10973
|
-
*/
|
|
10974
|
-
async function importDashboardConfig$1(
|
|
10975
|
-
win,
|
|
10976
|
-
appId,
|
|
10977
|
-
widgetRegistry = null,
|
|
10978
|
-
options = {},
|
|
10979
|
-
) {
|
|
10980
|
-
try {
|
|
10981
|
-
let zipPath;
|
|
10959
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
10960
|
+
const zipBuffer = Buffer.from(arrayBuffer);
|
|
10982
10961
|
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
} else {
|
|
10987
|
-
// Show file picker
|
|
10988
|
-
const { canceled, filePaths } = await dialog$1.showOpenDialog(win, {
|
|
10989
|
-
title: "Import Dashboard Configuration",
|
|
10990
|
-
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
10991
|
-
properties: ["openFile"],
|
|
10992
|
-
});
|
|
10962
|
+
// Extract .theme.json from ZIP
|
|
10963
|
+
const zip = new AdmZip$1(zipBuffer);
|
|
10964
|
+
const entries = zip.getEntries();
|
|
10993
10965
|
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10966
|
+
const themeEntry = entries.find((entry) =>
|
|
10967
|
+
entry.entryName.endsWith(".theme.json"),
|
|
10968
|
+
);
|
|
10997
10969
|
|
|
10998
|
-
|
|
10970
|
+
if (!themeEntry) {
|
|
10971
|
+
return { success: false, error: "ZIP does not contain a .theme.json file" };
|
|
10999
10972
|
}
|
|
11000
10973
|
|
|
11001
|
-
//
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
// Validate ZIP entries for path traversal
|
|
11005
|
-
const tempDir = path$2.join(app$2.getPath("temp"), "dash-import");
|
|
11006
|
-
const { validateZipEntries } = widgetRegistryExports;
|
|
11007
|
-
validateZipEntries(zip, tempDir);
|
|
11008
|
-
|
|
11009
|
-
// Find the .dashboard.json file
|
|
11010
|
-
const entries = zip.getEntries();
|
|
11011
|
-
const configEntry = entries.find((e) =>
|
|
11012
|
-
e.entryName.endsWith(".dashboard.json"),
|
|
11013
|
-
);
|
|
11014
|
-
|
|
11015
|
-
if (!configEntry) {
|
|
11016
|
-
return {
|
|
11017
|
-
success: false,
|
|
11018
|
-
error: "No .dashboard.json file found in ZIP archive",
|
|
11019
|
-
};
|
|
10974
|
+
// Validate entry path (security: prevent path traversal)
|
|
10975
|
+
if (themeEntry.entryName.includes("..") || path$2.isAbsolute(themeEntry.entryName)) {
|
|
10976
|
+
return { success: false, error: "Invalid file path in ZIP" };
|
|
11020
10977
|
}
|
|
11021
10978
|
|
|
11022
|
-
|
|
11023
|
-
|
|
10979
|
+
// Parse theme data
|
|
10980
|
+
const themeJson = themeEntry.getData().toString("utf-8");
|
|
10981
|
+
let themeData;
|
|
11024
10982
|
try {
|
|
11025
|
-
|
|
11026
|
-
} catch (
|
|
11027
|
-
return {
|
|
11028
|
-
success: false,
|
|
11029
|
-
error: `Invalid JSON in ${configEntry.entryName}: ${parseError.message}`,
|
|
11030
|
-
};
|
|
10983
|
+
themeData = JSON.parse(themeJson);
|
|
10984
|
+
} catch (parseErr) {
|
|
10985
|
+
return { success: false, error: "Invalid JSON in theme file: " + parseErr.message };
|
|
11031
10986
|
}
|
|
11032
10987
|
|
|
11033
|
-
//
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
};
|
|
11040
|
-
}
|
|
10988
|
+
// Add registry metadata
|
|
10989
|
+
themeData._registryMeta = {
|
|
10990
|
+
source: "registry",
|
|
10991
|
+
packageName,
|
|
10992
|
+
installedAt: new Date().toISOString(),
|
|
10993
|
+
};
|
|
11041
10994
|
|
|
11042
|
-
//
|
|
11043
|
-
|
|
10995
|
+
// Determine theme key from package display name or name
|
|
10996
|
+
const themeKey = pkg.displayName || pkg.name;
|
|
11044
10997
|
|
|
11045
|
-
//
|
|
11046
|
-
|
|
10998
|
+
// Save via themeController
|
|
10999
|
+
const saveResult = themeController$2.saveThemeForApplication(
|
|
11047
11000
|
win,
|
|
11048
11001
|
appId,
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
{
|
|
11052
|
-
name: options.name,
|
|
11053
|
-
menuId: options.menuId,
|
|
11054
|
-
themeKey: options.themeKey,
|
|
11055
|
-
},
|
|
11056
|
-
);
|
|
11057
|
-
} catch (error) {
|
|
11058
|
-
console.error(
|
|
11059
|
-
"[DashboardConfigController] Error importing dashboard:",
|
|
11060
|
-
error,
|
|
11002
|
+
themeKey,
|
|
11003
|
+
themeData,
|
|
11061
11004
|
);
|
|
11005
|
+
|
|
11006
|
+
if (saveResult.error) {
|
|
11007
|
+
return { success: false, error: "Failed to save theme: " + saveResult.message };
|
|
11008
|
+
}
|
|
11009
|
+
|
|
11010
|
+
console.log("[ThemeRegistryController] Theme installed:", themeKey);
|
|
11011
|
+
|
|
11062
11012
|
return {
|
|
11063
|
-
success:
|
|
11064
|
-
|
|
11013
|
+
success: true,
|
|
11014
|
+
themeKey,
|
|
11015
|
+
theme: themeData,
|
|
11016
|
+
themes: saveResult.themes,
|
|
11065
11017
|
};
|
|
11018
|
+
} catch (err) {
|
|
11019
|
+
console.error("[ThemeRegistryController] Error installing theme:", err);
|
|
11020
|
+
return { success: false, error: err.message };
|
|
11066
11021
|
}
|
|
11067
11022
|
}
|
|
11068
11023
|
|
|
11069
11024
|
/**
|
|
11070
|
-
*
|
|
11071
|
-
* Used by both importDashboardConfig (ZIP) and installDashboardFromRegistry.
|
|
11025
|
+
* Get a preview of theme data for the publish modal.
|
|
11072
11026
|
*
|
|
11073
|
-
* @param {BrowserWindow} win - The main window
|
|
11074
11027
|
* @param {string} appId - Application identifier
|
|
11075
|
-
* @param {
|
|
11076
|
-
* @
|
|
11077
|
-
* @param {Object} options - Additional options
|
|
11078
|
-
* @param {string} options.source - Source label ("zip" or "registry")
|
|
11079
|
-
* @returns {Promise<Object>} Result with success, workspace, and summary
|
|
11028
|
+
* @param {string} themeKey - Theme key
|
|
11029
|
+
* @returns {Object} Preview data with theme name, colors, etc.
|
|
11080
11030
|
*/
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
options = {},
|
|
11087
|
-
) {
|
|
11088
|
-
const source = options.source || "zip";
|
|
11089
|
-
|
|
11090
|
-
// 1. Auto-install missing widgets from registry
|
|
11091
|
-
const installSummary = {
|
|
11092
|
-
installed: [],
|
|
11093
|
-
alreadyInstalled: [],
|
|
11094
|
-
failed: [],
|
|
11095
|
-
};
|
|
11096
|
-
|
|
11097
|
-
if (
|
|
11098
|
-
widgetRegistry &&
|
|
11099
|
-
dashboardConfig.widgets &&
|
|
11100
|
-
dashboardConfig.widgets.length
|
|
11101
|
-
) {
|
|
11102
|
-
const installedWidgets = widgetRegistry.getWidgets();
|
|
11103
|
-
const installedPackages = new Set(installedWidgets.map((w) => w.name));
|
|
11104
|
-
|
|
11105
|
-
for (const widgetDep of dashboardConfig.widgets) {
|
|
11106
|
-
const packageName = widgetDep.package;
|
|
11107
|
-
|
|
11108
|
-
if (installedPackages.has(packageName)) {
|
|
11109
|
-
installSummary.alreadyInstalled.push(packageName);
|
|
11110
|
-
continue;
|
|
11111
|
-
}
|
|
11112
|
-
|
|
11113
|
-
// Try to find the widget in the registry and install it
|
|
11114
|
-
try {
|
|
11115
|
-
const registryPkg = await getPackage(packageName);
|
|
11116
|
-
if (registryPkg && registryPkg.downloadUrl) {
|
|
11117
|
-
await widgetRegistry.downloadWidget(
|
|
11118
|
-
packageName,
|
|
11119
|
-
registryPkg.downloadUrl,
|
|
11120
|
-
registryPkg.dashConfigUrl || null,
|
|
11121
|
-
);
|
|
11122
|
-
installSummary.installed.push(packageName);
|
|
11123
|
-
installedPackages.add(packageName);
|
|
11124
|
-
} else {
|
|
11125
|
-
installSummary.failed.push({
|
|
11126
|
-
package: packageName,
|
|
11127
|
-
reason: "Not found in registry",
|
|
11128
|
-
});
|
|
11129
|
-
}
|
|
11130
|
-
} catch (installError) {
|
|
11131
|
-
installSummary.failed.push({
|
|
11132
|
-
package: packageName,
|
|
11133
|
-
reason: installError.message,
|
|
11134
|
-
});
|
|
11135
|
-
}
|
|
11031
|
+
function getThemePublishPreview$1(appId, themeKey) {
|
|
11032
|
+
try {
|
|
11033
|
+
const themesResult = themeController$2.listThemesForApplication(null, appId);
|
|
11034
|
+
if (themesResult.error) {
|
|
11035
|
+
return { success: false, error: "Failed to read themes: " + themesResult.message };
|
|
11136
11036
|
}
|
|
11137
|
-
}
|
|
11138
|
-
|
|
11139
|
-
// 2. Build workspace from config
|
|
11140
|
-
const workspace = { ...dashboardConfig.workspace };
|
|
11141
|
-
|
|
11142
|
-
if (!workspace || !workspace.layout) {
|
|
11143
|
-
return {
|
|
11144
|
-
success: false,
|
|
11145
|
-
error: "Dashboard config has no workspace data",
|
|
11146
|
-
};
|
|
11147
|
-
}
|
|
11148
|
-
|
|
11149
|
-
// Generate a unique ID for the imported workspace
|
|
11150
|
-
workspace.id = Date.now();
|
|
11151
|
-
|
|
11152
|
-
// Apply name/menuId/themeKey overrides if provided
|
|
11153
|
-
if (options.name) workspace.name = options.name;
|
|
11154
|
-
if (options.menuId !== undefined) workspace.menuId = options.menuId;
|
|
11155
|
-
if (options.themeKey !== undefined) workspace.themeKey = options.themeKey;
|
|
11156
11037
|
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
dashboardConfig.eventWiring &&
|
|
11161
|
-
dashboardConfig.eventWiring.length &&
|
|
11162
|
-
workspace.layout
|
|
11163
|
-
) {
|
|
11164
|
-
applyEventWiringToLayout(workspace.layout, dashboardConfig.eventWiring);
|
|
11165
|
-
for (const wire of dashboardConfig.eventWiring) {
|
|
11166
|
-
eventWiringSummary.push(
|
|
11167
|
-
`${wire.source?.widget}.${wire.source?.event} → ${wire.target?.widget}.${wire.target?.handler}`,
|
|
11168
|
-
);
|
|
11038
|
+
const themeData = themesResult.themes[themeKey];
|
|
11039
|
+
if (!themeData) {
|
|
11040
|
+
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
11169
11041
|
}
|
|
11170
|
-
}
|
|
11171
|
-
|
|
11172
|
-
// 4. Mark as not shareable (imported dashboards cannot be re-published)
|
|
11173
|
-
workspace._dashboardConfig = {
|
|
11174
|
-
shareable: false,
|
|
11175
|
-
source,
|
|
11176
|
-
importedFrom: dashboardConfig.name,
|
|
11177
|
-
importedAt: new Date().toISOString(),
|
|
11178
|
-
originalAuthor: dashboardConfig.author,
|
|
11179
|
-
schemaVersion: dashboardConfig.schemaVersion,
|
|
11180
|
-
registryPackage: options.registryPackage || null,
|
|
11181
|
-
installedVersion: options.installedVersion || null,
|
|
11182
|
-
};
|
|
11183
11042
|
|
|
11184
|
-
|
|
11185
|
-
const workspaceController = workspaceController_1;
|
|
11186
|
-
const saveResult = workspaceController.saveWorkspaceForApplication(
|
|
11187
|
-
win,
|
|
11188
|
-
appId,
|
|
11189
|
-
workspace,
|
|
11190
|
-
);
|
|
11043
|
+
const colors = extractColors(themeData);
|
|
11191
11044
|
|
|
11192
|
-
if (saveResult.error) {
|
|
11193
11045
|
return {
|
|
11194
|
-
success:
|
|
11195
|
-
|
|
11046
|
+
success: true,
|
|
11047
|
+
themeName: themeKey,
|
|
11048
|
+
colors,
|
|
11049
|
+
hasRegistryMeta: !!themeData._registryMeta,
|
|
11196
11050
|
};
|
|
11051
|
+
} catch (err) {
|
|
11052
|
+
console.error("[ThemeRegistryController] Error getting preview:", err);
|
|
11053
|
+
return { success: false, error: err.message };
|
|
11197
11054
|
}
|
|
11055
|
+
}
|
|
11198
11056
|
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11057
|
+
var themeRegistryController$1 = {
|
|
11058
|
+
prepareThemeForPublish: prepareThemeForPublish$1,
|
|
11059
|
+
installThemeFromRegistry: installThemeFromRegistry$1,
|
|
11060
|
+
getThemePublishPreview: getThemePublishPreview$1,
|
|
11061
|
+
generateThemeRegistryManifest,
|
|
11062
|
+
extractColors,
|
|
11063
|
+
};
|
|
11206
11064
|
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11065
|
+
/**
|
|
11066
|
+
* dashboardConfigController.js
|
|
11067
|
+
*
|
|
11068
|
+
* Handles export and import of dashboard configuration files.
|
|
11069
|
+
* Runs in the Electron main process.
|
|
11070
|
+
*
|
|
11071
|
+
* Export: serializes a workspace into a .dashboard.json config,
|
|
11072
|
+
* resolving widget dependencies, extracting event wiring from
|
|
11073
|
+
* layout listeners, and aggregating provider requirements.
|
|
11074
|
+
*
|
|
11075
|
+
* Import: validates and processes a .dashboard.json config,
|
|
11076
|
+
* auto-installs missing widgets, creates workspace, and
|
|
11077
|
+
* applies event wiring. (Import is implemented in DASH-13.)
|
|
11078
|
+
*/
|
|
11210
11079
|
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11080
|
+
const { app: app$1, dialog } = require$$0$1;
|
|
11081
|
+
const path$1 = require$$1$1;
|
|
11082
|
+
const AdmZip = require$$3$3;
|
|
11083
|
+
const { getFileContents: getFileContents$1 } = file;
|
|
11084
|
+
const {
|
|
11085
|
+
validateDashboardConfig,
|
|
11086
|
+
applyDefaults,
|
|
11087
|
+
CURRENT_SCHEMA_VERSION,
|
|
11088
|
+
} = dashboardConfigValidator$1;
|
|
11089
|
+
const {
|
|
11090
|
+
collectComponentNames,
|
|
11091
|
+
extractEventWiring,
|
|
11092
|
+
buildWidgetDependencies,
|
|
11093
|
+
buildProviderRequirements,
|
|
11094
|
+
applyEventWiringToLayout,
|
|
11095
|
+
} = dashboardConfigUtils$1;
|
|
11096
|
+
const { searchRegistry, getPackage } = registryController$2;
|
|
11097
|
+
const themeController$1 = themeController_1;
|
|
11098
|
+
|
|
11099
|
+
const configFilename = "workspaces.json";
|
|
11100
|
+
const appName$1 = "Dashboard";
|
|
11224
11101
|
|
|
11225
11102
|
/**
|
|
11226
|
-
*
|
|
11227
|
-
*
|
|
11228
|
-
* Fetches the dashboard ZIP from the registry, extracts the .dashboard.json,
|
|
11229
|
-
* validates it, and delegates to the shared import pipeline.
|
|
11103
|
+
* Export a workspace as a .dashboard.json config inside a ZIP file.
|
|
11230
11104
|
*
|
|
11231
|
-
* @param {BrowserWindow} win - The main window
|
|
11105
|
+
* @param {BrowserWindow} win - The main window (for dialog)
|
|
11232
11106
|
* @param {string} appId - Application identifier
|
|
11233
|
-
* @param {string}
|
|
11234
|
-
* @param {Object}
|
|
11235
|
-
* @
|
|
11107
|
+
* @param {number|string} workspaceId - ID of the workspace to export
|
|
11108
|
+
* @param {Object} options - Export options
|
|
11109
|
+
* @param {string} options.authorName - Dashboard author name
|
|
11110
|
+
* @param {string} options.authorId - Dashboard author ID
|
|
11111
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
11112
|
+
* @returns {Promise<Object>} Result with success flag and file path
|
|
11236
11113
|
*/
|
|
11237
|
-
async function
|
|
11114
|
+
async function exportDashboardConfig$1(
|
|
11238
11115
|
win,
|
|
11239
11116
|
appId,
|
|
11240
|
-
|
|
11117
|
+
workspaceId,
|
|
11118
|
+
options = {},
|
|
11241
11119
|
widgetRegistry = null,
|
|
11242
11120
|
) {
|
|
11243
11121
|
try {
|
|
11244
|
-
// 1.
|
|
11245
|
-
const
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
return {
|
|
11255
|
-
success: false,
|
|
11256
|
-
error: `Dashboard package has no download URL: ${packageName}`,
|
|
11257
|
-
};
|
|
11258
|
-
}
|
|
11259
|
-
|
|
11260
|
-
// 2. Resolve the download URL and fetch the ZIP
|
|
11261
|
-
const version = registryPkg.version || "1.0.0";
|
|
11262
|
-
let downloadUrl = registryPkg.downloadUrl;
|
|
11263
|
-
downloadUrl = downloadUrl.replace("{version}", version);
|
|
11264
|
-
downloadUrl = downloadUrl.replace("{name}", packageName);
|
|
11265
|
-
|
|
11266
|
-
// Enforce HTTPS
|
|
11267
|
-
const parsedUrl = new URL(downloadUrl);
|
|
11268
|
-
if (parsedUrl.protocol !== "https:") {
|
|
11269
|
-
return {
|
|
11270
|
-
success: false,
|
|
11271
|
-
error: `Dashboard downloads must use HTTPS. Refusing: ${downloadUrl}`,
|
|
11272
|
-
};
|
|
11273
|
-
}
|
|
11274
|
-
|
|
11275
|
-
console.log(
|
|
11276
|
-
`[DashboardConfigController] Fetching dashboard from: ${downloadUrl}`,
|
|
11122
|
+
// 1. Read workspace from workspaces.json
|
|
11123
|
+
const filename = path$1.join(
|
|
11124
|
+
app$1.getPath("userData"),
|
|
11125
|
+
appName$1,
|
|
11126
|
+
appId,
|
|
11127
|
+
configFilename,
|
|
11128
|
+
);
|
|
11129
|
+
const workspacesArray = getFileContents$1(filename);
|
|
11130
|
+
const workspace = workspacesArray.find(
|
|
11131
|
+
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
11277
11132
|
);
|
|
11278
11133
|
|
|
11279
|
-
|
|
11280
|
-
if (!response.ok) {
|
|
11134
|
+
if (!workspace) {
|
|
11281
11135
|
return {
|
|
11282
11136
|
success: false,
|
|
11283
|
-
error: `
|
|
11137
|
+
error: `Workspace not found: ${workspaceId}`,
|
|
11284
11138
|
};
|
|
11285
11139
|
}
|
|
11286
11140
|
|
|
11287
|
-
const
|
|
11288
|
-
const zip = new AdmZip$1(Buffer.from(buffer));
|
|
11141
|
+
const layout = workspace.layout || [];
|
|
11289
11142
|
|
|
11290
|
-
//
|
|
11291
|
-
const tempDir = path$2.join(app$2.getPath("temp"), "dash-registry-import");
|
|
11292
|
-
const { validateZipEntries } = widgetRegistryExports;
|
|
11293
|
-
validateZipEntries(zip, tempDir);
|
|
11294
|
-
|
|
11295
|
-
// 4. Find and parse .dashboard.json
|
|
11296
|
-
const entries = zip.getEntries();
|
|
11297
|
-
const configEntry = entries.find((e) =>
|
|
11298
|
-
e.entryName.endsWith(".dashboard.json"),
|
|
11299
|
-
);
|
|
11300
|
-
|
|
11301
|
-
if (!configEntry) {
|
|
11302
|
-
return {
|
|
11303
|
-
success: false,
|
|
11304
|
-
error: "No .dashboard.json file found in downloaded archive",
|
|
11305
|
-
};
|
|
11306
|
-
}
|
|
11307
|
-
|
|
11308
|
-
const configJson = configEntry.getData().toString("utf-8");
|
|
11309
|
-
let dashboardConfig;
|
|
11310
|
-
try {
|
|
11311
|
-
dashboardConfig = JSON.parse(configJson);
|
|
11312
|
-
} catch (parseError) {
|
|
11313
|
-
return {
|
|
11314
|
-
success: false,
|
|
11315
|
-
error: `Invalid JSON in dashboard config: ${parseError.message}`,
|
|
11316
|
-
};
|
|
11317
|
-
}
|
|
11318
|
-
|
|
11319
|
-
// 5. Validate against schema
|
|
11320
|
-
const validation = validateDashboardConfig(dashboardConfig);
|
|
11321
|
-
if (!validation.valid) {
|
|
11322
|
-
return {
|
|
11323
|
-
success: false,
|
|
11324
|
-
error: `Invalid dashboard config: ${validation.errors.join(", ")}`,
|
|
11325
|
-
};
|
|
11326
|
-
}
|
|
11327
|
-
|
|
11328
|
-
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11329
|
-
|
|
11330
|
-
// 6. Delegate to shared import pipeline
|
|
11331
|
-
return await processDashboardConfig(
|
|
11332
|
-
win,
|
|
11333
|
-
appId,
|
|
11334
|
-
dashboardConfig,
|
|
11335
|
-
widgetRegistry,
|
|
11336
|
-
{
|
|
11337
|
-
source: "registry",
|
|
11338
|
-
registryPackage: packageName,
|
|
11339
|
-
installedVersion: registryPkg.version || null,
|
|
11340
|
-
},
|
|
11341
|
-
);
|
|
11342
|
-
} catch (error) {
|
|
11343
|
-
console.error(
|
|
11344
|
-
"[DashboardConfigController] Error installing dashboard from registry:",
|
|
11345
|
-
error,
|
|
11346
|
-
);
|
|
11347
|
-
return {
|
|
11348
|
-
success: false,
|
|
11349
|
-
error: error.message,
|
|
11350
|
-
};
|
|
11351
|
-
}
|
|
11352
|
-
}
|
|
11353
|
-
|
|
11354
|
-
/**
|
|
11355
|
-
* Check compatibility of a dashboard's widget dependencies against
|
|
11356
|
-
* installed widgets and registry availability.
|
|
11357
|
-
*
|
|
11358
|
-
* @param {Array} dashboardWidgets - Widget deps from dashboard config
|
|
11359
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets())
|
|
11360
|
-
* @returns {Promise<Object>} Compatibility report
|
|
11361
|
-
*/
|
|
11362
|
-
async function checkCompatibility$1(dashboardWidgets, widgetRegistry = null) {
|
|
11363
|
-
const {
|
|
11364
|
-
checkDashboardCompatibility,
|
|
11365
|
-
} = dashboardConfigUtils$1;
|
|
11366
|
-
const { fetchRegistryIndex } = registryController$2;
|
|
11367
|
-
|
|
11368
|
-
const installedWidgets = widgetRegistry ? widgetRegistry.getWidgets() : [];
|
|
11369
|
-
|
|
11370
|
-
let registryPackages = [];
|
|
11371
|
-
try {
|
|
11372
|
-
const index = await fetchRegistryIndex();
|
|
11373
|
-
registryPackages = index.packages || [];
|
|
11374
|
-
} catch (err) {
|
|
11375
|
-
console.warn(
|
|
11376
|
-
"[DashboardConfigController] Could not fetch registry index for compatibility check:",
|
|
11377
|
-
err.message,
|
|
11378
|
-
);
|
|
11379
|
-
}
|
|
11380
|
-
|
|
11381
|
-
return checkDashboardCompatibility(
|
|
11382
|
-
dashboardWidgets,
|
|
11383
|
-
installedWidgets,
|
|
11384
|
-
registryPackages,
|
|
11385
|
-
);
|
|
11386
|
-
}
|
|
11387
|
-
|
|
11388
|
-
/**
|
|
11389
|
-
* Prepare a dashboard for publishing to the registry.
|
|
11390
|
-
*
|
|
11391
|
-
* Validates that the workspace is shareable, builds the dashboard config,
|
|
11392
|
-
* checks that all widgets exist in the registry, generates a registry
|
|
11393
|
-
* manifest, and creates a ZIP containing both the manifest and
|
|
11394
|
-
* .dashboard.json config.
|
|
11395
|
-
*
|
|
11396
|
-
* @param {BrowserWindow} win - The main window (for save dialog)
|
|
11397
|
-
* @param {string} appId - Application identifier
|
|
11398
|
-
* @param {number|string} workspaceId - ID of the workspace to publish
|
|
11399
|
-
* @param {Object} options - Publishing options
|
|
11400
|
-
* @param {string} options.authorName - Author name
|
|
11401
|
-
* @param {string} options.authorId - Author ID
|
|
11402
|
-
* @param {string} options.description - Dashboard description
|
|
11403
|
-
* @param {string[]} options.tags - Tags
|
|
11404
|
-
* @param {string} options.icon - Icon name
|
|
11405
|
-
* @param {string} options.githubUser - GitHub user/org for registry scope
|
|
11406
|
-
* @param {string} options.category - Registry category
|
|
11407
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11408
|
-
* @returns {Promise<Object>} Result with success, manifest, and filePath
|
|
11409
|
-
*/
|
|
11410
|
-
async function prepareDashboardForPublish$1(
|
|
11411
|
-
win,
|
|
11412
|
-
appId,
|
|
11413
|
-
workspaceId,
|
|
11414
|
-
options = {},
|
|
11415
|
-
widgetRegistry = null,
|
|
11416
|
-
) {
|
|
11417
|
-
try {
|
|
11418
|
-
const {
|
|
11419
|
-
generateRegistryManifest,
|
|
11420
|
-
} = dashboardConfigUtils$1;
|
|
11421
|
-
|
|
11422
|
-
// 1. Read workspace
|
|
11423
|
-
const filename = path$2.join(
|
|
11424
|
-
app$2.getPath("userData"),
|
|
11425
|
-
appName$1,
|
|
11426
|
-
appId,
|
|
11427
|
-
configFilename,
|
|
11428
|
-
);
|
|
11429
|
-
const workspacesArray = getFileContents$1(filename);
|
|
11430
|
-
const workspace = workspacesArray.find(
|
|
11431
|
-
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
11432
|
-
);
|
|
11433
|
-
|
|
11434
|
-
if (!workspace) {
|
|
11435
|
-
return {
|
|
11436
|
-
success: false,
|
|
11437
|
-
error: `Workspace not found: ${workspaceId}`,
|
|
11438
|
-
};
|
|
11439
|
-
}
|
|
11440
|
-
|
|
11441
|
-
// 2. Check shareable flag — imported dashboards cannot be published
|
|
11442
|
-
if (
|
|
11443
|
-
workspace._dashboardConfig &&
|
|
11444
|
-
workspace._dashboardConfig.shareable === false
|
|
11445
|
-
) {
|
|
11446
|
-
return {
|
|
11447
|
-
success: false,
|
|
11448
|
-
error:
|
|
11449
|
-
"This dashboard was imported and cannot be published. Only dashboards you created can be shared.",
|
|
11450
|
-
};
|
|
11451
|
-
}
|
|
11452
|
-
|
|
11453
|
-
const layout = workspace.layout || [];
|
|
11454
|
-
|
|
11455
|
-
// 3. Build the dashboard config (reuse export logic)
|
|
11143
|
+
// 2. Collect components, extract wiring, resolve deps
|
|
11456
11144
|
const componentNames = collectComponentNames(layout);
|
|
11457
11145
|
const eventWiring = extractEventWiring(layout);
|
|
11458
11146
|
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
11459
11147
|
const providers = buildProviderRequirements(componentNames, widgetRegistry);
|
|
11460
11148
|
|
|
11149
|
+
// 3. Build the dashboard config
|
|
11461
11150
|
const dashboardConfig = applyDefaults({
|
|
11462
11151
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
11463
|
-
name: workspace.name || workspace.label || "Dashboard",
|
|
11152
|
+
name: workspace.name || workspace.label || "Exported Dashboard",
|
|
11464
11153
|
description: options.description || "",
|
|
11465
11154
|
...(options.authorName
|
|
11466
11155
|
? { author: { name: options.authorName, id: options.authorId || "" } }
|
|
@@ -11482,7 +11171,33 @@ async function prepareDashboardForPublish$1(
|
|
|
11482
11171
|
eventWiring,
|
|
11483
11172
|
});
|
|
11484
11173
|
|
|
11485
|
-
// 4.
|
|
11174
|
+
// 4. Bundle theme if workspace has a themeKey
|
|
11175
|
+
if (workspace.themeKey) {
|
|
11176
|
+
try {
|
|
11177
|
+
const themeResult = themeController$1.listThemesForApplication(
|
|
11178
|
+
win,
|
|
11179
|
+
appId,
|
|
11180
|
+
);
|
|
11181
|
+
const themeData = themeResult.themes?.[workspace.themeKey];
|
|
11182
|
+
if (themeData) {
|
|
11183
|
+
dashboardConfig.theme = {
|
|
11184
|
+
key: workspace.themeKey,
|
|
11185
|
+
data: themeData,
|
|
11186
|
+
};
|
|
11187
|
+
if (themeData._registryMeta?.packageName) {
|
|
11188
|
+
dashboardConfig.theme.registryPackage =
|
|
11189
|
+
themeData._registryMeta.packageName;
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
11192
|
+
} catch (themeErr) {
|
|
11193
|
+
console.warn(
|
|
11194
|
+
"[DashboardConfigController] Could not bundle theme:",
|
|
11195
|
+
themeErr.message,
|
|
11196
|
+
);
|
|
11197
|
+
}
|
|
11198
|
+
}
|
|
11199
|
+
|
|
11200
|
+
// 5. Validate the generated config
|
|
11486
11201
|
const validation = validateDashboardConfig(dashboardConfig);
|
|
11487
11202
|
if (!validation.valid) {
|
|
11488
11203
|
return {
|
|
@@ -11491,53 +11206,17 @@ async function prepareDashboardForPublish$1(
|
|
|
11491
11206
|
};
|
|
11492
11207
|
}
|
|
11493
11208
|
|
|
11494
|
-
//
|
|
11495
|
-
const
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
const index = await fetchRegistryIndex();
|
|
11500
|
-
registryPackages = index.packages || [];
|
|
11501
|
-
} catch (err) {
|
|
11502
|
-
console.warn(
|
|
11503
|
-
`[DashboardConfigController] Unable to verify registry: ${err.message}`,
|
|
11504
|
-
);
|
|
11505
|
-
registryCheckFailed = true;
|
|
11506
|
-
}
|
|
11507
|
-
|
|
11508
|
-
let missingFromRegistry = [];
|
|
11509
|
-
if (!registryCheckFailed) {
|
|
11510
|
-
const registryNames = new Set(registryPackages.map((p) => p.name));
|
|
11511
|
-
const missingWidgets = widgets.filter(
|
|
11512
|
-
(w) => w.required !== false && !registryNames.has(w.package),
|
|
11513
|
-
);
|
|
11514
|
-
const grouped = {};
|
|
11515
|
-
for (const w of missingWidgets) {
|
|
11516
|
-
if (!grouped[w.package]) grouped[w.package] = [];
|
|
11517
|
-
const widgetName = w.id.includes(".") ? w.id.split(".")[1] : w.id;
|
|
11518
|
-
if (!grouped[w.package].includes(widgetName)) {
|
|
11519
|
-
grouped[w.package].push(widgetName);
|
|
11520
|
-
}
|
|
11521
|
-
}
|
|
11522
|
-
missingFromRegistry = Object.entries(grouped).map(
|
|
11523
|
-
([pkg, widgetNames]) => ({ package: pkg, widgets: widgetNames }),
|
|
11524
|
-
);
|
|
11525
|
-
}
|
|
11526
|
-
|
|
11527
|
-
// 6. Generate registry manifest
|
|
11528
|
-
const manifest = generateRegistryManifest(dashboardConfig, {
|
|
11529
|
-
githubUser: options.githubUser || options.authorId || "",
|
|
11530
|
-
category: options.category || "general",
|
|
11531
|
-
repository: options.repository || "",
|
|
11532
|
-
});
|
|
11209
|
+
// 6. Show save dialog
|
|
11210
|
+
const sanitizedName = (workspace.name || "dashboard")
|
|
11211
|
+
.replace(/[^a-zA-Z0-9-_ ]/g, "")
|
|
11212
|
+
.replace(/\s+/g, "-")
|
|
11213
|
+
.toLowerCase();
|
|
11533
11214
|
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
app$2.getPath("desktop"),
|
|
11540
|
-
`dashboard-${sanitizedName}-v${manifest.version}.zip`,
|
|
11215
|
+
const { canceled, filePath } = await dialog.showSaveDialog(win, {
|
|
11216
|
+
title: "Export Dashboard as ZIP",
|
|
11217
|
+
defaultPath: path$1.join(
|
|
11218
|
+
app$1.getPath("desktop"),
|
|
11219
|
+
`dashboard-${sanitizedName}.zip`,
|
|
11541
11220
|
),
|
|
11542
11221
|
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11543
11222
|
});
|
|
@@ -11546,840 +11225,1301 @@ async function prepareDashboardForPublish$1(
|
|
|
11546
11225
|
return { success: false, canceled: true };
|
|
11547
11226
|
}
|
|
11548
11227
|
|
|
11549
|
-
//
|
|
11550
|
-
const zip = new AdmZip
|
|
11551
|
-
|
|
11552
|
-
"manifest.json",
|
|
11553
|
-
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
11554
|
-
);
|
|
11228
|
+
// 7. Create ZIP with the config
|
|
11229
|
+
const zip = new AdmZip();
|
|
11230
|
+
const configJson = JSON.stringify(dashboardConfig, null, 2);
|
|
11555
11231
|
zip.addFile(
|
|
11556
11232
|
`${sanitizedName}.dashboard.json`,
|
|
11557
|
-
Buffer.from(
|
|
11233
|
+
Buffer.from(configJson, "utf-8"),
|
|
11558
11234
|
);
|
|
11235
|
+
|
|
11559
11236
|
zip.writeZip(filePath);
|
|
11560
11237
|
|
|
11561
11238
|
console.log(
|
|
11562
|
-
`[DashboardConfigController]
|
|
11239
|
+
`[DashboardConfigController] Exported dashboard to: ${filePath}`,
|
|
11563
11240
|
);
|
|
11564
11241
|
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11242
|
+
return {
|
|
11243
|
+
success: true,
|
|
11244
|
+
filePath,
|
|
11245
|
+
config: dashboardConfig,
|
|
11246
|
+
};
|
|
11247
|
+
} catch (error) {
|
|
11248
|
+
console.error(
|
|
11249
|
+
"[DashboardConfigController] Error exporting dashboard:",
|
|
11250
|
+
error,
|
|
11251
|
+
);
|
|
11252
|
+
return {
|
|
11253
|
+
success: false,
|
|
11254
|
+
error: error.message,
|
|
11255
|
+
};
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11571
11258
|
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11259
|
+
/**
|
|
11260
|
+
* Select and preview a dashboard ZIP file without importing it.
|
|
11261
|
+
* Opens the file picker, extracts and validates the .dashboard.json,
|
|
11262
|
+
* and returns a preview of the config + the file path for later import.
|
|
11263
|
+
*
|
|
11264
|
+
* @param {BrowserWindow} win - The main window (for dialog)
|
|
11265
|
+
* @returns {Promise<Object>} Result with success, filePath, and dashboardConfig preview
|
|
11266
|
+
*/
|
|
11267
|
+
async function selectDashboardFile$1(win) {
|
|
11268
|
+
try {
|
|
11269
|
+
const { canceled, filePaths } = await dialog.showOpenDialog(win, {
|
|
11270
|
+
title: "Import Dashboard Configuration",
|
|
11271
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11272
|
+
properties: ["openFile"],
|
|
11273
|
+
});
|
|
11274
|
+
|
|
11275
|
+
if (canceled || !filePaths || !filePaths.length) {
|
|
11276
|
+
return { success: false, canceled: true };
|
|
11277
|
+
}
|
|
11278
|
+
|
|
11279
|
+
const zipPath = filePaths[0];
|
|
11280
|
+
|
|
11281
|
+
// Extract and validate
|
|
11282
|
+
const zip = new AdmZip(zipPath);
|
|
11283
|
+
const tempDir = path$1.join(app$1.getPath("temp"), "dash-import");
|
|
11284
|
+
const { validateZipEntries } = widgetRegistryExports;
|
|
11285
|
+
validateZipEntries(zip, tempDir);
|
|
11286
|
+
|
|
11287
|
+
const entries = zip.getEntries();
|
|
11288
|
+
const configEntry = entries.find((e) =>
|
|
11289
|
+
e.entryName.endsWith(".dashboard.json"),
|
|
11290
|
+
);
|
|
11291
|
+
|
|
11292
|
+
if (!configEntry) {
|
|
11293
|
+
return {
|
|
11294
|
+
success: false,
|
|
11295
|
+
error: "No .dashboard.json file found in ZIP archive",
|
|
11296
|
+
};
|
|
11297
|
+
}
|
|
11298
|
+
|
|
11299
|
+
const configJson = configEntry.getData().toString("utf-8");
|
|
11300
|
+
let dashboardConfig;
|
|
11301
|
+
try {
|
|
11302
|
+
dashboardConfig = JSON.parse(configJson);
|
|
11303
|
+
} catch (parseError) {
|
|
11304
|
+
return {
|
|
11305
|
+
success: false,
|
|
11306
|
+
error: `Invalid JSON: ${parseError.message}`,
|
|
11307
|
+
};
|
|
11308
|
+
}
|
|
11309
|
+
|
|
11310
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11311
|
+
if (!validation.valid) {
|
|
11312
|
+
return {
|
|
11313
|
+
success: false,
|
|
11314
|
+
error: `Invalid config: ${validation.errors.join(", ")}`,
|
|
11315
|
+
};
|
|
11592
11316
|
}
|
|
11593
11317
|
|
|
11318
|
+
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11319
|
+
|
|
11594
11320
|
return {
|
|
11595
11321
|
success: true,
|
|
11596
|
-
filePath,
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11322
|
+
filePath: zipPath,
|
|
11323
|
+
dashboardConfig: {
|
|
11324
|
+
name: dashboardConfig.name,
|
|
11325
|
+
description: dashboardConfig.description,
|
|
11326
|
+
author: dashboardConfig.author,
|
|
11327
|
+
workspace: dashboardConfig.workspace,
|
|
11328
|
+
widgets: dashboardConfig.widgets || [],
|
|
11329
|
+
providers: dashboardConfig.providers || [],
|
|
11330
|
+
},
|
|
11603
11331
|
};
|
|
11604
11332
|
} catch (error) {
|
|
11605
11333
|
console.error(
|
|
11606
|
-
"[DashboardConfigController] Error
|
|
11334
|
+
"[DashboardConfigController] Error selecting dashboard file:",
|
|
11607
11335
|
error,
|
|
11608
11336
|
);
|
|
11609
|
-
return {
|
|
11610
|
-
success: false,
|
|
11611
|
-
error: error.message,
|
|
11612
|
-
};
|
|
11337
|
+
return { success: false, error: error.message };
|
|
11613
11338
|
}
|
|
11614
11339
|
}
|
|
11615
11340
|
|
|
11616
11341
|
/**
|
|
11617
|
-
*
|
|
11618
|
-
* Combines the structured preview data with a compatibility check.
|
|
11342
|
+
* Import a dashboard from a ZIP file containing a .dashboard.json config.
|
|
11619
11343
|
*
|
|
11620
|
-
*
|
|
11621
|
-
*
|
|
11622
|
-
*
|
|
11344
|
+
* Steps:
|
|
11345
|
+
* 1. Show native file picker for .zip selection (or use options.filePath)
|
|
11346
|
+
* 2. Extract and validate .dashboard.json
|
|
11347
|
+
* 3. Auto-install missing widgets from registry
|
|
11348
|
+
* 4. Create workspace in workspaces.json
|
|
11349
|
+
* 5. Apply event wiring to layout
|
|
11350
|
+
* 6. Mark imported dashboard shareable: false
|
|
11351
|
+
*
|
|
11352
|
+
* @param {BrowserWindow} win - The main window (for dialog)
|
|
11353
|
+
* @param {string} appId - Application identifier
|
|
11354
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets(), downloadWidget())
|
|
11355
|
+
* @param {Object} options - Import options
|
|
11356
|
+
* @param {string} options.filePath - Skip file picker, use this path directly
|
|
11357
|
+
* @param {string} options.name - Override workspace name
|
|
11358
|
+
* @param {number} options.menuId - Override workspace menuId (folder)
|
|
11359
|
+
* @param {string} options.themeKey - Override workspace themeKey
|
|
11360
|
+
* @returns {Promise<Object>} Result with success, workspace, and import summary
|
|
11623
11361
|
*/
|
|
11624
|
-
async function
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
if (!pkg) {
|
|
11633
|
-
return {
|
|
11634
|
-
success: false,
|
|
11635
|
-
error: `Dashboard package not found: ${packageName}`,
|
|
11636
|
-
};
|
|
11637
|
-
}
|
|
11362
|
+
async function importDashboardConfig$1(
|
|
11363
|
+
win,
|
|
11364
|
+
appId,
|
|
11365
|
+
widgetRegistry = null,
|
|
11366
|
+
options = {},
|
|
11367
|
+
) {
|
|
11368
|
+
try {
|
|
11369
|
+
let zipPath;
|
|
11638
11370
|
|
|
11639
|
-
|
|
11371
|
+
if (options.filePath) {
|
|
11372
|
+
// Use the provided file path (from selectDashboardFile)
|
|
11373
|
+
zipPath = options.filePath;
|
|
11374
|
+
} else {
|
|
11375
|
+
// Show file picker
|
|
11376
|
+
const { canceled, filePaths } = await dialog.showOpenDialog(win, {
|
|
11377
|
+
title: "Import Dashboard Configuration",
|
|
11378
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11379
|
+
properties: ["openFile"],
|
|
11380
|
+
});
|
|
11640
11381
|
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
try {
|
|
11645
|
-
const index = await fetchRegistryIndex();
|
|
11646
|
-
registryPackages = index.packages || [];
|
|
11647
|
-
} catch (err) {
|
|
11648
|
-
// Non-fatal — preview still works without compatibility
|
|
11649
|
-
}
|
|
11382
|
+
if (canceled || !filePaths || !filePaths.length) {
|
|
11383
|
+
return { success: false, canceled: true };
|
|
11384
|
+
}
|
|
11650
11385
|
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
installedWidgets,
|
|
11654
|
-
registryPackages,
|
|
11655
|
-
);
|
|
11386
|
+
zipPath = filePaths[0];
|
|
11387
|
+
}
|
|
11656
11388
|
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
preview,
|
|
11660
|
-
compatibility,
|
|
11661
|
-
};
|
|
11662
|
-
}
|
|
11389
|
+
// 2. Extract and validate .dashboard.json from ZIP
|
|
11390
|
+
const zip = new AdmZip(zipPath);
|
|
11663
11391
|
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
*
|
|
11669
|
-
* @param {string} appId - Application identifier
|
|
11670
|
-
* @returns {Promise<Object>} Result with updates array
|
|
11671
|
-
*/
|
|
11672
|
-
async function checkDashboardUpdatesForApp$1(appId) {
|
|
11673
|
-
const { checkDashboardUpdates } = dashboardConfigUtils$1;
|
|
11674
|
-
const { fetchRegistryIndex } = registryController$2;
|
|
11392
|
+
// Validate ZIP entries for path traversal
|
|
11393
|
+
const tempDir = path$1.join(app$1.getPath("temp"), "dash-import");
|
|
11394
|
+
const { validateZipEntries } = widgetRegistryExports;
|
|
11395
|
+
validateZipEntries(zip, tempDir);
|
|
11675
11396
|
|
|
11676
|
-
|
|
11677
|
-
const
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
appId,
|
|
11681
|
-
configFilename,
|
|
11397
|
+
// Find the .dashboard.json file
|
|
11398
|
+
const entries = zip.getEntries();
|
|
11399
|
+
const configEntry = entries.find((e) =>
|
|
11400
|
+
e.entryName.endsWith(".dashboard.json"),
|
|
11682
11401
|
);
|
|
11683
|
-
const workspaces = getFileContents$1(filename) || [];
|
|
11684
11402
|
|
|
11685
|
-
|
|
11686
|
-
|
|
11403
|
+
if (!configEntry) {
|
|
11404
|
+
return {
|
|
11405
|
+
success: false,
|
|
11406
|
+
error: "No .dashboard.json file found in ZIP archive",
|
|
11407
|
+
};
|
|
11408
|
+
}
|
|
11687
11409
|
|
|
11688
|
-
const
|
|
11410
|
+
const configJson = configEntry.getData().toString("utf-8");
|
|
11411
|
+
let dashboardConfig;
|
|
11412
|
+
try {
|
|
11413
|
+
dashboardConfig = JSON.parse(configJson);
|
|
11414
|
+
} catch (parseError) {
|
|
11415
|
+
return {
|
|
11416
|
+
success: false,
|
|
11417
|
+
error: `Invalid JSON in ${configEntry.entryName}: ${parseError.message}`,
|
|
11418
|
+
};
|
|
11419
|
+
}
|
|
11689
11420
|
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11421
|
+
// Validate against schema
|
|
11422
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11423
|
+
if (!validation.valid) {
|
|
11424
|
+
return {
|
|
11425
|
+
success: false,
|
|
11426
|
+
error: `Invalid dashboard config: ${validation.errors.join(", ")}`,
|
|
11427
|
+
};
|
|
11428
|
+
}
|
|
11429
|
+
|
|
11430
|
+
// Apply defaults to fill in optional fields
|
|
11431
|
+
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11432
|
+
|
|
11433
|
+
// Delegate to shared import pipeline with overrides
|
|
11434
|
+
return await processDashboardConfig(
|
|
11435
|
+
win,
|
|
11436
|
+
appId,
|
|
11437
|
+
dashboardConfig,
|
|
11438
|
+
widgetRegistry,
|
|
11439
|
+
{
|
|
11440
|
+
name: options.name,
|
|
11441
|
+
menuId: options.menuId,
|
|
11442
|
+
themeKey: options.themeKey,
|
|
11443
|
+
},
|
|
11444
|
+
);
|
|
11697
11445
|
} catch (error) {
|
|
11698
11446
|
console.error(
|
|
11699
|
-
"[DashboardConfigController] Error
|
|
11447
|
+
"[DashboardConfigController] Error importing dashboard:",
|
|
11700
11448
|
error,
|
|
11701
11449
|
);
|
|
11702
11450
|
return {
|
|
11703
11451
|
success: false,
|
|
11704
11452
|
error: error.message,
|
|
11705
|
-
updates: [],
|
|
11706
11453
|
};
|
|
11707
11454
|
}
|
|
11708
11455
|
}
|
|
11709
11456
|
|
|
11710
11457
|
/**
|
|
11711
|
-
*
|
|
11712
|
-
*
|
|
11713
|
-
*
|
|
11458
|
+
* Shared import pipeline: install widgets, create workspace, wire events.
|
|
11459
|
+
* Used by both importDashboardConfig (ZIP) and installDashboardFromRegistry.
|
|
11460
|
+
*
|
|
11461
|
+
* @param {BrowserWindow} win - The main window
|
|
11714
11462
|
* @param {string} appId - Application identifier
|
|
11715
|
-
* @param {
|
|
11716
|
-
* @
|
|
11463
|
+
* @param {Object} dashboardConfig - Validated dashboard config object
|
|
11464
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11465
|
+
* @param {Object} options - Additional options
|
|
11466
|
+
* @param {string} options.source - Source label ("zip" or "registry")
|
|
11467
|
+
* @returns {Promise<Object>} Result with success, workspace, and summary
|
|
11717
11468
|
*/
|
|
11718
|
-
function
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11469
|
+
async function processDashboardConfig(
|
|
11470
|
+
win,
|
|
11471
|
+
appId,
|
|
11472
|
+
dashboardConfig,
|
|
11473
|
+
widgetRegistry = null,
|
|
11474
|
+
options = {},
|
|
11475
|
+
) {
|
|
11476
|
+
const source = options.source || "zip";
|
|
11723
11477
|
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
err.message,
|
|
11731
|
-
);
|
|
11732
|
-
}
|
|
11478
|
+
// 1. Auto-install missing widgets from registry
|
|
11479
|
+
const installSummary = {
|
|
11480
|
+
installed: [],
|
|
11481
|
+
alreadyInstalled: [],
|
|
11482
|
+
failed: [],
|
|
11483
|
+
};
|
|
11733
11484
|
|
|
11734
|
-
|
|
11735
|
-
|
|
11485
|
+
if (
|
|
11486
|
+
widgetRegistry &&
|
|
11487
|
+
dashboardConfig.widgets &&
|
|
11488
|
+
dashboardConfig.widgets.length
|
|
11489
|
+
) {
|
|
11490
|
+
const installedWidgets = widgetRegistry.getWidgets();
|
|
11491
|
+
const installedPackages = new Set(installedWidgets.map((w) => w.name));
|
|
11736
11492
|
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
* Returns widget/layout info without creating a ZIP or uploading.
|
|
11740
|
-
*
|
|
11741
|
-
* @param {string} appId - Application identifier
|
|
11742
|
-
* @param {number|string} workspaceId - Workspace to preview
|
|
11743
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
11744
|
-
* @returns {Object} Preview with dashboardName, widgetCount, widgets, componentNames
|
|
11745
|
-
*/
|
|
11746
|
-
function getDashboardPublishPreview$1(appId, workspaceId, widgetRegistry = null) {
|
|
11747
|
-
try {
|
|
11748
|
-
const filename = path$2.join(
|
|
11749
|
-
app$2.getPath("userData"),
|
|
11750
|
-
appName$1,
|
|
11751
|
-
appId,
|
|
11752
|
-
configFilename,
|
|
11753
|
-
);
|
|
11754
|
-
const workspacesArray = getFileContents$1(filename);
|
|
11755
|
-
const workspace = workspacesArray.find(
|
|
11756
|
-
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
11757
|
-
);
|
|
11493
|
+
for (const widgetDep of dashboardConfig.widgets) {
|
|
11494
|
+
const packageName = widgetDep.package;
|
|
11758
11495
|
|
|
11759
|
-
|
|
11760
|
-
|
|
11496
|
+
if (installedPackages.has(packageName)) {
|
|
11497
|
+
installSummary.alreadyInstalled.push(packageName);
|
|
11498
|
+
continue;
|
|
11499
|
+
}
|
|
11500
|
+
|
|
11501
|
+
// Try to find the widget in the registry and install it
|
|
11502
|
+
try {
|
|
11503
|
+
const registryPkg = await getPackage(packageName);
|
|
11504
|
+
if (registryPkg && registryPkg.downloadUrl) {
|
|
11505
|
+
await widgetRegistry.downloadWidget(
|
|
11506
|
+
packageName,
|
|
11507
|
+
registryPkg.downloadUrl,
|
|
11508
|
+
registryPkg.dashConfigUrl || null,
|
|
11509
|
+
);
|
|
11510
|
+
installSummary.installed.push(packageName);
|
|
11511
|
+
installedPackages.add(packageName);
|
|
11512
|
+
} else {
|
|
11513
|
+
installSummary.failed.push({
|
|
11514
|
+
package: packageName,
|
|
11515
|
+
reason: "Not found in registry",
|
|
11516
|
+
});
|
|
11517
|
+
}
|
|
11518
|
+
} catch (installError) {
|
|
11519
|
+
installSummary.failed.push({
|
|
11520
|
+
package: packageName,
|
|
11521
|
+
reason: installError.message,
|
|
11522
|
+
});
|
|
11523
|
+
}
|
|
11761
11524
|
}
|
|
11525
|
+
}
|
|
11762
11526
|
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11527
|
+
// 2. Install bundled theme if present
|
|
11528
|
+
let themeInstalled = null;
|
|
11529
|
+
if (dashboardConfig.theme) {
|
|
11530
|
+
const bundledTheme = dashboardConfig.theme;
|
|
11531
|
+
try {
|
|
11532
|
+
const themeResult = themeController$1.listThemesForApplication(win, appId);
|
|
11533
|
+
const existingThemes = themeResult.themes || {};
|
|
11534
|
+
const themeKey = bundledTheme.key;
|
|
11535
|
+
|
|
11536
|
+
if (bundledTheme.data && themeKey && !existingThemes[themeKey]) {
|
|
11537
|
+
// Embed registry origin metadata if present
|
|
11538
|
+
const themeData = { ...bundledTheme.data };
|
|
11539
|
+
if (bundledTheme.registryPackage) {
|
|
11540
|
+
themeData._registryMeta = {
|
|
11541
|
+
source: "dashboard-import",
|
|
11542
|
+
packageName: bundledTheme.registryPackage,
|
|
11543
|
+
installedAt: new Date().toISOString(),
|
|
11544
|
+
};
|
|
11545
|
+
}
|
|
11546
|
+
themeController$1.saveThemeForApplication(
|
|
11547
|
+
win,
|
|
11548
|
+
appId,
|
|
11549
|
+
themeKey,
|
|
11550
|
+
themeData,
|
|
11551
|
+
);
|
|
11552
|
+
themeInstalled = themeKey;
|
|
11553
|
+
console.log(
|
|
11554
|
+
`[DashboardConfigController] Installed bundled theme: ${themeKey}`,
|
|
11555
|
+
);
|
|
11556
|
+
} else if (
|
|
11557
|
+
!bundledTheme.data &&
|
|
11558
|
+
bundledTheme.registryPackage &&
|
|
11559
|
+
themeKey &&
|
|
11560
|
+
!existingThemes[themeKey]
|
|
11561
|
+
) {
|
|
11562
|
+
// Fallback: try to install from registry by package name
|
|
11563
|
+
try {
|
|
11564
|
+
const {
|
|
11565
|
+
installThemeFromRegistry,
|
|
11566
|
+
} = themeRegistryController$1;
|
|
11567
|
+
await installThemeFromRegistry(
|
|
11568
|
+
win,
|
|
11569
|
+
appId,
|
|
11570
|
+
bundledTheme.registryPackage,
|
|
11571
|
+
);
|
|
11572
|
+
themeInstalled = themeKey;
|
|
11573
|
+
console.log(
|
|
11574
|
+
`[DashboardConfigController] Installed theme from registry: ${bundledTheme.registryPackage}`,
|
|
11575
|
+
);
|
|
11576
|
+
} catch (registryErr) {
|
|
11577
|
+
console.warn(
|
|
11578
|
+
`[DashboardConfigController] Could not install theme from registry: ${registryErr.message}`,
|
|
11579
|
+
);
|
|
11580
|
+
}
|
|
11581
|
+
}
|
|
11582
|
+
} catch (themeErr) {
|
|
11583
|
+
console.warn(
|
|
11584
|
+
`[DashboardConfigController] Could not install bundled theme: ${themeErr.message}`,
|
|
11585
|
+
);
|
|
11586
|
+
}
|
|
11587
|
+
}
|
|
11588
|
+
|
|
11589
|
+
// 3. Build workspace from config
|
|
11590
|
+
const workspace = { ...dashboardConfig.workspace };
|
|
11766
11591
|
|
|
11592
|
+
if (!workspace || !workspace.layout) {
|
|
11767
11593
|
return {
|
|
11768
|
-
success:
|
|
11769
|
-
|
|
11770
|
-
widgetCount: componentNames.length,
|
|
11771
|
-
widgets: widgets.map((w) => ({ name: w.name, package: w.package })),
|
|
11772
|
-
componentNames: [...componentNames],
|
|
11594
|
+
success: false,
|
|
11595
|
+
error: "Dashboard config has no workspace data",
|
|
11773
11596
|
};
|
|
11774
|
-
} catch (error) {
|
|
11775
|
-
console.error(
|
|
11776
|
-
"[DashboardConfigController] Error getting publish preview:",
|
|
11777
|
-
error,
|
|
11778
|
-
);
|
|
11779
|
-
return { success: false, error: error.message };
|
|
11780
11597
|
}
|
|
11781
|
-
}
|
|
11782
11598
|
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
selectDashboardFile: selectDashboardFile$1,
|
|
11786
|
-
importDashboardConfig: importDashboardConfig$1,
|
|
11787
|
-
installDashboardFromRegistry: installDashboardFromRegistry$1,
|
|
11788
|
-
checkCompatibility: checkCompatibility$1,
|
|
11789
|
-
prepareDashboardForPublish: prepareDashboardForPublish$1,
|
|
11790
|
-
getDashboardPreview: getDashboardPreview$1,
|
|
11791
|
-
checkDashboardUpdatesForApp: checkDashboardUpdatesForApp$1,
|
|
11792
|
-
getProviderSetupManifest: getProviderSetupManifest$1,
|
|
11793
|
-
getDashboardPublishPreview: getDashboardPublishPreview$1,
|
|
11794
|
-
};
|
|
11599
|
+
// Generate a unique ID for the imported workspace
|
|
11600
|
+
workspace.id = Date.now();
|
|
11795
11601
|
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
* Manages preferences (electron-store), rate limiting, deduplication,
|
|
11801
|
-
* and dispatching native Notification instances.
|
|
11802
|
-
*/
|
|
11602
|
+
// Apply name/menuId/themeKey overrides if provided
|
|
11603
|
+
if (options.name) workspace.name = options.name;
|
|
11604
|
+
if (options.menuId !== undefined) workspace.menuId = options.menuId;
|
|
11605
|
+
if (options.themeKey !== undefined) workspace.themeKey = options.themeKey;
|
|
11803
11606
|
|
|
11804
|
-
|
|
11805
|
-
|
|
11607
|
+
// Set themeKey from bundled theme if it was installed and no override given
|
|
11608
|
+
if (themeInstalled && options.themeKey === undefined) {
|
|
11609
|
+
workspace.themeKey = themeInstalled;
|
|
11610
|
+
}
|
|
11806
11611
|
|
|
11807
|
-
|
|
11612
|
+
// 4. Apply event wiring to layout
|
|
11613
|
+
const eventWiringSummary = [];
|
|
11614
|
+
if (
|
|
11615
|
+
dashboardConfig.eventWiring &&
|
|
11616
|
+
dashboardConfig.eventWiring.length &&
|
|
11617
|
+
workspace.layout
|
|
11618
|
+
) {
|
|
11619
|
+
applyEventWiringToLayout(workspace.layout, dashboardConfig.eventWiring);
|
|
11620
|
+
for (const wire of dashboardConfig.eventWiring) {
|
|
11621
|
+
eventWiringSummary.push(
|
|
11622
|
+
`${wire.source?.widget}.${wire.source?.event} → ${wire.target?.widget}.${wire.target?.handler}`,
|
|
11623
|
+
);
|
|
11624
|
+
}
|
|
11625
|
+
}
|
|
11808
11626
|
|
|
11809
|
-
//
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11627
|
+
// 5. Mark as not shareable (imported dashboards cannot be re-published)
|
|
11628
|
+
workspace._dashboardConfig = {
|
|
11629
|
+
shareable: false,
|
|
11630
|
+
source,
|
|
11631
|
+
importedFrom: dashboardConfig.name,
|
|
11632
|
+
importedAt: new Date().toISOString(),
|
|
11633
|
+
originalAuthor: dashboardConfig.author,
|
|
11634
|
+
schemaVersion: dashboardConfig.schemaVersion,
|
|
11635
|
+
registryPackage: options.registryPackage || null,
|
|
11636
|
+
installedVersion: options.installedVersion || null,
|
|
11637
|
+
};
|
|
11814
11638
|
|
|
11815
|
-
//
|
|
11816
|
-
|
|
11817
|
-
const
|
|
11818
|
-
|
|
11639
|
+
// Save workspace to workspaces.json
|
|
11640
|
+
const workspaceController = workspaceController_1;
|
|
11641
|
+
const saveResult = workspaceController.saveWorkspaceForApplication(
|
|
11642
|
+
win,
|
|
11643
|
+
appId,
|
|
11644
|
+
workspace,
|
|
11645
|
+
);
|
|
11819
11646
|
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11647
|
+
if (saveResult.error) {
|
|
11648
|
+
return {
|
|
11649
|
+
success: false,
|
|
11650
|
+
error: `Failed to save workspace: ${saveResult.message}`,
|
|
11651
|
+
};
|
|
11652
|
+
}
|
|
11823
11653
|
|
|
11824
|
-
|
|
11825
|
-
const
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
}
|
|
11654
|
+
// Build provider requirements summary
|
|
11655
|
+
const providerSummary = (dashboardConfig.providers || []).map((p) => ({
|
|
11656
|
+
type: p.type,
|
|
11657
|
+
providerClass: p.providerClass,
|
|
11658
|
+
required: p.required,
|
|
11659
|
+
usedBy: p.usedBy,
|
|
11660
|
+
}));
|
|
11832
11661
|
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11662
|
+
console.log(
|
|
11663
|
+
`[DashboardConfigController] Imported dashboard "${dashboardConfig.name}" (${source}) as workspace ${workspace.id}`,
|
|
11664
|
+
);
|
|
11665
|
+
|
|
11666
|
+
return {
|
|
11667
|
+
success: true,
|
|
11668
|
+
workspace,
|
|
11669
|
+
summary: {
|
|
11670
|
+
name: dashboardConfig.name,
|
|
11671
|
+
description: dashboardConfig.description || "",
|
|
11672
|
+
author: dashboardConfig.author,
|
|
11673
|
+
widgets: installSummary,
|
|
11674
|
+
eventsWired: eventWiringSummary,
|
|
11675
|
+
providersRequired: providerSummary,
|
|
11676
|
+
themeInstalled: themeInstalled || null,
|
|
11677
|
+
},
|
|
11678
|
+
};
|
|
11837
11679
|
}
|
|
11838
11680
|
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
}
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11681
|
+
/**
|
|
11682
|
+
* Install a dashboard from the registry by package name.
|
|
11683
|
+
*
|
|
11684
|
+
* Fetches the dashboard ZIP from the registry, extracts the .dashboard.json,
|
|
11685
|
+
* validates it, and delegates to the shared import pipeline.
|
|
11686
|
+
*
|
|
11687
|
+
* @param {BrowserWindow} win - The main window
|
|
11688
|
+
* @param {string} appId - Application identifier
|
|
11689
|
+
* @param {string} packageName - Registry package name for the dashboard
|
|
11690
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11691
|
+
* @returns {Promise<Object>} Result with success, workspace, and summary
|
|
11692
|
+
*/
|
|
11693
|
+
async function installDashboardFromRegistry$1(
|
|
11694
|
+
win,
|
|
11695
|
+
appId,
|
|
11696
|
+
packageName,
|
|
11697
|
+
widgetRegistry = null,
|
|
11698
|
+
) {
|
|
11699
|
+
try {
|
|
11700
|
+
// 1. Look up the dashboard package in the registry
|
|
11701
|
+
const registryPkg = await getPackage(packageName);
|
|
11702
|
+
if (!registryPkg) {
|
|
11703
|
+
return {
|
|
11704
|
+
success: false,
|
|
11705
|
+
error: `Dashboard package not found in registry: ${packageName}`,
|
|
11706
|
+
};
|
|
11856
11707
|
}
|
|
11857
|
-
}
|
|
11858
|
-
}, 30_000);
|
|
11859
|
-
|
|
11860
|
-
const notificationController$2 = {
|
|
11861
|
-
/**
|
|
11862
|
-
* Send a notification if it passes all checks.
|
|
11863
|
-
*
|
|
11864
|
-
* @param {BrowserWindow} win - the main window (for click routing)
|
|
11865
|
-
* @param {Object} payload
|
|
11866
|
-
* @param {string} payload.widgetName - component name of the widget
|
|
11867
|
-
* @param {string} payload.widgetId - widget instance UUID
|
|
11868
|
-
* @param {string} payload.workspaceId - workspace containing the widget
|
|
11869
|
-
* @param {string} payload.type - notification type key (must match .dash.js config)
|
|
11870
|
-
* @param {string} payload.title - notification title
|
|
11871
|
-
* @param {string} payload.body - notification body
|
|
11872
|
-
* @param {Object} [payload.data] - arbitrary data returned on click
|
|
11873
|
-
* @param {boolean} [payload.silent] - suppress sound
|
|
11874
|
-
* @param {string} [payload.urgency] - "low" | "normal" | "critical"
|
|
11875
|
-
* @returns {{ success: boolean, reason?: string }}
|
|
11876
|
-
*/
|
|
11877
|
-
send: (win, payload) => {
|
|
11878
|
-
try {
|
|
11879
|
-
const {
|
|
11880
|
-
widgetName,
|
|
11881
|
-
widgetId,
|
|
11882
|
-
workspaceId,
|
|
11883
|
-
type,
|
|
11884
|
-
title,
|
|
11885
|
-
body,
|
|
11886
|
-
data,
|
|
11887
|
-
silent,
|
|
11888
|
-
urgency,
|
|
11889
|
-
} = payload;
|
|
11890
11708
|
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
}
|
|
11896
|
-
|
|
11897
|
-
// 2. Check doNotDisturb
|
|
11898
|
-
const dnd = store$1.get("doNotDisturb", false);
|
|
11899
|
-
if (dnd) {
|
|
11900
|
-
return { success: false, reason: "do_not_disturb" };
|
|
11901
|
-
}
|
|
11709
|
+
if (!registryPkg.downloadUrl) {
|
|
11710
|
+
return {
|
|
11711
|
+
success: false,
|
|
11712
|
+
error: `Dashboard package has no download URL: ${packageName}`,
|
|
11713
|
+
};
|
|
11714
|
+
}
|
|
11902
11715
|
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
}
|
|
11716
|
+
// 2. Resolve the download URL and fetch the ZIP
|
|
11717
|
+
const version = registryPkg.version || "1.0.0";
|
|
11718
|
+
let downloadUrl = registryPkg.downloadUrl;
|
|
11719
|
+
downloadUrl = downloadUrl.replace("{version}", version);
|
|
11720
|
+
downloadUrl = downloadUrl.replace("{name}", packageName);
|
|
11909
11721
|
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11722
|
+
// Enforce HTTPS
|
|
11723
|
+
const parsedUrl = new URL(downloadUrl);
|
|
11724
|
+
if (parsedUrl.protocol !== "https:") {
|
|
11725
|
+
return {
|
|
11726
|
+
success: false,
|
|
11727
|
+
error: `Dashboard downloads must use HTTPS. Refusing: ${downloadUrl}`,
|
|
11728
|
+
};
|
|
11729
|
+
}
|
|
11914
11730
|
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
}
|
|
11731
|
+
console.log(
|
|
11732
|
+
`[DashboardConfigController] Fetching dashboard from: ${downloadUrl}`,
|
|
11733
|
+
);
|
|
11919
11734
|
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11735
|
+
const response = await fetch(downloadUrl);
|
|
11736
|
+
if (!response.ok) {
|
|
11737
|
+
return {
|
|
11738
|
+
success: false,
|
|
11739
|
+
error: `Failed to download dashboard: ${response.status} ${response.statusText}`,
|
|
11740
|
+
};
|
|
11741
|
+
}
|
|
11927
11742
|
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
win.show();
|
|
11931
|
-
win.focus();
|
|
11932
|
-
win.webContents.send("notification:clicked", {
|
|
11933
|
-
widgetName,
|
|
11934
|
-
widgetId,
|
|
11935
|
-
workspaceId,
|
|
11936
|
-
type,
|
|
11937
|
-
data: data || null,
|
|
11938
|
-
});
|
|
11939
|
-
}
|
|
11940
|
-
});
|
|
11743
|
+
const buffer = await response.arrayBuffer();
|
|
11744
|
+
const zip = new AdmZip(Buffer.from(buffer));
|
|
11941
11745
|
|
|
11942
|
-
|
|
11943
|
-
|
|
11746
|
+
// 3. Validate ZIP entries
|
|
11747
|
+
const tempDir = path$1.join(app$1.getPath("temp"), "dash-registry-import");
|
|
11748
|
+
const { validateZipEntries } = widgetRegistryExports;
|
|
11749
|
+
validateZipEntries(zip, tempDir);
|
|
11944
11750
|
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11751
|
+
// 4. Find and parse .dashboard.json
|
|
11752
|
+
const entries = zip.getEntries();
|
|
11753
|
+
const configEntry = entries.find((e) =>
|
|
11754
|
+
e.entryName.endsWith(".dashboard.json"),
|
|
11755
|
+
);
|
|
11948
11756
|
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
"
|
|
11953
|
-
|
|
11954
|
-
);
|
|
11955
|
-
return { error: true, message: error.message };
|
|
11757
|
+
if (!configEntry) {
|
|
11758
|
+
return {
|
|
11759
|
+
success: false,
|
|
11760
|
+
error: "No .dashboard.json file found in downloaded archive",
|
|
11761
|
+
};
|
|
11956
11762
|
}
|
|
11957
|
-
},
|
|
11958
11763
|
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
*
|
|
11962
|
-
* @returns {{ globalEnabled: boolean, doNotDisturb: boolean, instances: Object }}
|
|
11963
|
-
*/
|
|
11964
|
-
getPreferences: () => {
|
|
11764
|
+
const configJson = configEntry.getData().toString("utf-8");
|
|
11765
|
+
let dashboardConfig;
|
|
11965
11766
|
try {
|
|
11767
|
+
dashboardConfig = JSON.parse(configJson);
|
|
11768
|
+
} catch (parseError) {
|
|
11966
11769
|
return {
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
instances: store$1.get("instances", {}),
|
|
11770
|
+
success: false,
|
|
11771
|
+
error: `Invalid JSON in dashboard config: ${parseError.message}`,
|
|
11970
11772
|
};
|
|
11971
|
-
}
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11773
|
+
}
|
|
11774
|
+
|
|
11775
|
+
// 5. Validate against schema
|
|
11776
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11777
|
+
if (!validation.valid) {
|
|
11976
11778
|
return {
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
globalEnabled: true,
|
|
11980
|
-
doNotDisturb: false,
|
|
11981
|
-
instances: {},
|
|
11779
|
+
success: false,
|
|
11780
|
+
error: `Invalid dashboard config: ${validation.errors.join(", ")}`,
|
|
11982
11781
|
};
|
|
11983
11782
|
}
|
|
11984
|
-
},
|
|
11985
11783
|
|
|
11986
|
-
|
|
11987
|
-
* Set notification preferences for a specific widget instance.
|
|
11988
|
-
*
|
|
11989
|
-
* @param {string} widgetId - widget instance UUID
|
|
11990
|
-
* @param {Object} prefs - { [notificationType]: boolean }
|
|
11991
|
-
* @returns {{ success: boolean }}
|
|
11992
|
-
*/
|
|
11993
|
-
setPreferences: (widgetId, prefs) => {
|
|
11994
|
-
try {
|
|
11995
|
-
const instances = store$1.get("instances", {});
|
|
11996
|
-
instances[widgetId] = { ...(instances[widgetId] || {}), ...prefs };
|
|
11997
|
-
store$1.set("instances", instances);
|
|
11998
|
-
console.log(
|
|
11999
|
-
`[notificationController] Preferences updated for widget ${widgetId}`,
|
|
12000
|
-
);
|
|
12001
|
-
return { success: true };
|
|
12002
|
-
} catch (error) {
|
|
12003
|
-
console.error(
|
|
12004
|
-
"[notificationController] Error setting preferences:",
|
|
12005
|
-
error,
|
|
12006
|
-
);
|
|
12007
|
-
return { error: true, message: error.message };
|
|
12008
|
-
}
|
|
12009
|
-
},
|
|
11784
|
+
dashboardConfig = applyDefaults(dashboardConfig);
|
|
12010
11785
|
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
console.error("[notificationController] Error setting global:", error);
|
|
12034
|
-
return { error: true, message: error.message };
|
|
12035
|
-
}
|
|
12036
|
-
},
|
|
12037
|
-
};
|
|
12038
|
-
|
|
12039
|
-
var notificationController_1 = notificationController$2;
|
|
12040
|
-
|
|
12041
|
-
/**
|
|
12042
|
-
* themeRegistryController.js
|
|
12043
|
-
*
|
|
12044
|
-
* Handles publishing themes to and installing themes from the Dash registry.
|
|
12045
|
-
* Mirrors dashboardConfigController patterns for ZIP creation, manifest generation,
|
|
12046
|
-
* and registry interaction.
|
|
12047
|
-
*/
|
|
12048
|
-
const path$1 = require$$1$1;
|
|
12049
|
-
const { app: app$1, dialog } = require$$0$1;
|
|
12050
|
-
const AdmZip = require$$3$3;
|
|
12051
|
-
|
|
12052
|
-
const themeController$1 = themeController_1;
|
|
12053
|
-
const registryController$1 = registryController$2;
|
|
12054
|
-
const registryApiController$1 = registryApiController$2;
|
|
12055
|
-
const { getAuthStatus } = registryAuthController$1;
|
|
12056
|
-
|
|
12057
|
-
/**
|
|
12058
|
-
* Sanitize a name for use as a filename (lowercase, hyphens only).
|
|
12059
|
-
*/
|
|
12060
|
-
function sanitizeName(name) {
|
|
12061
|
-
return (name || "theme")
|
|
12062
|
-
.toLowerCase()
|
|
12063
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
12064
|
-
.replace(/^-|-$/g, "");
|
|
11786
|
+
// 6. Delegate to shared import pipeline
|
|
11787
|
+
return await processDashboardConfig(
|
|
11788
|
+
win,
|
|
11789
|
+
appId,
|
|
11790
|
+
dashboardConfig,
|
|
11791
|
+
widgetRegistry,
|
|
11792
|
+
{
|
|
11793
|
+
source: "registry",
|
|
11794
|
+
registryPackage: packageName,
|
|
11795
|
+
installedVersion: registryPkg.version || null,
|
|
11796
|
+
},
|
|
11797
|
+
);
|
|
11798
|
+
} catch (error) {
|
|
11799
|
+
console.error(
|
|
11800
|
+
"[DashboardConfigController] Error installing dashboard from registry:",
|
|
11801
|
+
error,
|
|
11802
|
+
);
|
|
11803
|
+
return {
|
|
11804
|
+
success: false,
|
|
11805
|
+
error: error.message,
|
|
11806
|
+
};
|
|
11807
|
+
}
|
|
12065
11808
|
}
|
|
12066
11809
|
|
|
12067
11810
|
/**
|
|
12068
|
-
*
|
|
11811
|
+
* Check compatibility of a dashboard's widget dependencies against
|
|
11812
|
+
* installed widgets and registry availability.
|
|
12069
11813
|
*
|
|
12070
|
-
* @param {
|
|
12071
|
-
* @param {
|
|
12072
|
-
* @
|
|
12073
|
-
* @returns {Object} Registry manifest
|
|
12074
|
-
*/
|
|
12075
|
-
function generateThemeRegistryManifest(themeData, themeKey, options = {}) {
|
|
12076
|
-
const sanitizedName = sanitizeName(themeKey);
|
|
12077
|
-
const colors = extractColors(themeData);
|
|
12078
|
-
|
|
12079
|
-
return {
|
|
12080
|
-
scope: options.scope || "",
|
|
12081
|
-
name: sanitizedName,
|
|
12082
|
-
displayName: themeKey,
|
|
12083
|
-
author: options.authorName || "",
|
|
12084
|
-
description: options.description || "",
|
|
12085
|
-
version: "1.0.0",
|
|
12086
|
-
type: "theme",
|
|
12087
|
-
category: "general",
|
|
12088
|
-
tags: options.tags || [],
|
|
12089
|
-
icon: "palette",
|
|
12090
|
-
colors,
|
|
12091
|
-
appOrigin: options.appOrigin || "",
|
|
12092
|
-
publishedAt: new Date().toISOString(),
|
|
12093
|
-
};
|
|
12094
|
-
}
|
|
12095
|
-
|
|
12096
|
-
/**
|
|
12097
|
-
* Extract primary/secondary/tertiary/neutral colors from a theme object.
|
|
12098
|
-
* Theme objects store colors in various structures; this normalizes them.
|
|
11814
|
+
* @param {Array} dashboardWidgets - Widget deps from dashboard config
|
|
11815
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets())
|
|
11816
|
+
* @returns {Promise<Object>} Compatibility report
|
|
12099
11817
|
*/
|
|
12100
|
-
function
|
|
12101
|
-
const
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
neutral: "",
|
|
12106
|
-
};
|
|
12107
|
-
|
|
12108
|
-
if (!themeData) return colors;
|
|
11818
|
+
async function checkCompatibility$1(dashboardWidgets, widgetRegistry = null) {
|
|
11819
|
+
const {
|
|
11820
|
+
checkDashboardCompatibility,
|
|
11821
|
+
} = dashboardConfigUtils$1;
|
|
11822
|
+
const { fetchRegistryIndex } = registryController$2;
|
|
12109
11823
|
|
|
12110
|
-
|
|
12111
|
-
if (themeData.primary) colors.primary = themeData.primary;
|
|
12112
|
-
if (themeData.secondary) colors.secondary = themeData.secondary;
|
|
12113
|
-
if (themeData.tertiary) colors.tertiary = themeData.tertiary;
|
|
12114
|
-
if (themeData.neutral) colors.neutral = themeData.neutral;
|
|
11824
|
+
const installedWidgets = widgetRegistry ? widgetRegistry.getWidgets() : [];
|
|
12115
11825
|
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
11826
|
+
let registryPackages = [];
|
|
11827
|
+
try {
|
|
11828
|
+
const index = await fetchRegistryIndex();
|
|
11829
|
+
registryPackages = index.packages || [];
|
|
11830
|
+
} catch (err) {
|
|
11831
|
+
console.warn(
|
|
11832
|
+
"[DashboardConfigController] Could not fetch registry index for compatibility check:",
|
|
11833
|
+
err.message,
|
|
11834
|
+
);
|
|
12123
11835
|
}
|
|
12124
11836
|
|
|
12125
|
-
return
|
|
11837
|
+
return checkDashboardCompatibility(
|
|
11838
|
+
dashboardWidgets,
|
|
11839
|
+
installedWidgets,
|
|
11840
|
+
registryPackages,
|
|
11841
|
+
);
|
|
12126
11842
|
}
|
|
12127
11843
|
|
|
12128
11844
|
/**
|
|
12129
|
-
* Prepare a
|
|
11845
|
+
* Prepare a dashboard for publishing to the registry.
|
|
12130
11846
|
*
|
|
12131
|
-
*
|
|
12132
|
-
*
|
|
11847
|
+
* Validates that the workspace is shareable, builds the dashboard config,
|
|
11848
|
+
* checks that all widgets exist in the registry, generates a registry
|
|
11849
|
+
* manifest, and creates a ZIP containing both the manifest and
|
|
11850
|
+
* .dashboard.json config.
|
|
12133
11851
|
*
|
|
12134
|
-
* @param {BrowserWindow} win - The
|
|
11852
|
+
* @param {BrowserWindow} win - The main window (for save dialog)
|
|
12135
11853
|
* @param {string} appId - Application identifier
|
|
12136
|
-
* @param {string}
|
|
12137
|
-
* @param {Object} options -
|
|
12138
|
-
* @
|
|
11854
|
+
* @param {number|string} workspaceId - ID of the workspace to publish
|
|
11855
|
+
* @param {Object} options - Publishing options
|
|
11856
|
+
* @param {string} options.authorName - Author name
|
|
11857
|
+
* @param {string} options.authorId - Author ID
|
|
11858
|
+
* @param {string} options.description - Dashboard description
|
|
11859
|
+
* @param {string[]} options.tags - Tags
|
|
11860
|
+
* @param {string} options.icon - Icon name
|
|
11861
|
+
* @param {string} options.githubUser - GitHub user/org for registry scope
|
|
11862
|
+
* @param {string} options.category - Registry category
|
|
11863
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11864
|
+
* @returns {Promise<Object>} Result with success, manifest, and filePath
|
|
12139
11865
|
*/
|
|
12140
|
-
async function
|
|
11866
|
+
async function prepareDashboardForPublish$1(
|
|
11867
|
+
win,
|
|
11868
|
+
appId,
|
|
11869
|
+
workspaceId,
|
|
11870
|
+
options = {},
|
|
11871
|
+
widgetRegistry = null,
|
|
11872
|
+
) {
|
|
12141
11873
|
try {
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
return { success: false, error: "Failed to read themes: " + themesResult.message };
|
|
12146
|
-
}
|
|
11874
|
+
const {
|
|
11875
|
+
generateRegistryManifest,
|
|
11876
|
+
} = dashboardConfigUtils$1;
|
|
12147
11877
|
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
11878
|
+
// 1. Read workspace
|
|
11879
|
+
const filename = path$1.join(
|
|
11880
|
+
app$1.getPath("userData"),
|
|
11881
|
+
appName$1,
|
|
11882
|
+
appId,
|
|
11883
|
+
configFilename,
|
|
11884
|
+
);
|
|
11885
|
+
const workspacesArray = getFileContents$1(filename);
|
|
11886
|
+
const workspace = workspacesArray.find(
|
|
11887
|
+
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
11888
|
+
);
|
|
12152
11889
|
|
|
12153
|
-
|
|
12154
|
-
const auth = getAuthStatus();
|
|
12155
|
-
const scope = auth.profile?.username || options.scope || "";
|
|
12156
|
-
if (!scope) {
|
|
11890
|
+
if (!workspace) {
|
|
12157
11891
|
return {
|
|
12158
11892
|
success: false,
|
|
12159
|
-
error:
|
|
12160
|
-
authRequired: true,
|
|
11893
|
+
error: `Workspace not found: ${workspaceId}`,
|
|
12161
11894
|
};
|
|
12162
11895
|
}
|
|
12163
11896
|
|
|
12164
|
-
//
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
});
|
|
12170
|
-
|
|
12171
|
-
// Validate colors
|
|
12172
|
-
if (!manifest.colors.primary || !manifest.colors.secondary || !manifest.colors.tertiary) {
|
|
11897
|
+
// 2. Check shareable flag — imported dashboards cannot be published
|
|
11898
|
+
if (
|
|
11899
|
+
workspace._dashboardConfig &&
|
|
11900
|
+
workspace._dashboardConfig.shareable === false
|
|
11901
|
+
) {
|
|
12173
11902
|
return {
|
|
12174
11903
|
success: false,
|
|
12175
|
-
error:
|
|
11904
|
+
error:
|
|
11905
|
+
"This dashboard was imported and cannot be published. Only dashboards you created can be shared.",
|
|
12176
11906
|
};
|
|
12177
11907
|
}
|
|
12178
11908
|
|
|
12179
|
-
|
|
12180
|
-
const sanitizedName = sanitizeName(themeKey);
|
|
12181
|
-
const defaultFilename = `theme-${sanitizedName}-v${manifest.version}.zip`;
|
|
11909
|
+
const layout = workspace.layout || [];
|
|
12182
11910
|
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
11911
|
+
// 3. Build the dashboard config (reuse export logic)
|
|
11912
|
+
const componentNames = collectComponentNames(layout);
|
|
11913
|
+
const eventWiring = extractEventWiring(layout);
|
|
11914
|
+
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
11915
|
+
const providers = buildProviderRequirements(componentNames, widgetRegistry);
|
|
11916
|
+
|
|
11917
|
+
const dashboardConfig = applyDefaults({
|
|
11918
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
11919
|
+
name: workspace.name || workspace.label || "Dashboard",
|
|
11920
|
+
description: options.description || "",
|
|
11921
|
+
...(options.authorName
|
|
11922
|
+
? { author: { name: options.authorName, id: options.authorId || "" } }
|
|
11923
|
+
: {}),
|
|
11924
|
+
shareable: true,
|
|
11925
|
+
tags: options.tags || [],
|
|
11926
|
+
icon: options.icon || "grip",
|
|
11927
|
+
workspace: {
|
|
11928
|
+
id: workspace.id,
|
|
11929
|
+
name: workspace.name,
|
|
11930
|
+
type: workspace.type || "workspace",
|
|
11931
|
+
label: workspace.label || workspace.name,
|
|
11932
|
+
version: workspace.version || 1,
|
|
11933
|
+
layout,
|
|
11934
|
+
menuId: workspace.menuId || 1,
|
|
11935
|
+
},
|
|
11936
|
+
widgets,
|
|
11937
|
+
providers,
|
|
11938
|
+
eventWiring,
|
|
12187
11939
|
});
|
|
12188
11940
|
|
|
12189
|
-
|
|
12190
|
-
|
|
11941
|
+
// 4. Bundle theme if workspace has a themeKey
|
|
11942
|
+
if (workspace.themeKey) {
|
|
11943
|
+
try {
|
|
11944
|
+
const themeResult = themeController$1.listThemesForApplication(
|
|
11945
|
+
win,
|
|
11946
|
+
appId,
|
|
11947
|
+
);
|
|
11948
|
+
const themeData = themeResult.themes?.[workspace.themeKey];
|
|
11949
|
+
if (themeData) {
|
|
11950
|
+
dashboardConfig.theme = {
|
|
11951
|
+
key: workspace.themeKey,
|
|
11952
|
+
data: themeData,
|
|
11953
|
+
};
|
|
11954
|
+
if (themeData._registryMeta?.packageName) {
|
|
11955
|
+
dashboardConfig.theme.registryPackage =
|
|
11956
|
+
themeData._registryMeta.packageName;
|
|
11957
|
+
}
|
|
11958
|
+
}
|
|
11959
|
+
} catch (themeErr) {
|
|
11960
|
+
console.warn(
|
|
11961
|
+
"[DashboardConfigController] Could not bundle theme for publish:",
|
|
11962
|
+
themeErr.message,
|
|
11963
|
+
);
|
|
11964
|
+
}
|
|
12191
11965
|
}
|
|
12192
11966
|
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
11967
|
+
// 5. Validate the config
|
|
11968
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11969
|
+
if (!validation.valid) {
|
|
11970
|
+
return {
|
|
11971
|
+
success: false,
|
|
11972
|
+
error: `Generated config is invalid: ${validation.errors.join(", ")}`,
|
|
11973
|
+
};
|
|
11974
|
+
}
|
|
11975
|
+
|
|
11976
|
+
// 6. Check which widgets exist in the registry (soft warning, not blocking)
|
|
11977
|
+
const { fetchRegistryIndex } = registryController$2;
|
|
11978
|
+
let registryPackages = [];
|
|
11979
|
+
let registryCheckFailed = false;
|
|
11980
|
+
try {
|
|
11981
|
+
const index = await fetchRegistryIndex();
|
|
11982
|
+
registryPackages = index.packages || [];
|
|
11983
|
+
} catch (err) {
|
|
11984
|
+
console.warn(
|
|
11985
|
+
`[DashboardConfigController] Unable to verify registry: ${err.message}`,
|
|
11986
|
+
);
|
|
11987
|
+
registryCheckFailed = true;
|
|
11988
|
+
}
|
|
11989
|
+
|
|
11990
|
+
let missingFromRegistry = [];
|
|
11991
|
+
if (!registryCheckFailed) {
|
|
11992
|
+
const registryNames = new Set(registryPackages.map((p) => p.name));
|
|
11993
|
+
const missingWidgets = widgets.filter(
|
|
11994
|
+
(w) => w.required !== false && !registryNames.has(w.package),
|
|
11995
|
+
);
|
|
11996
|
+
const grouped = {};
|
|
11997
|
+
for (const w of missingWidgets) {
|
|
11998
|
+
if (!grouped[w.package]) grouped[w.package] = [];
|
|
11999
|
+
const widgetName = w.id.includes(".") ? w.id.split(".")[1] : w.id;
|
|
12000
|
+
if (!grouped[w.package].includes(widgetName)) {
|
|
12001
|
+
grouped[w.package].push(widgetName);
|
|
12002
|
+
}
|
|
12003
|
+
}
|
|
12004
|
+
missingFromRegistry = Object.entries(grouped).map(
|
|
12005
|
+
([pkg, widgetNames]) => ({ package: pkg, widgets: widgetNames }),
|
|
12006
|
+
);
|
|
12007
|
+
}
|
|
12008
|
+
|
|
12009
|
+
// 7. Generate registry manifest
|
|
12010
|
+
const manifest = generateRegistryManifest(dashboardConfig, {
|
|
12011
|
+
githubUser: options.githubUser || options.authorId || "",
|
|
12012
|
+
category: options.category || "general",
|
|
12013
|
+
repository: options.repository || "",
|
|
12014
|
+
});
|
|
12015
|
+
|
|
12016
|
+
// 8. Show save dialog for the publish package
|
|
12017
|
+
const sanitizedName = manifest.name;
|
|
12018
|
+
const { canceled, filePath } = await dialog.showSaveDialog(win, {
|
|
12019
|
+
title: "Save Dashboard Package for Registry",
|
|
12020
|
+
defaultPath: path$1.join(
|
|
12021
|
+
app$1.getPath("desktop"),
|
|
12022
|
+
`dashboard-${sanitizedName}-v${manifest.version}.zip`,
|
|
12023
|
+
),
|
|
12024
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
12025
|
+
});
|
|
12026
|
+
|
|
12027
|
+
if (canceled || !filePath) {
|
|
12028
|
+
return { success: false, canceled: true };
|
|
12029
|
+
}
|
|
12030
|
+
|
|
12031
|
+
// 9. Create ZIP with manifest and dashboard config
|
|
12032
|
+
const zip = new AdmZip();
|
|
12033
|
+
zip.addFile(
|
|
12034
|
+
"manifest.json",
|
|
12199
12035
|
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
12200
12036
|
);
|
|
12201
12037
|
zip.addFile(
|
|
12202
|
-
`${sanitizedName}.
|
|
12203
|
-
Buffer.from(JSON.stringify(
|
|
12038
|
+
`${sanitizedName}.dashboard.json`,
|
|
12039
|
+
Buffer.from(JSON.stringify(dashboardConfig, null, 2), "utf-8"),
|
|
12204
12040
|
);
|
|
12205
12041
|
zip.writeZip(filePath);
|
|
12206
12042
|
|
|
12207
|
-
console.log(
|
|
12043
|
+
console.log(
|
|
12044
|
+
`[DashboardConfigController] Prepared publish package: ${filePath}`,
|
|
12045
|
+
);
|
|
12208
12046
|
|
|
12209
|
-
// Attempt to publish to registry
|
|
12210
|
-
let
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12047
|
+
// 10. Attempt to publish to registry if authenticated
|
|
12048
|
+
let registrySubmission = null;
|
|
12049
|
+
try {
|
|
12050
|
+
const { getAuthStatus } = registryAuthController$1;
|
|
12051
|
+
const { publishToRegistry } = registryApiController$2;
|
|
12052
|
+
const authStatus = getAuthStatus();
|
|
12053
|
+
|
|
12054
|
+
if (authStatus.authenticated) {
|
|
12055
|
+
console.log("[DashboardConfigController] Publishing to registry...");
|
|
12056
|
+
registrySubmission = await publishToRegistry(filePath, manifest);
|
|
12057
|
+
if (registrySubmission.success) {
|
|
12058
|
+
console.log(
|
|
12059
|
+
`[DashboardConfigController] Published to registry: ${registrySubmission.registryUrl}`,
|
|
12060
|
+
);
|
|
12061
|
+
} else {
|
|
12062
|
+
console.warn(
|
|
12063
|
+
`[DashboardConfigController] Registry publish failed: ${registrySubmission.error}`,
|
|
12064
|
+
);
|
|
12065
|
+
}
|
|
12066
|
+
} else {
|
|
12067
|
+
registrySubmission = { success: false, authRequired: true };
|
|
12068
|
+
}
|
|
12069
|
+
} catch (err) {
|
|
12070
|
+
console.warn(
|
|
12071
|
+
`[DashboardConfigController] Registry publish error: ${err.message}`,
|
|
12215
12072
|
);
|
|
12216
|
-
|
|
12073
|
+
registrySubmission = { success: false, error: err.message };
|
|
12217
12074
|
}
|
|
12218
12075
|
|
|
12219
12076
|
return {
|
|
12220
12077
|
success: true,
|
|
12221
|
-
manifest,
|
|
12222
12078
|
filePath,
|
|
12223
|
-
|
|
12079
|
+
manifest,
|
|
12080
|
+
config: dashboardConfig,
|
|
12081
|
+
warnings:
|
|
12082
|
+
missingFromRegistry.length > 0 ? missingFromRegistry : undefined,
|
|
12083
|
+
registryCheckFailed: registryCheckFailed || undefined,
|
|
12084
|
+
registrySubmission,
|
|
12085
|
+
};
|
|
12086
|
+
} catch (error) {
|
|
12087
|
+
console.error(
|
|
12088
|
+
"[DashboardConfigController] Error preparing dashboard for publish:",
|
|
12089
|
+
error,
|
|
12090
|
+
);
|
|
12091
|
+
return {
|
|
12092
|
+
success: false,
|
|
12093
|
+
error: error.message,
|
|
12224
12094
|
};
|
|
12225
|
-
} catch (err) {
|
|
12226
|
-
console.error("[ThemeRegistryController] Error preparing theme for publish:", err);
|
|
12227
|
-
return { success: false, error: err.message };
|
|
12228
12095
|
}
|
|
12229
12096
|
}
|
|
12230
12097
|
|
|
12231
12098
|
/**
|
|
12232
|
-
*
|
|
12233
|
-
*
|
|
12234
|
-
* Looks up the theme package, downloads the ZIP, extracts the .theme.json,
|
|
12235
|
-
* and saves it via themeController.
|
|
12099
|
+
* Get a full preview of a dashboard package from the registry.
|
|
12100
|
+
* Combines the structured preview data with a compatibility check.
|
|
12236
12101
|
*
|
|
12237
|
-
* @param {
|
|
12238
|
-
* @param {
|
|
12239
|
-
* @
|
|
12240
|
-
* @returns {Object} Result with success, themeKey, theme
|
|
12102
|
+
* @param {string} packageName - Registry package name
|
|
12103
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
12104
|
+
* @returns {Promise<Object>} Preview data with compatibility report
|
|
12241
12105
|
*/
|
|
12242
|
-
async function
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
}
|
|
12249
|
-
|
|
12250
|
-
// Resolve download URL
|
|
12251
|
-
let downloadUrl = pkg.downloadUrl;
|
|
12252
|
-
if (!downloadUrl) {
|
|
12253
|
-
return { success: false, error: "Package has no download URL" };
|
|
12254
|
-
}
|
|
12106
|
+
async function getDashboardPreview$1(packageName, widgetRegistry = null) {
|
|
12107
|
+
const {
|
|
12108
|
+
buildDashboardPreview,
|
|
12109
|
+
checkDashboardCompatibility,
|
|
12110
|
+
} = dashboardConfigUtils$1;
|
|
12111
|
+
const { getPackage, fetchRegistryIndex } = registryController$2;
|
|
12255
12112
|
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12113
|
+
const pkg = await getPackage(packageName);
|
|
12114
|
+
if (!pkg) {
|
|
12115
|
+
return {
|
|
12116
|
+
success: false,
|
|
12117
|
+
error: `Dashboard package not found: ${packageName}`,
|
|
12118
|
+
};
|
|
12119
|
+
}
|
|
12260
12120
|
|
|
12261
|
-
|
|
12262
|
-
if (!downloadUrl.startsWith("https://")) {
|
|
12263
|
-
return { success: false, error: "Download URL must use HTTPS" };
|
|
12264
|
-
}
|
|
12121
|
+
const preview = buildDashboardPreview(pkg);
|
|
12265
12122
|
|
|
12266
|
-
|
|
12123
|
+
// Get compatibility report
|
|
12124
|
+
const installedWidgets = widgetRegistry ? widgetRegistry.getWidgets() : [];
|
|
12125
|
+
let registryPackages = [];
|
|
12126
|
+
try {
|
|
12127
|
+
const index = await fetchRegistryIndex();
|
|
12128
|
+
registryPackages = index.packages || [];
|
|
12129
|
+
} catch (err) {
|
|
12130
|
+
// Non-fatal — preview still works without compatibility
|
|
12131
|
+
}
|
|
12267
12132
|
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
error: `Failed to download theme: ${response.status} ${response.statusText}`,
|
|
12274
|
-
};
|
|
12275
|
-
}
|
|
12133
|
+
const compatibility = checkDashboardCompatibility(
|
|
12134
|
+
pkg.widgets || [],
|
|
12135
|
+
installedWidgets,
|
|
12136
|
+
registryPackages,
|
|
12137
|
+
);
|
|
12276
12138
|
|
|
12277
|
-
|
|
12278
|
-
|
|
12139
|
+
return {
|
|
12140
|
+
success: true,
|
|
12141
|
+
preview,
|
|
12142
|
+
compatibility,
|
|
12143
|
+
};
|
|
12144
|
+
}
|
|
12279
12145
|
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12146
|
+
/**
|
|
12147
|
+
* Check installed dashboards for available updates.
|
|
12148
|
+
* Reads workspaces, finds those installed from the registry,
|
|
12149
|
+
* and compares versions against the current registry index.
|
|
12150
|
+
*
|
|
12151
|
+
* @param {string} appId - Application identifier
|
|
12152
|
+
* @returns {Promise<Object>} Result with updates array
|
|
12153
|
+
*/
|
|
12154
|
+
async function checkDashboardUpdatesForApp$1(appId) {
|
|
12155
|
+
const { checkDashboardUpdates } = dashboardConfigUtils$1;
|
|
12156
|
+
const { fetchRegistryIndex } = registryController$2;
|
|
12283
12157
|
|
|
12284
|
-
|
|
12285
|
-
|
|
12158
|
+
try {
|
|
12159
|
+
const filename = path$1.join(
|
|
12160
|
+
app$1.getPath("userData"),
|
|
12161
|
+
appName$1,
|
|
12162
|
+
appId,
|
|
12163
|
+
configFilename,
|
|
12286
12164
|
);
|
|
12165
|
+
const workspaces = getFileContents$1(filename) || [];
|
|
12287
12166
|
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
}
|
|
12291
|
-
|
|
12292
|
-
// Validate entry path (security: prevent path traversal)
|
|
12293
|
-
if (themeEntry.entryName.includes("..") || path$1.isAbsolute(themeEntry.entryName)) {
|
|
12294
|
-
return { success: false, error: "Invalid file path in ZIP" };
|
|
12295
|
-
}
|
|
12167
|
+
const index = await fetchRegistryIndex();
|
|
12168
|
+
const registryPackages = index.packages || [];
|
|
12296
12169
|
|
|
12297
|
-
|
|
12298
|
-
const themeJson = themeEntry.getData().toString("utf-8");
|
|
12299
|
-
let themeData;
|
|
12300
|
-
try {
|
|
12301
|
-
themeData = JSON.parse(themeJson);
|
|
12302
|
-
} catch (parseErr) {
|
|
12303
|
-
return { success: false, error: "Invalid JSON in theme file: " + parseErr.message };
|
|
12304
|
-
}
|
|
12170
|
+
const updates = checkDashboardUpdates(workspaces, registryPackages);
|
|
12305
12171
|
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12172
|
+
return {
|
|
12173
|
+
success: true,
|
|
12174
|
+
updates,
|
|
12175
|
+
totalInstalled: workspaces.filter(
|
|
12176
|
+
(w) => w._dashboardConfig?.registryPackage,
|
|
12177
|
+
).length,
|
|
12311
12178
|
};
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
// Save via themeController
|
|
12317
|
-
const saveResult = themeController$1.saveThemeForApplication(
|
|
12318
|
-
win,
|
|
12319
|
-
appId,
|
|
12320
|
-
themeKey,
|
|
12321
|
-
themeData,
|
|
12179
|
+
} catch (error) {
|
|
12180
|
+
console.error(
|
|
12181
|
+
"[DashboardConfigController] Error checking dashboard updates:",
|
|
12182
|
+
error,
|
|
12322
12183
|
);
|
|
12323
|
-
|
|
12324
|
-
if (saveResult.error) {
|
|
12325
|
-
return { success: false, error: "Failed to save theme: " + saveResult.message };
|
|
12326
|
-
}
|
|
12327
|
-
|
|
12328
|
-
console.log("[ThemeRegistryController] Theme installed:", themeKey);
|
|
12329
|
-
|
|
12330
12184
|
return {
|
|
12331
|
-
success:
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
themes: saveResult.themes,
|
|
12185
|
+
success: false,
|
|
12186
|
+
error: error.message,
|
|
12187
|
+
updates: [],
|
|
12335
12188
|
};
|
|
12189
|
+
}
|
|
12190
|
+
}
|
|
12191
|
+
|
|
12192
|
+
/**
|
|
12193
|
+
* Get a provider setup manifest for a dashboard's requirements.
|
|
12194
|
+
* Compares required providers against the user's configured providers.
|
|
12195
|
+
*
|
|
12196
|
+
* @param {string} appId - Application identifier
|
|
12197
|
+
* @param {Array} requiredProviders - Provider requirements from dashboard config
|
|
12198
|
+
* @returns {Object} Setup manifest with per-provider status
|
|
12199
|
+
*/
|
|
12200
|
+
function getProviderSetupManifest$1(appId, requiredProviders = []) {
|
|
12201
|
+
const {
|
|
12202
|
+
buildProviderSetupManifest,
|
|
12203
|
+
} = dashboardConfigUtils$1;
|
|
12204
|
+
const { listProviders } = requireProviderController();
|
|
12205
|
+
|
|
12206
|
+
let configuredProviders = [];
|
|
12207
|
+
try {
|
|
12208
|
+
configuredProviders = listProviders(null, appId) || [];
|
|
12336
12209
|
} catch (err) {
|
|
12337
|
-
console.
|
|
12338
|
-
|
|
12210
|
+
console.warn(
|
|
12211
|
+
"[DashboardConfigController] Could not list providers:",
|
|
12212
|
+
err.message,
|
|
12213
|
+
);
|
|
12339
12214
|
}
|
|
12215
|
+
|
|
12216
|
+
return buildProviderSetupManifest(requiredProviders, configuredProviders);
|
|
12340
12217
|
}
|
|
12341
12218
|
|
|
12342
12219
|
/**
|
|
12343
|
-
* Get a preview
|
|
12220
|
+
* Get a publish preview for a dashboard workspace.
|
|
12221
|
+
* Returns widget/layout info without creating a ZIP or uploading.
|
|
12344
12222
|
*
|
|
12345
12223
|
* @param {string} appId - Application identifier
|
|
12346
|
-
* @param {string}
|
|
12347
|
-
* @
|
|
12224
|
+
* @param {number|string} workspaceId - Workspace to preview
|
|
12225
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
12226
|
+
* @returns {Object} Preview with dashboardName, widgetCount, widgets, componentNames
|
|
12348
12227
|
*/
|
|
12349
|
-
function
|
|
12228
|
+
function getDashboardPublishPreview$1(appId, workspaceId, widgetRegistry = null) {
|
|
12350
12229
|
try {
|
|
12351
|
-
const
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12230
|
+
const filename = path$1.join(
|
|
12231
|
+
app$1.getPath("userData"),
|
|
12232
|
+
appName$1,
|
|
12233
|
+
appId,
|
|
12234
|
+
configFilename,
|
|
12235
|
+
);
|
|
12236
|
+
const workspacesArray = getFileContents$1(filename);
|
|
12237
|
+
const workspace = workspacesArray.find(
|
|
12238
|
+
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
12239
|
+
);
|
|
12355
12240
|
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
12241
|
+
if (!workspace) {
|
|
12242
|
+
return { success: false, error: `Workspace not found: ${workspaceId}` };
|
|
12359
12243
|
}
|
|
12360
12244
|
|
|
12361
|
-
const
|
|
12245
|
+
const layout = workspace.layout || [];
|
|
12246
|
+
const componentNames = collectComponentNames(layout);
|
|
12247
|
+
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
12362
12248
|
|
|
12363
12249
|
return {
|
|
12364
12250
|
success: true,
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12251
|
+
dashboardName: workspace.name || workspace.label || "Dashboard",
|
|
12252
|
+
widgetCount: componentNames.length,
|
|
12253
|
+
widgets: widgets.map((w) => ({ name: w.name, package: w.package })),
|
|
12254
|
+
componentNames: [...componentNames],
|
|
12368
12255
|
};
|
|
12369
|
-
} catch (
|
|
12370
|
-
console.error(
|
|
12371
|
-
|
|
12256
|
+
} catch (error) {
|
|
12257
|
+
console.error(
|
|
12258
|
+
"[DashboardConfigController] Error getting publish preview:",
|
|
12259
|
+
error,
|
|
12260
|
+
);
|
|
12261
|
+
return { success: false, error: error.message };
|
|
12372
12262
|
}
|
|
12373
12263
|
}
|
|
12374
12264
|
|
|
12375
|
-
var
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12265
|
+
var dashboardConfigController$1 = {
|
|
12266
|
+
exportDashboardConfig: exportDashboardConfig$1,
|
|
12267
|
+
selectDashboardFile: selectDashboardFile$1,
|
|
12268
|
+
importDashboardConfig: importDashboardConfig$1,
|
|
12269
|
+
installDashboardFromRegistry: installDashboardFromRegistry$1,
|
|
12270
|
+
checkCompatibility: checkCompatibility$1,
|
|
12271
|
+
prepareDashboardForPublish: prepareDashboardForPublish$1,
|
|
12272
|
+
getDashboardPreview: getDashboardPreview$1,
|
|
12273
|
+
checkDashboardUpdatesForApp: checkDashboardUpdatesForApp$1,
|
|
12274
|
+
getProviderSetupManifest: getProviderSetupManifest$1,
|
|
12275
|
+
getDashboardPublishPreview: getDashboardPublishPreview$1,
|
|
12381
12276
|
};
|
|
12382
12277
|
|
|
12278
|
+
/**
|
|
12279
|
+
* notificationController.js
|
|
12280
|
+
*
|
|
12281
|
+
* Main process controller for OS-level notifications.
|
|
12282
|
+
* Manages preferences (electron-store), rate limiting, deduplication,
|
|
12283
|
+
* and dispatching native Notification instances.
|
|
12284
|
+
*/
|
|
12285
|
+
|
|
12286
|
+
const { Notification } = require$$0$1;
|
|
12287
|
+
const Store = require$$1;
|
|
12288
|
+
|
|
12289
|
+
const store$1 = new Store({ name: "dash-notifications" });
|
|
12290
|
+
|
|
12291
|
+
// --- Rate limiting ---
|
|
12292
|
+
// Sliding window: max 10 notifications per 60s per widget
|
|
12293
|
+
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
12294
|
+
const RATE_LIMIT_MAX = 10;
|
|
12295
|
+
const rateBuckets = new Map(); // widgetId -> [timestamp, ...]
|
|
12296
|
+
|
|
12297
|
+
// --- Deduplication ---
|
|
12298
|
+
// Same (widgetName, type, title, body) within 5s is dropped
|
|
12299
|
+
const DEDUP_WINDOW_MS = 5_000;
|
|
12300
|
+
const recentNotifications = new Map(); // dedup key -> timestamp
|
|
12301
|
+
|
|
12302
|
+
function getDedupKey(payload) {
|
|
12303
|
+
return `${payload.widgetName}:${payload.type}:${payload.title}:${payload.body}`;
|
|
12304
|
+
}
|
|
12305
|
+
|
|
12306
|
+
function isRateLimited(widgetId) {
|
|
12307
|
+
const now = Date.now();
|
|
12308
|
+
let timestamps = rateBuckets.get(widgetId) || [];
|
|
12309
|
+
// Prune old entries
|
|
12310
|
+
timestamps = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
12311
|
+
rateBuckets.set(widgetId, timestamps);
|
|
12312
|
+
return timestamps.length >= RATE_LIMIT_MAX;
|
|
12313
|
+
}
|
|
12314
|
+
|
|
12315
|
+
function recordNotification(widgetId) {
|
|
12316
|
+
const timestamps = rateBuckets.get(widgetId) || [];
|
|
12317
|
+
timestamps.push(Date.now());
|
|
12318
|
+
rateBuckets.set(widgetId, timestamps);
|
|
12319
|
+
}
|
|
12320
|
+
|
|
12321
|
+
function isDuplicate(payload) {
|
|
12322
|
+
const key = getDedupKey(payload);
|
|
12323
|
+
const now = Date.now();
|
|
12324
|
+
const lastSent = recentNotifications.get(key);
|
|
12325
|
+
if (lastSent && now - lastSent < DEDUP_WINDOW_MS) {
|
|
12326
|
+
return true;
|
|
12327
|
+
}
|
|
12328
|
+
recentNotifications.set(key, now);
|
|
12329
|
+
return false;
|
|
12330
|
+
}
|
|
12331
|
+
|
|
12332
|
+
// Clean up stale dedup entries periodically
|
|
12333
|
+
setInterval(() => {
|
|
12334
|
+
const now = Date.now();
|
|
12335
|
+
for (const [key, ts] of recentNotifications) {
|
|
12336
|
+
if (now - ts > DEDUP_WINDOW_MS) {
|
|
12337
|
+
recentNotifications.delete(key);
|
|
12338
|
+
}
|
|
12339
|
+
}
|
|
12340
|
+
}, 30_000);
|
|
12341
|
+
|
|
12342
|
+
const notificationController$2 = {
|
|
12343
|
+
/**
|
|
12344
|
+
* Send a notification if it passes all checks.
|
|
12345
|
+
*
|
|
12346
|
+
* @param {BrowserWindow} win - the main window (for click routing)
|
|
12347
|
+
* @param {Object} payload
|
|
12348
|
+
* @param {string} payload.widgetName - component name of the widget
|
|
12349
|
+
* @param {string} payload.widgetId - widget instance UUID
|
|
12350
|
+
* @param {string} payload.workspaceId - workspace containing the widget
|
|
12351
|
+
* @param {string} payload.type - notification type key (must match .dash.js config)
|
|
12352
|
+
* @param {string} payload.title - notification title
|
|
12353
|
+
* @param {string} payload.body - notification body
|
|
12354
|
+
* @param {Object} [payload.data] - arbitrary data returned on click
|
|
12355
|
+
* @param {boolean} [payload.silent] - suppress sound
|
|
12356
|
+
* @param {string} [payload.urgency] - "low" | "normal" | "critical"
|
|
12357
|
+
* @returns {{ success: boolean, reason?: string }}
|
|
12358
|
+
*/
|
|
12359
|
+
send: (win, payload) => {
|
|
12360
|
+
try {
|
|
12361
|
+
const {
|
|
12362
|
+
widgetName,
|
|
12363
|
+
widgetId,
|
|
12364
|
+
workspaceId,
|
|
12365
|
+
type,
|
|
12366
|
+
title,
|
|
12367
|
+
body,
|
|
12368
|
+
data,
|
|
12369
|
+
silent,
|
|
12370
|
+
urgency,
|
|
12371
|
+
} = payload;
|
|
12372
|
+
|
|
12373
|
+
// 1. Check globalEnabled
|
|
12374
|
+
const globalEnabled = store$1.get("globalEnabled", true);
|
|
12375
|
+
if (!globalEnabled) {
|
|
12376
|
+
return { success: false, reason: "notifications_disabled" };
|
|
12377
|
+
}
|
|
12378
|
+
|
|
12379
|
+
// 2. Check doNotDisturb
|
|
12380
|
+
const dnd = store$1.get("doNotDisturb", false);
|
|
12381
|
+
if (dnd) {
|
|
12382
|
+
return { success: false, reason: "do_not_disturb" };
|
|
12383
|
+
}
|
|
12384
|
+
|
|
12385
|
+
// 3. Check per-widget per-type preference
|
|
12386
|
+
const instances = store$1.get("instances", {});
|
|
12387
|
+
const widgetPrefs = instances[widgetId];
|
|
12388
|
+
if (widgetPrefs && widgetPrefs[type] === false) {
|
|
12389
|
+
return { success: false, reason: "type_disabled" };
|
|
12390
|
+
}
|
|
12391
|
+
|
|
12392
|
+
// 4. Rate limit
|
|
12393
|
+
if (isRateLimited(widgetId)) {
|
|
12394
|
+
return { success: false, reason: "rate_limited" };
|
|
12395
|
+
}
|
|
12396
|
+
|
|
12397
|
+
// 5. Deduplication
|
|
12398
|
+
if (isDuplicate(payload)) {
|
|
12399
|
+
return { success: false, reason: "duplicate" };
|
|
12400
|
+
}
|
|
12401
|
+
|
|
12402
|
+
// 6. All checks passed — send native notification
|
|
12403
|
+
const notification = new Notification({
|
|
12404
|
+
title,
|
|
12405
|
+
body,
|
|
12406
|
+
silent: silent || false,
|
|
12407
|
+
urgency: urgency || "normal",
|
|
12408
|
+
});
|
|
12409
|
+
|
|
12410
|
+
notification.on("click", () => {
|
|
12411
|
+
if (win && !win.isDestroyed()) {
|
|
12412
|
+
win.show();
|
|
12413
|
+
win.focus();
|
|
12414
|
+
win.webContents.send("notification:clicked", {
|
|
12415
|
+
widgetName,
|
|
12416
|
+
widgetId,
|
|
12417
|
+
workspaceId,
|
|
12418
|
+
type,
|
|
12419
|
+
data: data || null,
|
|
12420
|
+
});
|
|
12421
|
+
}
|
|
12422
|
+
});
|
|
12423
|
+
|
|
12424
|
+
notification.show();
|
|
12425
|
+
recordNotification(widgetId);
|
|
12426
|
+
|
|
12427
|
+
console.log(
|
|
12428
|
+
`[notificationController] Sent: ${widgetName}.${type} — "${title}"`,
|
|
12429
|
+
);
|
|
12430
|
+
|
|
12431
|
+
return { success: true };
|
|
12432
|
+
} catch (error) {
|
|
12433
|
+
console.error(
|
|
12434
|
+
"[notificationController] Error sending notification:",
|
|
12435
|
+
error,
|
|
12436
|
+
);
|
|
12437
|
+
return { error: true, message: error.message };
|
|
12438
|
+
}
|
|
12439
|
+
},
|
|
12440
|
+
|
|
12441
|
+
/**
|
|
12442
|
+
* Get all notification preferences.
|
|
12443
|
+
*
|
|
12444
|
+
* @returns {{ globalEnabled: boolean, doNotDisturb: boolean, instances: Object }}
|
|
12445
|
+
*/
|
|
12446
|
+
getPreferences: () => {
|
|
12447
|
+
try {
|
|
12448
|
+
return {
|
|
12449
|
+
globalEnabled: store$1.get("globalEnabled", true),
|
|
12450
|
+
doNotDisturb: store$1.get("doNotDisturb", false),
|
|
12451
|
+
instances: store$1.get("instances", {}),
|
|
12452
|
+
};
|
|
12453
|
+
} catch (error) {
|
|
12454
|
+
console.error(
|
|
12455
|
+
"[notificationController] Error getting preferences:",
|
|
12456
|
+
error,
|
|
12457
|
+
);
|
|
12458
|
+
return {
|
|
12459
|
+
error: true,
|
|
12460
|
+
message: error.message,
|
|
12461
|
+
globalEnabled: true,
|
|
12462
|
+
doNotDisturb: false,
|
|
12463
|
+
instances: {},
|
|
12464
|
+
};
|
|
12465
|
+
}
|
|
12466
|
+
},
|
|
12467
|
+
|
|
12468
|
+
/**
|
|
12469
|
+
* Set notification preferences for a specific widget instance.
|
|
12470
|
+
*
|
|
12471
|
+
* @param {string} widgetId - widget instance UUID
|
|
12472
|
+
* @param {Object} prefs - { [notificationType]: boolean }
|
|
12473
|
+
* @returns {{ success: boolean }}
|
|
12474
|
+
*/
|
|
12475
|
+
setPreferences: (widgetId, prefs) => {
|
|
12476
|
+
try {
|
|
12477
|
+
const instances = store$1.get("instances", {});
|
|
12478
|
+
instances[widgetId] = { ...(instances[widgetId] || {}), ...prefs };
|
|
12479
|
+
store$1.set("instances", instances);
|
|
12480
|
+
console.log(
|
|
12481
|
+
`[notificationController] Preferences updated for widget ${widgetId}`,
|
|
12482
|
+
);
|
|
12483
|
+
return { success: true };
|
|
12484
|
+
} catch (error) {
|
|
12485
|
+
console.error(
|
|
12486
|
+
"[notificationController] Error setting preferences:",
|
|
12487
|
+
error,
|
|
12488
|
+
);
|
|
12489
|
+
return { error: true, message: error.message };
|
|
12490
|
+
}
|
|
12491
|
+
},
|
|
12492
|
+
|
|
12493
|
+
/**
|
|
12494
|
+
* Set global notification settings.
|
|
12495
|
+
*
|
|
12496
|
+
* @param {Object} settings
|
|
12497
|
+
* @param {boolean} [settings.globalEnabled]
|
|
12498
|
+
* @param {boolean} [settings.doNotDisturb]
|
|
12499
|
+
* @returns {{ success: boolean }}
|
|
12500
|
+
*/
|
|
12501
|
+
setGlobal: (settings) => {
|
|
12502
|
+
try {
|
|
12503
|
+
if (typeof settings.globalEnabled === "boolean") {
|
|
12504
|
+
store$1.set("globalEnabled", settings.globalEnabled);
|
|
12505
|
+
}
|
|
12506
|
+
if (typeof settings.doNotDisturb === "boolean") {
|
|
12507
|
+
store$1.set("doNotDisturb", settings.doNotDisturb);
|
|
12508
|
+
}
|
|
12509
|
+
console.log(
|
|
12510
|
+
"[notificationController] Global settings updated:",
|
|
12511
|
+
settings,
|
|
12512
|
+
);
|
|
12513
|
+
return { success: true };
|
|
12514
|
+
} catch (error) {
|
|
12515
|
+
console.error("[notificationController] Error setting global:", error);
|
|
12516
|
+
return { error: true, message: error.message };
|
|
12517
|
+
}
|
|
12518
|
+
},
|
|
12519
|
+
};
|
|
12520
|
+
|
|
12521
|
+
var notificationController_1 = notificationController$2;
|
|
12522
|
+
|
|
12383
12523
|
/**
|
|
12384
12524
|
* clientFactories.js
|
|
12385
12525
|
*
|