g360-cli 1.15.0 → 1.15.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.15.0",
3
+ "version": "1.15.8",
4
4
  "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
@@ -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
@@ -5,6 +5,7 @@ description = "G360 Desktop Application - Polished Template"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
7
7
  "flet[desktop]==0.28.3",
8
+ "pip-system-certs>=4.38",
8
9
  "requests>=2.31.0",
9
10
  "openpyxl>=3.0.0",
10
11
  ]
@@ -1,3 +1,4 @@
1
1
  flet[desktop]==0.28.3
2
+ pip-system-certs>=4.38
2
3
  requests>=2.31.0
3
4
  openpyxl>=3.0.0
@@ -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 de forma limpia antes de cerrar."""
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():
@@ -1,105 +1,171 @@
1
1
  import json
2
2
  from pathlib import Path
3
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",
4
+ # Colores G360 estandar como referencia
5
+ G360_DEFAULTS = {
6
+ "dark": {
7
+ "accent": "#10B981",
8
+ "accent_dark": "#047857",
9
+ "success": "#34D399",
10
+ "warning": "#F59E0B",
11
+ "error": "#EF4444",
12
+ "info": "#3B82F6",
13
+ "violet": "#8B5CF6",
14
+ "pink": "#EC4899",
15
+ "cyan": "#06B6D4",
16
+ "orange": "#F97316",
17
+ "surface": "#141D33",
18
+ "surface_variant": "#1B2740",
19
+ "surface_sunken": "#0E1627",
20
+ "background": "#0A0F1E",
21
+ "border": "#FFFFFF17",
22
+ "text_muted": "#8FA0BA",
23
+ "text_primary": "#F1F5FB",
24
+ "text_secondary": "#C9D4E6",
25
+ "kpis": {
26
+ "primary": "#06B6D4",
27
+ "secondary": "#8B5CF6",
28
+ "tertiary": "#0EA5E9",
29
+ "quaternary": "#6366F1",
30
+ "quinary": "#EC4899",
31
+ "senary": "#F59E0B",
32
+ "septenary": "#EF4444",
33
+ "octonary": "#F97316",
34
+ },
27
35
  },
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",
36
+ "light": {
37
+ "accent": "#047857",
38
+ "accent_dark": "#065F46",
39
+ "success": "#15803D",
40
+ "warning": "#B45309",
41
+ "error": "#DC2626",
42
+ "info": "#2563EB",
43
+ "violet": "#7C3AED",
44
+ "pink": "#DB2777",
45
+ "cyan": "#0891B2",
46
+ "orange": "#EA580C",
47
+ "surface": "#FFFFFF",
48
+ "surface_variant": "#F7F9FC",
49
+ "surface_sunken": "#EEF1F6",
50
+ "background": "#F3F5F9",
51
+ "border": "#E3E8F0",
52
+ "text_muted": "#64748B",
53
+ "text_primary": "#0F172A",
54
+ "text_secondary": "#334155",
55
+ "kpis": {
56
+ "primary": "#0891B2",
57
+ "secondary": "#7C3AED",
58
+ "tertiary": "#0284C7",
59
+ "quaternary": "#4F46E5",
60
+ "quinary": "#DB2777",
61
+ "senary": "#B45309",
62
+ "septenary": "#DC2626",
63
+ "octonary": "#EA580C",
64
+ },
58
65
  },
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
66
  }
68
67
 
69
68
 
70
- def get_colors(mode: str) -> dict:
71
- return dict(DARK_COLORS if mode == "dark" else LIGHT_COLORS)
69
+ def get_colors(mode: str = None, skill_path: Path = None) -> dict:
70
+ """
71
+ Carga la paleta de colores para el modo especificado.
72
+
73
+ Prioridad:
74
+ 1. skill.json del proyecto (si existe)
75
+ 2. Configuracion guardada en ~/.g360/
76
+ 3. Valores por defecto G360
77
+
78
+ Args:
79
+ mode: 'dark' o 'light'. Si es None, lee de la preferencia guardada.
80
+ skill_path: Ruta al skill.json. Si es None, busca automaticamente.
81
+
82
+ Returns:
83
+ dict con los colores aplicados
84
+ """
85
+ # Determinar modo
86
+ if mode is None:
87
+ mode = load_theme_preference()
88
+
89
+ # Cargar desde skill.json si existe
90
+ skill_colors = _load_skill_colors(skill_path)
91
+
92
+ # Combinar: defaults -> skill overrides
93
+ defaults = G360_DEFAULTS.get(mode, G360_DEFAULTS["dark"])
94
+
95
+ if skill_colors:
96
+ # Skill tiene colores propios, usarlos
97
+ colors = {**defaults, **skill_colors}
98
+ # Asegurar que kpis se fusionen correctamente
99
+ if "kpis" in skill_colors:
100
+ colors["kpis"] = {**defaults.get("kpis", {}), **skill_colors["kpis"]}
101
+ else:
102
+ colors = defaults.copy()
103
+ if "kpis" not in colors:
104
+ colors["kpis"] = defaults.get("kpis", {}).copy()
105
+
106
+ return colors
107
+
108
+
109
+ def _load_skill_colors(skill_path: Path = None) -> dict | None:
110
+ """Carga colores desde skill.json del proyecto."""
111
+ try:
112
+ if skill_path is None:
113
+ skill_path = _find_skill_json()
114
+
115
+ if skill_path and skill_path.exists():
116
+ with open(skill_path, encoding="utf-8") as f:
117
+ data = json.load(f)
118
+ return data.get("colors", {})
119
+ except Exception:
120
+ pass
121
+ return None
122
+
123
+
124
+ def _find_skill_json() -> Path | None:
125
+ """Busca skill.json en las ubicaciones posibles."""
126
+ candidates = [
127
+ Path("skill.json"),
128
+ Path("src/core/skill.json"),
129
+ Path(__file__).resolve().parent.parent.parent / "skill.json",
130
+ ]
131
+ for candidate in candidates:
132
+ if candidate.exists():
133
+ return candidate
134
+ return None
72
135
 
73
136
 
74
137
  def rgba(color: str, opacity: float) -> str:
138
+ """Convierte un color hex a rgba con la opacidad especificada."""
75
139
  if color.startswith("#") and len(color) == 9:
76
- return color
140
+ return color # Ya tiene alpha
77
141
  if color.startswith("#") and len(color) == 7:
78
142
  hex_color = color[1:]
79
143
  alpha = int(max(0.0, min(1.0, opacity)) * 255)
80
144
  return f"#{alpha:02x}{hex_color}"
81
- return color
145
+ return color # Retornar como esta si no es hex valido
82
146
 
83
147
 
84
148
  def get_theme_file() -> Path:
85
- return Path.home() / ".g360" / f"{_get_app_name()}_config.json"
149
+ """Retorna la ruta del archivo de preferencia de tema."""
150
+ app_name = _get_app_name()
151
+ return Path.home() / ".g360" / f"{app_name}_config.json"
86
152
 
87
153
 
88
154
  def _get_app_name() -> str:
155
+ """Obtiene el nombre de la app desde skill.json o el nombre del directorio."""
89
156
  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("-", "_")
157
+ skill_path = _find_skill_json()
158
+ if skill_path and skill_path.exists():
159
+ with open(skill_path, encoding="utf-8") as f:
160
+ data = json.load(f)
161
+ return data.get("name", Path(__file__).resolve().parent.parent.parent.name)
97
162
  except Exception:
98
163
  pass
99
- return "g360_app"
164
+ return Path(__file__).resolve().parent.parent.parent.name
100
165
 
101
166
 
102
167
  def load_theme_preference() -> str:
168
+ """Carga la preferencia de tema guardada (dark/light)."""
103
169
  try:
104
170
  config_file = get_theme_file()
105
171
  if config_file.exists():
@@ -112,6 +178,7 @@ def load_theme_preference() -> str:
112
178
 
113
179
 
114
180
  def save_theme_preference(mode: str):
181
+ """Guarda la preferencia de tema."""
115
182
  try:
116
183
  config_file = get_theme_file()
117
184
  config_file.parent.mkdir(parents=True, exist_ok=True)
@@ -120,3 +187,15 @@ def save_theme_preference(mode: str):
120
187
  json.dump(config, f)
121
188
  except Exception:
122
189
  pass
190
+
191
+
192
+ def set_brand_colors(colors: dict):
193
+ """
194
+ Permite sobrescribir los colores de la marca en runtime.
195
+ Useful para apps que necesitan colores dinamicos.
196
+ """
197
+ global G360_DEFAULTS
198
+ # Actualizar solo los modos que existan
199
+ for mode in ["dark", "light"]:
200
+ if mode in G360_DEFAULTS and mode in colors:
201
+ G360_DEFAULTS[mode].update(colors[mode])