356 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e9e79e8d3a Address code review feedback: Add constants and improve path parsing
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-22 17:58:16 +00:00
copilot-swe-agent[bot]
c9125e5b75 Add remoteStorage.js support as alternative to PouchDB
Co-authored-by: naielv <109038805+naielv@users.noreply.github.com>
2026-01-22 17:55:05 +00:00
copilot-swe-agent[bot]
dadbfb56a4 Initial plan 2026-01-22 17:48:02 +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
78 changed files with 8670 additions and 2195 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}}'

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

@@ -0,0 +1,224 @@
# 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
│ │ ├── pouchdb (via CDN) # PouchDB is used for local storage and replication
│ ├── webrtc.js # WebRTC functionality
│ ├── 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

6
.gitignore vendored
View File

@@ -1 +1,5 @@
dist/*
dist/*
radata/*
node_modules/*
.DS_Store
._*

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

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"
}

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: 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

@@ -0,0 +1,431 @@
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 * {
display: none !important;
}
}
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;
}

View File

@@ -1,6 +1,6 @@
;(function(){
// assets/static/gun.js - Deprecated. Replaced by PouchDB (DB module).
console.warn('assets/static/gun.js is deprecated and unused.');
/* UNBUILD */
function USE(arg, req){
return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){
arg(mod = {exports: {}});
@@ -1775,7 +1775,7 @@
var wait = 2 * 999;
function reconnect(peer){
clearTimeout(peer.defer);
if(!opt.peers[peer.url]){ return }
//if(!opt.peers[peer.url]){ return }
if(doc && peer.retry <= 0){ return }
peer.retry = (peer.retry || opt.retry+1 || 60) - ((-peer.tried + (peer.tried = +new Date) < wait*4)?1:0);
peer.defer = setTimeout(function to(){

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

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 185 KiB

View File

@@ -1,4 +1,6 @@
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
// assets/static/open.js - Deprecated. Part of Gun library, not used after migration to PouchDB.
console.warn('assets/static/open.js is deprecated and unused.');
var Gun = (typeof window !== "undefined")? window.Gun || {} : {};
Gun.chain.open = function(cb, opt, at, depth){ // this is a recursive function, BEWARE!
depth = depth || 1;

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,6 +1,6 @@
;(function(){
// assets/static/sea.js - Deprecated. Replaced by CryptoJS AES usage in app_modules.js.
console.warn('assets/static/sea.js is deprecated and unused.');
/* UNBUILD */
function USE(arg, req){
return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){
arg(mod = {exports: {}});

View File

@@ -1,5 +1,7 @@
import json
import os
import shutil
import sys
def get_all_files(directory):
files = []
@@ -12,27 +14,36 @@ def get_all_files(directory):
return files
PREFETCH = ""
VERSIONCO = "2025-07-31_1"
VERSIONCO = "2025-08"
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%%", "TeleSec")
string = string.replace("%%HOSTER%%", HOSTER)
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
return string
for file in HANDLEPARSE:
print(file)
with open("src/" + file, "r") as f1:
with open("src/" + file, "r", encoding="utf-8") as f1:
out = replace_handles(f1.read())
with open("dist/" + file, "w") as f2:
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>

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
requests

View File

@@ -1,40 +1,26 @@
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();
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 (SUB_LOGGED_IN != true) {
// 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 = [];
if (SUB_LOGGED_IN != true && params != "login,setup") {
PAGES["login"].index();
return;
}
@@ -49,30 +35,24 @@ function open_page(params) {
}
PAGES[app].edit(path[1]);
}
function setUrlHash(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("#", ""));
};
function download(filename, text) {
var element = document.createElement("a");
element.setAttribute(
@@ -80,14 +60,15 @@ function download(filename, text) {
"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,
@@ -95,70 +76,94 @@ function resizeInputImage(
targetQuality = 0.75
) {
const reader = new FileReader();
reader.onload = function (event) {
reader.onload = function(event) {
const img = new Image();
img.onload = function () {
img.onload = function() {
const aspectRatio = img.width / img.height;
const targetWidth = targetHeight * aspectRatio;
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
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);
callback(dataURL);
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
function CurrentISODate() {
return new Date().toISOString().split("T")[0].replace("T", " ");
}
function CurrentISOTime() {
return new Date().toISOString();
}
function fixGunLocalStorage() {
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");
// 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;
}
});
setTimeout(() => {
ref.put(data);
}, 250);
setTimeout(() => {
ref.put(data);
}, 500);
}
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;
});
// 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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,65 @@
var EVENTLISTENER = null;
var EVENTLISTENER2 = null;
var EventListeners = {
GunJS: [],
Timeout: [],
Interval: [],
QRScanner: [],
Custom: [],
};
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 urlParams = new URLSearchParams(location.search);
if (urlParams.get("hidenav") == "yes") {
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";
}
var GROUPID = "";
// 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';
}
// 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",
];
// `TABLE` variable removed. The CouchDB database name should be configured via the login/setup form
// and passed to `DB.init({ dbname: '<your-db>' })` so it becomes the app's primary DB.
// Legacy relay list removed (migrated to CouchDB/PouchDB)
const RELAYS = [];
var SECRET = "";
var SUB_LOGGED_IN = false;
var SUB_LOGGED_IN_DETAILS = false;
var SUB_LOGGED_IN_ID = 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);
}
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();
}

619
src/db.js Normal file
View File

@@ -0,0 +1,619 @@
// TeleSec Database Layer - Supports both PouchDB and remoteStorage.js
// - Users can choose their preferred backend via login settings
// - Provides unified API: init, put, get, del, map, list, replicate
// - Stores records as docs with _id = "<table>:<id>" and field `data` containing either plain object or encrypted string
var DB = (function () {
// Constants
const ENCRYPTED_PREFIX = 'RSA{';
const ENCRYPTED_SUFFIX = '}';
let backend = null; // 'pouchdb' or 'remotestorage'
let callbacks = {}; // table -> [cb]
let docCache = {}; // _id -> last data snapshot (stringified)
// PouchDB-specific state
let pouchLocal = null;
let pouchRemote = null;
let pouchChanges = null;
let pouchRepPush = null;
let pouchRepPull = null;
// remoteStorage-specific state
let rsClient = null;
let rsModule = null;
// ==================== PouchDB Backend Implementation ====================
function ensurePouchLocal() {
if (pouchLocal) return;
try {
const localName = 'telesec';
pouchLocal = new PouchDB(localName);
if (pouchChanges) {
try { pouchChanges.cancel(); } catch (e) {}
}
pouchChanges = pouchLocal.changes({ live: true, since: 'now', include_docs: true }).on('change', onPouchChange);
} catch (e) {
console.warn('ensurePouchLocal error', e);
}
}
function onPouchChange(change) {
const doc = change.doc;
if (!doc || !doc._id) return;
const [table, id] = doc._id.split(':');
// handle deletes
if (change.deleted || doc._deleted) {
delete docCache[doc._id];
if (callbacks[table]) {
callbacks[table].forEach(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(cb => {
try { cb(doc.data, id); } catch (e) { console.error(e); }
});
}
}
async function initPouchDB(opts) {
const localName = 'telesec';
try {
if (opts && opts.secret) {
SECRET = opts.secret;
try { localStorage.setItem('TELESEC_SECRET', SECRET); } catch (e) {}
}
} catch (e) {}
pouchLocal = 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) pouchRemote = new PouchDB(remoteUrl.replace(/:\/\//, '://' + authPart));
else pouchRemote = new PouchDB(remoteUrl);
replicatePouchToRemote();
} catch (e) {
console.warn('PouchDB Remote init error', e);
}
}
if (pouchChanges) pouchChanges.cancel();
pouchChanges = pouchLocal.changes({ live: true, since: 'now', include_docs: true }).on('change', onPouchChange);
return Promise.resolve();
}
function replicatePouchToRemote() {
ensurePouchLocal();
if (!pouchLocal || !pouchRemote) return;
try { if (pouchRepPush && pouchRepPush.cancel) pouchRepPush.cancel(); } catch (e) {}
try { if (pouchRepPull && pouchRepPull.cancel) pouchRepPull.cancel(); } catch (e) {}
pouchRepPush = PouchDB.replicate(pouchLocal, pouchRemote, { live: true, retry: true })
.on('error', function (err) { console.warn('Replication push error', err); });
pouchRepPull = PouchDB.replicate(pouchRemote, pouchLocal, { live: true, retry: true })
.on('error', function (err) { console.warn('Replication pull error', err); });
}
async function pouchPut(table, id, data) {
ensurePouchLocal();
const _id = table + ':' + id;
try {
const existing = await pouchLocal.get(_id).catch(() => null);
if (data === null) {
if (existing) await pouchLocal.remove(existing);
return;
}
const doc = existing || { _id: _id };
var toStore = data;
try {
var isEncryptedString = (typeof data === 'string' && data.startsWith(ENCRYPTED_PREFIX) && data.endsWith(ENCRYPTED_SUFFIX));
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 pouchLocal.put(doc);
// manually trigger callbacks for local update
onPouchChange({ doc: doc });
} catch (e) {
console.error('PouchDB.put error', e);
}
}
async function pouchGet(table, id) {
ensurePouchLocal();
const _id = table + ':' + id;
try {
const doc = await pouchLocal.get(_id);
return doc.data;
} catch (e) { return null; }
}
async function pouchList(table) {
ensurePouchLocal();
try {
const res = await pouchLocal.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 pouchMap(table, cb) {
ensurePouchLocal();
callbacks[table] = callbacks[table] || [];
callbacks[table].push(cb);
pouchList(table).then(rows => rows.forEach(r => cb(r.data, r.id)));
return () => { callbacks[table] = callbacks[table].filter(x => x !== cb); }
}
// PouchDB Attachment methods
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 pouchPutAttachment(table, id, name, dataUrlOrBlob, contentType) {
ensurePouchLocal();
const _id = table + ':' + id;
try {
let doc = await pouchLocal.get(_id).catch(() => null);
if (!doc) {
await pouchLocal.put({ _id: _id, table: table, ts: new Date().toISOString(), data: {} });
doc = await pouchLocal.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 pouchLocal.putAttachment(_id, name, doc._rev, blob, type);
return true;
} catch (e) { console.error('putAttachment error', e); return false; }
}
async function pouchGetAttachment(table, id, name) {
ensurePouchLocal();
const _id = table + ':' + id;
try {
const blob = await pouchLocal.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 pouchListAttachments(table, id) {
ensurePouchLocal();
const _id = table + ':' + id;
try {
const doc = await pouchLocal.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 pouchGetAttachment(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 pouchLocal.get(_id).catch(() => null);
if (!doc || !doc._attachments) return [];
const out = [];
for (const name of Object.keys(doc._attachments)) {
try {
const durl = await pouchGetAttachment(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 pouchDeleteAttachment(table, id, name) {
ensurePouchLocal();
const _id = table + ':' + id;
try {
const doc = await pouchLocal.get(_id);
if (!doc || !doc._attachments || !doc._attachments[name]) return false;
delete doc._attachments[name];
await pouchLocal.put(doc);
return true;
} catch (e) { console.error('deleteAttachment error', e); return false; }
}
// ==================== remoteStorage Backend Implementation ====================
function initRemoteStorage(opts) {
try {
if (opts && opts.secret) {
SECRET = opts.secret;
try { localStorage.setItem('TELESEC_SECRET', SECRET); } catch (e) {}
}
// Initialize RemoteStorage client
rsClient = new RemoteStorage({ logging: opts.logging || false });
// Define TeleSec data module
rsClient.access.claim('telesec', 'rw');
rsClient.caching.enable('/telesec/');
// Define module to handle our data structure
rsModule = rsClient.scope('/telesec/');
// Listen for changes
rsModule.on('change', onRemoteStorageChange);
// Auto-connect if user address is provided
if (opts.rsUserAddress) {
rsClient.connect(opts.rsUserAddress, opts.rsToken || undefined).then(() => {
console.log('RemoteStorage connected successfully');
}).catch((err) => {
console.warn('RemoteStorage connection error', err);
});
}
return Promise.resolve();
} catch (e) {
console.error('RemoteStorage init error', e);
return Promise.reject(e);
}
}
function onRemoteStorageChange(event) {
try {
if (!event || !event.path) return;
// Parse path: /telesec/table/id
const pathParts = event.path.split('/').filter(p => p);
if (pathParts.length < 3 || pathParts[0] !== 'telesec') return;
const table = pathParts[1];
const id = pathParts[2];
const _id = table + ':' + id;
// Handle deletion
if (!event.newValue) {
delete docCache[_id];
if (callbacks[table]) {
callbacks[table].forEach(cb => {
try { cb(null, id); } catch (e) { console.error(e); }
});
}
return;
}
// Handle insert/update
try {
const data = event.newValue;
const now = typeof data === 'string' ? data : JSON.stringify(data);
const prev = docCache[_id];
if (prev === now) return; // no meaningful change
docCache[_id] = now;
if (callbacks[table]) {
callbacks[table].forEach(cb => {
try { cb(data, id); } catch (e) { console.error(e); }
});
}
} catch (e) {
console.error('RemoteStorage change handler error', e);
}
} catch (e) {
console.error('onRemoteStorageChange error', e);
}
}
async function rsPut(table, id, data) {
if (!rsModule) throw new Error('RemoteStorage not initialized');
try {
const path = table + '/' + id;
if (data === null) {
await rsModule.remove(path);
// Trigger change manually
onRemoteStorageChange({ path: '/telesec/' + path, newValue: null });
return;
}
var toStore = data;
try {
var isEncryptedString = (typeof data === 'string' && data.startsWith(ENCRYPTED_PREFIX) && data.endsWith(ENCRYPTED_SUFFIX));
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; }
await rsModule.storeObject('application/json', path, toStore);
// Trigger change manually for immediate UI update
onRemoteStorageChange({ path: '/telesec/' + path, newValue: toStore });
} catch (e) {
console.error('RemoteStorage.put error', e);
}
}
async function rsGet(table, id) {
if (!rsModule) return null;
try {
const path = table + '/' + id;
const data = await rsModule.getObject(path);
return data || null;
} catch (e) {
return null;
}
}
async function rsList(table) {
if (!rsModule) return [];
try {
const listing = await rsModule.getListing(table + '/');
if (!listing) return [];
const results = [];
for (const filename of Object.keys(listing)) {
// Remove trailing slash if present, otherwise use as-is
const id = filename.endsWith('/') ? filename.slice(0, -1) : filename;
try {
const data = await rsGet(table, id);
if (data !== null) {
results.push({ id: id, data: data });
const _id = table + ':' + id;
try { docCache[_id] = typeof data === 'string' ? data : JSON.stringify(data); } catch (e) {}
}
} catch (e) {
console.warn('Error loading item ' + id, e);
}
}
return results;
} catch (e) {
console.warn('RemoteStorage.list error', e);
return [];
}
}
function rsMap(table, cb) {
if (!rsModule) return () => {};
callbacks[table] = callbacks[table] || [];
callbacks[table].push(cb);
rsList(table).then(rows => rows.forEach(r => cb(r.data, r.id)));
return () => { callbacks[table] = callbacks[table].filter(x => x !== cb); }
}
// RemoteStorage doesn't have built-in attachment support, store as base64 strings
async function rsPutAttachment(table, id, name, dataUrlOrBlob, contentType) {
if (!rsModule) return false;
try {
let dataUrl = dataUrlOrBlob;
if (dataUrlOrBlob instanceof Blob) {
dataUrl = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.readAsDataURL(dataUrlOrBlob);
});
}
const path = table + '/' + id + '/attachments/' + name;
await rsModule.storeObject('application/json', path, { dataUrl: dataUrl, contentType: contentType });
return true;
} catch (e) {
console.error('RemoteStorage putAttachment error', e);
return false;
}
}
async function rsGetAttachment(table, id, name) {
if (!rsModule) return null;
try {
const path = table + '/' + id + '/attachments/' + name;
const obj = await rsModule.getObject(path);
return obj ? obj.dataUrl : null;
} catch (e) {
return null;
}
}
async function rsListAttachments(table, id) {
if (!rsModule) return [];
try {
const path = table + '/' + id + '/attachments/';
const listing = await rsModule.getListing(path);
if (!listing) return [];
const results = [];
for (const filename of Object.keys(listing)) {
// Remove trailing slash if present, otherwise use as-is
const name = filename.endsWith('/') ? filename.slice(0, -1) : filename;
try {
const att = await rsGetAttachment(table, id, name);
results.push({ name: name, dataUrl: att, content_type: null });
} catch (e) {
results.push({ name: name, dataUrl: null, content_type: null });
}
}
return results;
} catch (e) {
return [];
}
}
async function rsDeleteAttachment(table, id, name) {
if (!rsModule) return false;
try {
const path = table + '/' + id + '/attachments/' + name;
await rsModule.remove(path);
return true;
} catch (e) {
console.error('RemoteStorage deleteAttachment error', e);
return false;
}
}
// ==================== Unified Public API ====================
async function init(opts) {
backend = opts.backend || localStorage.getItem('TELESEC_BACKEND') || 'pouchdb';
// Save backend preference
try {
localStorage.setItem('TELESEC_BACKEND', backend);
} catch (e) {}
if (backend === 'remotestorage') {
return initRemoteStorage(opts);
} else {
return initPouchDB(opts);
}
}
async function put(table, id, data) {
if (backend === 'remotestorage') return rsPut(table, id, data);
else return pouchPut(table, id, data);
}
async function get(table, id) {
if (backend === 'remotestorage') return rsGet(table, id);
else return pouchGet(table, id);
}
async function del(table, id) {
return put(table, id, null);
}
async function list(table) {
if (backend === 'remotestorage') return rsList(table);
else return pouchList(table);
}
function map(table, cb) {
if (backend === 'remotestorage') return rsMap(table, cb);
else return pouchMap(table, cb);
}
function replicateToRemote() {
if (backend === 'pouchdb') replicatePouchToRemote();
// remoteStorage syncs automatically
}
async function putAttachment(table, id, name, dataUrlOrBlob, contentType) {
if (backend === 'remotestorage') return rsPutAttachment(table, id, name, dataUrlOrBlob, contentType);
else return pouchPutAttachment(table, id, name, dataUrlOrBlob, contentType);
}
async function getAttachment(table, id, name) {
if (backend === 'remotestorage') return rsGetAttachment(table, id, name);
else return pouchGetAttachment(table, id, name);
}
async function listAttachments(table, id) {
if (backend === 'remotestorage') return rsListAttachments(table, id);
else return pouchListAttachments(table, id);
}
async function deleteAttachment(table, id, name) {
if (backend === 'remotestorage') return rsDeleteAttachment(table, id, name);
else return pouchDeleteAttachment(table, id, name);
}
// Listen for online event (PouchDB only)
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', function () {
try { setTimeout(replicateToRemote, 1000); } catch (e) {}
});
}
return {
init,
put,
get,
del,
list,
map,
replicateToRemote,
listAttachments,
deleteAttachment,
putAttachment,
getAttachment,
getBackend: () => backend,
_internal: {
pouchLocal: () => pouchLocal,
rsClient: () => rsClient
}
};
})();
window.DB = DB;
// Auto-initialize DB on startup using saved settings (non-blocking)
(function autoInitDB() {
try {
const savedBackend = localStorage.getItem('TELESEC_BACKEND') || 'pouchdb';
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;
const rsUserAddress = localStorage.getItem('TELESEC_RS_USER') || '';
const rsToken = localStorage.getItem('TELESEC_RS_TOKEN') || '';
try { SECRET = localStorage.getItem('TELESEC_SECRET') || ''; } catch (e) { SECRET = ''; }
const opts = {
backend: savedBackend,
secret: SECRET,
remoteServer,
username,
password,
dbname,
rsUserAddress,
rsToken
};
DB.init(opts).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,89 +1,61 @@
<!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>-->
<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" />
<div class="ribbon-content">
<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>
<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="
<!-- 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>
<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;" />
<details class="supermesh-indicator">
<summary>
<b>Sincronización</b><br />
<br /><small id="peerPID" style="font-family: monospace">Estado: local</small>
</summary>
<ul id="peerList"></ul>
<i>Todos los datos están encriptados.</i>
</details>
<main id="container"></main>
<img id="actionStatus" src="static/ico/statusok.png" style="
z-index: 2048;
margin: 0px;
position: fixed;
@@ -93,62 +65,41 @@
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/login.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>
" />
<div id="snackbar">
Hay una nueva versión de %%TITLE%%.<br /><a id="reload">Pulsa aqui para actualizar.</a>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js" integrity="sha256-/H4YS+7aYb9kJ5OKhFYPUjSJdrtV6AeyJOtTkw6X72o=" crossorigin="anonymous"></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/jquery.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pouchdb@7.3.1/dist/pouchdb.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/remotestoragejs@2.0.0/release/remotestorage.min.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 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/resumen_diario.js"></script>
<script src="page/personas.js"></script>
<script src="page/supercafe.js"></script>
<!-- <script src="page/avisos.js"></script> -->
<script src="page/comedor.js"></script>
<script src="page/notas.js"></script>
<!-- <script src="page/chat.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%%"
}

416
src/page/aulas.js Normal file
View File

@@ -0,0 +1,416 @@
PERMS["aulas"] = "Aulas (Solo docentes!)";
PAGES.aulas = {
//navcss: "btn1",
Title: "Gest-Aula",
icon: "static/appico/components.png",
AccessControl: true,
index: function () {
if (!checkRole("aulas")) {
setUrlHash("index");
return;
}
var data_Comedor = safeuuid();
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
container.innerHTML = `
<h1>Gestión del Aula</h1>
<div>
<fieldset style="float: left;">
<legend><img src="${PAGES.notas.icon}" height="20"> Notas esenciales</legend>
<a class="button" style="font-size: 25px;" href="#notas,inicio_dia">Como iniciar el día</a>
<a class="button" style="font-size: 25px;" href="#notas,realizacion_cafe">Como realizar el café</a>
<a class="button" style="font-size: 25px;" href="#notas,fin_dia">Como acabar el día</a>
<a class="button" style="font-size: 25px;" href="#notas,horario">Horario</a>
<a class="button" style="font-size: 25px;" href="#notas,tareas">Tareas</a>
</fieldset>
<fieldset style="float: left;">
<legend>Acciones</legend>
<a class="button" style="font-size: 25px;" href="#aulas,solicitudes"><img src="${PAGES.materiales.icon}" height="20"> Solicitudes de material</a>
<a class="button" style="font-size: 25px;" href="#aulas,informes,diario-${CurrentISODate()}">Diario de hoy</a>
<a class="button rojo" style="font-size: 25px;" href="#notas,alertas"><img src="${PAGES.notas.icon}" height="20"> Ver Alertas</a>
<a class="button" style="font-size: 25px;" href="#aulas,informes"><img src="${PAGES.aulas.icon}" height="20"> Informes y diarios</a>
<a class="button btn4" style="font-size: 25px;" href="#supercafe"><img src="${PAGES.supercafe.icon}" height="20"> Ver comandas</a>
</fieldset>
<fieldset style="float: left;">
<legend>Datos de hoy</legend>
<span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Menú Comedor:</b> <br><span id="${data_Comedor}">Cargando...</span></span>
<span class="btn6" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Tareas:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">Cargando...</pre></span>
<span class="btn5" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Diario:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">Cargando...</pre></span>
<span class="btn4" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black; max-width: 25rem;"><b>Clima:</b> <br><img loading="lazy" style="padding: 15px; background-color: white; width: 245px;" id="${data_Weather}"></span>
</fieldset>
</div>
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings','weather_location').then((loc) => {
if (!loc) {
loc = prompt("Introduce tu ubicación para el clima (ciudad, país):", "Madrid, Spain");
if (loc) {
DB.put('settings','weather_location', loc);
}
}
if (loc) {
document.getElementById(data_Weather).src = "https://wttr.in/" + encodeURIComponent(loc) + "_IF0m_background=FFFFFF.png";
} else {
document.getElementById(data_Weather).src = "https://wttr.in/_IF0m_background=FFFFFF.png";
}
});
//#endregion Cargar Clima
//#region Cargar Comedor
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Platos = data.Platos || "No hay platos registrados para hoy.";
// Display platos
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
/\n/g,
"<br>"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'comedor', CurrentISODate());
} else {
add_row(data || {});
}
});
//#endregion Cargar Comedor
//#region Cargar Tareas
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay tareas.";
// Display platos
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'notas', 'tareas');
} else {
add_row(data || {});
}
});
//#endregion Cargar Tareas
//#region Cargar Diario
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay un diario.";
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'aulas_informes', 'diario-' + CurrentISODate());
} else {
add_row(data || {});
}
});
//#endregion Cargar Diario
},
_solicitudes: function () {
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
<h1>Solicitudes de Material</h1>
<button id="${btn_new}">Nueva solicitud</button>
<div id="cont"></div>
`;
TS_IndexElement(
"aulas,solicitudes",
[
{
key: "Solicitante",
type: "persona",
default: "",
label: "Solicitante",
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
},
],
"aulas_solicitudes",
document.querySelector("#cont")
);
document.getElementById(btn_new).onclick = () => {
setUrlHash("aulas,solicitudes," + safeuuid(""));
};
},
_solicitudes__edit: function (mid) {
var nameh1 = safeuuid();
var field_asunto = safeuuid();
var field_contenido = safeuuid();
var field_autor = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
<a class="button" href="#aulas,solicitudes">← Volver a solicitudes</a>
<h1>Solicitud <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}" readonly value="">
</div>
<label>
Contenido - ¡Incluye el material a solicitar!<br>
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
</label>
<hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
(async () => {
const data = await DB.get('aulas_solicitudes', mid);
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["Solicitante"] || SUB_LOGGED_IN_ID || "";
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'aulas_solicitudes', 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";
var data = {
Solicitante: 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('aulas_solicitudes', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("aulas,solicitudes");
}, 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 la solicitud");
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta solicitud?") == true) {
DB.del('aulas_solicitudes', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("aulas,solicitudes");
}, SAVE_WAIT);
});
}
};
},
_informes: function () {
const tablebody = safeuuid();
var btn_new = safeuuid();
var field_new_byday = safeuuid();
var btn_new_byday = safeuuid();
container.innerHTML = `
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
<h1>Informes</h1>
<div style="display: inline-block; border: 2px solid black; padding: 5px; border-radius: 5px;">
<b>Diario:</b><br>
<input type="date" id="${field_new_byday}" value="${CurrentISODate()}">
<button id="${btn_new_byday}">Abrir / Nuevo</button>
</div><br>
<button id="${btn_new}">Nuevo informe</button>
<div id="cont"></div>
`;
TS_IndexElement(
"aulas,informes",
[
{
key: "Autor",
type: "persona",
default: "",
label: "Autor",
},
{
key: "Fecha",
type: "fecha",
default: "",
label: "Fecha",
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
},
],
"aulas_informes",
document.querySelector("#cont")
);
document.getElementById(btn_new).onclick = () => {
setUrlHash("aulas,informes," + safeuuid(""));
};
document.getElementById(btn_new_byday).onclick = () => {
const day = document.getElementById(field_new_byday).value;
if (day) {
setUrlHash("aulas,informes,diario-" + day);
} else {
toastr.error("Selecciona un día válido");
}
}
},
_informes__edit: function (mid) {
var nameh1 = safeuuid();
var field_asunto = safeuuid();
var field_contenido = safeuuid();
var field_autor = safeuuid();
var field_fecha = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var title = "";
if (mid.startsWith("diario-")) {
var date = mid.replace("diario-", "").split("-");
title = "Diario " + date[2] + "/" + date[1] + "/" + date[0];
}
container.innerHTML = `
<a class="button" href="#aulas,informes">← Volver a informes</a>
<h1>Informe <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}" readonly value="">
<input type="hidden" id="${field_fecha}" value="">
</div>
<label>
Contenido<br>
<textarea id="${field_contenido}" style="width: 100%; height: 400px;"></textarea><br><br>
</label>
<hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
(async () => {
const data = await DB.get('aulas_informes', mid);
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_asunto).value = data["Asunto"] || title || "";
document.getElementById(field_contenido).value = data["Contenido"] || "";
document.getElementById(field_autor).value = data["Autor"] || SUB_LOGGED_IN_ID || "";
document.getElementById(field_fecha).value = data["Fecha"] || mid.startsWith("diario-") ? mid.replace("diario-", "") : CurrentISODate();
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data) => {
load_data(data, "%E");
});
} 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";
var data = {
Autor: document.getElementById(field_autor).value,
Contenido: document.getElementById(field_contenido).value,
Asunto: document.getElementById(field_asunto).value,
Fecha: document.getElementById(field_fecha).value || CurrentISODate(),
};
document.getElementById("actionStatus").style.display = "block";
DB.put('aulas_informes', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("aulas,informes");
}, 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 informe");
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar este informe?") == true) {
DB.del('aulas_informes', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("aulas,informes");
}, SAVE_WAIT);
});
}
};
},
edit: function (section) {
if (!checkRole("aulas")) {
setUrlHash("index");
return;
}
var item = location.hash.replace("#", "").split(",")[2];
if (!item) {
// No item, show section
switch (section) {
case "solicitudes":
this._solicitudes();
break;
case "informes":
this._informes();
break;
default:
this.index();
break;
}
} else {
// Show section__edit
switch (section) {
case "solicitudes":
this._solicitudes__edit(item);
break;
case "informes":
this._informes__edit(item);
break;
}
}
},
};

234
src/page/avisos.js Normal file
View File

@@ -0,0 +1,234 @@
PERMS["avisos"] = "Avisos"
PERMS["avisos:edit"] = "&gt; Editar"
PAGES.avisos = {
navcss: "btn5",
icon: "static/appico/File_Plugin.svg",
AccessControl: true,
Title: "Avisos",
edit: function (mid) {
if (!checkRole("avisos:edit")) {setUrlHash("avisos");return}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_asunto = safeuuid();
var field_origen = safeuuid();
var field_destino = safeuuid();
var field_estado = safeuuid();
var field_mensaje = safeuuid();
var field_respuesta = safeuuid();
var btn_leer = safeuuid();
var btn_desleer = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = `
<h1>Aviso <code id="${nameh1}"></code></h1>
<fieldset style="float: left;">
<legend>Valores</legend>
<label>
Fecha<br>
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
</label>
<label>
Asunto<br>
<input type="text" id="${field_asunto}" value=""><br><br>
</label>
<input type="hidden" id="${field_origen}">
<input type="hidden" id="${field_destino}">
<div id="${div_actions}"></div>
<label>
Mensaje<br>
<textarea id="${field_mensaje}"></textarea><br><br>
</label>
<label>
Respuesta<br>
<textarea id="${field_respuesta}"></textarea><br><br>
</label>
<label>
Estado<br>
<input readonly disabled type="text" id="${field_estado}" value="">
<br>
<button id="${btn_leer}">Leido</button>
<button id="${btn_desleer}">No leido</button>
<br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
document.getElementById(btn_leer).onclick = () => {
document.getElementById(field_estado).value = "leido";
};
document.getElementById(btn_desleer).onclick = () => {
document.getElementById(field_estado).value = "por_leer";
};
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_origen).value = value;
},
"Origen"
);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_destino).value = value;
},
"Destino"
);
(async () => {
const data = await DB.get('notificaciones', mid);
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate() || "";
document.getElementById(field_asunto).value = data["Asunto"] || "";
document.getElementById(field_mensaje).value = data["Mensaje"] || "";
document.getElementById(field_origen).value = data["Origen"] || SUB_LOGGED_IN_ID || "";
document.getElementById(field_destino).value = data["Destino"] || "";
document.getElementById(field_estado).value = data["Estado"] || "%%" || "";
document.getElementById(field_respuesta).value = data["Respuesta"] || "";
// Persona select
divact.innerHTML = "";
addCategory_Personas(
divact,
SC_Personas,
data["Origen"] || "",
(value) => {
document.getElementById(field_origen).value = value;
},
"Origen"
);
addCategory_Personas(
divact,
SC_Personas,
data["Destino"] || "",
(value) => {
document.getElementById(field_destino).value = value;
},
"Destino"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'notificaciones', mid);
} else {
load_data(data || {});
}
})();
document.getElementById(btn_guardar).onclick = () => {
// Check if button is already disabled to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
// Validate before disabling button
if (document.getElementById(field_origen).value == "") {
alert("¡Hay que elegir una persona de origen!");
return;
}
if (document.getElementById(field_destino).value == "") {
alert("¡Hay que elegir una persona de origen!");
return;
}
// Disable button after validation passes
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
var data = {
Fecha: document.getElementById(field_fecha).value,
Origen: document.getElementById(field_origen).value,
Destino: document.getElementById(field_destino).value,
Mensaje: document.getElementById(field_mensaje).value,
Respuesta: document.getElementById(field_respuesta).value,
Asunto: document.getElementById(field_asunto).value,
Estado: document
.getElementById(field_estado)
.value.replace("%%", "por_leer"),
};
document.getElementById("actionStatus").style.display = "block";
DB.put('notificaciones', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("avisos");
}, 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 la notificación");
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta notificación?") == true) {
DB.del('notificaciones', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("avisos");
}, SAVE_WAIT);
});
}
};
},
index: function () {
if (!checkRole("avisos")) {setUrlHash("index");return}
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<h1>Avisos</h1>
<button id="${btn_new}">Nuevo aviso</button>
<div id="cont"></div>
`;
TS_IndexElement(
"avisos",
[
{
key: "Origen",
type: "persona",
default: "",
label: "Origen",
},
{
key: "Destino",
type: "persona",
default: "",
label: "Destino",
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
},
{
key: "Estado",
type: "raw",
default: "",
label: "Estado",
},
],
"notificaciones",
document.querySelector("#cont"),
(data, new_tr) => {
new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == "leido") {
new_tr.style.backgroundColor = "lightgreen";
}
}
);
if (!checkRole("avisos:edit")) {
document.getElementById(btn_new).style.display = "none"
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("avisos," + safeuuid(""));
};
}
},
}

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

@@ -0,0 +1,266 @@
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 = `
<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 = `
<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 = `
<fieldset>
<legend>Error</legend>
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
</fieldset>
`;
return;
}
// Show loading
resultsEl.innerHTML = `
<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 = `📊 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 = `
<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();
},
};

View File

@@ -1,48 +1,56 @@
PERMS["comedor"] = "Comedor"
PERMS["comedor:edit"] = "&gt; Editar"
PAGES.comedor = {
navcss: "btn7",
Title: "Menú comedor",
navcss: "btn6",
icon: "static/appico/apple.png",
AccessControl: true,
Title: "Comedor",
edit: function (mid) {
if (!checkRole("comedor:edit")) {setUrlHash("comedor");return}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_platos = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
<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="${CurrentISODate()}"><br><br>
</label>
<label>
Platos<br>
<textarea id="${field_platos}"></textarea><br><br>
</label>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
gun
.get(TABLE)
.get("comedor")
.get(mid)
.once((data, key) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = key;
document.getElementById(field_fecha).value = data["Fecha"];
document.getElementById(field_platos).value =
data["Platos"] || "";
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
load_data(data, "%E");
});
} else {
load_data(data);
}
});
<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>
Platos<br>
<textarea id="${field_platos}"></textarea><br><br>
</label>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
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_platos).value =
data["Platos"] || "";
}
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;
var data = {
Fecha: newDate,
@@ -51,37 +59,44 @@ PAGES.comedor = {
// If the date has changed, we need to delete the old entry
if (mid !== newDate && mid !== "") {
betterGunPut(gun.get(TABLE).get("comedor").get(mid), null);
DB.del('comedor', mid);
}
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
document.getElementById("actionStatus").style.display = "block";
betterGunPut(gun.get(TABLE).get("comedor").get(newDate), encrypted);
document.getElementById("actionStatus").style.display = "block";
DB.put('comedor', newDate, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("comedor");
}, 1500);
}, 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) {
betterGunPut(gun.get(TABLE).get("comedor").get(mid), null);
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("comedor");
}, 1500);
DB.del('comedor', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("comedor");
}, SAVE_WAIT);
});
}
};
},
index: function () {
const tablebody = safeuuid();
if (!checkRole("comedor")) {setUrlHash("index");return}
const cont = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<h1>Menú del comedor</h1>
<button id="${btn_new}">Nueva entrada</button>
<div id="cont"></div>
`;
<h1>Menú del comedor</h1>
<button id="${btn_new}">Nueva entrada</button>
<div id="${cont}"></div>
`;
TS_IndexElement(
"comedor",
[
@@ -98,8 +113,8 @@ PAGES.comedor = {
label: "Platos",
}
],
gun.get(TABLE).get("comedor"),
document.querySelector("#cont"),
"comedor",
document.getElementById(cont),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
if (data.Fecha == CurrentISODate()) {
@@ -107,8 +122,13 @@ PAGES.comedor = {
}
}
);
document.getElementById(btn_new).onclick = () => {
setUrlHash("comedor," + safeuuid(""));
};
if (!checkRole("comedor:edit")) {
document.getElementById(btn_new).style.display = "none"
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("comedor," + safeuuid(""));
};
}
},
};

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

@@ -0,0 +1,232 @@
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;
default:
// Tab to edit
}
},
__config: function () {
var form = safeuuid();
container.innerHTML = `
<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 select_type = safeuuid();
var textarea_content = safeuuid();
var button_export_local = safeuuid();
var button_export_safe = safeuuid();
var button_export_safe_cloud = safeuuid();
var button_clear = safeuuid();
container.innerHTML = `
<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>
<button id="${button_export_safe_cloud}" style="display: none;" type="button">Exportar a EuskadiTech - cifrado</button>
<!--<br><br><em>Para descargar envia un correo a telesec@tech.eus con el asunto "TSBK %${getDBName()}".</em>-->
</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)
);
})();
};
// document.getElementById(button_export_safe_cloud).onclick = () => {
// var download_data = (DATA) => {
// toastr.info("Exportado todo, subiendo!");
// fetch(
// "https://telesec-sync.tech.eus/upload_backup.php?table=" + getDBName(),
// {
// method: "POST",
// body: JSON.stringify(DATA),
// }
// )
// .then(() => {
// toastr.success("Subido correctamente!");
// })
// .catch(() => {
// toastr.error("Ha ocurrido un error en la subida.");
// });
// };
// gun.get(TABLE).load(download_data);
// };
},
__import: function () {
var select_type = safeuuid();
var textarea_content = safeuuid();
var button_import = safeuuid();
var button_clear = safeuuid();
container.innerHTML = `
<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 = `
<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);
}
});
},
index: function () {
container.innerHTML = `
<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,config">Ajustes</a>
`;
},
};

View File

@@ -1,89 +0,0 @@
PAGES.exportar = {
navcss: "btn1",
Title: "Exportar",
index: function () {
var select_type = safeuuid();
var textarea_content = safeuuid();
var button_export_local = safeuuid();
var button_export_safe = safeuuid();
var button_export_safe_cloud = safeuuid();
var button_clear = safeuuid();
container.innerHTML = `
<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>
<button id="${button_export_safe_cloud}" style="display: none;" type="button">Exportar a EuskadiTech - cifrado</button>
<!--<br><br><em>Para descargar envia un correo a telesec@tech.eus con el asunto "TSBK %${GROUPID}".</em>-->
</fieldset>
`;
document.getElementById(button_export_local).onclick = () => {
var data_export = {};
var output = {
materiales: {},
personas: {},
};
var download_data = (DATA) => {
Object.keys(DATA).forEach((modul) => {
Object.entries(DATA[modul] || {}).forEach((entry) => {
var key = entry[0];
var value = entry[1];
if (value != null) {
if (typeof value == "string") {
SEA.decrypt(value, SECRET, (data) => {
output[modul][key] = data;
});
} else {
output[modul][key] = value;
}
}
});
toastr.success("Exportado todo, descargando!");
console.error(output);
download(
`Export TeleSec ${GROUPID}.json.txt`,
JSON.stringify(output)
);
//setUrlHash(sel);
}, 2500);
};
gun.get(TABLE).load(download_data);
};
document.getElementById(button_export_safe).onclick = () => {
var download_data = (DATA) => {
toastr.success("Exportado todo, descargado!");
console.error(DATA);
download(
`Export TeleSec Encriptado ${GROUPID}.json.txt`,
JSON.stringify(DATA)
);
//setUrlHash(sel);
};
gun.get(TABLE).load(download_data);
};
document.getElementById(button_export_safe_cloud).onclick = () => {
var download_data = (DATA) => {
toastr.info("Exportado todo, subiendo!");
console.error(DATA);
fetch(
"https://telesec-sync.tech.eus/upload_backup.php?table=" + GROUPID,
{
method: "POST",
body: JSON.stringify(DATA),
}
)
.then(() => {
toastr.success("Subido correctamente!");
})
.catch(() => {
toastr.error("Ha ocurrido un error en la subida.");
});
};
gun.get(TABLE).load(download_data);
};
},
};

View File

@@ -1,54 +0,0 @@
PAGES.importar = {
navcss: "btn1",
Title: "Importar",
index: function () {
var select_type = safeuuid();
var textarea_content = safeuuid();
var button_import = safeuuid();
var button_clear = safeuuid();
container.innerHTML = `
<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") {
gun.get(TABLE).put(JSON.parse(val), (ack) => {
toastr.info("Importado " + entry[0] + ".");
});
} else {
Object.entries(JSON.parse(val)["data"]).forEach((entry) => {
var enc = SEA.encrypt(entry[1], SECRET, (encrypted) => {
betterGunPut(gun.get(TABLE).get(sel).get(entry[0]), encrypted);
});
});
}
setTimeout(() => {
toastr.info("Importado todo!");
if (sel == "%telesec") {
setUrlHash("inicio");
} else {
setUrlHash(sel);
}
}, 5000);
};
},
};

View File

@@ -1,11 +1,44 @@
PAGES.index = {
//navcss: "btn1",
Title: "Inicio",
index: function () {
icon: "static/appico/house.png",
index: function() {
container.innerHTML = `
<h1>Inicio</h1>
<em>Utiliza el menú superior para abrir un modulo</em>
<br><br>
`;
<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>
<em>Utiliza el menú superior para abrir un modulo</em>
<br><br>
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
`;
},
};
edit: function(mid) {
switch (mid) {
case 'qr':
PAGES.index.__scan()
break;
}
},
__scan: function(mid) {
var qrscan = safeuuid()
container.innerHTML = `
<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)
}
}

View File

@@ -1,22 +1,292 @@
PAGES.login = {
Esconder: true,
Title: "Login",
edit: function (mid) {
// Setup form to configure backend (PouchDB or remoteStorage) and credentials
var field_backend = safeuuid();
var field_couch = safeuuid();
var field_couch_dbname = safeuuid();
var field_couch_user = safeuuid();
var field_couch_pass = safeuuid();
var field_rs_user = safeuuid();
var field_rs_token = safeuuid();
var field_secret = safeuuid();
var btn_import_json = safeuuid();
var div_import_area = safeuuid();
var field_json = safeuuid();
var field_file = safeuuid();
var btn_parse_json = safeuuid();
var btn_start_scan = safeuuid();
var div_scan = safeuuid();
var div_pouchdb_fields = safeuuid();
var div_remotestorage_fields = safeuuid();
var btn_save = safeuuid();
var savedBackend = localStorage.getItem('TELESEC_BACKEND') || 'pouchdb';
container.innerHTML = `
<h1>Configuración de almacenamiento</h1>
<b>Aviso: Después de guardar, la aplicación intentará sincronizar en segundo plano. Tenga paciencia.</b>
<fieldset>
<legend>Tipo de almacenamiento</legend>
<label>
<input type="radio" name="${field_backend}" value="pouchdb" ${savedBackend === 'pouchdb' ? 'checked' : ''}>
PouchDB + CouchDB (sincronización con servidor CouchDB)
</label><br>
<label>
<input type="radio" name="${field_backend}" value="remotestorage" ${savedBackend === 'remotestorage' ? 'checked' : ''}>
remoteStorage (sincronización con servidor remoteStorage)
</label><br><br>
<div id="${div_pouchdb_fields}" style="display:${savedBackend === 'pouchdb' ? 'block' : 'none'};">
<fieldset>
<legend>Configuración CouchDB</legend>
<label>Servidor CouchDB (ej: couch.example.com)
<input type="text" id="${field_couch}" value="${(localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/^https?:\/\//, '')}"><br><br>
</label>
<label>Nombre de la base (opcional, por defecto usa telesec)
<input type="text" id="${field_couch_dbname}" value="${localStorage.getItem('TELESEC_COUCH_DBNAME') || ''}"><br><br>
</label>
<label>Usuario
<input type="text" id="${field_couch_user}" value="${localStorage.getItem('TELESEC_COUCH_USER') || ''}"><br><br>
</label>
<label>Contraseña
<input type="password" id="${field_couch_pass}" value="${localStorage.getItem('TELESEC_COUCH_PASS') || ''}"><br><br>
</label>
</fieldset>
</div>
<div id="${div_remotestorage_fields}" style="display:${savedBackend === 'remotestorage' ? 'block' : 'none'};">
<fieldset>
<legend>Configuración remoteStorage</legend>
<label>Dirección de usuario (ej: user@5apps.com o user@example.com)
<input type="text" id="${field_rs_user}" value="${localStorage.getItem('TELESEC_RS_USER') || ''}"><br><br>
</label>
<label>Token de acceso (opcional, se pedirá al conectar si no se proporciona)
<input type="password" id="${field_rs_token}" value="${localStorage.getItem('TELESEC_RS_TOKEN') || ''}"><br><br>
</label>
</fieldset>
</div>
<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>
<div style="margin-top:8px;">
<button id="${btn_import_json}" class="btn4">Importar desde JSON / QR</button>
</div>
<div id="${div_import_area}" style="display:none;margin-top:10px;border:1px solid #eee;padding:8px;">
<label>Pegar JSON de configuración (o usar archivo / QR):</label><br>
<textarea id="${field_json}" style="width:100%;height:120px;margin-top:6px;" placeholder='{"backend":"pouchdb","server":"couch.example.com","dbname":"telesec-test","username":"user","password":"pass","secret":"SECRET123"}'></textarea>
<div style="margin-top:6px;">
<input type="file" id="${field_file}" accept="application/json">
<button id="${btn_parse_json}" class="btn5">Aplicar JSON</button>
<button id="${btn_start_scan}" class="btn3">Escanear QR (si disponible)</button>
</div>
<div id="${div_scan}" style="margin-top:8px;"></div>
</div>
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
</fieldset>
<p>Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.</p>
`;
// Toggle fields based on backend selection
var radios = document.getElementsByName(field_backend);
for (var i = 0; i < radios.length; i++) {
radios[i].addEventListener('change', function() {
var selectedBackend = document.querySelector('input[name="' + field_backend + '"]:checked').value;
document.getElementById(div_pouchdb_fields).style.display = selectedBackend === 'pouchdb' ? 'block' : 'none';
document.getElementById(div_remotestorage_fields).style.display = selectedBackend === 'remotestorage' ? 'block' : 'none';
});
}
// Helper: normalize and apply config object
function applyConfig(cfg) {
try {
if (!cfg) throw new Error('JSON vacío');
var backend = cfg.backend || 'pouchdb';
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
localStorage.setItem('TELESEC_BACKEND', backend);
if (backend === 'remotestorage') {
var rsUser = cfg.rsUserAddress || cfg.rsUser || cfg.rs_user || cfg.user || '';
var rsToken = cfg.rsToken || cfg.rs_token || cfg.token || '';
if (!rsUser) throw new Error('Falta campo "rsUserAddress" o "user" en JSON para remoteStorage');
localStorage.setItem('TELESEC_RS_USER', rsUser);
if (rsToken) localStorage.setItem('TELESEC_RS_TOKEN', rsToken);
if (secret) {
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
}
DB.init({ backend: 'remotestorage', secret: SECRET, rsUserAddress: rsUser, rsToken: rsToken });
} else {
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;
if (!url) throw new Error('Falta campo "server" en JSON para PouchDB');
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + url.replace(/^https?:\/\//, ''));
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({ backend: 'pouchdb', 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));
}
}
// Toggle import area
document.getElementById(btn_import_json).onclick = function () {
var el = document.getElementById(div_import_area);
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
};
// Parse textarea JSON
document.getElementById(btn_parse_json).onclick = function () {
var txt = document.getElementById(field_json).value.trim();
if (!txt) { toastr.error('JSON vacío'); return; }
try {
var obj = JSON.parse(txt);
applyConfig(obj);
} catch (e) {
toastr.error('JSON inválido: ' + e.message);
}
};
// File input: read JSON file and apply
document.getElementById(field_file).addEventListener('change', function (ev) {
var f = ev.target.files && ev.target.files[0];
if (!f) return;
var r = new FileReader();
r.onload = function (e) {
try {
var txt = e.target.result;
document.getElementById(field_json).value = txt;
var obj = JSON.parse(txt);
applyConfig(obj);
} catch (err) {
toastr.error('Error leyendo archivo JSON: ' + (err && err.message ? err.message : err));
}
};
r.readAsText(f);
});
// QR scanning (if html5-qrcode available)
document.getElementById(btn_start_scan).onclick = function () {
var scanDiv = document.getElementById(div_scan);
scanDiv.innerHTML = '';
if (window.Html5QrcodeScanner || window.Html5Qrcode) {
try {
var targetId = div_scan + '-cam';
scanDiv.innerHTML = '<div id="' + targetId + '"></div><div style="margin-top:6px;"><button id="' + targetId + '-stop" class="btn3">Detener</button></div>';
var html5Qr;
if (window.Html5Qrcode) {
html5Qr = new Html5Qrcode(targetId);
Html5Qrcode.getCameras().then(function(cameras){
var camId = (cameras && cameras[0] && cameras[0].id) ? cameras[0].id : undefined;
html5Qr.start({ facingMode: 'environment' }, { fps: 10, qrbox: 250 }, function(decodedText){
try {
var obj = JSON.parse(decodedText);
html5Qr.stop();
applyConfig(obj);
} catch (e) {
toastr.error('QR no contiene JSON válido');
}
}, function(err){ /* ignore scan errors */ }).catch(function(err){ toastr.error('Error iniciando cámara: ' + err); });
}).catch(function(){
// fallback: start without camera list
html5Qr.start({ facingMode: 'environment' }, { fps: 10, qrbox: 250 }, function(decodedText){
try { applyConfig(JSON.parse(decodedText)); } catch(e){ toastr.error('QR no contiene JSON válido'); }
}, function(){}).catch(function(err){
toastr.error('Error iniciando cámara: ' + (err && err.message ? err.message : err));
});
});
} else {
// Html5QrcodeScanner fallback
var scanner = new Html5QrcodeScanner(targetId, { fps: 10, qrbox: 250 });
scanner.render(function(decodedText){
try { applyConfig(JSON.parse(decodedText)); scanner.clear(); } catch(e){ toastr.error('QR no contiene JSON válido'); }
});
}
// stop button
document.getElementById(targetId + '-stop').onclick = function () {
if (html5Qr && html5Qr.getState && html5Qr.getState() === Html5Qrcode.ScanStatus.SCANNING) {
html5Qr.stop().catch(function(){});
}
scanDiv.innerHTML = '';
};
} catch (e) {
toastr.error('Error al iniciar escáner: ' + (e && e.message ? e.message : e));
}
} else {
scanDiv.innerHTML = '<p>Escáner no disponible. Copia/pega el JSON o sube un archivo.</p>';
}
};
document.getElementById(btn_save).onclick = () => {
var backend = document.querySelector('input[name="' + field_backend + '"]:checked').value;
var secret = document.getElementById(field_secret).value || '';
localStorage.setItem('TELESEC_BACKEND', backend);
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
SECRET = secret.toUpperCase();
try {
if (backend === 'remotestorage') {
var rsUser = document.getElementById(field_rs_user).value.trim();
var rsToken = document.getElementById(field_rs_token).value || '';
if (!rsUser) {
toastr.error('Debe proporcionar una dirección de usuario remoteStorage');
return;
}
localStorage.setItem('TELESEC_RS_USER', rsUser);
localStorage.setItem('TELESEC_RS_TOKEN', rsToken);
DB.init({ backend: 'remotestorage', secret: SECRET, rsUserAddress: rsUser, rsToken: rsToken });
toastr.success('Iniciando sincronización con remoteStorage');
} else {
var url = document.getElementById(field_couch).value.trim();
var dbname = document.getElementById(field_couch_dbname).value.trim();
var user = document.getElementById(field_couch_user).value.trim();
var pass = document.getElementById(field_couch_pass).value;
localStorage.setItem('TELESEC_COUCH_URL', "https://" + url);
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
localStorage.setItem('TELESEC_COUCH_USER', user);
localStorage.setItem('TELESEC_COUCH_PASS', pass);
DB.init({ backend: 'pouchdb', secret: SECRET, remoteServer: "https://" + url, 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) {
var field_persona = safeuuid();
var btn_guardar = safeuuid();
var btn_reload = safeuuid();
var div_actions = safeuuid();
container.innerHTML = `
<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="btn1" id="${btn_reload}">Recargar lista</button>
</fieldset>
<a style="color: rgb(240,240,240)">Acceso sin cuenta</a>
`;
<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,
@@ -27,9 +297,8 @@ PAGES.login = {
},
"¿Quién eres?",
true,
"- Pulsa recargar -"
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
);
document.getElementById("appendApps").style.display = "none"
document.getElementById(btn_guardar).onclick = () => {
if (document.getElementById(field_persona).value == "") {
alert("Tienes que elegir tu cuenta!");
@@ -38,13 +307,40 @@ PAGES.login = {
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID]
SUB_LOGGED_IN = true
setUrlHash("index")
document.getElementById("appendApps").style.display = "unset"
SetPages()
if (location.hash.replace("#", "").startsWith("login")) {
open_page("index");
setUrlHash("index")
} else{
open_page(location.hash.replace("#", ""));
}
};
document.getElementById(btn_reload).onclick = () => {
setUrlHash("login," + safeuuid(""))
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));
});
};
}
}
}

View File

@@ -1,192 +1,265 @@
PERMS["materiales"] = "Almacén";
PERMS["materiales:edit"] = "&gt; Editar";
PAGES.materiales = {
navcss: "btn2",
Title: "Materiales",
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_abierto = safeuuid();
var field_ubicacion = safeuuid();
var field_referencia = safeuuid();
var field_notas = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var FECHA_ISO = new Date().toISOString().split("T")[0];
container.innerHTML = `
<h1>Material <code id="${nameh1}"></code></h1>
<fieldset>
<label>
Referencia<br>
<input type="text" id="${field_referencia}" value="?"><br><br>
</label>
<label>
Nombre<br>
<input type="text" id="${field_nombre}"><br><br>
</label>
<label>
Unidad<br>
<input type="text" id="${field_unidad}"><br><br>
</label>
<label>
Cantidad Actual<br>
<input type="number" step="0.5" id="${field_cantidad}"><br><br>
</label>
<label>
Cantidad Minima<br>
<input type="number" step="0.5" id="${field_cantidad_min}"><br><br>
</label>
<label>
Ubicación<br>
<input type="text" id="${field_ubicacion}" value="-"><br><br>
</label>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
gun
.get(TABLE)
.get("materiales")
.get(mid)
.once((data, key) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = key;
document.getElementById(field_nombre).value = data["Nombre"] || "";
document.getElementById(field_unidad).value = data["Unidad"] || "";
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_referencia).value =
data["Referencia"] || "?";
document.getElementById(field_notas).value = data["Notas"] || "";
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
load_data(data, "%E");
});
} else {
load_data(data);
}
});
<h1>Material <code id="${nameh1}"></code></h1>
${BuildQR("materiales," + mid, "Este Material")}
<fieldset>
<label>
Fecha Revisión<br>
<input type="date" id="${field_revision}"> <a onclick='document.getElementById("${field_revision}").value = "${FECHA_ISO}";'>Hoy - Contado todas las existencias</a><br><br>
</label>
<label>
Nombre<br>
<input type="text" id="${field_nombre}"><br><br>
</label>
<label>
Unidad<br>
<input type="text" id="${field_unidad}"><br><br>
</label>
<label>
Cantidad Actual<br>
<input type="number" step="0.5" id="${field_cantidad}"><br><br>
</label>
<label>
Cantidad Minima<br>
<input type="number" step="0.5" id="${field_cantidad_min}"><br><br>
</label>
<label>
Ubicación<br>
<input type="text" id="${field_ubicacion}" value="-"><br><br>
</label>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
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"] || "";
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'materiales', 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";
var data = {
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,
Referencia: document.getElementById(field_referencia).value,
Revision: document.getElementById(field_revision).value,
Notas: document.getElementById(field_notas).value,
};
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
document.getElementById("actionStatus").style.display = "block";
betterGunPut(gun.get(TABLE).get("materiales").get(mid), encrypted);
document.getElementById("actionStatus").style.display = "block";
DB.put('materiales', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("materiales");
}, 1500);
}, 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) {
betterGunPut(gun.get(TABLE).get("materiales").get(mid), null);
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("materiales");
}, 1500);
DB.del('materiales', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("materiales");
}, SAVE_WAIT);
});
}
};
},
index: function () {
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<h1>Materiales</h1>
<button id="${btn_new}">Nuevo Material</button>
<div id="scrolltable"><table>
<thead>
<tr>
<th>Referencia</th>
<th>Nombre</th>
<th>Ubicación</th>
<th>Cantidad</th>
<th>Notas</th>
</tr>
</thead>
<tbody id="${tablebody}">
</tbody>
</table></div>
`;
tableScroll("#scrolltable");
var tablebody_EL = document.getElementById(tablebody);
var rows = {};
function render() {
function sorter(a, b) {
if (a.Nombre < b.Nombre) {
return -1;
}
if (a.Nombre > b.Nombre) {
return 1;
}
return 0;
}
var tablebody_EL = document.getElementById(tablebody);
tablebody_EL.innerHTML = "";
Object.values(rows)
.sort(sorter)
.forEach((data) => {
var new_tr = document.createElement("tr");
new_tr.innerHTML = `
<td>${data.Referencia || "?"}</td>
<td>${data.Nombre || "?"}</td>
<td>${data.Ubicacion || "?"}</td>
<td>${data.Cantidad || "?"} ${data.Unidad || "?"}</td>
<td>${data.Notas || "?"}</td>
`;
var min = parseFloat(data.Cantidad_Minima);
var act = parseFloat(data.Cantidad);
if (act < min) {
new_tr.style.backgroundColor = "lightcoral";
}
new_tr.onclick = () => {
setUrlHash("materiales," + data._key);
};
tablebody_EL.append(new_tr);
});
if (!checkRole("materiales")) {
setUrlHash("index");
return;
}
gun
.get(TABLE)
.get("materiales")
.map()
.on((data, key, _msg, _ev) => {
EVENTLISTENER = _ev;
function add_row(data, key) {
if (data != null) {
data["_key"] = key;
rows[key] = data;
} else {
delete rows[key];
var btn_new = safeuuid();
var select_ubicacion = safeuuid();
var check_lowstock = safeuuid();
var tableContainer = safeuuid();
container.innerHTML = `
<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 = `${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);
}
render();
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
add_row(data, key);
});
if (typeof data === "string") {
TS_decrypt(data, SECRET, (dec, wasEncrypted) => {
if (dec && typeof dec === "object") {
addUbicacion(dec);
}
}, 'materiales', key);
} else {
add_row(data, key);
addUbicacion(data);
}
});
document.getElementById(btn_new).onclick = () => {
setUrlHash("materiales," + safeuuid(""));
} 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(""));
};
}
},
};

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

@@ -0,0 +1,252 @@
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 = `
<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="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</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 = `
<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(""));
};
}
},
}

View File

@@ -1,217 +0,0 @@
PAGES.notificaciones = {
navcss: "btn6",
Title: "Notificaciones",
edit: function (mid) {
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_asunto = safeuuid();
var field_origen = safeuuid();
var field_destino = safeuuid();
var field_estado = safeuuid();
var field_mensaje = safeuuid();
var field_respuesta = safeuuid();
var btn_leer = safeuuid();
var btn_desleer = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var div_actions = safeuuid();
container.innerHTML = `
<h1>Notificación <code id="${nameh1}"></code></h1>
<fieldset style="float: left;">
<legend>Valores</legend>
<label>
Fecha<br>
<input readonly disabled type="text" id="${field_fecha}" value="${CurrentISODate()}"><br><br>
</label>
<label>
Asunto<br>
<input type="text" id="${field_asunto}" value=""><br><br>
</label>
<input type="hidden" id="${field_origen}">
<input type="hidden" id="${field_destino}">
<div id="${div_actions}"></div>
<label>
Mensaje<br>
<textarea id="${field_mensaje}"></textarea><br><br>
</label>
<label>
Respuesta<br>
<textarea id="${field_respuesta}"></textarea><br><br>
</label>
<label>
Estado<br>
<input readonly disabled type="text" id="${field_estado}" value="%%">
<br>
<button id="${btn_leer}">Leido</button>
<button id="${btn_desleer}">No leido</button>
<br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
document.getElementById(btn_leer).onclick = () => {
document.getElementById(field_estado).value = "leido";
};
document.getElementById(btn_desleer).onclick = () => {
document.getElementById(field_estado).value = "por_leer";
};
var divact = document.getElementById(div_actions);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_origen).value = value;
},
"Origen"
);
addCategory_Personas(
divact,
SC_Personas,
"",
(value) => {
document.getElementById(field_destino).value = value;
},
"Destino"
);
gun
.get(TABLE)
.get("notificaciones")
.get(mid)
.once((data, key) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = key;
document.getElementById(field_fecha).value = data["Fecha"];
document.getElementById(field_asunto).value = data["Asunto"] || "";
document.getElementById(field_mensaje).value =
data["Mensaje"] || "";
document.getElementById(field_origen).value = data["Origen"] || "";
document.getElementById(field_destino).value =
data["Destino"] || "";
document.getElementById(field_estado).value = data["Estado"] || "";
document.getElementById(field_respuesta).value =
data["Respuesta"] || "";
// Persona select
divact.innerHTML = "";
addCategory_Personas(
divact,
SC_Personas,
data["Origen"] || "",
(value) => {
document.getElementById(field_origen).value = value;
},
"Origen"
);
addCategory_Personas(
divact,
SC_Personas,
data["Destino"] || "",
(value) => {
document.getElementById(field_destino).value = value;
},
"Destino"
);
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
load_data(data, "%E");
});
} else {
load_data(data);
}
});
document.getElementById(btn_guardar).onclick = () => {
if (document.getElementById(field_origen).value == "") {
alert("¡Hay que elegir una persona de origen!");
return;
}
if (document.getElementById(field_destino).value == "") {
alert("¡Hay que elegir una persona de origen!");
return;
}
var data = {
Fecha: document.getElementById(field_fecha).value,
Origen: document.getElementById(field_origen).value,
Destino: document.getElementById(field_destino).value,
Mensaje: document.getElementById(field_mensaje).value,
Respuesta: document.getElementById(field_respuesta).value,
Asunto: document.getElementById(field_asunto).value,
Estado: document
.getElementById(field_estado)
.value.replace("%%", "por_leer"),
};
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
document.getElementById("actionStatus").style.display = "block";
betterGunPut(
gun.get(TABLE).get("notificaciones").get(mid),
encrypted
);
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("notificaciones");
}, 1500);
});
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta notificación?") == true) {
betterGunPut(gun.get(TABLE).get("notificaciones").get(mid), null);
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("notificaciones");
}, 1500);
}
};
},
index: function () {
const tablebody = safeuuid();
var btn_new = safeuuid();
container.innerHTML = `
<h1>Notificaciones</h1>
<button id="${btn_new}">Nueva notificación</button>
<div id="cont"></div>
`;
TS_IndexElement(
"notificaciones",
[
{
key: "Origen",
type: "persona",
default: "",
label: "Origen",
},
{
key: "Destino",
type: "persona",
default: "",
label: "Destino",
},
{
key: "Asunto",
type: "raw",
default: "",
label: "Asunto",
},
{
key: "Estado",
type: "raw",
default: "",
label: "Estado",
},
],
gun.get(TABLE).get("notificaciones"),
document.querySelector("#cont"),
(data, new_tr) => {
new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == "leido") {
new_tr.style.backgroundColor = "lightgreen";
}
}
);
document.getElementById(btn_new).onclick = () => {
setUrlHash("notificaciones," + safeuuid(""));
};
},
}

1420
src/page/pagos.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,222 +1,255 @@
PERMS["personas"] = "Personas";
PERMS["personas:edit"] = "&gt; Editar";
PAGES.personas = {
navcss: "btn4",
navcss: "btn3",
icon: "static/appico/users.png",
AccessControl: true,
Title: "Personas",
edit: function (mid) {
if (!checkRole("personas:edit")) {
setUrlHash("personas");
return;
}
var nameh1 = safeuuid();
var permisosdet = safeuuid();
var field_nombre = safeuuid();
var field_zona = safeuuid();
var field_roles = safeuuid();
var field_puntos = safeuuid();
var field_notas = safeuuid();
var field_anilla = safeuuid();
var field_foto = safeuuid();
var render_foto = safeuuid();
var field_monedero_balance = safeuuid();
var field_monedero_notas = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
var btn_ver_monedero = safeuuid();
container.innerHTML = `
<h1>Persona <code id="${nameh1}"></code></h1>
<fieldset>
<label>
Nombre<br>
<input type="text" id="${field_nombre}"><br><br>
</label>
<label>
Zona<br>
<input type="text" id="${field_zona}"><br><br>
</label>
<label>
Permisos<br>
<input type="text" id="${field_roles}"><br><br>
</label>
<label>
Puntos<br>
<input type="number" id="${field_puntos}"><br><br>
</label>
<label>
Anilla<br>
<input type="color" id="${field_anilla}"><br><br>
</label>
<label>
Foto (PNG o JPG)<br>
<img id="${render_foto}" height="100px" style="border: 3px inset; min-width: 7px;" src="static/camera2.png">
<input type="file" accept="image/*" id="${field_foto}" style="display: none;"><br><br>
</label>
<h1>Persona <code id="${nameh1}"></code></h1>
${BuildQR("personas," + mid, "Esta Persona")}
<fieldset>
<label>
Nombre<br>
<input type="text" id="${field_nombre}"><br><br>
</label>
<label>
Zona<br>
<input type="text" id="${field_zona}"><br><br>
</label>
</label>
<details>
<summary>Permisos</summary>
<form id="${permisosdet}">
</form>
</details>
<label>
Anilla<br>
<input type="color" id="${field_anilla}"><br><br>
</label>
<label>
Foto (PNG o JPG)<br>
<img id="${render_foto}" height="100px" style="border: 3px inset; min-width: 7px;" src="static/ico/user_generic.png">
<input type="file" accept="image/*" id="${field_foto}" style="display: none;"><br><br>
</label>
<details style="background: #e3f2fd; border: 2px solid #2196f3; border-radius: 8px; padding: 10px; margin: 15px 0;">
<summary style="cursor: pointer; font-weight: bold; color: #1976d2;">💳 Tarjeta Monedero</summary>
<div style="padding: 15px;">
<label>
Balance Actual<br>
<input type="number" step="0.01" id="${field_monedero_balance}" style="font-size: 24px; font-weight: bold; color: #1976d2;"><br>
<small>Se actualiza automáticamente con las transacciones</small><br><br>
</label>
<label>
Notas del Monedero<br>
<textarea id="${field_monedero_notas}" rows="3" placeholder="Notas adicionales sobre el monedero..."></textarea><br><br>
</label>
<button type="button" id="${btn_ver_monedero}" class="btn5">Ver Transacciones del Monedero</button>
</div>
</details>
<details style="background: #e3fde3ff; border: 2px solid #21f328ff; border-radius: 8px; padding: 10px; margin: 15px 0; display: none;">
<summary style="cursor: pointer; font-weight: bold; color: rgba(26, 141, 3, 1);">🔗 Generar enlaces</summary>
<div style="padding: 15px;">
<label>
Este servidor<br>
<input type="url" value="${location.protocol}//${location.hostname}:${location.port}${location.pathname}?login=${getDBName()}:${SECRET}&sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;"><br>
</label>
<label>
Cualquier Servidor<br>
<input type="url" value="https://tech.eus/ts/?login=${getDBName()}:${SECRET}&sublogin=${mid}" style="font-size: 10px; font-weight: bold; color: #000;"><br>
</label>
</div>
</details>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label><hr>
<button class="btn5" id="${btn_guardar}">Guardar</button>
<button class="rojo" id="${btn_borrar}">Borrar</button>
</fieldset>
`;
var resized = "";
gun
.get(TABLE)
.get("personas")
.get(mid)
.once((data, key) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = key;
document.getElementById(field_nombre).value = data["Nombre"] || "";
document.getElementById(field_zona).value = data["Region"] || "";
document.getElementById(field_roles).value = data["Roles"] || "";
document.getElementById(field_puntos).value = data["Puntos"] || 0;
document.getElementById(field_anilla).value = data["SC_Anilla"] || "";
// document.getElementById(field_foto).value = "";
document.getElementById(render_foto).src =
data["Foto"] || "static/ico/user_generic.png";
resized = data["Foto"] || "static/ico/user_generic.png";
document.getElementById(field_notas).value = data["markdown"] || "";
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
load_data(data, "%E");
});
} else {
load_data(data);
}
});
document
.getElementById(field_foto)
.addEventListener("change", function (e) {
const file = e.target.files[0];
if (!file) return;
resizeInputImage(
file,
function (url) {
document.getElementById(render_foto).src = url;
resized = url;
},
125,
0.7
);
});
var pdel = document.getElementById(permisosdet);
DB.get('personas', mid).then((data) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = mid;
var pot = "<ul>";
Object.entries(PERMS).forEach((page) => {
var c = "";
if ((data["Roles"] || ",").split(",").includes(page[0])) {
c = "checked";
}
pot += `
<li><label>
<input name="perm" value="${page[0]}" type="checkbox" ${c}>
${page[1]}
</label></li>
`;
});
pdel.innerHTML = pot + "</ul>";
document.getElementById(field_nombre).value = data["Nombre"] || "";
document.getElementById(field_zona).value = data["Region"] || "";
document.getElementById(field_anilla).value = data["SC_Anilla"] || "";
// set fallback image immediately
document.getElementById(render_foto).src = data["Foto"] || "static/ico/user_generic.png";
resized = data["Foto"] || "static/ico/user_generic.png";
// try to load attachment 'foto' if present (preferred storage)
DB.getAttachment('personas', mid, 'foto').then((durl) => {
if (durl) {
document.getElementById(render_foto).src = durl;
resized = durl;
}
}).catch(() => {});
document.getElementById(field_notas).value = data["markdown"] || "";
document.getElementById(field_monedero_balance).value =
data["Monedero_Balance"] || 0;
document.getElementById(field_monedero_notas).value =
data["Monedero_Notas"] || "";
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'personas', mid);
} else {
load_data(data || {});
}
});
document.getElementById(field_foto).addEventListener("change", function (e) {
const file = e.target.files[0];
if (!file) return;
// Do NOT resize — keep original uploaded image
const reader = new FileReader();
reader.onload = function (ev) {
const url = ev.target.result;
document.getElementById(render_foto).src = url;
resized = url;
};
reader.readAsDataURL(file);
});
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 dt = new FormData(pdel);
var data = {
Nombre: document.getElementById(field_nombre).value,
Region: document.getElementById(field_zona).value,
Roles: document.getElementById(field_roles).value,
Puntos: document.getElementById(field_puntos).value,
Roles: dt.getAll("perm").join(",") + ",",
SC_Anilla: document.getElementById(field_anilla).value,
Foto: resized,
// Foto moved to PouchDB attachment named 'foto'
markdown: document.getElementById(field_notas).value,
Monedero_Balance:
parseFloat(document.getElementById(field_monedero_balance).value) ||
0,
Monedero_Notas: document.getElementById(field_monedero_notas).value,
};
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
document.getElementById("actionStatus").style.display = "block";
betterGunPut(gun.get(TABLE).get("personas").get(mid), encrypted);
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "block";
DB.put('personas', mid, data).then(() => {
// if resized is a data URL (new/updated image), save as attachment
var attachPromise = Promise.resolve(true);
if (typeof resized === 'string' && resized.indexOf('data:') === 0) {
attachPromise = DB.putAttachment('personas', mid, 'foto', resized, 'image/png');
}
attachPromise.then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("personas");
}, SAVE_WAIT);
}).catch((e) => {
console.warn('putAttachment error', e);
document.getElementById("actionStatus").style.display = "none";
setUrlHash("personas");
}, 1500);
guardarBtn.disabled = false;
guardarBtn.style.opacity = "1";
toastr.error("Error al guardar la foto");
});
}).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 persona");
});
};
document.getElementById(btn_ver_monedero).onclick = () => {
setUrlHash("pagos"); // Navigate to pagos and show transactions for this person
};
document.getElementById(btn_borrar).onclick = () => {
if (confirm("¿Quieres borrar esta persona?") == true) {
betterGunPut(gun.get(TABLE).get("personas").get(mid), null);
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("personas");
}, 1500);
DB.del('personas', mid).then(() => {
toastr.error("Borrado!");
setTimeout(() => {
setUrlHash("personas");
}, SAVE_WAIT);
});
}
};
},
index: function () {
const tablebody = safeuuid();
if (!checkRole("personas")) {
setUrlHash("index");
return;
}
var btn_new = safeuuid();
container.innerHTML = `
<h1>Personas</h1>
<button id="${btn_new}">Nueva Persona</button>
<div id="scrolltable"><table>
<thead>
<tr>
<th>Nombre</th>
<th>Zona</th>
<th>Puntos</th>
<th>Permisos</th>
</tr>
</thead>
<tbody id="${tablebody}">
</tbody>
</table></div>
`;
tableScroll("#scrolltable"); // id="scrolltable"
var tablebody_EL = document.getElementById(tablebody);
var rows = {};
function render() {
function sorter(a, b) {
if (a.Region.toUpperCase() < b.Region.toUpperCase()) {
return -1;
}
if (a.Region.toUpperCase() > b.Region.toUpperCase()) {
return 1;
}
return 0;
}
var tablebody_EL = document.getElementById(tablebody);
tablebody_EL.innerHTML = "";
// SC_Personas = rows
Object.values(rows)
.sort(sorter)
.forEach((data) => {
var btn_comanda = safeuuid();
var new_tr = document.createElement("tr");
new_tr.innerHTML = `
<td class="TextBorder" style="background-color: ${
data.SC_Anilla
}; text-align: center"><img src="${
data.Foto || "static/ico/user_generic.png"
}" height="50"> <br> ${data.Nombre || ""}</td>
<td>${data.Region || "?"}</td>
<td>${data.Puntos || 0}</td>
<td>${data.Roles || ""}</td>
`;
<h1>Personas</h1>
<button id="${btn_new}">Nueva Persona</button>
<div id="tableContainer"></div>
`;
// <button id="${btn_comanda}" class="${PAGES.ventas.navcss}">Nueva venta</button>
var act = parseFloat(data.Puntos);
if (act >= 10) {
new_tr.style.backgroundColor = "gold";
}
new_tr.onclick = () => {
setUrlHash("personas," + data._key);
};
tablebody_EL.append(new_tr);
// document.getElementById(btn_comanda).onclick = (e) => {
// setUrlHash("ventas," + data._key);
// if (!e) var e = window.event;
// e.cancelBubble = true;
// if (e.stopPropagation) e.stopPropagation();
// };
});
const config = [
// {
// label: "Persona",
// type: "persona",
// self: true,
// },
{ key: "Foto", label: "Foto", type: "attachment-persona", default: "", self: true },
{ key: "Nombre", label: "Nombre", type: "text", default: "" },
{ key: "Region", label: "Zona", type: "text", default: "" },
{ key: "Monedero_Balance", label: "Saldo Monedero", type: "moneda" },
//{ key: "markdown", label: "Notas", type: "markdown", default: "" },
//{ key: "Roles", label: "Permisos", type: "text", default: "" }
];
TS_IndexElement(
"personas",
config,
"personas",
document.getElementById("tableContainer"),
undefined,
undefined,
true // Enable global search bar
);
if (!checkRole("personas:edit")) {
document.getElementById(btn_new).style.display = "none";
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("personas," + safeuuid(""));
};
}
gun
.get(TABLE)
.get("personas")
.map()
.on((data, key, _msg, _ev) => {
EVENTLISTENER = _ev;
function add_row(data, key) {
if (data != null) {
data["_key"] = key;
rows[key] = data;
} else {
delete rows[key];
}
render();
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
add_row(data, key);
});
} else {
add_row(data, key);
}
});
document.getElementById(btn_new).onclick = () => {
setUrlHash("personas," + safeuuid(""));
};
},
};

View File

@@ -1,167 +1,103 @@
PERMS["resumen_diario"] = "Resumen diario (Solo docentes!)";
PAGES.resumen_diario = {
icon: "static/appico/calendar.png",
navcss: "btn3",
AccessControl: true,
Title: "Resumen Diario",
index: function () {
var table_materialesLow = safeuuid();
var table_personasHigh = safeuuid();
var table_comedor = safeuuid();
var data_Comedor = safeuuid();
var data_Tareas = safeuuid();
var data_Diario = safeuuid();
var data_Weather = safeuuid();
if (!checkRole("resumen_diario")) {
setUrlHash("index");
return;
}
container.innerHTML = `
<h1>Resumen Diario</h1>
<h2>Menú del comedor de hoy</h2>
<span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;" id="${table_comedor}"></span>
<h2>Personas con café gratis (para el Viernes)</h2>
<div id="${table_personasHigh}"></div>
<h2>Materiales faltantes (o por llegar)</h2>
<div id="${table_materialesLow}"></div>
`;
var materiales_low = {};
var personas_high = {};
function render_materialesLow() {
function sorter(a, b) {
if (a.Nombre < b.Nombre) {
return -1;
<h1>Resumen Diario ${CurrentISODate()}</h1>
<button onclick="print()">Imprimir</button>
<br><span class="btn7" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Menú Comedor:</b> <br><span id="${data_Comedor}">Cargando...</span></span>
<br><span class="btn6" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Tareas:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Tareas}">Cargando...</pre></span>
<br><span class="btn5" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Diario:</b> <br><pre style="overflow-wrap: break-word;white-space:pre-wrap;" id="${data_Diario}">Cargando...</pre></span>
<br><span class="btn4" style="display: inline-block; margin: 5px; padding: 5px; border-radius: 5px; border: 2px solid black;"><b>Clima:</b> <br><img loading="lazy" style="padding: 15px; background-color: white; height: 75px;" id="${data_Weather}"></span>
`;
//#region Cargar Clima
// Get location from DB settings.weather_location; if missing ask user and save it
// url format: https://wttr.in/<loc>?F0m
DB.get('settings','weather_location').then((loc) => {
if (!loc) {
loc = prompt("Introduce tu ubicación para el clima (ciudad, país):", "Madrid, Spain");
if (loc) {
DB.put('settings','weather_location', loc);
}
if (a.Nombre > b.Nombre) {
return 1;
}
return 0;
}
var tablebody_EL = document.getElementById(table_materialesLow);
tablebody_EL.innerHTML = "";
Object.values(materiales_low)
.sort(sorter)
.forEach((data) => {
var min = parseFloat(data.Cantidad_Minima);
var act = parseFloat(data.Cantidad);
var falta = min - act;
if (act < min) {
var new_tr = document.createElement("span");
new_tr.innerHTML = `<b>${data.Nombre || "?"}</b><br>Faltan ${
falta || "?"
} ${data.Unidad || "?"} <br><i style="font-size: 75%">${
data.Ubicacion || "?"
}</i>`;
new_tr.className = PAGES["materiales"].navcss;
new_tr.style.display = "inline-block";
new_tr.style.margin = "5px";
new_tr.style.padding = "5px";
new_tr.style.borderRadius = "5px";
new_tr.style.border = "2px solid black";
new_tr.style.cursor = "pointer";
new_tr.onclick = () => {
setUrlHash("materiales," + data._key);
};
tablebody_EL.append(new_tr);
}
});
}
gun
.get(TABLE)
.get("materiales")
.map()
.on((data, key, _msg, _ev) => {
EVENTLISTENER2 = _ev;
function add_row(data, key) {
if (data != null) {
data["_key"] = key;
materiales_low[key] = data;
} else {
delete materiales_low[key];
}
render_materialesLow();
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
add_row(data, key);
});
} else {
add_row(data, key);
}
});
function render_personasHigh() {
function sorter(a, b) {
if (a.Nombre < b.Nombre) {
return -1;
}
if (a.Nombre > b.Nombre) {
return 1;
}
return 0;
if (loc) {
document.getElementById(data_Weather).src = "https://wttr.in/" + encodeURIComponent(loc) + "_IF0m_background=FFFFFF.png";
} else {
document.getElementById(data_Weather).src = "https://wttr.in/_IF0m_background=FFFFFF.png";
}
var tablebody_EL = document.getElementById(table_personasHigh);
tablebody_EL.innerHTML = "";
Object.values(personas_high)
.sort(sorter)
.forEach((data) => {
if (data.Puntos >= 10) {
var new_tr = document.createElement("span");
new_tr.innerHTML = `<img src="${
data.Foto || ""
}" alt="" height="55" style="float: left; margin-right: 5px;"><b>${
data.Nombre || "?"
}</b><br>Tiene ${
data.Puntos || "?"
} puntos <br><i style="font-size: 75%">${data.Region || "?"}</i>`;
new_tr.className = PAGES["personas"].navcss;
new_tr.style.display = "inline-block";
new_tr.style.margin = "5px";
new_tr.style.padding = "5px";
new_tr.style.borderRadius = "5px";
new_tr.style.border = "2px solid black";
new_tr.style.cursor = "pointer";
new_tr.style.width = "200px";
new_tr.onclick = () => {
setUrlHash("personas," + data._key);
};
tablebody_EL.append(new_tr);
}
});
}
gun
.get(TABLE)
.get("personas")
.map()
.on((data, key, _msg, _ev) => {
EVENTLISTENER = _ev;
function add_row(data, key) {
if (data != null) {
data["_key"] = key;
personas_high[key] = data;
} else {
delete personas_high[key];
}
render_personasHigh();
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
add_row(data, key);
});
} else {
add_row(data, key);
}
});
// Comedor (.get("comedor").get(<current iso day>))
gun
.get(TABLE)
.get("comedor")
.get(CurrentISODate())
.once((data, key) => {
function add_row(data) {
// Fix newlines
data.Platos = data.Platos.replace(/\n/g, "<br>");
// Display platos
document.getElementById(table_comedor).innerHTML += data.Platos || "No hay platos registrados para hoy.";
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
add_row(data);
});
} else {
add_row(data);
}
});
});
//#endregion Cargar Clima
//#region Cargar Comedor
DB.get('comedor', CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Platos = data.Platos || "No hay platos registrados para hoy.";
// Display platos
document.getElementById(data_Comedor).innerHTML = data.Platos.replace(
/\n/g,
"<br>"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'comedor', CurrentISODate());
} else {
add_row(data || {});
}
});
//#endregion Cargar Comedor
//#region Cargar Tareas
DB.get('notas', 'tareas').then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay tareas.";
// Display platos
document.getElementById(data_Tareas).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'notas', 'tareas');
} else {
add_row(data || {});
}
});
//#endregion Cargar Tareas
//#region Cargar Diario
DB.get('aulas_informes', 'diario-' + CurrentISODate()).then((data) => {
function add_row(data) {
// Fix newlines
data.Contenido = data.Contenido || "No hay un diario.";
// Display platos
document.getElementById(data_Diario).innerHTML = data.Contenido.replace(
/\n/g,
"<br>"
);
}
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
add_row(data || {});
}, 'aulas_informes', 'diario-' + CurrentISODate());
} else {
add_row(data || {});
}
});
//#endregion Cargar Diario
},
};

View File

@@ -1,314 +1,346 @@
PERMS["supercafe"] = "Cafetería";
PERMS["supercafe:edit"] = "&gt; Editar";
PAGES.supercafe = {
navcss: "btn5",
Title: "SuperCafé",
edit: function (mid) {
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_persona = safeuuid();
var field_comanda = safeuuid();
var field_notas = safeuuid();
var field_estado = safeuuid();
var div_actions = safeuuid();
var btn_pagos = safeuuid();
var btn_cocina = safeuuid();
var btn_guardar = safeuuid();
var btn_guardar2 = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
<h1>Comanda <code id="${nameh1}"></code></h1>
<button onclick="setUrlHash('supercafe');">Salir</button>
<fieldset style="text-align: center;">
<legend>Rellenar comanda</legend>
<label style="display: none;">
Fecha<br>
<input readonly disabled type="text" id="${field_fecha}" value="${CurrentISODate()}"><br><br>
</label>
<label style="display: none;">
Persona<br>
<input type="hidden" id="${field_persona}">
<br><br>
</label>
<label style="display: none;">
Comanda (utiliza el panel de relleno)<br>
<textarea readonly disabled id="${field_comanda}"></textarea><br><br>
</label>
<div id="${div_actions}" open>
<!--<summary>Mostrar botones de relleno</summary>-->
</div>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label>
<label style="display: none;">
Estado<br>
<input readonly disabled type="text" id="${field_estado}" value="%%">
<br>Modificar en el listado de comandas<br>
</label>
<button id=${btn_guardar} class="btn5">Guardar</button>
<button id=${btn_borrar} class="rojo">Borrar</button>
</fieldset>
`;
var currentData = {};
var currentPersonaID = "";
var divact = document.getElementById(div_actions);
navcss: "btn4",
icon: "static/appico/cup.png",
AccessControl: true,
Title: "Cafetería",
edit: function (mid) {
if (!checkRole("supercafe:edit")) {
setUrlHash("supercafe");
return;
}
var nameh1 = safeuuid();
var field_fecha = safeuuid();
var field_persona = safeuuid();
var field_comanda = safeuuid();
var field_notas = safeuuid();
var field_estado = safeuuid();
var div_actions = safeuuid();
var btn_guardar = safeuuid();
var btn_borrar = safeuuid();
container.innerHTML = `
<h1>Comanda <code id="${nameh1}"></code></h1>
<button onclick="setUrlHash('supercafe');">Salir</button>
<fieldset style="text-align: center;">
<legend>Rellenar comanda</legend>
<label style="display: none;">
Fecha<br>
<input readonly disabled type="text" id="${field_fecha}" value=""><br><br>
</label>
<label style="display: none;">
Persona<br>
<input type="hidden" id="${field_persona}">
<br><br>
</label>
<label style="display: none;">
Comanda (utiliza el panel de relleno)<br>
<textarea readonly disabled id="${field_comanda}"></textarea><br><br>
</label>
<div id="${div_actions}" open>
<!--<summary>Mostrar botones de relleno</summary>-->
</div>
<label>
Notas<br>
<textarea id="${field_notas}"></textarea><br><br>
</label>
<label style="display: none;">
Estado<br>
<input readonly disabled type="text" id="${field_estado}" value="%%">
<br>Modificar en el listado de comandas<br>
</label>
<button id=${btn_guardar} class="btn5">Guardar</button>
<button id=${btn_borrar} class="rojo">Borrar</button>
</fieldset>
`;
var currentData = {};
var currentPersonaID = "";
var divact = document.getElementById(div_actions);
function loadActions() {
divact.innerHTML = "";
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
document.getElementById(field_persona).value = value;
});
Object.entries(SC_actions).forEach((category) => {
addCategory(
divact,
category[0],
SC_actions_icons[category[0]],
category[1],
currentData,
(values) => {
document.getElementById(field_comanda).value = SC_parse(values);
}
);
});
function loadActions() {
divact.innerHTML = "";
addCategory_Personas(divact, SC_Personas, currentPersonaID, (value) => {
document.getElementById(field_persona).value = value;
});
Object.entries(SC_actions).forEach((category) => {
addCategory(
divact,
category[0],
SC_actions_icons[category[0]],
category[1],
currentData,
(values) => {
document.getElementById(field_comanda).value = SC_parse(values);
}
);
});
}
loadActions();
DB.get('supercafe', mid).then((data) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = mid;
document.getElementById(field_fecha).value = data["Fecha"] || CurrentISODate();
document.getElementById(field_persona).value = data["Persona"] || "";
currentPersonaID = data["Persona"] || "";
document.getElementById(field_comanda).value =
SC_parse(JSON.parse(data["Comanda"] || "{}")) || "";
document.getElementById(field_notas).value = data["Notas"] || "";
document.getElementById(field_estado).value = data["Estado"] || "%%";
currentData = JSON.parse(data["Comanda"] || "{}");
loadActions();
}
loadActions();
gun
.get(TABLE)
.get("supercafe")
.get(mid)
.once((data, key) => {
function load_data(data, ENC = "") {
document.getElementById(nameh1).innerText = key;
document.getElementById(field_fecha).value = data["Fecha"];
document.getElementById(field_persona).value =
data["Persona"] || "";
currentPersonaID = data["Persona"] || "";
document.getElementById(field_comanda).value =
SC_parse(JSON.parse(data["Comanda"] || "{}")) || "";
document.getElementById(field_notas).value = data["Notas"] || "";
document.getElementById(field_estado).value = data["Estado"] || "";
currentData = JSON.parse(data["Comanda"] || "{}");
loadActions();
}
if (typeof data == "string") {
SEA.decrypt(data, SECRET, (data) => {
load_data(data, "%E");
});
} else {
load_data(data);
}
});
document.getElementById(btn_guardar).onclick = () => {
if (document.getElementById(field_persona).value == "") {
alert("¡Hay que elegir una persona!");
return;
}
var data = {
Fecha: document.getElementById(field_fecha).value,
Persona: document.getElementById(field_persona).value,
Comanda: JSON.stringify(currentData),
Notas: document.getElementById(field_notas).value,
Estado: document
.getElementById(field_estado)
.value.replace("%%", "Pedido"),
};
var enc = SEA.encrypt(data, SECRET, (encrypted) => {
document.getElementById("actionStatus").style.display = "block";
betterGunPut(gun.get(TABLE).get("supercafe").get(mid), encrypted);
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("supercafe");
}, 1500);
});
if (typeof data == "string") {
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
load_data(data, "%E");
}, 'supercafe', mid);
} else {
load_data(data || {});
}
});
document.getElementById(btn_guardar).onclick = () => {
// Check if button is already disabled to prevent double-clicking
var guardarBtn = document.getElementById(btn_guardar);
if (guardarBtn.disabled) return;
// Validate before disabling button
if (document.getElementById(field_persona).value == "") {
alert("¡Hay que elegir una persona!");
return;
}
// Disable button after validation passes
guardarBtn.disabled = true;
guardarBtn.style.opacity = "0.5";
var data = {
Fecha: document.getElementById(field_fecha).value,
Persona: document.getElementById(field_persona).value,
Comanda: JSON.stringify(currentData),
Notas: document.getElementById(field_notas).value,
Estado: document
.getElementById(field_estado)
.value.replace("%%", "Pedido"),
};
document.getElementById(btn_borrar).onclick = () => {
if (
confirm(
"¿Quieres borrar esta comanda? - NO se actualizaran los puntos de la persona asignada."
) == true
) {
betterGunPut(gun.get(TABLE).get("supercafe").get(mid), null);
document.getElementById("actionStatus").style.display = "block";
DB.put('supercafe', mid, data).then(() => {
toastr.success("Guardado!");
setTimeout(() => {
document.getElementById("actionStatus").style.display = "none";
setUrlHash("supercafe");
}, 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 la comanda");
});
};
document.getElementById(btn_borrar).onclick = () => {
if (
confirm(
"¿Quieres borrar esta comanda? - NO se actualizará el monedero de la persona asignada."
) == true
) {
DB.del('supercafe', mid).then(() => {
setTimeout(() => {
setUrlHash("supercafe");
}, 1500);
}
};
},
index: function () {
var tts = false;
var sc_nobtn = "";
if (urlParams.get("sc_nobtn") == "yes") {
sc_nobtn = "pointer-events: none; opacity: 0.5";
}, SAVE_WAIT);
});
}
setTimeout(() => {
tts = true;
console.log("TTS Enabled");
toastr.info("Texto a voz disponible");
}, 6500);
const tablebody = safeuuid();
const tablebody2 = safeuuid();
var btn_new = safeuuid();
var tts_check = safeuuid();
var old = {};
container.innerHTML = `
<h1>SuperCafé</h1>
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
<br>
<label>
<b>Habilitar avisos:</b>
<input type="checkbox" id="${tts_check}" style="height: 25px;width: 25px;">
</label>
};
},
index: function () {
if (!checkRole("supercafe")) {
setUrlHash("index");
return;
}
var tts = false;
var sc_nobtn = "";
if (urlParams.get("sc_nobtn") == "yes") {
sc_nobtn = "pointer-events: none; opacity: 0.5";
}
var ev = setTimeout(() => {
tts = true;
console.log("TTS Enabled");
toastr.info("Texto a voz disponible");
}, 6500);
EventListeners.Timeout.push(ev);
const tablebody = safeuuid();
const tablebody2 = safeuuid();
var btn_new = safeuuid();
var totalprecio = safeuuid();
var tts_check = safeuuid();
var old = {};
container.innerHTML = `
<h1>Cafetería - Total: <span id="${totalprecio}">0</span>c</h1>
<button id="${btn_new}" style="${sc_nobtn};">Nueva comanda</button>
<br>
<label>
<b>Habilitar avisos:</b>
<input type="checkbox" id="${tts_check}" style="height: 25px;width: 25px;">
</label>
<details style="background: beige; padding: 15px; border-radius: 15px; border: 2px solid black" open>
<summary>Todas las comandas</summary>
<div id="cont1"></div>
</details>
<br>
<details style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black" open>
<summary>Deudas</summary>
<div id="cont2"></div>
</details>
`;
//Todas las comandas
TS_IndexElement(
"supercafe",
[
{
key: "Persona",
type: "persona",
default: "",
label: "Persona",
},
{
key: "Estado",
type: "comanda-status",
default: "",
label: "Estado",
},
{
key: "Comanda",
type: "comanda",
default: "",
label: "Comanda",
},
],
gun.get(TABLE).get("supercafe"),
document.querySelector("#cont1"),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == "Pedido") {
new_tr.style.backgroundColor = "#FFFFFF";
}
if (data.Estado == "En preparación") {
new_tr.style.backgroundColor = "#FFCCCB";
}
if (data.Estado == "Listo") {
new_tr.style.backgroundColor = "gold";
}
if (data.Estado == "Entregado") {
new_tr.style.backgroundColor = "lightgreen";
}
if (data.Estado == "Deuda") {
new_tr.style.backgroundColor = "#f5d3ff";
}
<details style="background: beige; padding: 15px; border-radius: 15px; border: 2px solid black" open>
<summary>Todas las comandas</summary>
<div id="cont1"></div>
</details>
<br>
<details style="background: lightpink; padding: 15px; border-radius: 15px; border: 2px solid black" open>
<summary>Deudas</summary>
<div id="cont2"></div>
</details>
`;
var config = [
{
key: "Persona",
type: "persona",
default: "",
label: "Persona",
},
{
key: "Estado",
type: "comanda-status",
default: "",
label: "Estado",
},
{
key: "Comanda",
type: "comanda",
default: "",
label: "Comanda",
},
];
if (!checkRole("supercafe:edit")) {
config = [
{
key: "Persona",
type: "persona",
default: "",
label: "Persona",
},
(data) => {
if (data.Estado == "Deuda") {
return true;
}
var key = data._key;
if (old[key] == undefined) {
old[key] = "";
}
if (old[key] != data.Estado) {
if (tts && document.getElementById(tts_check).checked) {
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
JSON.parse(data.Comanda)["Selección"]
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
console.log("TTS: " + msg);
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = 0.9;
// utterance.voice = speechSynthesis.getVoices()[7]
speechSynthesis.speak(utterance);
}
}
old[key] = data.Estado;
}
);
//Deudas
TS_IndexElement(
"supercafe",
[
{
key: "Persona",
type: "persona",
default: "",
label: "Persona",
},
{
key: "Estado",
type: "comanda-status",
default: "",
label: "Estado",
},
{
key: "Comanda",
type: "comanda",
default: "",
label: "Comanda",
},
],
gun.get(TABLE).get("supercafe"),
document.querySelector("#cont2"),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
if (data.Estado == "Pedido") {
new_tr.style.backgroundColor = "#FFFFFF";
}
if (data.Estado == "En preparación") {
new_tr.style.backgroundColor = "#FFCCCB";
}
if (data.Estado == "Listo") {
new_tr.style.backgroundColor = "gold";
}
if (data.Estado == "Entregado") {
new_tr.style.backgroundColor = "lightgreen";
}
if (data.Estado == "Deuda") {
new_tr.style.backgroundColor = "#f5d3ff";
}
{
key: "Comanda",
type: "comanda",
default: "",
label: "Comanda",
},
(data) => {
if (data.Estado != "Deuda") {
return true;
}
var key = data._key;
if (old[key] == undefined) {
old[key] = "";
}
if (old[key] != data.Estado) {
if (tts && document.getElementById(tts_check).checked) {
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
JSON.parse(data.Comanda)["Selección"]
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
console.log("TTS: " + msg);
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = 0.9;
// utterance.voice = speechSynthesis.getVoices()[7]
speechSynthesis.speak(utterance);
}
}
old[key] = data.Estado;
];
}
//Todas las comandas
var comandasTot = {};
function calcPrecio() {
var tot = 0;
Object.values(comandasTot).forEach((precio) => {
tot += precio;
});
document.getElementById(totalprecio).innerText = tot;
return tot;
}
TS_IndexElement(
"supercafe",
config,
"supercafe",
document.querySelector("#cont1"),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
comandasTot[data._key] = SC_priceCalc(JSON.parse(data.Comanda))[0];
calcPrecio();
if (data.Estado == "Pedido") {
new_tr.style.backgroundColor = "#FFFFFF";
}
);
if (data.Estado == "En preparación") {
new_tr.style.backgroundColor = "#FFCCCB";
}
if (data.Estado == "Listo") {
new_tr.style.backgroundColor = "gold";
}
if (data.Estado == "Entregado") {
new_tr.style.backgroundColor = "lightgreen";
}
if (data.Estado == "Deuda") {
new_tr.style.backgroundColor = "#f5d3ff";
}
},
(data) => {
if (data.Estado == "Deuda") {
return true;
}
var key = data._key;
if (old[key] == undefined) {
old[key] = "";
}
if (old[key] != data.Estado) {
if (tts && document.getElementById(tts_check).checked) {
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
JSON.parse(data.Comanda)["Selección"]
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = 0.9;
// utterance.voice = speechSynthesis.getVoices()[7]
speechSynthesis.speak(utterance);
}
}
old[key] = data.Estado;
}
);
//Deudas
TS_IndexElement(
"supercafe",
config,
"supercafe",
document.querySelector("#cont2"),
(data, new_tr) => {
// new_tr.style.backgroundColor = "#FFCCCB";
comandasTot[data._key] = 0; // No mostrar comandas en deuda.
calcPrecio();
if (data.Estado == "Pedido") {
new_tr.style.backgroundColor = "#FFFFFF";
}
if (data.Estado == "En preparación") {
new_tr.style.backgroundColor = "#FFCCCB";
}
if (data.Estado == "Listo") {
new_tr.style.backgroundColor = "gold";
}
if (data.Estado == "Entregado") {
new_tr.style.backgroundColor = "lightgreen";
}
if (data.Estado == "Deuda") {
new_tr.style.backgroundColor = "#f5d3ff";
}
},
(data) => {
if (data.Estado != "Deuda") {
return true;
}
var key = data._key;
if (old[key] == undefined) {
old[key] = "";
}
if (old[key] != data.Estado) {
if (tts && document.getElementById(tts_check).checked) {
var msg = `Comanda de ${SC_Personas[data.Persona].Region}. - ${
JSON.parse(data.Comanda)["Selección"]
}. - ${SC_Personas[data.Persona].Nombre}. - ${data.Estado}`;
let utterance = new SpeechSynthesisUtterance(msg);
utterance.rate = 0.9;
// utterance.voice = speechSynthesis.getVoices()[7]
speechSynthesis.speak(utterance);
}
}
old[key] = data.Estado;
}
);
if (!checkRole("supercafe:edit")) {
document.getElementById(btn_new).style.display = "none";
} else {
document.getElementById(btn_new).onclick = () => {
setUrlHash("supercafe," + safeuuid(""));
};
},
}
}
},
};

View File

@@ -14,7 +14,7 @@ document.getElementById("reload").addEventListener("click", function () {
});
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").then((reg) => {
navigator.serviceWorker.register("sw.js").then((reg) => {
reg.addEventListener("updatefound", () => {
// A wild service worker has appeared in reg.installing!
newWorker = reg.installing;

View File

@@ -1,27 +1,26 @@
var cacheName = 'telesec_2025-07-30_4';
var CACHE = "telesec_%%VERSIONCO%%";
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
);
self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheName)
.then(cache => cache.addAll(["index.html", "icon512_maskable.png", "icon512_rounded.png", "cola_cao.jpg", "manifest.json", "static/webrtc.js", "static/synchronous.js", "static/sea.js", "static/gun.js", "static/toastr.min.css", "static/store.js", "static/simplemde.min.css", "static/doublescroll.js", "static/rindexed.js", "static/yson.js", "static/toastr.min.js", "static/showdown.min.js", "static/load.js", "static/radix.js", "static/axe.js", "static/TeleSec.jpg", "static/path.js", "static/radisk.js", "static/open.js", "static/simplemde.min.js", "static/jquery.js", "static/euskaditech-css/README.md", "static/euskaditech-css/.gitignore", "static/euskaditech-css/simple.css", "static/euskaditech-css/.git", "static/euskaditech-css/logos/EuskadiTech/long nobg color.svg", "static/euskaditech-css/logos/EuskadiTech/nobg color.png", "static/euskaditech-css/logos/EuskadiTech/nobg color.svg", "static/euskaditech-css/logos/EuskadiTech/long nobg color.png", "static/ico/add.png", "static/ico/user_generic.png", "static/ico/keyboard_key_g.png", "static/ico/keyboard_key_p.png", "static/ico/snowflake.png", "static/ico/coffee_bean.png", "static/ico/arrow_up_red.png", "static/ico/milk (1).png", "static/ico/azucar-moreno.png", "static/ico/arrow_down_blue.png", "static/ico/camera2.png", "static/ico/fire.png", "static/ico/cookies.png", "static/ico/checkbox_unchecked.png", "static/ico/wheat.png", "static/ico/sacarina.jpg", "static/ico/arrow_left_green.png", "static/ico/tea_bag.png", "static/ico/cow.png", "static/ico/connect_ko.svg", "static/ico/milk.png", "static/ico/user.png", "static/ico/stevia.jpg", "static/ico/water_tap.png", "static/ico/thermometer2.png", "static/ico/statusok.png", "static/ico/lollipop.png", "static/ico/colacao.jpg", "static/ico/delete.png", "static/ico/cereales.png", "static/ico/checkbox.png", "static/ico/azucar-blanco.jpg", "static/ico/preferences.png", "static/ico/sizes.png", "static/ico/stevia-gotas.webp", "static/ico/connect_ok.svg", "static/ico/layered1/Azucar-Az. Moreno.png", "static/ico/layered1/Azucar-Stevia (Pastillas).png", "static/ico/layered1/Azucar-Sacarina.png", "static/ico/layered1/Selección-ColaCao.png", "static/ico/layered1/Temperatura-Templado.png", "static/ico/layered1/Tamaño-Pequeño.png", "static/ico/layered1/Leche-Sin lactosa.png", "static/ico/layered1/Cafeina-Sin.png", "static/ico/layered1/Leche-Vegetal.png", "static/ico/layered1/Leche-de Vaca.png", "static/ico/layered1/Selección-Infusion.png", "static/ico/layered1/Azucar-Sin.png", "static/ico/layered1/Selección-Leche.png", "static/ico/layered1/Temperatura-Frio.png", "static/ico/layered1/Background.png", "static/ico/layered1/Azucar-Edulcorante.png", "static/ico/layered1/Cafeina-Con.png", "static/ico/layered1/Selección-CaféLeche.png", "static/ico/layered1/Tamaño-Grande.png", "static/ico/layered1/Selección-CafeSolo.png", "static/ico/layered1/Leche-Agua.png", "static/ico/layered1/Temperatura-Caliente.png", "static/ico/layered1/Azucar-Stevia (Gotas).png", "static/ico/layered1/Azucar-Az. Blanco.png"]))
);
});
self.addEventListener('message', function (event) {
if (event.data.action === 'skipWaiting') {
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
// workbox.routing.registerRoute(
// new RegExp("/*"),
// new workbox.strategies.StaleWhileRevalidate({
// cacheName: CACHE,
// })
// );
// All but couchdb
workbox.routing.registerRoute(
({ url }) =>
!url.pathname.startsWith("/_couchdb/") && url.origin === self.location.origin,
new workbox.strategies.NetworkFirst({
cacheName: CACHE,
})
);