project-diagnose 0.1.9__py3-none-any.whl
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.
- project_diagnose/__init__.py +1 -0
- project_diagnose/analyzer.py +438 -0
- project_diagnose/cli.py +55 -0
- project_diagnose/config_schema.py +34 -0
- project_diagnose/rendering.py +18 -0
- project_diagnose/static/script.js +76 -0
- project_diagnose/static/style.css +144 -0
- project_diagnose/templates/index.html +65 -0
- project_diagnose/templates/settings.html +293 -0
- project_diagnose/web.py +125 -0
- project_diagnose-0.1.9.dist-info/METADATA +157 -0
- project_diagnose-0.1.9.dist-info/RECORD +16 -0
- project_diagnose-0.1.9.dist-info/WHEEL +5 -0
- project_diagnose-0.1.9.dist-info/entry_points.txt +2 -0
- project_diagnose-0.1.9.dist-info/licenses/LICENSE +5 -0
- project_diagnose-0.1.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f6f6f6;
|
|
3
|
+
--text: #222;
|
|
4
|
+
--panel-bg: #ffffff;
|
|
5
|
+
--code-bg: #f0f0f0;
|
|
6
|
+
--accent: #333;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body.dark {
|
|
10
|
+
--bg: #121212;
|
|
11
|
+
--text: #e6e6e6;
|
|
12
|
+
--panel-bg: #1d1d1d;
|
|
13
|
+
--code-bg: #161616;
|
|
14
|
+
--accent: #ddd;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--text);
|
|
20
|
+
margin: 0;
|
|
21
|
+
font-family: system-ui, sans-serif;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
header {
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
align-items: center;
|
|
28
|
+
background: var(--panel-bg);
|
|
29
|
+
padding: 15px 20px;
|
|
30
|
+
border-bottom: 1px solid #4444;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.toolbar {
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 10px;
|
|
36
|
+
padding: 15px 20px;
|
|
37
|
+
align-items: center;
|
|
38
|
+
flex-wrap: wrap;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button, .button {
|
|
42
|
+
background: var(--accent);
|
|
43
|
+
color: var(--bg);
|
|
44
|
+
border: none;
|
|
45
|
+
padding: 8px 16px;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button:hover, .button:hover {
|
|
52
|
+
opacity: 0.8;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input[type="text"] {
|
|
56
|
+
padding: 6px 10px;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
border: 1px solid #6666;
|
|
59
|
+
background: var(--panel-bg);
|
|
60
|
+
color: var(--text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.panel {
|
|
64
|
+
margin: 20px;
|
|
65
|
+
background: var(--panel-bg);
|
|
66
|
+
padding: 15px 20px;
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pre {
|
|
71
|
+
background: var(--code-bg);
|
|
72
|
+
padding: 15px;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
max-height: 65vh;
|
|
75
|
+
overflow: auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.collapsible {
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
position: relative;
|
|
81
|
+
padding-right: 20px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.collapsible::after {
|
|
85
|
+
content: "▼";
|
|
86
|
+
position: absolute;
|
|
87
|
+
right: 0;
|
|
88
|
+
transition: transform 0.25s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.collapsible.active::after {
|
|
92
|
+
transform: rotate(-180deg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.content {
|
|
96
|
+
display: none;
|
|
97
|
+
padding-top: 10px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.content.open {
|
|
101
|
+
display: block;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.highlight {
|
|
105
|
+
background: #ffe08a;
|
|
106
|
+
padding: 2px 4px;
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
body.dark .highlight {
|
|
111
|
+
background: #6b5200;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* метрики */
|
|
115
|
+
|
|
116
|
+
.metrics-row {
|
|
117
|
+
display: flex;
|
|
118
|
+
gap: 12px;
|
|
119
|
+
flex-wrap: wrap;
|
|
120
|
+
margin-bottom: 15px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.metric-card {
|
|
124
|
+
flex: 1 1 150px;
|
|
125
|
+
min-width: 150px;
|
|
126
|
+
background: var(--code-bg);
|
|
127
|
+
border-radius: 8px;
|
|
128
|
+
padding: 10px 12px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.metric-label {
|
|
132
|
+
font-size: 12px;
|
|
133
|
+
opacity: 0.8;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.metric-value {
|
|
137
|
+
font-size: 20px;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
margin-top: 4px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.chart-wrapper {
|
|
143
|
+
max-width: 600px;
|
|
144
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Project Diagnose</title>
|
|
6
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
|
|
10
|
+
<header>
|
|
11
|
+
<h1>Project Diagnose</h1>
|
|
12
|
+
<button id="themeToggle">🌓 Тема</button>
|
|
13
|
+
</header>
|
|
14
|
+
|
|
15
|
+
<div class="toolbar">
|
|
16
|
+
<a href="/settings" class="button">⚙ Настройки</a>
|
|
17
|
+
<button id="refactorBtn">🛠 Рефакторинг</button>
|
|
18
|
+
<a href="/dump" class="button">📦 Dump проекта</a>
|
|
19
|
+
<a href="/report.txt" class="button">⬇ Скачать отчёт (.txt)</a>
|
|
20
|
+
<input id="searchInput" type="text" placeholder="Поиск в отчёте…" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<section class="panel">
|
|
24
|
+
<h2>📈 Показатели проекта</h2>
|
|
25
|
+
<div class="metrics-row">
|
|
26
|
+
<div class="metric-card">
|
|
27
|
+
<div class="metric-label">Индекс хаоса</div>
|
|
28
|
+
<div class="metric-value">{{ "%.2f"|format(metrics.chaos) }}</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="metric-card">
|
|
31
|
+
<div class="metric-label">Техническая карма</div>
|
|
32
|
+
<div class="metric-value">{{ "%.2f"|format(metrics.tech_karma) }}</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="metric-card">
|
|
35
|
+
<div class="metric-label">FRI</div>
|
|
36
|
+
<div class="metric-value">{{ "%.2f"|format(metrics.future_regret) }}</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="chart-wrapper">
|
|
40
|
+
<canvas id="extChart"></canvas>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
44
|
+
<section class="panel">
|
|
45
|
+
<h2 class="collapsible">📁 Структура проекта</h2>
|
|
46
|
+
<div class="content open">
|
|
47
|
+
<pre id="treeBlock">{{ tree }}</pre>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section class="panel">
|
|
52
|
+
<h2 class="collapsible">📊 Отчёт анализа</h2>
|
|
53
|
+
<div class="content open">
|
|
54
|
+
<pre id="reportBlock">{{ report }}</pre>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
// пробрасываем метрики из Python в JS
|
|
60
|
+
window.PROJECT_METRICS = {{ metrics | tojson }};
|
|
61
|
+
</script>
|
|
62
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
63
|
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<title>Настройки Diagnose</title>
|
|
6
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
7
|
+
|
|
8
|
+
<style>
|
|
9
|
+
body.dark {
|
|
10
|
+
--panel-bg: #1e1e1e;
|
|
11
|
+
--text-color: #ddd;
|
|
12
|
+
background: #121212;
|
|
13
|
+
color: var(--text-color);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
background: #ffffff;
|
|
18
|
+
color: #222;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.folder {
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.folder-children .file-item,
|
|
27
|
+
.folder-children .folder {
|
|
28
|
+
padding-left: 20px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.indent-file {
|
|
32
|
+
padding-left: 16px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.file-item {
|
|
36
|
+
user-select: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.settings-grid {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
42
|
+
gap: 16px;
|
|
43
|
+
}
|
|
44
|
+
.settings-card {
|
|
45
|
+
background: var(--panel-bg);
|
|
46
|
+
padding: 12px 14px;
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
}
|
|
49
|
+
.settings-card h3 {
|
|
50
|
+
margin-top: 0;
|
|
51
|
+
margin-bottom: 6px;
|
|
52
|
+
font-size: 15px;
|
|
53
|
+
}
|
|
54
|
+
.item-row {
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 6px;
|
|
57
|
+
margin-bottom: 4px;
|
|
58
|
+
}
|
|
59
|
+
.item-row input {
|
|
60
|
+
flex: 1;
|
|
61
|
+
}
|
|
62
|
+
.small-btn {
|
|
63
|
+
padding: 4px 8px;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
}
|
|
66
|
+
#statusBlock {
|
|
67
|
+
margin-top: 10px;
|
|
68
|
+
white-space: pre-wrap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.file-tree {
|
|
72
|
+
background: var(--panel-bg);
|
|
73
|
+
padding: 10px 14px;
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
max-height: 45vh;
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
font-family: monospace;
|
|
78
|
+
font-size: 14px;
|
|
79
|
+
}
|
|
80
|
+
.file-item {
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
padding: 2px 0;
|
|
83
|
+
}
|
|
84
|
+
.file-item:hover {
|
|
85
|
+
background: rgba(255,255,255,0.06);
|
|
86
|
+
}
|
|
87
|
+
.folder {
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
}
|
|
90
|
+
.indent-1 { padding-left: 16px; }
|
|
91
|
+
.indent-2 { padding-left: 32px; }
|
|
92
|
+
.indent-3 { padding-left: 48px; }
|
|
93
|
+
.indent-4 { padding-left: 64px; }
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
|
|
99
|
+
<body>
|
|
100
|
+
<script>
|
|
101
|
+
if (localStorage.getItem("project_diagnose_theme") === "dark") {
|
|
102
|
+
document.body.classList.add("dark");
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<header>
|
|
107
|
+
<h1>Настройки Diagnose</h1>
|
|
108
|
+
<a href="/" class="button">← Назад</a>
|
|
109
|
+
</header>
|
|
110
|
+
|
|
111
|
+
<section class="panel">
|
|
112
|
+
<h2>Файлы проекта</h2>
|
|
113
|
+
<div id="fileTree" class="file-tree"></div>
|
|
114
|
+
<p style="font-size:12px; opacity:0.7;">
|
|
115
|
+
Двойной клик по файлу или директории добавляет путь в include_files.
|
|
116
|
+
</p>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<section class="panel">
|
|
120
|
+
<h2>Конфигурация config.diagnose</h2>
|
|
121
|
+
|
|
122
|
+
<div class="settings-grid">
|
|
123
|
+
<div class="settings-card" data-field="include_ext">
|
|
124
|
+
<h3>Расширения для включения</h3>
|
|
125
|
+
<div class="list" id="include_ext_list"></div>
|
|
126
|
+
<button class="small-btn" data-add="include_ext">+ Добавить</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="settings-card" data-field="exclude_dirs">
|
|
130
|
+
<h3>Исключаемые директории</h3>
|
|
131
|
+
<div class="list" id="exclude_dirs_list"></div>
|
|
132
|
+
<button class="small-btn" data-add="exclude_dirs">+ Добавить</button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="settings-card" data-field="exclude_files">
|
|
136
|
+
<h3>Исключаемые файлы</h3>
|
|
137
|
+
<div class="list" id="exclude_files_list"></div>
|
|
138
|
+
<button class="small-btn" data-add="exclude_files">+ Добавить</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="settings-card" data-field="include_files">
|
|
142
|
+
<h3>Принудительно включаемые файлы</h3>
|
|
143
|
+
<div class="list" id="include_files_list"></div>
|
|
144
|
+
<button class="small-btn" data-add="include_files">+ Добавить</button>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="settings-card" data-field="special_json">
|
|
148
|
+
<h3>Особые JSON-файлы</h3>
|
|
149
|
+
<div class="list" id="special_json_list"></div>
|
|
150
|
+
<button class="small-btn" data-add="special_json">+ Добавить</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<button id="saveBtn" style="margin-top:15px;">💾 Сохранить настройки</button>
|
|
155
|
+
<pre id="statusBlock"></pre>
|
|
156
|
+
</section>
|
|
157
|
+
|
|
158
|
+
<script>
|
|
159
|
+
const FILE_TREE = {{ tree | tojson }};
|
|
160
|
+
window.CURRENT_CONFIG = {{ config | tojson }};
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<script>
|
|
164
|
+
const FILE_TREE = {{ tree | tojson }};
|
|
165
|
+
window.CURRENT_CONFIG = {{ config | tojson }};
|
|
166
|
+
|
|
167
|
+
function buildTree(parent, node, basePath = "") {
|
|
168
|
+
for (const key in node) {
|
|
169
|
+
if (key === "_files") {
|
|
170
|
+
node["_files"].forEach(file => {
|
|
171
|
+
const full = basePath ? `${basePath}/${file}` : file;
|
|
172
|
+
|
|
173
|
+
const item = document.createElement("div");
|
|
174
|
+
item.className = "file-item indent-file";
|
|
175
|
+
item.dataset.path = full;
|
|
176
|
+
item.textContent = "📄 " + file;
|
|
177
|
+
|
|
178
|
+
parent.appendChild(item);
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const folderPath = basePath ? `${basePath}/${key}` : key;
|
|
184
|
+
|
|
185
|
+
const folder = document.createElement("div");
|
|
186
|
+
folder.className = "file-item folder indent-folder";
|
|
187
|
+
folder.dataset.path = folderPath;
|
|
188
|
+
folder.textContent = "📁 " + key;
|
|
189
|
+
|
|
190
|
+
// контейнер для содержимого
|
|
191
|
+
const children = document.createElement("div");
|
|
192
|
+
children.style.display = "none";
|
|
193
|
+
children.className = "folder-children";
|
|
194
|
+
|
|
195
|
+
// переключатель при клике
|
|
196
|
+
folder.addEventListener("click", () => {
|
|
197
|
+
children.style.display =
|
|
198
|
+
children.style.display === "none" ? "block" : "none";
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
parent.appendChild(folder);
|
|
202
|
+
parent.appendChild(children);
|
|
203
|
+
|
|
204
|
+
buildTree(children, node[key], folderPath);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const container = document.getElementById("fileTree");
|
|
209
|
+
buildTree(container, FILE_TREE);
|
|
210
|
+
|
|
211
|
+
// двойной клик добавляет путь
|
|
212
|
+
container.addEventListener("dblclick", e => {
|
|
213
|
+
const item = e.target.closest(".file-item");
|
|
214
|
+
if (!item) return;
|
|
215
|
+
|
|
216
|
+
const path = item.dataset.path;
|
|
217
|
+
const list = document.getElementById("include_files_list");
|
|
218
|
+
list.appendChild(createRow("include_files", path));
|
|
219
|
+
});
|
|
220
|
+
</script>
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
<script>
|
|
224
|
+
function createRow(fieldName, value = "") {
|
|
225
|
+
const row = document.createElement("div");
|
|
226
|
+
row.className = "item-row";
|
|
227
|
+
|
|
228
|
+
const input = document.createElement("input");
|
|
229
|
+
input.type = "text";
|
|
230
|
+
input.value = value;
|
|
231
|
+
|
|
232
|
+
const delBtn = document.createElement("button");
|
|
233
|
+
delBtn.type = "button";
|
|
234
|
+
delBtn.textContent = "✕";
|
|
235
|
+
delBtn.className = "small-btn";
|
|
236
|
+
delBtn.onclick = () => row.remove();
|
|
237
|
+
|
|
238
|
+
row.appendChild(input);
|
|
239
|
+
row.appendChild(delBtn);
|
|
240
|
+
return row;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function initList(field) {
|
|
244
|
+
const cont = document.getElementById(field + "_list");
|
|
245
|
+
cont.innerHTML = "";
|
|
246
|
+
(window.CURRENT_CONFIG[field] || []).forEach(v => cont.appendChild(createRow(field, v)));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
["include_ext","exclude_dirs","exclude_files","include_files","special_json"].forEach(initList);
|
|
250
|
+
|
|
251
|
+
document.querySelectorAll("[data-add]").forEach(btn => {
|
|
252
|
+
btn.addEventListener("click", () => {
|
|
253
|
+
const field = btn.dataset.add;
|
|
254
|
+
document.getElementById(field + "_list").appendChild(createRow(field, ""));
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
document.getElementById("fileTree").addEventListener("dblclick", e => {
|
|
259
|
+
const item = e.target.closest(".file-item");
|
|
260
|
+
if (!item) return;
|
|
261
|
+
|
|
262
|
+
const path = item.dataset.path;
|
|
263
|
+
const list = document.getElementById("include_files_list");
|
|
264
|
+
list.appendChild(createRow("include_files", path));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
document.getElementById("saveBtn").addEventListener("click", () => {
|
|
268
|
+
const fields = ["include_ext","exclude_dirs","exclude_files","include_files","special_json"];
|
|
269
|
+
const cfg = {};
|
|
270
|
+
|
|
271
|
+
fields.forEach(field => {
|
|
272
|
+
cfg[field] = Array.from(document.querySelectorAll(`#${field}_list input`))
|
|
273
|
+
.map(i => i.value.trim())
|
|
274
|
+
.filter(v => v);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
fetch("/save_settings", {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: { "Content-Type": "application/json" },
|
|
280
|
+
body: JSON.stringify({ config: cfg })
|
|
281
|
+
})
|
|
282
|
+
.then(r => r.json())
|
|
283
|
+
.then(data => {
|
|
284
|
+
const s = document.getElementById("statusBlock");
|
|
285
|
+
s.textContent = data.status === "ok"
|
|
286
|
+
? "✓ Настройки сохранены.\nОбновите страницу."
|
|
287
|
+
: "Ошибка:\n" + data.message;
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
</body>
|
|
293
|
+
</html>
|
project_diagnose/web.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from flask import Flask, render_template, Response, request, jsonify
|
|
4
|
+
|
|
5
|
+
from .analyzer import (
|
|
6
|
+
analyze_project,
|
|
7
|
+
calc_chaos_score,
|
|
8
|
+
calc_tech_karma,
|
|
9
|
+
calc_future_regret_index,
|
|
10
|
+
USER_CFG_PATH,
|
|
11
|
+
build_tree_json,
|
|
12
|
+
collect_files,
|
|
13
|
+
)
|
|
14
|
+
from .rendering import render_text_report, render_tree
|
|
15
|
+
from .config_schema import DiagnoseConfig
|
|
16
|
+
|
|
17
|
+
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
18
|
+
|
|
19
|
+
_stats_cache = None
|
|
20
|
+
_report_cache = None
|
|
21
|
+
_tree_cache = None
|
|
22
|
+
_metrics_cache = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compute_metrics(stats):
|
|
26
|
+
return {
|
|
27
|
+
"chaos": calc_chaos_score(stats),
|
|
28
|
+
"tech_karma": calc_tech_karma(stats),
|
|
29
|
+
"future_regret": calc_future_regret_index(stats),
|
|
30
|
+
"ext_lines": stats.get("ext_lines", {}),
|
|
31
|
+
"total_size": stats.get("total_size", 0),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_stats():
|
|
36
|
+
global _stats_cache, _report_cache, _tree_cache, _metrics_cache
|
|
37
|
+
if _stats_cache is None:
|
|
38
|
+
stats = analyze_project()
|
|
39
|
+
_stats_cache = stats
|
|
40
|
+
_report_cache = render_text_report(stats)
|
|
41
|
+
_tree_cache = render_tree(stats)
|
|
42
|
+
_metrics_cache = compute_metrics(stats)
|
|
43
|
+
return _stats_cache, _report_cache, _tree_cache, _metrics_cache
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.route("/")
|
|
47
|
+
def index():
|
|
48
|
+
_, report, tree, metrics = get_stats()
|
|
49
|
+
return render_template("index.html", report=report, tree=tree, metrics=metrics)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.route("/report.txt")
|
|
53
|
+
def download_txt():
|
|
54
|
+
_, report, _, _ = get_stats()
|
|
55
|
+
return Response(report, mimetype="text/plain")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ===================== SETTINGS =====================
|
|
59
|
+
|
|
60
|
+
def load_config_for_ui():
|
|
61
|
+
if os.path.exists(USER_CFG_PATH):
|
|
62
|
+
try:
|
|
63
|
+
with open(USER_CFG_PATH, "r", encoding="utf-8") as f:
|
|
64
|
+
raw = json.load(f)
|
|
65
|
+
cfg = DiagnoseConfig(**raw)
|
|
66
|
+
return cfg.model_dump()
|
|
67
|
+
except Exception:
|
|
68
|
+
return DiagnoseConfig().model_dump()
|
|
69
|
+
return DiagnoseConfig().model_dump()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.route("/settings")
|
|
73
|
+
def settings():
|
|
74
|
+
cfg = load_config_for_ui()
|
|
75
|
+
tree = build_tree_json()
|
|
76
|
+
return render_template("settings.html", config=cfg, tree=tree)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.route("/save_settings", methods=["POST"])
|
|
80
|
+
def save_settings():
|
|
81
|
+
data = request.json
|
|
82
|
+
cfg_obj = data.get("config")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
cfg = DiagnoseConfig(**cfg_obj)
|
|
86
|
+
with open(USER_CFG_PATH, "w", encoding="utf-8") as f:
|
|
87
|
+
json.dump(cfg.model_dump(), f, indent=4, ensure_ascii=False)
|
|
88
|
+
|
|
89
|
+
global _stats_cache, _report_cache, _tree_cache, _metrics_cache
|
|
90
|
+
_stats_cache = _report_cache = _tree_cache = _metrics_cache = None
|
|
91
|
+
|
|
92
|
+
return jsonify({"status": "ok"})
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return jsonify({"status": "error", "message": str(e)})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ===================== DUMP =====================
|
|
98
|
+
|
|
99
|
+
@app.route("/dump")
|
|
100
|
+
def dump_project():
|
|
101
|
+
files = collect_files()
|
|
102
|
+
buffer = []
|
|
103
|
+
|
|
104
|
+
for path in files:
|
|
105
|
+
rel = os.path.relpath(path, os.getcwd())
|
|
106
|
+
buffer.append(f"# -------- {rel}")
|
|
107
|
+
try:
|
|
108
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
109
|
+
buffer.append(f.read())
|
|
110
|
+
except Exception as e:
|
|
111
|
+
buffer.append(f"<<Error reading file: {e}>>")
|
|
112
|
+
buffer.append("\n")
|
|
113
|
+
|
|
114
|
+
out = "\n".join(buffer)
|
|
115
|
+
|
|
116
|
+
return Response(
|
|
117
|
+
out,
|
|
118
|
+
mimetype="text/plain",
|
|
119
|
+
headers={"Content-Disposition": "attachment; filename=project_dump.txt"}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_web():
|
|
124
|
+
print("Web-интерфейс запущен: http://127.0.0.1:5000")
|
|
125
|
+
app.run(debug=False)
|