@trops/dash-core 0.1.132 → 0.1.134

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