g360-cli 1.15.1 → 1.16.0
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/README.md +1547 -1424
- package/package.json +2 -1
- package/src/assets/pptx/layouts/base.js +239 -0
- package/src/assets/pptx/layouts/index.js +14 -0
- package/src/assets/pptx/scripts/analyze-app.js +194 -0
- package/src/assets/pptx/templates/app-manual.js +275 -0
- package/src/assets/pptx/themes/base.js +63 -0
- package/src/assets/pptx/themes/cipsa.js +46 -0
- package/src/assets/pptx/themes/g360.js +32 -0
- package/src/assets/pptx/themes/index.js +2 -0
- package/src/assets/templates/python-flet-polished/README.md +39 -0
- package/src/assets/templates/python-flet-polished/main.py +7 -0
- package/src/assets/templates/python-flet-polished/pyproject.toml +1 -0
- package/src/assets/templates/python-flet-polished/requirements.txt +1 -0
- package/src/assets/templates/python-flet-polished/skill.json +27 -1
- package/src/assets/templates/python-flet-polished/src/app.py +44 -1
- package/src/assets/templates/python-flet-polished/src/config/theme.py +153 -74
- package/src/assets/templates/python-flet-polished/src/core/g360_registry.py +562 -0
- package/src/assets/templates/python-flet-polished/src/ui/dashboard.py +2 -2
- package/src/assets/templates/python-flet-polished/src/ui/kpi_card.py +1 -1
- package/src/assets/templates/python-flet-polished/src/ui/search_overlay.py +4 -4
- package/src/cli.js +11 -0
- package/src/commands/addon.test.js +2 -2
- package/src/commands/pptx.js +89 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Manual de App G360 — 12 slides A4.
|
|
3
|
+
*
|
|
4
|
+
* Estructura:
|
|
5
|
+
* 1. Portada
|
|
6
|
+
* 2. ¿Qué es esta app?
|
|
7
|
+
* 3. Requisitos / Instalacion
|
|
8
|
+
* 4. Inicio — primera pantalla
|
|
9
|
+
* 5-N. Funcionalidades (una por feature detectada)
|
|
10
|
+
* N+1. Flujos de trabajo (modals)
|
|
11
|
+
* N+2. Arquitectura
|
|
12
|
+
* N+3. Buenas prácticas
|
|
13
|
+
* N+4. Resumen
|
|
14
|
+
*/
|
|
15
|
+
import PptxGenJS from 'pptxgenjs';
|
|
16
|
+
import { createG360Theme, createCipsaTheme } from '../themes/index.js';
|
|
17
|
+
import {
|
|
18
|
+
coverSlide,
|
|
19
|
+
screenshotSlide,
|
|
20
|
+
featureSlide,
|
|
21
|
+
workflowSlide,
|
|
22
|
+
architectureSlide,
|
|
23
|
+
checklistSlide,
|
|
24
|
+
kpiSlide,
|
|
25
|
+
addFooter,
|
|
26
|
+
addSectionHeader,
|
|
27
|
+
} from '../layouts/base.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Genera un manual PPTX completo para una app G360.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} appData — resultado de analyzeApp()
|
|
33
|
+
* @param {object} options — { mode, theme, outDir }
|
|
34
|
+
* @returns {Promise<string>} — ruta del archivo .pptx generado
|
|
35
|
+
*/
|
|
36
|
+
export async function generateManualPptx(appData, options = {}) {
|
|
37
|
+
const { mode = 'manual', theme: themeName = null } = options;
|
|
38
|
+
|
|
39
|
+
// Seleccionar theme
|
|
40
|
+
const theme = themeName === 'cipsa' || (!themeName && appData.brand === 'cipsa')
|
|
41
|
+
? createCipsaTheme()
|
|
42
|
+
: createG360Theme();
|
|
43
|
+
|
|
44
|
+
const { pptx, colors } = theme;
|
|
45
|
+
|
|
46
|
+
// Configurar tamaño segun modo
|
|
47
|
+
if (mode === 'demo') {
|
|
48
|
+
pptx.layout = 'LAYOUT_16x9';
|
|
49
|
+
} else {
|
|
50
|
+
pptx.layout = 'A4';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ===== SLIDE 1: Portada =====
|
|
54
|
+
let slide = pptx.addSlide();
|
|
55
|
+
slide.background = { color: colors.bg };
|
|
56
|
+
coverSlide(slide, {
|
|
57
|
+
appName: appData.name || 'Mi Aplicacion G360',
|
|
58
|
+
description: appData.description || 'Documentacion de uso y funcionalidades',
|
|
59
|
+
version: appData.version,
|
|
60
|
+
brand: appData.brand,
|
|
61
|
+
}, theme);
|
|
62
|
+
addFooter(slide, theme, 1, 12);
|
|
63
|
+
|
|
64
|
+
// ===== SLIDE 2: ¿Qué es? =====
|
|
65
|
+
slide = pptx.addSlide();
|
|
66
|
+
slide.background = { color: colors.bg };
|
|
67
|
+
addSectionHeader(slide, 'INTRODUCCION', theme);
|
|
68
|
+
featureSlide(slide, {
|
|
69
|
+
title: '¿Qué es ' + (appData.name || 'esta app') + '?',
|
|
70
|
+
description: appData.description || 'Aplicacion de escritorio construida con Flet y los estandares G360.',
|
|
71
|
+
bullets: [
|
|
72
|
+
'Monitoriza datos en tiempo real desde el ERP',
|
|
73
|
+
'Genera reportes y analisis automaticos',
|
|
74
|
+
'Soporta dual theme (claro/oscuro)',
|
|
75
|
+
'Funciona offline con cache local',
|
|
76
|
+
],
|
|
77
|
+
}, theme);
|
|
78
|
+
addFooter(slide, theme, 2, 12);
|
|
79
|
+
|
|
80
|
+
// ===== SLIDE 3: Instalacion =====
|
|
81
|
+
slide = pptx.addSlide();
|
|
82
|
+
slide.background = { color: colors.bg };
|
|
83
|
+
addSectionHeader(slide, 'INSTALACION', theme);
|
|
84
|
+
const installSteps = [
|
|
85
|
+
{ title: 'Ejecutar run.bat', desc: 'El launcher instala uv, Python 3.11, dependencias y crea acceso directo.' },
|
|
86
|
+
{ title: 'O usar comando directo', desc: 'uv sync && uv run python main.py' },
|
|
87
|
+
{ title: 'Version portable', desc: 'Descomprimir zip y ejecutar launch.vbs (sin dependencias).' },
|
|
88
|
+
];
|
|
89
|
+
workflowSlide(slide, {
|
|
90
|
+
title: 'Como iniciar la aplicacion',
|
|
91
|
+
steps: installSteps,
|
|
92
|
+
}, theme);
|
|
93
|
+
addFooter(slide, theme, 3, 12);
|
|
94
|
+
|
|
95
|
+
// ===== SLIDES 4+: Features / Screenshots =====
|
|
96
|
+
let slideNum = 4;
|
|
97
|
+
const totalFeatures = Math.min(appData.features.length, 6); // max 6 features en manual
|
|
98
|
+
const totalSlides = 3 + totalFeatures + 3; // intro + features + workflows + arch + summary
|
|
99
|
+
|
|
100
|
+
// Slide 4: Dashboard / inicio
|
|
101
|
+
slide = pptx.addSlide();
|
|
102
|
+
slide.background = { color: colors.bg };
|
|
103
|
+
addSectionHeader(slide, 'PRIMERA VISTA', theme);
|
|
104
|
+
const dashScreen = appData.screenshots?.find(s => s.filename.toLowerCase().includes('dash'));
|
|
105
|
+
screenshotSlide(slide, {
|
|
106
|
+
title: 'Dashboard Principal',
|
|
107
|
+
description: appData.hasAutoRefresh
|
|
108
|
+
? 'Vista principal con KPIs en tiempo real y auto-refresh cada 15 minutos.'
|
|
109
|
+
: 'Vista principal de la aplicacion con los indicadores clave del negocio.',
|
|
110
|
+
screenshotPath: dashScreen?.path,
|
|
111
|
+
}, theme);
|
|
112
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
113
|
+
slideNum++;
|
|
114
|
+
|
|
115
|
+
// Features individual slides
|
|
116
|
+
for (const feature of appData.features.slice(0, totalFeatures)) {
|
|
117
|
+
slide = pptx.addSlide();
|
|
118
|
+
slide.background = { color: colors.bg };
|
|
119
|
+
addSectionHeader(slide, 'FUNCIONALIDAD', theme);
|
|
120
|
+
|
|
121
|
+
const shot = appData.screenshots?.find(s =>
|
|
122
|
+
feature.name.toLowerCase().includes(s.filename.toLowerCase().slice(0, 6))
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const classification = classifyFeatureForSlide(feature);
|
|
126
|
+
|
|
127
|
+
if (classification === 'kpi' && feature.name.includes('Kpi')) {
|
|
128
|
+
kpiSlide(slide, {
|
|
129
|
+
title: feature.display || feature.name,
|
|
130
|
+
kpis: generateKpiPlaceholders(feature),
|
|
131
|
+
}, theme);
|
|
132
|
+
} else if (classification === 'workflow') {
|
|
133
|
+
workflowSlide(slide, {
|
|
134
|
+
title: feature.display || feature.name,
|
|
135
|
+
steps: [`Acceder desde el menu principal`, 'Seleccionar opciones disponibles', 'Confirmar y generar resultado'],
|
|
136
|
+
}, theme);
|
|
137
|
+
} else {
|
|
138
|
+
featureSlide(slide, {
|
|
139
|
+
title: feature.display || feature.name,
|
|
140
|
+
description: `Module ubicado en src/ui/${feature.file}`,
|
|
141
|
+
bullets: [
|
|
142
|
+
'Clase exportada desde src/ui/',
|
|
143
|
+
'Integracion con Dashboard principal',
|
|
144
|
+
'Soporta dual theme automaticamente',
|
|
145
|
+
],
|
|
146
|
+
}, theme);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!shot) {
|
|
150
|
+
// Agregar placeholder si no hay screenshot
|
|
151
|
+
slide.addShape('rect', [1, 3.5, 8, 6], {
|
|
152
|
+
fill: { color: colors.surface },
|
|
153
|
+
line: { color: colors.border, width: 1, dashType: 'dash' },
|
|
154
|
+
});
|
|
155
|
+
slide.addText('📷 Screenshot: ' + (feature.display || feature.name), [1, 6, 8, 0.5], {
|
|
156
|
+
fontSize: 12, color: colors.textLight, fontFace: 'Inter', align: 'center',
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
screenshotSlide(slide, {
|
|
160
|
+
title: feature.display || feature.name,
|
|
161
|
+
description: `Módulo: src/ui/${feature.file}`,
|
|
162
|
+
screenshotPath: shot.path,
|
|
163
|
+
}, theme);
|
|
164
|
+
slide.clearShapes?.(); // remove placeholder
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
168
|
+
slideNum++;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ===== Workflows (modals) =====
|
|
172
|
+
if (appData.hasModals.length > 0) {
|
|
173
|
+
slide = pptx.addSlide();
|
|
174
|
+
slide.background = { color: colors.bg };
|
|
175
|
+
addSectionHeader(slide, 'FLUJOS DE TRABAJO', theme);
|
|
176
|
+
|
|
177
|
+
const steps = appData.hasModals.slice(0, 4).map((modal, i) => ({
|
|
178
|
+
title: `Modal: ${modal}`,
|
|
179
|
+
desc: 'Paso 1 → Paso 2 → Paso 3',
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
workflowSlide(slide, {
|
|
183
|
+
title: 'Modales y flujos de usuario',
|
|
184
|
+
steps,
|
|
185
|
+
}, theme);
|
|
186
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
187
|
+
slideNum++;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ===== Architecture =====
|
|
191
|
+
slide = pptx.addSlide();
|
|
192
|
+
slide.background = { color: colors.bg };
|
|
193
|
+
addSectionHeader(slide, 'ARQUITECTURA', theme);
|
|
194
|
+
architectureSlide(slide, {
|
|
195
|
+
title: 'Estructura de capas',
|
|
196
|
+
layers: [
|
|
197
|
+
{ name: 'UI', desc: 'Dashboard, Cards, Modals, Overlays', color: colors.accent },
|
|
198
|
+
{ name: 'Core', desc: 'Processor, Downloader, Models', color: colors.info },
|
|
199
|
+
{ name: 'Config', desc: 'Theme, Constants, Skill', color: colors.violet },
|
|
200
|
+
{ name: 'Data', desc: 'API S1, Catalogo JSON, Cache', color: colors.success },
|
|
201
|
+
],
|
|
202
|
+
}, theme);
|
|
203
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
204
|
+
slideNum++;
|
|
205
|
+
|
|
206
|
+
// ===== Checklist / Buenas prácticas =====
|
|
207
|
+
slide = pptx.addSlide();
|
|
208
|
+
slide.background = { color: colors.bg };
|
|
209
|
+
addSectionHeader(slide, 'BUENAS PRÁCTICAS', theme);
|
|
210
|
+
checklistSlide(slide, {
|
|
211
|
+
title: 'Recomendaciones de uso',
|
|
212
|
+
items: [
|
|
213
|
+
{ text: 'Mantener nombres de clases siguiendo PascalCase (KpiCard, Dashboard)', checked: true },
|
|
214
|
+
{ text: 'Usar theme.py para colores, nunca hardcodear #HEX', checked: true },
|
|
215
|
+
{ text: 'Registrar eventos con publish_g360_event() para comunicacion entre apps', checked: true },
|
|
216
|
+
{ text: 'Implementar shutdown() para limpieza de threads al cerrar', checked: true },
|
|
217
|
+
{ text: 'Documentar funciones con docstrings en español', checked: true },
|
|
218
|
+
{ text: 'Usar G360 Signature widget en el footer de la app', checked: true },
|
|
219
|
+
],
|
|
220
|
+
}, theme);
|
|
221
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
222
|
+
slideNum++;
|
|
223
|
+
|
|
224
|
+
// ===== Summary =====
|
|
225
|
+
slide = pptx.addSlide();
|
|
226
|
+
slide.background = { color: colors.bg };
|
|
227
|
+
addSectionHeader(slide, 'RESUMEN', theme);
|
|
228
|
+
|
|
229
|
+
const summaryItems = [
|
|
230
|
+
`Nombre: ${appData.name || 'N/A'}`,
|
|
231
|
+
`Version: ${appData.version || '1.0.0'}`,
|
|
232
|
+
`Framework: ${appData.framework || 'Flet'}`,
|
|
233
|
+
`Skills: ${appData.skill || 'N/A'}`,
|
|
234
|
+
`Caracteristicas: ${appData.features.length} modulos UI`,
|
|
235
|
+
appData.hasAutoRefresh ? 'Auto-refresh activado' : '',
|
|
236
|
+
appData.hasSearch ? 'Buscador flotante implementado' : '',
|
|
237
|
+
appData.hasExport ? 'Exportacion a Excel disponible' : '',
|
|
238
|
+
appData.hasModals.length > 0 ? `${appData.hasModals.length} modales de trabajo` : '',
|
|
239
|
+
].filter(Boolean);
|
|
240
|
+
|
|
241
|
+
checklistSlide(slide, {
|
|
242
|
+
title: 'Resumen de la aplicacion',
|
|
243
|
+
items: summaryItems.map(t => ({ text: t, checked: true })),
|
|
244
|
+
}, theme);
|
|
245
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
246
|
+
|
|
247
|
+
// Save
|
|
248
|
+
const outPath = options.outPath || path.join(process.cwd(), `${(appData.name || 'app').toLowerCase().replace(/\s+/g, '-')}-manual.pptx`);
|
|
249
|
+
await pptx.writeFile({ fileName: outPath });
|
|
250
|
+
return outPath;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Classifica feature para decidir que slide usar.
|
|
255
|
+
*/
|
|
256
|
+
function classifyFeatureForSlide(feature) {
|
|
257
|
+
if (feature.name.includes('Kpi')) return 'kpi';
|
|
258
|
+
if (feature.name.includes('Modal') || feature.name.includes('Dialog')) return 'workflow';
|
|
259
|
+
if (feature.name.includes('Dashboard')) return 'screenshot';
|
|
260
|
+
return 'feature';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Genera placeholders de KPIs basado en features detectadas.
|
|
265
|
+
*/
|
|
266
|
+
function generateKpiPlaceholders(feature) {
|
|
267
|
+
return [
|
|
268
|
+
{ label: 'Total', value: '—', color: '#64748B' },
|
|
269
|
+
{ label: 'Con Stock', value: '—', color: '#34D399' },
|
|
270
|
+
{ label: 'Sin Stock', value: '—', color: '#EF4444' },
|
|
271
|
+
{ label: 'Alertas', value: '—', color: '#F59E0B' },
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
import path from 'path';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tema G360 — paleta esmeralda, fuente Inter, fondo oscuro/claro.
|
|
3
|
+
* Usa los colores definidos en skill.json del proyecto (fallback a valores G360).
|
|
4
|
+
*/
|
|
5
|
+
import PptxGenJS from 'pptxgenjs';
|
|
6
|
+
|
|
7
|
+
export function createG360Theme() {
|
|
8
|
+
const colors = {
|
|
9
|
+
accent: '#10B981',
|
|
10
|
+
accentDark: '#047857',
|
|
11
|
+
success: '#34D399',
|
|
12
|
+
warning: '#F59E0B',
|
|
13
|
+
error: '#EF4444',
|
|
14
|
+
text: '#0F172A',
|
|
15
|
+
textLight: '#64748B',
|
|
16
|
+
bg: '#FFFFFF',
|
|
17
|
+
surface: '#F3F5F9',
|
|
18
|
+
border: '#E3E8F0',
|
|
19
|
+
darkBg: '#0A0F1E',
|
|
20
|
+
darkText: '#F1F5FB',
|
|
21
|
+
darkSurface: '#141D33',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Definir layout A4 vertical para manual (portrait)
|
|
25
|
+
const pptx = new PptxGenJS();
|
|
26
|
+
pptx.defineLayout({ name: 'A4', width: 10, height: 14.28 }); // A4 portrait en pulgadas
|
|
27
|
+
pptx.defineLayout({ name: 'LAYOUT_16x9', width: 10, height: 5.625 });
|
|
28
|
+
pptx.defineLayout({ name: 'LAYOUT_4x3', width: 10, height: 7.5 });
|
|
29
|
+
|
|
30
|
+
// Usar A4 como default (manual mode)
|
|
31
|
+
pptx.layout = 'A4';
|
|
32
|
+
|
|
33
|
+
// Theme global
|
|
34
|
+
pptx.author = 'g360-cli';
|
|
35
|
+
pptx.title = '';
|
|
36
|
+
pptx.subject = 'Documentacion generada por g360-cli';
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
pptx,
|
|
40
|
+
colors,
|
|
41
|
+
layout: 'A4',
|
|
42
|
+
fonts: ['Inter', 'Arial'],
|
|
43
|
+
/**
|
|
44
|
+
* Agregar logo de marca a la presentacion
|
|
45
|
+
*/
|
|
46
|
+
setLogo(pptxObj, brand, width = 1.2, height = 0.6) {
|
|
47
|
+
const logoPath = brand === 'cipsa'
|
|
48
|
+
? path.join(__dirname, '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.svg')
|
|
49
|
+
: path.join(__dirname, '..', 'brand', 'g360', 'logotypes', 'logo-g360-light.svg');
|
|
50
|
+
// Nota: SVG no soportado nativamente por PptxGenJS; fallback a PNG
|
|
51
|
+
const pngPath = brand === 'cipsa'
|
|
52
|
+
? path.join(__dirname, '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.png')
|
|
53
|
+
: path.join(__dirname, '..', 'brand', 'g360', 'logotypes', 'logo-g360-dark.png');
|
|
54
|
+
if (require('fs').existsSync(pngPath)) {
|
|
55
|
+
return { path: pngPath, width, height };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
import path from 'path';
|
|
63
|
+
import fs from 'fs';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tema CIPSA — verde corporativo con logo CIPSA.
|
|
3
|
+
*/
|
|
4
|
+
import PptxGenJS from 'pptxgenjs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
export function createCipsaTheme() {
|
|
9
|
+
const colors = {
|
|
10
|
+
accent: '#00d084',
|
|
11
|
+
accentDark: '#00796B',
|
|
12
|
+
success: '#22c55e',
|
|
13
|
+
warning: '#f59e0b',
|
|
14
|
+
error: '#ef4444',
|
|
15
|
+
text: '#0F172A',
|
|
16
|
+
textLight: '#64748B',
|
|
17
|
+
bg: '#FFFFFF',
|
|
18
|
+
surface: '#F0F9F4',
|
|
19
|
+
border: '#CCF0E0',
|
|
20
|
+
darkBg: '#0d1117',
|
|
21
|
+
darkText: '#f0f6fc',
|
|
22
|
+
darkSurface: '#161b22',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const pptx = new PptxGenJS();
|
|
26
|
+
pptx.defineLayout({ name: 'A4', width: 10, height: 14.28 });
|
|
27
|
+
pptx.defineLayout({ name: 'LAYOUT_16x9', width: 10, height: 5.625 });
|
|
28
|
+
pptx.layout = 'A4';
|
|
29
|
+
|
|
30
|
+
pptx.author = 'g360-cli';
|
|
31
|
+
pptx.title = '';
|
|
32
|
+
pptx.subject = 'Documentacion CIPSA generada por g360-cli';
|
|
33
|
+
|
|
34
|
+
const cipsaLogo = path.join(process.cwd(), 'assets', 'images', 'Logo_cipsa_solid.png');
|
|
35
|
+
const fallbackLogo = fs.existsSync(cipsaLogo)
|
|
36
|
+
? cipsaLogo
|
|
37
|
+
: path.join(__dirname, '..', '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.png');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
pptx,
|
|
41
|
+
colors,
|
|
42
|
+
layout: 'A4',
|
|
43
|
+
fonts: ['Inter', 'Arial'],
|
|
44
|
+
logo: fs.existsSync(fallbackLogo) ? { path: fallbackLogo, width: 1.5, height: 0.5 } : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tema G360 — paleta esmeralda, fuente Inter.
|
|
3
|
+
*/
|
|
4
|
+
import PptxGenJS from 'pptxgenjs';
|
|
5
|
+
|
|
6
|
+
const colors = {
|
|
7
|
+
accent: '#10B981',
|
|
8
|
+
accentDark: '#047857',
|
|
9
|
+
success: '#34D399',
|
|
10
|
+
warning: '#F59E0B',
|
|
11
|
+
error: '#EF4444',
|
|
12
|
+
info: '#3B82F6',
|
|
13
|
+
violet: '#8B5CF6',
|
|
14
|
+
text: '#0F172A',
|
|
15
|
+
textLight: '#64748B',
|
|
16
|
+
bg: '#FFFFFF',
|
|
17
|
+
surface: '#F3F5F9',
|
|
18
|
+
border: '#E3E8F0',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createG360Theme() {
|
|
22
|
+
const pptx = new PptxGenJS();
|
|
23
|
+
pptx.defineLayout({ name: 'A4', width: 10, height: 14.28 });
|
|
24
|
+
pptx.defineLayout({ name: 'LAYOUT_16x9', width: 10, height: 5.625 });
|
|
25
|
+
pptx.layout = 'A4';
|
|
26
|
+
pptx.author = 'g360-cli';
|
|
27
|
+
pptx.subject = 'Documentacion generada por g360-cli';
|
|
28
|
+
|
|
29
|
+
return { pptx, colors, layout: 'A4' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { colors };
|
|
@@ -132,6 +132,45 @@ Colores semanticos definidos en `skill.json`, accesibles via `get_colors(mode)`:
|
|
|
132
132
|
| KPI red | `#EF4444` | `#DC2626` | Indicador critico |
|
|
133
133
|
| KPI amber | `#F59E0B` | `#B45309` | Indicador warning |
|
|
134
134
|
|
|
135
|
+
### Personalizar Colores
|
|
136
|
+
|
|
137
|
+
Los colores se leen automaticamente de `skill.json`. Para personalizar:
|
|
138
|
+
|
|
139
|
+
**Opcion 1: Modificar skill.json**
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"colors": {
|
|
143
|
+
"accent": "#2563eb", // Azul corporativo
|
|
144
|
+
"surface": "#1e293b", // Gris oscuro
|
|
145
|
+
"success": "#16a34a", // Verde mas oscuro
|
|
146
|
+
...
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Opcion 2: Sobrescribir en runtime**
|
|
152
|
+
```python
|
|
153
|
+
from src.config.theme import set_brand_colors
|
|
154
|
+
|
|
155
|
+
set_brand_colors({
|
|
156
|
+
"dark": {
|
|
157
|
+
"accent": "#2563eb",
|
|
158
|
+
"surface": "#1e273e",
|
|
159
|
+
},
|
|
160
|
+
"light": {
|
|
161
|
+
"accent": "#1d4ed8",
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Opcion 3: Usar paleta existente**
|
|
167
|
+
- **Verde esmeralda** (default): `#10B981` — Moderno, fresco
|
|
168
|
+
- **Verde corporativo**: `#2563eb` — Azul/verde profesional
|
|
169
|
+
- **Verde bosque**: `#059669` — Mas sobrio, enterprise
|
|
170
|
+
- **Verde fluorescente**: `#34d399` — Mas vibrante, tech
|
|
171
|
+
|
|
172
|
+
> **Nota**: Todos los colores deben ser validos hex (`#RRGGBB`). El sistema usa defaults G360 y aplica overrides solo de los colores especificados.
|
|
173
|
+
|
|
135
174
|
## Arquitectura
|
|
136
175
|
|
|
137
176
|
```
|
|
@@ -6,6 +6,13 @@ import traceback
|
|
|
6
6
|
from logging.handlers import RotatingFileHandler
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
# Import pip-system-certs to fix SSL issues on Windows corporate networks
|
|
10
|
+
try:
|
|
11
|
+
import pip_system_certs
|
|
12
|
+
pip_system_certs.install()
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass # Not available, continue without it
|
|
15
|
+
|
|
9
16
|
import flet as ft
|
|
10
17
|
|
|
11
18
|
BASE_DIR = Path(__file__).resolve().parent
|
|
@@ -6,16 +6,30 @@
|
|
|
6
6
|
"brand": "g360",
|
|
7
7
|
"description": "G360 Flet Polished - Plantilla base con patrones de UI avanzada",
|
|
8
8
|
"framework": "flet",
|
|
9
|
+
"flet_version": "0.28.3",
|
|
9
10
|
"portable": true,
|
|
10
11
|
"colors": {
|
|
11
12
|
"bg": "#0b1220",
|
|
12
13
|
"surface": "#1a2333",
|
|
13
14
|
"accent": "#34d399",
|
|
15
|
+
"accent_dark": "#047857",
|
|
14
16
|
"text": "#f0f6fc",
|
|
15
17
|
"muted": "#8b949e",
|
|
16
18
|
"success": "#34d399",
|
|
17
19
|
"warning": "#f59e0b",
|
|
18
|
-
"error": "#ef4444"
|
|
20
|
+
"error": "#ef4444",
|
|
21
|
+
"info": "#3b82f6",
|
|
22
|
+
"violet": "#8b5cf6",
|
|
23
|
+
"pink": "#ec4899",
|
|
24
|
+
"cyan": "#06b6d4",
|
|
25
|
+
"orange": "#f97316",
|
|
26
|
+
"surface_variant": "#1b2740",
|
|
27
|
+
"surface_sunken": "#0e1627",
|
|
28
|
+
"background": "#0a0f1e",
|
|
29
|
+
"border": "#ffffff17",
|
|
30
|
+
"text_muted": "#8fa0ba",
|
|
31
|
+
"text_primary": "#f1f5fb",
|
|
32
|
+
"text_secondary": "#c9d4e6"
|
|
19
33
|
},
|
|
20
34
|
"effects": {
|
|
21
35
|
"glassmorphism": true,
|
|
@@ -24,5 +38,17 @@
|
|
|
24
38
|
"signature": {
|
|
25
39
|
"mode": "powered",
|
|
26
40
|
"text": "powered by G360"
|
|
41
|
+
},
|
|
42
|
+
"events": [
|
|
43
|
+
"app:{name}:refresh",
|
|
44
|
+
"app:{name}:data:update",
|
|
45
|
+
"app:{name}:theme:change",
|
|
46
|
+
"g360:app:list",
|
|
47
|
+
"g360:app:register"
|
|
48
|
+
],
|
|
49
|
+
"endpoints": {
|
|
50
|
+
"health": "/api/health",
|
|
51
|
+
"data": "/api/data",
|
|
52
|
+
"events": "/api/events"
|
|
27
53
|
}
|
|
28
54
|
}
|
|
@@ -24,6 +24,7 @@ from src.core.constants import (
|
|
|
24
24
|
get_app_name,
|
|
25
25
|
)
|
|
26
26
|
from src.ui.dashboard import Dashboard
|
|
27
|
+
from src.core.g360_registry import register_g360_app, get_event_bus
|
|
27
28
|
|
|
28
29
|
import logging
|
|
29
30
|
_log_logger = logging.getLogger("g360.app")
|
|
@@ -156,6 +157,9 @@ class G360App:
|
|
|
156
157
|
self.dashboard.register_overlay()
|
|
157
158
|
_log("_build: overlay (FilePickers) registrado")
|
|
158
159
|
|
|
160
|
+
# Registrar app en registry G360
|
|
161
|
+
self._register_app()
|
|
162
|
+
|
|
159
163
|
sample_path = Path(__file__).resolve().parent.parent / "assets" / "data" / "sample_data.json"
|
|
160
164
|
cache, ts = _load_cache()
|
|
161
165
|
if cache:
|
|
@@ -215,8 +219,38 @@ class G360App:
|
|
|
215
219
|
except Exception as ex:
|
|
216
220
|
_log(f"_start_auto_refresh: ERROR {ex}")
|
|
217
221
|
|
|
222
|
+
def _register_app(self):
|
|
223
|
+
"""Registrar la app en el registry G360."""
|
|
224
|
+
try:
|
|
225
|
+
skill_data = self._load_skill_config()
|
|
226
|
+
events = skill_data.get("events", [])
|
|
227
|
+
endpoints = skill_data.get("endpoints", {})
|
|
228
|
+
|
|
229
|
+
register_g360_app(
|
|
230
|
+
name=get_app_name(),
|
|
231
|
+
version=self._local_version,
|
|
232
|
+
skill=skill_data.get("skill", "custom"),
|
|
233
|
+
framework="flet",
|
|
234
|
+
events=events,
|
|
235
|
+
endpoints=endpoints,
|
|
236
|
+
description=skill_data.get("description", ""),
|
|
237
|
+
)
|
|
238
|
+
_log(f"_register_app: {get_app_name()} registrado en G360 registry")
|
|
239
|
+
except Exception as ex:
|
|
240
|
+
_log(f"_register_app: ERROR {ex}")
|
|
241
|
+
|
|
242
|
+
def _load_skill_config(self) -> dict:
|
|
243
|
+
"""Cargar config del skill actual."""
|
|
244
|
+
import json
|
|
245
|
+
from pathlib import Path
|
|
246
|
+
skill_path = Path(__file__).resolve().parent.parent.parent / "skill.json"
|
|
247
|
+
if skill_path.exists():
|
|
248
|
+
with open(skill_path, encoding="utf-8") as f:
|
|
249
|
+
return json.load(f)
|
|
250
|
+
return {}
|
|
251
|
+
|
|
218
252
|
def shutdown(self):
|
|
219
|
-
"""Detiene el auto-refresh
|
|
253
|
+
"""Detiene el auto-refresh y desregistra la app antes de cerrar."""
|
|
220
254
|
if self._auto_refresh_stop:
|
|
221
255
|
_log("_shutdown: deteniendo auto-refresh...")
|
|
222
256
|
self._auto_refresh_stop.set()
|
|
@@ -224,6 +258,15 @@ class G360App:
|
|
|
224
258
|
if t and t.is_alive():
|
|
225
259
|
t.join(timeout=3)
|
|
226
260
|
_log("_shutdown: auto-refresh detenido")
|
|
261
|
+
|
|
262
|
+
# Desregistrar del registry
|
|
263
|
+
try:
|
|
264
|
+
from src.core.g360_registry import get_app_registry
|
|
265
|
+
registry = get_app_registry()
|
|
266
|
+
registry.update_status(get_app_name(), "offline")
|
|
267
|
+
_log(f"_shutdown: {get_app_name()} desregistrado")
|
|
268
|
+
except Exception as ex:
|
|
269
|
+
_log(f"_shutdown: ERROR desregistrando {ex}")
|
|
227
270
|
|
|
228
271
|
def _auto_refresh_loop(self):
|
|
229
272
|
while not self._auto_refresh_stop.is_set():
|