appclean 1.9.0 → 2.0.2

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.
Files changed (212) hide show
  1. package/.github/workflows/npm-publish.yml +61 -0
  2. package/DEVELOPMENT.md +84 -0
  3. package/GUI_IMPLEMENTATION_STATUS.md +143 -0
  4. package/MD_Files/INDEX.md +51 -0
  5. package/PHASE2_COMPLETION.md +281 -0
  6. package/PHASE3_COMPLETION.md +364 -0
  7. package/README.md +411 -91
  8. package/RELEASE_GUIDE.md +236 -0
  9. package/assets/logo.svg +34 -0
  10. package/dist/core/appUpdateChecker.js +12 -16
  11. package/dist/core/appUpdateChecker.js.map +1 -1
  12. package/dist/core/detector.js +14 -18
  13. package/dist/core/detector.js.map +1 -1
  14. package/dist/core/duplicateFileFinder.js +12 -19
  15. package/dist/core/duplicateFileFinder.js.map +1 -1
  16. package/dist/core/orphanedDependencyDetector.js +19 -26
  17. package/dist/core/orphanedDependencyDetector.js.map +1 -1
  18. package/dist/core/performanceOptimizer.js +6 -10
  19. package/dist/core/performanceOptimizer.js.map +1 -1
  20. package/dist/core/permissionHandler.js +21 -25
  21. package/dist/core/permissionHandler.js.map +1 -1
  22. package/dist/core/pluginSystem.js +9 -13
  23. package/dist/core/pluginSystem.js.map +1 -1
  24. package/dist/core/removalRecorder.js +12 -19
  25. package/dist/core/removalRecorder.js.map +1 -1
  26. package/dist/core/remover.js +59 -66
  27. package/dist/core/remover.js.map +1 -1
  28. package/dist/core/reportGenerator.d.ts +1 -1
  29. package/dist/core/reportGenerator.d.ts.map +1 -1
  30. package/dist/core/reportGenerator.js +27 -34
  31. package/dist/core/reportGenerator.js.map +1 -1
  32. package/dist/core/scheduledCleanup.js +23 -30
  33. package/dist/core/scheduledCleanup.js.map +1 -1
  34. package/dist/core/serviceFileDetector.js +24 -31
  35. package/dist/core/serviceFileDetector.js.map +1 -1
  36. package/dist/core/verificationModule.js +10 -14
  37. package/dist/core/verificationModule.js.map +1 -1
  38. package/dist/index.js +118 -156
  39. package/dist/index.js.map +1 -1
  40. package/dist/managers/brewManager.js +30 -37
  41. package/dist/managers/brewManager.js.map +1 -1
  42. package/dist/managers/customManager.js +23 -30
  43. package/dist/managers/customManager.js.map +1 -1
  44. package/dist/managers/linuxManager.js +29 -36
  45. package/dist/managers/linuxManager.js.map +1 -1
  46. package/dist/managers/npmManager.js +27 -34
  47. package/dist/managers/npmManager.js.map +1 -1
  48. package/dist/types/index.js +1 -2
  49. package/dist/ui/client/api/client.d.ts +24 -0
  50. package/dist/ui/client/api/client.d.ts.map +1 -0
  51. package/dist/ui/client/api/client.js +100 -0
  52. package/dist/ui/client/api/client.js.map +1 -0
  53. package/dist/ui/client/app.d.ts +7 -0
  54. package/dist/ui/client/app.d.ts.map +1 -0
  55. package/dist/ui/client/app.js +75 -0
  56. package/dist/ui/client/app.js.map +1 -0
  57. package/dist/ui/client/index.html +107 -0
  58. package/dist/ui/client/pages/appDetails.d.ts +8 -0
  59. package/dist/ui/client/pages/appDetails.d.ts.map +1 -0
  60. package/dist/ui/client/pages/appDetails.js +287 -0
  61. package/dist/ui/client/pages/appDetails.js.map +1 -0
  62. package/dist/ui/client/pages/appSearch.d.ts +2 -0
  63. package/dist/ui/client/pages/appSearch.d.ts.map +1 -0
  64. package/dist/ui/client/pages/appSearch.js +221 -0
  65. package/dist/ui/client/pages/appSearch.js.map +1 -0
  66. package/dist/ui/client/pages/dashboard.d.ts +2 -0
  67. package/dist/ui/client/pages/dashboard.d.ts.map +1 -0
  68. package/dist/ui/client/pages/dashboard.js +175 -0
  69. package/dist/ui/client/pages/dashboard.js.map +1 -0
  70. package/dist/ui/client/pages/settings.d.ts +7 -0
  71. package/dist/ui/client/pages/settings.d.ts.map +1 -0
  72. package/dist/ui/client/pages/settings.js +279 -0
  73. package/dist/ui/client/pages/settings.js.map +1 -0
  74. package/dist/ui/client/state/appStore.d.ts +38 -0
  75. package/dist/ui/client/state/appStore.d.ts.map +1 -0
  76. package/dist/ui/client/state/appStore.js +130 -0
  77. package/dist/ui/client/state/appStore.js.map +1 -0
  78. package/dist/ui/client/state/dashboardStore.d.ts +31 -0
  79. package/dist/ui/client/state/dashboardStore.d.ts.map +1 -0
  80. package/dist/ui/client/state/dashboardStore.js +76 -0
  81. package/dist/ui/client/state/dashboardStore.js.map +1 -0
  82. package/dist/ui/client/state/uiStore.d.ts +43 -0
  83. package/dist/ui/client/state/uiStore.d.ts.map +1 -0
  84. package/dist/ui/client/state/uiStore.js +109 -0
  85. package/dist/ui/client/state/uiStore.js.map +1 -0
  86. package/dist/ui/client/styles/animations.css +349 -0
  87. package/dist/ui/client/styles/base.css +214 -0
  88. package/dist/ui/client/styles/components.css +400 -0
  89. package/dist/ui/client/styles/layout.css +224 -0
  90. package/dist/ui/client/styles/variables.css +140 -0
  91. package/dist/ui/client/utils/events.d.ts +19 -0
  92. package/dist/ui/client/utils/events.d.ts.map +1 -0
  93. package/dist/ui/client/utils/events.js +54 -0
  94. package/dist/ui/client/utils/events.js.map +1 -0
  95. package/dist/ui/client/utils/formatting.d.ts +11 -0
  96. package/dist/ui/client/utils/formatting.d.ts.map +1 -0
  97. package/dist/ui/client/utils/formatting.js +104 -0
  98. package/dist/ui/client/utils/formatting.js.map +1 -0
  99. package/dist/ui/client/utils/router.d.ts +25 -0
  100. package/dist/ui/client/utils/router.d.ts.map +1 -0
  101. package/dist/ui/client/utils/router.js +90 -0
  102. package/dist/ui/client/utils/router.js.map +1 -0
  103. package/dist/ui/guiServer.d.ts +11 -5
  104. package/dist/ui/guiServer.d.ts.map +1 -1
  105. package/dist/ui/guiServer.js +180 -501
  106. package/dist/ui/guiServer.js.map +1 -1
  107. package/dist/ui/menu.js +18 -27
  108. package/dist/ui/menu.js.map +1 -1
  109. package/dist/ui/prompts.js +34 -47
  110. package/dist/ui/prompts.js.map +1 -1
  111. package/dist/ui/server/middleware/errorHandler.d.ts +19 -0
  112. package/dist/ui/server/middleware/errorHandler.d.ts.map +1 -0
  113. package/dist/ui/server/middleware/errorHandler.js +100 -0
  114. package/dist/ui/server/middleware/errorHandler.js.map +1 -0
  115. package/dist/ui/server/routes/apps.d.ts +8 -0
  116. package/dist/ui/server/routes/apps.d.ts.map +1 -0
  117. package/dist/ui/server/routes/apps.js +74 -0
  118. package/dist/ui/server/routes/apps.js.map +1 -0
  119. package/dist/ui/server/routes/dashboard.d.ts +4 -0
  120. package/dist/ui/server/routes/dashboard.d.ts.map +1 -0
  121. package/dist/ui/server/routes/dashboard.js +57 -0
  122. package/dist/ui/server/routes/dashboard.js.map +1 -0
  123. package/dist/ui/server/routes/settings.d.ts +6 -0
  124. package/dist/ui/server/routes/settings.d.ts.map +1 -0
  125. package/dist/ui/server/routes/settings.js +31 -0
  126. package/dist/ui/server/routes/settings.js.map +1 -0
  127. package/dist/ui/server/services/appService.d.ts +45 -0
  128. package/dist/ui/server/services/appService.d.ts.map +1 -0
  129. package/dist/ui/server/services/appService.js +114 -0
  130. package/dist/ui/server/services/appService.js.map +1 -0
  131. package/dist/ui/server/services/removalService.d.ts +24 -0
  132. package/dist/ui/server/services/removalService.d.ts.map +1 -0
  133. package/dist/ui/server/services/removalService.js +83 -0
  134. package/dist/ui/server/services/removalService.js.map +1 -0
  135. package/dist/utils/filesystem.js +32 -49
  136. package/dist/utils/filesystem.js.map +1 -1
  137. package/dist/utils/logger.js +9 -18
  138. package/dist/utils/logger.js.map +1 -1
  139. package/dist/utils/platform.js +10 -22
  140. package/dist/utils/platform.js.map +1 -1
  141. package/dist/utils/upgrade.d.ts +2 -1
  142. package/dist/utils/upgrade.d.ts.map +1 -1
  143. package/dist/utils/upgrade.js +24 -15
  144. package/dist/utils/upgrade.js.map +1 -1
  145. package/package.json +4 -2
  146. package/scripts/publish-npm.sh +64 -0
  147. package/src/core/appUpdateChecker.ts +1 -1
  148. package/src/core/detector.ts +6 -6
  149. package/src/core/duplicateFileFinder.ts +1 -1
  150. package/src/core/orphanedDependencyDetector.ts +2 -2
  151. package/src/core/performanceOptimizer.ts +1 -1
  152. package/src/core/permissionHandler.ts +2 -2
  153. package/src/core/pluginSystem.ts +1 -1
  154. package/src/core/removalRecorder.ts +2 -2
  155. package/src/core/remover.ts +11 -11
  156. package/src/core/reportGenerator.ts +2 -2
  157. package/src/core/scheduledCleanup.ts +2 -2
  158. package/src/core/serviceFileDetector.ts +2 -2
  159. package/src/core/verificationModule.ts +2 -2
  160. package/src/index.ts +8 -8
  161. package/src/managers/brewManager.ts +3 -3
  162. package/src/managers/customManager.ts +2 -2
  163. package/src/managers/linuxManager.ts +3 -3
  164. package/src/managers/npmManager.ts +3 -3
  165. package/src/ui/client/api/client.ts +168 -0
  166. package/src/ui/client/app.ts +125 -0
  167. package/src/ui/client/index.html +107 -0
  168. package/src/ui/client/pages/appDetails.ts +356 -0
  169. package/src/ui/client/pages/appSearch.ts +283 -0
  170. package/src/ui/client/pages/dashboard.ts +211 -0
  171. package/src/ui/client/pages/settings.ts +342 -0
  172. package/src/ui/client/state/appStore.ts +181 -0
  173. package/src/ui/client/state/dashboardStore.ts +123 -0
  174. package/src/ui/client/state/uiStore.ts +166 -0
  175. package/src/ui/client/styles/animations.css +349 -0
  176. package/src/ui/client/styles/base.css +214 -0
  177. package/src/ui/client/styles/components.css +400 -0
  178. package/src/ui/client/styles/layout.css +224 -0
  179. package/src/ui/client/styles/variables.css +140 -0
  180. package/src/ui/client/utils/events.ts +74 -0
  181. package/src/ui/client/utils/formatting.ts +157 -0
  182. package/src/ui/client/utils/router.ts +161 -0
  183. package/src/ui/guiServer.ts +245 -498
  184. package/src/ui/prompts.ts +1 -1
  185. package/src/ui/server/middleware/errorHandler.ts +174 -0
  186. package/src/ui/server/routes/apps.ts +132 -0
  187. package/src/ui/server/routes/dashboard.ts +93 -0
  188. package/src/ui/server/routes/settings.ts +63 -0
  189. package/src/ui/server/services/appService.ts +184 -0
  190. package/src/ui/server/services/removalService.ts +138 -0
  191. package/src/utils/upgrade.ts +19 -2
  192. package/tsconfig.json +3 -2
  193. package/INDEX.md +0 -165
  194. /package/{ACTION_CHECKLIST.md → MD_Files/ACTION_CHECKLIST.md} +0 -0
  195. /package/{APPCLEAN_SUMMARY.md → MD_Files/APPCLEAN_SUMMARY.md} +0 -0
  196. /package/{CHANGELOG.md → MD_Files/CHANGELOG.md} +0 -0
  197. /package/{CODE_OF_CONDUCT.md → MD_Files/CODE_OF_CONDUCT.md} +0 -0
  198. /package/{CODE_REVIEW_REPORT.md → MD_Files/CODE_REVIEW_REPORT.md} +0 -0
  199. /package/{COMMUNITY_POSTS.md → MD_Files/COMMUNITY_POSTS.md} +0 -0
  200. /package/{DEPLOYMENT_GUIDE.md → MD_Files/DEPLOYMENT_GUIDE.md} +0 -0
  201. /package/{DEPLOYMENT_STATUS.md → MD_Files/DEPLOYMENT_STATUS.md} +0 -0
  202. /package/{EXECUTIVE_REPORT.md → MD_Files/EXECUTIVE_REPORT.md} +0 -0
  203. /package/{GITHUB_OPTIMIZATION.md → MD_Files/GITHUB_OPTIMIZATION.md} +0 -0
  204. /package/{MARKETING_SUMMARY.md → MD_Files/MARKETING_SUMMARY.md} +0 -0
  205. /package/{NPM_PACKAGE_OPTIMIZATION.md → MD_Files/NPM_PACKAGE_OPTIMIZATION.md} +0 -0
  206. /package/{NPM_PUBLISH.md → MD_Files/NPM_PUBLISH.md} +0 -0
  207. /package/{PROJECT_SUMMARY.txt → MD_Files/PROJECT_SUMMARY.txt} +0 -0
  208. /package/{PUBLICATION_SUCCESS_REPORT.md → MD_Files/PUBLICATION_SUCCESS_REPORT.md} +0 -0
  209. /package/{QUICKSTART.md → MD_Files/QUICKSTART.md} +0 -0
  210. /package/{SETUP_GITHUB.md → MD_Files/SETUP_GITHUB.md} +0 -0
  211. /package/{TESTING_SUMMARY.md → MD_Files/TESTING_SUMMARY.md} +0 -0
  212. /package/{setup-github.sh → MD_Files/setup-github.sh} +0 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * AppClean GUI - Main SPA Controller
3
+ * Initializes router, stores, and pages
4
+ */
5
+
6
+ import { router } from './utils/router.js';
7
+ import { appStore } from './state/appStore.js';
8
+ import { dashboardStore } from './state/dashboardStore.js';
9
+ import { uiStore } from './state/uiStore.js';
10
+
11
+ // Import page modules
12
+ import { renderDashboard } from './pages/dashboard.js';
13
+ import { renderAppSearch } from './pages/appSearch.js';
14
+ import { renderAppDetails } from './pages/appDetails.js';
15
+ import { renderSettings } from './pages/settings.js';
16
+
17
+ /**
18
+ * Initialize the SPA
19
+ */
20
+ export async function initApp(): Promise<void> {
21
+ console.log('🧹 AppClean GUI v1.0.0 initializing...');
22
+
23
+ // Register routes first
24
+ registerRoutes();
25
+
26
+ // Setup UI listeners
27
+ setupUIListeners();
28
+
29
+ // Initialize stores with data
30
+ await initializeStores();
31
+
32
+ // Navigate to initial route
33
+ router.navigate('');
34
+
35
+ console.log('✓ AppClean GUI ready');
36
+ }
37
+
38
+ /**
39
+ * Register all routes
40
+ */
41
+ function registerRoutes(): void {
42
+ router.register('', () => {
43
+ uiStore.navigateTo('dashboard');
44
+ renderDashboard();
45
+ }, 'AppClean - Dashboard');
46
+
47
+ router.register('apps', () => {
48
+ uiStore.navigateTo('apps');
49
+ renderAppSearch();
50
+ }, 'AppClean - Apps');
51
+
52
+ router.register('apps/:appName', (params) => {
53
+ uiStore.navigateTo('app-details');
54
+ renderAppDetails(params.appName);
55
+ }, 'AppClean - App Details');
56
+
57
+ router.register('settings', () => {
58
+ uiStore.navigateTo('settings');
59
+ renderSettings();
60
+ }, 'AppClean - Settings');
61
+ }
62
+
63
+ /**
64
+ * Initialize stores with data
65
+ */
66
+ async function initializeStores(): Promise<void> {
67
+ try {
68
+ // Load dashboard stats (required for initial render)
69
+ await dashboardStore.loadStats();
70
+
71
+ // Load app list in background (don't block on this)
72
+ appStore.loadApps().catch((error) => {
73
+ console.error('Failed to load apps:', error);
74
+ });
75
+ } catch (error) {
76
+ console.error('Failed to initialize stores:', error);
77
+ uiStore.showError('Failed to load application data');
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Setup UI event listeners
83
+ */
84
+ function setupUIListeners(): void {
85
+ // Subscribe to UI store changes
86
+ uiStore.subscribe((state) => {
87
+ console.log('UI State changed:', state.currentView);
88
+ });
89
+
90
+ // Subscribe to app store changes
91
+ appStore.subscribe((state) => {
92
+ if (state && state.apps) {
93
+ console.log(`Apps loaded: ${state.apps.length}`);
94
+ }
95
+ });
96
+
97
+ // Subscribe to dashboard store changes
98
+ dashboardStore.subscribe((state) => {
99
+ if (state.stats) {
100
+ console.log(`Dashboard stats: ${state.stats.totalApps} apps, ${state.stats.totalSpaceUsed} bytes`);
101
+ }
102
+ });
103
+
104
+ // Handle route changes
105
+ router.onchange((route) => {
106
+ console.log('Navigated to:', route.path);
107
+ });
108
+
109
+ // Theme toggle listener
110
+ document.addEventListener('theme-toggle', () => {
111
+ uiStore.toggleTheme();
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Export for consumption in HTML
117
+ */
118
+ export { appStore, dashboardStore, uiStore, router };
119
+
120
+ // Initialize on DOM ready
121
+ if (document.readyState === 'loading') {
122
+ document.addEventListener('DOMContentLoaded', () => initApp().catch(e => console.error('Init error:', e)));
123
+ } else {
124
+ initApp().catch(e => console.error('Init error:', e));
125
+ }
@@ -0,0 +1,107 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="AppClean - Intelligently remove applications and all their hidden files">
7
+ <meta name="theme-color" content="#3b82f6">
8
+ <title>AppClean - Application Remover</title>
9
+
10
+ <!-- Styles -->
11
+ <link rel="stylesheet" href="/static/styles/variables.css">
12
+ <link rel="stylesheet" href="/static/styles/base.css">
13
+ <link rel="stylesheet" href="/static/styles/layout.css">
14
+ <link rel="stylesheet" href="/static/styles/components.css">
15
+ <link rel="stylesheet" href="/static/styles/animations.css">
16
+ </head>
17
+ <body>
18
+ <!-- Root container for SPA -->
19
+ <div id="app" class="app-root">
20
+ <!-- Navbar -->
21
+ <nav class="navbar" id="navbar">
22
+ <div class="container-xl">
23
+ <div class="flex-between">
24
+ <div class="flex items-center gap-4">
25
+ <h1 class="navbar-brand" id="navbar-brand">🧹 AppClean</h1>
26
+ <button class="btn btn-ghost navbar-menu-toggle" id="navbar-menu-toggle" aria-label="Toggle navigation">
27
+ <span class="hamburger">☰</span>
28
+ </button>
29
+ </div>
30
+ <div class="flex items-center gap-2">
31
+ <button class="btn btn-ghost btn-sm" id="theme-toggle" aria-label="Toggle dark mode">
32
+ <span class="theme-icon">🌙</span>
33
+ </button>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </nav>
38
+
39
+ <!-- Main layout -->
40
+ <div class="app-layout flex">
41
+ <!-- Sidebar -->
42
+ <aside class="sidebar" id="sidebar">
43
+ <div class="sidebar-nav">
44
+ <nav class="nav-links">
45
+ <a href="#/" class="nav-link active" data-view="dashboard">
46
+ <span class="nav-icon">📊</span>
47
+ <span class="nav-label">Dashboard</span>
48
+ </a>
49
+ <a href="#/apps" class="nav-link" data-view="apps">
50
+ <span class="nav-icon">📦</span>
51
+ <span class="nav-label">Apps</span>
52
+ </a>
53
+ <a href="#/settings" class="nav-link" data-view="settings">
54
+ <span class="nav-icon">⚙️</span>
55
+ <span class="nav-label">Settings</span>
56
+ </a>
57
+ </nav>
58
+ </div>
59
+ <div class="sidebar-footer">
60
+ <a href="https://github.com/praveenkay/AppClean" target="_blank" rel="noopener" class="sidebar-link">
61
+ <span class="nav-icon">💬</span>
62
+ <span class="nav-label">GitHub</span>
63
+ </a>
64
+ </div>
65
+ </aside>
66
+
67
+ <!-- Main content -->
68
+ <main class="main-content" id="main-content">
69
+ <div class="container-xl">
70
+ <!-- Pages will be rendered here -->
71
+ <div id="page-container" class="page-container">
72
+ <!-- Loading skeleton -->
73
+ <div class="loading-state" id="loading-state">
74
+ <div class="spinner-lg spinner"></div>
75
+ <p class="text-center text-muted mt-4">Loading AppClean...</p>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </main>
80
+ </div>
81
+
82
+ <!-- Modals -->
83
+ <div class="modal-backdrop" id="modal-backdrop">
84
+ <div class="modal" id="modal" role="dialog" aria-labelledby="modal-title">
85
+ <div class="modal-header">
86
+ <h2 class="modal-title" id="modal-title">Dialog</h2>
87
+ <button class="modal-close" id="modal-close" aria-label="Close dialog">&times;</button>
88
+ </div>
89
+ <div class="modal-body" id="modal-body">
90
+ <!-- Content will be rendered here -->
91
+ </div>
92
+ <div class="modal-footer" id="modal-footer">
93
+ <!-- Buttons will be rendered here -->
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Notifications -->
99
+ <div class="notifications-container" id="notifications-container" role="region" aria-label="Notifications" aria-live="polite">
100
+ <!-- Notifications will be rendered here -->
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Scripts -->
105
+ <script type="module" src="/static/app.js"></script>
106
+ </body>
107
+ </html>
@@ -0,0 +1,356 @@
1
+ /**
2
+ * App Details Page - Show artifacts, analysis, and removal options
3
+ */
4
+
5
+ import { apiClient } from '../api/client.js';
6
+ import { appStore } from '../state/appStore.js';
7
+ import { dashboardStore } from '../state/dashboardStore.js';
8
+ import { uiStore } from '../state/uiStore.js';
9
+ import { formatBytes, formatInstallMethod, getMethodBadgeColor } from '../utils/formatting.js';
10
+
11
+ export interface AppAnalysis {
12
+ app: any;
13
+ artifacts: any[];
14
+ totalSize: number;
15
+ breakdown: Record<string, number>;
16
+ }
17
+
18
+ /**
19
+ * Render App Details Page
20
+ */
21
+ export async function renderAppDetails(appName: string): Promise<void> {
22
+ const container = document.getElementById('page-container');
23
+ if (!container) return;
24
+
25
+ // Show loading state
26
+ container.innerHTML = `
27
+ <div class="loading-state" style="text-align: center; padding: 60px 20px;">
28
+ <div class="spinner-lg spinner"></div>
29
+ <p class="text-center text-muted mt-4">Analyzing ${escapeHtml(appName)}...</p>
30
+ </div>
31
+ `;
32
+
33
+ try {
34
+ const analysis = await apiClient.get<AppAnalysis>(`/api/apps/${encodeURIComponent(appName)}/analysis`);
35
+ renderAppDetailsContent(container, analysis, appName);
36
+ } catch (error) {
37
+ container.innerHTML = `
38
+ <div class="alert alert-danger">
39
+ <span>Failed to load app details: ${escapeHtml((error as Error).message)}</span>
40
+ </div>
41
+ <button class="btn btn-secondary mt-4" onclick="window.location.hash = '#/apps'">
42
+ ← Back to Apps
43
+ </button>
44
+ `;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Render app details content
50
+ */
51
+ function renderAppDetailsContent(
52
+ container: HTMLElement,
53
+ analysis: AppAnalysis,
54
+ appName: string
55
+ ): void {
56
+ const { app, artifacts, totalSize, breakdown } = analysis;
57
+
58
+ container.innerHTML = `
59
+ <div class="app-details-page">
60
+ <!-- Header -->
61
+ <div class="flex-between items-start mb-8">
62
+ <div class="flex-1">
63
+ <button class="btn btn-ghost mb-4" onclick="window.location.hash = '#/apps'">
64
+ ← Back to Apps
65
+ </button>
66
+ <h1 class="text-3xl font-bold">${escapeHtml(app.name)}</h1>
67
+ <div class="flex gap-2 items-center mt-3">
68
+ <span class="badge ${getMethodBadgeColor(app.installMethod)}">
69
+ ${formatInstallMethod(app.installMethod)}
70
+ </span>
71
+ <span class="text-muted">v${escapeHtml(app.version || 'unknown')}</span>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Stats -->
77
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
78
+ ${renderStatCard('Total Size', formatBytes(totalSize))}
79
+ ${renderStatCard('Files & Dirs', artifacts.length.toString())}
80
+ ${renderStatCard('Install Method', formatInstallMethod(app.installMethod))}
81
+ </div>
82
+
83
+ <!-- Size Breakdown -->
84
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
85
+ <div class="card">
86
+ <div class="card-header">
87
+ <h2 class="text-lg font-bold">📊 Size Breakdown</h2>
88
+ </div>
89
+ <div class="card-body">
90
+ ${renderBreakdownChart(breakdown)}
91
+ </div>
92
+ </div>
93
+
94
+ <div class="card">
95
+ <div class="card-header">
96
+ <h2 class="text-lg font-bold">📈 By Category</h2>
97
+ </div>
98
+ <div class="card-body">
99
+ ${renderBreakdownTable(breakdown)}
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Artifacts Table -->
105
+ <div class="card mb-8">
106
+ <div class="card-header">
107
+ <h2 class="text-lg font-bold">📂 Artifacts (${artifacts.length})</h2>
108
+ </div>
109
+ <div class="card-body overflow-x-auto">
110
+ ${renderArtifactsTable(artifacts)}
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Action Buttons -->
115
+ <div class="flex gap-3">
116
+ <button class="btn btn-primary btn-lg flex-1" id="preview-btn">
117
+ 👁️ Preview Removal
118
+ </button>
119
+ <button class="btn btn-danger btn-lg flex-1" id="remove-btn">
120
+ 🗑️ Remove App
121
+ </button>
122
+ <button class="btn btn-secondary btn-lg" onclick="window.location.hash = '#/apps'">
123
+ Cancel
124
+ </button>
125
+ </div>
126
+
127
+ <!-- Preview Section (hidden) -->
128
+ <div id="preview-section" style="display: none; margin-top: 30px;">
129
+ <div class="alert alert-warning mb-4">
130
+ <span>This is a preview. No files will be deleted.</span>
131
+ </div>
132
+ <div class="card">
133
+ <div class="card-header">
134
+ <h3 class="text-lg font-bold">Files to Remove</h3>
135
+ </div>
136
+ <div class="card-body overflow-x-auto" id="preview-content">
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ `;
142
+
143
+ // Setup event listeners
144
+ setupAppDetailsListeners(appName, totalSize, artifacts);
145
+
146
+ // Scroll to top
147
+ window.scrollTo(0, 0);
148
+ }
149
+
150
+ /**
151
+ * Render stat card
152
+ */
153
+ function renderStatCard(label: string, value: string): string {
154
+ return `
155
+ <div class="card">
156
+ <div class="card-body text-center">
157
+ <p class="text-muted text-sm mb-1">${escapeHtml(label)}</p>
158
+ <p class="text-2xl font-bold">${escapeHtml(value)}</p>
159
+ </div>
160
+ </div>
161
+ `;
162
+ }
163
+
164
+ /**
165
+ * Render size breakdown chart (simplified pie)
166
+ */
167
+ function renderBreakdownChart(breakdown: Record<string, number>): string {
168
+ const total = Object.values(breakdown).reduce((a, b) => a + b, 0);
169
+ if (total === 0) return '<p class="text-muted">No size data available</p>';
170
+
171
+ const colors: Record<string, string> = {
172
+ binaries: '#3b82f6',
173
+ configs: '#10b981',
174
+ caches: '#f59e0b',
175
+ data: '#8b5cf6',
176
+ logs: '#ec4899',
177
+ other: '#6b7280',
178
+ };
179
+
180
+ let percent = 0;
181
+ const stops = Object.entries(breakdown)
182
+ .filter(([_, size]) => size > 0)
183
+ .map(([category, size]) => {
184
+ const categoryPercent = (size / total) * 100;
185
+ const start = percent;
186
+ percent += categoryPercent;
187
+ return `${colors[category] || '#666'} ${start}% ${percent}%`;
188
+ })
189
+ .join(',');
190
+
191
+ return `
192
+ <div style="
193
+ width: 200px;
194
+ height: 200px;
195
+ border-radius: 50%;
196
+ background: conic-gradient(${stops});
197
+ margin: 0 auto;
198
+ "></div>
199
+ `;
200
+ }
201
+
202
+ /**
203
+ * Render breakdown table
204
+ */
205
+ function renderBreakdownTable(breakdown: Record<string, number>): string {
206
+ const categories = [
207
+ { key: 'binaries', label: 'Binaries' },
208
+ { key: 'configs', label: 'Configs' },
209
+ { key: 'caches', label: 'Caches' },
210
+ { key: 'data', label: 'Data' },
211
+ { key: 'logs', label: 'Logs' },
212
+ { key: 'other', label: 'Other' },
213
+ ];
214
+
215
+ return `
216
+ <table style="width: 100%; font-size: 14px;">
217
+ <tbody>
218
+ ${categories
219
+ .map(
220
+ ({ key, label }) => `
221
+ <tr style="border-bottom: 1px solid var(--border-color); padding: 8px 0;">
222
+ <td style="padding: 8px 0;">${label}</td>
223
+ <td style="text-align: right; font-weight: 600;">${formatBytes(breakdown[key] || 0)}</td>
224
+ </tr>
225
+ `
226
+ )
227
+ .join('')}
228
+ </tbody>
229
+ </table>
230
+ `;
231
+ }
232
+
233
+ /**
234
+ * Render artifacts table
235
+ */
236
+ function renderArtifactsTable(artifacts: any[]): string {
237
+ if (artifacts.length === 0) {
238
+ return '<p class="text-muted">No artifacts found</p>';
239
+ }
240
+
241
+ return `
242
+ <table style="width: 100%; font-size: 13px;">
243
+ <thead style="border-bottom: 2px solid var(--border-color);">
244
+ <tr>
245
+ <th style="text-align: left; padding: 8px; font-weight: 600;">Path</th>
246
+ <th style="text-align: right; padding: 8px; font-weight: 600; width: 100px;">Size</th>
247
+ </tr>
248
+ </thead>
249
+ <tbody>
250
+ ${artifacts
251
+ .slice(0, 20)
252
+ .map(
253
+ (artifact) => `
254
+ <tr style="border-bottom: 1px solid var(--border-color); padding: 8px 0;">
255
+ <td style="padding: 8px; word-break: break-all;">
256
+ <code style="font-size: 12px; color: var(--text-muted);">
257
+ ${escapeHtml(artifact.path)}
258
+ </code>
259
+ </td>
260
+ <td style="text-align: right; padding: 8px; white-space: nowrap;">
261
+ ${formatBytes(artifact.size || 0)}
262
+ </td>
263
+ </tr>
264
+ `
265
+ )
266
+ .join('')}
267
+ </tbody>
268
+ </table>
269
+ ${artifacts.length > 20 ? `<p class="text-muted text-sm mt-4">... and ${artifacts.length - 20} more files</p>` : ''}
270
+ `;
271
+ }
272
+
273
+ /**
274
+ * Setup event listeners
275
+ */
276
+ function setupAppDetailsListeners(appName: string, totalSize: number, artifacts: any[]): void {
277
+ const previewBtn = document.getElementById('preview-btn');
278
+ const removeBtn = document.getElementById('remove-btn');
279
+ const previewSection = document.getElementById('preview-section');
280
+ const previewContent = document.getElementById('preview-content');
281
+
282
+ if (previewBtn) {
283
+ previewBtn.addEventListener('click', async () => {
284
+ if (previewSection?.style.display === 'none') {
285
+ previewSection.style.display = 'block';
286
+ previewBtn.textContent = '👁️ Hide Preview';
287
+
288
+ // Render preview
289
+ if (previewContent) {
290
+ previewContent.innerHTML = `
291
+ <p class="text-sm text-muted mb-4">
292
+ This will remove <strong>${artifacts.length} files and directories</strong>,
293
+ freeing <strong>${formatBytes(totalSize)}</strong> of disk space.
294
+ </p>
295
+ ${renderArtifactsTable(artifacts)}
296
+ `;
297
+ }
298
+ } else {
299
+ previewSection!.style.display = 'none';
300
+ previewBtn.textContent = '👁️ Preview Removal';
301
+ }
302
+ });
303
+ }
304
+
305
+ if (removeBtn) {
306
+ removeBtn.addEventListener('click', async () => {
307
+ const confirmed = confirm(
308
+ `Are you sure you want to remove "${appName}"?\n\nThis will delete ${artifacts.length} files and free ${formatBytes(totalSize)}.`
309
+ );
310
+
311
+ if (!confirmed) return;
312
+
313
+ uiStore.setProcessing(true);
314
+ (removeBtn as HTMLButtonElement).disabled = true;
315
+
316
+ try {
317
+ const result = await apiClient.post(`/api/apps/${encodeURIComponent(appName)}/remove`, {
318
+ dryRun: false,
319
+ createBackup: true,
320
+ });
321
+
322
+ uiStore.showSuccess(`Successfully removed ${appName}!`);
323
+ dashboardStore.addRemovalRecord({
324
+ appName,
325
+ timestamp: Date.now(),
326
+ freedSpace: result.freedSpace,
327
+ filesRemoved: result.removedFiles,
328
+ });
329
+
330
+ // Redirect to dashboard after 2 seconds
331
+ setTimeout(() => {
332
+ window.location.hash = '#/';
333
+ }, 2000);
334
+ } catch (error) {
335
+ uiStore.showError(`Failed to remove app: ${(error as Error).message}`);
336
+ (removeBtn as HTMLButtonElement).disabled = false;
337
+ } finally {
338
+ uiStore.setProcessing(false);
339
+ }
340
+ });
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Escape HTML
346
+ */
347
+ function escapeHtml(text: string): string {
348
+ const map: Record<string, string> = {
349
+ '&': '&amp;',
350
+ '<': '&lt;',
351
+ '>': '&gt;',
352
+ '"': '&quot;',
353
+ "'": '&#039;',
354
+ };
355
+ return String(text).replace(/[&<>"']/g, (m) => map[m]);
356
+ }