426 Commits

Author SHA1 Message Date
Naiel
c940c846b8 Mejorar la presentación de la transacción en la página de pagos y optimizar la carga de datos 2026-03-04 23:02:26 +00:00
Naiel
7822e46b61 Añadir funcionalidad para gestionar puntos de interés en Gest-Aula 2026-03-04 14:45:04 +00:00
Naiel
a39ecca990 Mejoras generales, rediseño de Gest-Aula, borrado de modulos sin usar y Mensajes, movido el resumen diario a gest-aula. 2026-03-04 14:33:28 +00:00
Naiel
80e9262bcb fix logic 2026-03-03 11:33:23 +01:00
Naiel
cd456ab9f1 fix 2026-03-03 11:29:38 +01:00
Naiel
c0c40ecd99 fix 2026-03-03 11:24:19 +01:00
Naiel
98c6ba39f3 fix 2026-03-03 11:20:57 +01:00
Naiel
03f52c8a92 fix 2026-03-03 11:06:30 +01:00
Naiel
f655a736b3 fix 2026-03-03 10:59:02 +01:00
Naiel
89a68f27da fix 2026-03-03 10:50:05 +01:00
Naiel
4d322e5696 fix 2026-03-03 10:36:42 +01:00
Naiel
0138e0ca69 fix 2026-03-03 10:33:36 +01:00
Naiel
3e8542c9de Fix import error handling for telesec_couchdb 2026-03-03 10:27:04 +01:00
Naiel
90df81d308 Update asset path for GitHub Release upload 2026-03-03 10:24:12 +01:00
Naiel
53941da35c Update Windows agent release workflow for PyInstaller 2026-03-03 10:21:47 +01:00
Naiel
105c911c59 feat: añadir soporte para configuración JSON en el agente de Windows 2026-03-02 12:47:02 +00:00
Naiel
cb12894455 feat: añadir agente de Windows y soporte para control remoto de ordenadores 2026-03-02 12:39:13 +00:00
Naiel
9d808ed63e feat: update print button functionality to use onclick event for improved usability 2026-03-02 08:08:59 +00:00
Naiel
d0593d3d46 chore: clean up empty code change sections in the changes log 2026-03-02 08:06:06 +00:00
Naiel
8b7d0258ae Refactor code structure for improved readability and maintainability 2026-03-01 23:37:37 +00:00
Naiel
9a760a1d24 fix: Ajustar el tamaño mínimo de las columnas en la cuadrícula de estadísticas en la página de inicio 2026-02-25 14:27:33 +00:00
Naiel
e1f780ea11 feat: Añadir estadísticas de ingresos, gastos y mensajes sin leer en la página de inicio 2026-02-25 14:25:12 +00:00
Naiel
7ad2e9c142 feat: Añadir funcionalidad de filtrado en la búsqueda de elementos y actualizar etiquetas en la página de pagos 2026-02-25 14:00:42 +00:00
Naiel
879554a7ab feat: Implementar limpieza automática de menús antiguos en el comedor y eliminar la tienda de apps 2026-02-25 13:31:05 +00:00
Naiel
0ef6e5a233 feat: Añadir sincronización en tiempo real para la gestión de aplicaciones 2026-02-25 13:20:43 +00:00
Naiel
d905e86bbf feat: Añadir funcionalidad de gestión de mensajes con soporte para adjuntos 2026-02-25 13:17:52 +00:00
Naiel
3764473b5b feat: Añadir funcionalidad de gestión de apps en la tienda de apps 2026-02-25 12:45:38 +00:00
Naiel
382e31158a feat: Añadir modo de revisión y mejorar la retroalimentación en el panel 2026-02-24 12:17:26 +00:00
Naiel
09a9a95df0 refactor: Actualizar referencias de contenedores en la gestión de comandas 2026-02-23 15:14:12 +00:00
Naiel
b04dbbf19d feat: Actualizar el título y el mensaje de seguridad en la interfaz principal 2026-02-23 15:12:01 +00:00
Naiel
7619444556 feat: Añadir funcionalidad para alternar la visibilidad del menú en la interfaz 2026-02-23 15:10:03 +00:00
Naiel
076aa45337 feat: Añadir funcionalidad para mostrar pictogramas en opciones del panel y mejorar estilos de presentación 2026-02-23 14:59:29 +00:00
Naiel
0b1419fae2 Añadir parámetro de visibilidad en mensajes de 'No hay personas registradas' en secciones de Monedero 2026-02-23 14:41:18 +00:00
Naiel
74afb2a499 Modificar parámetro de función 'edit' para mejorar la claridad en la gestión de secciones 2026-02-23 14:38:26 +00:00
Naiel
543d1c3202 feat: Add panel page with daily quiz and update functionality
- Introduced a new panel page that includes a daily quiz based on the menu and tasks for the day.
- Implemented functions to fetch and decrypt daily data, build quiz questions, and render the quiz interface.
- Added a button to refresh the application, clearing the cache and updating the service worker.
- Enhanced service worker to manage application version checks and handle CouchDB URL prefix.
- Created a version.json file to manage application versioning.
2026-02-23 14:37:08 +00:00
Naiel
75947d3468 Refactor manejo de URL para eliminar parámetros de búsqueda en la navegación y mejorar la visibilidad de personas en formularios 2026-02-23 12:38:28 +00:00
Naiel
9ab0472e2a Modificar parámetro de función 'edit' para mejorar la claridad y manejo de transacciones 2026-02-23 11:34:25 +00:00
naielv
aa993df2bf Añadir funcionalidad de selección de pictogramas en el formulario del comedor y refactorizar campos de entrada 2026-02-23 00:23:11 +01:00
Naiel
e0da65811e Modificar texto de "Total Gastos" a "Total Ganancias" y simplificar lógica de cálculo de ingresos y gastos 2026-02-12 14:37:27 +00:00
Naiel
eb6a956cdc Añadir manejo de eventos de base de datos y mejorar la carga de precios del café 2026-02-12 14:30:59 +00:00
Naiel
dc4ba25b20 Refactor code structure for improved readability and maintainability 2026-02-12 14:17:05 +00:00
Naiel
129188c022 Añadir configuración de precios del café y formulario de edición en la página de administración 2026-02-12 14:05:26 +00:00
Naiel
9d4ce881c6 fix 2026-02-10 12:54:12 +00:00
Naiel
4e1727adc3 update 2026-02-10 12:52:55 +00:00
naielv
db5b07bb44 update 2026-02-06 23:55:29 +01:00
naielv
61b8cb8af4 Reapply "Add Cajas module for cash register transaction management"
This reverts commit 8b29e3f425.
2026-02-06 23:26:51 +01:00
Naiel
2ee03aa204 Merge pull request #19 from EuskadiTech/revert-18-copilot/add-cajas-module
Revert "Add Cajas module for cash register transaction management"
2026-02-06 13:17:33 +01:00
Naiel
8b29e3f425 Revert "Add Cajas module for cash register transaction management" 2026-02-06 13:17:04 +01:00
Naiel
31697f2448 Merge pull request #18 from EuskadiTech/copilot/add-cajas-module
Add Cajas module for cash register transaction management
2026-02-06 13:09:06 +01:00
copilot-swe-agent[bot]
468d08110d Fix code review issues: placeholder images, transfer logic, and filter condition
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-02-06 12:02:12 +00:00
copilot-swe-agent[bot]
07e2c9a98e Add Cajas (Cash Register) module with transaction management and Monederos integration
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-02-06 11:59:09 +00:00
copilot-swe-agent[bot]
8b6140929e Initial plan 2026-02-06 11:54:05 +00:00
Naiel
05ea9a9d8b serve from local 2026-02-06 09:23:33 +00:00
naielv
75c319c701 update 2026-02-05 23:57:54 +01:00
naielv
8a9fee46da Refactor code to use single quotes for strings, update HTML structure for better readability, and improve error handling in various modules. Added Prettier configuration for consistent code formatting. 2026-02-05 23:07:35 +01:00
Naiel
6d7def5f18 Merge pull request #17 from EuskadiTech/copilot/add-onboarding-flow-login-js
Enhance onboarding: prevent duplicate personas, auto-detect from CouchDB, and add server presets
2026-02-05 21:55:21 +01:00
copilot-swe-agent[bot]
ddfd653d68 Add server preset selector for EuskadiTech servers
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-02-05 15:22:21 +00:00
copilot-swe-agent[bot]
b0160b3b66 Add button disable, persona detection, and mandatory encryption password
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-02-05 15:20:00 +00:00
copilot-swe-agent[bot]
d6809e51d1 Fix infinite recursion by allowing onboarding routes without login
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-02-05 15:09:00 +00:00
naielv
0db86f3dd2 Refactor peer status handling and update synchronization color logic 2026-02-03 19:36:34 +01:00
Naiel
dd195c5157 Increase prices for various beverage sizes 2026-02-02 11:07:19 +01:00
copilot-swe-agent[bot]
f472baacf6 Fix URL protocol handling to support both HTTP and HTTPS
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-22 19:15:34 +00:00
copilot-swe-agent[bot]
1e5de2c686 Use open_page() consistently for proper navigation cleanup
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-22 19:13:53 +00:00
copilot-swe-agent[bot]
bf9ba4ceef Improve onboarding: use safeuuid() and avoid unnecessary reload
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-22 19:12:11 +00:00
copilot-swe-agent[bot]
c2eac955fe Implement onboarding flow with database config and admin persona creation
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-22 19:09:54 +00:00
copilot-swe-agent[bot]
a02d7956ca Initial plan 2026-01-22 19:05:26 +00:00
Naiel
f96a408852 Delete TESTING_SUPERCAFE_FIX.md 2026-01-21 13:17:01 +01:00
Naiel
8afe2eedee Merge pull request #15 from EuskadiTech/copilot/fix-supercafe-deuda-lista
Fix local DB updates not triggering change listeners in SuperCafé
2026-01-21 13:16:27 +01:00
copilot-swe-agent[bot]
4faea51004 Add manual testing guide for SuperCafé debt list fix
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 12:14:06 +00:00
copilot-swe-agent[bot]
70ea752992 Remove redundant docCache update (onChange already handles it)
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 12:12:28 +00:00
copilot-swe-agent[bot]
492889b9e1 Fix: Move onChange call before docCache update to detect local changes
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 12:11:18 +00:00
copilot-swe-agent[bot]
ea54dc5471 Initial plan 2026-01-21 12:07:40 +00:00
Naiel
8ff431ca10 Merge pull request #13 from EuskadiTech/copilot/disable-button-during-save
Prevent duplicate transactions by disabling save buttons during async operations
2026-01-21 12:59:26 +01:00
copilot-swe-agent[bot]
7d5fe84b3a Add error message for photo attachment save failure
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 11:54:51 +00:00
copilot-swe-agent[bot]
f287eb63f6 Add user-facing error messages to all save operations
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 11:53:52 +00:00
copilot-swe-agent[bot]
565d88def8 Fix button disabling to occur after validation
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 11:52:02 +00:00
copilot-swe-agent[bot]
92feb05a0d Complete error handling for all save buttons
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 11:50:46 +00:00
copilot-swe-agent[bot]
013413a01c Add error handling to re-enable buttons on save failure
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 11:49:40 +00:00
copilot-swe-agent[bot]
9e7f8ebd1f Implement button disabling during save operations
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-21 11:46:16 +00:00
copilot-swe-agent[bot]
07d657002e Initial plan 2026-01-21 11:39:38 +00:00
Naiel
24520d1f01 fix 2 2026-01-09 12:09:21 +01:00
Naiel
6b8202992d update 2026-01-09 12:06:29 +01:00
naielv
bc0755b9bf Add login qr 2026-01-05 02:13:10 +01:00
Naiel
cb61374582 Modify TS_encrypt to skip callback execution
Commented out callback and return statements in TS_encrypt function.
2026-01-04 20:17:07 +01:00
naielv
cb70222c04 updates 2025-12-26 01:32:48 +01:00
naielv
dfcd22fadf update debounce 2025-12-25 19:44:57 +01:00
naielv
7277c6ab34 Also fix this 2025-12-25 19:36:21 +01:00
naielv
fbf0a8c9e4 Disable encryption 2025-12-25 19:31:58 +01:00
naielv
ee219e1d96 update 2025-12-25 19:29:16 +01:00
naielv
90b8223385 updated 2025-12-25 19:15:28 +01:00
naielv
0bc662dbde fix orientation on tablets 2025-12-25 18:48:58 +01:00
naielv
15df8d12fe update 2025-12-25 16:40:53 +01:00
naielv
a9cdfb567a Add fecha-diff to materiales 2025-12-25 01:46:48 +01:00
Naiel
f0a6f3b6b3 Merge pull request #12 from EuskadiTech/deluxe
TeleSec Deluxe: Ahora con PouchDB
2025-12-25 01:19:29 +01:00
naielv
8802952e5a fix login 2025-12-25 01:11:14 +01:00
naielv
ab4a05bc7f some changes 2025-12-25 01:02:51 +01:00
naielv
648854190e finished 2025-12-25 00:45:14 +01:00
naielv
13a4367c92 V1 2025-12-24 23:30:32 +01:00
naielv
2258e74960 update gitignore 2025-12-24 17:21:54 +01:00
Naiel
fd63885507 Update pagos.js 2025-12-15 10:21:57 +01:00
naielv
4b88679b37 Arreglado el orden en addCategory_Personas 2025-12-14 20:28:48 +01:00
naielv
dd9fda10f7 Cambiado el sorter de addCategory_Personas, y arreglado el monedero de la pagina de inicio y TS_IndexElement 2025-12-14 20:20:17 +01:00
naielv
3dbaa9bd33 Creado un sorter central 2025-12-14 20:17:02 +01:00
naielv
3402183f3c 2ª actualización importante 2025-12-14 20:14:33 +01:00
naielv
1bc9aa5295 update 2025-12-14 13:43:54 +01:00
naielv
c946dad334 Update 2025-12-14 13:42:36 +01:00
Naiel
a32aa89a56 Update pagos.js 2025-12-13 12:59:03 +01:00
Naiel
4d1952d998 Update app_logic.js 2025-12-13 12:48:35 +01:00
Naiel
5fe308eac6 Update pagos.js 2025-12-13 12:36:02 +01:00
Naiel
ad46651ed8 Update pagos.js 2025-12-13 12:27:32 +01:00
Naiel
0eb519dea4 Update pagos.js 2025-12-13 12:24:00 +01:00
naielv
9b0d33710f add asunto-sorting, remove hoster var, fix formatting of pagos, fix account login URLs 2025-12-12 23:55:01 +01:00
naielv
28a0fced87 fix pwa sw 2025-12-12 23:15:36 +01:00
naielv
60a7649c36 major update 2025-12-12 19:13:53 +01:00
Naiel
cca21ac3d3 Create wrangler.jsonc 2025-12-12 16:41:36 +01:00
Naiel
78c0abf92d Create static.yml 2025-12-12 16:32:01 +01:00
Naiel
d40d600a49 fix 2025-12-12 16:30:18 +01:00
Naiel
0e1ab0c619 Make it faster 2025-12-12 16:14:00 +01:00
Naiel
0366f62dfb fix peercount & relays 2025-12-12 16:06:52 +01:00
Naiel
33594b2508 add public relays 2025-12-12 13:20:31 +01:00
Naiel
db9626aa7b remove point system, remove old code, upgraded avatar res, indexelement persona's now show balances 2025-12-12 13:07:04 +01:00
Naiel
ba022dea3c fix 2025-11-26 11:59:08 +01:00
Naiel
f0e32b4ad0 fix 2025-11-26 11:57:25 +01:00
Naiel
bbbc8b1d63 update 2025-11-26 11:54:46 +01:00
Naiel
af2f642d45 fix promobono 2025-11-19 11:48:16 +00:00
Naiel
9f00b97677 2nd emergency fix. 2025-11-19 11:35:17 +00:00
Naiel
196245ffa0 Emergency fix 2025-11-19 11:31:54 +00:00
Naiel
7cf1bf40c7 fix issue 2025-11-13 12:20:41 +00:00
Naiel
860f6019ad update 2025-11-13 12:18:29 +00:00
Naiel
4fefdcaf3d updated 2025-11-13 12:16:19 +00:00
Naiel
9a22545ec2 Add some CSS 2025-11-13 11:36:55 +00:00
Naiel
49a021b9dd Merge pull request #9 from EuskadiTech/copilot/fix-escanear-qr-page
Add QR scanner for wallet selection and transaction management features
2025-11-12 15:47:59 +01:00
copilot-swe-agent[bot]
0cd6048bf2 Add QR scanner for wallet selection and transaction management features
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2025-11-12 14:33:11 +00:00
copilot-swe-agent[bot]
1bec976efc Initial plan 2025-11-12 14:23:23 +00:00
Naiel
a03a224cda Update payment terminal title and add navigation button 2025-11-12 15:22:36 +01:00
Naiel
0bdd3ba8b1 Merge pull request #8 from EuskadiTech/copilot/add-pagos-module-transaction-log
Add Pagos module with ATM-style Datafono terminal and digital wallet system
2025-11-12 15:09:41 +01:00
Naiel
96b3c60568 Update pagos.js 2025-11-12 15:07:06 +01:00
Naiel
811fabfced Refactor pagos.js to remove unused id parameter 2025-11-12 15:02:28 +01:00
Naiel
a3d9278d6f Implement persistent totals for ingresos and gastos
Refactor total calculations to use a persistent totals object by ID.
2025-11-12 14:55:55 +01:00
Naiel
4e67381cf0 Revise transaction type options in pagos.js
Updated transaction type options with new icons.
2025-11-12 14:51:59 +01:00
Naiel
f1593de431 Update pagos.js 2025-11-12 14:46:57 +01:00
Naiel
889722451c Update pagos.js 2025-11-12 14:43:05 +01:00
Naiel
bb9c1ee7d3 Refactor tid variable handling in pagos.js 2025-11-12 14:36:12 +01:00
Naiel
fc2e4d27d2 Rename variable 'data' to 'sdata' for clarity 2025-11-12 14:30:09 +01:00
Naiel
6bde2fb2b8 Fix formatting issue in setUrlHash call 2025-11-12 14:27:41 +01:00
Naiel
7a510329ba Refactor tid handling for datafono cases 2025-11-12 14:25:45 +01:00
Naiel
a5cc4e7cc7 Refactor sessionStorage data handling for Pagos module 2025-11-12 14:24:50 +01:00
Naiel
c77ac5c264 Add datafono handling in pagos.js 2025-11-12 14:21:08 +01:00
copilot-swe-agent[bot]
f14d19f59a Add Pagos module with Datafono UI and wallet integration
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2025-11-12 12:47:42 +00:00
copilot-swe-agent[bot]
fccd9308e2 Initial plan 2025-11-12 12:37:22 +00:00
Naiel
2265ad28f4 disable call due to user request 2025-11-05 10:49:53 +01:00
Naiel
b1993ba83a updated 2025-11-05 10:36:17 +01:00
Naiel
d00d4c7af2 Update addCategory to close on selection. 2025-10-31 12:42:25 +01:00
Naiel
4d175d9aa1 update 2025-10-15 11:07:03 +00:00
Naiel
b0fa3d0844 fixed icons and added chat 2025-10-15 10:51:42 +00:00
naielv
617711fb1a add global search 2025-10-02 22:45:41 +02:00
naielv
f46ec17c03 update app_modules.js 2025-09-19 21:33:25 +02:00
naielv
de163f7f9b update app_modules.js 2025-09-19 21:30:20 +02:00
naielv
3250669dc9 update app_modules.js 2025-09-19 21:28:07 +02:00
naielv
d00d004dd8 update materiales.js 2025-09-19 21:24:08 +02:00
naielv
b38d470b02 update app_modules.js 2025-09-12 09:39:10 +02:00
naielv
0f2a894edb update app_modules.js 2025-09-12 09:38:42 +02:00
Naiel
ad71ceae21 fix 2025-09-11 12:31:23 +02:00
Naiel
6e3e809435 fix 2025-09-11 12:30:57 +02:00
Naiel
4719c346f5 fix 2025-09-11 12:26:34 +02:00
Naiel
2e070ea7fd fix chained val 2025-09-11 12:22:14 +02:00
Naiel
5f573c49be fix 2025-09-11 12:15:57 +02:00
Naiel
9524c9e3f3 fix 2025-09-11 12:13:50 +02:00
Naiel
b3a6a19f95 olé 2025-09-11 12:09:28 +02:00
Naiel
332b39aa23 fix 2025-09-11 12:04:56 +02:00
Naiel
14ee3ab4a0 fix ev 2025-09-11 12:00:23 +02:00
Naiel
75ce2aa207 add date to resumen diario 2025-09-11 11:31:07 +02:00
naielv
48b68eff83 update app_modules.js 2025-09-11 10:59:35 +02:00
naielv
3124540f4f update app_modules.js 2025-09-11 10:56:36 +02:00
naielv
42728ab445 update 2025-09-10 21:22:49 +02:00
naielv
af5141099d update 2025-09-10 10:12:16 +02:00
naielv
1ce4207294 add almendras 2025-09-09 22:58:16 +02:00
naielv
9d3d6bc363 oooops 2025-09-09 22:51:21 +02:00
naielv
74dc7fe404 updated 2025-09-09 22:51:04 +02:00
naielv
2f3efbcf66 rename 2025-09-09 22:32:54 +02:00
naielv
b3ff2a7a9d update 2025-09-09 22:32:29 +02:00
naielv
2b4e28eea6 update 2025-09-09 22:23:45 +02:00
naielv
f7b3d95526 update 2025-09-09 22:17:12 +02:00
naielv
553f43c7b3 Add weather services 2025-09-09 21:58:07 +02:00
naielv
8b1185b507 updates!!! 2025-09-09 21:50:14 +02:00
Naiel
eecab547df Merge pull request #7 from EuskadiTech/naielv-faster
several changes
2025-09-09 21:35:37 +02:00
naielv
b069f7db61 several changes 2025-09-09 21:34:27 +02:00
Naiel
424d767549 Merge pull request #6 from EuskadiTech/naielv-faster
Faster TS_IndexElement loads
2025-09-09 21:16:40 +02:00
naielv
4f3d16326c Mejorado la carga de IndexElement 2025-09-09 21:12:34 +02:00
naielv
ac68228e26 two 2025-09-09 16:16:31 +02:00
naielv
673d64e720 update 2025-09-09 16:07:10 +02:00
naielv
ec7746f79f fix 2025-09-09 15:31:35 +02:00
naielv
6ceae4f1d5 fix AES logic 2025-09-09 15:29:38 +02:00
naielv
c50c29f743 Add RSA to TeleSec (more efficient) 2025-09-09 15:25:06 +02:00
naielv
34f61777c3 add AXE 2025-09-09 15:06:39 +02:00
naielv
9c81cdb1e0 update 2025-09-09 13:36:22 +02:00
naielv
bdd880bc24 update to qr 2025-09-09 13:35:31 +02:00
naielv
aa01eaeaa5 update 2025-09-09 11:06:02 +02:00
naielv
fc1aa567bc update 2025-09-09 10:21:05 +02:00
naielv
cc3d694ce3 fix sc sorting 2025-09-08 11:07:38 +02:00
naielv
f11760d867 update 2025-09-07 15:52:00 +02:00
naielv
fc4170acb8 Errores gramaticos. 2025-09-07 14:51:53 +02:00
naielv
98bda7db5d Update 2025-09-07 14:42:12 +02:00
naielv
3bfbdc11af updated 2025-09-07 14:16:42 +02:00
naielv
a59a26fa29 Un tallarin 2025-09-05 15:27:56 +02:00
naielv
a5de1b3855 update 2025-09-05 13:16:51 +02:00
naielv
0624103778 fix 2025-09-05 13:13:38 +02:00
naielv
cd6e4e8b64 Add total payment 2025-09-05 13:11:03 +02:00
naielv
fb9d574ff4 fix comedor 2025-09-05 13:04:54 +02:00
naielv
d27b3ec90c update 2025-09-04 22:26:49 +02:00
naielv
7d5d631b05 Añadido Aulas (En desarrollo) 2025-09-04 15:42:16 +02:00
naielv
f13218e0b1 Add type conversion 2025-09-03 10:36:47 +02:00
naielv
ac12dc627d update app_modules.js 2025-09-03 10:32:21 +02:00
naielv
c550812deb update personas.js 2025-09-03 10:30:39 +02:00
naielv
9efb05bc1e update app_modules.js 2025-09-03 10:28:30 +02:00
naielv
db244e2953 update app_modules.js 2025-09-03 10:25:07 +02:00
naielv
a3098a15f2 update 2025-09-03 10:21:41 +02:00
naielv
12b90e3e1e update index.js 2025-09-03 10:11:27 +02:00
naielv
1763de12bd update 2025-09-03 10:10:01 +02:00
naielv
92dc877942 update app_modules.js 2025-09-03 00:13:25 +02:00
naielv
841f063695 update app_modules.js 2025-09-03 00:10:26 +02:00
naielv
e6301bfb64 update app_modules.js 2025-09-03 00:00:06 +02:00
naielv
7e1c6f1bf8 update app_modules.js 2025-09-02 23:53:16 +02:00
naielv
5adff05283 update app_modules.js 2025-09-02 23:49:52 +02:00
naielv
6f3108134b update app_modules.js 2025-09-02 23:42:30 +02:00
naielv
d5650b2e3b update index.js 2025-09-02 23:13:12 +02:00
naielv
e3b062d9ea update simple.css 2025-08-30 16:50:12 +02:00
naielv
6bd797e5bb update simple.css 2025-08-30 16:46:50 +02:00
naielv
ac3b43361c update sw.js 2025-08-30 16:40:00 +02:00
naielv
b9d3c5a10d update app_modules.js 2025-08-30 16:36:54 +02:00
naielv
d9b379ec42 update gun_init.js 2025-08-30 09:54:26 +02:00
naielv
1f3ec25f32 update sw.js 2025-08-30 09:50:17 +02:00
naielv
2458082968 update 2025-08-30 09:45:57 +02:00
naielv
5f59f9f0c0 update 2025-08-30 09:43:36 +02:00
naielv
28ffd17bed update notas.js 2025-08-27 17:59:31 +02:00
naielv
e2446de20f update 2025-08-27 17:56:03 +02:00
naielv
c33b9e6ace update notas.js 2025-08-27 17:52:53 +02:00
naielv
73cb8d5614 update 2025-08-27 17:49:34 +02:00
naielv
69c981de3c update dataman.js 2025-08-27 16:37:35 +02:00
naielv
bdc5c42cde update dataman.js 2025-08-27 16:37:11 +02:00
naielv
5588c94ec3 update app_modules.js 2025-08-27 16:29:11 +02:00
naielv
42310c1f55 update app_modules.js 2025-08-27 15:57:01 +02:00
naielv
08431defbb update dataman.js 2025-08-27 15:55:35 +02:00
naielv
52c15e4863 update dataman.js 2025-08-27 15:52:06 +02:00
naielv
bab0ebc858 update 2025-08-27 15:50:42 +02:00
naielv
b64a9bc78b update 2025-08-27 15:47:54 +02:00
naielv
e9342b6fec update app_modules.js 2025-08-27 14:35:15 +02:00
naielv
b2617f605f update 2025-08-27 14:28:11 +02:00
naielv
c28aaecc66 update index.html 2025-08-27 14:25:37 +02:00
naielv
e3d4998d80 update app_modules.js 2025-08-27 14:23:52 +02:00
naielv
be9c87790d update app_modules.js 2025-08-27 14:21:43 +02:00
naielv
e243f27b70 update app_modules.js 2025-08-27 14:03:07 +02:00
naielv
704241335e update app_modules.js 2025-08-27 13:56:27 +02:00
naielv
8406bd02c2 update app_modules.js 2025-08-27 13:51:35 +02:00
naielv
8a13e6e71b update app_modules.js 2025-08-27 13:49:13 +02:00
naielv
34b27b15ba update 2025-08-27 13:47:23 +02:00
naielv
14c081a615 update supercafe.js 2025-08-27 12:45:14 +02:00
naielv
81d200899d update supercafe.js 2025-08-27 10:43:12 +02:00
naielv
056f705f25 update supercafe.js 2025-08-27 10:41:06 +02:00
naielv
e084d42eb3 update app_logic.js 2025-08-26 15:05:32 +02:00
naielv
b4700f46fd update app_modules.js 2025-08-26 14:57:57 +02:00
naielv
369eb040af update config.js 2025-08-26 14:51:47 +02:00
naielv
d21fae5052 update 2025-08-23 17:21:54 +02:00
naielv
1bc4364084 update supercafe.js 2025-08-23 17:18:30 +02:00
naielv
eebca7752b update build.py 2025-08-23 17:08:05 +02:00
naielv
a42ad8d9ad update app_modules.js 2025-08-23 17:06:08 +02:00
naielv
63546605f2 update 2025-08-23 17:05:17 +02:00
naielv
357166b159 update 2025-08-23 16:50:31 +02:00
naielv
8866c68cee update build.py 2025-08-23 16:31:36 +02:00
naielv
df46f378d7 update index.js 2025-08-16 09:50:10 +02:00
naielv
b00b503d4c update main.yml 2025-08-15 17:38:16 +02:00
naielv
1575b61051 update WinApp.spec 2025-08-15 17:37:03 +02:00
naielv
7d5667451c update main.py 2025-08-15 17:36:58 +02:00
naielv
dbddd70ef9 update main.py 2025-08-15 17:30:16 +02:00
naielv
027669cfd6 update main.py 2025-08-15 17:23:00 +02:00
naielv
693f5c0af9 update 2025-08-15 17:16:05 +02:00
naielv
d596edc107 update main.yml 2025-08-15 17:13:46 +02:00
naielv
eeb540f269 update main.yml 2025-08-15 17:13:08 +02:00
naielv
a9b43a7f7d update 2025-08-15 17:10:58 +02:00
naielv
ba3cc7424d update build.py 2025-08-15 17:08:16 +02:00
naielv
ed2148be8d update build.py 2025-08-15 17:07:24 +02:00
naielv
36e3eaf958 update build.py 2025-08-15 17:05:54 +02:00
naielv
77600a45d5 update 2025-08-15 17:05:02 +02:00
naielv
4988bdff1e update build.py 2025-08-15 17:01:46 +02:00
naielv
8accafc599 update build.py 2025-08-15 16:59:17 +02:00
naielv
bd9db0df22 update build.py 2025-08-15 16:57:49 +02:00
naielv
7794d53802 update 2025-08-15 16:56:47 +02:00
Naiel
014bc50690 Create WinApp.spec 2025-08-15 16:52:53 +02:00
Naiel
ad93d958f9 Rename icon512_maskable.ico to favicon.ico 2025-08-15 16:50:42 +02:00
Naiel
c040215bb6 Add files via upload 2025-08-15 16:50:13 +02:00
Naiel
0132c88ce7 Update main.py 2025-08-15 16:48:46 +02:00
Naiel
a74280f087 Create requirements.txt 2025-08-15 16:47:28 +02:00
Naiel
ccefabbc4a Create main.py 2025-08-15 16:46:56 +02:00
Naiel
e29c02ebe5 Create main.yml 2025-08-15 16:43:18 +02:00
Naiel
2f1b036c3f Merge pull request #5 from EuskadiTech/naielv-add-ribbon
Actualización masiva!
2025-08-15 16:36:11 +02:00
naielv
f480dd5491 update avisos.js 2025-08-14 21:34:17 +02:00
naielv
d4d4fd4b5f update personas.js 2025-08-14 21:30:31 +02:00
naielv
0feba6a09b update simple.css 2025-08-14 21:28:44 +02:00
naielv
4ba8141675 update gun_init.js 2025-08-14 17:45:24 +02:00
naielv
d71fca510f update supercafe.js 2025-08-14 17:35:01 +02:00
naielv
4303a02c1a update app_modules.js 2025-08-14 17:34:58 +02:00
naielv
afa224cbdc Testing IndexedDB 2025-08-14 17:30:21 +02:00
naielv
8de9af66e2 update app_modules.js 2025-08-14 17:23:00 +02:00
naielv
d13c66aa01 update app_logic.js 2025-08-14 17:21:49 +02:00
naielv
faf1d112c9 update testing.html 2025-08-14 17:19:30 +02:00
naielv
138af3a364 update 2025-08-14 17:18:38 +02:00
naielv
e0470d0dd1 update personas.js 2025-08-14 15:39:27 +02:00
naielv
ab75e3089e update simple.css 2025-08-14 15:30:00 +02:00
naielv
bfce6f32b6 update personas.js 2025-08-14 15:27:05 +02:00
naielv
737c0bd65f update personas.js 2025-08-14 15:24:37 +02:00
naielv
93a78a9d97 update 2025-08-14 15:21:11 +02:00
naielv
415334ca4a update 2025-08-14 09:48:26 +02:00
naielv
341119988c update simple.css 2025-08-14 09:36:55 +02:00
naielv
2567b65ff2 update app_modules.js 2025-08-13 22:26:41 +02:00
naielv
69458ecc97 update 2025-08-13 22:25:18 +02:00
naielv
7a391c338a update app_modules.js 2025-08-13 22:12:01 +02:00
naielv
9e8ba5a2e9 update personas.js 2025-08-13 22:08:47 +02:00
naielv
4f56f06208 update 2025-08-13 22:05:43 +02:00
naielv
c42ebadf2d update app_modules.js 2025-08-13 22:01:22 +02:00
naielv
62d1cafef9 update app_modules.js 2025-08-13 21:57:22 +02:00
naielv
6ec7a5feb1 update 2025-08-13 21:55:48 +02:00
naielv
a02f1cb588 update config.js 2025-08-13 21:53:01 +02:00
naielv
aa3f312047 update 2025-08-13 21:51:37 +02:00
naielv
0fbc7cad39 update dataman.js 2025-08-13 21:45:48 +02:00
naielv
293f57133b update app_modules.js 2025-08-13 21:43:55 +02:00
naielv
9ceb62dff0 update dataman.js 2025-08-13 21:43:49 +02:00
naielv
7cb0fdea76 update dataman.js 2025-08-13 21:42:26 +02:00
naielv
8820e16974 update app_modules.js 2025-08-13 21:36:05 +02:00
naielv
58e0c55480 update simple.css 2025-08-13 21:34:05 +02:00
naielv
7c7cec6ac8 update 2025-08-13 21:33:13 +02:00
naielv
8316377344 update dataman.js 2025-08-13 21:27:55 +02:00
naielv
830fdd3206 update icon 2025-08-13 21:26:53 +02:00
naielv
9eadb04a93 move dataOps to Admin. Datos 2025-08-13 21:25:14 +02:00
naielv
cf0ecc4d27 update index.js 2025-08-13 17:04:30 +02:00
naielv
6d1b3fa97e update avisos.js 2025-08-13 17:02:50 +02:00
naielv
e4b9ebe7a4 update 2025-08-13 17:01:07 +02:00
naielv
416188a572 update 2025-08-13 16:58:17 +02:00
naielv
a8e496e78a update 2025-08-13 16:55:59 +02:00
naielv
20553d4c99 update 2025-08-13 16:52:41 +02:00
naielv
618ac4e6db update simple.css 2025-08-13 16:47:37 +02:00
naielv
3dd674b172 update simple.css 2025-08-13 16:45:37 +02:00
naielv
919ff298ba update simple.css 2025-08-13 16:43:08 +02:00
naielv
0599756b63 update simple.css 2025-08-13 16:40:51 +02:00
naielv
5b8641680d update simple.css 2025-08-13 16:38:46 +02:00
naielv
2b15258bd9 update simple.css 2025-08-13 16:37:13 +02:00
naielv
3ad83a523d update app_logic.js 2025-08-13 16:36:00 +02:00
naielv
cdd80eb4be update simple.css 2025-08-13 16:34:23 +02:00
naielv
f5aa8c4366 update 2025-08-13 16:33:26 +02:00
naielv
97c97b561f update simple.css 2025-08-13 16:31:41 +02:00
naielv
975ed0426c update 2025-08-13 16:30:02 +02:00
naielv
c3475e707b update simple.css 2025-08-13 16:24:41 +02:00
naielv
8321ba9373 update simple.css 2025-08-13 16:22:40 +02:00
naielv
c81202edf9 update 2025-08-13 16:21:25 +02:00
naielv
24a82911b6 update simple.css 2025-08-13 16:18:41 +02:00
naielv
3fcdc49502 update simple.css 2025-08-13 16:17:24 +02:00
naielv
1beadb739b update index.html 2025-08-13 16:16:25 +02:00
naielv
846c0e8898 update 2025-08-13 16:14:57 +02:00
naielv
1ea087e683 update 2025-08-13 16:10:57 +02:00
naielv
096042e7c1 update index.html 2025-08-13 16:08:44 +02:00
naielv
cb5e481c4d update index.html 2025-08-13 16:06:36 +02:00
naielv
e91ac0c719 update 2025-08-13 16:04:45 +02:00
naielv
45f6cf82d5 update index.html 2025-08-13 16:01:37 +02:00
naielv
0cccaafb38 update testing.html 2025-08-13 16:00:07 +02:00
naielv
b90bbcb8c2 update app_modules.js 2025-08-13 15:59:55 +02:00
naielv
ae9fcf4887 update simple.css 2025-08-13 15:58:48 +02:00
naielv
1ebc7079eb update 2025-08-13 15:56:59 +02:00
naielv
ea8726e0d6 update index.html 2025-08-13 15:51:14 +02:00
naielv
83125bcf96 update simple.css 2025-08-13 15:49:41 +02:00
naielv
8823159a30 update 2025-08-13 15:49:10 +02:00
naielv
ad458de818 update 2025-08-13 15:42:35 +02:00
naielv
d2a11d65a7 update 2025-08-12 16:15:00 +02:00
naielv
1d8ddd8d8b update index.js 2025-08-12 09:41:37 +02:00
naielv
9de720c920 update build.py 2025-08-12 09:39:54 +02:00
naielv
0c4c9df396 update 2025-08-12 09:38:50 +02:00
naielv
8fc2e96985 update app_modules.js 2025-08-12 09:36:56 +02:00
naielv
852f9eddc9 update materiales.js 2025-08-12 09:04:00 +02:00
naielv
d51e57fcee update app_modules.js 2025-08-12 09:03:04 +02:00
naielv
1be3885c70 update app_modules.js 2025-08-12 09:01:51 +02:00
naielv
52e6e3c09d update app_modules.js 2025-08-12 09:00:18 +02:00
naielv
310696a4c6 update app_modules.js 2025-08-11 22:21:43 +02:00
naielv
505f591839 update 2025-08-11 22:16:08 +02:00
naielv
b07dcd8683 update index.js 2025-08-11 22:04:59 +02:00
naielv
fe5c54ff3e update 2025-08-11 21:59:23 +02:00
naielv
09fa34007b Add Aztec Code Generation 2025-08-11 21:48:39 +02:00
naielv
57171963be update 2025-08-11 15:51:53 +02:00
naielv
881dfedb14 update resumen_diario.js 2025-08-11 15:47:29 +02:00
naielv
1cc6e512ab update resumen_diario.js 2025-08-11 15:45:05 +02:00
naielv
0b748e45f8 update app_logic.js 2025-08-11 15:43:05 +02:00
naielv
e6688bf74c update login.js 2025-08-11 15:36:54 +02:00
naielv
cbd5351981 update login.js 2025-08-11 15:35:07 +02:00
naielv
31007d6979 update app_modules.js 2025-08-11 15:33:27 +02:00
naielv
7fb2d5f67f update 2025-08-11 15:31:34 +02:00
naielv
9b9bd730dd update 2025-08-11 15:25:21 +02:00
naielv
2e438c4b9e update app_modules.js 2025-08-11 15:18:22 +02:00
naielv
6ed2922ba4 update app_modules.js 2025-08-11 15:17:28 +02:00
naielv
b0ff7dd456 update sw.js 2025-08-11 15:14:01 +02:00
Naiel
43dfa414aa Update sw.js 2025-08-11 14:49:22 +02:00
Naiel
1d73f3c427 Update materiales.js 2025-08-07 11:35:31 +02:00
Naiel
72fba9e976 Update personas.js 2025-08-07 11:34:50 +02:00
Naiel
84719e4c6b Merge pull request #2 from EuskadiTech/copilot/fix-1
Fix table sorting with hybrid date-name prioritization
2025-08-07 11:26:49 +02:00
copilot-swe-agent[bot]
9131d25dd5 Invert sorting logic - check Fecha first, then sub-sort by Nombre
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2025-08-07 09:25:09 +00:00
Naiel
c65b1b25a4 Merge pull request #4 from EuskadiTech/copilot/fix-3
Add comprehensive GitHub Copilot instructions for TeleSec development
2025-08-07 11:19:29 +02:00
copilot-swe-agent[bot]
e8a9cff2ec Add comprehensive .github/copilot-instructions.md with full build and testing validation
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2025-08-07 09:16:14 +00:00
copilot-swe-agent[bot]
d39f92554a Fix materials table alphabetical sorting - materials now sort by name
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2025-08-07 09:13:46 +00:00
copilot-swe-agent[bot]
84cc070e68 Initial plan 2025-08-07 09:04:34 +00:00
copilot-swe-agent[bot]
cf553295bb Initial plan 2025-08-07 09:03:48 +00:00
naielv
23c4122afb Ignore GunJS runtime & npm folders 2025-08-05 17:11:23 +02:00
naielv
7414e3d6de fix 2025-08-04 20:39:27 +02:00
naielv
5f2ef09672 fixes 2025-08-04 20:35:57 +02:00
naielv
f2588fc50f Some fixes 2025-08-04 20:31:00 +02:00
naielv
0ea230e99b Rewrite materiales 2025-08-04 20:16:58 +02:00
naielv
a83be0f7c7 Fix slow networks 2025-08-04 20:06:29 +02:00
naielv
ed5755b005 v2025-08-04_1 2025-08-04 20:01:03 +02:00
naielv
0fcd6ccaba Fixed 2025-08-04 19:59:25 +02:00
Naiel
1ee951fc9f Create simple.css 2025-07-31 21:38:28 +02:00
Naiel
325465f9a5 fix submodules 2025-07-31 19:36:36 +00:00
Naiel
7dee605d95 Delete .gitmodules 2025-07-31 21:34:34 +02:00
Naiel
645e8c194a Delete .gitea/workflows/static.yml 2025-07-31 21:31:57 +02:00
naielv
e35428f3ee added search bar 2025-07-31 20:18:40 +02:00
naielv
def79a2015 update 2025-07-31 17:02:09 +02:00
naielv
64d11fe224 Add comedor to ResumenDiario & fix unclosed tags 2025-07-31 16:56:24 +02:00
naielv
a016e06557 General fixes 2025-07-31 16:43:01 +02:00
naielv
a274de3c4d Actualizar build.py 2025-07-30 23:04:01 +02:00
naielv
0f2b2df969 fix scrolltable 2025-07-30 22:47:37 +02:00
naielv
8052d21fcc oops 2025-07-30 22:46:42 +02:00
naielv
03c132a6bd update 2025-07-30 22:38:25 +02:00
116 changed files with 13005 additions and 8175 deletions

View File

@@ -1,47 +0,0 @@
name: Build server
on:
push:
tags: ["v*-*-*_*"]
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: '${{ secrets.TOKEN_PULL }}'
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- uses: actions/setup-go@v5
with:
go-version: '^1.13.1'
- name: "Build app"
run: "python3 build.py"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
# Upload built files
name: 'TeleSec-dist'
path: ./dist
- uses: mdallasanta/ssh-scp-deploy@v1.2.0
with:
local: './dist/' # Local file path - REQUIRED false - DEFAULT ./
remote: '${{ secrets.FOLDER }}' # Remote file path - REQUIRED false - DEFAULT ~/
host: ${{secrets.HOST}} # Remote server address - REQUIRED true
port: 22 # Remote server port - REQUIRED false - DEFAULT 22
user: ${{secrets.USER}} # Remote server user - REQUIRED true
key: ${{secrets.KEY}} # Remote server private key - REQUIRED at least one of "password" or "key"
- name: "zip-it-up"
run: "zip -r TeleSec-dist.zip ./dist"
- name: Upload as release
uses: https://gitea.com/actions/release-action@main
with:
files: |-
TeleSec-dist.zip
api_key: '${{secrets.TOKEN_PUSH_PLUS}}'

222
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,222 @@
# TeleSec - Secure Distributed Communication Application
TeleSec is a Spanish Progressive Web Application (PWA) built with vanilla JavaScript, HTML, and CSS that provides secure group communication using a local-first PouchDB datastore with optional CouchDB replication for syncing. The application allows users to join encrypted communication groups using group codes and secret keys.
**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**
## Working Effectively
### Build and Deploy Process
- **Build the application (FASTEST BUILD EVER - 0.036 seconds):**
- `cd /home/runner/work/TeleSec/TeleSec`
- `python3 build.py`
- **NEVER CANCEL**: Build completes in under 0.1 seconds. No timeout needed.
- The build script copies files from `assets/` to `dist/` and processes template variables in `src/` files.
### Serving the Application
- **Python HTTP Server (Recommended for development):**
- `cd /home/runner/work/TeleSec/TeleSec/dist`
- `python3 -m http.server 8000`
- Access at: `http://localhost:8000`
- **Node.js HTTP Server (Alternative with CORS support):**
- `cd /home/runner/work/TeleSec/TeleSec/dist`
- `npx http-server . --port 8001 --cors`
- **NEVER CANCEL**: First run takes ~14 seconds to download http-server package. Set timeout to 30+ seconds.
- Access at: `http://localhost:8001`
### Development Environment Requirements
- **Python 3.x** (for build script) - Version 3.12.3+ confirmed working
- **Node.js and npm** (optional, for alternative serving) - Version 20.19.4+ confirmed working
- **Web browser** (for testing the PWA functionality)
## Validation and Testing
### Mandatory Validation Steps
1. **Build Validation:**
- Run `python3 build.py` and verify it completes in under 1 second
- Verify `dist/` directory is created with all assets and processed files
- Check that template variables (%%PREFETCH%%, %%VERSIONCO%%, %%ASSETSJSON%%) are replaced
2. **Application Functionality Test:**
- Start web server: `python3 -m http.server 8000` in `dist/` directory
- Navigate to `http://localhost:8000` in browser
- **CRITICAL LOGIN TEST:** Enter any group code (e.g., "TEST") and secret key (e.g., "SECRET123")
- Click "Iniciar sesión" button
- **VERIFY NETWORK CONNECTIVITY:** Confirm the header shows connected nodes (e.g., "TeleSec - TEST - (8 nodos)")
- **SUCCESS INDICATORS:**
- Application loads without errors
- Login form accepts credentials
- Distributed network connects (node count > 0)
- No JavaScript console errors except expected WebSocket connection failures
3. **PWA Features Test:**
- Verify Service Worker registration in browser console
- Check manifest.json loads correctly
- Confirm offline caching functionality
### Error Handling Validation
- **Build Script Errors:** Python syntax errors will cause build to fail with clear error messages
- **Network Connectivity:** Some WebSocket connections to gun-manhattan.herokuapp.com may fail - this is expected
- **Browser Compatibility:** Application works in modern browsers supporting Service Workers
## Repository Structure
### Key Files and Directories
```
/home/runner/work/TeleSec/TeleSec/
├── build.py # Main build script - processes template variables
├── index.html # Build error fallback (should never be served)
├── README.md # Basic repository information (minimal)
├── LICENSE # Project license
├── CNAME # GitHub Pages configuration
├── .gitignore # Excludes dist/, radata/, node_modules/
├── src/ # Source files with template variables
│ ├── index.html # Main application HTML with %%PREFETCH%% variables
│ ├── app_logic.js # Core application logic and authentication
│ ├── app_modules.js # Application modules and utilities
│ ├── config.js # Configuration and CouchDB setup
│ ├── db.js # PouchDB wrapper and replication
│ ├── pwa.js # Progressive Web App functionality
│ ├── sw.js # Service Worker with cache configuration
│ └── page/ # Individual page modules
│ ├── login.js # Login functionality
│ ├── index.js # Main dashboard
│ ├── materiales.js # Materials management
│ ├── personas.js # People management
│ ├── supercafe.js # SuperCafé module
│ ├── comedor.js # Dining hall module
│ ├── importar.js # Data import functionality
│ ├── exportar.js # Data export functionality
│ ├── resumen_diario.js # Daily summary
│ └── notificaciones.js # Notifications
└── assets/ # Static assets copied to dist/
├── manifest.json # PWA manifest
├── *.png, *.jpg # Icons and images
├── static/ # JavaScript libraries and CSS
│ ├── euskaditech-css/ # CSS framework
│ └── ico/ # Application icons
└── page/ # Page-specific assets (empty placeholder)
```
### Build Process Details
The `build.py` script performs these operations:
1. **Clean:** Removes existing `dist/` directory (if it exists)
2. **Copy Assets:** Copies all files from `assets/` to `dist/`
3. **Process Templates:** Processes files from `src/` and replaces:
- `%%PREFETCH%%` - Generates link prefetch tags for all assets
- `%%VERSIONCO%%` - Inserts version code "2025-08-04_1"
- `%%ASSETSJSON%%` - Inserts JSON array of all asset files
4. **Output:** Creates complete deployable application in `dist/`
## Application Architecture
### Technology Stack
- **Frontend:** Vanilla JavaScript, HTML5, CSS3
- **Data Layer:** PouchDB (local-first) with optional CouchDB replication
- **Networking:** WebRTC for peer-to-peer connections (where applicable)
- **Authentication:** Group codes + secret keys (converted to uppercase)
- **Storage:** Browser LocalStorage + PouchDB local datastore
- **PWA Features:** Service Worker, Web App Manifest
### Remote Sync (Optional)
The application can optionally replicate to a remote CouchDB server for cloud backup and multi-device syncing. Configure the CouchDB server, database name, and credentials in the login/setup form in the application.
### Application Modules
- **Login/Authentication:** Group-based access with secret keys
- **Materials Management:** Track and manage materials/supplies
- **People Management:** Manage group members
- **SuperCafé:** Café/beverage ordering system
- **Dining Hall:** Restaurant/meal management
- **Import/Export:** Data backup and restoration
- **Daily Summary:** Reports and analytics
- **Notifications:** Alert system
## Common Development Tasks
### Making Changes to the Application
1. **ALWAYS** edit files in `src/` directory, never `dist/`
2. Run `python3 build.py` to rebuild after changes
3. Refresh browser or restart web server to see changes
4. Test authentication flow and network connectivity after any changes
### Adding New Features
1. Create new JavaScript files in `src/page/` for new modules
2. Add script references in `src/index.html`
3. Update assets if new static files are needed
4. Rebuild and test complete user workflows
### Debugging Common Issues
- **Login Issues:** Check browser console for replication/auth errors and DB initialization logs
- **Network Connectivity:** Verify remote CouchDB server is reachable and replication is active
- **Build Issues:** Check Python syntax in build.py
- **Performance:** Monitor browser DevTools Network tab for asset loading
## Validation Scenarios
### Complete User Workflow Test
After making any changes, ALWAYS test this complete scenario:
1. **Build and Serve:**
```bash
cd /home/runner/work/TeleSec/TeleSec
python3 build.py
cd dist
python3 -m http.server 8000
```
2. **Login Test:**
- Navigate to `http://localhost:8000`
- Enter group code: "TEST"
- Enter secret key: "SECRET123"
- Click "Iniciar sesión"
- Verify header shows: "TeleSec - TEST" and that the login is accepted
3. **Network Connectivity Test:**
- Confirm green connection indicator appears (bottom right)
- Check browser console shows PouchDB replication logs when a remote is configured
- Verify heartbeat/last-seen docs are being updated in the local DB
4. **PWA Functionality Test:**
- Check Service Worker registers successfully
- Verify offline caching works (Network tab → Offline)
- Test manifest.json loads correctly
## Quick Reference Commands
### Essential Operations
```bash
# Build application (< 0.1 seconds)
python3 build.py
# Serve with Python (most compatible)
cd dist && python3 -m http.server 8000
# Serve with Node.js (advanced features)
cd dist && npx http-server . --port 8001 --cors
# Clean rebuild
rm -rf dist && python3 build.py
# Check build output
ls -la dist/
```
### File Structure Verification
```bash
# Verify all source files exist
find src/ -name "*.js" -o -name "*.html" | sort
# Check template variable processing
grep -r "%%.*%%" dist/ || echo "All template variables processed correctly"
# Verify assets copied correctly
diff -r assets/ dist/ --exclude="*.js" --exclude="*.html" || echo "Some differences expected due to processing"
```
**CRITICAL REMINDERS:**
- **NEVER CANCEL**: Builds complete in under 0.1 seconds - no timeout needed
- **ALWAYS test login and network connectivity** after changes
- **Edit source files in `src/` directory only**, never `dist/`
- **Test with real user credentials** to verify distributed networking
- **Monitor browser console** for connection status and errors

45
.github/workflows/static.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy to Github Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build
run: TELESEC_HOSTER=GitHub-Pages python3 build.py
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: './dist/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -0,0 +1,46 @@
name: Build Windows Agent (Release)
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: write
jobs:
build-windows-agent:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
- name: Build hidden EXE with PyInstaller
shell: bash
run: |
cd python_sdk ; pyinstaller --noconfirm --clean --onefile --noconsole --hidden-import=telesec_couchdb --name telesec-windows-agent windows_agent.py
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: telesec-windows-agent
path: python_sdk/dist/telesec-windows-agent.exe
- name: Upload asset to GitHub Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: python_sdk/dist/telesec-windows-agent.exe

15
.gitignore vendored
View File

@@ -1 +1,14 @@
dist/*
dist/*
radata/*
node_modules/*
.DS_Store
._*
# Python
__pycache__/*
*.pyc
*.pyo
*.pyd
*.egg-info/*
*.egg
.venv/*
venv/*

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "assets/static/euskaditech-css"]
path = assets/static/euskaditech-css
url = https://git.tech.eus/EuskadiTech/css.git

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"embeddedLanguageFormatting": "auto"
}

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"tobermory.es6-string-html",
"esbenp.prettier-vscode"
]
}

View File

@@ -1,2 +1,94 @@
# TeleSec
Nuevo programa de datos
## Python SDK (CouchDB directo)
Se añadió un SDK Python en `python_sdk/` para acceder directamente a CouchDB (sin replicación local), compatible con el formato de cifrado de `TS_encrypt`:
- Formato: `RSA{...}`
- Algoritmo: `CryptoJS.AES.encrypt(payload, secret)` (modo passphrase/OpenSSL)
### Instalación
```bash
pip install -r requirements.txt
```
### Uso rápido
```python
from python_sdk import TeleSecCouchDB
db = TeleSecCouchDB(
server_url="https://tu-couchdb",
dbname="telesec",
username="usuario",
password="clave",
secret="SECRET123",
)
# Guardar cifrado (como TS_encrypt)
db.put("personas", "abc123", {"nombre": "Ana"}, encrypt=True)
# Leer y descifrar
obj = db.get("personas", "abc123", decrypt=True)
# Listar una tabla
rows = db.list("personas", decrypt=True)
for row in rows:
print(row.id, row.data)
```
API principal:
- `TeleSecCouchDB.put(table, item_id, data, encrypt=True)`
- `TeleSecCouchDB.get(table, item_id, decrypt=True)`
- `TeleSecCouchDB.list(table, decrypt=True)`
- `TeleSecCouchDB.delete(table, item_id)`
- `ts_encrypt(value, secret)` / `ts_decrypt(value, secret)`
## Agente Windows (Gest-Aula > Ordenadores)
Se añadió soporte para control de ordenadores del aula:
- Tabla: `aulas_ordenadores`
- Campos reportados por agente: `Hostname`, `UsuarioActual`, `AppActualEjecutable`, `AppActualTitulo`, `LastSeenAt`
- Control remoto: `ShutdownBeforeDate` (programado desde web a `hora_servidor + 2 minutos`)
### Ejecutar agente en Windows
El agente usa un archivo de configuración en la carpeta personal del usuario:
- Ruta por defecto: `~/.telesec/windows_agent.json`
- Se crea automáticamente si no existe
```bash
python -m python_sdk.windows_agent --once
```
Ejemplo del JSON de configuración:
```json
{
"server": "https://tu-couchdb",
"db": "telesec",
"user": "usuario",
"password": "clave",
"secret": "SECRET123",
"machine_id": "",
"interval": 15
}
```
También puedes sobrescribir valores por CLI (`--server`, `--secret`, etc.).
Opciones útiles:
- `--once`: una sola iteración
- `--interval 15`: intervalo (segundos)
- `--dry-run`: no apaga realmente, solo simula
- `--config <ruta>`: ruta alternativa del archivo JSON
### Hora de servidor (sin depender del reloj local)
El frontend y el agente usan la hora del servidor (cabecera HTTP `Date` de CouchDB) para comparar `ShutdownBeforeDate`.

View File

@@ -1,85 +0,0 @@
[
"icon512_maskable.png",
"icon512_rounded.png",
"manifest.json",
"static/axe.js",
"static/doublescroll.js",
"static/gun.js",
"static/jquery.js",
"static/load.js",
"static/open.js",
"static/path.js",
"static/radisk.js",
"static/radix.js",
"static/rindexed.js",
"static/sea.js",
"static/showdown.min.js",
"static/simplemde.min.css",
"static/simplemde.min.js",
"static/store.js",
"static/synchronous.js",
"static/TeleSec.jpg",
"static/toastr.min.css",
"static/toastr.min.js",
"static/webrtc.js",
"static/yson.js",
"static/ico/add.png",
"static/ico/azucar-moreno.png",
"static/ico/azucar-blanco.jpg",
"static/ico/stevia.jpg",
"static/ico/stevia-gotas.webp",
"static/ico/sacarina.jpg",
"static/ico/arrow_down_blue.png",
"static/ico/arrow_left_green.png",
"static/ico/arrow_up_red.png",
"static/ico/camera2.png",
"static/ico/cereales.png",
"static/ico/checkbox.png",
"static/ico/checkbox_unchecked.png",
"static/ico/connect_ok.svg",
"static/ico/connect_ko.svg",
"static/ico/coffee_bean.png",
"static/ico/colacao.jpg",
"static/ico/cookies.png",
"static/ico/cow.png",
"static/ico/delete.png",
"static/ico/fire.png",
"static/ico/keyboard_key_g.png",
"static/ico/keyboard_key_p.png",
"static/ico/lollipop.png",
"static/ico/milk.png",
"static/ico/preferences.png",
"static/ico/sizes.png",
"static/ico/statusok.png",
"static/ico/snowflake.png",
"static/ico/tea_bag.png",
"static/ico/thermometer2.png",
"static/ico/user.png",
"static/ico/user_generic.png",
"static/ico/water_tap.png",
"static/ico/wheat.png",
"static/ico/layered1/Azucar-Az. Blanco.png",
"static/ico/layered1/Azucar-Az. Moreno.png",
"static/ico/layered1/Azucar-Edulcorante.png",
"static/ico/layered1/Azucar-Sacarina.png",
"static/ico/layered1/Azucar-Sin.png",
"static/ico/layered1/Azucar-Stevia (Gotas).png",
"static/ico/layered1/Azucar-Stevia (Pastillas).png",
"static/ico/layered1/Background.png",
"static/ico/layered1/Cafeina-Con.png",
"static/ico/layered1/Cafeina-Sin.png",
"static/ico/layered1/Leche-Agua.png",
"static/ico/layered1/Leche-Sin lactosa.png",
"static/ico/layered1/Leche-Vegetal.png",
"static/ico/layered1/Leche-de Vaca.png",
"static/ico/layered1/Selección-CafeSolo.png",
"static/ico/layered1/Selección-CaféLeche.png",
"static/ico/layered1/Selección-ColaCao.png",
"static/ico/layered1/Selección-Infusion.png",
"static/ico/layered1/Selección-Leche.png",
"static/ico/layered1/Tamaño-Grande.png",
"static/ico/layered1/Tamaño-Pequeño.png",
"static/ico/layered1/Temperatura-Caliente.png",
"static/ico/layered1/Temperatura-Frio.png",
"static/ico/layered1/Temperatura-Templado.png"
]

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -1,252 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="manifest.json" />
<title>TeleSec</title>
<link rel="icon" type="image/png" href="static/TeleSec.jpg" />
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
<link href="static/toastr.min.css" rel="stylesheet" />
<link rel="prefetch" href="icon512_maskable.png" />
<link rel="prefetch" href="icon512_rounded.png" />
<link rel="prefetch" href="manifest.json" />
<link rel="prefetch" href="static/axe.js" />
<link rel="prefetch" href="static/doublescroll.js" />
<link rel="prefetch" href="static/gun.js" />
<link rel="prefetch" href="static/jquery.js" />
<link rel="prefetch" href="static/load.js" />
<link rel="prefetch" href="static/open.js" />
<link rel="prefetch" href="static/path.js" />
<link rel="prefetch" href="static/radisk.js" />
<link rel="prefetch" href="static/radix.js" />
<link rel="prefetch" href="static/rindexed.js" />
<link rel="prefetch" href="static/sea.js" />
<link rel="prefetch" href="static/showdown.min.js" />
<link rel="prefetch" href="static/simplemde.min.css" />
<link rel="prefetch" href="static/simplemde.min.js" />
<link rel="prefetch" href="static/store.js" />
<link rel="prefetch" href="static/synchronous.js" />
<link rel="prefetch" href="static/TeleSec.jpg" />
<link rel="prefetch" href="static/toastr.min.css" />
<link rel="prefetch" href="static/toastr.min.js" />
<link rel="prefetch" href="static/webrtc.js" />
<link rel="prefetch" href="static/yson.js" />
<link rel="prefetch" href="static/ico/add.png" />
<link rel="prefetch" href="static/ico/azucar-moreno.png" />
<link rel="prefetch" href="static/ico/azucar-blanco.jpg" />
<link rel="prefetch" href="static/ico/stevia.jpg" />
<link rel="prefetch" href="static/ico/stevia-gotas.webp" />
<link rel="prefetch" href="static/ico/sacarina.jpg" />
<link rel="prefetch" href="static/ico/arrow_down_blue.png" />
<link rel="prefetch" href="static/ico/arrow_left_green.png" />
<link rel="prefetch" href="static/ico/arrow_up_red.png" />
<link rel="prefetch" href="static/ico/camera2.png" />
<link rel="prefetch" href="static/ico/cereales.png" />
<link rel="prefetch" href="static/ico/checkbox.png" />
<link rel="prefetch" href="static/ico/checkbox_unchecked.png" />
<link rel="prefetch" href="static/ico/connect_ok.svg" />
<link rel="prefetch" href="static/ico/connect_ko.svg" />
<link rel="prefetch" href="static/ico/coffee_bean.png" />
<link rel="prefetch" href="static/ico/colacao.jpg" />
<link rel="prefetch" href="static/ico/cookies.png" />
<link rel="prefetch" href="static/ico/cow.png" />
<link rel="prefetch" href="static/ico/delete.png" />
<link rel="prefetch" href="static/ico/fire.png" />
<link rel="prefetch" href="static/ico/keyboard_key_g.png" />
<link rel="prefetch" href="static/ico/keyboard_key_p.png" />
<link rel="prefetch" href="static/ico/lollipop.png" />
<link rel="prefetch" href="static/ico/milk.png" />
<link rel="prefetch" href="static/ico/preferences.png" />
<link rel="prefetch" href="static/ico/sizes.png" />
<link rel="prefetch" href="static/ico/statusok.png" />
<link rel="prefetch" href="static/ico/snowflake.png" />
<link rel="prefetch" href="static/ico/tea_bag.png" />
<link rel="prefetch" href="static/ico/thermometer2.png" />
<link rel="prefetch" href="static/ico/user.png" />
<link rel="prefetch" href="static/ico/user_generic.png" />
<link rel="prefetch" href="static/ico/water_tap.png" />
<link rel="prefetch" href="static/ico/wheat.png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Az. Blanco.png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Az. Moreno.png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Edulcorante.png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Sacarina.png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Sin.png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Stevia (Gotas).png" />
<link rel="prefetch" href="static/ico/layered1/Azucar-Stevia (Pastillas).png" />
<link rel="prefetch" href="static/ico/layered1/Background.png" />
<link rel="prefetch" href="static/ico/layered1/Cafeina-Con.png" />
<link rel="prefetch" href="static/ico/layered1/Cafeina-Sin.png" />
<link rel="prefetch" href="static/ico/layered1/Leche-Agua.png" />
<link rel="prefetch" href="static/ico/layered1/Leche-Sin lactosa.png" />
<link rel="prefetch" href="static/ico/layered1/Leche-Vegetal.png" />
<link rel="prefetch" href="static/ico/layered1/Leche-de Vaca.png" />
<link rel="prefetch" href="static/ico/layered1/Selección-CafeSolo.png" />
<link rel="prefetch" href="static/ico/layered1/Selección-CaféLeche.png" />
<link rel="prefetch" href="static/ico/layered1/Selección-ColaCao.png" />
<link rel="prefetch" href="static/ico/layered1/Selección-Infusion.png" />
<link rel="prefetch" href="static/ico/layered1/Selección-Leche.png" />
<link rel="prefetch" href="static/ico/layered1/Tamaño-Grande.png" />
<link rel="prefetch" href="static/ico/layered1/Tamaño-Pequeño.png" />
<link rel="prefetch" href="static/ico/layered1/Temperatura-Caliente.png" />
<link rel="prefetch" href="static/ico/layered1/Temperatura-Frio.png" />
<link rel="prefetch" href="static/ico/layered1/Temperatura-Templado.png" />
<link rel="prefetch" href="page__supercafe.js" />
<link rel="prefetch" href="app_logic.js" />
<link rel="prefetch" href="page__index.js" />
<link rel="prefetch" href="page__notificaciones.js" />
<link rel="prefetch" href="page__materiales.js" />
<link rel="prefetch" href="index.html" />
<link rel="prefetch" href="page__exportar.js" />
<link rel="prefetch" href="gun_init.js" />
<link rel="prefetch" href="page__personas.js" />
<link rel="prefetch" href="page__importar.js" />
<link rel="prefetch" href="config.js" />
<link rel="prefetch" href="app_modules.js" />
<link rel="prefetch" href="page__comedor.js" />
<link rel="prefetch" href="page__resumen_diario.js" />
<link rel="prefetch" href="pwa.js" />
</head>
<body>
<details class="supermesh-indicator">
<summary>
<b>SuperMesh</b><br />
<br /><small id="peerPID" style="font-family: monospace"
>PID ??????????</small
>
</summary>
<ul id="peerList"></ul>
<i>Todos los datos están encriptados.</i>
</details>
<main>
<header class="no_print" id="header_hide_query">
<details id="LinkAccount_details" open>
<summary>
<b
>TeleSec - <span id="groupId">???</span> - (<span id="peerCount"
>?</span
>
nodos)</b
>
</summary>
<fieldset id="auth_fieldSet">
<legend>Credenciales</legend>
<br />
<label
>Codigo de grupo:<br />
<input type="text" id="LinkAccount_group"
/></label>
<br />
<br />
<label
>Clave secreta:<br />
<input type="text" id="LinkAccount_secret"
/></label>
<br /><br />
<button
type="button"
onclick='LinkAccount(document.getElementById("LinkAccount_group").value, document.getElementById("LinkAccount_secret").value, true)'
>
Iniciar sesión
</button>
</fieldset>
</details>
<!-- <button onclick="displayPost('index')">Ir a la pagina de inicio</button> -->
<div id="appendApps">
<!--<a class="button nav-supercafe nav-disabled" disabled>SuperCafé</a>
<a class="button nav-comedor nav-disabled" disabled>Menú Comedor</a>
<a class="button nav-recetas nav-disabled" disabled>Recetas</a>-->
</div>
<hr />
</header>
<div id="container"></div>
<!-- <br><br><br>
<footer>
<hr>
<details>
<summary><b>Apps SuperMesh</b></summary>
<button type="button">
<img src="static/TeleSec.jpg" alt="" width="100px">
<br>TeleSec
</button>
</details>
</footer> -->
<img
id="connectStatus"
style="bottom: 15px; right: 15px; position: fixed; width: 50px"
/>
</main>
<img
id="actionStatus"
src="static/ico/statusok.png"
style="
z-index: 2048;
margin: 0px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
display: none;
"
/>
<div id="snackbar">
Hay una nueva versión de TeleSec.<br /><a id="reload"
>Pulsa aqui para actualizar.</a
>
</div>
<script src="static/showdown.min.js"></script>
<script src="static/jquery.js"></script>
<script src="static/gun.js"></script>
<script src="static/webrtc.js"></script>
<script src="static/sea.js"></script>
<script src="static/yson.js"></script>
<script src="static/radix.js"></script>
<!-- <script src="static/radisk.js"></script> -->
<!-- <script src="static/store.js"></script> -->
<script src="static/rindexed.js"></script>
<script src="static/path.js"></script>
<script src="static/open.js"></script>
<script src="static/load.js"></script>
<!--<script src="static/synchronous.js"></script>-->
<!--<script src="static/axe.js"></script>-->
<script src="static/toastr.min.js"></script>
<script src="static/doublescroll.js"></script>
<!--<script src="static/simplemde.min.js"></script>-->
<script async>
async function getQuota(cb = () => {}) {
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(
`You've used ${percentageUsed}% of the available storage.`
);
const remaining = quota.quota - quota.usage;
cb(percentageUsed, remaining);
console.log(`You can write up to ${remaining} more bytes.`);
}
}
getQuota();
</script>
<script src="pwa.js"></script>
<script src="config.js"></script>
<script src="gun_init.js"></script>
<script src="app_logic.js"></script>
<script src="app_modules.js"></script>
<script src="page__index.js"></script>
<script src="page__importar.js"></script>
<script src="page__exportar.js"></script>
<script src="page__materiales.js"></script>
<script src="page__resumen_diario.js"></script>
<script src="page__personas.js"></script>
<script src="page__supercafe.js"></script>
<script src="page__notificaciones.js"></script>
<script src="page__comedor.js"></script>
</body>
</html>

BIN
assets/load.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,18 +0,0 @@
{
"theme_color":"#23365e",
"background_color":"#ffffff",
"icons":[
{"purpose":"maskable","sizes":"512x512","src":"icon512_maskable.png","type":"image/png"},
{"purpose":"any","sizes":"512x512","src":"icon512_rounded.png","type":"image/png"}
],
"orientation":"portrait",
"display":"standalone",
"dir":"auto",
"lang":"es-ES",
"start_url":"index.html",
"scope":"/",
"description":"La app de TeleSec",
"id":"telesec.tech.eus",
"name": "TeleSec",
"short_name": "TeleSec"
}

0
assets/page/.placeholder Normal file
View File

35
assets/static/aes.js Normal file
View File

@@ -0,0 +1,35 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<e.length)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)c.push(4294967296*u.random()|0);return new r.init(c,a)}}),w=d.enc={},v=w.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt(a.substr(j,
2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++)e.push(String.fromCharCode(c[j>>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j++)e[j>>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}},
q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=e.splice(0,a);c.sigBytes-=j}return new r.init(q,j)},clone:function(){var a=t.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:t.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new a.init(e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new n.HMAC.init(a,
e)).finalize(b)}}});var n=d.algo={};return d}(Math);
(function(){var u=CryptoJS,p=u.lib.WordArray;u.enc.Base64={stringify:function(d){var l=d.words,p=d.sigBytes,t=this._map;d.clamp();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)d.push(t.charAt(w>>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])},
_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/
4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length<q;){n&&s.update(n);var n=s.update(d).finalize(r);s.reset();for(var a=1;a<p;a++)n=s.finalize(n),s.reset();b.concat(n)}b.sigBytes=4*q;return b}});u.EvpKDF=function(d,l,p){return s.create(p).compute(d,
l)}})();
CryptoJS.lib.Cipher||function(u){var p=CryptoJS,d=p.lib,l=d.Base,s=d.WordArray,t=d.BufferedBlockAlgorithm,r=p.enc.Base64,w=p.algo.EvpKDF,v=d.Cipher=t.extend({cfg:l.extend(),createEncryptor:function(e,a){return this.create(this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return this.create(this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){this.cfg=this.cfg.extend(b);this._xformMode=e;this._key=a;this.reset()},reset:function(){t.reset.call(this);this._doReset()},process:function(e){this._append(e);return this._process()},
finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});d.StreamCipher=v.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=p.mode={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
c[d]},q=(d.BlockCipherMode=l.extend({createEncryptor:function(e,a){return this.Encryptor.create(e,a)},createDecryptor:function(e,a){return this.Decryptor.create(e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();q.Encryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize;x.call(this,e,a,c);b.encryptBlock(e,a);this._prevBlock=e.slice(a,a+c)}});q.Decryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize,d=e.slice(a,a+c);b.decryptBlock(e,a);x.call(this,
e,a,c);this._prevBlock=d}});b=b.CBC=q;q=(p.pad={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=c-a.sigBytes%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)l.push(d);c=s.create(l,c);a.concat(c)},unpad:function(a){a.sigBytes-=a.words[a.sigBytes-1>>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a,
this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684,
1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})},
decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d,
b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}();
(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
<g>
<path style="fill:#010101; fill-opacity:0.4549"
d="M30 62H36L38 60L42 62H56L64 52L54 48H44L43 49L40 48H38L30 62z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4"
d="M14 41V52L30 60L38 52V41L22 35L14 41z"
/>
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="56.22" y1="56.15" x2="49.67" y2="65.65">
<stop offset="0" stop-color="#e07900"/>
<stop offset="1" stop-color="#fff289"/>
</linearGradient>
<path style="fill:url(#gradient0)"
d="M14 41L30 48L38 41L22 35L14 41z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4"
d="M14 6V34L30 41L38 35V6L22 2L14 6z"
/>
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="24.32" y1="-24.13" x2="51.3" y2="-13.84">
<stop offset="0" stop-color="#fff49e"/>
<stop offset="1" stop-color="#ffbd30"/>
</linearGradient>
<path style="fill:url(#gradient1)"
d="M14 6V34L30 41V11L14 6z
M14 41V52L30 60V48L14 41z"
/>
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="30.5" y1="-22.81" x2="56.01" y2="-9.13">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#fff289"/>
</linearGradient>
<path style="fill:url(#gradient2)"
d="M14 6L30 11L38 6L22 2L14 6z"
/>
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="33.3" y1="-0.68" x2="46.77" y2="0.74">
<stop offset="0" stop-color="#ed9406"/>
<stop offset="1" stop-color="#fcb23d"/>
</linearGradient>
<path style="fill:url(#gradient3)"
d="M30 11V41L38 35V6L30 11z
M30 48V60L38 52V41L30 48z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
<g>
<path style="fill:#010101; fill-opacity:0.4078"
d="M38 40C25.84 40 16 45.37 16 52C16 52 25.84 64 38 64C50.14 64 60 58.61 60 52C60 45.37 50.14 40 38 40z"
/>
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="52" y1="-64" x2="68" y2="-64">
<stop offset="0" stop-color="#010000"/>
<stop offset="1" stop-color="#010000" stop-opacity="0"/>
</linearGradient>
<path style="fill:none; stroke:url(#gradient0); stroke-width:4"
d="M44 44C44 44 49.51 38 54 38C58 38 56 46 64 42"
/>
<path style="fill:none; stroke:#010000; stroke-width:4"
d="M28 34C28 34 23 48 14 49V52L34 62L48 48L45 34H28z"
/>
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="41.67" y1="-9.73" x2="71.92" y2="-3.5">
<stop offset="0" stop-color="#5c5c5c"/>
<stop offset="1" stop-color="#b8b8b8"/>
</linearGradient>
<path style="fill:url(#gradient1)"
d="M34 36V62L48 48L45 34L34 36z"
/>
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="14.26" y1="97.46" x2="-8.94" y2="81.76">
<stop offset="0" stop-color="#c9c9c9"/>
<stop offset="1" stop-color="#ffffff"/>
</linearGradient>
<path style="fill:url(#gradient2)"
d="M14 52L34 62V58L14 49V52z"
/>
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="51.94" y1="63.65" x2="26.51" y2="73.34">
<stop offset="0" stop-color="#797979"/>
<stop offset="1" stop-color="#e4e4e4"/>
</linearGradient>
<path style="fill:url(#gradient3)"
d="M34 58C41 51 40 39 40 39L28 34C28 34 23 48 14 49L34 58z"
/>
<path style="fill:none; stroke:#010000; stroke-width:4"
d="M30 2C18.95 2 10 10.95 10 22C10 33.03 18.95 42 30 42C41.03 42 50 33.03 50 22C50 10.95 41.03 2 30 2z"
/>
<radialGradient id="gradient4" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="64" gradientTransform="matrix(0.373,0.2924,-0.3363,0.4291,25.2905,16.1419)">
<stop offset="0.1647" stop-color="#ffffff"/>
<stop offset="0.7686" stop-color="#959191"/>
<stop offset="1" stop-color="#b9b9b9"/>
</radialGradient>
<path style="fill:url(#gradient4)"
d="M30 2C18.95 2 10 10.95 10 22C10 33.03 18.95 42 30 42C41.03 42 50 33.03 50 22C50 10.95 41.03 2 30 2z"
/>
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="-6" y1="-4" x2="38" y2="-4">
<stop offset="0.2509" stop-color="#010101"/>
<stop offset="1" stop-color="#c1acac"/>
</linearGradient>
<path style="fill:none; stroke:url(#gradient5); stroke-width:4"
d="M12.25 43.25C9.83 45.6 10.61 50.31 13.99 53.76C17.35 57.23 22.03 58.1 24.46 55.75C26.87 53.41 26.09 48.7 22.73 45.24C19.36 41.79 14.67 40.91 12.25 43.25z"
transform="matrix(0.9846,0.4347,-0.4379,0.9773,24.6832,-30.2941)"
/>
<radialGradient id="gradient6" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="64" gradientTransform="matrix(0.1345,0.0871,-0.0673,0.1039,20.7047,50.7358)">
<stop offset="0" stop-color="#434a68"/>
<stop offset="1" stop-color="#0f1238"/>
</radialGradient>
<path style="fill:url(#gradient6)"
d="M12.25 43.25C9.83 45.6 10.61 50.31 13.99 53.76C17.35 57.23 22.03 58.1 24.46 55.75C26.87 53.41 26.09 48.7 22.73 45.24C19.36 41.79 14.67 40.91 12.25 43.25z"
transform="matrix(0.9846,0.4347,-0.4379,0.9773,26.8385,-30.2941)"
/>
<path style="fill:#ffffff"
d="M12.25 43.25C9.83 45.6 10.61 50.31 13.99 53.76C17.35 57.23 22.03 58.1 24.46 55.75C26.87 53.41 26.09 48.7 22.73 45.24C19.36 41.79 14.67 40.91 12.25 43.25z"
transform="matrix(-0.0781,0.1744,-0.1757,-0.0775,31.659,20.9543)"
/>
<path style="fill:#ffffff"
d="M19 53C19 53 22.24 53.24 23.24 52.24C24.24 51.24 24 49 24 49C24 49 24.87 52.1 23.87 53.1C22.87 54.1 19 53 19 53z"
transform="matrix(0.9846,0.4347,-0.4379,0.9773,26.8385,-30.2941)"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
<g>
<path style="fill:#000000; fill-opacity:0.396"
d="M32 62H40L44 64L61 43L55.8 40.4L60 38L52 37L32 62z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4; stroke-linejoin:round"
d="M10 39V49L32 60L50 42V36"
/>
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="46.07" y1="78.24" x2="23.93" y2="73.15">
<stop offset="0" stop-color="#82aac8"/>
<stop offset="1" stop-color="#40407c"/>
</linearGradient>
<path style="fill:url(#gradient0)"
d="M32 38L50 20V42L32 60V38z"
/>
<path style="fill:#3d3d5d"
d="M32 38H42L32 48V38z"
/>
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="44.47" y1="70.75" x2="29.89" y2="79.36">
<stop offset="0" stop-color="#5a6e82"/>
<stop offset="1" stop-color="#9be2ff"/>
</linearGradient>
<path style="fill:url(#gradient1)"
d="M11.89 36.01L14 46L32 55V60L10 49V35.99L11.89 36.01z"
/>
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="54.08" y1="49.54" x2="50.66" y2="59.95">
<stop offset="0" stop-color="#5a6e82"/>
<stop offset="1" stop-color="#3a7d99"/>
</linearGradient>
<path style="fill:url(#gradient2)"
d="M32 38V55L14 46L11.89 36.01L32 38z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4; stroke-linejoin:round"
d="M24 46L2 35L10 27H2L24 14H30L37 6L54 12L50 23L62 26L44 44L32 38L24 46z"
/>
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="44.25" y1="2.48" x2="55.01" y2="17.28">
<stop offset="0" stop-color="#40407c"/>
<stop offset="0.9962" stop-color="#417297"/>
</linearGradient>
<path style="fill:url(#gradient3)"
d="M10 27L30 14V33L23.89 37.88L10 30.99V27z"
/>
<linearGradient id="gradient4" gradientUnits="userSpaceOnUse" x1="42.68" y1="-29.81" x2="61.51" y2="-25.25">
<stop offset="0" stop-color="#6dc8ed"/>
<stop offset="1" stop-color="#a4c6e1"/>
</linearGradient>
<path style="fill:url(#gradient4)"
d="M30 14L34 30L50 37V22L30 14z"
/>
<path style="fill:#3d3d5d"
d="M30 14L34 30L50 37L41.85 38.33L30 33V14z"
/>
<path style="fill:#376181"
d="M30 33L23.89 37.88L32 38L36.1 35.74L30 33z"
/>
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="32.24" y1="-14.37" x2="49.83" y2="-16.21">
<stop offset="0" stop-color="#9be2ff"/>
<stop offset="1" stop-color="#5789bd"/>
</linearGradient>
<path style="fill:url(#gradient5)"
d="M37 6L54 12L50 23L30 14L37 6z"
/>
<linearGradient id="gradient6" gradientUnits="userSpaceOnUse" x1="1.22" y1="-19.6" x2="44.89" y2="-14.22">
<stop offset="0" stop-color="#9be2ff"/>
<stop offset="1" stop-color="#5d81a5"/>
</linearGradient>
<path style="fill:url(#gradient6)"
d="M2 35L24 46L32 38L10 27L2 35z"
/>
<path style="fill:#9be2ff"
d="M2 27L24 14H30L10 27H2z"
/>
<linearGradient id="gradient7" gradientUnits="userSpaceOnUse" x1="43.44" y1="10.75" x2="62.48" y2="20.22">
<stop offset="0" stop-color="#9be2ff"/>
<stop offset="1" stop-color="#5a6e82"/>
</linearGradient>
<path style="fill:url(#gradient7)"
d="M32 38L50 23L62 26L44 44L32 38z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256px" height="256px" viewBox="0 0 428.428 428.428" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512.001 512.001" xml:space="preserve">
<g>
<g>
<path d="M467.309,16.768H221.454c-6.128,0-11.095,4.967-11.095,11.095v86.451l12.305-7.64c3.131-1.945,6.475-3.257,9.884-3.978
V38.958h223.665v160.016H232.549v-25.89l-22.19,13.778v23.208c0,6.128,4.967,11.095,11.095,11.095h245.855
c6.127,0,11.095-4.967,11.095-11.095V27.863C478.404,21.735,473.436,16.768,467.309,16.768z"/>
</g>
</g>
<g>
<g>
<path d="M306.001,78.356c-2.919-3.702-8.285-4.335-11.986-1.418l-38.217,30.133c3.649,2.385,6.85,5.58,9.301,9.527
c0.695,1.117,1.298,2.266,1.834,3.431l37.651-29.687C308.286,87.424,308.92,82.057,306.001,78.356z"/>
</g>
</g>
<g>
<g>
<circle cx="121.535" cy="31.935" r="31.935"/>
</g>
</g>
<g>
<g>
<path d="M252.01,124.728c-4.489-7.229-13.987-9.451-21.218-4.963l-31.206,19.375c-0.13-25.879-0.061-12.145-0.144-28.811
c-0.101-20.005-16.458-36.281-36.464-36.281h-15.159c-12.951,33.588-8.779,21.12-19.772,49.63l4.623-20.131
c0.32-1.508,0.088-3.08-0.655-4.43l-6.264-11.393l5.559-10.109c0.829-1.508-0.264-3.356-1.985-3.356h-15.271
c-1.72,0-2.815,1.848-1.985,3.356l5.57,10.13l-6.276,11.414c-0.728,1.325-0.966,2.865-0.672,4.347l4.005,20.172
c-2.159-5.599-17.084-44.306-19.137-49.63H80.093c-20.005,0-36.363,16.275-36.464,36.281l-0.569,113.2
c-0.042,8.51,6.821,15.443,15.331,15.486c0.027,0,0.052,0,0.079,0c8.473,0,15.364-6.848,15.406-15.331l0.569-113.2
c0-0.018,0-0.036,0-0.053c0.024-1.68,1.399-3.026,3.079-3.013c1.68,0.012,3.034,1.378,3.034,3.058l0.007,160.381
c14.106-0.6,27.176,4.488,36.981,13.423v-62.568h7.983v71.773c5.623,8.268,8.914,18.243,8.914,28.974
c0,9.777-2.732,18.928-7.469,26.731c4.866,0.023,9.592,0.669,14.099,1.861c6.076-5.271,13.385-9.151,21.437-11.136
c0-279.342-0.335-106.627-0.335-229.418c0-1.779,1.439-3.221,3.218-3.224c1.779-0.004,3.224,1.432,3.232,3.211
c0.054,10.807,0.224,44.59,0.283,56.351c0.028,5.579,3.07,10.708,7.953,13.407c4.874,2.694,10.835,2.554,15.583-0.394
l54.604-33.903C254.276,141.458,256.499,131.957,252.01,124.728z"/>
</g>
</g>
<g>
<g>
<circle cx="429.221" cy="322.831" r="33.803"/>
</g>
</g>
<g>
<g>
<path d="M511.459,405.811c-0.107-21.176-17.421-38.404-38.598-38.404c-9.137,0-76.583,0-84.781,0
c3.637,7.068,5.704,15.069,5.704,23.55c0,9.005-2.405,18.413-7.5,26.782c18.904,0.764,35.468,10.91,45.149,25.897h40.579v-37.43
c0-1.842,1.46-3.352,3.301-3.415s3.402,1.345,3.526,3.182c0,0,0,0.001,0,0.002l0.19,37.661h32.621L511.459,405.811z"/>
</g>
</g>
<g>
<g>
<path d="M290.469,390.956c0-8.629,2.138-16.763,5.894-23.92c-22.009,0-47.852,0-75.267,0c3.472,6.939,5.437,14.756,5.437,23.029
c0,9.721-2.73,18.926-7.469,26.731c15.558,0.074,29.912,6.538,40.283,17.267c10.054-9.822,23.759-15.914,38.836-15.995
C292.948,409.616,290.469,400.126,290.469,390.956z"/>
</g>
</g>
<g>
<g>
<path d="M264.819,288.655c-18.668,0-33.804,15.132-33.804,33.803c0,18.628,15.107,33.803,33.804,33.803
c18.518,0,33.803-14.965,33.803-33.803C298.622,303.808,283.517,288.655,264.819,288.655z"/>
</g>
</g>
<g>
<g>
<path d="M123.217,390.065c0-8.252,1.956-16.053,5.411-22.98c-1.457-0.072,4.672-0.049-89.485-0.049
c-21.068,0-38.491,17.138-38.598,38.404l-0.192,38.196c14.907,0,17.906,0,32.621,0l0.191-38.031
c0.01-1.884,1.541-3.402,3.423-3.397c1.882,0.006,3.404,1.532,3.404,3.414v38.014h45.727c9.855-15.754,26.8-25.646,45.243-26.406
C125.956,409.168,123.217,399.865,123.217,390.065z"/>
</g>
</g>
<g>
<g>
<path d="M82.786,288.655c-18.668,0-33.803,15.134-33.803,33.803c0,18.584,15.046,33.803,33.803,33.803
c18.536,0,33.804-15.015,33.804-33.803C116.59,303.788,101.455,288.655,82.786,288.655z"/>
</g>
</g>
<g>
<g>
<path d="M422.533,473.807c-0.105-21.178-17.42-38.406-38.597-38.406c-2.246,0-82.969,0-85.507,0
c-21.176,0-39.601,17.227-39.708,38.404l-0.275-0.891c-0.105-21.092-17.341-38.404-38.597-38.404c-24.544,0-59.795,0-85.507,0
c-21.176,0-39.601,17.227-39.708,38.404L94.442,512h32.621l0.191-38.922c0.008-1.622,1.327-2.93,2.948-2.926
c1.621,0.004,2.932,1.32,2.932,2.941v38.908c19.121,0,68.483,0,86.392,0v-38.908c0-1.736,1.405-3.144,3.141-3.149
c1.735-0.004,3.149,1.397,3.158,3.133l0.191,38.923c6.669,0,58.238,0,65.134,0l0.191-38.031c0,0,0-0.001,0-0.002
c0.009-1.621,1.328-2.928,2.949-2.924c1.621,0.004,2.931,1.32,2.931,2.941v38.016c19.121,0,68.483,0,86.392,0v-38.016
c0-1.736,1.405-3.144,3.141-3.149c1.735-0.004,3.149,1.397,3.158,3.133l0.191,38.031h32.621L422.533,473.807z"/>
</g>
</g>
<g>
<g>
<circle cx="175.934" cy="389.933" r="34.198"/>
</g>
</g>
<g>
<g>
<circle cx="342.07" cy="390.821" r="34.198"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="287.96585" height="275.66766" id="svg2" version="1.1" inkscape:version="0.48.0 r9654" sodipodi:docname="Coffee cup.svg">
<defs id="defs4">
<linearGradient id="linearGradient3817">
<stop style="stop-color:#bdbdbd;stop-opacity:1;" offset="0" id="stop3819"/>
<stop style="stop-color:#ececec;stop-opacity:1;" offset="1" id="stop3821"/>
</linearGradient>
<linearGradient id="linearGradient3801">
<stop style="stop-color:#ececec;stop-opacity:1;" offset="0" id="stop3803"/>
<stop style="stop-color:#bdbdbd;stop-opacity:1;" offset="1" id="stop3805"/>
</linearGradient>
<linearGradient id="linearGradient3791">
<stop style="stop-color:#ececec;stop-opacity:1;" offset="0" id="stop3793"/>
<stop style="stop-color:#bdbdbd;stop-opacity:1;" offset="1" id="stop3795"/>
</linearGradient>
<linearGradient id="linearGradient3767">
<stop style="stop-color:#2b2b2b;stop-opacity:1;" offset="0" id="stop3769"/>
<stop style="stop-color:#666666;stop-opacity:1;" offset="1" id="stop3771"/>
</linearGradient>
<linearGradient id="linearGradient3755">
<stop style="stop-color:#dddddd;stop-opacity:1;" offset="0" id="stop3757"/>
<stop style="stop-color:#b2b2b2;stop-opacity:1;" offset="1" id="stop3759"/>
</linearGradient>
<filter inkscape:collect="always" id="filter3781" x="-0.079655327" width="1.1593107" y="-0.23315777" height="1.4663155" color-interpolation-filters="sRGB">
<feGaussianBlur inkscape:collect="always" stdDeviation="13.323301" id="feGaussianBlur3783"/>
</filter>
<filter inkscape:collect="always" id="filter3842" x="-0.1819846" width="1.3639692" y="-0.44528148" height="1.890563" color-interpolation-filters="sRGB">
<feGaussianBlur inkscape:collect="always" stdDeviation="4.4043134" id="feGaussianBlur3844"/>
</filter>
<filter inkscape:collect="always" id="filter3868" color-interpolation-filters="sRGB">
<feGaussianBlur inkscape:collect="always" stdDeviation="1.8652408" id="feGaussianBlur3870"/>
</filter>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3884" gradientUnits="userSpaceOnUse" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3886" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 0, 57.7087)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3888" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -233.089, 102.502)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3910" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3918" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3755" id="radialGradient3920" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="396.42856" cy="491.49908" fx="396.42856" fy="491.49908" r="200.71428"/>
<filter inkscape:collect="always" id="filter3939" x="-0.16236658" width="1.3247333" y="-0.47526056" height="1.9505211" color-interpolation-filters="sRGB">
<feGaussianBlur inkscape:collect="always" stdDeviation="27.157745" id="feGaussianBlur3941"/>
</filter>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3950" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3953" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3957" gradientUnits="userSpaceOnUse" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3971" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3973" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3975" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3997" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3999" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3755" id="radialGradient4001" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="396.42856" cy="491.49908" fx="396.42856" fy="491.49908" r="200.71428"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient4003" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient4005" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient4007" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3061" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 185.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3063" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 107.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3065" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -125.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3801" id="radialGradient3074" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83246, 2.65218e-08, 0, 0.162304, -123.31, 250.603)" cx="280" cy="73.071892" fx="280" fy="73.071892" r="95.5"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3791" id="radialGradient3077" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.7, 109.779, 205.809)" cx="280" cy="139.50504" fx="280" fy="139.50504" r="100"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3817" id="linearGradient3081" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.800412, 0, 0, 0.800412, 187.295, 183.555)" x1="343.33261" y1="220.75931" x2="422.52917" y2="140.95724"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3083" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3767" id="radialGradient3085" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="392.14285" cy="583.4931" fx="392.14285" fy="583.4931" r="200.71428"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3755" id="radialGradient3087" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1, 0, 0, 0.341637, 0, 263.019)" cx="396.42856" cy="491.49908" fx="396.42856" fy="491.49908" r="200.71428"/>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.7071068" inkscape:cx="7.436507" inkscape:cy="230.98232" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1024" inkscape:window-height="742" inkscape:window-x="-4" inkscape:window-y="-4" inkscape:window-maximized="1" fit-margin-top="10" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0">
<inkscape:grid type="xygrid" id="grid3785" empspacing="5" visible="true" enabled="true" snapvisiblegridlinesonly="true"/>
</sodipodi:namedview>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-246.532, -190.385)">
<g id="g3094">
<path transform="matrix(0.618775, 0, 0, 0.646379, 143.447, 142.829)" sodipodi:type="arc" style="fill:url(#radialGradient3083);fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3781)" id="path3779" sodipodi:cx="399.28571" sodipodi:cy="399.50504" sodipodi:rx="200.71428" sodipodi:ry="68.571426" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z"/>
<path d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z" sodipodi:ry="68.571426" sodipodi:rx="200.71428" sodipodi:cy="399.50504" sodipodi:cx="399.28571" id="path3765" style="fill:url(#radialGradient3085);fill-opacity:1;fill-rule:nonzero;stroke:none" sodipodi:type="arc" transform="matrix(0.618775, 0, 0, 0.646379, 143.447, 142.829)"/>
<path transform="matrix(0.646379, 0, 0, 0.646379, 132.426, 136.365)" sodipodi:type="arc" style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3763" sodipodi:cx="399.28571" sodipodi:cy="399.50504" sodipodi:rx="200.71428" sodipodi:ry="68.571426" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z"/>
<path transform="matrix(0.646379, 0, 0, 0.646379, 132.426, 133.78)" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z" sodipodi:ry="68.571426" sodipodi:rx="200.71428" sodipodi:cy="399.50504" sodipodi:cx="399.28571" id="path2985" style="fill:url(#radialGradient3087);fill-opacity:1;fill-rule:nonzero;stroke:none" sodipodi:type="arc"/>
<path transform="matrix(0.372876, 0, 0, 0.319443, 241.631, 265.685)" sodipodi:type="arc" style="fill:#979797;fill-opacity:0.39215686;fill-rule:nonzero;stroke:#b7b7b7;stroke-width:5.61860895;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" id="path3775" sodipodi:cx="399.28571" sodipodi:cy="399.50504" sodipodi:rx="200.71428" sodipodi:ry="68.571426" d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z"/>
<path d="m 599.99998,399.50504 a 200.71428,68.571426 0 1 1 -401.42855,0 200.71428,68.571426 0 1 1 401.42855,0 z" sodipodi:ry="68.571426" sodipodi:rx="200.71428" sodipodi:cy="399.50504" sodipodi:cx="399.28571" id="path3926" style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3939)" sodipodi:type="arc" transform="matrix(0.265496, 0, 0, 0.200192, 283.77, 318.155)"/>
<path sodipodi:nodetypes="cczcczc" inkscape:connector-curvature="0" id="path3815" d="m 473.13884,312.5941 0.45571,-13.03877 c 16.79252,-1.26498 38.44962,-8.39059 38.25779,26.6325 -0.19184,35.02311 -38.64614,28.28143 -48.43686,23.68663 l 0.65784,-10.56391 c 7.09726,2.48526 35.21153,4.81796 35.59877,-13.93125 0.38725,-18.74922 -9.93254,-15.43796 -26.53325,-12.7852 z" style="fill:url(#linearGradient3081);fill-opacity:1;stroke:#d6d6d6;stroke-width:0.80041194;stroke-opacity:1"/>
<path sodipodi:nodetypes="czczc" inkscape:connector-curvature="0" id="path3787" d="m 289.77863,270.46269 c 0,0 0,-20 100,-20 100,0 100,20 100,20 0,0 0,20 -100,20 -100,0 -100,-20 -100,-20 z" style="fill:#e6e6e6;fill-opacity:1;stroke:none"/>
<path sodipodi:nodetypes="czczc" inkscape:connector-curvature="0" id="path3789" d="m 289.77863,270.46269 c 0,0 0,140 100,140 100,0 100,-140 100,-140 0,0 0,20 -100,20 -100,0 -100,-20 -100,-20 z" style="fill:url(#radialGradient3077);fill-opacity:1;stroke:none"/>
<path style="fill:url(#radialGradient3074);fill-opacity:1;stroke:#d6d6d6;stroke-opacity:1" d="m 294.77863,270.46269 c 0,0 0,-15 95,-15 95,0 95,15 95,15 0,0 0,15 -95,15 -95,0 -95,-15 -95,-15 z" id="path3799" inkscape:connector-curvature="0" sodipodi:nodetypes="czczc"/>
<path id="path3809" d="m 389.77863,265.47551 c -64.11627,0 -83.29251,7.59423 -84.875,11.90625 11.22479,3.93491 34.8967,8.09375 84.875,8.09375 49.9783,0 73.65021,-4.15884 84.875,-8.09375 -1.58249,-4.31202 -20.75873,-11.90625 -84.875,-11.90625 z" style="fill:#562c08;fill-opacity:1;stroke:none" inkscape:connector-curvature="0"/>
<path inkscape:connector-curvature="0" id="path3827" d="m 292.10929,298.02015 c 2.55938,4.81149 16.9432,18.3125 97.68492,18.3125 80.69081,0 95.08106,-13.49308 97.65375,-18.3125 -5.46348,5.78106 -25.32465,16.3125 -97.65375,16.3125 -72.39656,0 -92.24323,-10.53491 -97.68492,-16.3125 z" style="fill:#000000;fill-opacity:1;stroke:none"/>
<path style="fill:#000000;fill-opacity:1;stroke:none" d="m 291.54108,294.02015 c 2.57427,4.81149 17.04177,18.3125 98.25322,18.3125 81.16025,0 95.63422,-13.49308 98.22187,-18.3125 -5.49527,5.78106 -25.47198,16.3125 -98.22187,16.3125 -72.81774,0 -92.77987,-10.53491 -98.25322,-16.3125 z" id="path3832" inkscape:connector-curvature="0"/>
<path sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3834" d="m 302.9703,288.54748 25.00128,5.17703 c -2.71055,42.30856 3.19121,65.46435 23.73858,99.6263 -25.41202,-23.58673 -42.16474,-53.00575 -48.73986,-104.80333 z" style="fill:#ffffff;fill-opacity:0.35294118;stroke:none"/>
<path transform="matrix(1.31762, 0, 0, 1.673, 20.9841, 66.1965)" d="m 308.60161,118.72869 a 29.041885,11.869292 0 1 1 -58.08377,0 29.041885,11.869292 0 1 1 58.08377,0 z" sodipodi:ry="11.869292" sodipodi:rx="29.041885" sodipodi:cy="118.72869" sodipodi:cx="279.55972" id="path3836" style="fill:#ffffff;fill-opacity:0.35294118;fill-rule:nonzero;stroke:none;filter:url(#filter3842)" sodipodi:type="arc"/>
<path transform="matrix(0.69763, 0, 0, 0.617722, 194.056, 186.002)" sodipodi:nodetypes="cczzczzzc" inkscape:connector-curvature="0" id="path3846" d="m 307.31913,54.771706 c -11.66884,-21.1275 -16.92627,-23.67626 -27.87348,-26.494634 -20.94721,-2.318374 -29.96874,3.083901 -29.28106,17.972485 0.68768,14.888584 26.53898,25.731883 26.8681,42.599832 0.32912,16.867951 -19.63135,51.743261 -19.63135,51.743261 0,0 31.42782,-33.99061 30.98863,-51.465344 -0.43918,-17.474744 -21.70744,-27.295456 -24.67515,-34.229816 -2.96771,-6.934361 -1.4691,-17.248349 14.72575,-17.437672 16.19485,-0.189323 28.87856,17.311888 28.87856,17.311888 z" style="opacity:0.5;fill:#eaa21f;fill-opacity:0.35294118;stroke:none;filter:url(#filter3868)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,12 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="#000000" width="256px" height="256px" viewBox="0 0 54.13 54.13" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.0005413">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.5413"/>
<g id="SVGRepo_iconCarrier">
<title>cogs</title>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="353" height="512" preserveAspectRatio="xMidYMid">
<path fill-rule="evenodd" d="M299.596 492.223C266.856 504.976 223.496 512 177.5 512c-45.996 0-89.357-7.024-122.095-19.777-35.083-13.667-54.404-32.766-54.404-53.78V73.557c0-21.014 19.321-40.113 54.404-53.78C88.144 7.024 131.504 0 177.5 0c45.996 0 89.356 7.024 122.096 19.778 35.069 13.661 54.387 32.751 54.402 53.756 0 .007.001.014.001.023v364.886c0 21.014-19.321 40.113-54.403 53.78ZM293.872 34.44C262.914 22.381 221.585 15.739 177.5 15.739c-44.085 0-85.414 6.642-116.372 18.702-28.2 10.984-44.374 25.243-44.374 39.116 0 13.873 16.174 28.131 44.374 39.116 30.958 12.058 72.287 18.7 116.372 18.7 16.532 0 32.675-.934 48.031-2.738 25.592-3.005 48.992-8.425 68.341-15.963 28.199-10.985 44.372-25.243 44.372-39.116 0-13.873-16.172-28.131-44.372-39.116Zm44.375 70.393c-.015.014-.031.027-.046.041-.133.122-.275.244-.41.367-10.912 9.91-24.554 16.781-38.194 22.095-21.485 8.369-47.544 14.27-75.921 17.339-14.865 1.608-30.364 2.438-46.175 2.438-45.996 0-89.357-7.024-122.095-19.777-13.896-5.413-27.587-12.266-38.614-22.469-.012-.011-.024-.021-.036-.032v90.35c0 13.872 16.173 28.13 44.373 39.115 26.121 10.176 59.624 16.495 95.938 18.221 6.805.325 13.62.482 20.434.482 48.743 0 106.184-3.582 146.132-34.692 5.307-4.133 10.264-9.024 12.936-15.305 1.049-2.46 1.678-5.139 1.678-7.82v-90.353Zm.001 121.627c-.012.009-.024.02-.034.029-2.528 2.319-5.204 4.475-7.998 6.469-17.383 12.41-38.247 19.695-58.788 24.922-16.994 4.324-35.701 7.424-55.406 9.177-.172.014-.343.032-.515.047-6.485.568-13.077.987-19.749 1.261-6.026.248-12.117.377-18.256.377-45.996 0-89.357-7.024-122.095-19.777-13.652-5.316-27.217-12.143-38.139-22.045-.155-.141-.317-.279-.471-.421-.013-.012-.026-.024-.04-.036v90.352c0 13.873 16.173 28.131 44.373 39.116 30.958 12.059 72.287 18.701 116.372 18.701 11.881 0 23.559-.487 34.89-1.429.813-.066 1.625-.137 2.433-.208 29.767-2.655 57.032-8.487 79.049-17.064 12.028-4.685 21.859-9.967 29.189-15.559 6.171-4.708 12.25-10.671 14.436-18.344.015-.052.024-.104.039-.155.458-1.648.71-3.348.71-5.059V226.46Zm.002 121.628c-.013.012-.025.022-.038.034-.172.159-.356.317-.531.475-2.552 2.309-5.255 4.445-8.073 6.42-9.261 6.487-19.492 11.48-30.01 15.577-28.508 11.103-65.07 17.861-104.411 19.423-5.839.232-11.738.352-17.683.352-45.996 0-89.357-7.024-122.095-19.777-13.687-5.33-27.127-12.071-38.086-21.997-.174-.157-.356-.314-.528-.472-.013-.012-.025-.022-.038-.034v90.352c0 13.872 16.173 28.13 44.373 39.115 30.959 12.06 72.286 18.702 116.372 18.702s85.414-6.642 116.373-18.702c28.199-10.985 44.372-25.241 44.372-39.115h.003v-90.353Zm-28.174 80.883c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.95 0 14.394 6.439 14.394 14.381 0 7.943-6.444 14.381-14.394 14.381Zm-49.956 15.047c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.394 6.439 14.394 14.381 0 7.943-6.445 14.381-14.394 14.381Zm-53.012 8.237c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.393 6.439 14.393 14.381 0 7.943-6.444 14.381-14.393 14.381Zm102.968-143.877c-7.949 0-14.393-6.439-14.393-14.381 0-7.943 6.444-14.381 14.393-14.381 7.95 0 14.394 6.438 14.394 14.381 0 7.942-6.444 14.381-14.394 14.381Zm-49.956 15.057c-7.949 0-14.393-6.439-14.393-14.381 0-7.943 6.444-14.381 14.393-14.381 7.949 0 14.394 6.438 14.394 14.381 0 7.942-6.445 14.381-14.394 14.381Zm-53.012 8.237c-7.949 0-14.393-6.439-14.393-14.381 0-7.943 6.444-14.381 14.393-14.381 7.949 0 14.393 6.438 14.393 14.381 0 7.942-6.444 14.381-14.393 14.381Zm102.968-145.986c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.95 0 14.394 6.439 14.394 14.381 0 7.943-6.444 14.381-14.394 14.381Zm-49.956 15.057c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.394 6.439 14.394 14.381 0 7.943-6.445 14.381-14.394 14.381Zm-53.012 8.237c-7.949 0-14.393-6.438-14.393-14.381 0-7.942 6.444-14.381 14.393-14.381 7.949 0 14.393 6.439 14.393 14.381 0 7.943-6.444 14.381-14.393 14.381Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
<g>
<path style="fill:#010101; fill-opacity:0.4235"
d="M28 64C28 64 33 64 38 64C43 64 48.2 60.96 52.75 61.12C63.12 61.5 67.25 56.37 60.87 54.37C57.04 53.17 52 53.25 49.12 51.25C45.45 48.69 38 48 38 48V55L28 64z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4"
d="M12 18V35L16 39V56L20 58L22 58L23 59.5L28 62L36 54V43L40 39V21L21 12L12 18z"
/>
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="73.74" y1="-57.07" x2="116.61" y2="-8.05">
<stop offset="0" stop-color="#ffdb97"/>
<stop offset="1" stop-color="#fcaf29"/>
</linearGradient>
<path style="fill:url(#gradient0)"
d="M12 18V35L16 39V56L20 58L22 58L23 59.5L28 62V45H32V27L12 18z"
/>
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="-43.97" y1="-33.98" x2="-24.83" y2="-51.95">
<stop offset="0" stop-color="#fff7ea"/>
<stop offset="0.9962" stop-color="#fdd17b"/>
</linearGradient>
<path style="fill:url(#gradient1)"
d="M12 18L32 27L40 21L26 20L31.99 16.99L21 12L12 18z"
/>
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="54.23" y1="-52.61" x2="75.84" y2="-45.97">
<stop offset="0" stop-color="#c85805"/>
<stop offset="1" stop-color="#f06306"/>
</linearGradient>
<path style="fill:url(#gradient2)"
d="M32 45H28V62L36 54V43L40 39V21L32 27V45z"
/>
<path style="fill:#a32904"
d="M28 45V51L36 43L32 45H28z"
/>
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="28.92" y1="-64" x2="39.07" y2="-64">
<stop offset="0" stop-color="#c85804"/>
<stop offset="1" stop-color="#dc952f"/>
</linearGradient>
<path style="fill:url(#gradient3)"
d="M26 20L40 21L32 17L26 20z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4"
d="M26 2C22 2 18 6 18 10C18 14 22 18 26 18C30 18 34 14 34 10C34 6 30 2 26 2z"
/>
<radialGradient id="gradient4" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="64" gradientTransform="matrix(0.2361,0,0,0.2321,22.625,6.375)">
<stop offset="0" stop-color="#f2f2f2"/>
<stop offset="1" stop-color="#bca184"/>
<stop offset="0.6742" stop-color="#7d7a7a"/>
</radialGradient>
<path style="fill:url(#gradient4)"
d="M26 2C22 2 18 6 18 10C18 14 22 18 26 18C30 18 34 14 34 10C34 6 30 2 26 2z"
/>
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="54.23" y1="-52.61" x2="75.84" y2="-45.97">
<stop offset="0" stop-color="#c85805"/>
<stop offset="1" stop-color="#f06306"/>
</linearGradient>
<path style="fill:url(#gradient5)"
d="M20 58H22V44L20 43V58z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg version="1.1" width="64" height="64" color-interpolation="linearRGB"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
<g>
<path style="fill:#000000; fill-opacity:0.3882"
d="M60 35.39L32 64H38L64 37.39L60 35.39z"
/>
<path style="fill:none; stroke:#000000; stroke-width:4"
d="M2 30V47L32 62L58 36V19L30 10L2 30z"
/>
<linearGradient id="gradient0" gradientUnits="userSpaceOnUse" x1="45.2" y1="-3.44" x2="66.1" y2="20.77">
<stop offset="0" stop-color="#6499e8"/>
<stop offset="1" stop-color="#1b63ce"/>
</linearGradient>
<path style="fill:url(#gradient0)"
d="M32 43L2 30V47L32 62V43z"
/>
<linearGradient id="gradient1" gradientUnits="userSpaceOnUse" x1="64.29" y1="5.5" x2="75.95" y2="14.74">
<stop offset="0" stop-color="#10489b"/>
<stop offset="1" stop-color="#0a54c3"/>
</linearGradient>
<path style="fill:url(#gradient1)"
d="M32 43L58 19V36L32 62V43z"
/>
<linearGradient id="gradient2" gradientUnits="userSpaceOnUse" x1="8" y1="-64" x2="56" y2="-64">
<stop offset="0" stop-color="#c0d9ff"/>
<stop offset="1" stop-color="#5e95e8"/>
</linearGradient>
<path style="fill:url(#gradient2)"
d="M32 43L58 19L30 10L2 30L32 43z"
/>
<linearGradient id="gradient3" gradientUnits="userSpaceOnUse" x1="18" y1="20" x2="34" y2="20">
<stop offset="0" stop-color="#2a4fae"/>
<stop offset="0.25" stop-color="#4ca0da"/>
<stop offset="1" stop-color="#0434a1"/>
</linearGradient>
<path style="fill:url(#gradient3)"
d="M15 28C15 25.79 18.58 24 23 24C27.41 24 31 25.79 31 28V31C31 33.2 27.41 35 23 35C18.58 35 15 33.2 15 31V28z"
/>
<linearGradient id="gradient4" gradientUnits="userSpaceOnUse" x1="4" y1="-65" x2="50" y2="-65">
<stop offset="0" stop-color="#c0d9ff"/>
<stop offset="1" stop-color="#5e95e8"/>
</linearGradient>
<path style="fill:url(#gradient4)"
d="M15 27C15 24.79 18.58 23 23 23C27.41 23 31 24.79 31 27C31 29.2 27.41 31 23 31C18.58 31 15 29.2 15 27z"
/>
<linearGradient id="gradient5" gradientUnits="userSpaceOnUse" x1="18" y1="20" x2="34" y2="20">
<stop offset="0" stop-color="#2a4fae"/>
<stop offset="0.25" stop-color="#4ca0da"/>
<stop offset="1" stop-color="#0434a1"/>
</linearGradient>
<path style="fill:url(#gradient5)"
d="M15 28C15 25.79 18.58 24 23 24C27.41 24 31 25.79 31 28V31C31 33.2 27.41 35 23 35C18.58 35 15 33.2 15 31V28z"
transform="matrix(0.9375,0,0,0.9375,15,-8.125)"
/>
<linearGradient id="gradient6" gradientUnits="userSpaceOnUse" x1="4" y1="-65" x2="50" y2="-65">
<stop offset="0" stop-color="#c0d9ff"/>
<stop offset="1" stop-color="#5e95e8"/>
</linearGradient>
<path style="fill:url(#gradient6)"
d="M15 27C15 24.79 18.58 23 23 23C27.41 23 31 24.79 31 27C31 29.2 27.41 31 23 31C18.58 31 15 29.2 15 27z"
transform="matrix(0.9375,0,0,0.9375,15,-8.125)"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 115.21" style="enable-background:new 0 0 122.88 115.21" xml:space="preserve"><g><path d="M29.03,100.46l20.79-25.21l9.51,12.13L41,110.69C33.98,119.61,20.99,110.21,29.03,100.46L29.03,100.46z M53.31,43.05 c1.98-6.46,1.07-11.98-6.37-20.18L28.76,1c-2.58-3.03-8.66,1.42-6.12,5.09L37.18,24c2.75,3.34-2.36,7.76-5.2,4.32L16.94,9.8 c-2.8-3.21-8.59,1.03-5.66,4.7c4.24,5.1,10.8,13.43,15.04,18.53c2.94,2.99-1.53,7.42-4.43,3.69L6.96,18.32 c-2.19-2.38-5.77-0.9-6.72,1.88c-1.02,2.97,1.49,5.14,3.2,7.34L20.1,49.06c5.17,5.99,10.95,9.54,17.67,7.53 c1.03-0.31,2.29-0.94,3.64-1.77l44.76,57.78c2.41,3.11,7.06,3.44,10.08,0.93l0.69-0.57c3.4-2.83,3.95-8,1.04-11.34L50.58,47.16 C51.96,45.62,52.97,44.16,53.31,43.05L53.31,43.05z M65.98,55.65l7.37-8.94C63.87,23.21,99-8.11,116.03,6.29 C136.72,23.8,105.97,66,84.36,55.57l-8.73,11.09L65.98,55.65L65.98,55.65z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" width="256px" height="256px" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 493.474 493.474" xml:space="preserve" width="256px" height="256px" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" /></svg>

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11,9H9V2H7V9H5V2H3V9C3,11.12 4.66,12.84 6.75,12.97V22H9.25V12.97C11.34,12.84 13,11.12 13,9V2H11V9M16,6V14H18.5V22H21V2C18.24,2 16,4.24 16,6Z" /></svg>

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,109 +0,0 @@
;(function(){
var sT = setTimeout || {}, u;
if(typeof window !== ''+u){ sT.window = window }
var AXE = (sT.window||'').AXE || function(){};
if(AXE.window = sT.window){ AXE.window.AXE = AXE }
var Gun = (AXE.window||'').GUN || require('./gun');
(Gun.AXE = AXE).GUN = AXE.Gun = Gun;
//if(!Gun.window){ try{ require('./lib/axe') }catch(e){} }
if(!Gun.window){ require('./lib/axe') }
Gun.on('opt', function(at){ start(at) ; this.to.next(at) }); // make sure to call the "next" middleware adapter.
function start(root){
if(root.axe){ return }
var opt = root.opt, peers = opt.peers;
if(false === opt.axe){ return }
if(!Gun.window){ return } // handled by ^ lib/axe.js
var w = Gun.window, lS = w.localStorage || opt.localStorage || {}, loc = w.location || opt.location || {}, nav = w.navigator || opt.navigator || {};
var axe = root.axe = {}, tmp, id;
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
tmp = peers[id = loc.origin + '/gun'] = peers[id] || {};
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
tmp = peers[id = 'http://localhost:8765/gun'] = peers[id] || {};
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
Gun.log.once("AXE", "AXE enabled: Trying to find network via (1) local peer (2) last used peers (3) a URL parameter, and last (4) hard coded peers.");
Gun.log.once("AXEWarn", "Warning: AXE is in alpha, use only for testing!");
var last = lS.peers || ''; if(last){ last += ' ' }
last += ((loc.search||'').split('peers=')[1]||'').split('&')[0];
root.on('bye', function(peer){
this.to.next(peer);
if(!peer.url){ return } // ignore WebRTC disconnects for now.
if(!nav.onLine){ peer.retry = 1 }
if(peer.retry){ return }
if(axe.fall){ delete axe.fall[peer.url || peer.id] }
(function next(){
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
var fall = Object.keys(axe.fall||''), one = fall[(Math.random()*fall.length) >> 0];
if(!fall.length){ lS.peers = ''; one = 'https://gunjs.herokuapp.com/gun' } // out of peers
if(peers[one]){ next(); return } // already choose
mesh.hi(one);
}());
});
root.on('hi', function(peer){ // TEMPORARY! Try to connect all peers.
this.to.next(peer);
if(!peer.url){ return } // ignore WebRTC disconnects for now.
return; // DO NOT COMMIT THIS FEATURE YET! KEEP TESTING NETWORK PERFORMANCE FIRST!
(function next(){
if(!peer.wire){ return }
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
var one = (next.fall = next.fall || Object.keys(axe.fall||'')).pop();
if(!one){ return }
setTimeout(next, 99);
mesh.say({dam: 'opt', opt: {peers: one}}, peer);
}());
});
function found(text){
axe.fall = {};
((text||'').match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ig)||[]).forEach(function(url){
axe.fall[url] = {url: url, id: url, retry: 0}; // RETRY
});
return;
// TODO: Finish porting below? Maybe not.
Object.keys(last.peers||'').forEach(function(key){
tmp = peers[id = key] = peers[id] || {};
tmp.id = tmp.url = id;
});
tmp = peers[id = 'https://guntest.herokuapp.com/gun'] = peers[id] || {};
tmp.id = tmp.url = id;
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
mesh.way = function(msg){
if(root.$ === msg.$ || (msg._||'').via){
mesh.say(msg, opt.peers);
return;
}
var at = (msg.$||'')._;
if(!at){ mesh.say(msg, opt.peers); return }
if(msg.get){
if(at.axe){ return } // don't ask for it again!
at.axe = {};
}
mesh.say(msg, opt.peers);
}
}
if(last){ found(last); return }
try{ fetch(((loc.search||'').split('axe=')[1]||'').split('&')[0] || loc.axe || 'https://raw.githubusercontent.com/wiki/amark/gun/volunteer.dht.md').then(function(res){
return res.text()
}).then(function(text){
found(lS.peers = text);
}).catch(function(){
found(); // nothing
})}catch(e){found()}
}
var empty = {}, yes = true;
try{ if(typeof module != ''+u){ module.exports = AXE } }catch(e){}
}());

BIN
assets/static/cash_flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

20
assets/static/chart.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,476 @@
html {
color-scheme: light only;
margin: 0;
}
body {
font-family: Arial, Helvetica, sans-serif;
margin: 0;
}
main {
/*max-width: 45rem;
margin: 0 auto;*/
padding: 15px;
margin: 0;
margin-bottom: 20px;
}
.supermesh-indicator {
border-top-left-radius: 15px;
background-color: greenyellow;
border-top: 5px solid green;
border-left: 5px solid green;
padding: 15px;
max-width: 30rem;
position: fixed;
right: 0;
bottom: 0;
color: black;
display: none;
}
.supermesh-indicator a {
color: blue;
}
details.supermesh-indicator summary {
font-size: unset;
}
.link,
a {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.link:hover,
a:hover {
text-decoration: underline;
}
#articleID {
font-family: monospace;
}
@media (prefers-color-scheme: dark) {
.link,
a {
color: lightblue;
}
}
@media print {
.supermesh-indicator,
.no_print,
.no_print *,
.saveico, .delico, .opicon {
display: none !important;
}
main {padding: 0;}
}
button,
.button {
display: inline-block;
padding: 5px 10px;
background-color: beige;
border: 2px solid black;
font-size: 20px;
margin: 3px;
text-decoration: none;
color: black;
}
button:hover,
.button:hover {
text-decoration: underline;
}
/* https://coolors.co/palette/ff0000-ff8700-ffd300-deff0a-a1ff0a-0aff99-0aefff-147df5-580aff-be0aff */
.rojo {
background: #ff0000;
color: white;
}
.btn1 {
background: #ff0000;
color: white;
}
.btn2 {
background: #ff8700;
color: white;
}
.btn3 {
background: #ffd300;
color: black;
}
.btn4 {
background: #deff0a;
color: black;
}
.btn5 {
background: #a1ff0a;
color: black;
}
.btn6 {
background: #0aff99;
color: black;
}
.btn7 {
background: #0aefff;
color: black;
}
.btn8 {
background: #147df5;
color: white;
}
.nav-disabled {
background: black !important;
color: grey !important;
}
.nav-disabled:hover {
text-decoration: unset !important;
}
input,
select,
textarea {
font-size: 18px;
padding: 5px;
width: calc(100% - 11px);
}
input[type="checkbox"]{
padding: 0;
width: 24px;
height: 24px;
}
select {
width: 100%;
}
details input,
details select,
details textarea {
font-size: 18px;
padding: 5px;
width: calc(100% - 15px);
}
input[type="color"] {
width: 50px;
height: 50px;
}
textarea {
height: 150px;
}
details summary {
font-size: 20px;
}
thead tr {
background-color: black;
color: white;
}
table {
display: block;
line-break: loose;
width: fit-content;
min-width: 750px;
border: 1px solid black;
}
table tr th {
line-break: auto;
padding: 2px 5px;
}
table tr td {
border-bottom: 3px solid black !important;
padding: 5px;
}
.scase {
text-transform: lowercase;
}
.scase:first-letter {
text-transform: uppercase;
}
table tr:hover td {
text-decoration: underline;
background: rgba(200, 200, 200, 0.5);
/* color: black; */
}
table tr:hover td.TextBorder {
background: inherit;
color: inherit;
text-decoration: none;
}
fieldset {
max-width: 25rem;
}
.TextBorder {
color: black;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff,
1px 1px 0 #fff;
-webkit-text-stroke: 0.25px #fff;
}
code {
font-size: x-small;
color: gray;
}
.activeSCButton {
border: 7px dashed beige;
color: beige;
background: black !important;
}
.btn1.activeSCButton {
border-color: #ff0000;
color: #ff0000;
}
.btn2.activeSCButton {
border-color: #ff8700;
color: #ff8700;
}
.btn3.activeSCButton {
border-color: #ffd300;
color: #ffd300;
}
.btn4.activeSCButton {
border-color: #deff0a;
color: #deff0a;
}
.btn5.activeSCButton {
border-color: #a1ff0a;
color: #a1ff0a;
}
.btn6.activeSCButton {
border-color: #0aff99;
color: #0aff99;
}
.btn7.activeSCButton {
border-color: #0aefff;
color: #0aefff;
}
.btn8.activeSCButton {
border-color: #147df5;
color: #147df5;
}
hr {
border-color: black;
border-style: solid;
}
#snackbar {
visibility: hidden;
/* min-width: 250px; */
background-color: #333;
color: #fff;
text-align: center;
border-radius: 2px;
padding: 16px;
position: fixed;
z-index: 1;
right: 70px;
bottom: 25px;
}
#snackbar a {
color: lightblue;
}
#snackbar.show {
visibility: visible;
}
.ribbon {
display: flex;
background: linear-gradient(to bottom, #d0d8ec, #eef2fa);
border-bottom: 1px solid #a2a9b9;
padding: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.ribbon-orb {
width: 50px !important;
height: 50px;
border-radius: 50%;
/* background: url(/icon512_maskable.png); */
background-size: 50px 50px;
background-position: center middle;
border: 1px solid #a2a9b9;
margin-right: 10px;
margin-top: 0;
}
.ribbon-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-top: 1px;
width: calc(100% - 60px);
}
.ribbon-tabs {
display: flex;
background: #c8d4eb;
border: 1px solid #a2a9b9;
height: 26px;
align-items: center;
padding: 0 5px;
border-radius: 3px 3px 0 0;
overflow-x: auto;
}
.ribbon-tab {
padding: 4px 9px;
cursor: pointer;
border-right: 1px solid #a2a9b9;
font-size: 13px;
}
.ribbon-tab.active {
background-color: #eaf0fb;
font-weight: bold;
}
.ribbon-panel {
display: flex;
gap: 3px;
background-color: #c8d4eb;
border: 1px solid #a2a9b9;
overflow-x: auto;
padding: 5px;
}
.ribbon-button {
width: auto;
text-align: center;
cursor: pointer;
background: none;
border: none;
color: black;
white-space: nowrap;
margin: 0;
padding: 0;
border: 1px solid lightskyblue;
background: white;
padding: 4px;
display: inline-block;
border-radius: 10px;
}
.ribbon-button img {
height: 60px;
display: block;
margin: auto;
}
.ribbon-button .label {
font-size: 12px;
margin-top: 5px;
display: inline-block;
}
.ribbon-button.orange {
background-color: orange;
border-radius: 3px;
padding: 2px;
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2);
}
details {
margin: 0;
}
details[open] .ribbon-panel {
display: flex;
}
details:not([open]) .ribbon-panel {
display: none;
}
fieldset legend {
font-size: 20px;
font-weight: bold;
}
pre {
font-size: 15px;
}
.picto {
min-height: 125px;
width: 100px;
border: 2.5px solid black;
border-radius: 5px;
text-align: center;
background: white;
margin-bottom: 20px;
margin-left: auto;
margin-right: auto;
}
.picto b {
padding-top: 40px;
display: inline-block;
}
.panel-option input {
display: none;
}
.panel-option:has(input:checked) {
background-color: #ccc;
outline: 5px solid blue;
}
.saveico {
border-color: green !important;
}
.delico {
border-color: red !important;
}
.opicon {
border-color: blue !important;
}
.saveico img, .delico img, .opicon img {
height: 52px;
vertical-align: middle;
}
.saveico, .delico, .opicon {
padding: 2.5px 7.5px;
background: transparent;
border-radius: 10px;
border: 4px solid;
}

BIN
assets/static/exchange.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/static/exit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/static/find.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/static/garbage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#000000" d="M428.3 27.27c-5 0-10.3.34-15.9.95-11 17-20.9 33.24-23.5 48.93l-17.8-2.94c2.5-15.07 9.2-28.82 17.1-41.86-.9.19-1.7.36-2.6.57-36.2 8.57-79.3 26.23-122.5 49.46-5.4 2.89-10.8 5.88-16.1 8.94-.8 4.97-1.8 10.98-3.1 17.58-3.2 16.3-7.8 35.2-16.8 48.1-11.5 16.2-32.6 30.4-51.2 41.6-18.6 11.2-34.7 18.9-34.7 18.9l-7.8-16.2s15.4-7.4 33.2-18.1c17.8-10.8 37.8-25.4 45.7-36.6 5.6-7.9 10.9-25.8 14-41.2.1-.7.3-1.4.4-2.1-73.3 44.9-141.29 103.3-171.34 154.8-15.1 26-23.44 58.6-23.11 90.8 13.76-26.2 29.02-52.8 54.31-77.8l12.66 12.8C68.79 314 53.9 347.3 35.71 381.3c5.89 28.2 19.41 53.8 41.59 71.8 4.04 3.3 8.49 6.4 13.29 9.2 1.75-6.2 4.27-14.6 7.45-23.7 6.46-18.5 14.86-39.8 27.26-53 15-15.9 35.8-21.4 54.5-27 18.7-5.5 35.2-11 45.2-22.6 9.3-11 17.9-36.1 23-57.9 5.1-21.7 7.4-40.2 7.4-40.2l17.8 2.2s-2.3 19.3-7.7 42.1c-5.3 22.8-12.9 49.1-26.9 65.4-14.2 16.7-34.9 22.7-53.7 28.3-18.8 5.6-35.9 10.9-46.4 22.1-8.2 8.7-17.4 28.9-23.5 46.5-3.7 10.7-6.4 20-8 25.9 14.1 5.9 30.1 10 46.8 12.3 9-14.4 16.6-22.2 30.1-76.9l17.4 4.4c-11.6 46.7-19.8 62.2-27.4 74.3 36.2 1.6 73.4-5.3 100-19.7 75.3-41.2 138.2-140.1 173.7-233.8 6.8-17.9 12.5-35.6 17.3-52.7-17.9 15.3-32.8 32-41.1 53.1l-16.8-6.6c13-32.8 38.2-55.4 65-75.5 3.5-16.6 5.9-32.2 7-46.3.5-6.82.8-13.29.7-19.34-6.5 3.66-13.9 7.91-21.7 12.71-24.4 15.03-51.9 35.33-62.8 51.93-5.1 7.7-6 18.9-6.7 31.9-.7 13.1-1 27.9-9.6 41-7.8 12-19.9 18.2-30.5 23.7-10.6 5.5-19.8 10.5-25 17.4-10.3 13.6-20.8 41-27.9 64.4-7.2 23.3-11.4 42.8-11.4 42.8l-17.6-3.8s4.4-20.2 11.8-44.3c7.3-24.1 17.3-52.1 30.7-69.9 8.5-11.3 20.6-17.1 31.1-22.5 10.4-5.5 19.1-10.5 23.8-17.6 5-7.8 6-19.1 6.6-32.1.7-13 1.1-27.9 9.7-40.9v-.1c14.5-21.8 43.1-41.68 68.4-57.25 11.1-6.84 21.4-12.65 29.7-17.11-.3-1.99-.7-3.9-1.1-5.71-2.6-11.57-7-18.85-12-22.32-7.3-5.12-18.1-8.01-31.7-8.55-1.7-.1-3.4-.1-5.2-.1zm-113 58.9l17.4 4.5s-4.1 15.83-10.7 34.63c-6.7 18.9-15.4 40.6-27.3 54.4-20.7 24.3-49.8 36.9-77.5 49-27.7 12.1-54 23.9-71.7 43.8v.1c-13.8 15.6-28.7 47.3-39.3 74.5-10.73 27.3-17.58 50.2-17.58 50.2l-17.24-5.2s7.04-23.5 18.02-51.5c11-28 25.4-60.4 42.7-79.9 21-24 50.2-36.3 77.9-48.5 27.7-12.1 53.8-23.9 71.1-44.1 8.2-9.7 17.5-30.7 23.9-48.8 6.4-18 10.3-33.13 10.3-33.13zM197.6 273.2l17.8 2.6c-2.8 19.4-11.8 33.8-23.2 44.2-11.4 10.3-25 17.1-37.9 23.5l-8-16c12.8-6.5 24.8-12.7 33.8-20.9 9-8.1 15.3-17.8 17.5-33.4zm180.3 7.3l16.4 7.2s-9.6 22-23.6 47.7c-14 25.8-31.9 55.4-51 72.3-13.6 12.1-35 21.6-53.6 28.9-18.6 7.4-34.2 12.1-34.2 12.1l-5.2-17.2s15-4.5 32.8-11.6c17.8-7 38.6-17 48.2-25.6 15.3-13.5 33.5-42.4 47.2-67.4 13.6-25.1 23-46.4 23-46.4z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#d92d27;}</style></defs><title>no-wifi</title><path class="cls-1" d="M101.68,32.93,32.92,101.68a49.29,49.29,0,0,0,77.83-40.24h0A49.34,49.34,0,0,0,108,45.15a48.85,48.85,0,0,0-6.32-12.22ZM24,93.5,93.49,24A49.31,49.31,0,0,0,24,93.5Z"/><path d="M30.29,52A3,3,0,0,1,26,51.63v0a3,3,0,0,1,.34-4.24h0A59.27,59.27,0,0,1,43.27,37a48,48,0,0,1,36.4.31A61,61,0,0,1,96.46,47.9a1.29,1.29,0,0,1,.17.16,3,3,0,0,1,.27,4.07,1.54,1.54,0,0,1-.17.19,3,3,0,0,1-4.16.19A55.23,55.23,0,0,0,77.47,43a41.86,41.86,0,0,0-32.08-.27A53.38,53.38,0,0,0,30.29,52ZM61.44,76.09A6.59,6.59,0,1,1,56.77,78h0a6.62,6.62,0,0,1,4.67-1.93ZM50.05,72.5a3,3,0,0,1-4.16-.35,1.37,1.37,0,0,1-.16-.18,3,3,0,0,1,.43-4.07l.17-.14a27.64,27.64,0,0,1,7.33-4.33,21.68,21.68,0,0,1,7.84-1.52,21.35,21.35,0,0,1,7.8,1.47,27.12,27.12,0,0,1,7.34,4.36A3,3,0,0,1,77.08,72h0a3,3,0,0,1-2,1.1,3.06,3.06,0,0,1-2.21-.66h0a21.27,21.27,0,0,0-5.62-3.37,15.12,15.12,0,0,0-11.47,0,22,22,0,0,0-5.7,3.41Zm-9.56-9.71-.15.13a3.06,3.06,0,0,1-2.08.67,3,3,0,0,1-2-1,1,1,0,0,1-.14-.15,3,3,0,0,1,.34-4.16,45.78,45.78,0,0,1,12.36-8,30.76,30.76,0,0,1,25.6.42,45.74,45.74,0,0,1,12.11,8.41l.08.07a3.09,3.09,0,0,1,.87,2,3,3,0,0,1-.82,2.15l-.07.08a3,3,0,0,1-2,.87,3,3,0,0,1-2.15-.81A40.13,40.13,0,0,0,72,56.28a24.75,24.75,0,0,0-21-.35,39.68,39.68,0,0,0-10.5,6.86Z"/><path class="cls-2" d="M61.44,0A61.31,61.31,0,1,1,38,4.66,61.29,61.29,0,0,1,61.44,0Zm40.24,32.93L32.93,101.68A49.44,49.44,0,0,0,80.31,107,49.53,49.53,0,0,0,107,80.3a49,49,0,0,0,3.73-18.86h0a48.93,48.93,0,0,0-9.08-28.51ZM24,93.5,93.5,24A49.32,49.32,0,0,0,24,93.5Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,7 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.chain.open || require('./open');
Gun.chain.load = function(cb, opt, at){
(opt = opt || {}).off = !0;
return this.open(cb, opt, at);
}

View File

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 185 KiB

View File

@@ -1,58 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.chain.open = function(cb, opt, at, depth){ // this is a recursive function, BEWARE!
depth = depth || 1;
opt = opt || {}; // init top level options.
opt.doc = opt.doc || {};
opt.ids = opt.ids || {};
opt.any = opt.any || cb;
opt.meta = opt.meta || false;
opt.eve = opt.eve || {off: function(){ // collect all recursive events to unsubscribe to if needed.
Object.keys(opt.eve.s).forEach(function(i,e){ // switch to CPU scheduled setTimeout.each?
if(e = opt.eve.s[i]){ e.off() }
});
opt.eve.s = {};
}, s:{}}
return this.on(function(data, key, ctx, eve){ // subscribe to 1 deeper of data!
clearTimeout(opt.to); // do not trigger callback if bunch of changes...
opt.to = setTimeout(function(){ // but schedule the callback to fire soon!
if(!opt.any){ return }
opt.any.call(opt.at.$, opt.doc, opt.key, opt, opt.eve); // call it.
if(opt.off){ // check for unsubscribing.
opt.eve.off();
opt.any = null;
}
}, opt.wait || 9);
opt.at = opt.at || ctx; // opt.at will always be the first context it finds.
opt.key = opt.key || key;
opt.eve.s[this._.id] = eve; // collect all the events together.
if(true === Gun.valid(data)){ // if primitive value...
if(!at){
opt.doc = data;
} else {
at[key] = data;
}
return;
}
var tmp = this; // else if a sub-object, CPU schedule loop over properties to do recursion.
setTimeout.each(Object.keys(data), function(key, val){
if('_' === key && !opt.meta){ return }
val = data[key];
var doc = at || opt.doc, id; // first pass this becomes the root of open, then at is passed below, and will be the parent for each sub-document/object.
if(!doc){ return } // if no "parent"
if('string' !== typeof (id = Gun.valid(val))){ // if primitive...
doc[key] = val;
return;
}
if(opt.ids[id]){ // if we've already seen this sub-object/document
doc[key] = opt.ids[id]; // link to itself, our already in-memory one, not a new copy.
return;
}
if(opt.depth <= depth){ // stop recursive open at max depth.
doc[key] = doc[key] || val; // show link so app can load it if need.
return;
} // now open up the recursion of sub-documents!
tmp.get(key).open(opt.any, opt, opt.ids[id] = doc[key] = {}, depth+1); // 3rd param is now where we are "at".
});
})
}

View File

@@ -1,31 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.chain.path = function(field, opt){
var back = this, gun = back, tmp;
if(typeof field === 'string'){
tmp = field.split(opt || '.');
if(1 === tmp.length){
gun = back.get(field);
return gun;
}
field = tmp;
}
if(field instanceof Array){
if(field.length > 1){
gun = back;
var i = 0, l = field.length;
for(i; i < l; i++){
//gun = gun.get(field[i], (i+1 === l)? cb : null, opt);
gun = gun.get(field[i]);
}
} else {
gun = back.get(field[0]);
}
return gun;
}
if(!field && 0 != field){
return back;
}
gun = back.get(''+field);
return gun;
}

7
assets/static/pouchdb.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
assets/static/printer2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2
assets/static/qrcode/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,606 +0,0 @@
;(function(){
function Radisk(opt){
opt = opt || {};
opt.log = opt.log || console.log;
opt.file = String(opt.file || 'radata');
var has = (Radisk.has || (Radisk.has = {}))[opt.file];
if(has){ return has }
opt.max = opt.max || (opt.memory? (opt.memory * 999 * 999) : 300000000) * 0.3;
opt.until = opt.until || opt.wait || 250;
opt.batch = opt.batch || (10 * 1000);
opt.chunk = opt.chunk || (1024 * 1024 * 1); // 1MB
opt.code = opt.code || {};
opt.code.from = opt.code.from || '!';
opt.jsonify = true;
function ename(t){ return encodeURIComponent(t).replace(/\*/g, '%2A') } // TODO: Hash this also, but allow migration!
function atomic(v){ return u !== v && (!v || 'object' != typeof v) }
var timediate = (''+u === typeof setImmediate)? setTimeout : setImmediate;
var puff = setTimeout.turn || timediate, u;
var map = Radix.object;
var ST = 0;
if(!opt.store){
return opt.log("ERROR: Radisk needs `opt.store` interface with `{get: fn, put: fn (, list: fn)}`!");
}
if(!opt.store.put){
return opt.log("ERROR: Radisk needs `store.put` interface with `(file, data, cb)`!");
}
if(!opt.store.get){
return opt.log("ERROR: Radisk needs `store.get` interface with `(file, cb)`!");
}
if(!opt.store.list){
//opt.log("WARNING: `store.list` interface might be needed!");
}
if(''+u != typeof require){ require('./yson') }
var parse = JSON.parseAsync || function(t,cb,r){ var u; try{ cb(u, JSON.parse(t,r)) }catch(e){ cb(e) } }
var json = JSON.stringifyAsync || function(v,cb,r,s){ var u; try{ cb(u, JSON.stringify(v,r,s)) }catch(e){ cb(e) } }
/*
Any and all storage adapters should...
1. Because writing to disk takes time, we should batch data to disk. This improves performance, and reduces potential disk corruption.
2. If a batch exceeds a certain number of writes, we should immediately write to disk when physically possible. This caps total performance, but reduces potential loss.
*/
var r = function(key, data, cb, tag, DBG){
if('function' === typeof data){
var o = cb || {};
cb = data;
r.read(key, cb, o, DBG || tag);
return;
}
//var tmp = (tmp = r.batch = r.batch || {})[key] = tmp[key] || {};
//var tmp = (tmp = r.batch = r.batch || {})[key] = data;
r.save(key, data, cb, tag, DBG);
}
r.save = function(key, data, cb, tag, DBG){
var s = {key: key}, tags, f, d, q;
s.find = function(file){ var tmp;
s.file = file || (file = opt.code.from);
DBG && (DBG = DBG[file] = DBG[file] || {});
DBG && (DBG.sf = DBG.sf || +new Date);
//console.only.i && console.log('found', file);
if(tmp = r.disk[file]){ s.mix(u, tmp); return }
r.parse(file, s.mix, u, DBG);
}
s.mix = function(err, disk){
DBG && (DBG.sml = +new Date);
DBG && (DBG.sm = DBG.sm || +new Date);
if(s.err = err || s.err){ cb(err); return } // TODO: HANDLE BATCH EMIT
var file = s.file = (disk||'').file || s.file, tmp;
if(!disk && file !== opt.code.from){ // corrupt file?
r.find.bad(file); // remove from dir list
r.save(key, data, cb, tag); // try again
return;
}
(disk = r.disk[file] || (r.disk[file] = disk || Radix())).file || (disk.file = file);
if(opt.compare){
data = opt.compare(disk(key), data, key, file);
if(u === data){ cb(err, -1); return } // TODO: HANDLE BATCH EMIT
}
(s.disk = disk)(key, data);
if(tag){
(tmp = (tmp = disk.tags || (disk.tags = {}))[tag] || (tmp[tag] = r.tags[tag] || (r.tags[tag] = {})))[file] || (tmp[file] = r.one[tag] || (r.one[tag] = cb));
cb = null;
}
DBG && (DBG.st = DBG.st || +new Date);
//console.only.i && console.log('mix', disk.Q);
if(disk.Q){ cb && disk.Q.push(cb); return } disk.Q = (cb? [cb] : []);
disk.to = setTimeout(s.write, opt.until);
}
s.write = function(){
DBG && (DBG.sto = DBG.sto || +new Date);
var file = f = s.file, disk = d = s.disk;
q = s.q = disk.Q;
tags = s.tags = disk.tags;
delete disk.Q;
delete r.disk[file];
delete disk.tags;
//console.only.i && console.log('write', file, disk, 'was saving:', key, data);
r.write(file, disk, s.ack, u, DBG);
}
s.ack = function(err, ok){
DBG && (DBG.sa = DBG.sa || +new Date);
DBG && (DBG.sal = q.length);
var ack, tmp;
// TODO!!!! CHANGE THIS INTO PUFF!!!!!!!!!!!!!!!!
for(var id in r.tags){
if(!r.tags.hasOwnProperty(id)){ continue } var tag = r.tags[id];
if((tmp = r.disk[f]) && (tmp = tmp.tags) && tmp[tag]){ continue }
ack = tag[f];
delete tag[f];
var ne; for(var k in tag){ if(tag.hasOwnProperty(k)){ ne = true; break } } // is not empty?
if(ne){ continue } //if(!obj_empty(tag)){ continue }
delete r.tags[tag];
ack && ack(err, ok);
}
!q && (q = '');
var l = q.length, i = 0;
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
var S = +new Date;
for(;i < l; i++){ (ack = q[i]) && ack(err, ok) }
console.STAT && console.STAT(S, +new Date - S, 'rad acks', ename(s.file));
console.STAT && console.STAT(S, q.length, 'rad acks #', ename(s.file));
}
cb || (cb = function(err, ok){ // test delete!
if(!err){ return }
});
//console.only.i && console.log('save', key);
r.find(key, s.find);
}
r.disk = {};
r.one = {};
r.tags = {};
/*
Any storage engine at some point will have to do a read in order to write.
This is true of even systems that use an append only log, if they support updates.
Therefore it is unavoidable that a read will have to happen,
the question is just how long you delay it.
*/
var RWC = 0;
r.write = function(file, rad, cb, o, DBG){
if(!rad){ cb('No radix!'); return }
o = ('object' == typeof o)? o : {force: o};
var f = function Fractal(){}, a, b;
f.text = '';
f.file = file = rad.file || (rad.file = file);
if(!file){ cb('What file?'); return }
f.write = function(){
var text = rad.raw = f.text;
r.disk[file = rad.file || f.file || file] = rad;
var S = +new Date;
DBG && (DBG.wd = S);
//console.only.i && console.log('add', file);
r.find.add(file, function add(err){
DBG && (DBG.wa = +new Date);
if(err){ cb(err); return }
//console.only.i && console.log('disk', file, text);
opt.store.put(ename(file), text, function safe(err, ok){
DBG && (DBG.wp = +new Date);
console.STAT && console.STAT(S, ST = +new Date - S, "wrote disk", JSON.stringify(file), ++RWC, 'total all writes.');
//console.only.i && console.log('done', err, ok || 1, cb);
cb(err, ok || 1);
if(!rad.Q){ delete r.disk[file] } // VERY IMPORTANT! Clean up memory, but not if there is already queued writes on it!
});
});
}
f.split = function(){
var S = +new Date;
DBG && (DBG.wf = S);
f.text = '';
if(!f.count){ f.count = 0;
Radix.map(rad, function count(){ f.count++ }); // TODO: Perf? Any faster way to get total length?
}
DBG && (DBG.wfc = f.count);
f.limit = Math.ceil(f.count/2);
var SC = f.count;
f.count = 0;
DBG && (DBG.wf1 = +new Date);
f.sub = Radix();
Radix.map(rad, f.slice, {reverse: 1}); // IMPORTANT: DO THIS IN REVERSE, SO LAST HALF OF DATA MOVED TO NEW FILE BEFORE DROPPING FROM CURRENT FILE.
DBG && (DBG.wf2 = +new Date);
r.write(f.end, f.sub, f.both, o);
DBG && (DBG.wf3 = +new Date);
f.hub = Radix();
Radix.map(rad, f.stop);
DBG && (DBG.wf4 = +new Date);
r.write(rad.file, f.hub, f.both, o);
DBG && (DBG.wf5 = +new Date);
console.STAT && console.STAT(S, +new Date - S, "rad split", ename(rad.file), SC);
return true;
}
f.slice = function(val, key){
f.sub(f.end = key, val);
if(f.limit <= (++f.count)){ return true }
}
f.stop = function(val, key){
if(key >= f.end){ return true }
f.hub(key, val);
}
f.both = function(err, ok){
DBG && (DBG.wfd = +new Date);
if(b){ cb(err || b); return }
if(a){ cb(err, ok); return }
a = true;
b = err;
}
f.each = function(val, key, k, pre){
if(u !== val){ f.count++ }
if(opt.max <= (val||'').length){ return cb("Data too big!"), true }
var enc = Radisk.encode(pre.length) +'#'+ Radisk.encode(k) + (u === val? '' : ':'+ Radisk.encode(val)) +'\n';
if((opt.chunk < f.text.length + enc.length) && (1 < f.count) && !o.force){
return f.split();
}
f.text += enc;
}
//console.only.i && console.log('writing');
if(opt.jsonify){ r.write.jsonify(f, rad, cb, o, DBG); return } // temporary testing idea
if(!Radix.map(rad, f.each, true)){ f.write() }
}
r.write.jsonify = function(f, rad, cb, o, DBG){
var raw;
var S = +new Date;
DBG && (DBG.w = S);
try{raw = JSON.stringify(rad.$);
}catch(e){ cb("Cannot radisk!"); return }
DBG && (DBG.ws = +new Date);
console.STAT && console.STAT(S, +new Date - S, "rad stringified JSON");
if(opt.chunk < raw.length && !o.force){
var c = 0;
Radix.map(rad, function(){
if(c++){ return true } // more than 1 item
});
if(c > 1){
return f.split();
}
}
f.text = raw;
f.write();
}
r.range = function(tree, o){
if(!tree || !o){ return }
if(u === o.start && u === o.end){ return tree }
if(atomic(tree)){ return tree }
var sub = Radix();
Radix.map(tree, function(v,k){ sub(k,v) }, o); // ONLY PLACE THAT TAKES TREE, maybe reduce API for better perf?
return sub('');
}
;(function(){
r.read = function(key, cb, o, DBG){
o = o || {};
var g = {key: key};
g.find = function(file){ var tmp;
g.file = file || (file = opt.code.from);
DBG && (DBG = DBG[file] = DBG[file] || {});
DBG && (DBG.rf = DBG.rf || +new Date);
if(tmp = r.disk[g.file = file]){ g.check(u, tmp); return }
r.parse(file, g.check, u, DBG);
}
g.get = function(err, disk, info){
DBG && (DBG.rgl = +new Date);
DBG && (DBG.rg = DBG.rg || +new Date);
if(g.err = err || g.err){ cb(err); return }
var file = g.file = (disk||'').file || g.file;
if(!disk && file !== opt.code.from){ // corrupt file?
r.find.bad(file); // remove from dir list
r.read(key, cb, o); // try again
return;
}
disk = r.disk[file] || (r.disk[file] = disk);
if(!disk){ cb(file === opt.code.from? u : "No file!"); return }
disk.file || (disk.file = file);
var data = r.range(disk(key), o);
DBG && (DBG.rr = +new Date);
o.unit = disk.unit;
o.chunks = (o.chunks || 0) + 1;
o.parsed = (o.parsed || 0) + ((info||'').parsed||(o.chunks*opt.chunk));
o.more = 1;
o.next = u;
Radix.map(r.list, function next(v,f){
if(!v || file === f){ return }
o.next = f;
return 1;
}, o.reverse? {reverse: 1, end: file} : {start: file});
DBG && (DBG.rl = +new Date);
if(!o.next){ o.more = 0 }
if(o.next){
if(!o.reverse && ((key < o.next && 0 != o.next.indexOf(key)) || (u !== o.end && (o.end || '\uffff') < o.next))){ o.more = 0 }
if(o.reverse && ((key > o.next && 0 != key.indexOf(o.next)) || ((u !== o.start && (o.start || '') > o.next && file <= o.start)))){ o.more = 0 }
}
//console.log(5, process.memoryUsage().heapUsed);
if(!o.more){ cb(g.err, data, o); return }
if(data){ cb(g.err, data, o) }
if(o.parsed >= o.limit){ return }
var S = +new Date;
DBG && (DBG.rm = S);
var next = o.next;
timediate(function(){
console.STAT && console.STAT(S, +new Date - S, 'rad more');
r.parse(next, g.check);
},0);
}
g.check = function(err, disk, info){
//console.log(4, process.memoryUsage().heapUsed);
g.get(err, disk, info);
if(!disk || disk.check){ return } disk.check = 1;
var S = +new Date;
(info || (info = {})).file || (info.file = g.file);
Radix.map(disk, function(val, key){
// assume in memory for now, since both write/read already call r.find which will init it.
r.find(key, function(file){
if((file || (file = opt.code.from)) === info.file){ return }
var id = (''+Math.random()).slice(-3);
puff(function(){
r.save(key, val, function ack(err, ok){
if(err){ r.save(key, val, ack); return } // ad infinitum???
// TODO: NOTE!!! Mislocated data could be because of a synchronous `put` from the `g.get(` other than perf shouldn't we do the check first before acking?
console.STAT && console.STAT("MISLOCATED DATA CORRECTED", id, ename(key), ename(info.file), ename(file));
});
},0);
})
});
console.STAT && console.STAT(S, +new Date - S, "rad check");
}
r.find(key || (o.reverse? (o.end||'') : (o.start||'')), g.find);
}
function rev(a,b){ return b }
var revo = {reverse: true};
}());
;(function(){
/*
Let us start by assuming we are the only process that is
changing the directory or bucket. Not because we do not want
to be multi-process/machine, but because we want to experiment
with how much performance and scale we can get out of only one.
Then we can work on the harder problem of being multi-process.
*/
var RPC = 0;
var Q = {}, s = String.fromCharCode(31);
r.parse = function(file, cb, raw, DBG){ var q;
if(!file){ return cb(); }
if(q = Q[file]){ q.push(cb); return } q = Q[file] = [cb];
var p = function Parse(){}, info = {file: file};
(p.disk = Radix()).file = file;
p.read = function(err, data){ var tmp;
DBG && (DBG.rpg = +new Date);
console.STAT && console.STAT(S, +new Date - S, 'read disk', JSON.stringify(file), ++RPC, 'total all parses.');
//console.log(2, process.memoryUsage().heapUsed);
if((p.err = err) || (p.not = !data)){
delete Q[file];
p.map(q, p.ack);
return;
}
if('string' !== typeof data){
try{
if(opt.max <= data.length){
p.err = "Chunk too big!";
} else {
data = data.toString(); // If it crashes, it crashes here. How!?? We check size first!
}
}catch(e){ p.err = e }
if(p.err){
delete Q[file];
p.map(q, p.ack);
return;
}
}
info.parsed = data.length;
DBG && (DBG.rpl = info.parsed);
DBG && (DBG.rpa = q.length);
S = +new Date;
if(!(opt.jsonify || '{' === data[0])){
p.radec(err, data);
return;
}
parse(data, function(err, tree){
//console.log(3, process.memoryUsage().heapUsed);
if(!err){
delete Q[file];
p.disk.$ = tree;
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'rad parsed JSON');
DBG && (DBG.rpd = +new Date);
p.map(q, p.ack); // hmmm, v8 profiler can't see into this cause of try/catch?
return;
}
if('{' === data[0]){
delete Q[file];
p.err = tmp || "JSON error!";
p.map(q, p.ack);
return;
}
p.radec(err, data);
});
}
p.map = function(){ // switch to setTimeout.each now?
if(!q || !q.length){ return }
//var i = 0, l = q.length, ack;
var S = +new Date;
var err = p.err, data = p.not? u : p.disk;
var i = 0, ack; while(i < 9 && (ack = q[i++])){ ack(err, data, info) } // too much?
console.STAT && console.STAT(S, +new Date - S, 'rad packs', ename(file));
console.STAT && console.STAT(S, i, 'rad packs #', ename(file));
if(!(q = q.slice(i)).length){ return }
puff(p.map, 0);
}
p.ack = function(cb){
if(!cb){ return }
if(p.err || p.not){
cb(p.err, u, info);
return;
}
cb(u, p.disk, info);
}
p.radec = function(err, data){
delete Q[file];
S = +new Date;
var tmp = p.split(data), pre = [], i, k, v;
if(!tmp || 0 !== tmp[1]){
p.err = "File '"+file+"' does not have root radix! ";
p.map(q, p.ack);
return;
}
while(tmp){
k = v = u;
i = tmp[1];
tmp = p.split(tmp[2])||'';
if('#' == tmp[0]){
k = tmp[1];
pre = pre.slice(0,i);
if(i <= pre.length){
pre.push(k);
}
}
tmp = p.split(tmp[2])||'';
if('\n' == tmp[0]){ continue }
if('=' == tmp[0] || ':' == tmp[0]){ v = tmp[1] }
if(u !== k && u !== v){ p.disk(pre.join(''), v) }
tmp = p.split(tmp[2]);
}
console.STAT && console.STAT(S, +new Date - S, 'parsed RAD');
p.map(q, p.ack);
};
p.split = function(t){
if(!t){ return }
var l = [], o = {}, i = -1, a = '', b, c;
i = t.indexOf(s);
if(!t[i]){ return }
a = t.slice(0, i);
l[0] = a;
l[1] = b = Radisk.decode(t.slice(i), o);
l[2] = t.slice(i + o.i);
return l;
}
if(r.disk){ raw || (raw = (r.disk[file]||'').raw) }
var S = +new Date, SM, SL;
DBG && (DBG.rp = S);
if(raw){ return puff(function(){ p.read(u, raw) }, 0) }
opt.store.get(ename(file), p.read);
// TODO: What if memory disk gets filled with updates, and we get an old one back?
}
}());
;(function(){
var dir, f = String.fromCharCode(28), Q;
r.find = function(key, cb){
if(!dir){
if(Q){ Q.push([key, cb]); return } Q = [[key, cb]];
r.parse(f, init);
return;
}
Radix.map(r.list = dir, function(val, key){
if(!val){ return }
return cb(key) || true;
}, {reverse: 1, end: key}) || cb(opt.code.from);
}
r.find.add = function(file, cb){
var has = dir(file);
if(has || file === f){ cb(u, 1); return }
dir(file, 1);
cb.found = (cb.found || 0) + 1;
r.write(f, dir, function(err, ok){
if(err){ cb(err); return }
cb.found = (cb.found || 0) - 1;
if(0 !== cb.found){ return }
cb(u, 1);
}, true);
}
r.find.bad = function(file, cb){
dir(file, 0);
r.write(f, dir, cb||noop);
}
function init(err, disk){
if(err){
opt.log('list', err);
setTimeout(function(){ r.parse(f, init) }, 1000);
return;
}
if(disk){ drain(disk); return }
dir = dir || disk || Radix();
if(!opt.store.list){ drain(dir); return }
// import directory.
opt.store.list(function(file){
if(!file){ drain(dir); return }
r.find.add(file, noop);
});
}
function drain(rad, tmp){
dir = dir || rad;
dir.file = f;
tmp = Q; Q = null;
map(tmp, function(arg){
r.find(arg[0], arg[1]);
});
}
}());
try{ !Gun.window && require('./radmigtmp')(r) }catch(e){}
var noop = function(){}, RAD, u;
Radisk.has[opt.file] = r;
return r;
}
;(function(){
var _ = String.fromCharCode(31), u;
Radisk.encode = function(d, o, s){ s = s || _;
var t = s, tmp;
if(typeof d == 'string'){
var i = d.indexOf(s);
while(i != -1){ t += s; i = d.indexOf(s, i+1) }
return t + '"' + d + s;
} else
if(d && d['#'] && 1 == Object.keys(d).length){
return t + '#' + tmp + t;
} else
if('number' == typeof d){
return t + '+' + (d||0) + t;
} else
if(null === d){
return t + ' ' + t;
} else
if(true === d){
return t + '+' + t;
} else
if(false === d){
return t + '-' + t;
}// else
//if(binary){}
}
Radisk.decode = function(t, o, s){ s = s || _;
var d = '', i = -1, n = 0, c, p;
if(s !== t[0]){ return }
while(s === t[++i]){ ++n }
p = t[c = n] || true;
while(--n >= 0){ i = t.indexOf(s, i+1) }
if(i == -1){ i = t.length }
d = t.slice(c+1, i);
if(o){ o.i = i+1 }
if('"' === p){
return d;
} else
if('#' === p){
return {'#':d};
} else
if('+' === p){
if(0 === d.length){
return true;
}
return parseFloat(d);
} else
if(' ' === p){
return null;
} else
if('-' === p){
return false;
}
}
}());
if(typeof window !== "undefined"){
var Gun = window.Gun;
var Radix = window.Radix;
window.Radisk = Radisk;
} else {
var Gun = require('../gun');
var Radix = require('./radix');
//var Radix = require('./radix2'); Radisk = require('./radisk2');
try{ module.exports = Radisk }catch(e){}
}
Radisk.Radix = Radix;
}());

View File

@@ -1,124 +0,0 @@
;(function(){
function Radix(){
var radix = function(key, val, t){
radix.unit = 0;
if(!t && u !== val){
radix.last = (''+key < radix.last)? radix.last : ''+key;
delete (radix.$||{})[_];
}
t = t || radix.$ || (radix.$ = {});
if(!key && Object.keys(t).length){ return t }
key = ''+key;
var i = 0, l = key.length-1, k = key[i], at, tmp;
while(!(at = t[k]) && i < l){
k += key[++i];
}
if(!at){
if(!each(t, function(r, s){
var ii = 0, kk = '';
if((s||'').length){ while(s[ii] == key[ii]){
kk += s[ii++];
} }
if(kk){
if(u === val){
if(ii <= l){ return }
(tmp || (tmp = {}))[s.slice(ii)] = r;
//(tmp[_] = function $(){ $.sort = Object.keys(tmp).sort(); return $ }()); // get rid of this one, cause it is on read?
return r;
}
var __ = {};
__[s.slice(ii)] = r;
ii = key.slice(ii);
('' === ii)? (__[''] = val) : ((__[ii] = {})[''] = val);
//(__[_] = function $(){ $.sort = Object.keys(__).sort(); return $ }());
t[kk] = __;
if(Radix.debug && 'undefined' === ''+kk){ console.log(0, kk); debugger }
delete t[s];
//(t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }());
return true;
}
})){
if(u === val){ return; }
(t[k] || (t[k] = {}))[''] = val;
if(Radix.debug && 'undefined' === ''+k){ console.log(1, k); debugger }
//(t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }());
}
if(u === val){
return tmp;
}
} else
if(i == l){
//if(u === val){ return (u === (tmp = at['']))? at : tmp } // THIS CODE IS CORRECT, below is
if(u === val){ return (u === (tmp = at['']))? at : ((radix.unit = 1) && tmp) } // temporary help??
at[''] = val;
//(at[_] = function $(){ $.sort = Object.keys(at).sort(); return $ }());
} else {
if(u !== val){ delete at[_] }
//at && (at[_] = function $(){ $.sort = Object.keys(at).sort(); return $ }());
return radix(key.slice(++i), val, at || (at = {}));
}
}
return radix;
};
Radix.map = function rap(radix, cb, opt, pre){
try {
pre = pre || []; // TODO: BUG: most out-of-memory crashes come from here.
var t = ('function' == typeof radix)? radix.$ || {} : radix;
//!opt && console.log("WHAT IS T?", JSON.stringify(t).length);
if(!t){ return }
if('string' == typeof t){ if(Radix.debug){ throw ['BUG:', radix, cb, opt, pre] } return; }
var keys = (t[_]||no).sort || (t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }()).sort, rev; // ONLY 17% of ops are pre-sorted!
//var keys = Object.keys(t).sort();
opt = (true === opt)? {branch: true} : (opt || {});
if(rev = opt.reverse){ keys = keys.slice(0).reverse() }
var start = opt.start, end = opt.end, END = '\uffff';
var i = 0, l = keys.length;
for(;i < l; i++){ var key = keys[i], tree = t[key], tmp, p, pt;
if(!tree || '' === key || _ === key || 'undefined' === key){ continue }
p = pre.slice(0); p.push(key);
pt = p.join('');
if(u !== start && pt < (start||'').slice(0,pt.length)){ continue }
if(u !== end && (end || END) < pt){ continue }
if(rev){ // children must be checked first when going in reverse.
tmp = rap(tree, cb, opt, p);
if(u !== tmp){ return tmp }
}
if(u !== (tmp = tree[''])){
var yes = 1;
if(u !== start && pt < (start||'')){ yes = 0 }
if(u !== end && pt > (end || END)){ yes = 0 }
if(yes){
tmp = cb(tmp, pt, key, pre);
if(u !== tmp){ return tmp }
}
} else
if(opt.branch){
tmp = cb(u, pt, key, pre);
if(u !== tmp){ return tmp }
}
pre = p;
if(!rev){
tmp = rap(tree, cb, opt, pre);
if(u !== tmp){ return tmp }
}
pre.pop();
}
} catch (e) { console.error(e); }
};
if(typeof window !== "undefined"){
window.Radix = Radix;
} else {
try{ module.exports = Radix }catch(e){}
}
var each = Radix.object = function(o, f, r){
for(var k in o){
if(!o.hasOwnProperty(k)){ continue }
if((r = f(o[k], k)) !== u){ return r }
}
}, no = {}, u;
var _ = String.fromCharCode(24);
}());

View File

@@ -1,79 +0,0 @@
;(function(){
/* // from @jabis
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
*/
function Store(opt){
opt = opt || {};
opt.file = String(opt.file || 'radata');
var store = Store[opt.file], db = null, u;
if(store){
console.log("Warning: reusing same IndexedDB store and options as 1st.");
return Store[opt.file];
}
store = Store[opt.file] = function(){};
try{opt.indexedDB = opt.indexedDB || Store.indexedDB || indexedDB}catch(e){}
try{if(!opt.indexedDB || 'file:' == location.protocol){
var s = store.d || (store.d = {});
store.put = function(f, d, cb){ s[f] = d; setTimeout(function(){ cb(null, 1) },250) };
store.get = function(f, cb){ setTimeout(function(){ cb(null, s[f] || u) },5) };
console.log('Warning: No indexedDB exists to persist data to!');
return store;
}}catch(e){}
store.start = function(){
var o = indexedDB.open(opt.file, 1);
o.onupgradeneeded = function(eve){ (eve.target.result).createObjectStore(opt.file) }
o.onsuccess = function(){ db = o.result }
o.onerror = function(eve){ console.log(eve||1); }
}; store.start();
store.put = function(key, data, cb){
if(!db){ setTimeout(function(){ store.put(key, data, cb) },1); return }
var tx = db.transaction([opt.file], 'readwrite');
var obj = tx.objectStore(opt.file);
var req = obj.put(data, ''+key);
req.onsuccess = obj.onsuccess = tx.onsuccess = function(){ cb(null, 1) }
req.onabort = obj.onabort = tx.onabort = function(eve){ cb(eve||'put.tx.abort') }
req.onerror = obj.onerror = tx.onerror = function(eve){ cb(eve||'put.tx.error') }
}
store.get = function(key, cb){
if(!db){ setTimeout(function(){ store.get(key, cb) },9); return }
var tx = db.transaction([opt.file], 'readonly');
var obj = tx.objectStore(opt.file);
var req = obj.get(''+key);
req.onsuccess = function(){ cb(null, req.result) }
req.onabort = function(eve){ cb(eve||4) }
req.onerror = function(eve){ cb(eve||5) }
}
setInterval(function(){ db && db.close(); db = null; store.start() }, 1000 * 15); // reset webkit bug?
return store;
}
if(typeof window !== "undefined"){
(Store.window = window).RindexedDB = Store;
Store.indexedDB = window.indexedDB; // safari bug
} else {
try{ module.exports = Store }catch(e){}
}
try{
var Gun = Store.window.Gun || require('../gun');
Gun.on('create', function(root){
this.to.next(root);
root.opt.store = root.opt.store || Store(root.opt);
});
}catch(e){}
}());

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,150 +0,0 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
Gun.on('create', function(root){
if(Gun.TESTING){ root.opt.file = 'radatatest' }
this.to.next(root);
var opt = root.opt, empty = {}, u;
if(false === opt.rad || false === opt.radisk){ return }
if((u+'' != typeof process) && 'false' === ''+(process.env||'').RAD){ return }
var Radisk = (Gun.window && Gun.window.Radisk) || require('./radisk');
var Radix = Radisk.Radix;
var dare = Radisk(opt), esc = String.fromCharCode(27);
var ST = 0;
root.on('put', function(msg){
this.to.next(msg);
if((msg._||'').rad){ return } // don't save what just came from a read.
//if(msg['@']){ return } // WHY DID I NOT ADD THIS?
var id = msg['#'], put = msg.put, soul = put['#'], key = put['.'], val = put[':'], state = put['>'], tmp;
var DBG = (msg._||'').DBG; DBG && (DBG.sp = DBG.sp || +new Date);
//var lot = (msg._||'').lot||''; count[id] = (count[id] || 0) + 1;
var S = (msg._||'').RPS || ((msg._||'').RPS = +new Date);
//console.log("PUT ------->>>", soul,key, val, state);
//dare(soul+esc+key, {':': val, '>': state}, dare.one[id] || function(err, ok){
dare(soul+esc+key, {':': val, '>': state}, function(err, ok){
//console.log("<<<------- PAT", soul,key, val, state, 'in', +new Date - S);
DBG && (DBG.spd = DBG.spd || +new Date);
console.STAT && console.STAT(S, +new Date - S, 'put');
//if(!err && count[id] !== lot.s){ console.log(err = "Disk count not same as ram count."); console.STAT && console.STAT(+new Date, lot.s - count[id], 'put ack != count') } delete count[id];
if(err){ root.on('in', {'@': id, err: err, DBG: DBG}); return }
root.on('in', {'@': id, ok: ok, DBG: DBG});
//}, id, DBG && (DBG.r = DBG.r || {}));
}, false && id, DBG && (DBG.r = DBG.r || {}));
DBG && (DBG.sps = DBG.sps || +new Date);
});
var count = {}, obj_empty = Object.empty;
root.on('get', function(msg){
this.to.next(msg);
var ctx = msg._||'', DBG = ctx.DBG = msg.DBG; DBG && (DBG.sg = +new Date);
var id = msg['#'], get = msg.get, soul = msg.get['#'], has = msg.get['.']||'', o = {}, graph, lex, key, tmp, force;
if('string' == typeof soul){
key = soul;
} else
if(soul){
if(u !== (tmp = soul['*'])){ o.limit = force = 1 }
if(u !== soul['>']){ o.start = soul['>'] }
if(u !== soul['<']){ o.end = soul['<'] }
key = force? (''+tmp) : tmp || soul['='];
force = null;
}
if(key && !o.limit){ // a soul.has must be on a soul, and not during soul*
if('string' == typeof has){
key = key+esc+(o.atom = has);
} else
if(has){
if(u !== has['>']){ o.start = has['>']; o.limit = 1 }
if(u !== has['<']){ o.end = has['<']; o.limit = 1 }
if(u !== (tmp = has['*'])){ o.limit = force = 1 }
if(key){ key = key+esc + (force? (''+(tmp||'')) : tmp || (o.atom = has['='] || '')) }
}
}
if((tmp = get['%']) || o.limit){
o.limit = (tmp <= (o.pack || (1000 * 100)))? tmp : 1;
}
if(has['-'] || (soul||{})['-'] || get['-']){ o.reverse = true }
if((tmp = (root.next||'')[soul]) && tmp.put){
if(o.atom){
tmp = (tmp.next||'')[o.atom] ;
if(tmp && tmp.root && tmp.root.graph && tmp.root.graph[soul] && tmp.root.graph[soul][o.atom]){ return }
} else
if(tmp && tmp.rad){ return }
}
var now = Gun.state();
var S = (+new Date), C = 0, SPT = 0; // STATS!
DBG && (DBG.sgm = S);
//var GID = String.random(3); console.log("GET ------->>>", GID, key, o, '?', get);
dare(key||'', function(err, data, info){
//console.log("<<<------- GOT", GID, +new Date - S, err, data);
DBG && (DBG.sgr = +new Date);
DBG && (DBG.sgi = info);
try{opt.store.stats.get.time[statg % 50] = (+new Date) - S; ++statg;
opt.store.stats.get.count++;
if(err){ opt.store.stats.get.err = err }
}catch(e){} // STATS!
//if(u === data && info.chunks > 1){ return } // if we already sent a chunk, ignore ending empty responses. // this causes tests to fail.
console.STAT && console.STAT(S, +new Date - S, 'got', JSON.stringify(key)); S = +new Date;
info = info || '';
var va, ve;
if(info.unit && data && u !== (va = data[':']) && u !== (ve = data['>'])){ // new format
var tmp = key.split(esc), so = tmp[0], ha = tmp[1];
(graph = graph || {})[so] = Gun.state.ify(graph[so], ha, ve, va, so);
root.$.get(so).get(ha)._.rad = now;
// REMEMBER TO ADD _rad TO NODE/SOUL QUERY!
} else
if(data){ // old code path
if(typeof data !== 'string'){
if(o.atom){
data = u;
} else {
Radix.map(data, each, o); // IS A RADIX TREE, NOT FUNCTION!
}
}
if(!graph && data){ each(data, '') }
// TODO: !has what about soul lookups?
if(!o.atom && !has & 'string' == typeof soul && !o.limit && !o.more){
root.$.get(soul)._.rad = now;
}
}
DBG && (DBG.sgp = +new Date);
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
// Or benchmark by reusing first start date.
if(console.STAT && (ST = +new Date - S) > 9){ console.STAT(S, ST, 'got prep time'); console.STAT(S, C, 'got prep #') } SPT += ST; C = 0; S = +new Date;
var faith = function(){}; faith.faith = true; faith.rad = get; // HNPERF: We're testing performance improvement by skipping going through security again, but this should be audited.
root.on('in', {'@': id, put: graph, '%': info.more? 1 : u, err: err? err : u, _: faith, DBG: DBG});
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'got emit', Object.keys(graph||{}).length);
graph = u; // each is outside our scope, we have to reset graph to nothing!
}, o, DBG && (DBG.r = DBG.r || {}));
DBG && (DBG.sgd = +new Date);
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'get call'); // TODO: Perf: this was half a second??????
function each(val, has, a,b){ // TODO: THIS CODE NEEDS TO BE FASTER!!!!
C++;
if(!val){ return }
has = (key+has).split(esc);
var soul = has.slice(0,1)[0];
has = has.slice(-1)[0];
if(o.limit && o.limit <= o.count){ return true }
var va, ve, so = soul, ha = has;
//if(u !== (va = val[':']) && u !== (ve = val['>'])){ // THIS HANDLES NEW CODE!
if('string' != typeof val){ // THIS HANDLES NEW CODE!
va = val[':']; ve = val['>'];
(graph = graph || {})[so] = Gun.state.ify(graph[so], ha, ve, va, so);
//root.$.get(so).get(ha)._.rad = now;
o.count = (o.count || 0) + ((va||'').length || 9);
return;
}
o.count = (o.count || 0) + val.length;
var tmp = val.lastIndexOf('>');
var state = Radisk.decode(val.slice(tmp+1), null, esc);
val = Radisk.decode(val.slice(0,tmp), null, esc);
(graph = graph || {})[soul] = Gun.state.ify(graph[soul], has, state, val, soul);
}
});
var val_is = Gun.valid;
(opt.store||{}).stats = {get:{time:{}, count:0}, put: {time:{}, count:0}}; // STATS!
var statg = 0, statp = 0; // STATS!
});

View File

@@ -1,58 +0,0 @@
(function (env) {
var Gun;
if(typeof module !== "undefined" && module.exports){ Gun = require('gun/gun') }
if(typeof window !== "undefined"){ Gun = window.Gun }
Gun.chain.sync = function (obj, opt, cb, o) {
var gun = this;
if (!Gun.obj.is(obj)) {
console.log('First param is not an object');
return gun;
}
if (Gun.bi.is(opt)) {
opt = {
meta: opt
};
}
if(Gun.fn.is(opt)){
cb = opt;
opt = null;
}
cb = cb || function(){};
opt = opt || {};
opt.ctx = opt.ctx || {};
gun.on(function (change, field) {
Gun.obj.map(change, function (val, field) {
if (!obj) {
return;
}
if (field === '_' || field === '#') {
if (opt.meta) {
obj[field] = val;
}
return;
}
if (Gun.obj.is(val)) {
var soul = Gun.val.rel.is(val);
if (opt.ctx[soul + field]) {
// don't re-subscribe.
return;
}
// unique subscribe!
opt.ctx[soul + field] = true;
this.path(field).sync(
obj[field] = (obj[field] || {}),
Gun.obj.copy(opt),
cb,
o || obj
);
return;
}
obj[field] = val;
}, this);
cb(o || obj);
});
return gun;
};
}());

View File

@@ -1,134 +0,0 @@
;(function(){
var GUN = (typeof window !== "undefined")? window.Gun : require('../gun');
GUN.on('opt', function(root){
this.to.next(root);
var opt = root.opt;
if(root.once){ return }
if(!GUN.Mesh){ return }
if(false === opt.RTCPeerConnection){ return }
var env;
if(typeof window !== "undefined"){ env = window }
if(typeof global !== "undefined"){ env = global }
env = env || {};
var rtcpc = opt.RTCPeerConnection || env.RTCPeerConnection || env.webkitRTCPeerConnection || env.mozRTCPeerConnection;
var rtcsd = opt.RTCSessionDescription || env.RTCSessionDescription || env.webkitRTCSessionDescription || env.mozRTCSessionDescription;
var rtcic = opt.RTCIceCandidate || env.RTCIceCandidate || env.webkitRTCIceCandidate || env.mozRTCIceCandidate;
if(!rtcpc || !rtcsd || !rtcic){ return }
opt.RTCPeerConnection = rtcpc;
opt.RTCSessionDescription = rtcsd;
opt.RTCIceCandidate = rtcic;
opt.rtc = opt.rtc || {'iceServers': [
{urls: 'stun:stun.l.google.com:19302'},
{urls: 'stun:stun.cloudflare.com:3478'}/*,
{urls: "stun:stun.sipgate.net:3478"},
{urls: "stun:stun.stunprotocol.org"},
{urls: "stun:stun.sipgate.net:10000"},
{urls: "stun:217.10.68.152:10000"},
{urls: 'stun:stun.services.mozilla.com'}*/
]};
// TODO: Select the most appropriate stuns.
// FIXME: Find the wire throwing ICE Failed
// The above change corrects at least firefox RTC Peer handler where it **throws** on over 6 ice servers, and updates url: to urls: removing deprecation warning
opt.rtc.dataChannel = opt.rtc.dataChannel || {ordered: false, maxRetransmits: 2};
opt.rtc.sdp = opt.rtc.sdp || {mandatory: {OfferToReceiveAudio: false, OfferToReceiveVideo: false}};
opt.rtc.max = opt.rtc.max || 55; // is this a magic number? // For Future WebRTC notes: Chrome 500 max limit, however 256 likely - FF "none", webtorrent does 55 per torrent.
opt.rtc.room = opt.rtc.room || GUN.window && (window.rtcRoom || location.hash.slice(1) || location.pathname.slice(1));
opt.announce = function(to){
opt.rtc.start = +new Date; // handle room logic:
root.$.get('/RTC/'+opt.rtc.room+'<?99').get('+').put(opt.pid, function(ack){
if(!ack.ok || !ack.ok.rtc){ return }
plan(ack);
}, {acks: opt.rtc.max}).on(function(last,key, msg){
if(last === opt.pid || opt.rtc.start > msg.put['>']){ return }
plan({'#': ''+msg['#'], ok: {rtc: {id: last}}});
});
};
var mesh = opt.mesh = opt.mesh || GUN.Mesh(root), wired = mesh.wire;
mesh.hear['rtc'] = plan;
mesh.wire = function(media){ try{ wired && wired(media);
if(!(media instanceof MediaStream)){ return }
(open.media = open.media||{})[media.id] = media;
for(var p in opt.peers){ p = opt.peers[p]||'';
p.addTrack && media.getTracks().forEach(track => {
p.addTrack(track, media);
});
p.createOffer && p.createOffer(function(offer){
p.setLocalDescription(offer);
mesh.say({'#': root.ask(plan), dam: 'rtc', ok: {rtc: {offer: offer, id: opt.pid}}}, p);
}, function(){}, opt.rtc.sdp);
}
} catch(e){console.log(e)} }
root.on('create', function(at){
this.to.next(at);
setTimeout(opt.announce, 1);
});
function plan(msg){
if(!msg.ok){ return }
var rtc = msg.ok.rtc, peer, tmp;
if(!rtc || !rtc.id || rtc.id === opt.pid){ return }
peer = open(msg, rtc);
if(tmp = rtc.candidate){
return peer.addIceCandidate(new opt.RTCIceCandidate(tmp));
}
if(tmp = rtc.answer){
tmp.sdp = tmp.sdp.replace(/\\r\\n/g, '\r\n');
return peer.setRemoteDescription(peer.remoteSet = new opt.RTCSessionDescription(tmp));
}
if(tmp = rtc.offer){
rtc.offer.sdp = rtc.offer.sdp.replace(/\\r\\n/g, '\r\n');
peer.setRemoteDescription(new opt.RTCSessionDescription(tmp));
return peer.createAnswer(function(answer){
peer.setLocalDescription(answer);
root.on('out', {'@': msg['#'], ok: {rtc: {answer: answer, id: opt.pid}}});
}, function(){}, opt.rtc.sdp);
}
}
function open(msg, rtc, peer){
if(peer = opt.peers[rtc.id] || open[rtc.id]){ return peer }
(peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id;
var wire = peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel);
function rtceve(eve){ eve.peer = peer; gun.on('rtc', eve) }
peer.$ = gun;
open[rtc.id] = peer;
peer.ontrack = rtceve;
peer.onremovetrack = rtceve;
peer.onconnectionstatechange = rtceve;
wire.to = setTimeout(function(){delete open[rtc.id]},1000*60);
wire.onclose = function(){ mesh.bye(peer) };
wire.onerror = function(err){ };
wire.onopen = function(e){
delete open[rtc.id];
mesh.hi(peer);
}
wire.onmessage = function(msg){
if(!msg){ return }
mesh.hear(msg.data || msg, peer);
};
peer.onicecandidate = function(e){ rtceve(e);
if(!e.candidate){ return }
root.on('out', {'@': (msg||'')['#'], '#': root.ask(plan), ok: {rtc: {candidate: e.candidate, id: opt.pid}}});
}
peer.ondatachannel = function(e){ rtceve(e);
var rc = e.channel;
rc.onmessage = wire.onmessage;
rc.onopen = wire.onopen;
rc.onclose = wire.onclose;
}
if(rtc.offer){ return peer }
for(var m in open.media){ m = open.media[m];
m.getTracks().forEach(track => {
peer.addTrack(track, m);
});
}
peer.createOffer(function(offer){
peer.setLocalDescription(offer);
root.on('out', {'@': (msg||'')['#'], '#': root.ask(plan), ok: {rtc: {offer: offer, id: opt.pid}}});
}, function(){}, opt.rtc.sdp);
return peer;
}
});
}());

View File

@@ -1,244 +0,0 @@
;(function(){
// JSON: JavaScript Object Notation
// YSON: Yielding javaScript Object Notation
var yson = {}, u, sI = setTimeout.turn || (typeof setImmediate != ''+u && setImmediate) || setTimeout;
yson.parseAsync = function(text, done, revive, M){
if('string' != typeof text){ try{ done(u,JSON.parse(text)) }catch(e){ done(e) } return }
var ctx = {i: 0, text: text, done: done, l: text.length, up: []};
//M = 1024 * 1024 * 100;
//M = M || 1024 * 64;
M = M || 1024 * 32;
parse();
function parse(){
//var S = +new Date;
var s = ctx.text;
var i = ctx.i, l = ctx.l, j = 0;
var w = ctx.w, b, tmp;
while(j++ < M){
var c = s[i++];
if(i > l){
ctx.end = true;
break;
}
if(w){
i = s.indexOf('"', i-1); c = s[i];
tmp = 0; while('\\' == s[i-(++tmp)]){}; tmp = !(tmp % 2);//tmp = ('\\' == s[i-1]); // json is stupid
b = b || tmp;
if('"' == c && !tmp){
w = u;
tmp = ctx.s;
if(ctx.a){
tmp = s.slice(ctx.sl, i);
if(b || (1+tmp.indexOf('\\'))){ tmp = JSON.parse('"'+tmp+'"') } // escape + unicode :( handling
if(ctx.at instanceof Array){
ctx.at.push(ctx.s = tmp);
} else {
if(!ctx.at){ ctx.end = j = M; tmp = u }
(ctx.at||{})[ctx.s] = ctx.s = tmp;
}
ctx.s = u;
} else {
ctx.s = s.slice(ctx.sl, i);
if(b || (1+ctx.s.indexOf('\\'))){ ctx.s = JSON.parse('"'+ctx.s+'"'); } // escape + unicode :( handling
}
ctx.a = b = u;
}
++i;
} else {
switch(c){
case '"':
ctx.sl = i;
w = true;
break;
case ':':
ctx.ai = i;
ctx.a = true;
break;
case ',':
if(ctx.a || ctx.at instanceof Array){
if(tmp = s.slice(ctx.ai, i-1)){
if(u !== (tmp = value(tmp))){
if(ctx.at instanceof Array){
ctx.at.push(tmp);
} else {
ctx.at[ctx.s] = tmp;
}
}
}
}
ctx.a = u;
if(ctx.at instanceof Array){
ctx.a = true;
ctx.ai = i;
}
break;
case '{':
ctx.up.push(ctx.at||(ctx.at = {}));
if(ctx.at instanceof Array){
ctx.at.push(ctx.at = {});
} else
if(u !== (tmp = ctx.s)){
ctx.at[tmp] = ctx.at = {};
}
ctx.a = u;
break;
case '}':
if(ctx.a){
if(tmp = s.slice(ctx.ai, i-1)){
if(u !== (tmp = value(tmp))){
if(ctx.at instanceof Array){
ctx.at.push(tmp);
} else {
if(!ctx.at){ ctx.end = j = M; tmp = u }
(ctx.at||{})[ctx.s] = tmp;
}
}
}
}
ctx.a = u;
ctx.at = ctx.up.pop();
break;
case '[':
if(u !== (tmp = ctx.s)){
ctx.up.push(ctx.at);
ctx.at[tmp] = ctx.at = [];
} else
if(!ctx.at){
ctx.up.push(ctx.at = []);
}
ctx.a = true;
ctx.ai = i;
break;
case ']':
if(ctx.a){
if(tmp = s.slice(ctx.ai, i-1)){
if(u !== (tmp = value(tmp))){
if(ctx.at instanceof Array){
ctx.at.push(tmp);
} else {
ctx.at[ctx.s] = tmp;
}
}
}
}
ctx.a = u;
ctx.at = ctx.up.pop();
break;
}
}
}
ctx.s = u;
ctx.i = i;
ctx.w = w;
if(ctx.end){
tmp = ctx.at;
if(u === tmp){
try{ tmp = JSON.parse(text)
}catch(e){ return ctx.done(e) }
}
ctx.done(u, tmp);
} else {
sI(parse);
}
}
}
function value(s){
var n = parseFloat(s);
if(!isNaN(n)){
return n;
}
s = s.trim();
if('true' == s){
return true;
}
if('false' == s){
return false;
}
if('null' == s){
return null;
}
}
yson.stringifyAsync = function(data, done, replacer, space, ctx){
//try{done(u, JSON.stringify(data, replacer, space))}catch(e){done(e)}return;
ctx = ctx || {};
ctx.text = ctx.text || "";
ctx.up = [ctx.at = {d: data}];
ctx.done = done;
ctx.i = 0;
var j = 0;
ify();
function ify(){
var at = ctx.at, data = at.d, add = '', tmp;
if(at.i && (at.i - at.j) > 0){ add += ',' }
if(u !== (tmp = at.k)){ add += JSON.stringify(tmp) + ':' } //'"'+tmp+'":' } // only if backslash
switch(typeof data){
case 'boolean':
add += ''+data;
break;
case 'string':
add += JSON.stringify(data); //ctx.text += '"'+data+'"';//JSON.stringify(data); // only if backslash
break;
case 'number':
add += (isNaN(data)? 'null' : data);
break;
case 'object':
if(!data){
add += 'null';
break;
}
if(data instanceof Array){
add += '[';
at = {i: -1, as: data, up: at, j: 0};
at.l = data.length;
ctx.up.push(ctx.at = at);
break;
}
if('function' != typeof (data||'').toJSON){
add += '{';
at = {i: -1, ok: Object.keys(data).sort(), as: data, up: at, j: 0};
at.l = at.ok.length;
ctx.up.push(ctx.at = at);
break;
}
if(tmp = data.toJSON()){
add += tmp;
break;
}
// let this & below pass into default case...
case 'function':
if(at.as instanceof Array){
add += 'null';
break;
}
default: // handle wrongly added leading `,` if previous item not JSON-able.
add = '';
at.j++;
}
ctx.text += add;
while(1+at.i >= at.l){
ctx.text += (at.ok? '}' : ']');
at = ctx.at = at.up;
}
if(++at.i < at.l){
if(tmp = at.ok){
at.d = at.as[at.k = tmp[at.i]];
} else {
at.d = at.as[at.i];
}
if(++j < 9){ return ify() } else { j = 0 }
sI(ify);
return;
}
ctx.done(u, ctx.text);
}
}
if(typeof window != ''+u){ window.YSON = yson }
try{ if(typeof module != ''+u){ module.exports = yson } }catch(e){}
if(typeof JSON != ''+u){
JSON.parseAsync = yson.parseAsync;
JSON.stringifyAsync = yson.stringifyAsync;
}
}());

View File

@@ -1,24 +1,50 @@
import json
import os
import shutil
import sys
import time
def get_all_files(directory):
files = []
for root, _, filenames in os.walk(directory):
for filename in filenames:
path = os.path.join(root, filename)
# Convert to relative path and normalize separators
rel_path = os.path.relpath(path, directory)
files.append(rel_path.replace('\\', '/'))
return files
PREFETCH = ""
VERSIONCO = "2025-07-30_3"
HANDLEPARSE = os.listdir("src/")
ASSETS = json.load(open("_assets.json", "r")) + HANDLEPARSE
VERSIONCO = "2026-02-23_" + time.strftime("%Y%m%d%H%M%S")
HANDLEPARSE = get_all_files("src")
TITLE = os.environ.get("TELESEC_TITLE", "TeleSec")
HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech")
# Combine assets from JSON and recursively found files
ASSETS = get_all_files("assets")
for asset in ASSETS:
if asset != "sw.js":
PREFETCH += f'<link rel="prefetch" href="{asset}" />\n'
for src in HANDLEPARSE:
if src != "sw.js":
PREFETCH += f'<link rel="prefetch" href="{src}" />\n'
if os.path.exists("dist"):
shutil.rmtree("dist")
shutil.copytree("assets","dist", dirs_exist_ok=True)
os.system("rm -r dist ; mkdir -p dist ; cp -r assets/* dist/")
def replace_handles(string):
string = string.replace("%%PREFETCH%%", PREFETCH)
string = string.replace("%%VERSIONCO%%", VERSIONCO)
string = string.replace("%%TITLE%%", TITLE)
string = string.replace("%%HOSTER%%", HOSTER)
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
return string
for file in HANDLEPARSE:
with open("src/" + file, "r") as f:
out = replace_handles(f.read())
with open("dist/" + file, "w") as f:
f.write(out)
print(file)
with open("src/" + file, "r", encoding="utf-8") as f1:
out = replace_handles(f1.read())
with open("dist/" + file, "w", encoding="utf-8") as f2:
f2.write(out)

175
decrypt.js Normal file
View File

@@ -0,0 +1,175 @@
function TS_decrypt(input, secret, callback, table, id) {
// Accept objects or plaintext strings. Also support legacy RSA{...} AES-encrypted entries.
var __ts_sync = true;
if (typeof input !== "string") {
try {
callback(input, false);
} catch (e) {
console.error(e);
}
return __ts_sync;
}
// Legacy encrypted format: RSA{...}
if (
input.startsWith("RSA{") &&
input.endsWith("}") &&
typeof CryptoJS !== "undefined"
) {
try {
var data = input.slice(4, -1);
var words = CryptoJS.AES.decrypt(data, secret);
var decryptedUtf8 = null;
try {
decryptedUtf8 = words.toString(CryptoJS.enc.Utf8);
} catch (utfErr) {
// Malformed UTF-8 — try Latin1 fallback
try {
decryptedUtf8 = words.toString(CryptoJS.enc.Latin1);
} catch (latinErr) {
console.warn(
"TS_decrypt: failed to decode decrypted bytes",
utfErr,
latinErr
);
try {
callback(input, false);
} catch (ee) {}
return;
}
}
var parsed = null;
try {
parsed = JSON.parse(decryptedUtf8);
} catch (pe) {
// If JSON parsing fails, the decrypted string may be raw Latin1 bytes.
// Try to convert Latin1 byte string -> UTF-8 and parse again.
try {
if (
typeof TextDecoder !== "undefined" &&
typeof decryptedUtf8 === "string"
) {
var bytes = new Uint8Array(decryptedUtf8.length);
for (var _i = 0; _i < decryptedUtf8.length; _i++)
bytes[_i] = decryptedUtf8.charCodeAt(_i) & 0xff;
var converted = new TextDecoder("utf-8").decode(bytes);
try {
parsed = JSON.parse(converted);
decryptedUtf8 = converted;
} catch (e2) {
parsed = decryptedUtf8;
}
} else {
parsed = decryptedUtf8;
}
} catch (convErr) {
parsed = decryptedUtf8;
}
}
try {
callback(parsed, true);
} catch (e) {
console.error(e);
}
// Migrate to plaintext in DB if table/id provided
if (table && id && window.DB && DB.put && typeof parsed !== "string") {
DB.put(table, id, parsed).catch(() => {});
}
return;
} catch (e) {
console.error("TS_decrypt: invalid encrypted payload", e);
try {
callback(input, false);
} catch (ee) {}
return;
}
}
if (input.startsWith("SEA{") && input.endsWith("}")) {
__ts_sync = false;
SEA.decrypt(input, secret, (decrypted) => {
try {
callback(decrypted, true);
} catch (e) {
console.error(e);
}
});
return __ts_sync;
}
// Try to parse JSON strings and migrate to object
try {
var parsed = JSON.parse(input);
try {
callback(parsed, false);
} catch (e) {
console.error(e);
}
if (table && id && window.DB && DB.put) {
DB.put(table, id, parsed).catch(() => {});
}
} catch (e) {
// Not JSON, return raw string
try {
callback(input, false);
} catch (err) {
console.error(err);
}
}
return __ts_sync;
}
function recursiveTSDecrypt(input, secret = "") {
// Skip null values (do not show on decrypted output)
if (input === null) return null;
if (typeof input === "string") {
let result = null;
let resolver = null;
const promise = new Promise((resolve) => {
resolver = resolve;
});
const sync = TS_decrypt(input, secret, (decrypted) => {
result = decrypted;
if (resolver) resolver(decrypted);
});
if (sync === false) return promise;
return result;
} else if (Array.isArray(input)) {
const mapped = input.map((item) => recursiveTSDecrypt(item, secret));
if (mapped.some((v) => v && typeof v.then === "function")) {
return Promise.all(mapped).then((values) => values.filter((v) => v !== null && typeof v !== 'undefined'));
}
return mapped.filter((v) => v !== null && typeof v !== 'undefined');
} else if (typeof input === "object" && input !== null) {
const keys = Object.keys(input);
const mapped = keys.map((k) => recursiveTSDecrypt(input[k], secret));
if (mapped.some((v) => v && typeof v.then === "function")) {
return Promise.all(mapped).then((values) => {
const out = {};
for (let i = 0; i < keys.length; i++) {
const val = values[i];
if (val !== null && typeof val !== 'undefined') out[keys[i]] = val;
}
return out;
});
} else {
const out = {};
for (let i = 0; i < keys.length; i++) {
const val = mapped[i];
if (val !== null && typeof val !== 'undefined') out[keys[i]] = val;
}
return out;
}
} else {
return input;
}
}
gun.get(TABLE).load((DATA) => {
var plain2 = recursiveTSDecrypt(DATA, SECRET);
plain2.then(function (result) {
download(
`Export TeleSec ${GROUPID} Decrypted.json.txt`,
JSON.stringify(result)
);
});
});

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -8,5 +8,6 @@
<body>
<h1>Si ves esto, ha ocurrido un fallo critico en la compilación de TeleSec</h1>
<p>Esto NUNCA deberia de ser mostrado.</p>
<a href="dist/index.html">Intenta recargar la página</a>
</body>
</html>

View File

@@ -0,0 +1,273 @@
import base64
import email.utils
import hashlib
import json
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from urllib.parse import quote
import requests
from Crypto.Cipher import AES
class TeleSecCryptoError(Exception):
pass
class TeleSecCouchDBError(Exception):
pass
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len]) * pad_len
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
if not data or len(data) % block_size != 0:
raise TeleSecCryptoError("Invalid padded data length")
pad_len = data[-1]
if pad_len < 1 or pad_len > block_size:
raise TeleSecCryptoError("Invalid PKCS7 padding")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
return data[:-pad_len]
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
d = b""
prev = b""
while len(d) < key_len + iv_len:
prev = hashlib.md5(prev + passphrase + salt).digest()
d += prev
return d[:key_len], d[key_len : key_len + iv_len]
def _json_dumps_like_js(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def ts_encrypt(input_value: Any, secret: str) -> str:
"""
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
wrapped as RSA{<ciphertext>}.
"""
if secret is None or secret == "":
if isinstance(input_value, str):
return input_value
return _json_dumps_like_js(input_value)
payload = input_value
if not isinstance(input_value, str):
try:
payload = _json_dumps_like_js(input_value)
except Exception:
payload = str(input_value)
payload_bytes = payload.encode("utf-8")
salt = os.urandom(8)
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
openssl_blob = b"Salted__" + salt + encrypted
b64 = base64.b64encode(openssl_blob).decode("utf-8")
return f"RSA{{{b64}}}"
def ts_encrypt(input_value: Any, secret: str) -> str:
if not isinstance(input_value, str):
payload = json.dumps(input_value, separators=(",", ":"), ensure_ascii=False)
else:
payload = input_value
payload_bytes = payload.encode("utf-8")
salt = os.urandom(8)
# OpenSSL EVP_BytesToKey (MD5)
dx = b""
salted = b""
while len(salted) < 48: # 32 key + 16 iv
dx = hashlib.md5(dx + secret.encode() + salt).digest()
salted += dx
key = salted[:32]
iv = salted[32:48]
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
openssl_blob = b"Salted__" + salt + encrypted
b64 = base64.b64encode(openssl_blob).decode("utf-8")
return f"RSA{{{b64}}}"
@dataclass
class TeleSecDoc:
id: str
data: Any
raw: Dict[str, Any]
class TeleSecCouchDB:
"""
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
No local replication layer.
"""
def __init__(
self,
server_url: str,
dbname: str,
secret: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
timeout: int = 30,
session: Optional[requests.Session] = None,
) -> None:
self.server_url = server_url.rstrip("/")
self.dbname = dbname
self.secret = secret or ""
self.timeout = timeout
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
self.session = session or requests.Session()
self.session.headers.update({"Accept": "application/json"})
if username is not None:
self.session.auth = (username, password or "")
def _iso_now(self) -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def _doc_id(self, table: str, item_id: str) -> str:
return f"{table}:{item_id}"
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
kwargs.setdefault("timeout", self.timeout)
res = self.session.request(method=method, url=url, **kwargs)
return res
def get_server_datetime(self) -> datetime:
"""
Returns server datetime using HTTP Date header from CouchDB.
Avoids reliance on local machine clock.
"""
candidates = [
("HEAD", self.base_url),
("GET", self.base_url),
("HEAD", self.server_url),
("GET", self.server_url),
]
for method, url in candidates:
try:
res = self.session.request(method=method, url=url, timeout=self.timeout)
date_header = res.headers.get("Date")
if not date_header:
continue
dt = email.utils.parsedate_to_datetime(date_header)
if dt is None:
continue
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except Exception:
continue
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
now = self.get_server_datetime()
target = now.timestamp() + (minutes * 60)
out = datetime.fromtimestamp(target, tz=timezone.utc)
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
def check_connection(self) -> Dict[str, Any]:
res = self._request("GET")
if res.status_code >= 400:
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
return res.json()
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
res = self._request("GET", quote(doc_id, safe=""))
if res.status_code == 404:
return None
if res.status_code >= 400:
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
return res.json()
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
if "_id" not in doc:
raise ValueError("Document must include _id")
res = self._request(
"PUT",
quote(doc["_id"], safe=""),
headers={"Content-Type": "application/json"},
data=_json_dumps_like_js(doc).encode("utf-8"),
)
if res.status_code >= 400:
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
return res.json()
def delete_raw(self, doc_id: str) -> bool:
doc = self.get_raw(doc_id)
if not doc:
return False
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
if res.status_code >= 400:
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
return True
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
doc_id = self._doc_id(table, item_id)
if data is None:
self.delete_raw(doc_id)
return {"ok": True, "id": doc_id, "deleted": True}
existing = self.get_raw(doc_id)
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
to_store = data
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
if encrypt and self.secret and not is_encrypted_string:
to_store = ts_encrypt(data, self.secret)
doc["data"] = to_store
doc["table"] = table
doc["ts"] = self._iso_now()
return self.put_raw(doc)
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
doc_id = self._doc_id(table, item_id)
doc = self.get_raw(doc_id)
if not doc:
return None
value = doc.get("data")
if decrypt:
return ts_decrypt(value, self.secret)
return value
def delete(self, table: str, item_id: str) -> bool:
return self.delete_raw(self._doc_id(table, item_id))
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
params = {
"include_docs": "true",
"startkey": f'"{table}:"',
"endkey": f'"{table}:\uffff"',
}
res = self._request("GET", "_all_docs", params=params)
if res.status_code >= 400:
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
rows = res.json().get("rows", [])
out: List[TeleSecDoc] = []
for row in rows:
doc = row.get("doc") or {}
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
value = doc.get("data")
if decrypt:
value = ts_decrypt(value, self.secret)
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
return out

545
python_sdk/windows_agent.py Normal file
View File

@@ -0,0 +1,545 @@
import argparse
import ctypes
import json
import os
import socket
import subprocess
import sys
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional, List
import psutil
import base64
import email.utils
import hashlib
from dataclasses import dataclass
from urllib.parse import quote
import requests
from Crypto.Cipher import AES
class TeleSecCryptoError(Exception):
pass
class TeleSecCouchDBError(Exception):
pass
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len]) * pad_len
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
if not data or len(data) % block_size != 0:
raise TeleSecCryptoError("Invalid padded data length")
pad_len = data[-1]
if pad_len < 1 or pad_len > block_size:
raise TeleSecCryptoError("Invalid PKCS7 padding")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
return data[:-pad_len]
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
d = b""
prev = b""
while len(d) < key_len + iv_len:
prev = hashlib.md5(prev + passphrase + salt).digest()
d += prev
return d[:key_len], d[key_len : key_len + iv_len]
def _json_dumps_like_js(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def ts_encrypt(input_value: Any, secret: str) -> str:
"""
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
wrapped as RSA{<ciphertext>}.
"""
if secret is None or secret == "":
if isinstance(input_value, str):
return input_value
return _json_dumps_like_js(input_value)
payload = input_value
if not isinstance(input_value, str):
try:
payload = _json_dumps_like_js(input_value)
except Exception:
payload = str(input_value)
payload_bytes = payload.encode("utf-8")
salt = os.urandom(8)
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
openssl_blob = b"Salted__" + salt + encrypted
b64 = base64.b64encode(openssl_blob).decode("utf-8")
return f"RSA{{{b64}}}"
def ts_decrypt(input_value: Any, secret: str) -> Any:
"""
Compatible with JS TS_decrypt behavior:
- If not string: return as-is.
- If RSA{...}: decrypt AES(CryptoJS passphrase mode), parse JSON when possible.
- If plain string JSON: parse JSON.
- Else: return raw string.
"""
if not isinstance(input_value, str):
return input_value
is_wrapped = input_value.startswith("RSA{") and input_value.endswith("}")
if is_wrapped:
if not secret:
raise TeleSecCryptoError("Secret is required to decrypt RSA payload")
b64 = input_value[4:-1]
try:
raw = base64.b64decode(b64)
except Exception as exc:
raise TeleSecCryptoError("Invalid base64 payload") from exc
if len(raw) < 16 or not raw.startswith(b"Salted__"):
raise TeleSecCryptoError("Unsupported encrypted payload format")
salt = raw[8:16]
ciphertext = raw[16:]
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
decrypted = cipher.decrypt(ciphertext)
decrypted = _pkcs7_unpad(decrypted, 16)
try:
text = decrypted.decode("utf-8")
except UnicodeDecodeError:
text = decrypted.decode("latin-1")
try:
return json.loads(text)
except Exception:
return text
try:
return json.loads(input_value)
except Exception:
return input_value
@dataclass
class TeleSecDoc:
id: str
data: Any
raw: Dict[str, Any]
class TeleSecCouchDB:
"""
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
No local replication layer.
"""
def __init__(
self,
server_url: str,
dbname: str,
secret: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
timeout: int = 30,
session: Optional[requests.Session] = None,
) -> None:
self.server_url = server_url.rstrip("/")
self.dbname = dbname
self.secret = secret or ""
self.timeout = timeout
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
self.session = session or requests.Session()
self.session.headers.update({"Accept": "application/json"})
if username is not None:
self.session.auth = (username, password or "")
def _iso_now(self) -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def _doc_id(self, table: str, item_id: str) -> str:
return f"{table}:{item_id}"
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
kwargs.setdefault("timeout", self.timeout)
res = self.session.request(method=method, url=url, **kwargs)
return res
def get_server_datetime(self) -> datetime:
"""
Returns server datetime using HTTP Date header from CouchDB.
Avoids reliance on local machine clock.
"""
candidates = [
("HEAD", self.base_url),
("GET", self.base_url),
("HEAD", self.server_url),
("GET", self.server_url),
]
for method, url in candidates:
try:
res = self.session.request(method=method, url=url, timeout=self.timeout)
date_header = res.headers.get("Date")
if not date_header:
continue
dt = email.utils.parsedate_to_datetime(date_header)
if dt is None:
continue
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except Exception:
continue
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
now = self.get_server_datetime()
target = now.timestamp() + (minutes * 60)
out = datetime.fromtimestamp(target, tz=timezone.utc)
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
def check_connection(self) -> Dict[str, Any]:
res = self._request("GET")
if res.status_code >= 400:
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
return res.json()
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
res = self._request("GET", quote(doc_id, safe=""))
if res.status_code == 404:
return None
if res.status_code >= 400:
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
return res.json()
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
if "_id" not in doc:
raise ValueError("Document must include _id")
res = self._request(
"PUT",
quote(doc["_id"], safe=""),
headers={"Content-Type": "application/json"},
data=_json_dumps_like_js(doc).encode("utf-8"),
)
if res.status_code >= 400:
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
return res.json()
def delete_raw(self, doc_id: str) -> bool:
doc = self.get_raw(doc_id)
if not doc:
return False
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
if res.status_code >= 400:
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
return True
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
doc_id = self._doc_id(table, item_id)
if data is None:
self.delete_raw(doc_id)
return {"ok": True, "id": doc_id, "deleted": True}
existing = self.get_raw(doc_id)
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
to_store = data
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
if encrypt and self.secret and not is_encrypted_string:
to_store = ts_encrypt(data, self.secret)
doc["data"] = to_store
doc["table"] = table
doc["ts"] = self._iso_now()
return self.put_raw(doc)
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
doc_id = self._doc_id(table, item_id)
doc = self.get_raw(doc_id)
if not doc:
return None
value = doc.get("data")
if decrypt:
return ts_decrypt(value, self.secret)
return value
def delete(self, table: str, item_id: str) -> bool:
return self.delete_raw(self._doc_id(table, item_id))
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
params = {
"include_docs": "true",
"startkey": f'"{table}:"',
"endkey": f'"{table}:\uffff"',
}
res = self._request("GET", "_all_docs", params=params)
if res.status_code >= 400:
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
rows = res.json().get("rows", [])
out: List[TeleSecDoc] = []
for row in rows:
doc = row.get("doc") or {}
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
value = doc.get("data")
if decrypt:
value = ts_decrypt(value, self.secret)
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
return out
def utcnow_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def parse_iso(value: str) -> Optional[datetime]:
if not value:
return None
try:
v = value.strip().replace("Z", "+00:00")
dt = datetime.fromisoformat(v)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except Exception:
return None
def get_current_username() -> str:
try:
return psutil.Process().username() or os.getlogin()
except Exception:
try:
return os.getlogin()
except Exception:
return os.environ.get("USERNAME", "")
def _window_title(hwnd: int) -> str:
buf_len = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
if buf_len <= 0:
return ""
buf = ctypes.create_unicode_buffer(buf_len + 1)
ctypes.windll.user32.GetWindowTextW(hwnd, buf, buf_len + 1)
return buf.value or ""
def get_active_app() -> Dict[str, str]:
exe = ""
title = ""
try:
hwnd = ctypes.windll.user32.GetForegroundWindow()
if hwnd:
title = _window_title(hwnd)
pid = ctypes.c_ulong()
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
if pid.value:
try:
proc = psutil.Process(pid.value)
exe = proc.name() or ""
except Exception:
exe = ""
except Exception:
pass
return {"exe": exe, "title": title}
def build_payload(machine_id: str) -> Dict[str, Any]:
app = get_active_app()
return {
"Hostname": machine_id,
"UsuarioActual": get_current_username(),
"AppActualEjecutable": app.get("exe", ""),
"AppActualTitulo": app.get("title", ""),
# campo local diagnóstico (no se usa para decisión remota)
"AgentLocalSeenAt": utcnow_iso(),
}
def should_shutdown(data: Dict[str, Any], server_now: datetime) -> bool:
target = parse_iso(str(data.get("ShutdownBeforeDate", "") or ""))
if not target:
return False
return server_now <= target
def execute_shutdown(dry_run: bool = False) -> None:
if dry_run:
print("[DRY-RUN] Ejecutaría: shutdown /s /t 0 /f")
return
subprocess.run(["shutdown", "/s", "/t", "0", "/f"], check=False)
def run_once(client: TeleSecCouchDB, machine_id: str, dry_run: bool = False) -> None:
server_now = client.get_server_datetime()
server_now_iso = server_now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
raw = client.get(table="aulas_ordenadores", item_id=machine_id, decrypt=False)
current: Dict[str, Any] = {}
if raw is not None:
current = ts_decrypt(raw, client.secret)
if not isinstance(current, dict):
current = {}
update = build_payload(machine_id)
update["LastSeenAt"] = server_now_iso
for key in ["ShutdownBeforeDate", "ShutdownRequestedAt", "ShutdownRequestedBy"]:
if key in current:
update[key] = current.get(key)
client.put(table="aulas_ordenadores", item_id=machine_id, data=update, encrypt=True)
if should_shutdown(update, server_now):
print(f"[{server_now_iso}] ShutdownBeforeDate alcanzado. Apagando {machine_id}...")
execute_shutdown(dry_run=dry_run)
else:
print(
f"[{server_now_iso}] Reportado {machine_id} user={update.get('UsuarioActual','')} "
f"exe={update.get('AppActualEjecutable','')} title={update.get('AppActualTitulo','')}"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
parser.add_argument("--server", default="", help="CouchDB server URL, ej. https://couch.example")
parser.add_argument("--db", default="", help="Database name")
parser.add_argument("--user", default="", help="CouchDB username")
parser.add_argument("--password", default="", help="CouchDB password")
parser.add_argument("--secret", default="", help="TeleSec secret para cifrado")
parser.add_argument("--machine-id", default="", help="ID de máquina (default: hostname)")
parser.add_argument("--interval", type=int, default=15, help="Intervalo en segundos")
parser.add_argument("--once", action="store_true", help="Ejecutar una sola iteración")
parser.add_argument("--dry-run", action="store_true", help="No apagar realmente, solo log")
parser.add_argument(
"--config",
default="",
help="Ruta de config JSON (default: ~/.telesec/windows_agent.json)",
)
return parser.parse_args()
def _default_config_path() -> str:
return os.path.join(os.path.expanduser("~"), ".telesec", "windows_agent.json")
def _load_or_init_config(path: str) -> Dict[str, Any]:
if not os.path.exists(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
default_cfg = {
"server": "https://tu-couchdb",
"db": "telesec",
"user": "",
"password": "",
"secret": "",
"machine_id": "",
"interval": 15,
}
with open(path, "w", encoding="utf-8") as f:
json.dump(default_cfg, f, ensure_ascii=False, indent=2)
return default_cfg
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
return data
return {}
def _save_config(path: str, data: Dict[str, Any]) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _pick(cli_value: Any, cfg_value: Any, default_value: Any = None) -> Any:
if cli_value is None:
return cfg_value if cfg_value not in [None, ""] else default_value
if isinstance(cli_value, str):
if cli_value.strip() == "":
return cfg_value if cfg_value not in [None, ""] else default_value
return cli_value
return cli_value
def main() -> int:
args = parse_args()
config_path = args.config or _default_config_path()
try:
cfg = _load_or_init_config(config_path)
except Exception as exc:
print(f"No se pudo cargar/crear config en {config_path}: {exc}", file=sys.stderr)
return 3
server = _pick(args.server, cfg.get("server"), "")
db = _pick(args.db, cfg.get("db"), "telesec")
user = _pick(args.user, cfg.get("user"), "")
password = _pick(args.password, cfg.get("password"), "")
secret = _pick(args.secret, cfg.get("secret"), "")
machine_id = _pick(args.machine_id, cfg.get("machine_id"), "")
interval = _pick(args.interval, cfg.get("interval"), 15)
machine_id = (machine_id or socket.gethostname() or "unknown-host").strip()
if not server or not secret:
print(
"Falta configuración obligatoria. Edita el JSON en: " + config_path,
file=sys.stderr,
)
return 4
# Persist effective parameters for next runs
try:
persistent_cfg = {
"server": server,
"db": db,
"user": user,
"password": password,
"secret": secret,
"machine_id": machine_id,
"interval": int(interval),
}
_save_config(config_path, persistent_cfg)
except Exception as exc:
print(f"No se pudo guardar config en {config_path}: {exc}", file=sys.stderr)
client = TeleSecCouchDB(
server_url=server,
dbname=db,
secret=secret,
username=user or None,
password=password or None,
)
try:
client.check_connection()
except TeleSecCouchDBError as exc:
print(f"Error de conexión CouchDB: {exc}", file=sys.stderr)
return 2
if args.once:
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
return 0
while True:
try:
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
except Exception as exc:
print(f"Error en iteración agente: {exc}", file=sys.stderr)
time.sleep(max(5, int(interval)))
if __name__ == "__main__":
raise SystemExit(main())

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
requests
pycryptodome
psutil

View File

@@ -1,95 +1,82 @@
function fixfloat(number) {
return parseFloat(number).toPrecision(8);
}
function tableScroll(query) {
$(query).doubleScroll();
}
//var secretTokenEl = document.getElementById("secretToken");
var groupIdEl = document.getElementById("groupId");
var container = document.getElementById("container");
function LinkAccount(LinkAccount_group, LinkAccount_secret, refresh = false) {
GROUPID = LinkAccount_group.toUpperCase();
SECRET = LinkAccount_secret.toUpperCase();
var container = document.getElementById('container');
localStorage.setItem("TELESEC_AUTO", "YES");
localStorage.setItem("TELESEC_groupid", GROUPID);
localStorage.setItem("TELESEC_secret", SECRET);
TABLE = GROUPID + ":telesec.tech.eus";
//secretTokenEl.innerText = SECRET;
groupIdEl.innerText = GROUPID;
document.getElementById("LinkAccount_details").open = false;
if (refresh == true) {
location.reload();
}
}
if (localStorage.getItem("TELESEC_AUTO") == "YES") {
LinkAccount(
localStorage.getItem("TELESEC_groupid"),
localStorage.getItem("TELESEC_secret")
);
}
if (urlParams.get("login") != null) {
LinkAccount(
urlParams.get("login").split(":")[0],
urlParams.get("login").split(":")[1]
);
//location.search = "";
}
function open_page(params) {
if (params == "") {
params = "index";
// Clear stored event listeners and timers
EventListeners.GunJS = [];
EventListeners.Timeout.forEach((ev) => clearTimeout(ev));
EventListeners.Timeout = [];
EventListeners.Interval.forEach((ev) => clearInterval(ev));
EventListeners.Interval = [];
EventListeners.QRScanner.forEach((ev) => ev.clear());
EventListeners.QRScanner = [];
EventListeners.Custom.forEach((ev) => ev());
EventListeners.Custom = [];
EventListeners.DB.forEach((ev) => DB.unlisten(ev));
EventListeners.DB = [];
if (SUB_LOGGED_IN != true && params != 'login,setup' && !params.startsWith('login,onboarding')) {
PAGES['login'].index();
return;
}
var path = params.split(",");
if (params == '') {
params = 'index';
}
var path = params.split(',');
var app = path[0];
if (!PAGES[app]) {
toastr.error('La app solicitada no existe.');
setUrlHash('index');
return;
}
if (path[1] == undefined) {
PAGES[app].index();
return;
}
PAGES[app].edit(path[1]);
PAGES[app].edit(path.slice(1).join(','));
}
function setUrlHash(hash) {
location.hash = "#" + hash;
location.hash = '#' + hash;
// Handle quick search transfer
if (hash === 'buscar') {
const quickSearchInput = document.getElementById('quickSearchInput');
if (quickSearchInput && quickSearchInput.value.trim()) {
// Store the search term temporarily
sessionStorage.setItem('telesec_quick_search', quickSearchInput.value.trim());
quickSearchInput.value = ''; // Clear the input
}
}
}
window.onhashchange = () => {
try {
if (EVENTLISTENER != null) {
try {
EVENTLISTENER.off();
EVENTLISTENER = null;
EVENTLISTENER2.off();
EVENTLISTENER2 = null;
// TypeError: Cannot read properties of null (reading 'off')
} catch (error) {
if (!error.name == "TypeError") {
console.debug("EVENTLISTENER error", error);
}
}
}
} catch (e) {
console.debug("EVENTLISTENER onhashchange", e);
}
open_page(location.hash.replace("#", ""));
open_page(location.hash.replace('#', '').split("?")[0]);
};
function download(filename, text) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:application/octet-stream;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.style.display = "none";
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute(
'href',
'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text)
);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function resizeInputImage(
file,
callback,
targetHeight = 256,
targetQuality = 0.75
) {
function resizeInputImage(file, callback, targetHeight = 256, targetQuality = 0.75) {
const reader = new FileReader();
reader.onload = function (event) {
@@ -98,19 +85,19 @@ function resizeInputImage(
const aspectRatio = img.width / img.height;
const targetWidth = targetHeight * aspectRatio;
const canvas = document.createElement("canvas");
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#ffffff";
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
// Get resized image as Blob
const dataURL = canvas.toDataURL("image/jpeg", targetQuality);
const dataURL = canvas.toDataURL('image/jpeg', targetQuality);
callback(dataURL);
};
img.src = event.target.result;
@@ -118,43 +105,56 @@ function resizeInputImage(
reader.readAsDataURL(file);
}
function CurrentISODate() {
return new Date().toISOString().split("T")[0].replace("T", " ");
return new Date().toISOString().split('T')[0].replace('T', ' ');
}
function CurrentISOTime() {
return new Date().toISOString();
}
function fixGunLocalStorage() {
localStorage.removeItem("radata");
localStorage.removeItem('radata');
removeCache();
location.reload();
}
function betterGunPut(ref, data) {
ref.put(data, (ack) => {
if (ack.err) {
toastr.error(
ack.err + "<br>Pulsa aqui para reiniciar la app",
"Error al guardar",
{ onclick: () => fixGunLocalStorage() }
);
} else {
console.debug("Guardado correctamente");
}
});
setTimeout(() => {
ref.put(data);
}, 250);
setTimeout(() => {
ref.put(data);
}, 500);
// Heartbeat: store a small "last seen" doc locally and replicate to remote when available
// setInterval(() => {
// if (typeof DB !== 'undefined') {
// DB.put('heartbeat', getDBName() || 'heartbeat', 'heartbeat-' + CurrentISOTime());
// }
// }, 5000);
function betterSorter(a, b) {
// 1. Fecha (ascending)
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
return a.Fecha > b.Fecha ? -1 : 1;
}
// 2. Region (ascending, from SC_Personas if Persona exists)
const regionA =
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Region || '' : a.Region || '';
const regionB =
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Region || '' : b.Region || '';
if (regionA !== regionB) {
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
}
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
const nombrePersonaA =
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Nombre || '' : '';
const nombrePersonaB =
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Nombre || '' : '';
if (nombrePersonaA !== nombrePersonaB) {
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase() ? -1 : 1;
}
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
return a.Nombre.toLowerCase() < b.Nombre.toLowerCase() ? -1 : 1;
}
// 5. Asunto (ascending, from a.Asunto/b.Asunto)
if (a.Asunto && b.Asunto && a.Asunto !== b.Asunto) {
return a.Asunto.toLowerCase() < b.Asunto.toLowerCase() ? -1 : 1;
}
return 0;
}
setInterval(() => {
betterGunPut(
gun.get(TABLE).get("heartbeat"),
"heartbeat-" + CurrentISOTime()
);
gun.get(TABLE).get("heartbeat").load(console.debug);
}, 5000);
gun.get(TABLE).on((data) => {
var e = true;
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,146 @@
var EVENTLISTENER = null;
var EVENTLISTENER2 = null;
var urlParams = new URLSearchParams(location.search);
if (urlParams.get("hidenav") == "yes") {
document.getElementById("header_hide_query").style.display = "none";
// Syntax helper for HTML template literals (e.g. html`<div>${content}</div>`)
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
// Global Event Listeners registry for cleanup on logout or other events. Each category can be used to track different types of listeners (e.g., GunJS events, timeouts, intervals, QRScanner events, custom events).
var EventListeners = {
GunJS: [],
Timeout: [],
Interval: [],
QRScanner: [],
Custom: [],
DB: [],
};
// Safe UUID for html element IDs: generates a unique identifier with a specified prefix, ensuring it is safe for use in HTML element IDs. It uses crypto.randomUUID if available, with a fallback to a random string generation method for environments that do not support it. The generated ID is prefixed to avoid collisions and ensure uniqueness across the application.
function safeuuid(prefix = 'AXLUID_') {
if (!crypto.randomUUID) {
// Fallback for environments without crypto.randomUUID()
const randomPart = Math.random().toString(36).substring(2, 10);
return prefix + randomPart;
}
return prefix + crypto.randomUUID().split('-')[4];
}
var GROUPID = "";
// const PUBLIC_KEY = "~cppGiuA4UFUPGTDoC-4r2izVC3F7MfpaCmF3iZdESN4.vntmjgbAVUpF_zfinYY6EKVFuuTYxh5xOrL4KmtdTmc"
var TABLE = GROUPID + ":telesec.tech.eus";
const RELAYS = [
"https://gun-es01.tech.eus/gun",
"https://gun-es02.tech.eus/gun",
"https://gun-es03.tech.eus/gun",
"https://gun-es04.tech.eus/gun",
"https://gun-es05.tech.eus/gun",
"https://gun-es06.tech.eus/gun",
// "https://gun-es07.tech.eus/gun", // No he podido instalar el nodo.
"https://gun-manhattan.herokuapp.com/gun",
"https://peer.wallie.io/gun",
"https://gun.defucc.me/gun",
];
var SECRET = "";
function parseURL(input) {
try {
return new URL(input);
} catch (e) {
try {
return new URL('https://' + input);
} catch (e2) {
return { hostname: '', username: '', password: '', pathname: '' };
}
}
}
var urlParams = new URLSearchParams(location.search);
var AC_BYPASS = false;
if (urlParams.get('ac_bypass') == 'yes') {
AC_BYPASS = true;
}
if (urlParams.get('hidenav') != undefined) {
document.getElementById('header_hide_query').style.display = 'none';
}
// CouchDB URI generator from components: host, user, pass, dbname. Host can include protocol or not, but will be normalized to just hostname in the display. If host is empty, returns empty string.
function makeCouchURLDisplay(host, user, pass, dbname) {
if (!host) return '';
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
return display;
}
// Auto-configure CouchDB from ?couch=<uri> parameter
if (urlParams.get('couch') != null) {
try {
var couchURI = urlParams.get('couch');
// Normalize URL: add https:// if no protocol specified
var normalizedUrl = couchURI;
if (!/^https?:\/\//i.test(couchURI)) {
normalizedUrl = 'https://' + couchURI;
}
var URL_PARSED = parseURL(normalizedUrl);
var user = URL_PARSED.username || '';
var pass = URL_PARSED.password || '';
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
var host = URL_PARSED.hostname || normalizedUrl;
// Extract secret from ?secret= parameter if provided
var secret = urlParams.get('secret') || '';
// Save to localStorage
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
if (secret) {
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
}
// Mark onboarding as complete since we have server config
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
// Clean URL by removing the couch parameter
urlParams.delete('couch');
urlParams.delete('secret');
history.replaceState(
null,
'',
location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '') + location.hash.split("?")[0]
);
console.log('CouchDB auto-configured from URL parameter');
} catch (e) {
console.error('Error auto-configuring CouchDB from URL:', e);
}
}
// getDBName: prefer explicit CouchDB dbname from settings. Single-group model: default to 'telesec'
function getDBName() {
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || '';
if (dbname && dbname.trim() !== '') return dbname.trim();
return 'telesec';
}
var SECRET = '';
var SUB_LOGGED_IN = false;
var SUB_LOGGED_IN_DETAILS = false;
var SUB_LOGGED_IN_ID = false;
var SAVE_WAIT = 500;
var SC_Personas = {};
var PeerConnectionInterval = 5000;
if (urlParams.get('sublogin') != null) {
SUB_LOGGED_IN = true;
SUB_LOGGED_IN_ID = urlParams.get('sublogin');
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
var sli = 15;
var slii = setInterval(() => {
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
sli -= 1;
if (sli < 0) {
clearInterval(slii);
}
}, 500);
}
// Logout function for sublogin: clears sublogin state and reloads the page without the sublogin parameter
function LogOutTeleSec() {
SUB_LOGGED_IN = false;
SUB_LOGGED_IN_DETAILS = false;
SUB_LOGGED_IN_ID = false;
document.getElementById('loading').style.display = 'block';
//Remove sublogin from URL and reload
urlParams.delete('sublogin');
history.replaceState(null, '', '?' + urlParams.toString());
location.reload();
}
var TTS_RATE = parseFloat(urlParams.get('tts_rate')) || 0.75;
function TS_SayTTS(msg) {
try {
if (window.speechSynthesis) {
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = TTS_RATE;
speechSynthesis.speak(utterance);
}
} catch { console.warn('TTS error'); }
}
function createElementFromHTML(htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div.firstChild;
}

417
src/db.js Normal file
View File

@@ -0,0 +1,417 @@
// Simple PouchDB wrapper for TeleSec
// - Uses PouchDB for local storage and optional replication to a CouchDB server
// - Stores records as docs with _id = "<table>:<id>" and field `data` containing either plain object or encrypted string
// - Exposes: init, put, get, del, map, list, replicate
var DB = (function () {
let local = null;
let remote = null;
let changes = null;
let repPush = null;
let repPull = null;
let callbacks = {}; // table -> [{ id, cb }]
let callbackSeq = 0;
let docCache = {}; // _id -> last data snapshot (stringified)
function ensureLocal() {
if (local) return;
try {
const localName = 'telesec';
local = new PouchDB(localName);
if (changes) {
try {
changes.cancel();
} catch (e) {}
}
changes = local
.changes({ live: true, since: 'now', include_docs: true })
.on('change', onChange);
} catch (e) {
console.warn('ensureLocal error', e);
}
}
function makeId(table, id) {
return table + ':' + id;
}
function makeCallbackId(table) {
callbackSeq += 1;
return table + '#' + callbackSeq;
}
function init(opts) {
const localName = 'telesec';
try {
if (opts && opts.secret) {
SECRET = opts.secret;
try {
localStorage.setItem('TELESEC_SECRET', SECRET);
} catch (e) {}
}
} catch (e) {}
local = new PouchDB(localName);
if (opts.remoteServer) {
try {
const server = opts.remoteServer.replace(/\/$/, '');
const dbname = encodeURIComponent(opts.dbname || localName);
let authPart = '';
if (opts.username) authPart = opts.username + ':' + (opts.password || '') + '@';
const remoteUrl = server.replace(/https?:\/\//, (m) => m) + '/' + dbname;
if (opts.username) remote = new PouchDB(remoteUrl.replace(/:\/\//, '://' + authPart));
else remote = new PouchDB(remoteUrl);
replicateToRemote();
} catch (e) {
console.warn('Remote DB init error', e);
}
}
if (changes) changes.cancel();
changes = local
.changes({ live: true, since: 'now', include_docs: true })
.on('change', onChange);
return Promise.resolve();
}
function replicateToRemote() {
ensureLocal();
if (!local || !remote) return;
try {
if (repPush && repPush.cancel) repPush.cancel();
} catch (e) {}
try {
if (repPull && repPull.cancel) repPull.cancel();
} catch (e) {}
repPush = PouchDB.replicate(local, remote, { live: true, retry: true }).on(
'error',
function (err) {
console.warn('Replication push error', err);
}
);
repPull = PouchDB.replicate(remote, local, { live: true, retry: true }).on(
'error',
function (err) {
console.warn('Replication pull error', err);
}
);
}
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', function () {
try {
setTimeout(replicateToRemote, 1000);
} catch (e) {}
});
}
function onChange(change) {
const doc = change.doc;
if (!doc || !doc._id) return;
try {
window.TELESEC_LAST_SYNC = Date.now();
// derive a stable color from the last record's data hash
let payload = '';
try {
payload = typeof doc.data === 'string' ? doc.data : JSON.stringify(doc.data || {});
} catch (e) {
payload = String(doc._id || '');
}
let hash = 0;
for (let i = 0; i < payload.length; i++) {
hash = (hash * 31 + payload.charCodeAt(i)) >>> 0;
}
const hue = hash % 360;
window.TELESEC_LAST_SYNC_COLOR = `hsl(${hue}, 70%, 50%)`;
updateStatusOrb();
} catch (e) {}
const [table, id] = doc._id.split(':');
// handle deletes
if (change.deleted || doc._deleted) {
delete docCache[doc._id];
if (callbacks[table]) {
callbacks[table].forEach((listener) => {
const cb = listener.cb;
try {
cb(null, id);
} catch (e) {
console.error(e);
}
});
}
return;
}
// handle insert/update
try {
const now = typeof doc.data === 'string' ? doc.data : JSON.stringify(doc.data);
const prev = docCache[doc._id];
if (prev === now) return; // no meaningful change
docCache[doc._id] = now;
} catch (e) {
/* ignore cache errors */
}
if (callbacks[table]) {
callbacks[table].forEach((listener) => {
const cb = listener.cb;
try {
cb(doc.data, id);
} catch (e) {
console.error(e);
}
});
}
}
async function put(table, id, data) {
ensureLocal();
const _id = makeId(table, id);
try {
const existing = await local.get(_id).catch(() => null);
if (data === null) {
if (existing) await local.remove(existing);
return;
}
const doc = existing || { _id: _id };
var toStore = data;
try {
var isEncryptedString =
typeof data === 'string' && data.startsWith('RSA{') && data.endsWith('}');
if (
!isEncryptedString &&
typeof TS_encrypt === 'function' &&
typeof SECRET !== 'undefined' &&
SECRET
) {
toStore = await new Promise((resolve) => {
try {
TS_encrypt(data, SECRET, (enc) => resolve(enc));
} catch (e) {
resolve(data);
}
});
}
} catch (e) {
toStore = data;
}
doc.data = toStore;
doc.table = table;
doc.ts = new Date().toISOString();
if (existing) doc._rev = existing._rev;
await local.put(doc);
// FIX: manually trigger map() callbacks for local update
// onChange will update docCache and notify all subscribers
onChange({ doc: doc });
} catch (e) {
console.error('DB.put error', e);
}
}
async function get(table, id) {
ensureLocal();
const _id = makeId(table, id);
try {
const doc = await local.get(_id);
return doc.data;
} catch (e) {
return null;
}
}
async function del(table, id) {
return put(table, id, null);
}
async function list(table) {
ensureLocal();
try {
const res = await local.allDocs({
include_docs: true,
startkey: table + ':',
endkey: table + ':\uffff',
});
return res.rows.map((r) => {
const id = r.id.split(':')[1];
try {
docCache[r.id] = typeof r.doc.data === 'string' ? r.doc.data : JSON.stringify(r.doc.data);
} catch (e) {}
return { id: id, data: r.doc.data };
});
} catch (e) {
return [];
}
}
function dataURLtoBlob(dataurl) {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
return new Blob([u8arr], { type: mime });
}
async function putAttachment(table, id, name, dataUrlOrBlob, contentType) {
ensureLocal();
const _id = makeId(table, id);
try {
let doc = await local.get(_id).catch(() => null);
if (!doc) {
await local.put({ _id: _id, table: table, ts: new Date().toISOString(), data: {} });
doc = await local.get(_id);
}
let blob = dataUrlOrBlob;
if (typeof dataUrlOrBlob === 'string' && dataUrlOrBlob.indexOf('data:') === 0)
blob = dataURLtoBlob(dataUrlOrBlob);
const type = contentType || (blob && blob.type) || 'application/octet-stream';
await local.putAttachment(_id, name, doc._rev, blob, type);
return true;
} catch (e) {
console.error('putAttachment error', e);
return false;
}
}
async function getAttachment(table, id, name) {
ensureLocal();
const _id = makeId(table, id);
try {
const blob = await local.getAttachment(_id, name);
if (!blob) return null;
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsDataURL(blob);
});
} catch (e) {
return null;
}
}
async function listAttachments(table, id) {
ensureLocal();
const _id = makeId(table, id);
try {
const doc = await local.get(_id, { attachments: true });
if (!doc || !doc._attachments) return [];
const out = [];
for (const name of Object.keys(doc._attachments)) {
try {
const att = doc._attachments[name];
if (att && att.data) {
const content_type = att.content_type || 'application/octet-stream';
const durl = 'data:' + content_type + ';base64,' + att.data;
out.push({ name: name, dataUrl: durl, content_type: content_type });
continue;
}
} catch (e) {}
try {
const durl = await getAttachment(table, id, name);
out.push({ name: name, dataUrl: durl, content_type: null });
} catch (e) {
out.push({ name: name, dataUrl: null, content_type: null });
}
}
return out;
} catch (e) {
try {
const doc = await local.get(_id).catch(() => null);
if (!doc || !doc._attachments) return [];
const out = [];
for (const name of Object.keys(doc._attachments)) {
try {
const durl = await getAttachment(table, id, name);
out.push({ name: name, dataUrl: durl, content_type: null });
} catch (e) {
out.push({ name: name, dataUrl: null, content_type: null });
}
}
return out;
} catch (e2) {
return [];
}
}
}
async function deleteAttachment(table, id, name) {
ensureLocal();
const _id = makeId(table, id);
try {
const doc = await local.get(_id);
if (!doc || !doc._attachments || !doc._attachments[name]) return false;
delete doc._attachments[name];
await local.put(doc);
return true;
} catch (e) {
console.error('deleteAttachment error', e);
return false;
}
}
function map(table, cb) {
ensureLocal();
const callbackId = makeCallbackId(table);
callbacks[table] = callbacks[table] || [];
callbacks[table].push({ id: callbackId, cb: cb });
list(table).then((rows) => {
const stillListening = (callbacks[table] || []).some((listener) => listener.id === callbackId);
if (!stillListening) return;
rows.forEach((r) => cb(r.data, r.id));
});
return callbackId;
}
function unlisten(callbackId) {
if (!callbackId) return false;
for (const table of Object.keys(callbacks)) {
const before = callbacks[table].length;
callbacks[table] = callbacks[table].filter((listener) => listener.id !== callbackId);
if (callbacks[table].length !== before) return true;
}
return false;
}
return {
init,
put,
get,
del,
list,
map,
unlisten,
replicateToRemote,
listAttachments,
deleteAttachment,
putAttachment,
getAttachment,
_internal: { local },
};
})();
window.DB = DB;
// Auto-initialize DB on startup using saved settings (non-blocking)
(function autoInitDB() {
try {
const remoteServer = localStorage.getItem('TELESEC_COUCH_URL') || '';
const username = localStorage.getItem('TELESEC_COUCH_USER') || '';
const password = localStorage.getItem('TELESEC_COUCH_PASS') || '';
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || undefined;
try {
SECRET = localStorage.getItem('TELESEC_SECRET') || '';
} catch (e) {
SECRET = '';
}
DB.init({ remoteServer, username, password, dbname }).catch((e) =>
console.warn('DB.autoInit error', e)
);
} catch (e) {
console.warn('DB.autoInit unexpected error', e);
}
})();

View File

@@ -1,61 +1,4 @@
window.rtcRoom = "telesec.tech.eus";
var gun = Gun(RELAYS, {
axe: false,
localStorage: true,
// radisk: true,
});
var SEA = Gun.SEA;
var user = gun.user();
function removeCache() {
caches.keys().then(function (names) {
for (let name of names) caches.delete(name);
console.log("Removing cache " + name);
console.log("OK");
location.reload(true);
});
}
function getPeers() {
var peerCount = 0;
var peerCountEl = document.getElementById("peerCount");
var peerListEl = document.getElementById("peerList");
var list = document.createElement("ul");
document.getElementById("peerPID").innerText = "PID " + gun.back("opt.pid");
Object.values(gun.back("opt.peers")).forEach((peer) => {
if (
peer.wire != undefined &&
(peer.wire.readyState == 1 || peer.wire.readyState == "open")
) {
peerCount += 1;
var wireType = peer.wire.constructor.name;
var wireHType = peer.wire.constructor.name;
var wireID = peer.id;
switch (wireType) {
case "WebSocket":
wireHType = "Web";
wireID = wireID.split("/")[2];
break;
case "RTCDataChannel":
wireHType = "Mesh";
wireID = peer.id;
}
var el = document.createElement("li");
el.innerText = `Nodo ${wireHType}: ${wireID}`;
list.append(el);
}
});
peerListEl.innerHTML = list.innerHTML;
peerCountEl.innerText = peerCount;
if (peerCount < 3) {
document.getElementById("connectStatus").src = "static/ico/connect_ko.svg";
gun.opt({ peers: RELAYS });
} else {
document.getElementById("connectStatus").src = "static/ico/connect_ok.svg";
}
}
getPeers();
setInterval(() => {
getPeers();
}, 2500);
function safeuuid(prefix = "AXLUID_") {
return prefix + crypto.randomUUID().split("-")[4];
}
// gun_init.js - Deprecated
// Gun/GunDB has been replaced by PouchDB/CouchDB in this project.
// This file is kept for reference only and should not be used.
console.warn('gun_init.js is deprecated; using PouchDB/DB module instead.');

View File

@@ -1,154 +1,97 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="manifest.json" />
<title>TeleSec</title>
<link rel="icon" type="image/png" href="static/TeleSec.jpg" />
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
<link href="static/toastr.min.css" rel="stylesheet" />
%%PREFETCH%%
</head>
<body>
<details class="supermesh-indicator">
<summary>
<b>SuperMesh</b><br />
<br /><small id="peerPID" style="font-family: monospace"
>PID ??????????</small
>
</summary>
<ul id="peerList"></ul>
<i>Todos los datos están encriptados.</i>
</details>
<main>
<header class="no_print" id="header_hide_query">
<details id="LinkAccount_details" open>
<summary>
<b
>TeleSec - <span id="groupId">???</span> - (<span id="peerCount"
>?</span
>
nodos)</b
>
</summary>
<fieldset id="auth_fieldSet">
<legend>Credenciales</legend>
<br />
<label
>Codigo de grupo:<br />
<input type="text" id="LinkAccount_group"
/></label>
<br />
<br />
<label
>Clave secreta:<br />
<input type="text" id="LinkAccount_secret"
/></label>
<br /><br />
<button
type="button"
onclick='LinkAccount(document.getElementById("LinkAccount_group").value, document.getElementById("LinkAccount_secret").value, true)'
>
Iniciar sesión
</button>
</fieldset>
</details>
<!-- <button onclick="displayPost('index')">Ir a la pagina de inicio</button> -->
<div id="appendApps">
<!--<a class="button nav-supercafe nav-disabled" disabled>SuperCafé</a>
<a class="button nav-comedor nav-disabled" disabled>Menú Comedor</a>
<a class="button nav-recetas nav-disabled" disabled>Recetas</a>-->
</div>
<hr />
</header>
<div id="container"></div>
<!-- <br><br><br>
<footer>
<hr>
<details>
<summary><b>Apps SuperMesh</b></summary>
<button type="button">
<img src="static/TeleSec.jpg" alt="" width="100px">
<br>TeleSec
</button>
</details>
</footer> -->
<img
id="connectStatus"
style="bottom: 15px; right: 15px; position: fixed; width: 50px"
/>
</main>
<img
id="actionStatus"
src="static/ico/statusok.png"
style="
z-index: 2048;
margin: 0px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
display: none;
"
/>
<div id="snackbar">
Hay una nueva versión de TeleSec.<br /><a id="reload"
>Pulsa aqui para actualizar.</a
>
</div>
<script src="static/showdown.min.js"></script>
<script src="static/jquery.js"></script>
<script src="static/gun.js"></script>
<script src="static/webrtc.js"></script>
<script src="static/sea.js"></script>
<script src="static/yson.js"></script>
<script src="static/radix.js"></script>
<!-- <script src="static/radisk.js"></script> -->
<!-- <script src="static/store.js"></script> -->
<script src="static/rindexed.js"></script>
<script src="static/path.js"></script>
<script src="static/open.js"></script>
<script src="static/load.js"></script>
<!--<script src="static/synchronous.js"></script>-->
<!--<script src="static/axe.js"></script>-->
<script src="static/toastr.min.js"></script>
<script src="static/doublescroll.js"></script>
<!--<script src="static/simplemde.min.js"></script>-->
<script async>
async function getQuota(cb = () => {}) {
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(
`You've used ${percentageUsed}% of the available storage.`
);
const remaining = quota.quota - quota.usage;
cb(percentageUsed, remaining);
console.log(`You can write up to ${remaining} more bytes.`);
}
}
getQuota();
</script>
<script src="pwa.js"></script>
<script src="config.js"></script>
<script src="gun_init.js"></script>
<script src="app_logic.js"></script>
<script src="app_modules.js"></script>
<script src="page__index.js"></script>
<script src="page__importar.js"></script>
<script src="page__exportar.js"></script>
<script src="page__materiales.js"></script>
<script src="page__resumen_diario.js"></script>
<script src="page__personas.js"></script>
<script src="page__supercafe.js"></script>
<script src="page__notificaciones.js"></script>
<script src="page__comedor.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="manifest.json" />
<title>%%TITLE%%</title>
<link rel="icon" type="image/png" href="static/logo.jpg" />
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
<link href="static/toastr.min.css" rel="stylesheet" />
%%PREFETCH%%
</head>
<body>
<div class="ribbon no_print" id="header_hide_query">
<img class="ribbon-orb" id="connectStatus" src="static/logo.jpg" style="cursor: pointer;" />
<div class="ribbon-content" id="ribbon-content" style="display: none;">
<div class="ribbon-tabs">
<div class="ribbon-tab active" data-tab="modulos">Modulos</div>
<div class="ribbon-tab" data-tab="buscar">Buscar</div>
</div>
<!-- Tab: Modulos -->
<details id="tab-modulos" open>
<summary hidden>Modulos</summary>
<div class="ribbon-panel" id="appendApps2">
</div>
</details>
<!-- Tab: Buscar -->
<details id="tab-buscar">
<summary hidden>Buscar</summary>
<div class="ribbon-panel">
<input type="text" id="quickSearchInput" placeholder="Búsqueda rápida..."
onkeypress="if(event.key==='Enter') document.getElementById('quickSearchBtn').click()">
<button id="quickSearchBtn" onclick="setUrlHash('buscar')" class="btn5">
Buscar
</button>
</div>
</details>
<small style="margin-top:10px;">Base de datos: <b id="peerLink">(no configurado)</b></small>
</div>
<div class="ribbon-content" id="ribbon-content-alternative">
<h2 style="margin: 0;">TeleSec, muy seguro. </h2>
<i><small>(pulsa el icono para mostrar el menú)</small></i>
</div>
</div>
<img id="loading" src="load.gif" style="display: block; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 50px); max-width: 400px;" />
<main id="container"></main>
<img id="actionStatus" src="static/ico/statusok.png" style="
z-index: 2048;
margin: 0px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
display: none;
" />
<div id="snackbar">
Hay una nueva versión de %%TITLE%%.<br /><a id="reload">Pulsa aqui para actualizar.</a>
</div>
<script src="static/aes.js"></script>
<script src="static/showdown.min.js"></script>
<script src="static/qrcode/html5-qrcode.min.js"></script>
<script src="static/qrcode/barcode.js"></script>
<script src="static/qrcode/qrcode.min.js"></script>
<script src="static/jquery.js"></script>
<script src="static/pouchdb.min.js"></script>
<script src="static/toastr.min.js"></script>
<script src="static/doublescroll.js"></script>
<script src="static/chart.umd.min.js"></script>
<script src="pwa.js"></script>
<script src="config.js"></script>
<script src="db.js"></script>
<script src="app_logic.js"></script>
<script src="app_modules.js"></script>
<script src="page/login.js"></script>
<script src="page/index.js"></script>
<script src="page/dataman.js"></script>
<script src="page/aulas.js"></script>
<script src="page/materiales.js"></script>
<script src="page/personas.js"></script>
<script src="page/supercafe.js"></script>
<script src="page/comedor.js"></script>
<script src="page/notas.js"></script>
<script src="page/panel.js"></script>
<script src="page/buscar.js"></script>
<script src="page/pagos.js"></script>
</body>
</html>

28
src/manifest.json Normal file
View File

@@ -0,0 +1,28 @@
{
"theme_color": "#23365e",
"background_color": "#ffffff",
"icons": [
{
"purpose": "maskable",
"sizes": "512x512",
"src": "icon512_maskable.png",
"type": "image/png"
},
{
"purpose": "any",
"sizes": "512x512",
"src": "icon512_rounded.png",
"type": "image/png"
}
],
"orientation": "any",
"display": "standalone",
"dir": "auto",
"lang": "es-ES",
"start_url": "index.html",
"scope": "/",
"description": "%%TITLE%% - Comunicación Hipersegura",
"id": "telesec.tech.eus",
"name": "%%TITLE%%",
"short_name": "%%TITLE%%"
}

1212
src/page/aulas.js Normal file

File diff suppressed because it is too large Load Diff

265
src/page/buscar.js Normal file
View File

@@ -0,0 +1,265 @@
PAGES.buscar = {
navcss: 'btn1',
icon: 'static/appico/view.svg',
Title: 'Buscar',
AccessControl: true,
Esconder: true,
index: function () {
const searchInput = safeuuid();
const resultsContainer = safeuuid();
const searchButton = safeuuid();
const recentSearches = safeuuid();
const moduleFilter = safeuuid();
container.innerHTML = html`
<h1>🔍 Búsqueda Global</h1>
<p>Busca en todos los módulos: personas, materiales, café, comedor, notas y avisos</p>
<fieldset>
<legend>Opciones de búsqueda</legend>
<input
type="text"
id="${searchInput}"
placeholder="Escribe aquí para buscar..."
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()"
/>
<select id="${moduleFilter}">
<option value="">Todos los módulos</option>
<!-- Options will be populated dynamically based on user permissions -->
</select>
<button id="${searchButton}" class="btn5">Buscar</button>
</fieldset>
<div id="${recentSearches}"></div>
<div id="${resultsContainer}">
<fieldset>
<legend>Resultados</legend>
<div>🔍 Introduce un término de búsqueda para comenzar</div>
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
<details>
<summary>💡 Consejos de búsqueda</summary>
<ul>
<li><strong>Busca por nombres:</strong> "María", "García"</li>
<li><strong>Busca por fechas:</strong> "2024-10-01" o "01/10/2024"</li>
<li><strong>Busca por ubicación:</strong> "aula", "laboratorio"</li>
<li><strong>Usa filtros:</strong> selecciona un módulo específico</li>
<li><strong>Atajos de teclado:</strong> Ctrl+F para buscar, Esc para limpiar</li>
</ul>
</details>
</fieldset>
</div>
`;
// Initialize global search
const globalSearch = GlobalSearch();
globalSearch.loadAllData();
// Get accessible modules for the current user
const accessibleModules = globalSearch.getAccessibleModules();
const searchInputEl = document.getElementById(searchInput);
const resultsEl = document.getElementById(resultsContainer);
const searchButtonEl = document.getElementById(searchButton);
const recentSearchesEl = document.getElementById(recentSearches);
const moduleFilterEl = document.getElementById(moduleFilter);
// Populate module filter dropdown with only accessible modules
function populateModuleFilter() {
// Clear existing options except "Todos los módulos"
moduleFilterEl.innerHTML = '<option value="">Todos los módulos</option>';
// Add only accessible modules
accessibleModules.forEach((module) => {
const option = document.createElement('option');
option.value = module.key;
option.textContent = `${getModuleIcon(module.key)} ${module.title}`;
moduleFilterEl.appendChild(option);
});
}
// Helper function to get module icons (fallback for older module mappings)
function getModuleIcon(moduleKey) {
const iconMap = {
personas: '👤',
materiales: '📦',
supercafe: '☕',
comedor: '🍽️',
avisos: '🔔',
aulas: '🏫',
resumen_diario: '📊',
};
return iconMap[moduleKey] || '📋';
}
// Load recent searches from localStorage
function loadRecentSearches() {
const recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
if (recent.length > 0) {
recentSearchesEl.innerHTML = html`
<fieldset>
<legend>Búsquedas recientes</legend>
${recent
.map(
(term) => `
<button onclick="document.getElementById('${searchInput}').value='${term}'; document.getElementById('${searchButton}').click();" class="btn4">
${term}
</button>
`
)
.join('')}
<button
onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';"
class="rojo"
>
Limpiar
</button>
</fieldset>
`;
}
}
// Populate the module filter dropdown
populateModuleFilter();
// Save search term to recent searches
function saveToRecent(term) {
if (!term || term.length < 2) return;
let recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
recent = recent.filter((t) => t !== term); // Remove if exists
recent.unshift(term); // Add to beginning
recent = recent.slice(0, 5); // Keep only 5 most recent
localStorage.setItem('telesec_recent_searches', JSON.stringify(recent));
loadRecentSearches();
}
// Perform search
function performSearch() {
const searchTerm = searchInputEl.value.trim();
const selectedModule = moduleFilterEl.value;
if (searchTerm.length < 2) {
resultsEl.innerHTML = html`
<fieldset>
<legend>Error</legend>
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
</fieldset>
`;
return;
}
// Show loading
resultsEl.innerHTML = html`
<fieldset>
<legend>Buscando...</legend>
<div>⏳ Procesando búsqueda...</div>
</fieldset>
`;
// Add small delay to show loading state
setTimeout(() => {
let results = globalSearch.performSearch(searchTerm);
// Filter by module if selected
if (selectedModule) {
results = results.filter((result) => result._module === selectedModule);
}
globalSearch.renderResults(results, resultsEl);
saveToRecent(searchTerm);
// Add stats
if (results.length > 0) {
const statsDiv = document.createElement('fieldset');
const legend = document.createElement('legend');
legend.textContent = 'Estadísticas';
statsDiv.appendChild(legend);
let filterText = selectedModule
? ` en ${moduleFilterEl.options[moduleFilterEl.selectedIndex].text}`
: '';
const content = document.createElement('div');
content.innerHTML = html`📊 Se encontraron <strong>${results.length}</strong> resultados
para "<strong>${searchTerm}</strong>"${filterText}`;
statsDiv.appendChild(content);
resultsEl.insertBefore(statsDiv, resultsEl.firstChild);
}
}, 500);
}
// Event listeners
searchButtonEl.onclick = performSearch;
// Filter change listener
moduleFilterEl.onchange = () => {
if (searchInputEl.value.trim().length >= 2) {
performSearch();
}
};
// Auto-search as user types (with debounce)
let searchTimeout;
searchInputEl.oninput = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
if (searchInputEl.value.trim().length >= 2) {
performSearch();
}
}, 1501);
};
// Focus on search input
searchInputEl.focus();
// Add keyboard shortcuts
document.addEventListener('keydown', function (e) {
// Ctrl+F or Cmd+F to focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
searchInputEl.focus();
searchInputEl.select();
}
// Escape to clear search
if (e.key === 'Escape') {
searchInputEl.value = '';
searchInputEl.focus();
resultsEl.innerHTML = html`
<fieldset>
<legend>Resultados</legend>
<div>🔍 Introduce un término de búsqueda para comenzar</div>
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
<details>
<summary>💡 Consejos de búsqueda</summary>
<ul>
<li><strong>Busca por nombres:</strong> "María", "García"</li>
<li><strong>Busca por fechas:</strong> "2024-10-01" o "01/10/2024"</li>
<li><strong>Busca por ubicación:</strong> "aula", "laboratorio"</li>
<li><strong>Usa filtros:</strong> selecciona un módulo específico</li>
<li><strong>Atajos de teclado:</strong> Ctrl+F para buscar, Esc para limpiar</li>
</ul>
</details>
</fieldset>
`;
}
});
// Check for quick search term from header
const quickSearchTerm = sessionStorage.getItem('telesec_quick_search');
if (quickSearchTerm) {
searchInputEl.value = quickSearchTerm;
sessionStorage.removeItem('telesec_quick_search');
// Perform search automatically
setTimeout(performSearch, 100);
}
// Load recent searches
loadRecentSearches();
},
};

297
src/page/comedor.js Normal file
View File

@@ -0,0 +1,297 @@
PERMS['comedor'] = 'Comedor';
PERMS['comedor:edit'] = '&gt; Editar';
PAGES.comedor = {
navcss: 'btn6',
icon: 'static/appico/apple.png',
AccessControl: true,
Title: 'Comedor',
__cleanupOldMenus: async function () {
try {
var rows = await DB.list('comedor');
var now = new Date();
var todayUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
var removed = 0;
function parseISODateToUTC(value) {
if (!value || typeof value !== 'string') return null;
var match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) return null;
var y = parseInt(match[1], 10);
var m = parseInt(match[2], 10) - 1;
var d = parseInt(match[3], 10);
return Date.UTC(y, m, d);
}
async function getFechaFromRow(row) {
var data = row.data;
if (typeof data === 'string') {
return await new Promise((resolve) => {
TS_decrypt(
data,
SECRET,
(decrypted) => {
if (decrypted && typeof decrypted === 'object') {
resolve(decrypted.Fecha || row.id.split(',')[0] || '');
} else {
resolve(row.id.split(',')[0] || '');
}
},
'comedor',
row.id
);
});
}
if (data && typeof data === 'object') {
return data.Fecha || row.id.split(',')[0] || '';
}
return row.id.split(',')[0] || '';
}
for (const row of rows) {
var fecha = await getFechaFromRow(row);
var rowUTC = parseISODateToUTC(fecha);
if (rowUTC == null) continue;
var ageDays = Math.floor((todayUTC - rowUTC) / 86400000);
if (ageDays >= 30) {
await DB.del('comedor', row.id);
removed += 1;
}
}
if (removed > 0) {
toastr.info('Limpieza automática: ' + removed + ' menús antiguos eliminados.');
}
} catch (e) {
console.warn('Comedor cleanup error', e);
}
},
edit: function (mid) {
if (!checkRole('comedor:edit')) {
setUrlHash('comedor');
return;
}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_tipo = safeuuid();
var field_primero = safeuuid();
var field_segundo = safeuuid();
var field_postre = safeuuid();
var btn_picto_primero = safeuuid();
var btn_picto_segundo = safeuuid();
var btn_picto_postre = safeuuid();
var debounce_picto = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = html`
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
<fieldset style="float: left;">
<legend>Valores</legend>
<label>
Fecha<br />
<input type="date" id="${field_fecha}" value="" /><br /><br />
</label>
<label>
Tipo<br />
<input type="text" id="${field_tipo}" value="" /><br /><br />
</label>
<label>
Primero<br />
<input type="text" id="${field_primero}" value="" /><br />
<div class="picto" id="${btn_picto_primero}"></div>
</label>
<label>
Segundo<br />
<input type="text" id="${field_segundo}" value="" /><br />
<div class="picto" id="${btn_picto_segundo}"></div>
</label>
<label>
Postre<br />
<input type="text" id="${field_postre}" value="" /><br />
<div class="picto" id="${btn_picto_postre}"></div>
</label>
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
<button class="opicon" onclick="setUrlHash('comedor')" style="float: right;"> <!-- Align to the right -->
<img src="static/exit.png" />
<br>Salir
</button>
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
<img src="static/printer2.png" />
<br>Imprimir
</button>
</fieldset>
`;
const pictogramSelector = TS_CreateArasaacSelector({
modal: true,
debounceId: debounce_picto,
onPick: (context, item) => {
TS_applyPictoValue(context.pictoId, {
text: item.label,
arasaacId: String(item.id),
});
},
});
document.getElementById(btn_picto_primero).onclick = () =>
pictogramSelector.open({ pictoId: btn_picto_primero });
document.getElementById(btn_picto_segundo).onclick = () =>
pictogramSelector.open({ pictoId: btn_picto_segundo });
document.getElementById(btn_picto_postre).onclick = () =>
pictogramSelector.open({ pictoId: btn_picto_postre });
DB.get('comedor', mid).then((data) => {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data['Fecha'] || mid || CurrentISODate();
document.getElementById(field_tipo).value = data['Tipo'] || '';
document.getElementById(field_primero).value = data['Primero'] || '';
document.getElementById(field_segundo).value = data['Segundo'] || '';
document.getElementById(field_postre).value = data['Postre'] || '';
TS_applyPictoValue(btn_picto_primero, data['Primero_Picto'] || '');
TS_applyPictoValue(btn_picto_segundo, data['Segundo_Picto'] || '');
TS_applyPictoValue(btn_picto_postre, data['Postre_Picto'] || '');
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'comedor',
mid
);
} else {
load_data(data || {});
}
});
document.getElementById(btn_guardar).onclick = () => {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
const newDate = document.getElementById(field_fecha).value;
const newTipo = document.getElementById(field_tipo).value.trim();
var data = {
Fecha: newDate,
Tipo: newTipo,
Primero: document.getElementById(field_primero).value.trim(),
Segundo: document.getElementById(field_segundo).value.trim(),
Postre: document.getElementById(field_postre).value.trim(),
Primero_Picto: TS_getPictoValue(btn_picto_primero),
Segundo_Picto: TS_getPictoValue(btn_picto_segundo),
Postre_Picto: TS_getPictoValue(btn_picto_postre),
};
// If the date has changed, we need to delete the old entry
if (mid !== newDate + "," + newTipo && mid !== '') {
DB.del('comedor', mid);
}
document.getElementById('actionStatus').style.display = 'block';
DB.put('comedor', newDate + "," + newTipo, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('comedor');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el menú');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar esta entrada?') == true) {
DB.del('comedor', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('comedor');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole('comedor')) {
setUrlHash('index');
return;
}
const cont = safeuuid();
var btn_new = safeuuid();
container.innerHTML = html`
<h1>Menú del comedor</h1>
<button id="${btn_new}">Nueva entrada</button>
<div id="${cont}"></div>
`;
var renderList = () => {
TS_IndexElement(
'comedor',
[
{
key: 'Fecha',
type: 'raw',
default: '',
label: 'Fecha',
},
{
key: 'Tipo',
type: 'raw',
default: '',
label: 'Tipo',
},
{
key: 'Primero_Picto',
type: 'picto',
default: '',
label: 'Primero',
labelkey: 'Primero',
},
{
key: 'Segundo_Picto',
type: 'picto',
default: '',
label: 'Segundo',
labelkey: 'Segundo',
},
{
key: 'Postre_Picto',
type: 'picto',
default: '',
label: 'Postre',
labelkey: 'Postre',
},
],
'comedor',
document.getElementById(cont),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
if (data.Fecha == CurrentISODate()) {
new_tr.style.backgroundColor = 'lightgreen';
}
}
);
};
PAGES.comedor.__cleanupOldMenus().finally(renderList);
if (!checkRole('comedor:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash('comedor,' + safeuuid(''));
};
}
},
};

300
src/page/dataman.js Normal file
View File

@@ -0,0 +1,300 @@
PAGES.dataman = {
navcss: 'btn1',
icon: 'static/appico/gear_edit.png',
AccessControl: true,
Title: 'Ajustes',
edit: function (mid) {
switch (mid) {
case 'export':
PAGES.dataman.__export();
break;
case 'import':
PAGES.dataman.__import();
break;
case 'config':
PAGES.dataman.__config();
break;
case 'labels':
PAGES.dataman.__labels();
break;
case 'precios':
PAGES.dataman.__precios();
break;
default:
// Tab to edit
}
},
__config: function () {
var form = safeuuid();
container.innerHTML = html`
<h1>Ajustes</h1>
<h2>No disponible</h2>
<form id="${form}">
<label>
<input type="checkbox" name="block_add_account" value="yes" />
<b>Bloquear crear cuenta de administrador?</b>
</label>
<button type="submit">Aplicar ajustes</button>
</form>
`;
document.getElementById(form).onsubmit = (ev) => {
ev.preventDefault();
var ford = new FormData(document.getElementById(form));
if (ford.get('block_add_account') == 'yes') {
config['block_add_account'] = true;
}
};
},
__export: function () {
var button_export_local = safeuuid();
var button_export_safe = safeuuid();
container.innerHTML = html`
<h1>Exportar Datos</h1>
<fieldset>
<legend>Exportar datos</legend>
<em>Al pulsar, Espera hasta que salga una notificacion verde.</em>
<br />
<br />
<button id="${button_export_local}" type="button">Exportar sin cifrar</button>
<button id="${button_export_safe}" type="button">Exportar con cifrado</button>
</fieldset>
`;
document.getElementById(button_export_local).onclick = () => {
var data_export = {};
var output = {
materiales: {},
personas: {},
};
(async () => {
const materiales = await DB.list('materiales');
materiales.forEach((entry) => {
const key = entry.id;
const value = entry.data;
if (value != null) {
if (typeof value == 'string') {
TS_decrypt(
value,
SECRET,
(data, wasEncrypted) => {
output.materiales[key] = data;
},
'materiales',
key
);
} else {
output.materiales[key] = value;
}
}
});
const personas = await DB.list('personas');
personas.forEach((entry) => {
const key = entry.id;
const value = entry.data;
if (value != null) {
if (typeof value == 'string') {
TS_decrypt(
value,
SECRET,
(data, wasEncrypted) => {
output.personas[key] = data;
},
'personas',
key
);
} else {
output.personas[key] = value;
}
}
});
toastr.success('Exportado todo, descargando!');
download(`Export %%TITLE%% ${getDBName()}.json.txt`, JSON.stringify(output));
})();
};
document.getElementById(button_export_safe).onclick = () => {
(async () => {
const result = { materiales: {}, personas: {} };
const materiales = await DB.list('materiales');
materiales.forEach((entry) => {
result.materiales[entry.id] = entry.data;
});
const personas = await DB.list('personas');
personas.forEach((entry) => {
result.personas[entry.id] = entry.data;
});
toastr.success('Exportado todo, descargado!');
download(`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`, JSON.stringify(result));
})();
};
},
__import: function () {
var select_type = safeuuid();
var textarea_content = safeuuid();
var button_import = safeuuid();
var button_clear = safeuuid();
container.innerHTML = html`
<h1>Importar Datos</h1>
<fieldset>
<legend>Importar datos</legend>
<em>Espera hasta que se vacien todas las notificaciones.</em>
<select id="${select_type}">
<option value="" disabled selected>Tipo de archivo...</option>
<option value="comedor">Galileo - db.comedor.axd</option>
<option value="recetas">Galileo - db.recetas.axd</option>
<option value="materiales">Galileo - db.materiales.axd</option>
<option value="personas">Galileo - db.personas.axd</option>
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
</select>
<textarea
id="${textarea_content}"
style="height: 100px;"
placeholder="Introduce el contenido del archivo"
></textarea>
<button id="${button_import}" type="button">Importar</button>
<button id="${button_clear}" type="button">Vaciar</button>
</fieldset>
`;
document.getElementById(button_import).onclick = () => {
toastr.info('Importando datos...');
var val = document.getElementById(textarea_content).value;
var sel = document.getElementById(select_type).value;
if (sel == '%telesec') {
// legacy import, store entire payload as-is
// for each top-level key, store their items in DB
var parsed = JSON.parse(val);
Object.entries(parsed).forEach((section) => {
const sectionName = section[0];
const sectionData = section[1];
Object.entries(sectionData).forEach((entry) => {
DB.put(sectionName, entry[0], entry[1]).catch((e) => {
console.warn('DB.put error', e);
});
});
});
} else {
Object.entries(JSON.parse(val)['data']).forEach((entry) => {
DB.put(sel, entry[0], entry[1]).catch((e) => {
console.warn('DB.put error', e);
});
});
}
setTimeout(() => {
toastr.info('Importado todo!');
if (sel == '%telesec') {
setUrlHash('inicio');
} else {
setUrlHash(sel);
}
}, 5000);
};
},
__labels: function (mid) {
var div_materiales = safeuuid();
container.innerHTML = html` <h1>Imprimir Etiquetas QR</h1>
<button onclick="print()">Imprimir</button>
<h2>Materiales</h2>
<div id="${div_materiales}"></div>
<br /><br />`;
div_materiales = document.getElementById(div_materiales);
DB.map('materiales', (data, key) => {
function add_row(data, key) {
if (data != null) {
div_materiales.innerHTML += BuildQR('materiales,' + key, data['Nombre'] || key);
}
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (data) => {
add_row(data, key);
});
} else {
add_row(data, key);
}
});
},
__precios: function () {
var form = safeuuid();
// Cargar precios actuales desde DB
DB.get('config', 'precios_cafe').then((raw) => {
TS_decrypt(raw, SECRET, (precios) => {
container.innerHTML = html`
<h1>Configuración de Precios del Café</h1>
<form id="${form}">
<fieldset>
<legend>Precios Base (en céntimos)</legend>
<label>
<b>Servicio base:</b>
<input type="number" name="servicio_base" value="${precios.servicio_base || 10}" min="0" step="1" />
céntimos
</label>
<br><br>
<label>
<b>Leche pequeña:</b>
<input type="number" name="leche_pequena" value="${precios.leche_pequena || 15}" min="0" step="1" />
céntimos
</label>
<br><br>
<label>
<b>Leche grande:</b>
<input type="number" name="leche_grande" value="${precios.leche_grande || 25}" min="0" step="1" />
céntimos
</label>
<br><br>
<label>
<b>Café:</b>
<input type="number" name="cafe" value="${precios.cafe || 25}" min="0" step="1" />
céntimos
</label>
<br><br>
<label>
<b>ColaCao:</b>
<input type="number" name="colacao" value="${precios.colacao || 25}" min="0" step="1" />
céntimos
</label>
</fieldset>
<br>
<button type="submit">💾 Guardar precios</button>
<button type="button" onclick="setUrlHash('dataman')">🔙 Volver</button>
</form>
`;
document.getElementById(form).onsubmit = (ev) => {
ev.preventDefault();
var formData = new FormData(document.getElementById(form));
var nuevosPrecios = {
servicio_base: parseInt(formData.get('servicio_base')) || 10,
leche_pequena: parseInt(formData.get('leche_pequena')) || 15,
leche_grande: parseInt(formData.get('leche_grande')) || 25,
cafe: parseInt(formData.get('cafe')) || 25,
colacao: parseInt(formData.get('colacao')) || 25,
};
DB.put('config', 'precios_cafe', nuevosPrecios).then(() => {
toastr.success('Precios guardados correctamente');
// Actualizar variable global
if (window.PRECIOS_CAFE) {
Object.assign(window.PRECIOS_CAFE, nuevosPrecios);
}
setTimeout(() => setUrlHash('dataman'), 1000);
}).catch((e) => {
toastr.error('Error al guardar precios: ' + e.message);
});
};
});
}).catch(() => {
// Si no hay precios guardados, usar valores por defecto
PAGES.dataman.__precios();
});
},
index: function () {
container.innerHTML = html`
<h1>Administración de datos</h1>
<a class="button" href="#dataman,import">Importar datos</a>
<a class="button" href="#dataman,export">Exportar datos</a>
<a class="button" href="#dataman,labels">Imprimir etiquetas</a>
<a class="button" href="#dataman,precios">⚙️ Precios del café</a>
<a class="button" href="#dataman,config">Ajustes</a>
`;
},
};

327
src/page/index.js Normal file
View File

@@ -0,0 +1,327 @@
PAGES.index = {
//navcss: "btn1",
Title: 'Inicio',
icon: 'static/appico/house.png',
index: function () {
var div_stats = safeuuid();
container.innerHTML = html`
<h1>¡Hola, ${SUB_LOGGED_IN_DETAILS.Nombre}!<br />Bienvenidx a %%TITLE%%</h1>
<h2>
Tienes ${parseFloat(SUB_LOGGED_IN_DETAILS.Monedero_Balance).toPrecision(2)} € en el
monedero.
</h2>
<details style="border: 2px solid black; padding: 15px; border-radius: 10px;">
<summary>Estadisticas</summary>
<div
id="${div_stats}"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; margin-bottom: 20px;"
></div>
</details>
<em>Utiliza el menú superior para abrir un modulo</em>
<br /><br />
<button class="btn1" onclick="ActualizarProgramaTeleSec()">Actualizar programa</button>
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
`;
if (checkRole('pagos')) {
var total_ingresos = safeuuid();
var total_gastos = safeuuid();
var balance_total = safeuuid();
var total_ingresos_srcel = html`
<div
style="background: linear-gradient(135deg, #2ed573, #26d063); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">Pagos</span><br>
<h3 style="margin: 0;">Total Ingresos</h3>
<div id="${total_ingresos}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0.00€
</div>
</div>
`;
var total_gastos_srcel = html`
<div
style="background: linear-gradient(135deg, #ff4757, #ff3838); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">Pagos</span><br>
<h3 style="margin: 0;">Total Gastos</h3>
<div id="${total_gastos}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0.00€
</div>
</div>
`;
var balance_total_srcel = html`
<div
style="background: linear-gradient(135deg, #667eea, #764ba2); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">Pagos</span><br>
<h3 style="margin: 0;">Balance Total</h3>
<div id="${balance_total}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0.00€
</div>
</div>
`;
document.getElementById(div_stats).appendChild(createElementFromHTML(total_ingresos_srcel));
document.getElementById(div_stats).appendChild(createElementFromHTML(total_gastos_srcel));
document.getElementById(div_stats).appendChild(createElementFromHTML(balance_total_srcel));
let totalData = {
ingresos: {},
gastos: {},
};
EventListeners.DB.push(
DB.map('pagos', (data, key) => {
function applyData(row) {
if (!row || typeof row !== 'object') {
delete totalData.ingresos[key];
delete totalData.gastos[key];
} else {
const monto = parseFloat(row.Monto || 0) || 0;
const tipo = row.Tipo;
if (tipo === 'Ingreso') {
if (row.Origen != 'Promo Bono') {
totalData.gastos[key] = 0;
totalData.ingresos[key] = monto;
}
} else if (tipo === 'Gasto') {
totalData.ingresos[key] = 0;
totalData.gastos[key] = monto;
} else {
totalData.ingresos[key] = 0;
totalData.gastos[key] = 0;
}
}
const totalIngresos = Object.values(totalData.ingresos).reduce((a, b) => a + b, 0);
const totalGastos = Object.values(totalData.gastos).reduce((a, b) => a + b, 0);
document.getElementById(total_ingresos).innerText = totalIngresos.toFixed(2) + '€';
document.getElementById(total_gastos).innerText = totalGastos.toFixed(2) + '€';
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (decoded) => {
applyData(decoded);
});
} else {
applyData(data);
}
})
);
EventListeners.Interval.push(
setInterval(() => {
var balanceReal = 0;
Object.values(SC_Personas).forEach((persona) => {
balanceReal += parseFloat(persona.Monedero_Balance || 0);
});
document.getElementById(balance_total).innerText = balanceReal.toFixed(2) + '€';
document.getElementById(balance_total).style.color =
balanceReal >= 0 ? 'white' : '#ffcccc';
}, 1000)
);
}
if (checkRole('mensajes')) {
var mensajes_sin_leer = safeuuid();
var mensajes_sin_leer_srcel = html`
<div
style="background: linear-gradient(135deg, #66380d, #a5570d); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">Mensajes</span><br>
<h3 style="margin: 0;">Sin leer</h3>
<div id="${mensajes_sin_leer}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0
</div>
</div>
`;
document.getElementById(div_stats).appendChild(createElementFromHTML(mensajes_sin_leer_srcel));
var unreadById = {};
EventListeners.DB.push(
DB.map('mensajes', (data, key) => {
function applyUnread(row) {
if (!row || typeof row !== 'object') {
delete unreadById[key];
} else {
var estado = String(row.Estado || '').trim().toLowerCase();
var isRead = estado === 'leido' || estado === 'leído';
unreadById[key] = isRead ? 0 : 1;
}
var totalUnread = Object.values(unreadById).reduce((a, b) => a + b, 0);
document.getElementById(mensajes_sin_leer).innerText = String(totalUnread);
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (decoded) => {
applyUnread(decoded);
});
} else {
applyUnread(data);
}
})
);
}
if (checkRole('supercafe')) {
var comandas_en_deuda = safeuuid();
var comandas_en_deuda_srcel = html`
<div
style="background: linear-gradient(135deg, #8e44ad, #6c3483); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">SuperCafé</span><br>
<h3 style="margin: 0;">Comandas en deuda</h3>
<div id="${comandas_en_deuda}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0
</div>
</div>
`;
document.getElementById(div_stats).appendChild(createElementFromHTML(comandas_en_deuda_srcel));
var deudaById = {};
EventListeners.DB.push(
DB.map('supercafe', (data, key) => {
function applyDeuda(row) {
if (!row || typeof row !== 'object') {
delete deudaById[key];
} else {
var estado = String(row.Estado || '').trim().toLowerCase();
deudaById[key] = estado === 'deuda' ? 1 : 0;
}
var totalDeuda = Object.values(deudaById).reduce((a, b) => a + b, 0);
document.getElementById(comandas_en_deuda).innerText = String(totalDeuda);
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (decoded) => {
applyDeuda(decoded);
});
} else {
applyDeuda(data);
}
})
);
}
if (checkRole('materiales')) {
var materiales_comprar = safeuuid();
var materiales_revisar = safeuuid();
var materiales_comprar_srcel = html`
<div
style="background: linear-gradient(135deg, #e67e22, #d35400); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">Almacén</span><br>
<h3 style="margin: 0;">Por comprar</h3>
<div id="${materiales_comprar}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0
</div>
</div>
`;
var materiales_revisar_srcel = html`
<div
style="background: linear-gradient(135deg, #2980b9, #1f6391); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
>
<span style="font-size: 16px;">Almacén</span><br>
<h3 style="margin: 0;">Por revisar</h3>
<div id="${materiales_revisar}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
0
</div>
</div>
`;
document.getElementById(div_stats).appendChild(createElementFromHTML(materiales_comprar_srcel));
document.getElementById(div_stats).appendChild(createElementFromHTML(materiales_revisar_srcel));
var comprarById = {};
var revisarById = {};
EventListeners.DB.push(
DB.map('materiales', (data, key) => {
function applyMaterialStats(row) {
if (!row || typeof row !== 'object') {
delete comprarById[key];
delete revisarById[key];
} else {
var cantidad = parseFloat(row.Cantidad);
var cantidadMinima = parseFloat(row.Cantidad_Minima);
var lowStock = !isNaN(cantidad) && !isNaN(cantidadMinima) && cantidad < cantidadMinima;
comprarById[key] = lowStock ? 1 : 0;
var revision = String(row.Revision || '?').trim();
var needsReview = false;
if (revision === '?' || revision === '' || revision === '-') {
needsReview = true;
} else {
var match = revision.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
needsReview = true;
} else {
var y = parseInt(match[1], 10);
var m = parseInt(match[2], 10) - 1;
var d = parseInt(match[3], 10);
var revisionMs = Date.UTC(y, m, d);
var now = new Date();
var todayMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
var diffDays = Math.floor((todayMs - revisionMs) / 86400000);
needsReview = diffDays >= 90;
}
}
revisarById[key] = needsReview ? 1 : 0;
}
var totalComprar = Object.values(comprarById).reduce((a, b) => a + b, 0);
var totalRevisar = Object.values(revisarById).reduce((a, b) => a + b, 0);
document.getElementById(materiales_comprar).innerText = String(totalComprar);
document.getElementById(materiales_revisar).innerText = String(totalRevisar);
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (decoded) => {
applyMaterialStats(decoded);
});
} else {
applyMaterialStats(data);
}
})
);
}
},
edit: function (mid) {
switch (mid) {
case 'qr':
PAGES.index.__scan();
break;
}
},
__scan: function (mid) {
var qrscan = safeuuid();
container.innerHTML = html` <h1>Escanear Codigo QR</h1>
<div style="max-width: 400px;" id="${qrscan}"></div>
<br /><br />`;
var html5QrcodeScanner = new Html5QrcodeScanner(qrscan, { fps: 10, qrbox: 250 });
function onScanSuccess(decodedText, decodedResult) {
html5QrcodeScanner.clear();
// Handle on success condition with the decoded text or result.
// alert(`Scan result: ${decodedText}`, decodedResult);
setUrlHash(decodedText);
// ...
// ^ this will stop the scanner (video feed) and clear the scan area.
}
html5QrcodeScanner.render(onScanSuccess);
EventListeners.QRScanner.push(html5QrcodeScanner);
},
};

461
src/page/login.js Normal file
View File

@@ -0,0 +1,461 @@
function makeCouchURLDisplay(host, user, pass, dbname) {
if (!host) return '';
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
return display;
}
PAGES.login = {
Esconder: true,
Title: 'Login',
onboarding: function (step) {
// Multi-step onboarding flow
step = step || 'config';
if (step === 'config') {
// Step 1: "Configuración de datos"
var field_couch = safeuuid();
var field_secret = safeuuid();
var btn_existing_server = safeuuid();
var btn_new_server = safeuuid();
var btn_skip = safeuuid();
var div_server_config = safeuuid();
container.innerHTML = html`
<h1>¡Bienvenido a TeleSec! 🎉</h1>
<h2>Paso 1: Configuración de datos</h2>
<p>Para comenzar, elige cómo quieres configurar tu base de datos:</p>
<fieldset>
<button id="${btn_existing_server}" class="btn5">Conectar a CouchDB existente</button>
<button id="${btn_new_server}" class="btn2">Solicitar un nuevo CouchDB</button>
<button id="${btn_skip}" class="btn3">No sincronizar (no recomendado)</button>
</fieldset>
<div id="${div_server_config}" style="display:none;margin-top:20px;">
<h3>Configuración del servidor CouchDB</h3>
<fieldset>
<label
>Origen CouchDB (ej: usuario:contraseña@servidor/basededatos)
<input
type="text"
id="${field_couch}"
value="${makeCouchURLDisplay(
localStorage.getItem('TELESEC_COUCH_URL'),
localStorage.getItem('TELESEC_COUCH_USER'),
localStorage.getItem('TELESEC_COUCH_PASS'),
localStorage.getItem('TELESEC_COUCH_DBNAME')
)}"
/><br /><br />
</label>
<label
>Clave de encriptación <span style="color: red;">*</span>
<input
type="password"
id="${field_secret}"
value="${localStorage.getItem('TELESEC_SECRET') || ''}"
required
/><br /><br />
</label>
<button id="${btn_skip}-save" class="btn5">Guardar y Continuar</button>
</fieldset>
</div>
`;
document.getElementById(btn_existing_server).onclick = () => {
document.getElementById(div_server_config).style.display = 'block';
};
document.getElementById(btn_new_server).onclick = () => {
window.open('https://tech.eus/telesec-signup.php', '_blank');
toastr.info(
'Una vez creado el servidor, vuelve aquí y conéctate usando el botón "Conectar a un servidor existente"'
);
};
document.getElementById(btn_skip).onclick = () => {
// Continue to persona creation without server config
// Check if personas already exist (shouldn't happen but safety check)
var hasPersonas = Object.keys(SC_Personas).length > 0;
if (hasPersonas) {
toastr.info('Ya existen personas. Saltando creación de cuenta.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
} else {
open_page('login,onboarding-persona');
setUrlHash('login,onboarding-persona');
}
};
document.getElementById(btn_skip + '-save').onclick = () => {
var url = document.getElementById(field_couch).value.trim();
var secret = document.getElementById(field_secret).value.trim();
if (!url) {
toastr.error('Por favor ingresa un servidor CouchDB');
return;
}
if (!secret) {
toastr.error('La clave de encriptación es obligatoria');
return;
}
// Normalize URL: add https:// if no protocol specified
var normalizedUrl = url;
if (!/^https?:\/\//i.test(url)) {
normalizedUrl = 'https://' + url;
}
var URL_PARSED = parseURL(normalizedUrl);
var user = URL_PARSED.username || '';
var pass = URL_PARSED.password || '';
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
var host = URL_PARSED.hostname || normalizedUrl;
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
DB.init({
secret: SECRET,
remoteServer: 'https://' + host,
username: user,
password: pass,
dbname: dbname || undefined,
});
toastr.success('Servidor configurado correctamente');
document.getElementById('loading').style.display = 'block';
function waitForReplicationIdle(maxWaitMs, idleMs) {
var startTime = Date.now();
var lastSeenSync = window.TELESEC_LAST_SYNC || 0;
return new Promise((resolve) => {
var interval = setInterval(() => {
var now = Date.now();
var currentSync = window.TELESEC_LAST_SYNC || 0;
if (currentSync > lastSeenSync) {
lastSeenSync = currentSync;
}
var lastActivity = Math.max(lastSeenSync, startTime);
var idleLongEnough = now - lastActivity >= idleMs;
var timedOut = now - startTime >= maxWaitMs;
if (idleLongEnough || timedOut) {
clearInterval(interval);
resolve();
}
}, 250);
});
}
// Wait until replication goes idle or timeout
waitForReplicationIdle(10000, 2500).then(() => {
// Check if personas were replicated from server
var hasPersonas = Object.keys(SC_Personas).length > 0;
document.getElementById('loading').style.display = 'none';
if (hasPersonas) {
// Personas found from server, skip persona creation step
toastr.info('Se encontraron personas en el servidor. Saltando creación de cuenta.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
} else {
// No personas found, continue to persona creation
open_page('login,onboarding-persona');
setUrlHash('login,onboarding-persona');
}
});
} catch (e) {
document.getElementById('loading').style.display = 'none';
toastr.error('Error al configurar el servidor: ' + (e.message || e));
}
};
} else if (step === 'persona') {
// Step 2: "Crea una persona"
var field_nombre = safeuuid();
var btn_crear = safeuuid();
// Check if personas already exist
var hasPersonas = Object.keys(SC_Personas).length > 0;
if (hasPersonas) {
toastr.info('Se detectaron personas existentes. Redirigiendo al login.');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
open_page('login');
setUrlHash('login');
return;
}
container.innerHTML = html`
<h1>¡Bienvenido a TeleSec! 🎉</h1>
<h2>Paso 2: Crea tu cuenta de administrador</h2>
<p>Para continuar, necesitas crear una cuenta personal con permisos de administrador.</p>
<fieldset>
<label
>Tu nombre:
<input
type="text"
id="${field_nombre}"
placeholder="Ej: Juan Pérez"
autofocus
/><br /><br />
</label>
<p>
<small
> Esta cuenta tendrá todos los permisos de administrador y podrás gestionar la
aplicación completamente.</small
>
</p>
<button id="${btn_crear}" class="btn5">Crear cuenta y empezar</button>
</fieldset>
`;
document.getElementById(btn_crear).onclick = () => {
var nombre = document.getElementById(field_nombre).value.trim();
if (!nombre) {
toastr.error('Por favor ingresa tu nombre');
return;
}
// Disable button to prevent duplicate creation
var btnElement = document.getElementById(btn_crear);
btnElement.disabled = true;
btnElement.style.opacity = '0.5';
btnElement.innerText = 'Creando...';
// Create persona with all admin permissions from PERMS object
var allPerms = Object.keys(PERMS).join(',') + ',';
var personaId = safeuuid('admin-');
var persona = {
Nombre: nombre,
Roles: allPerms,
Region: '',
Monedero_Balance: 0,
markdown: 'Cuenta de administrador creada durante el onboarding',
};
DB.put('personas', personaId, persona)
.then(() => {
toastr.success('¡Cuenta creada exitosamente! 🎉');
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
localStorage.setItem('TELESEC_ADMIN_ID', personaId);
// Auto-login
SUB_LOGGED_IN_ID = personaId;
SUB_LOGGED_IN_DETAILS = persona;
SUB_LOGGED_IN = true;
SetPages();
setTimeout(() => {
open_page('index');
setUrlHash('index');
}, 500);
})
.catch((e) => {
toastr.error('Error creando cuenta: ' + (e.message || e));
// Re-enable button on error
btnElement.disabled = false;
btnElement.style.opacity = '1';
btnElement.innerText = 'Crear cuenta y empezar';
});
};
}
},
edit: function (mid) {
// Handle onboarding routes
if (mid === 'onboarding-config') {
PAGES.login.onboarding('config');
return;
}
if (mid === 'onboarding-persona') {
PAGES.login.onboarding('persona');
return;
}
// Setup form to configure CouchDB remote and initial group/secret
var field_couch = safeuuid();
var field_secret = safeuuid();
var btn_save = safeuuid();
container.innerHTML = html`
<h1>Configuración del servidor CouchDB</h1>
<b
>Aviso: Después de guardar, la aplicación intentará sincronizar con el servidor CouchDB en
segundo plano. Puede que falten registros hasta que se termine. Tenga paciencia.</b
>
<fieldset>
<label
>Origen CouchDB (ej: usuario:contraseña@servidor/basededatos)
<input
type="text"
id="${field_couch}"
value="${makeCouchURLDisplay(
localStorage.getItem('TELESEC_COUCH_URL'),
localStorage.getItem('TELESEC_COUCH_USER'),
localStorage.getItem('TELESEC_COUCH_PASS'),
localStorage.getItem('TELESEC_COUCH_DBNAME')
)}"
/><br /><br />
</label>
<label
>Clave de encriptación (opcional) - usada para cifrar datos en reposo
<input
type="password"
id="${field_secret}"
value="${localStorage.getItem('TELESEC_SECRET') || ''}"
/><br /><br />
</label>
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
<button onclick="setUrlHash('login');" class="btn3">Cancelar</button>
</fieldset>
<p>
Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.
</p>
`;
// Helper: normalize and apply config object
function applyConfig(cfg) {
try {
if (!cfg) throw new Error('JSON vacío');
var url = cfg.server || cfg.couch || cfg.url || cfg.host || cfg.hostname || cfg.server_url;
var dbname = cfg.dbname || cfg.database || cfg.db || cfg.name;
var user = cfg.username || cfg.user || cfg.u;
var pass = cfg.password || cfg.pass || cfg.p;
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
if (!url) throw new Error('Falta campo "server" en JSON');
var URL_PARSED = parseURL(url);
var host = URL_PARSED.hostname || url;
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
if (dbname) localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
if (secret) {
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
}
DB.init({
secret: SECRET,
remoteServer: 'https://' + url.replace(/^https?:\/\//, ''),
username: user,
password: pass,
dbname: dbname || undefined,
});
toastr.success('Configuración aplicada e iniciando sincronización');
location.hash = '#login';
setTimeout(function () {
location.reload();
}, 400);
} catch (e) {
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
}
}
document.getElementById(btn_save).onclick = () => {
var url = document.getElementById(field_couch).value.trim();
var secret = document.getElementById(field_secret).value.trim();
var URL_PARSED = parseURL(url);
var host = URL_PARSED.hostname || url;
var user = URL_PARSED.username || '';
var pass = URL_PARSED.password || '';
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
DB.init({
secret: SECRET,
remoteServer: 'https://' + host,
username: user,
password: pass,
dbname: dbname || undefined,
});
toastr.success('Iniciando sincronización con CouchDB');
location.hash = '#login';
//location.reload();
} catch (e) {
toastr.error('Error al iniciar sincronización: ' + e.message);
}
};
},
index: function (mid) {
// Check if onboarding is needed
var onboardingComplete = localStorage.getItem('TELESEC_ONBOARDING_COMPLETE');
var hasPersonas = Object.keys(SC_Personas).length > 0;
// If no personas exist and onboarding not complete, redirect to onboarding
if (!hasPersonas && !onboardingComplete && !AC_BYPASS) {
open_page('login,onboarding-config');
setUrlHash('login,onboarding-config');
return;
}
var field_persona = safeuuid();
var btn_guardar = safeuuid();
var btn_reload = safeuuid();
var div_actions = safeuuid();
container.innerHTML = html`
<h1>Iniciar sesión</h1>
<fieldset>
<legend>Valores</legend>
<input type="hidden" id="${field_persona}" />
<div id="${div_actions}"></div>
<button class="btn5" id="${btn_guardar}">Acceder</button>
<button class="btn3" id="${btn_reload}">Recargar lista</button>
<a class="button btn1" href="#login,setup">Configurar base de datos</a>
</fieldset>
`;
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
'',
(value) => {
document.getElementById(field_persona).value = value;
},
'¿Quién eres?',
true,
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
);
document.getElementById(btn_guardar).onclick = () => {
if (document.getElementById(field_persona).value == '') {
alert('Tienes que elegir tu cuenta!');
return;
}
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value;
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
SUB_LOGGED_IN = true;
SetPages();
if (location.hash.replace('#', '').split("?")[0].startsWith('login')) {
open_page('index');
setUrlHash('index');
} else {
open_page(location.hash.replace('#', '').split("?")[0]);
}
};
document.getElementById(btn_reload).onclick = () => {
open_page('login');
};
// AC_BYPASS: allow creating a local persona from the login screen
if (AC_BYPASS) {
var btn_bypass_create = safeuuid();
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
document.getElementById(btn_bypass_create).onclick = () => {
var name = prompt('Nombre de la persona (ej: Admin):');
if (!name) return;
var id = 'bypass-' + Date.now();
var persona = { Nombre: name, Roles: 'ADMIN,' };
DB.put('personas', id, persona)
.then(() => {
toastr.success('Persona creada: ' + id);
localStorage.setItem('TELESEC_BYPASS_ID', id);
SUB_LOGGED_IN_ID = id;
SUB_LOGGED_IN_DETAILS = persona;
SUB_LOGGED_IN = true;
SetPages();
open_page('index');
})
.catch((e) => {
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
});
};
}
},
};

652
src/page/materiales.js Normal file
View File

@@ -0,0 +1,652 @@
PERMS['materiales'] = 'Almacén';
PERMS['materiales:edit'] = '&gt; Editar';
PAGES.materiales = {
navcss: 'btn2',
icon: 'static/appico/shelf.png',
AccessControl: true,
Title: 'Almacén',
edit: function (mid) {
if (!checkRole('materiales:edit')) {
setUrlHash('materiales');
return;
}
var nameh1 = safeuuid();
var field_nombre = safeuuid();
var field_revision = safeuuid();
var field_cantidad = safeuuid();
var field_unidad = safeuuid();
var field_cantidad_min = safeuuid();
var field_ubicacion = safeuuid();
var field_notas = safeuuid();
var mov_tipo = safeuuid();
var mov_cantidad = safeuuid();
var mov_nota = safeuuid();
var mov_btn = safeuuid();
var mov_registro = safeuuid();
var mov_chart = safeuuid();
var mov_chart_canvas = safeuuid();
var mov_open_modal_btn = safeuuid();
var btn_print_chart = safeuuid();
var mov_modal = safeuuid();
var mov_modal_close = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var FECHA_ISO = new Date().toISOString().split('T')[0];
var movimientos = [];
var movimientosChartInstance = null;
function parseNum(v, fallback = 0) {
var n = parseFloat(v);
return Number.isFinite(n) ? n : fallback;
}
function buildMaterialData() {
return {
Nombre: document.getElementById(field_nombre).value,
Unidad: document.getElementById(field_unidad).value,
Cantidad: document.getElementById(field_cantidad).value,
Cantidad_Minima: document.getElementById(field_cantidad_min).value,
Ubicacion: document.getElementById(field_ubicacion).value,
Revision: document.getElementById(field_revision).value,
Notas: document.getElementById(field_notas).value,
Movimientos: movimientos,
};
}
function renderMovimientos() {
var el = document.getElementById(mov_registro);
if (!el) return;
if (!movimientos.length) {
el.innerHTML = '<small>Sin movimientos registrados.</small>';
return;
}
var rows = movimientos
.map((mov) => {
var fecha = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
return html`<tr>
<td>${fecha}</td>
<td>${mov.Tipo || '-'}</td>
<td>${mov.Cantidad ?? '-'}</td>
<td>${mov.Antes ?? '-'}</td>
<td>${mov.Despues ?? '-'}</td>
<td>${mov.Nota || ''}</td>
</tr>`;
})
.join('');
el.innerHTML = html`
<table>
<thead>
<tr>
<th>Fecha</th>
<th>Tipo</th>
<th>Cantidad</th>
<th>Antes</th>
<th>Después</th>
<th>Nota</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
}
function renderMovimientosChart() {
var el = document.getElementById(mov_chart);
if (!el) return;
if (movimientosChartInstance) {
movimientosChartInstance.destroy();
movimientosChartInstance = null;
}
if (!movimientos.length) {
el.innerHTML = html`
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
<small>Sin datos para graficar.</small>
`;
return;
}
var ordered = [...movimientos].sort((a, b) => {
return new Date(a.Fecha || 0).getTime() - new Date(b.Fecha || 0).getTime();
});
var deltas = [];
var labelsShort = [];
ordered.forEach((mov) => {
var cantidad = parseNum(mov.Cantidad, 0);
var delta = 0;
if (mov.Tipo === 'Entrada') {
delta = cantidad;
} else if (mov.Tipo === 'Salida') {
delta = -cantidad;
} else {
var antes = parseNum(mov.Antes, 0);
var despues = parseNum(mov.Despues, antes);
delta = despues - antes;
}
deltas.push(Number(delta.toFixed(2)));
var fechaTxt = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
labelsShort.push(fechaTxt);
});
var currentStock = parseNum(document.getElementById(field_cantidad)?.value, 0);
var totalNeto = deltas.reduce((acc, n) => acc + n, 0);
var stockInicialInferido = currentStock - totalNeto;
if (ordered.length > 0 && Number.isFinite(parseNum(ordered[0].Antes, NaN))) {
stockInicialInferido = parseNum(ordered[0].Antes, stockInicialInferido);
}
var acumulado = stockInicialInferido;
var values = deltas.map((neto) => {
acumulado += neto;
return Number(acumulado.toFixed(2));
});
el.innerHTML = html`
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
<small style="display: block;margin-bottom: 6px;">Stock por fecha (cierre diario)</small>
<canvas id="${mov_chart_canvas}" style="width: 100%;height: 280px;"></canvas>
`;
if (typeof Chart === 'undefined') {
el.innerHTML += '<small>No se pudo cargar la librería de gráficos.</small>';
return;
}
var chartCanvasEl = document.getElementById(mov_chart_canvas);
if (!chartCanvasEl) return;
movimientosChartInstance = new Chart(chartCanvasEl, {
type: 'line',
data: {
labels: labelsShort,
datasets: [
{
label: 'Stock diario',
data: values,
borderColor: '#2d7ef7',
backgroundColor: 'rgba(45,126,247,0.16)',
fill: true,
tension: 0.25,
pointRadius: 3,
pointHoverRadius: 4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
display: false,
},
y: {
title: {
display: false,
text: 'Stock',
},
grace: '10%',
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
},
});
}
container.innerHTML = html`
<h1>Material <code id="${nameh1}"></code></h1>
<fieldset style="width: 100%;max-width: 980px;box-sizing: border-box;">
<div style="display: flex;flex-wrap: wrap;gap: 10px 16px;align-items: flex-end;">
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_revision}">Fecha Revisión</label>
<input type="date" id="${field_revision}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_nombre}">Nombre</label>
<input type="text" id="${field_nombre}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_ubicacion}">Ubicación</label>
<input
type="text"
id="${field_ubicacion}"
value="-"
list="${field_ubicacion}_list"
style="flex: 1;"
/>
<datalist id="${field_ubicacion}_list"></datalist>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_unidad}">Unidad</label>
<select id="${field_unidad}" style="flex: 1;">
<option value="unidad(es)">unidad(es)</option>
<option value="paquete(s)">paquete(s)</option>
<option value="caja(s)">caja(s)</option>
<option value="rollo(s)">rollo(s)</option>
<option value="bote(s)">bote(s)</option>
<option value="metro(s)">metro(s)</option>
<option value="litro(s)">litro(s)</option>
<option value="kg">kg</option>
</select>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_cantidad}">Cantidad Actual</label>
<input type="number" step="0.5" id="${field_cantidad}" style="flex: 1;" disabled />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
<label for="${field_cantidad_min}">Cantidad Minima</label>
<input type="number" step="0.5" id="${field_cantidad_min}" style="flex: 1;" />
</div>
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 220px;flex: 1 1 100%;">
Notas
<textarea id="${field_notas}"></textarea>
</label>
</div>
<div
id="${mov_modal}"
style="display: none;position: fixed;z-index: 9999;left: 0;top: 0;width: 100%;height: 100%;overflow: auto;background: rgba(0,0,0,0.45);"
>
<div
style="background: #fff;margin: 2vh auto;padding: 14px;border: 1px solid #888;width: min(960px, 96vw);max-height: 94vh;overflow: auto;border-radius: 8px;box-sizing: border-box;"
>
<div style="display: flex;justify-content: space-between;align-items: center;gap: 10px;">
<h3 style="margin: 0;">Realizar movimiento</h3>
<button type="button" id="${mov_modal_close}" class="rojo">Cerrar</button>
</div>
<div style="margin-top: 12px;display: flex;gap: 10px;align-items: flex-end;flex-wrap: wrap;">
<div style="display: flex;flex-wrap: wrap;gap: 10px 12px;align-items: flex-end;flex: 1 1 420px;">
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
<label for="${mov_tipo}">Tipo</label>
<select id="${mov_tipo}" style="flex: 1;">
<option value="Entrada">Entrada - Meter al almacen</option>
<option value="Salida">Salida - Sacar del almacen</option>
<option value="Ajuste">Ajuste - Existencias actuales</option>
</select>
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
<label for="${mov_cantidad}">Cantidad</label>
<input type="number" step="0.5" id="${mov_cantidad}" style="flex: 1;" />
</div>
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
<label for="${mov_nota}">Nota</label>
<input type="text" id="${mov_nota}" style="flex: 1;" placeholder="Motivo del movimiento" />
</div>
</div>
<div style="display: flex;justify-content: flex-end;flex: 1 1 120px;min-width: 120px;">
<button type="button" class="saveico" id="${mov_btn}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
</div>
</div>
<h4 style="margin: 14px 0 6px 0;">Registro de movimientos</h4>
<div id="${mov_registro}"></div>
</div>
</div>
<hr />
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
<button class="opicon" id="${mov_open_modal_btn}">
<img src="static/exchange.png" />
<br>Movimientos
</button>
<button class="opicon" onclick="setUrlHash('materiales')" style="float: right;"> <!-- Align to the right -->
<img src="static/exit.png" />
<br>Salir
</button>
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
<img src="static/printer2.png" />
<br>Imprimir
</button>
</fieldset>
<div id="${mov_chart}" style="max-width: 980px;width: 100%;margin-top: 14px;min-height: 260px;height: min(400px, 52vh);"></div>
`;
// Cargar ubicaciones existentes para autocompletar
DB.map('materiales', (data) => {
if (!data) return;
function addUbicacion(d) {
const ubicacion = d.Ubicacion || '-';
const datalist = document.getElementById(`${field_ubicacion}_list`);
if (!datalist) {
console.warn(`Element with ID "${field_ubicacion}_list" not found.`);
return;
}
const optionExists = Array.from(datalist.options).some((opt) => opt.value === ubicacion);
if (!optionExists) {
const option = document.createElement('option');
option.value = ubicacion;
datalist.appendChild(option);
}
}
if (typeof data === 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
if (data && typeof data === 'object') {
addUbicacion(data);
}
},
'materiales',
mid
);
} else {
addUbicacion(data);
}
});
// Cargar datos del material
DB.get('materiales', mid).then((data) => {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_nombre).value = data['Nombre'] || '';
document.getElementById(field_unidad).value = data['Unidad'] || 'unidad(es)';
document.getElementById(field_cantidad).value = data['Cantidad'] || '';
document.getElementById(field_cantidad_min).value = data['Cantidad_Minima'] || '';
document.getElementById(field_ubicacion).value = data['Ubicacion'] || '-';
document.getElementById(field_revision).value = data['Revision'] || '-';
document.getElementById(field_notas).value = data['Notas'] || '';
movimientos = Array.isArray(data['Movimientos']) ? data['Movimientos'] : [];
renderMovimientos();
renderMovimientosChart();
}
if (typeof data == 'string') {
TS_decrypt(
data,
SECRET,
(data, wasEncrypted) => {
load_data(data, '%E');
},
'materiales',
mid
);
} else {
load_data(data || {});
}
});
document.getElementById(mov_open_modal_btn).onclick = () => {
document.getElementById(mov_modal).style.display = 'block';
renderMovimientos();
};
document.getElementById(mov_modal_close).onclick = () => {
document.getElementById(mov_modal).style.display = 'none';
};
document.getElementById(mov_modal).onclick = (evt) => {
if (evt.target.id === mov_modal) {
document.getElementById(mov_modal).style.display = 'none';
}
};
document.getElementById(mov_btn).onclick = () => {
var btn = document.getElementById(mov_btn);
if (btn.disabled) return;
var tipo = document.getElementById(mov_tipo).value;
var cantidadMov = parseNum(document.getElementById(mov_cantidad).value, NaN);
var nota = document.getElementById(mov_nota).value || '';
var actual = parseNum(document.getElementById(field_cantidad).value, 0);
if ((!Number.isFinite(cantidadMov) || cantidadMov <= 0) && tipo !== 'Ajuste') {
toastr.warning('Indica una cantidad válida para el movimiento');
return;
}
var nuevaCantidad = actual;
if (tipo === 'Entrada') {
nuevaCantidad = actual + cantidadMov;
} else if (tipo === 'Salida') {
nuevaCantidad = actual - cantidadMov;
} else if (tipo === 'Ajuste') {
nuevaCantidad = cantidadMov;
}
movimientos.unshift({
Fecha: new Date().toISOString(),
Tipo: tipo,
Cantidad: cantidadMov,
Antes: actual,
Despues: nuevaCantidad,
Nota: nota,
});
document.getElementById(field_cantidad).value = nuevaCantidad;
document.getElementById(field_revision).value = FECHA_ISO;
document.getElementById(mov_cantidad).value = '';
document.getElementById(mov_nota).value = '';
renderMovimientos();
renderMovimientosChart();
btn.disabled = true;
btn.style.opacity = '0.5';
document.getElementById('actionStatus').style.display = 'block';
DB.put('materiales', mid, buildMaterialData())
.then(() => {
toastr.success('Movimiento registrado');
})
.catch((e) => {
console.warn('DB.put error', e);
toastr.error('Error al guardar el movimiento');
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
});
};
document.getElementById(btn_guardar).onclick = () => {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = buildMaterialData();
document.getElementById('actionStatus').style.display = 'block';
DB.put('materiales', mid, data)
.then(() => {
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('materiales');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('DB.put error', e);
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
document.getElementById('actionStatus').style.display = 'none';
toastr.error('Error al guardar el material');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar este material?') == true) {
DB.del('materiales', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('materiales');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole('materiales')) {
setUrlHash('index');
return;
}
var btn_new = safeuuid();
var select_ubicacion = safeuuid();
var check_lowstock = safeuuid();
var tableContainer = safeuuid();
container.innerHTML = html`
<h1>Materiales del Almacén</h1>
<label>
<b>Solo lo que falta:</b>
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;" /> </label
><br />
<label
>Filtrar por ubicación:
<select id="${select_ubicacion}">
<option value="">(Todas)</option>
</select>
</label>
<button id="${btn_new}">Nuevo Material</button>
<div id="${tableContainer}"></div>
`;
const config = [
{ key: 'Revision', label: 'Ult. Revisión', type: 'fecha-diff', default: '' },
{ key: 'Nombre', label: 'Nombre', type: 'text', default: '' },
{ key: 'Ubicacion', label: 'Ubicación', type: 'text', default: '--' },
{
key: 'Cantidad',
label: 'Cantidad',
type: 'template',
template: (data, element) => {
const min = parseFloat(data.Cantidad_Minima);
const act = parseFloat(data.Cantidad);
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || '?'}</small>` : '';
element.innerHTML = html`${data.Cantidad || '?'} ${data.Unidad || '?'} ${sma}`;
},
default: '?',
},
{ key: 'Notas', label: 'Notas', type: 'text', default: '' },
];
// Obtener todas las ubicaciones únicas y poblar el <select>, desencriptando si es necesario
DB.map('materiales', (data, key) => {
try {
if (!data) return;
function addUbicacion(d) {
const ubicacion = d.Ubicacion || '-';
const select = document.getElementById(select_ubicacion);
if (!select) {
console.warn(`Element with ID "${select_ubicacion}" not found.`);
return;
}
const optionExists = Array.from(select.options).some((opt) => opt.value === ubicacion);
if (!optionExists) {
const option = document.createElement('option');
option.value = ubicacion;
option.textContent = ubicacion;
select.appendChild(option);
}
}
if (typeof data === 'string') {
TS_decrypt(
data,
SECRET,
(dec, wasEncrypted) => {
if (dec && typeof dec === 'object') {
addUbicacion(dec);
}
},
'materiales',
key
);
} else {
addUbicacion(data);
}
} catch (error) {
console.warn('Error processing ubicacion:', error);
}
});
// Función para renderizar la tabla filtrada
function renderTable(filtroUbicacion) {
TS_IndexElement(
'materiales',
config,
'materiales',
document.getElementById(tableContainer),
function (data, new_tr) {
if (parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima)) {
new_tr.style.background = '#fcfcb0';
}
if (parseFloat(data.Cantidad) <= 0) {
new_tr.style.background = '#ffc0c0';
}
if ((data.Cantidad || '?') == '?') {
new_tr.style.background = '#d0d0ff';
}
if ((data.Revision || '?') == '?') {
new_tr.style.background = '#d0d0ff';
}
},
function (data) {
var is_low_stock =
!document.getElementById(check_lowstock).checked ||
parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima);
var is_region = filtroUbicacion === '' || data.Ubicacion === filtroUbicacion;
return !(is_low_stock && is_region);
}
);
}
// Inicializar tabla sin filtro
renderTable('');
// Evento para filtrar por ubicación
document.getElementById(select_ubicacion).onchange = function () {
renderTable(this.value);
};
// Recargar al cambiar filtro
document.getElementById(check_lowstock).onchange = function () {
renderTable(document.getElementById(select_ubicacion).value);
};
if (!checkRole('materiales:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash('materiales,' + safeuuid(''));
};
}
},
};

312
src/page/notas.js Normal file
View File

@@ -0,0 +1,312 @@
PERMS['notas'] = 'Notas';
PERMS['notas:edit'] = '&gt; Editar';
PAGES.notas = {
navcss: 'btn5',
icon: 'static/appico/edit.png',
AccessControl: true,
Title: 'Notas',
edit: function (mid) {
if (!checkRole('notas:edit')) {
setUrlHash('notas');
return;
}
var nameh1 = safeuuid();
var field_asunto = safeuuid();
var field_contenido = safeuuid();
var field_autor = safeuuid();
var field_files = safeuuid();
var attachments_list = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = html`
<h1>Nota <code id="${nameh1}"></code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Valores</legend>
<div style="max-width: 400px;">
<label>
Asunto<br />
<input type="text" id="${field_asunto}" value="" /><br /><br />
</label>
<input type="hidden" id="${field_autor}" value="" />
<div id="${div_actions}"></div>
</div>
<label>
Contenido<br />
<textarea
id="${field_contenido}"
style="width: calc(100% - 15px); height: 400px;"
></textarea
><br /><br />
</label>
<label>
Adjuntos (Fotos o archivos)<br />
<input type="file" id="${field_files}" multiple /><br /><br />
<div id="${attachments_list}"></div>
</label>
<hr />
<button class="saveico" id="${btn_guardar}">
<img src="static/floppy_disk_green.png" />
<br>Guardar
</button>
<button class="delico" id="${btn_borrar}">
<img src="static/garbage.png" />
<br>Borrar
</button>
<button class="opicon" onclick="setUrlHash('notas')" style="float: right;"> <!-- Align to the right -->
<img src="static/exit.png" />
<br>Salir
</button>
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
<img src="static/printer2.png" />
<br>Imprimir
</button>
</fieldset>
`;
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
SUB_LOGGED_IN_ID,
(value) => {
document.getElementById(field_autor).value = value;
},
'Autor'
);
DB.get('notas', mid).then((data) => {
function load_data(data, ENC = '') {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_asunto).value = data['Asunto'] || '';
document.getElementById(field_contenido).value = data['Contenido'] || '';
document.getElementById(field_autor).value = data['Autor'] || SUB_LOGGED_IN_ID || '';
// Persona select
divact.innerHTML = '';
addCategory_Personas(
divact,
SC_Personas,
data['Autor'] || SUB_LOGGED_IN_ID || '',
(value) => {
document.getElementById(field_autor).value = value;
},
'Autor'
);
// Mostrar adjuntos existentes (si los hay).
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
const attachContainer = document.getElementById(attachments_list);
attachContainer.innerHTML = '';
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
DB.listAttachments('notas', mid)
.then((list) => {
if (!list || !Array.isArray(list)) return;
list.forEach((att) => {
addAttachmentRow(att.name, att.dataUrl);
});
})
.catch((e) => {
console.warn('listAttachments error', e);
});
}
if (typeof data == 'string') {
TS_decrypt(data, SECRET, (data) => {
load_data(data, '%E');
});
} else {
load_data(data || {});
}
});
// gestión de archivos seleccionados antes de guardar
const attachmentsToUpload = [];
function addAttachmentRow(name, url) {
const attachContainer = document.getElementById(attachments_list);
const idRow = safeuuid();
const isImage = url && url.indexOf('data:image') === 0;
const preview = isImage
? `<img src="${url}" height="80" style="margin-right:8px;">`
: `<a href="${url}" target="_blank">${name}</a>`;
const html = `
<div id="${idRow}" style="display:flex;align-items:center;margin:6px 0;border:1px solid #ddd;padding:6px;border-radius:6px;">
<div style="flex:1">${preview}<strong style="margin-left:8px">${name}</strong></div>
<div><button type="button" class="rojo" data-name="${name}">Borrar</button></div>
</div>`;
attachContainer.insertAdjacentHTML('beforeend', html);
attachContainer.querySelectorAll(`button[data-name="${name}"]`).forEach((btn) => {
btn.onclick = () => {
if (!confirm('¿Borrar este adjunto?')) return;
// Usar API pública en DB para borrar metadata del attachment
DB.deleteAttachment('notas', mid, name)
.then((ok) => {
if (ok) {
document.getElementById(idRow).remove();
toastr.error('Adjunto borrado');
} else {
toastr.error('No se pudo borrar el adjunto');
}
})
.catch((e) => {
console.warn('deleteAttachment error', e);
toastr.error('Error borrando adjunto');
});
};
});
}
document.getElementById(field_files).addEventListener('change', function (e) {
const files = Array.from(e.target.files || []);
files.forEach((file) => {
const reader = new FileReader();
reader.onload = function (ev) {
const dataUrl = ev.target.result;
attachmentsToUpload.push({
name: file.name,
data: dataUrl,
type: file.type || 'application/octet-stream',
});
// mostrar preview temporal
addAttachmentRow(file.name, dataUrl);
};
reader.readAsDataURL(file);
});
// limpiar input para permitir re-subidas del mismo archivo
e.target.value = '';
});
document.getElementById(btn_guardar).onclick = () => {
// Disable button to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
guardarBtn.disabled = true;
guardarBtn.style.opacity = '0.5';
var data = {
Autor: document.getElementById(field_autor).value,
Contenido: document.getElementById(field_contenido).value,
Asunto: document.getElementById(field_asunto).value,
};
document.getElementById('actionStatus').style.display = 'block';
DB.put('notas', mid, data)
.then(() => {
// subir attachments si los hay
const uploadPromises = [];
attachmentsToUpload.forEach((att) => {
if (DB.putAttachment) {
uploadPromises.push(
DB.putAttachment('notas', mid, att.name, att.data, att.type).catch((e) => {
console.warn('putAttachment error', e);
})
);
}
});
Promise.all(uploadPromises)
.then(() => {
// limpiar lista temporal y recargar attachments
attachmentsToUpload.length = 0;
try {
// recargar lista actual sin salir
const pouchId = 'notas:' + mid;
if (DB && DB._internal && DB._internal.local) {
DB._internal.local
.get(pouchId, { attachments: true })
.then((doc) => {
const attachContainer = document.getElementById(attachments_list);
attachContainer.innerHTML = '';
if (doc && doc._attachments) {
Object.keys(doc._attachments).forEach((name) => {
try {
const att = doc._attachments[name];
if (att && att.data) {
const durl =
'data:' +
(att.content_type || 'application/octet-stream') +
';base64,' +
att.data;
addAttachmentRow(name, durl);
return;
}
} catch (e) {}
DB.getAttachment('notas', mid, name)
.then((durl) => {
addAttachmentRow(name, durl);
})
.catch(() => {});
});
}
})
.catch(() => {
/* ignore reload errors */
});
}
} catch (e) {}
toastr.success('Guardado!');
setTimeout(() => {
document.getElementById('actionStatus').style.display = 'none';
setUrlHash('notas');
}, SAVE_WAIT);
})
.catch((e) => {
console.warn('Attachment upload error', e);
document.getElementById('actionStatus').style.display = 'none';
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
toastr.error('Error al guardar los adjuntos');
});
})
.catch((e) => {
console.warn('DB.put error', e);
document.getElementById('actionStatus').style.display = 'none';
guardarBtn.disabled = false;
guardarBtn.style.opacity = '1';
toastr.error('Error al guardar la nota');
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm('¿Quieres borrar esta nota?') == true) {
DB.del('notas', mid).then(() => {
toastr.error('Borrado!');
setTimeout(() => {
setUrlHash('notas');
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole('notas')) {
setUrlHash('index');
return;
}
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = html`
<h1>Notas</h1>
<button id="${btn_new}">Nueva nota</button>
<div id="cont"></div>
`;
TS_IndexElement(
'notas',
[
{
key: 'Autor',
type: 'persona-nombre',
default: '',
label: 'Autor',
},
{
key: 'Asunto',
type: 'raw',
default: '',
label: 'Asunto',
},
],
'notas',
document.querySelector('#cont')
);
if (!checkRole('notas:edit')) {
document.getElementById(btn_new).style.display = 'none';
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash('notas,' + safeuuid(''));
};
}
},
};

Some files were not shown because too many files have changed in this diff Show More