g360-cli 1.13.1 → 1.15.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.
Files changed (77) hide show
  1. package/package.json +71 -71
  2. package/src/assets/config/g360-skills.json +165 -0
  3. package/src/assets/config/project-types.json +23 -4
  4. package/src/assets/snippets/snippets.json +4 -0
  5. package/src/assets/templates/lit-web/src/index.js +1 -1
  6. package/src/assets/templates/python-cli/src/core/__init__.py +0 -0
  7. package/src/assets/templates/python-flet/README.md +6 -1
  8. package/src/assets/templates/python-flet/pyproject.toml +2 -2
  9. package/src/assets/templates/python-flet/src/core/skill.json +1 -1
  10. package/src/assets/templates/python-flet-migrate/pyproject.toml +1 -3
  11. package/src/assets/templates/python-flet-polished/README.md +170 -0
  12. package/src/assets/templates/python-flet-polished/assets/data/sample_data.json +12 -0
  13. package/src/assets/templates/python-flet-polished/assets/fonts/Inter-Variable.ttf +0 -0
  14. package/src/assets/templates/python-flet-polished/assets/fonts/JetBrainsMono-Variable.ttf +0 -0
  15. package/src/assets/templates/python-flet-polished/assets/images/app.ico +0 -0
  16. package/src/assets/templates/python-flet-polished/assets/images/logo.svg +9 -0
  17. package/src/assets/templates/python-flet-polished/build-portable.bat +42 -0
  18. package/src/assets/templates/python-flet-polished/create_shortcut.vbs +15 -0
  19. package/src/assets/templates/python-flet-polished/g360_flet/__init__.py +0 -0
  20. package/src/assets/templates/python-flet-polished/g360_flet/g360_signature.py +210 -0
  21. package/src/assets/templates/python-flet-polished/launch.vbs +3 -0
  22. package/src/assets/templates/python-flet-polished/launch_minimized.bat +3 -0
  23. package/src/assets/templates/python-flet-polished/main.py +47 -0
  24. package/src/assets/templates/python-flet-polished/pyproject.toml +18 -0
  25. package/src/assets/templates/python-flet-polished/requirements.txt +3 -0
  26. package/src/assets/templates/python-flet-polished/run.bat +189 -0
  27. package/src/assets/templates/python-flet-polished/skill.json +28 -0
  28. package/src/assets/templates/python-flet-polished/src/__init__.py +0 -0
  29. package/src/assets/templates/python-flet-polished/src/app.py +343 -0
  30. package/src/assets/templates/python-flet-polished/src/config/__init__.py +0 -0
  31. package/src/assets/templates/python-flet-polished/src/config/theme.py +122 -0
  32. package/src/assets/templates/python-flet-polished/src/core/__init__.py +0 -0
  33. package/src/assets/templates/python-flet-polished/src/core/constants.py +49 -0
  34. package/src/assets/templates/python-flet-polished/src/core/processor.py +128 -0
  35. package/src/assets/templates/python-flet-polished/src/core/skill.json +40 -0
  36. package/src/assets/templates/python-flet-polished/src/ui/__init__.py +0 -0
  37. package/src/assets/templates/python-flet-polished/src/ui/dashboard.py +308 -0
  38. package/src/assets/templates/python-flet-polished/src/ui/kpi_card.py +69 -0
  39. package/src/assets/templates/python-flet-polished/src/ui/search_overlay.py +143 -0
  40. package/src/assets/templates/python-flet-polished/sync_portable.py +71 -0
  41. package/src/assets/templates/solid-web/src/components/App.jsx +2 -2
  42. package/src/cli.js +4 -4
  43. package/src/commands/clean.js +0 -12
  44. package/src/commands/docs.js +105 -1
  45. package/src/commands/ingest.js +11 -61
  46. package/src/commands/init.js +34 -3
  47. package/src/commands/lint.js +35 -77
  48. package/src/commands/scan.js +9 -37
  49. package/src/commands/validate.js +6 -36
  50. package/src/lib/file-utils.js +89 -2
  51. package/src/lib/python-runner.js +77 -0
  52. package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
  53. package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
  54. package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
  55. package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
  56. package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
  57. package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
  58. package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
  59. package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
  60. package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
  61. package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
  62. package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
  63. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
  64. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
  65. package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
  66. package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
  67. package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
  68. package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
  69. package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
  70. package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
  71. package/src/assets/ingestion/core/__pycache__/ingestion.cpython-312.pyc +0 -0
  72. package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-312.pyc +0 -0
  73. package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-312.pyc +0 -0
  75. package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-314.pyc +0 -0
  76. package/src/assets/templates/python-cli/package.json +0 -7
  77. /package/src/assets/config/{skills.json → agent-skills.json} +0 -0
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ import flet as ft
12
+
13
+ from src.config.theme import get_colors, load_theme_preference, save_theme_preference
14
+ from src.core.constants import (
15
+ AUTO_REFRESH_INTERVAL,
16
+ CACHE_FILE,
17
+ VERSION_CACHE_FILE,
18
+ VERSION_CHECK_INTERVAL,
19
+ WINDOW_WIDTH,
20
+ WINDOW_HEIGHT,
21
+ WINDOW_MIN_WIDTH,
22
+ WINDOW_MIN_HEIGHT,
23
+ get_local_version,
24
+ get_app_name,
25
+ )
26
+ from src.ui.dashboard import Dashboard
27
+
28
+ import logging
29
+ _log_logger = logging.getLogger("g360.app")
30
+ if not _log_logger.handlers:
31
+ from logging.handlers import RotatingFileHandler
32
+ _log_path = Path(__file__).resolve().parent.parent.parent / "run_log.txt"
33
+ _fmt = logging.Formatter("[%(asctime)s] %(message)s", datefmt="%H:%M:%S")
34
+ _handler = RotatingFileHandler(_log_path, maxBytes=2 * 1024 * 1024, backupCount=3, encoding="utf-8")
35
+ _handler.setFormatter(_fmt)
36
+ _log_logger.addHandler(_handler)
37
+ _log_logger.setLevel(logging.INFO)
38
+
39
+
40
+ def _log(msg: str):
41
+ _log_logger.info(msg)
42
+
43
+
44
+ def _load_cache() -> tuple[dict, str | None]:
45
+ """Carga el cache persistente. Retorna (raw_data, timestamp)."""
46
+ if not CACHE_FILE.exists():
47
+ return {}, None
48
+ try:
49
+ with open(CACHE_FILE, encoding="utf-8") as f:
50
+ data = json.load(f)
51
+ raw = data.get("raw_data", {})
52
+ ts = data.get("timestamp")
53
+ if raw:
54
+ return raw, ts
55
+ except (json.JSONDecodeError, UnicodeDecodeError) as ex:
56
+ _log(f"_load_cache: corrupt cache file, removing: {ex}")
57
+ try:
58
+ CACHE_FILE.unlink(missing_ok=True)
59
+ except Exception:
60
+ pass
61
+ except Exception:
62
+ pass
63
+ return {}, None
64
+
65
+
66
+ def _hash_data(raw_data: dict) -> str:
67
+ """SHA-256 del raw_data para detectar cambios sin descargar dos veces."""
68
+ serialized = json.dumps(raw_data, sort_keys=True, ensure_ascii=False).encode()
69
+ return hashlib.sha256(serialized).hexdigest()[:16]
70
+
71
+
72
+ def _data_changed(new_data: dict, last_hash: str | None) -> tuple[bool, str]:
73
+ """Retorna (changed, nuevo_hash)."""
74
+ h = _hash_data(new_data)
75
+ return h != last_hash, h
76
+
77
+
78
+ def _save_cache(raw_data: dict, api_timestamp: str | None = None):
79
+ """Guarda raw_data completo."""
80
+ try:
81
+ CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
82
+ payload = {
83
+ "raw_data": raw_data,
84
+ "timestamp": api_timestamp or datetime.now().isoformat(),
85
+ "api_timestamp": api_timestamp,
86
+ }
87
+ with open(CACHE_FILE, "w", encoding="utf-8") as f:
88
+ json.dump(payload, f, ensure_ascii=False)
89
+ except Exception as ex:
90
+ _log(f"_save_cache: ERROR {ex}")
91
+
92
+
93
+ class G360App:
94
+ """
95
+ Clase principal de la aplicacion G360 con Flet.
96
+
97
+ Patrones implementados:
98
+ - Dual theme (dark/light) con persistencia en ~/.g360/
99
+ - Auto-refresh con lock thread-safe
100
+ - Cache con hash diff para evitar rebuild innecesarios
101
+ - Logger RotatingFileHandler
102
+ - Shutdown limpio via page.on_close
103
+ """
104
+
105
+ def __init__(self, page: ft.Page):
106
+ try:
107
+ self.page = page
108
+ self._raw_data: dict = {}
109
+ self._theme_mode = load_theme_preference()
110
+ self._setup_page()
111
+ self.dashboard = Dashboard(page, self._theme_mode)
112
+ if hasattr(self.dashboard, "set_on_theme_toggle"):
113
+ self.dashboard.set_on_theme_toggle(self._toggle_theme)
114
+ self.dashboard.set_on_refresh(self._on_refresh)
115
+ self._cache_timestamp: str | None = None
116
+ self._stale_data: bool = False
117
+ self._local_version = get_local_version()
118
+ self._last_auto_refresh: float = 0.0
119
+ self._auto_refresh_stop: threading.Event | None = None
120
+ self._download_lock = threading.Lock()
121
+ self._last_data_hash: str | None = None
122
+ self._build()
123
+ except Exception:
124
+ _log(f"[FATAL] G360App.__init__:\n{traceback.format_exc()}")
125
+ raise
126
+
127
+ def _setup_page(self):
128
+ app_name = get_app_name()
129
+ self.page.title = f"{app_name.title()} - G360"
130
+ self.page.theme_mode = ft.ThemeMode.DARK if self._theme_mode == "dark" else ft.ThemeMode.LIGHT
131
+ self.page.bgcolor = get_colors(self._theme_mode)["background"]
132
+ self.page.padding = 0
133
+ self.page.window_width = WINDOW_WIDTH
134
+ self.page.window_height = WINDOW_HEIGHT
135
+ self.page.window_min_width = WINDOW_MIN_WIDTH
136
+ self.page.window_min_height = WINDOW_MIN_HEIGHT
137
+ try:
138
+ self.page.window_center()
139
+ except AttributeError:
140
+ pass
141
+
142
+ fonts_dir = Path(__file__).resolve().parent.parent.parent / "assets" / "fonts"
143
+ self.page.fonts = {
144
+ "Inter": str(fonts_dir / "Inter-Variable.ttf"),
145
+ "JetBrains Mono": str(fonts_dir / "JetBrainsMono-Variable.ttf"),
146
+ }
147
+ self.page.theme = ft.Theme(font_family="Inter")
148
+
149
+ def _build(self):
150
+ _log("_build: starting dashboard.build()...")
151
+ view = self.dashboard.build()
152
+ _log("_build: dashboard.build() OK")
153
+ self.page.add(view)
154
+ self.page.update()
155
+ _log("_build: page.add + update OK")
156
+ self.dashboard.register_overlay()
157
+ _log("_build: overlay (FilePickers) registrado")
158
+
159
+ sample_path = Path(__file__).resolve().parent.parent / "assets" / "data" / "sample_data.json"
160
+ cache, ts = _load_cache()
161
+ if cache:
162
+ _log(f"_build: loading cached data...")
163
+ self._raw_data = cache
164
+ self._cache_timestamp = ts
165
+ _, initial_hash = _data_changed(cache, None)
166
+ self._last_data_hash = initial_hash
167
+ self.dashboard.update_data(cache, cache_timestamp=ts)
168
+ self.page.update()
169
+ _log(f"_build: cache loaded OK ({len(cache)} items)")
170
+ elif sample_path.exists():
171
+ _log("_build: cache empty, loading sample_data.json...")
172
+ with open(sample_path, encoding="utf-8") as f:
173
+ raw = json.load(f)
174
+ self._raw_data = raw
175
+ self.dashboard.update_data(raw)
176
+ self.page.update()
177
+ _log("_build: sample_data loaded OK")
178
+ else:
179
+ _log("_build: neither cache nor sample_data.json found")
180
+ self.dashboard._show_empty_state("Sin datos disponibles")
181
+ self.dashboard._set_empty_state_status("Esperando datos", self.dashboard.c["warning"])
182
+
183
+ _log("_build: scheduling _delayed_load...")
184
+ self.page.run_task(self._delayed_load)
185
+ self._start_auto_refresh()
186
+ _log("_build: done")
187
+
188
+ def _toggle_theme(self):
189
+ self._theme_mode = "light" if self._theme_mode == "dark" else "dark"
190
+ save_theme_preference(self._theme_mode)
191
+ self.page.theme_mode = ft.ThemeMode.DARK if self._theme_mode == "dark" else ft.ThemeMode.LIGHT
192
+ self.page.bgcolor = get_colors(self._theme_mode)["background"]
193
+ self.dashboard.update_theme(self._theme_mode)
194
+ self.page.update()
195
+
196
+ async def _delayed_load(self):
197
+ import asyncio
198
+ await asyncio.sleep(0.5)
199
+ await self._load_data()
200
+
201
+ async def _on_refresh(self):
202
+ await self._load_data(is_manual=True)
203
+
204
+ def _start_auto_refresh(self):
205
+ try:
206
+ self._auto_refresh_stop = threading.Event()
207
+ t = threading.Thread(
208
+ target=self._auto_refresh_loop,
209
+ daemon=True,
210
+ name="auto-refresh",
211
+ )
212
+ t.start()
213
+ self._auto_refresh_thread = t
214
+ _log(f"_start_auto_refresh: hilo iniciado ({AUTO_REFRESH_INTERVAL}s)")
215
+ except Exception as ex:
216
+ _log(f"_start_auto_refresh: ERROR {ex}")
217
+
218
+ def shutdown(self):
219
+ """Detiene el auto-refresh de forma limpia antes de cerrar."""
220
+ if self._auto_refresh_stop:
221
+ _log("_shutdown: deteniendo auto-refresh...")
222
+ self._auto_refresh_stop.set()
223
+ t = getattr(self, "_auto_refresh_thread", None)
224
+ if t and t.is_alive():
225
+ t.join(timeout=3)
226
+ _log("_shutdown: auto-refresh detenido")
227
+
228
+ def _auto_refresh_loop(self):
229
+ while not self._auto_refresh_stop.is_set():
230
+ time.sleep(1)
231
+ try:
232
+ self._on_auto_refresh_tick()
233
+ except Exception as ex:
234
+ _log(f"_auto_refresh_loop: ERROR {ex}")
235
+
236
+ def _on_auto_refresh_tick(self):
237
+ try:
238
+ if not self._cache_timestamp:
239
+ return
240
+ self.dashboard._update_refresh_status(self._cache_timestamp)
241
+ if time.time() - self._last_auto_refresh < AUTO_REFRESH_INTERVAL:
242
+ return
243
+ self._last_auto_refresh = time.time()
244
+ _log("_on_auto_refresh_tick: ejecutando auto-refresh")
245
+ self.page.run_task(self._load_data, is_manual=False)
246
+ except Exception as ex:
247
+ _log(f"_on_auto_refresh_tick: ERROR {ex}")
248
+
249
+ async def _load_data(self, is_manual=False):
250
+ import time
251
+ _t0 = time.time()
252
+ _log(f"_load_data: start is_manual={is_manual}")
253
+ self.dashboard.set_loading(True, "Descargando datos...")
254
+
255
+ if not self._download_lock.acquire(blocking=False):
256
+ _log("_load_data: otra descarga en curso, omitiendo")
257
+ self.dashboard.set_loading(False)
258
+ return
259
+
260
+ try:
261
+ import asyncio
262
+ loop = asyncio.get_running_loop()
263
+ _log("_load_data: calling _fetch_data in executor...")
264
+ raw = await loop.run_in_executor(None, self._fetch_data)
265
+ _log(f"_load_data: _fetch_data returned {type(raw).__name__}")
266
+
267
+ if not raw:
268
+ self.dashboard.set_loading(False)
269
+ self.dashboard.status_text.value = "No se obtuvieron datos"
270
+ self.dashboard.status_text.color = "#ef4444"
271
+ self.dashboard._show_snack("Error: No se obtuvieron datos", is_error=True)
272
+ if not self._raw_data:
273
+ self.dashboard._set_empty_state_status("Sin datos - API no disponible", self.dashboard.c["error"])
274
+ _log("_load_data: no data, showing error")
275
+ return
276
+
277
+ changed, new_hash = _data_changed(raw, self._last_data_hash)
278
+ if not changed:
279
+ _log("_load_data: datos identicos, omitiendo UI refresh")
280
+ self.dashboard.set_loading(False)
281
+ self._download_lock.release()
282
+ return
283
+
284
+ self._raw_data = raw
285
+ self._last_data_hash = new_hash
286
+ _save_cache(raw)
287
+ self._cache_timestamp = datetime.now().isoformat()
288
+ self._last_auto_refresh = time.time()
289
+ _log("_load_data: cache saved")
290
+ self.dashboard.update_data(raw, cache_timestamp=self._cache_timestamp, stale=self._stale_data)
291
+ self.dashboard._hide_empty_state()
292
+ _log("_load_data: data updated in dashboard")
293
+ if is_manual:
294
+ self.dashboard._show_snack("Datos actualizados correctamente")
295
+ except Exception as ex:
296
+ _log(f"_load_data: EXCEPTION: {traceback.format_exc()}")
297
+ self.dashboard.status_text.value = f"Error: {str(ex)}"
298
+ self.dashboard.status_text.color = "#ef4444"
299
+ self.dashboard._show_snack(f"Fallo en la descarga: {str(ex)}", is_error=True)
300
+ self.dashboard.set_offline(True)
301
+ if not self._raw_data:
302
+ self.dashboard._set_empty_state_status("Error de conexión", self.dashboard.c["error"])
303
+ finally:
304
+ elapsed = time.time() - _t0
305
+ _log(f"_load_data: elapsed={elapsed:.1f}s")
306
+ self._download_lock.release()
307
+ if elapsed < 2:
308
+ self.dashboard.set_loading(True, "Actualizando vista...")
309
+ self.page.update()
310
+ import asyncio
311
+ await asyncio.sleep(2 - elapsed)
312
+ ts = self.dashboard.format_cache_timestamp(self._cache_timestamp) if not is_manual else ""
313
+ self.dashboard._ts_text.value = f"Ultima act. {ts}"
314
+ if self._stale_data:
315
+ self.dashboard._ts_text.color = self.dashboard.c["warning"]
316
+ self.dashboard._ts_text.value += " (caché)"
317
+ self.dashboard.status_text.value = "Datos en caché (fuera de horario)"
318
+ self.dashboard.status_text.color = self.dashboard.c["warning"]
319
+ self.dashboard._show_stale_warning()
320
+ else:
321
+ self.dashboard._ts_text.color = self.dashboard.c["accent"]
322
+ self.dashboard.status_text.value = "Datos actualizados"
323
+ self.dashboard.status_text.color = self.dashboard.c["accent"]
324
+ self.dashboard._hide_stale_warning()
325
+ self.dashboard._update_refresh_status(self._cache_timestamp)
326
+ self.dashboard.set_offline(False)
327
+ self.dashboard.set_loading(False)
328
+ self.page.update()
329
+ _log("_load_data: done")
330
+
331
+ def _fetch_data(self) -> dict | None:
332
+ """
333
+ Metodo que debe ser sobrescrito por subclases.
334
+ Implementa la logica de descarga/fetch de datos.
335
+ Retorna dict con los datos crudos o None si falla.
336
+ """
337
+ # Default: retorna None (no hay data)
338
+ # Subclases deben override这个方法
339
+ return None
340
+
341
+
342
+ def main(page: ft.Page):
343
+ G360App(page)
@@ -0,0 +1,122 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ ACCENT = "#10B981"
5
+ ACCENT_DARK = "#047857"
6
+
7
+ DARK_COLORS = {
8
+ "accent": "#10B981",
9
+ "accent_dark": "#047857",
10
+ "success": "#34D399",
11
+ "warning": "#F59E0B",
12
+ "error": "#EF4444",
13
+ "info": "#3B82F6",
14
+ "violet": "#8B5CF6",
15
+ "pink": "#EC4899",
16
+ "cyan": "#06B6D4",
17
+ "orange": "#F97316",
18
+ "kpis": {
19
+ "primary": "#06B6D4",
20
+ "secondary": "#8B5CF6",
21
+ "tertiary": "#0EA5E9",
22
+ "quaternary": "#6366F1",
23
+ "quinary": "#EC4899",
24
+ "senary": "#F59E0B",
25
+ "septenary": "#EF4444",
26
+ "octonary": "#F97316",
27
+ },
28
+ "surface": "#141D33",
29
+ "surface_variant": "#1B2740",
30
+ "surface_sunken": "#0E1627",
31
+ "background": "#0A0F1E",
32
+ "border": "#FFFFFF17",
33
+ "text_muted": "#8FA0BA",
34
+ "text_primary": "#F1F5FB",
35
+ "text_secondary": "#C9D4E6",
36
+ }
37
+
38
+ LIGHT_COLORS = {
39
+ "accent": "#047857",
40
+ "accent_dark": "#065F46",
41
+ "success": "#15803D",
42
+ "warning": "#B45309",
43
+ "error": "#DC2626",
44
+ "info": "#2563EB",
45
+ "violet": "#7C3AED",
46
+ "pink": "#DB2777",
47
+ "cyan": "#0891B2",
48
+ "orange": "#EA580C",
49
+ "kpis": {
50
+ "primary": "#0891B2",
51
+ "secondary": "#7C3AED",
52
+ "tertiary": "#0284C7",
53
+ "quaternary": "#4F46E5",
54
+ "quinary": "#DB2777",
55
+ "senary": "#B45309",
56
+ "septenary": "#DC2626",
57
+ "octonary": "#EA580C",
58
+ },
59
+ "surface": "#FFFFFF",
60
+ "surface_variant": "#F7F9FC",
61
+ "surface_sunken": "#EEF1F6",
62
+ "background": "#F3F5F9",
63
+ "border": "#E3E8F0",
64
+ "text_muted": "#64748B",
65
+ "text_primary": "#0F172A",
66
+ "text_secondary": "#334155",
67
+ }
68
+
69
+
70
+ def get_colors(mode: str) -> dict:
71
+ return dict(DARK_COLORS if mode == "dark" else LIGHT_COLORS)
72
+
73
+
74
+ def rgba(color: str, opacity: float) -> str:
75
+ if color.startswith("#") and len(color) == 9:
76
+ return color
77
+ if color.startswith("#") and len(color) == 7:
78
+ hex_color = color[1:]
79
+ alpha = int(max(0.0, min(1.0, opacity)) * 255)
80
+ return f"#{alpha:02x}{hex_color}"
81
+ return color
82
+
83
+
84
+ def get_theme_file() -> Path:
85
+ return Path.home() / ".g360" / f"{_get_app_name()}_config.json"
86
+
87
+
88
+ def _get_app_name() -> str:
89
+ try:
90
+ from pathlib import Path as _P
91
+ _root = _P(__file__).resolve().parent.parent.parent.parent
92
+ _skill = _root / "skill.json"
93
+ if _skill.exists():
94
+ with open(_skill, encoding="utf-8") as _f:
95
+ _data = json.load(_f)
96
+ return _data.get("name", "g360-app").replace("-", "_")
97
+ except Exception:
98
+ pass
99
+ return "g360_app"
100
+
101
+
102
+ def load_theme_preference() -> str:
103
+ try:
104
+ config_file = get_theme_file()
105
+ if config_file.exists():
106
+ with open(config_file, encoding="utf-8") as f:
107
+ config = json.load(f)
108
+ return config.get("theme_mode", "dark")
109
+ except Exception:
110
+ pass
111
+ return "dark"
112
+
113
+
114
+ def save_theme_preference(mode: str):
115
+ try:
116
+ config_file = get_theme_file()
117
+ config_file.parent.mkdir(parents=True, exist_ok=True)
118
+ config = {"theme_mode": mode}
119
+ with open(config_file, "w", encoding="utf-8") as f:
120
+ json.dump(config, f)
121
+ except Exception:
122
+ pass
@@ -0,0 +1,49 @@
1
+ from pathlib import Path
2
+ import os
3
+
4
+ try:
5
+ import tomllib
6
+ except ImportError:
7
+ import tomli as tomllib
8
+
9
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
10
+ ASSETS_DIR = BASE_DIR / "assets"
11
+ DATA_DIR = ASSETS_DIR / "data"
12
+
13
+ WINDOW_WIDTH = 1200
14
+ WINDOW_HEIGHT = 800
15
+ WINDOW_MIN_WIDTH = 900
16
+ WINDOW_MIN_HEIGHT = 600
17
+
18
+ AUTO_REFRESH_INTERVAL = 900
19
+
20
+ CACHE_FILE = DATA_DIR / ".last_raw.json"
21
+ VERSION_CACHE_FILE = DATA_DIR / ".version_check.json"
22
+ VERSION_CHECK_INTERVAL = 86400
23
+
24
+ LOG_PATH = BASE_DIR / "run_log.txt"
25
+
26
+
27
+ def get_local_version() -> str:
28
+ pyproject = BASE_DIR / "pyproject.toml"
29
+ if not pyproject.exists():
30
+ return "0.0.0"
31
+ try:
32
+ with open(pyproject, "rb") as f:
33
+ data = tomllib.load(f)
34
+ return str(data.get("project", {}).get("version", "0.0.0"))
35
+ except Exception:
36
+ return "0.0.0"
37
+
38
+
39
+ def get_app_name() -> str:
40
+ skill_path = BASE_DIR / "skill.json"
41
+ if skill_path.exists():
42
+ try:
43
+ with open(skill_path, encoding="utf-8") as f:
44
+ import json
45
+ data = json.load(f)
46
+ return data.get("name", "g360-app")
47
+ except Exception:
48
+ pass
49
+ return BASE_DIR.name
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ G360 Processor - Motor de logica de negocio.
5
+
6
+ Este modulo debe ser heredado y extendido por cada app G360.
7
+ Contiene los patrones base para:
8
+ - Calcular KPIs agregados
9
+ - Generar metricas por linea/categoria
10
+ - Sugiere transferencias entre almacenes
11
+ - Exporta reportes a Excel con formato profesional
12
+
13
+ Heredar y sobrescribir los metodos segun el dominio de cada app.
14
+ """
15
+
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+
19
+
20
+ APP_AUTHOR = "g360-app-polished"
21
+ APP_NAME = "G360"
22
+
23
+
24
+ def _make_report_name(title: str) -> str:
25
+ """Genera nombre de archivo con timestamp: G360_{slug}_{YYYYMMDD}_{HHMMSS}.xlsx"""
26
+ now = datetime.now()
27
+ ts = now.strftime("%Y%m%d_%H%M%S")
28
+ import re
29
+ slug = re.sub(r"[^a-zA-Z0-9]+", "_", title).strip("_").upper()
30
+ if len(slug) > 40:
31
+ slug = slug[:40]
32
+ if not slug:
33
+ slug = "REPORTE"
34
+ return f"{APP_NAME}_{slug}_{ts}"
35
+
36
+
37
+ class BaseProcessor:
38
+ """
39
+ Procesador base que debe heredarse.
40
+
41
+ Ejemplo de uso:
42
+ class MiProcessor(BaseProcessor):
43
+ def calcular_kpis(self, raw_data):
44
+ # Implementar logica especifica
45
+ return kpis_dict
46
+ """
47
+
48
+ def __init__(self):
49
+ self._last_kpis = None
50
+ self._last_hash = None
51
+
52
+ def calcular_kpis(self, raw_data: dict) -> dict:
53
+ """
54
+ Calcula KPIs a partir de datos crudos.
55
+ Debe ser sobrescrito por subclases.
56
+
57
+ Args:
58
+ raw_data: Datos crudos del dominio (dict por almacén/entidad)
59
+
60
+ Returns:
61
+ dict con KPIs calculados
62
+ """
63
+ raise NotImplementedError("Subclase debe implementar calcular_kpis()")
64
+
65
+ def obtener_metricas(self, kpis: dict) -> tuple[list[dict], list[dict]]:
66
+ """
67
+ Genera metricas agrupadas (por linea, categoria, etc).
68
+ Debe ser sobrescrito por subclases.
69
+
70
+ Returns:
71
+ (metricas_principales, metricas_secundarias)
72
+ """
73
+ raise NotImplementedError("Subclase debe implementar obtener_metricas()")
74
+
75
+ def sugerir_acciones(self, raw_data: dict, kpis: dict) -> list[dict]:
76
+ """
77
+ Sugiere acciones (transferencias, alertas, etc).
78
+ Debe ser sobrescrito por subclases.
79
+
80
+ Returns:
81
+ lista de dicts con sugerencias
82
+ """
83
+ return []
84
+
85
+ def export_to_excel(self, data: list, file_path: str, title: str = "Reporte"):
86
+ """
87
+ Exporta datos a Excel con formato profesional.
88
+ Metodo base reutilizable.
89
+
90
+ Args:
91
+ data: Lista de filas [(col1, col2, ...), ...]
92
+ file_path: Ruta del archivo de salida
93
+ title: Titulo del reporte
94
+ """
95
+ from openpyxl import Workbook
96
+ from openpyxl.styles import PatternFill, Font, Alignment
97
+
98
+ wb = Workbook()
99
+ wb.properties.creator = APP_AUTHOR
100
+ wb.properties.description = f"Reporte generado por {APP_NAME} — {datetime.now().strftime('%Y-%m-%d %H:%M')}"
101
+
102
+ ws = wb.active
103
+ ws.title = title[:31]
104
+
105
+ # Headers
106
+ if data:
107
+ headers = [f"Col {i+1}" for i in range(len(data[0]))]
108
+ ws.append(headers)
109
+ for cell in ws[1]:
110
+ cell.fill = PatternFill(start_color="10B981", end_color="10B981", fill_type="solid")
111
+ cell.font = Font(color="FFFFFF", bold=True)
112
+ cell.alignment = Alignment(horizontal="center")
113
+
114
+ for row in data:
115
+ ws.append([str(v) if v is not None else "" for v in row])
116
+
117
+ ws.column_dimensions['A'].width = 35
118
+ for col in "BCD":
119
+ ws.column_dimensions[col].width = 16
120
+
121
+ wb.save(file_path)
122
+
123
+ def hash_data(self, data: dict) -> str:
124
+ """Hash SHA-256 para deteccion de cambios."""
125
+ import hashlib
126
+ import json
127
+ serialized = json.dumps(data, sort_keys=True, ensure_ascii=False).encode()
128
+ return hashlib.sha256(serialized).hexdigest()[:16]
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "flet-desktop-polished",
3
+ "description": "G360 Flet Polished - Plantilla base con patrones de UI avanzada",
4
+ "framework": "flet",
5
+ "version": "1.0.0",
6
+ "python": ">=3.11",
7
+ "colors": {
8
+ "bg": "#0b1220",
9
+ "surface": "#1a2333",
10
+ "accent": "#34d399",
11
+ "text": "#f0f6fc",
12
+ "muted": "#8b949e",
13
+ "success": "#34d399",
14
+ "warning": "#f59e0b",
15
+ "error": "#ef4444"
16
+ },
17
+ "effects": {
18
+ "glassmorphism": true,
19
+ "blur": "12px",
20
+ "rounded": "12px"
21
+ },
22
+ "signature": {
23
+ "mode": "powered",
24
+ "text": "powered by G360"
25
+ },
26
+ "build": {
27
+ "tool": "flet",
28
+ "target": "windows",
29
+ "name": "G360-App"
30
+ },
31
+ "dependencies": {
32
+ "flet": ">=0.28.3"
33
+ },
34
+ "guidelines": {
35
+ "architecture": "src/core (logica), src/ui (componentes), src/config (theme)",
36
+ "naming": "PascalCase para clases UI, camelCase para metodos",
37
+ "state_management": "Usar atributos de clase, hash diff para cache",
38
+ "threading": "Usar threading.Lock() para operaciones concurrentes"
39
+ }
40
+ }