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.
- package/.github/workflows/npm-publish.yml +61 -0
- package/DEVELOPMENT.md +84 -0
- package/GUI_IMPLEMENTATION_STATUS.md +143 -0
- package/MD_Files/INDEX.md +51 -0
- package/PHASE2_COMPLETION.md +281 -0
- package/PHASE3_COMPLETION.md +364 -0
- package/README.md +411 -91
- package/RELEASE_GUIDE.md +236 -0
- package/assets/logo.svg +34 -0
- package/dist/core/appUpdateChecker.js +12 -16
- package/dist/core/appUpdateChecker.js.map +1 -1
- package/dist/core/detector.js +14 -18
- package/dist/core/detector.js.map +1 -1
- package/dist/core/duplicateFileFinder.js +12 -19
- package/dist/core/duplicateFileFinder.js.map +1 -1
- package/dist/core/orphanedDependencyDetector.js +19 -26
- package/dist/core/orphanedDependencyDetector.js.map +1 -1
- package/dist/core/performanceOptimizer.js +6 -10
- package/dist/core/performanceOptimizer.js.map +1 -1
- package/dist/core/permissionHandler.js +21 -25
- package/dist/core/permissionHandler.js.map +1 -1
- package/dist/core/pluginSystem.js +9 -13
- package/dist/core/pluginSystem.js.map +1 -1
- package/dist/core/removalRecorder.js +12 -19
- package/dist/core/removalRecorder.js.map +1 -1
- package/dist/core/remover.js +59 -66
- package/dist/core/remover.js.map +1 -1
- package/dist/core/reportGenerator.d.ts +1 -1
- package/dist/core/reportGenerator.d.ts.map +1 -1
- package/dist/core/reportGenerator.js +27 -34
- package/dist/core/reportGenerator.js.map +1 -1
- package/dist/core/scheduledCleanup.js +23 -30
- package/dist/core/scheduledCleanup.js.map +1 -1
- package/dist/core/serviceFileDetector.js +24 -31
- package/dist/core/serviceFileDetector.js.map +1 -1
- package/dist/core/verificationModule.js +10 -14
- package/dist/core/verificationModule.js.map +1 -1
- package/dist/index.js +118 -156
- package/dist/index.js.map +1 -1
- package/dist/managers/brewManager.js +30 -37
- package/dist/managers/brewManager.js.map +1 -1
- package/dist/managers/customManager.js +23 -30
- package/dist/managers/customManager.js.map +1 -1
- package/dist/managers/linuxManager.js +29 -36
- package/dist/managers/linuxManager.js.map +1 -1
- package/dist/managers/npmManager.js +27 -34
- package/dist/managers/npmManager.js.map +1 -1
- package/dist/types/index.js +1 -2
- package/dist/ui/client/api/client.d.ts +24 -0
- package/dist/ui/client/api/client.d.ts.map +1 -0
- package/dist/ui/client/api/client.js +100 -0
- package/dist/ui/client/api/client.js.map +1 -0
- package/dist/ui/client/app.d.ts +7 -0
- package/dist/ui/client/app.d.ts.map +1 -0
- package/dist/ui/client/app.js +75 -0
- package/dist/ui/client/app.js.map +1 -0
- package/dist/ui/client/index.html +107 -0
- package/dist/ui/client/pages/appDetails.d.ts +8 -0
- package/dist/ui/client/pages/appDetails.d.ts.map +1 -0
- package/dist/ui/client/pages/appDetails.js +287 -0
- package/dist/ui/client/pages/appDetails.js.map +1 -0
- package/dist/ui/client/pages/appSearch.d.ts +2 -0
- package/dist/ui/client/pages/appSearch.d.ts.map +1 -0
- package/dist/ui/client/pages/appSearch.js +221 -0
- package/dist/ui/client/pages/appSearch.js.map +1 -0
- package/dist/ui/client/pages/dashboard.d.ts +2 -0
- package/dist/ui/client/pages/dashboard.d.ts.map +1 -0
- package/dist/ui/client/pages/dashboard.js +175 -0
- package/dist/ui/client/pages/dashboard.js.map +1 -0
- package/dist/ui/client/pages/settings.d.ts +7 -0
- package/dist/ui/client/pages/settings.d.ts.map +1 -0
- package/dist/ui/client/pages/settings.js +279 -0
- package/dist/ui/client/pages/settings.js.map +1 -0
- package/dist/ui/client/state/appStore.d.ts +38 -0
- package/dist/ui/client/state/appStore.d.ts.map +1 -0
- package/dist/ui/client/state/appStore.js +130 -0
- package/dist/ui/client/state/appStore.js.map +1 -0
- package/dist/ui/client/state/dashboardStore.d.ts +31 -0
- package/dist/ui/client/state/dashboardStore.d.ts.map +1 -0
- package/dist/ui/client/state/dashboardStore.js +76 -0
- package/dist/ui/client/state/dashboardStore.js.map +1 -0
- package/dist/ui/client/state/uiStore.d.ts +43 -0
- package/dist/ui/client/state/uiStore.d.ts.map +1 -0
- package/dist/ui/client/state/uiStore.js +109 -0
- package/dist/ui/client/state/uiStore.js.map +1 -0
- package/dist/ui/client/styles/animations.css +349 -0
- package/dist/ui/client/styles/base.css +214 -0
- package/dist/ui/client/styles/components.css +400 -0
- package/dist/ui/client/styles/layout.css +224 -0
- package/dist/ui/client/styles/variables.css +140 -0
- package/dist/ui/client/utils/events.d.ts +19 -0
- package/dist/ui/client/utils/events.d.ts.map +1 -0
- package/dist/ui/client/utils/events.js +54 -0
- package/dist/ui/client/utils/events.js.map +1 -0
- package/dist/ui/client/utils/formatting.d.ts +11 -0
- package/dist/ui/client/utils/formatting.d.ts.map +1 -0
- package/dist/ui/client/utils/formatting.js +104 -0
- package/dist/ui/client/utils/formatting.js.map +1 -0
- package/dist/ui/client/utils/router.d.ts +25 -0
- package/dist/ui/client/utils/router.d.ts.map +1 -0
- package/dist/ui/client/utils/router.js +90 -0
- package/dist/ui/client/utils/router.js.map +1 -0
- package/dist/ui/guiServer.d.ts +11 -5
- package/dist/ui/guiServer.d.ts.map +1 -1
- package/dist/ui/guiServer.js +180 -501
- package/dist/ui/guiServer.js.map +1 -1
- package/dist/ui/menu.js +18 -27
- package/dist/ui/menu.js.map +1 -1
- package/dist/ui/prompts.js +34 -47
- package/dist/ui/prompts.js.map +1 -1
- package/dist/ui/server/middleware/errorHandler.d.ts +19 -0
- package/dist/ui/server/middleware/errorHandler.d.ts.map +1 -0
- package/dist/ui/server/middleware/errorHandler.js +100 -0
- package/dist/ui/server/middleware/errorHandler.js.map +1 -0
- package/dist/ui/server/routes/apps.d.ts +8 -0
- package/dist/ui/server/routes/apps.d.ts.map +1 -0
- package/dist/ui/server/routes/apps.js +74 -0
- package/dist/ui/server/routes/apps.js.map +1 -0
- package/dist/ui/server/routes/dashboard.d.ts +4 -0
- package/dist/ui/server/routes/dashboard.d.ts.map +1 -0
- package/dist/ui/server/routes/dashboard.js +57 -0
- package/dist/ui/server/routes/dashboard.js.map +1 -0
- package/dist/ui/server/routes/settings.d.ts +6 -0
- package/dist/ui/server/routes/settings.d.ts.map +1 -0
- package/dist/ui/server/routes/settings.js +31 -0
- package/dist/ui/server/routes/settings.js.map +1 -0
- package/dist/ui/server/services/appService.d.ts +45 -0
- package/dist/ui/server/services/appService.d.ts.map +1 -0
- package/dist/ui/server/services/appService.js +114 -0
- package/dist/ui/server/services/appService.js.map +1 -0
- package/dist/ui/server/services/removalService.d.ts +24 -0
- package/dist/ui/server/services/removalService.d.ts.map +1 -0
- package/dist/ui/server/services/removalService.js +83 -0
- package/dist/ui/server/services/removalService.js.map +1 -0
- package/dist/utils/filesystem.js +32 -49
- package/dist/utils/filesystem.js.map +1 -1
- package/dist/utils/logger.js +9 -18
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/platform.js +10 -22
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/upgrade.d.ts +2 -1
- package/dist/utils/upgrade.d.ts.map +1 -1
- package/dist/utils/upgrade.js +24 -15
- package/dist/utils/upgrade.js.map +1 -1
- package/package.json +4 -2
- package/scripts/publish-npm.sh +64 -0
- package/src/core/appUpdateChecker.ts +1 -1
- package/src/core/detector.ts +6 -6
- package/src/core/duplicateFileFinder.ts +1 -1
- package/src/core/orphanedDependencyDetector.ts +2 -2
- package/src/core/performanceOptimizer.ts +1 -1
- package/src/core/permissionHandler.ts +2 -2
- package/src/core/pluginSystem.ts +1 -1
- package/src/core/removalRecorder.ts +2 -2
- package/src/core/remover.ts +11 -11
- package/src/core/reportGenerator.ts +2 -2
- package/src/core/scheduledCleanup.ts +2 -2
- package/src/core/serviceFileDetector.ts +2 -2
- package/src/core/verificationModule.ts +2 -2
- package/src/index.ts +8 -8
- package/src/managers/brewManager.ts +3 -3
- package/src/managers/customManager.ts +2 -2
- package/src/managers/linuxManager.ts +3 -3
- package/src/managers/npmManager.ts +3 -3
- package/src/ui/client/api/client.ts +168 -0
- package/src/ui/client/app.ts +125 -0
- package/src/ui/client/index.html +107 -0
- package/src/ui/client/pages/appDetails.ts +356 -0
- package/src/ui/client/pages/appSearch.ts +283 -0
- package/src/ui/client/pages/dashboard.ts +211 -0
- package/src/ui/client/pages/settings.ts +342 -0
- package/src/ui/client/state/appStore.ts +181 -0
- package/src/ui/client/state/dashboardStore.ts +123 -0
- package/src/ui/client/state/uiStore.ts +166 -0
- package/src/ui/client/styles/animations.css +349 -0
- package/src/ui/client/styles/base.css +214 -0
- package/src/ui/client/styles/components.css +400 -0
- package/src/ui/client/styles/layout.css +224 -0
- package/src/ui/client/styles/variables.css +140 -0
- package/src/ui/client/utils/events.ts +74 -0
- package/src/ui/client/utils/formatting.ts +157 -0
- package/src/ui/client/utils/router.ts +161 -0
- package/src/ui/guiServer.ts +245 -498
- package/src/ui/prompts.ts +1 -1
- package/src/ui/server/middleware/errorHandler.ts +174 -0
- package/src/ui/server/routes/apps.ts +132 -0
- package/src/ui/server/routes/dashboard.ts +93 -0
- package/src/ui/server/routes/settings.ts +63 -0
- package/src/ui/server/services/appService.ts +184 -0
- package/src/ui/server/services/removalService.ts +138 -0
- package/src/utils/upgrade.ts +19 -2
- package/tsconfig.json +3 -2
- package/INDEX.md +0 -165
- /package/{ACTION_CHECKLIST.md → MD_Files/ACTION_CHECKLIST.md} +0 -0
- /package/{APPCLEAN_SUMMARY.md → MD_Files/APPCLEAN_SUMMARY.md} +0 -0
- /package/{CHANGELOG.md → MD_Files/CHANGELOG.md} +0 -0
- /package/{CODE_OF_CONDUCT.md → MD_Files/CODE_OF_CONDUCT.md} +0 -0
- /package/{CODE_REVIEW_REPORT.md → MD_Files/CODE_REVIEW_REPORT.md} +0 -0
- /package/{COMMUNITY_POSTS.md → MD_Files/COMMUNITY_POSTS.md} +0 -0
- /package/{DEPLOYMENT_GUIDE.md → MD_Files/DEPLOYMENT_GUIDE.md} +0 -0
- /package/{DEPLOYMENT_STATUS.md → MD_Files/DEPLOYMENT_STATUS.md} +0 -0
- /package/{EXECUTIVE_REPORT.md → MD_Files/EXECUTIVE_REPORT.md} +0 -0
- /package/{GITHUB_OPTIMIZATION.md → MD_Files/GITHUB_OPTIMIZATION.md} +0 -0
- /package/{MARKETING_SUMMARY.md → MD_Files/MARKETING_SUMMARY.md} +0 -0
- /package/{NPM_PACKAGE_OPTIMIZATION.md → MD_Files/NPM_PACKAGE_OPTIMIZATION.md} +0 -0
- /package/{NPM_PUBLISH.md → MD_Files/NPM_PUBLISH.md} +0 -0
- /package/{PROJECT_SUMMARY.txt → MD_Files/PROJECT_SUMMARY.txt} +0 -0
- /package/{PUBLICATION_SUCCESS_REPORT.md → MD_Files/PUBLICATION_SUCCESS_REPORT.md} +0 -0
- /package/{QUICKSTART.md → MD_Files/QUICKSTART.md} +0 -0
- /package/{SETUP_GITHUB.md → MD_Files/SETUP_GITHUB.md} +0 -0
- /package/{TESTING_SUMMARY.md → MD_Files/TESTING_SUMMARY.md} +0 -0
- /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">×</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
|
+
'&': '&',
|
|
350
|
+
'<': '<',
|
|
351
|
+
'>': '>',
|
|
352
|
+
'"': '"',
|
|
353
|
+
"'": ''',
|
|
354
|
+
};
|
|
355
|
+
return String(text).replace(/[&<>"']/g, (m) => map[m]);
|
|
356
|
+
}
|