@trops/dash-core 0.1.132 → 0.1.133

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