Compare commits
426 Commits
v2025-07-3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c940c846b8 | ||
|
|
7822e46b61 | ||
|
|
a39ecca990 | ||
|
|
80e9262bcb | ||
|
|
cd456ab9f1 | ||
|
|
c0c40ecd99 | ||
|
|
98c6ba39f3 | ||
|
|
03f52c8a92 | ||
|
|
f655a736b3 | ||
|
|
89a68f27da | ||
|
|
4d322e5696 | ||
|
|
0138e0ca69 | ||
|
|
3e8542c9de | ||
|
|
90df81d308 | ||
|
|
53941da35c | ||
|
|
105c911c59 | ||
|
|
cb12894455 | ||
|
|
9d808ed63e | ||
|
|
d0593d3d46 | ||
|
|
8b7d0258ae | ||
|
|
9a760a1d24 | ||
|
|
e1f780ea11 | ||
|
|
7ad2e9c142 | ||
|
|
879554a7ab | ||
|
|
0ef6e5a233 | ||
|
|
d905e86bbf | ||
|
|
3764473b5b | ||
|
|
382e31158a | ||
|
|
09a9a95df0 | ||
|
|
b04dbbf19d | ||
|
|
7619444556 | ||
|
|
076aa45337 | ||
|
|
0b1419fae2 | ||
|
|
74afb2a499 | ||
|
|
543d1c3202 | ||
|
|
75947d3468 | ||
|
|
9ab0472e2a | ||
|
|
aa993df2bf | ||
|
|
e0da65811e | ||
|
|
eb6a956cdc | ||
|
|
dc4ba25b20 | ||
|
|
129188c022 | ||
|
|
9d4ce881c6 | ||
|
|
4e1727adc3 | ||
|
|
db5b07bb44 | ||
|
|
61b8cb8af4 | ||
|
|
2ee03aa204 | ||
|
|
8b29e3f425 | ||
|
|
31697f2448 | ||
|
|
468d08110d | ||
|
|
07e2c9a98e | ||
|
|
8b6140929e | ||
|
|
05ea9a9d8b | ||
|
|
75c319c701 | ||
|
|
8a9fee46da | ||
|
|
6d7def5f18 | ||
|
|
ddfd653d68 | ||
|
|
b0160b3b66 | ||
|
|
d6809e51d1 | ||
|
|
0db86f3dd2 | ||
|
|
dd195c5157 | ||
|
|
f472baacf6 | ||
|
|
1e5de2c686 | ||
|
|
bf9ba4ceef | ||
|
|
c2eac955fe | ||
|
|
a02d7956ca | ||
|
|
f96a408852 | ||
|
|
8afe2eedee | ||
|
|
4faea51004 | ||
|
|
70ea752992 | ||
|
|
492889b9e1 | ||
|
|
ea54dc5471 | ||
|
|
8ff431ca10 | ||
|
|
7d5fe84b3a | ||
|
|
f287eb63f6 | ||
|
|
565d88def8 | ||
|
|
92feb05a0d | ||
|
|
013413a01c | ||
|
|
9e7f8ebd1f | ||
|
|
07d657002e | ||
|
|
24520d1f01 | ||
|
|
6b8202992d | ||
|
|
bc0755b9bf | ||
|
|
cb61374582 | ||
|
|
cb70222c04 | ||
|
|
dfcd22fadf | ||
|
|
7277c6ab34 | ||
|
|
fbf0a8c9e4 | ||
|
|
ee219e1d96 | ||
|
|
90b8223385 | ||
|
|
0bc662dbde | ||
|
|
15df8d12fe | ||
|
|
a9cdfb567a | ||
|
|
f0a6f3b6b3 | ||
|
|
8802952e5a | ||
|
|
ab4a05bc7f | ||
|
|
648854190e | ||
|
|
13a4367c92 | ||
|
|
2258e74960 | ||
|
|
fd63885507 | ||
|
|
4b88679b37 | ||
|
|
dd9fda10f7 | ||
|
|
3dbaa9bd33 | ||
|
|
3402183f3c | ||
|
|
1bc9aa5295 | ||
|
|
c946dad334 | ||
|
|
a32aa89a56 | ||
|
|
4d1952d998 | ||
|
|
5fe308eac6 | ||
|
|
ad46651ed8 | ||
|
|
0eb519dea4 | ||
|
|
9b0d33710f | ||
|
|
28a0fced87 | ||
|
|
60a7649c36 | ||
|
|
cca21ac3d3 | ||
|
|
78c0abf92d | ||
|
|
d40d600a49 | ||
|
|
0e1ab0c619 | ||
|
|
0366f62dfb | ||
|
|
33594b2508 | ||
|
|
db9626aa7b | ||
|
|
ba022dea3c | ||
|
|
f0e32b4ad0 | ||
|
|
bbbc8b1d63 | ||
|
|
af2f642d45 | ||
|
|
9f00b97677 | ||
|
|
196245ffa0 | ||
|
|
7cf1bf40c7 | ||
|
|
860f6019ad | ||
|
|
4fefdcaf3d | ||
|
|
9a22545ec2 | ||
|
|
49a021b9dd | ||
|
|
0cd6048bf2 | ||
|
|
1bec976efc | ||
|
|
a03a224cda | ||
|
|
0bdd3ba8b1 | ||
|
|
96b3c60568 | ||
|
|
811fabfced | ||
|
|
a3d9278d6f | ||
|
|
4e67381cf0 | ||
|
|
f1593de431 | ||
|
|
889722451c | ||
|
|
bb9c1ee7d3 | ||
|
|
fc2e4d27d2 | ||
|
|
6bde2fb2b8 | ||
|
|
7a510329ba | ||
|
|
a5cc4e7cc7 | ||
|
|
c77ac5c264 | ||
|
|
f14d19f59a | ||
|
|
fccd9308e2 | ||
|
|
2265ad28f4 | ||
|
|
b1993ba83a | ||
|
|
d00d4c7af2 | ||
|
|
4d175d9aa1 | ||
|
|
b0fa3d0844 | ||
|
|
617711fb1a | ||
|
|
f46ec17c03 | ||
|
|
de163f7f9b | ||
|
|
3250669dc9 | ||
|
|
d00d004dd8 | ||
|
|
b38d470b02 | ||
|
|
0f2a894edb | ||
|
|
ad71ceae21 | ||
|
|
6e3e809435 | ||
|
|
4719c346f5 | ||
|
|
2e070ea7fd | ||
|
|
5f573c49be | ||
|
|
9524c9e3f3 | ||
|
|
b3a6a19f95 | ||
|
|
332b39aa23 | ||
|
|
14ee3ab4a0 | ||
|
|
75ce2aa207 | ||
|
|
48b68eff83 | ||
|
|
3124540f4f | ||
|
|
42728ab445 | ||
|
|
af5141099d | ||
|
|
1ce4207294 | ||
|
|
9d3d6bc363 | ||
|
|
74dc7fe404 | ||
|
|
2f3efbcf66 | ||
|
|
b3ff2a7a9d | ||
|
|
2b4e28eea6 | ||
|
|
f7b3d95526 | ||
|
|
553f43c7b3 | ||
|
|
8b1185b507 | ||
|
|
eecab547df | ||
|
|
b069f7db61 | ||
|
|
424d767549 | ||
|
|
4f3d16326c | ||
|
|
ac68228e26 | ||
|
|
673d64e720 | ||
|
|
ec7746f79f | ||
|
|
6ceae4f1d5 | ||
|
|
c50c29f743 | ||
|
|
34f61777c3 | ||
|
|
9c81cdb1e0 | ||
|
|
bdd880bc24 | ||
|
|
aa01eaeaa5 | ||
|
|
fc1aa567bc | ||
|
|
cc3d694ce3 | ||
|
|
f11760d867 | ||
|
|
fc4170acb8 | ||
|
|
98bda7db5d | ||
|
|
3bfbdc11af | ||
|
|
a59a26fa29 | ||
|
|
a5de1b3855 | ||
|
|
0624103778 | ||
|
|
cd6e4e8b64 | ||
|
|
fb9d574ff4 | ||
|
|
d27b3ec90c | ||
|
|
7d5d631b05 | ||
|
|
f13218e0b1 | ||
|
|
ac12dc627d | ||
|
|
c550812deb | ||
|
|
9efb05bc1e | ||
|
|
db244e2953 | ||
|
|
a3098a15f2 | ||
|
|
12b90e3e1e | ||
|
|
1763de12bd | ||
|
|
92dc877942 | ||
|
|
841f063695 | ||
|
|
e6301bfb64 | ||
|
|
7e1c6f1bf8 | ||
|
|
5adff05283 | ||
|
|
6f3108134b | ||
|
|
d5650b2e3b | ||
|
|
e3b062d9ea | ||
|
|
6bd797e5bb | ||
|
|
ac3b43361c | ||
|
|
b9d3c5a10d | ||
|
|
d9b379ec42 | ||
|
|
1f3ec25f32 | ||
|
|
2458082968 | ||
|
|
5f59f9f0c0 | ||
|
|
28ffd17bed | ||
|
|
e2446de20f | ||
|
|
c33b9e6ace | ||
|
|
73cb8d5614 | ||
|
|
69c981de3c | ||
|
|
bdc5c42cde | ||
|
|
5588c94ec3 | ||
|
|
42310c1f55 | ||
|
|
08431defbb | ||
|
|
52c15e4863 | ||
|
|
bab0ebc858 | ||
|
|
b64a9bc78b | ||
|
|
e9342b6fec | ||
|
|
b2617f605f | ||
|
|
c28aaecc66 | ||
|
|
e3d4998d80 | ||
|
|
be9c87790d | ||
|
|
e243f27b70 | ||
|
|
704241335e | ||
|
|
8406bd02c2 | ||
|
|
8a13e6e71b | ||
|
|
34b27b15ba | ||
|
|
14c081a615 | ||
|
|
81d200899d | ||
|
|
056f705f25 | ||
|
|
e084d42eb3 | ||
|
|
b4700f46fd | ||
|
|
369eb040af | ||
|
|
d21fae5052 | ||
|
|
1bc4364084 | ||
|
|
eebca7752b | ||
|
|
a42ad8d9ad | ||
|
|
63546605f2 | ||
|
|
357166b159 | ||
|
|
8866c68cee | ||
|
|
df46f378d7 | ||
|
|
b00b503d4c | ||
|
|
1575b61051 | ||
|
|
7d5667451c | ||
|
|
dbddd70ef9 | ||
|
|
027669cfd6 | ||
|
|
693f5c0af9 | ||
|
|
d596edc107 | ||
|
|
eeb540f269 | ||
|
|
a9b43a7f7d | ||
|
|
ba3cc7424d | ||
|
|
ed2148be8d | ||
|
|
36e3eaf958 | ||
|
|
77600a45d5 | ||
|
|
4988bdff1e | ||
|
|
8accafc599 | ||
|
|
bd9db0df22 | ||
|
|
7794d53802 | ||
|
|
014bc50690 | ||
|
|
ad93d958f9 | ||
|
|
c040215bb6 | ||
|
|
0132c88ce7 | ||
|
|
a74280f087 | ||
|
|
ccefabbc4a | ||
|
|
e29c02ebe5 | ||
|
|
2f1b036c3f | ||
|
|
f480dd5491 | ||
|
|
d4d4fd4b5f | ||
|
|
0feba6a09b | ||
|
|
4ba8141675 | ||
|
|
d71fca510f | ||
|
|
4303a02c1a | ||
|
|
afa224cbdc | ||
|
|
8de9af66e2 | ||
|
|
d13c66aa01 | ||
|
|
faf1d112c9 | ||
|
|
138af3a364 | ||
|
|
e0470d0dd1 | ||
|
|
ab75e3089e | ||
|
|
bfce6f32b6 | ||
|
|
737c0bd65f | ||
|
|
93a78a9d97 | ||
|
|
415334ca4a | ||
|
|
341119988c | ||
|
|
2567b65ff2 | ||
|
|
69458ecc97 | ||
|
|
7a391c338a | ||
|
|
9e8ba5a2e9 | ||
|
|
4f56f06208 | ||
|
|
c42ebadf2d | ||
|
|
62d1cafef9 | ||
|
|
6ec7a5feb1 | ||
|
|
a02f1cb588 | ||
|
|
aa3f312047 | ||
|
|
0fbc7cad39 | ||
|
|
293f57133b | ||
|
|
9ceb62dff0 | ||
|
|
7cb0fdea76 | ||
|
|
8820e16974 | ||
|
|
58e0c55480 | ||
|
|
7c7cec6ac8 | ||
|
|
8316377344 | ||
|
|
830fdd3206 | ||
|
|
9eadb04a93 | ||
|
|
cf0ecc4d27 | ||
|
|
6d1b3fa97e | ||
|
|
e4b9ebe7a4 | ||
|
|
416188a572 | ||
|
|
a8e496e78a | ||
|
|
20553d4c99 | ||
|
|
618ac4e6db | ||
|
|
3dd674b172 | ||
|
|
919ff298ba | ||
|
|
0599756b63 | ||
|
|
5b8641680d | ||
|
|
2b15258bd9 | ||
|
|
3ad83a523d | ||
|
|
cdd80eb4be | ||
|
|
f5aa8c4366 | ||
|
|
97c97b561f | ||
|
|
975ed0426c | ||
|
|
c3475e707b | ||
|
|
8321ba9373 | ||
|
|
c81202edf9 | ||
|
|
24a82911b6 | ||
|
|
3fcdc49502 | ||
|
|
1beadb739b | ||
|
|
846c0e8898 | ||
|
|
1ea087e683 | ||
|
|
096042e7c1 | ||
|
|
cb5e481c4d | ||
|
|
e91ac0c719 | ||
|
|
45f6cf82d5 | ||
|
|
0cccaafb38 | ||
|
|
b90bbcb8c2 | ||
|
|
ae9fcf4887 | ||
|
|
1ebc7079eb | ||
|
|
ea8726e0d6 | ||
|
|
83125bcf96 | ||
|
|
8823159a30 | ||
|
|
ad458de818 | ||
|
|
d2a11d65a7 | ||
|
|
1d8ddd8d8b | ||
|
|
9de720c920 | ||
|
|
0c4c9df396 | ||
|
|
8fc2e96985 | ||
|
|
852f9eddc9 | ||
|
|
d51e57fcee | ||
|
|
1be3885c70 | ||
|
|
52e6e3c09d | ||
|
|
310696a4c6 | ||
|
|
505f591839 | ||
|
|
b07dcd8683 | ||
|
|
fe5c54ff3e | ||
|
|
09fa34007b | ||
|
|
57171963be | ||
|
|
881dfedb14 | ||
|
|
1cc6e512ab | ||
|
|
0b748e45f8 | ||
|
|
e6688bf74c | ||
|
|
cbd5351981 | ||
|
|
31007d6979 | ||
|
|
7fb2d5f67f | ||
|
|
9b9bd730dd | ||
|
|
2e438c4b9e | ||
|
|
6ed2922ba4 | ||
|
|
b0ff7dd456 | ||
|
|
43dfa414aa | ||
|
|
1d73f3c427 | ||
|
|
72fba9e976 | ||
|
|
84719e4c6b | ||
|
|
9131d25dd5 | ||
|
|
c65b1b25a4 | ||
|
|
e8a9cff2ec | ||
|
|
d39f92554a | ||
|
|
84cc070e68 | ||
|
|
cf553295bb | ||
|
|
23c4122afb | ||
|
|
7414e3d6de | ||
|
|
5f2ef09672 | ||
|
|
f2588fc50f | ||
|
|
0ea230e99b | ||
|
|
a83be0f7c7 | ||
|
|
ed5755b005 | ||
|
|
0fcd6ccaba | ||
|
|
1ee951fc9f | ||
|
|
325465f9a5 | ||
|
|
7dee605d95 | ||
|
|
645e8c194a | ||
|
|
e35428f3ee | ||
|
|
def79a2015 | ||
|
|
64d11fe224 | ||
|
|
a016e06557 | ||
|
|
a274de3c4d | ||
|
|
0f2b2df969 | ||
|
|
8052d21fcc | ||
|
|
03c132a6bd |
@@ -1,47 +0,0 @@
|
||||
name: Build server
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*-*-*_*"]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: '${{ secrets.TOKEN_PULL }}'
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.13.1'
|
||||
- name: "Build app"
|
||||
run: "python3 build.py"
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
# Upload built files
|
||||
name: 'TeleSec-dist'
|
||||
path: ./dist
|
||||
- uses: mdallasanta/ssh-scp-deploy@v1.2.0
|
||||
with:
|
||||
local: './dist/' # Local file path - REQUIRED false - DEFAULT ./
|
||||
remote: '${{ secrets.FOLDER }}' # Remote file path - REQUIRED false - DEFAULT ~/
|
||||
host: ${{secrets.HOST}} # Remote server address - REQUIRED true
|
||||
port: 22 # Remote server port - REQUIRED false - DEFAULT 22
|
||||
user: ${{secrets.USER}} # Remote server user - REQUIRED true
|
||||
key: ${{secrets.KEY}} # Remote server private key - REQUIRED at least one of "password" or "key"
|
||||
- name: "zip-it-up"
|
||||
run: "zip -r TeleSec-dist.zip ./dist"
|
||||
- name: Upload as release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: |-
|
||||
TeleSec-dist.zip
|
||||
api_key: '${{secrets.TOKEN_PUSH_PLUS}}'
|
||||
222
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
# TeleSec - Secure Distributed Communication Application
|
||||
|
||||
TeleSec is a Spanish Progressive Web Application (PWA) built with vanilla JavaScript, HTML, and CSS that provides secure group communication using a local-first PouchDB datastore with optional CouchDB replication for syncing. The application allows users to join encrypted communication groups using group codes and secret keys.
|
||||
|
||||
**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**
|
||||
|
||||
## Working Effectively
|
||||
|
||||
### Build and Deploy Process
|
||||
- **Build the application (FASTEST BUILD EVER - 0.036 seconds):**
|
||||
- `cd /home/runner/work/TeleSec/TeleSec`
|
||||
- `python3 build.py`
|
||||
- **NEVER CANCEL**: Build completes in under 0.1 seconds. No timeout needed.
|
||||
- The build script copies files from `assets/` to `dist/` and processes template variables in `src/` files.
|
||||
|
||||
### Serving the Application
|
||||
- **Python HTTP Server (Recommended for development):**
|
||||
- `cd /home/runner/work/TeleSec/TeleSec/dist`
|
||||
- `python3 -m http.server 8000`
|
||||
- Access at: `http://localhost:8000`
|
||||
|
||||
- **Node.js HTTP Server (Alternative with CORS support):**
|
||||
- `cd /home/runner/work/TeleSec/TeleSec/dist`
|
||||
- `npx http-server . --port 8001 --cors`
|
||||
- **NEVER CANCEL**: First run takes ~14 seconds to download http-server package. Set timeout to 30+ seconds.
|
||||
- Access at: `http://localhost:8001`
|
||||
|
||||
### Development Environment Requirements
|
||||
- **Python 3.x** (for build script) - Version 3.12.3+ confirmed working
|
||||
- **Node.js and npm** (optional, for alternative serving) - Version 20.19.4+ confirmed working
|
||||
- **Web browser** (for testing the PWA functionality)
|
||||
|
||||
## Validation and Testing
|
||||
|
||||
### Mandatory Validation Steps
|
||||
1. **Build Validation:**
|
||||
- Run `python3 build.py` and verify it completes in under 1 second
|
||||
- Verify `dist/` directory is created with all assets and processed files
|
||||
- Check that template variables (%%PREFETCH%%, %%VERSIONCO%%, %%ASSETSJSON%%) are replaced
|
||||
|
||||
2. **Application Functionality Test:**
|
||||
- Start web server: `python3 -m http.server 8000` in `dist/` directory
|
||||
- Navigate to `http://localhost:8000` in browser
|
||||
- **CRITICAL LOGIN TEST:** Enter any group code (e.g., "TEST") and secret key (e.g., "SECRET123")
|
||||
- Click "Iniciar sesión" button
|
||||
- **VERIFY NETWORK CONNECTIVITY:** Confirm the header shows connected nodes (e.g., "TeleSec - TEST - (8 nodos)")
|
||||
- **SUCCESS INDICATORS:**
|
||||
- Application loads without errors
|
||||
- Login form accepts credentials
|
||||
- Distributed network connects (node count > 0)
|
||||
- No JavaScript console errors except expected WebSocket connection failures
|
||||
|
||||
3. **PWA Features Test:**
|
||||
- Verify Service Worker registration in browser console
|
||||
- Check manifest.json loads correctly
|
||||
- Confirm offline caching functionality
|
||||
|
||||
### Error Handling Validation
|
||||
- **Build Script Errors:** Python syntax errors will cause build to fail with clear error messages
|
||||
- **Network Connectivity:** Some WebSocket connections to gun-manhattan.herokuapp.com may fail - this is expected
|
||||
- **Browser Compatibility:** Application works in modern browsers supporting Service Workers
|
||||
|
||||
## Repository Structure
|
||||
|
||||
### Key Files and Directories
|
||||
```
|
||||
/home/runner/work/TeleSec/TeleSec/
|
||||
├── build.py # Main build script - processes template variables
|
||||
├── index.html # Build error fallback (should never be served)
|
||||
├── README.md # Basic repository information (minimal)
|
||||
├── LICENSE # Project license
|
||||
├── CNAME # GitHub Pages configuration
|
||||
├── .gitignore # Excludes dist/, radata/, node_modules/
|
||||
├── src/ # Source files with template variables
|
||||
│ ├── index.html # Main application HTML with %%PREFETCH%% variables
|
||||
│ ├── app_logic.js # Core application logic and authentication
|
||||
│ ├── app_modules.js # Application modules and utilities
|
||||
│ ├── config.js # Configuration and CouchDB setup
|
||||
│ ├── db.js # PouchDB wrapper and replication
|
||||
│ ├── pwa.js # Progressive Web App functionality
|
||||
│ ├── sw.js # Service Worker with cache configuration
|
||||
│ └── page/ # Individual page modules
|
||||
│ ├── login.js # Login functionality
|
||||
│ ├── index.js # Main dashboard
|
||||
│ ├── materiales.js # Materials management
|
||||
│ ├── personas.js # People management
|
||||
│ ├── supercafe.js # SuperCafé module
|
||||
│ ├── comedor.js # Dining hall module
|
||||
│ ├── importar.js # Data import functionality
|
||||
│ ├── exportar.js # Data export functionality
|
||||
│ ├── resumen_diario.js # Daily summary
|
||||
│ └── notificaciones.js # Notifications
|
||||
└── assets/ # Static assets copied to dist/
|
||||
├── manifest.json # PWA manifest
|
||||
├── *.png, *.jpg # Icons and images
|
||||
├── static/ # JavaScript libraries and CSS
|
||||
│ ├── euskaditech-css/ # CSS framework
|
||||
│ └── ico/ # Application icons
|
||||
└── page/ # Page-specific assets (empty placeholder)
|
||||
```
|
||||
|
||||
### Build Process Details
|
||||
The `build.py` script performs these operations:
|
||||
1. **Clean:** Removes existing `dist/` directory (if it exists)
|
||||
2. **Copy Assets:** Copies all files from `assets/` to `dist/`
|
||||
3. **Process Templates:** Processes files from `src/` and replaces:
|
||||
- `%%PREFETCH%%` - Generates link prefetch tags for all assets
|
||||
- `%%VERSIONCO%%` - Inserts version code "2025-08-04_1"
|
||||
- `%%ASSETSJSON%%` - Inserts JSON array of all asset files
|
||||
4. **Output:** Creates complete deployable application in `dist/`
|
||||
|
||||
## Application Architecture
|
||||
|
||||
### Technology Stack
|
||||
- **Frontend:** Vanilla JavaScript, HTML5, CSS3
|
||||
- **Data Layer:** PouchDB (local-first) with optional CouchDB replication
|
||||
- **Networking:** WebRTC for peer-to-peer connections (where applicable)
|
||||
- **Authentication:** Group codes + secret keys (converted to uppercase)
|
||||
- **Storage:** Browser LocalStorage + PouchDB local datastore
|
||||
- **PWA Features:** Service Worker, Web App Manifest
|
||||
|
||||
### Remote Sync (Optional)
|
||||
The application can optionally replicate to a remote CouchDB server for cloud backup and multi-device syncing. Configure the CouchDB server, database name, and credentials in the login/setup form in the application.
|
||||
|
||||
### Application Modules
|
||||
- **Login/Authentication:** Group-based access with secret keys
|
||||
- **Materials Management:** Track and manage materials/supplies
|
||||
- **People Management:** Manage group members
|
||||
- **SuperCafé:** Café/beverage ordering system
|
||||
- **Dining Hall:** Restaurant/meal management
|
||||
- **Import/Export:** Data backup and restoration
|
||||
- **Daily Summary:** Reports and analytics
|
||||
- **Notifications:** Alert system
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Making Changes to the Application
|
||||
1. **ALWAYS** edit files in `src/` directory, never `dist/`
|
||||
2. Run `python3 build.py` to rebuild after changes
|
||||
3. Refresh browser or restart web server to see changes
|
||||
4. Test authentication flow and network connectivity after any changes
|
||||
|
||||
### Adding New Features
|
||||
1. Create new JavaScript files in `src/page/` for new modules
|
||||
2. Add script references in `src/index.html`
|
||||
3. Update assets if new static files are needed
|
||||
4. Rebuild and test complete user workflows
|
||||
|
||||
### Debugging Common Issues
|
||||
- **Login Issues:** Check browser console for replication/auth errors and DB initialization logs
|
||||
- **Network Connectivity:** Verify remote CouchDB server is reachable and replication is active
|
||||
- **Build Issues:** Check Python syntax in build.py
|
||||
- **Performance:** Monitor browser DevTools Network tab for asset loading
|
||||
|
||||
## Validation Scenarios
|
||||
|
||||
### Complete User Workflow Test
|
||||
After making any changes, ALWAYS test this complete scenario:
|
||||
|
||||
1. **Build and Serve:**
|
||||
```bash
|
||||
cd /home/runner/work/TeleSec/TeleSec
|
||||
python3 build.py
|
||||
cd dist
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
2. **Login Test:**
|
||||
- Navigate to `http://localhost:8000`
|
||||
- Enter group code: "TEST"
|
||||
- Enter secret key: "SECRET123"
|
||||
- Click "Iniciar sesión"
|
||||
- Verify header shows: "TeleSec - TEST" and that the login is accepted
|
||||
|
||||
3. **Network Connectivity Test:**
|
||||
- Confirm green connection indicator appears (bottom right)
|
||||
- Check browser console shows PouchDB replication logs when a remote is configured
|
||||
- Verify heartbeat/last-seen docs are being updated in the local DB
|
||||
|
||||
4. **PWA Functionality Test:**
|
||||
- Check Service Worker registers successfully
|
||||
- Verify offline caching works (Network tab → Offline)
|
||||
- Test manifest.json loads correctly
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
### Essential Operations
|
||||
```bash
|
||||
# Build application (< 0.1 seconds)
|
||||
python3 build.py
|
||||
|
||||
# Serve with Python (most compatible)
|
||||
cd dist && python3 -m http.server 8000
|
||||
|
||||
# Serve with Node.js (advanced features)
|
||||
cd dist && npx http-server . --port 8001 --cors
|
||||
|
||||
# Clean rebuild
|
||||
rm -rf dist && python3 build.py
|
||||
|
||||
# Check build output
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
### File Structure Verification
|
||||
```bash
|
||||
# Verify all source files exist
|
||||
find src/ -name "*.js" -o -name "*.html" | sort
|
||||
|
||||
# Check template variable processing
|
||||
grep -r "%%.*%%" dist/ || echo "All template variables processed correctly"
|
||||
|
||||
# Verify assets copied correctly
|
||||
diff -r assets/ dist/ --exclude="*.js" --exclude="*.html" || echo "Some differences expected due to processing"
|
||||
```
|
||||
|
||||
**CRITICAL REMINDERS:**
|
||||
- **NEVER CANCEL**: Builds complete in under 0.1 seconds - no timeout needed
|
||||
- **ALWAYS test login and network connectivity** after changes
|
||||
- **Edit source files in `src/` directory only**, never `dist/`
|
||||
- **Test with real user credentials** to verify distributed networking
|
||||
- **Monitor browser console** for connection status and errors
|
||||
45
.github/workflows/static.yml
vendored
Normal file
@@ -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
|
||||
46
.github/workflows/windows-agent-release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Build Windows Agent (Release)
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-windows-agent:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Build hidden EXE with PyInstaller
|
||||
shell: bash
|
||||
run: |
|
||||
cd python_sdk ; pyinstaller --noconfirm --clean --onefile --noconsole --hidden-import=telesec_couchdb --name telesec-windows-agent windows_agent.py
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: telesec-windows-agent
|
||||
path: python_sdk/dist/telesec-windows-agent.exe
|
||||
|
||||
- name: Upload asset to GitHub Release
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: python_sdk/dist/telesec-windows-agent.exe
|
||||
15
.gitignore
vendored
@@ -1 +1,14 @@
|
||||
dist/*
|
||||
dist/*
|
||||
radata/*
|
||||
node_modules/*
|
||||
.DS_Store
|
||||
._*
|
||||
# Python
|
||||
__pycache__/*
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info/*
|
||||
*.egg
|
||||
.venv/*
|
||||
venv/*
|
||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "assets/static/euskaditech-css"]
|
||||
path = assets/static/euskaditech-css
|
||||
url = https://git.tech.eus/EuskadiTech/css.git
|
||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"tobermory.es6-string-html",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
92
README.md
@@ -1,2 +1,94 @@
|
||||
# TeleSec
|
||||
Nuevo programa de datos
|
||||
|
||||
## Python SDK (CouchDB directo)
|
||||
|
||||
Se añadió un SDK Python en `python_sdk/` para acceder directamente a CouchDB (sin replicación local), compatible con el formato de cifrado de `TS_encrypt`:
|
||||
|
||||
- Formato: `RSA{...}`
|
||||
- Algoritmo: `CryptoJS.AES.encrypt(payload, secret)` (modo passphrase/OpenSSL)
|
||||
|
||||
### Instalación
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Uso rápido
|
||||
|
||||
```python
|
||||
from python_sdk import TeleSecCouchDB
|
||||
|
||||
db = TeleSecCouchDB(
|
||||
server_url="https://tu-couchdb",
|
||||
dbname="telesec",
|
||||
username="usuario",
|
||||
password="clave",
|
||||
secret="SECRET123",
|
||||
)
|
||||
|
||||
# Guardar cifrado (como TS_encrypt)
|
||||
db.put("personas", "abc123", {"nombre": "Ana"}, encrypt=True)
|
||||
|
||||
# Leer y descifrar
|
||||
obj = db.get("personas", "abc123", decrypt=True)
|
||||
|
||||
# Listar una tabla
|
||||
rows = db.list("personas", decrypt=True)
|
||||
for row in rows:
|
||||
print(row.id, row.data)
|
||||
```
|
||||
|
||||
API principal:
|
||||
|
||||
- `TeleSecCouchDB.put(table, item_id, data, encrypt=True)`
|
||||
- `TeleSecCouchDB.get(table, item_id, decrypt=True)`
|
||||
- `TeleSecCouchDB.list(table, decrypt=True)`
|
||||
- `TeleSecCouchDB.delete(table, item_id)`
|
||||
- `ts_encrypt(value, secret)` / `ts_decrypt(value, secret)`
|
||||
|
||||
## Agente Windows (Gest-Aula > Ordenadores)
|
||||
|
||||
Se añadió soporte para control de ordenadores del aula:
|
||||
|
||||
- Tabla: `aulas_ordenadores`
|
||||
- Campos reportados por agente: `Hostname`, `UsuarioActual`, `AppActualEjecutable`, `AppActualTitulo`, `LastSeenAt`
|
||||
- Control remoto: `ShutdownBeforeDate` (programado desde web a `hora_servidor + 2 minutos`)
|
||||
|
||||
### Ejecutar agente en Windows
|
||||
|
||||
El agente usa un archivo de configuración en la carpeta personal del usuario:
|
||||
|
||||
- Ruta por defecto: `~/.telesec/windows_agent.json`
|
||||
- Se crea automáticamente si no existe
|
||||
|
||||
```bash
|
||||
python -m python_sdk.windows_agent --once
|
||||
```
|
||||
|
||||
Ejemplo del JSON de configuración:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": "https://tu-couchdb",
|
||||
"db": "telesec",
|
||||
"user": "usuario",
|
||||
"password": "clave",
|
||||
"secret": "SECRET123",
|
||||
"machine_id": "",
|
||||
"interval": 15
|
||||
}
|
||||
```
|
||||
|
||||
También puedes sobrescribir valores por CLI (`--server`, `--secret`, etc.).
|
||||
|
||||
Opciones útiles:
|
||||
|
||||
- `--once`: una sola iteración
|
||||
- `--interval 15`: intervalo (segundos)
|
||||
- `--dry-run`: no apaga realmente, solo simula
|
||||
- `--config <ruta>`: ruta alternativa del archivo JSON
|
||||
|
||||
### Hora de servidor (sin depender del reloj local)
|
||||
|
||||
El frontend y el agente usan la hora del servidor (cabecera HTTP `Date` de CouchDB) para comparar `ShutdownBeforeDate`.
|
||||
|
||||
85
_assets.json
@@ -1,85 +0,0 @@
|
||||
[
|
||||
"icon512_maskable.png",
|
||||
"icon512_rounded.png",
|
||||
"manifest.json",
|
||||
"static/axe.js",
|
||||
"static/doublescroll.js",
|
||||
"static/gun.js",
|
||||
"static/jquery.js",
|
||||
"static/load.js",
|
||||
"static/open.js",
|
||||
"static/path.js",
|
||||
"static/radisk.js",
|
||||
"static/radix.js",
|
||||
"static/rindexed.js",
|
||||
"static/sea.js",
|
||||
"static/showdown.min.js",
|
||||
"static/simplemde.min.css",
|
||||
"static/simplemde.min.js",
|
||||
"static/store.js",
|
||||
"static/synchronous.js",
|
||||
"static/TeleSec.jpg",
|
||||
"static/toastr.min.css",
|
||||
"static/toastr.min.js",
|
||||
"static/webrtc.js",
|
||||
"static/yson.js",
|
||||
"static/ico/add.png",
|
||||
"static/ico/azucar-moreno.png",
|
||||
"static/ico/azucar-blanco.jpg",
|
||||
"static/ico/stevia.jpg",
|
||||
"static/ico/stevia-gotas.webp",
|
||||
"static/ico/sacarina.jpg",
|
||||
"static/ico/arrow_down_blue.png",
|
||||
"static/ico/arrow_left_green.png",
|
||||
"static/ico/arrow_up_red.png",
|
||||
"static/ico/camera2.png",
|
||||
"static/ico/cereales.png",
|
||||
"static/ico/checkbox.png",
|
||||
"static/ico/checkbox_unchecked.png",
|
||||
"static/ico/connect_ok.svg",
|
||||
"static/ico/connect_ko.svg",
|
||||
"static/ico/coffee_bean.png",
|
||||
"static/ico/colacao.jpg",
|
||||
"static/ico/cookies.png",
|
||||
"static/ico/cow.png",
|
||||
"static/ico/delete.png",
|
||||
"static/ico/fire.png",
|
||||
"static/ico/keyboard_key_g.png",
|
||||
"static/ico/keyboard_key_p.png",
|
||||
"static/ico/lollipop.png",
|
||||
"static/ico/milk.png",
|
||||
"static/ico/preferences.png",
|
||||
"static/ico/sizes.png",
|
||||
"static/ico/statusok.png",
|
||||
"static/ico/snowflake.png",
|
||||
"static/ico/tea_bag.png",
|
||||
"static/ico/thermometer2.png",
|
||||
"static/ico/user.png",
|
||||
"static/ico/user_generic.png",
|
||||
"static/ico/water_tap.png",
|
||||
"static/ico/wheat.png",
|
||||
"static/ico/layered1/Azucar-Az. Blanco.png",
|
||||
"static/ico/layered1/Azucar-Az. Moreno.png",
|
||||
"static/ico/layered1/Azucar-Edulcorante.png",
|
||||
"static/ico/layered1/Azucar-Sacarina.png",
|
||||
"static/ico/layered1/Azucar-Sin.png",
|
||||
"static/ico/layered1/Azucar-Stevia (Gotas).png",
|
||||
"static/ico/layered1/Azucar-Stevia (Pastillas).png",
|
||||
"static/ico/layered1/Background.png",
|
||||
"static/ico/layered1/Cafeina-Con.png",
|
||||
"static/ico/layered1/Cafeina-Sin.png",
|
||||
"static/ico/layered1/Leche-Agua.png",
|
||||
"static/ico/layered1/Leche-Sin lactosa.png",
|
||||
"static/ico/layered1/Leche-Vegetal.png",
|
||||
"static/ico/layered1/Leche-de Vaca.png",
|
||||
"static/ico/layered1/Selección-CafeSolo.png",
|
||||
"static/ico/layered1/Selección-CaféLeche.png",
|
||||
"static/ico/layered1/Selección-ColaCao.png",
|
||||
"static/ico/layered1/Selección-Infusion.png",
|
||||
"static/ico/layered1/Selección-Leche.png",
|
||||
"static/ico/layered1/Tamaño-Grande.png",
|
||||
"static/ico/layered1/Tamaño-Pequeño.png",
|
||||
"static/ico/layered1/Temperatura-Caliente.png",
|
||||
"static/ico/layered1/Temperatura-Frio.png",
|
||||
"static/ico/layered1/Temperatura-Templado.png"
|
||||
]
|
||||
BIN
assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 103 KiB |
@@ -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
|
After Width: | Height: | Size: 47 KiB |
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"theme_color":"#23365e",
|
||||
"background_color":"#ffffff",
|
||||
"icons":[
|
||||
{"purpose":"maskable","sizes":"512x512","src":"icon512_maskable.png","type":"image/png"},
|
||||
{"purpose":"any","sizes":"512x512","src":"icon512_rounded.png","type":"image/png"}
|
||||
],
|
||||
"orientation":"portrait",
|
||||
"display":"standalone",
|
||||
"dir":"auto",
|
||||
"lang":"es-ES",
|
||||
"start_url":"index.html",
|
||||
"scope":"/",
|
||||
"description":"La app de TeleSec",
|
||||
"id":"telesec.tech.eus",
|
||||
"name": "TeleSec",
|
||||
"short_name": "TeleSec"
|
||||
}
|
||||
0
assets/page/.placeholder
Normal file
35
assets/static/aes.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
CryptoJS v3.1.2
|
||||
code.google.com/p/crypto-js
|
||||
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||
code.google.com/p/crypto-js/wiki/License
|
||||
*/
|
||||
var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||
r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<e.length)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
|
||||
32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)c.push(4294967296*u.random()|0);return new r.init(c,a)}}),w=d.enc={},v=w.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt(a.substr(j,
|
||||
2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++)e.push(String.fromCharCode(c[j>>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j++)e[j>>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}},
|
||||
q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=e.splice(0,a);c.sigBytes-=j}return new r.init(q,j)},clone:function(){var a=t.clone.call(this);
|
||||
a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:t.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new a.init(e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new n.HMAC.init(a,
|
||||
e)).finalize(b)}}});var n=d.algo={};return d}(Math);
|
||||
(function(){var u=CryptoJS,p=u.lib.WordArray;u.enc.Base64={stringify:function(d){var l=d.words,p=d.sigBytes,t=this._map;d.clamp();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)d.push(t.charAt(w>>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
|
||||
l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
|
||||
(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])},
|
||||
_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
|
||||
f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
|
||||
m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
|
||||
E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/
|
||||
4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
|
||||
(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length<q;){n&&s.update(n);var n=s.update(d).finalize(r);s.reset();for(var a=1;a<p;a++)n=s.finalize(n),s.reset();b.concat(n)}b.sigBytes=4*q;return b}});u.EvpKDF=function(d,l,p){return s.create(p).compute(d,
|
||||
l)}})();
|
||||
CryptoJS.lib.Cipher||function(u){var p=CryptoJS,d=p.lib,l=d.Base,s=d.WordArray,t=d.BufferedBlockAlgorithm,r=p.enc.Base64,w=p.algo.EvpKDF,v=d.Cipher=t.extend({cfg:l.extend(),createEncryptor:function(e,a){return this.create(this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return this.create(this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){this.cfg=this.cfg.extend(b);this._xformMode=e;this._key=a;this.reset()},reset:function(){t.reset.call(this);this._doReset()},process:function(e){this._append(e);return this._process()},
|
||||
finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});d.StreamCipher=v.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=p.mode={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
|
||||
c[d]},q=(d.BlockCipherMode=l.extend({createEncryptor:function(e,a){return this.Encryptor.create(e,a)},createDecryptor:function(e,a){return this.Decryptor.create(e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();q.Encryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize;x.call(this,e,a,c);b.encryptBlock(e,a);this._prevBlock=e.slice(a,a+c)}});q.Decryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize,d=e.slice(a,a+c);b.decryptBlock(e,a);x.call(this,
|
||||
e,a,c);this._prevBlock=d}});b=b.CBC=q;q=(p.pad={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=c-a.sigBytes%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)l.push(d);c=s.create(l,c);a.concat(c)},unpad:function(a){a.sigBytes-=a.words[a.sigBytes-1>>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a,
|
||||
this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684,
|
||||
1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})},
|
||||
decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d,
|
||||
b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}();
|
||||
(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
|
||||
16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
|
||||
8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
|
||||
d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();
|
||||
45
assets/static/appico/Alert_Warning.svg
Normal 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 |
75
assets/static/appico/App_CodyCam.svg
Normal 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 |
83
assets/static/appico/App_Dropbox.svg
Normal 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 |
7
assets/static/appico/Chat.svg
Normal 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 |
99
assets/static/appico/Classroom.svg
Normal 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 |
97
assets/static/appico/Coffee.svg
Normal 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 |
12
assets/static/appico/Cogs.svg
Normal 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 |
3
assets/static/appico/Database.svg
Normal 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 |
61
assets/static/appico/File_Person.svg
Normal 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 |
65
assets/static/appico/File_Plugin.svg
Normal 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 |
1
assets/static/appico/Meal.svg
Normal 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 |
7
assets/static/appico/Newspaper.svg
Normal 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 |
7
assets/static/appico/Notepad.svg
Normal 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 |
1
assets/static/appico/account-group.svg
Normal 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 |
BIN
assets/static/appico/apple.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/static/appico/application_enterprise.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/static/appico/barcode.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/static/appico/calendar.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/static/appico/components.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/static/appico/credit_cards.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/static/appico/cup.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/static/appico/edit.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/static/appico/gear_edit.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/static/appico/house.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/static/appico/message.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
assets/static/appico/piggy_bank.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/static/appico/shelf.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
assets/static/appico/silverware-fork-knife.svg
Normal 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 |
BIN
assets/static/appico/users.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/static/appico/view.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
@@ -1,109 +0,0 @@
|
||||
;(function(){
|
||||
|
||||
var sT = setTimeout || {}, u;
|
||||
if(typeof window !== ''+u){ sT.window = window }
|
||||
var AXE = (sT.window||'').AXE || function(){};
|
||||
if(AXE.window = sT.window){ AXE.window.AXE = AXE }
|
||||
|
||||
var Gun = (AXE.window||'').GUN || require('./gun');
|
||||
(Gun.AXE = AXE).GUN = AXE.Gun = Gun;
|
||||
|
||||
//if(!Gun.window){ try{ require('./lib/axe') }catch(e){} }
|
||||
if(!Gun.window){ require('./lib/axe') }
|
||||
|
||||
Gun.on('opt', function(at){ start(at) ; this.to.next(at) }); // make sure to call the "next" middleware adapter.
|
||||
|
||||
function start(root){
|
||||
if(root.axe){ return }
|
||||
var opt = root.opt, peers = opt.peers;
|
||||
if(false === opt.axe){ return }
|
||||
if(!Gun.window){ return } // handled by ^ lib/axe.js
|
||||
var w = Gun.window, lS = w.localStorage || opt.localStorage || {}, loc = w.location || opt.location || {}, nav = w.navigator || opt.navigator || {};
|
||||
var axe = root.axe = {}, tmp, id;
|
||||
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
|
||||
|
||||
tmp = peers[id = loc.origin + '/gun'] = peers[id] || {};
|
||||
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
|
||||
tmp = peers[id = 'http://localhost:8765/gun'] = peers[id] || {};
|
||||
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
|
||||
Gun.log.once("AXE", "AXE enabled: Trying to find network via (1) local peer (2) last used peers (3) a URL parameter, and last (4) hard coded peers.");
|
||||
Gun.log.once("AXEWarn", "Warning: AXE is in alpha, use only for testing!");
|
||||
var last = lS.peers || ''; if(last){ last += ' ' }
|
||||
last += ((loc.search||'').split('peers=')[1]||'').split('&')[0];
|
||||
|
||||
root.on('bye', function(peer){
|
||||
this.to.next(peer);
|
||||
if(!peer.url){ return } // ignore WebRTC disconnects for now.
|
||||
if(!nav.onLine){ peer.retry = 1 }
|
||||
if(peer.retry){ return }
|
||||
if(axe.fall){ delete axe.fall[peer.url || peer.id] }
|
||||
(function next(){
|
||||
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
|
||||
var fall = Object.keys(axe.fall||''), one = fall[(Math.random()*fall.length) >> 0];
|
||||
if(!fall.length){ lS.peers = ''; one = 'https://gunjs.herokuapp.com/gun' } // out of peers
|
||||
if(peers[one]){ next(); return } // already choose
|
||||
mesh.hi(one);
|
||||
}());
|
||||
});
|
||||
|
||||
root.on('hi', function(peer){ // TEMPORARY! Try to connect all peers.
|
||||
this.to.next(peer);
|
||||
if(!peer.url){ return } // ignore WebRTC disconnects for now.
|
||||
return; // DO NOT COMMIT THIS FEATURE YET! KEEP TESTING NETWORK PERFORMANCE FIRST!
|
||||
(function next(){
|
||||
if(!peer.wire){ return }
|
||||
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
|
||||
var one = (next.fall = next.fall || Object.keys(axe.fall||'')).pop();
|
||||
if(!one){ return }
|
||||
setTimeout(next, 99);
|
||||
mesh.say({dam: 'opt', opt: {peers: one}}, peer);
|
||||
}());
|
||||
});
|
||||
|
||||
function found(text){
|
||||
|
||||
axe.fall = {};
|
||||
((text||'').match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ig)||[]).forEach(function(url){
|
||||
axe.fall[url] = {url: url, id: url, retry: 0}; // RETRY
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
// TODO: Finish porting below? Maybe not.
|
||||
|
||||
Object.keys(last.peers||'').forEach(function(key){
|
||||
tmp = peers[id = key] = peers[id] || {};
|
||||
tmp.id = tmp.url = id;
|
||||
});
|
||||
tmp = peers[id = 'https://guntest.herokuapp.com/gun'] = peers[id] || {};
|
||||
tmp.id = tmp.url = id;
|
||||
|
||||
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
|
||||
mesh.way = function(msg){
|
||||
if(root.$ === msg.$ || (msg._||'').via){
|
||||
mesh.say(msg, opt.peers);
|
||||
return;
|
||||
}
|
||||
var at = (msg.$||'')._;
|
||||
if(!at){ mesh.say(msg, opt.peers); return }
|
||||
if(msg.get){
|
||||
if(at.axe){ return } // don't ask for it again!
|
||||
at.axe = {};
|
||||
}
|
||||
mesh.say(msg, opt.peers);
|
||||
}
|
||||
}
|
||||
|
||||
if(last){ found(last); return }
|
||||
try{ fetch(((loc.search||'').split('axe=')[1]||'').split('&')[0] || loc.axe || 'https://raw.githubusercontent.com/wiki/amark/gun/volunteer.dht.md').then(function(res){
|
||||
return res.text()
|
||||
}).then(function(text){
|
||||
found(lS.peers = text);
|
||||
}).catch(function(){
|
||||
found(); // nothing
|
||||
})}catch(e){found()}
|
||||
}
|
||||
|
||||
var empty = {}, yes = true;
|
||||
try{ if(typeof module != ''+u){ module.exports = AXE } }catch(e){}
|
||||
}());
|
||||
BIN
assets/static/cash_flow.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
20
assets/static/chart.umd.min.js
vendored
Normal file
476
assets/static/euskaditech-css/simple.css
Normal file
@@ -0,0 +1,476 @@
|
||||
html {
|
||||
color-scheme: light only;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
/*max-width: 45rem;
|
||||
margin: 0 auto;*/
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.supermesh-indicator {
|
||||
border-top-left-radius: 15px;
|
||||
background-color: greenyellow;
|
||||
border-top: 5px solid green;
|
||||
border-left: 5px solid green;
|
||||
padding: 15px;
|
||||
max-width: 30rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: black;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.supermesh-indicator a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
details.supermesh-indicator summary {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.link,
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#articleID {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
.link,
|
||||
a {
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.supermesh-indicator,
|
||||
.no_print,
|
||||
.no_print *,
|
||||
.saveico, .delico, .opicon {
|
||||
display: none !important;
|
||||
}
|
||||
main {padding: 0;}
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
background-color: beige;
|
||||
border: 2px solid black;
|
||||
font-size: 20px;
|
||||
margin: 3px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* https://coolors.co/palette/ff0000-ff8700-ffd300-deff0a-a1ff0a-0aff99-0aefff-147df5-580aff-be0aff */
|
||||
.rojo {
|
||||
background: #ff0000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn1 {
|
||||
background: #ff0000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn2 {
|
||||
background: #ff8700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn3 {
|
||||
background: #ffd300;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn4 {
|
||||
background: #deff0a;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn5 {
|
||||
background: #a1ff0a;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn6 {
|
||||
background: #0aff99;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn7 {
|
||||
background: #0aefff;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn8 {
|
||||
background: #147df5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-disabled {
|
||||
background: black !important;
|
||||
color: grey !important;
|
||||
}
|
||||
|
||||
.nav-disabled:hover {
|
||||
text-decoration: unset !important;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 18px;
|
||||
padding: 5px;
|
||||
width: calc(100% - 11px);
|
||||
}
|
||||
|
||||
input[type="checkbox"]{
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
details input,
|
||||
details select,
|
||||
details textarea {
|
||||
font-size: 18px;
|
||||
padding: 5px;
|
||||
width: calc(100% - 15px);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
line-break: loose;
|
||||
width: fit-content;
|
||||
min-width: 750px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
table tr th {
|
||||
line-break: auto;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
border-bottom: 3px solid black !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.scase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.scase:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
table tr:hover td {
|
||||
text-decoration: underline;
|
||||
background: rgba(200, 200, 200, 0.5);
|
||||
/* color: black; */
|
||||
}
|
||||
|
||||
table tr:hover td.TextBorder {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
max-width: 25rem;
|
||||
}
|
||||
|
||||
.TextBorder {
|
||||
color: black;
|
||||
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff,
|
||||
1px 1px 0 #fff;
|
||||
-webkit-text-stroke: 0.25px #fff;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: x-small;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.activeSCButton {
|
||||
border: 7px dashed beige;
|
||||
color: beige;
|
||||
background: black !important;
|
||||
}
|
||||
|
||||
.btn1.activeSCButton {
|
||||
border-color: #ff0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.btn2.activeSCButton {
|
||||
border-color: #ff8700;
|
||||
color: #ff8700;
|
||||
}
|
||||
|
||||
.btn3.activeSCButton {
|
||||
border-color: #ffd300;
|
||||
color: #ffd300;
|
||||
}
|
||||
|
||||
.btn4.activeSCButton {
|
||||
border-color: #deff0a;
|
||||
color: #deff0a;
|
||||
}
|
||||
|
||||
.btn5.activeSCButton {
|
||||
border-color: #a1ff0a;
|
||||
color: #a1ff0a;
|
||||
}
|
||||
|
||||
.btn6.activeSCButton {
|
||||
border-color: #0aff99;
|
||||
color: #0aff99;
|
||||
}
|
||||
|
||||
.btn7.activeSCButton {
|
||||
border-color: #0aefff;
|
||||
color: #0aefff;
|
||||
}
|
||||
|
||||
.btn8.activeSCButton {
|
||||
border-color: #147df5;
|
||||
color: #147df5;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
#snackbar {
|
||||
visibility: hidden;
|
||||
/* min-width: 250px; */
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
right: 70px;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
#snackbar a {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
#snackbar.show {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
display: flex;
|
||||
background: linear-gradient(to bottom, #d0d8ec, #eef2fa);
|
||||
border-bottom: 1px solid #a2a9b9;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.ribbon-orb {
|
||||
width: 50px !important;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
/* background: url(/icon512_maskable.png); */
|
||||
background-size: 50px 50px;
|
||||
background-position: center middle;
|
||||
border: 1px solid #a2a9b9;
|
||||
margin-right: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ribbon-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1px;
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.ribbon-tabs {
|
||||
display: flex;
|
||||
background: #c8d4eb;
|
||||
border: 1px solid #a2a9b9;
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ribbon-tab {
|
||||
padding: 4px 9px;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid #a2a9b9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ribbon-tab.active {
|
||||
background-color: #eaf0fb;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ribbon-panel {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
background-color: #c8d4eb;
|
||||
border: 1px solid #a2a9b9;
|
||||
overflow-x: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.ribbon-button {
|
||||
width: auto;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
color: black;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid lightskyblue;
|
||||
background: white;
|
||||
padding: 4px;
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ribbon-button img {
|
||||
height: 60px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.ribbon-button .label {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ribbon-button.orange {
|
||||
background-color: orange;
|
||||
border-radius: 3px;
|
||||
padding: 2px;
|
||||
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
details[open] .ribbon-panel {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
details:not([open]) .ribbon-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset legend {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
pre {
|
||||
font-size: 15px;
|
||||
}
|
||||
.picto {
|
||||
min-height: 125px;
|
||||
width: 100px;
|
||||
border: 2.5px solid black;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
background: white;
|
||||
margin-bottom: 20px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.picto b {
|
||||
padding-top: 40px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.panel-option input {
|
||||
display: none;
|
||||
}
|
||||
.panel-option:has(input:checked) {
|
||||
background-color: #ccc;
|
||||
outline: 5px solid blue;
|
||||
}
|
||||
|
||||
.saveico {
|
||||
border-color: green !important;
|
||||
}
|
||||
.delico {
|
||||
border-color: red !important;
|
||||
}
|
||||
.opicon {
|
||||
border-color: blue !important;
|
||||
}
|
||||
.saveico img, .delico img, .opicon img {
|
||||
height: 52px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.saveico, .delico, .opicon {
|
||||
padding: 2.5px 7.5px;
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
border: 4px solid;
|
||||
}
|
||||
BIN
assets/static/exchange.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/static/exit.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/static/find.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/static/floppy_disk_green.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/static/garbage.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
2313
assets/static/gun.js
2
assets/static/ico/almond.svg
Normal 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 |
1
assets/static/ico/offline.svg
Normal 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 |
@@ -1,7 +0,0 @@
|
||||
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
|
||||
Gun.chain.open || require('./open');
|
||||
|
||||
Gun.chain.load = function(cb, opt, at){
|
||||
(opt = opt || {}).off = !0;
|
||||
return this.open(cb, opt, at);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
@@ -1,58 +0,0 @@
|
||||
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
|
||||
|
||||
Gun.chain.open = function(cb, opt, at, depth){ // this is a recursive function, BEWARE!
|
||||
depth = depth || 1;
|
||||
opt = opt || {}; // init top level options.
|
||||
opt.doc = opt.doc || {};
|
||||
opt.ids = opt.ids || {};
|
||||
opt.any = opt.any || cb;
|
||||
opt.meta = opt.meta || false;
|
||||
opt.eve = opt.eve || {off: function(){ // collect all recursive events to unsubscribe to if needed.
|
||||
Object.keys(opt.eve.s).forEach(function(i,e){ // switch to CPU scheduled setTimeout.each?
|
||||
if(e = opt.eve.s[i]){ e.off() }
|
||||
});
|
||||
opt.eve.s = {};
|
||||
}, s:{}}
|
||||
return this.on(function(data, key, ctx, eve){ // subscribe to 1 deeper of data!
|
||||
clearTimeout(opt.to); // do not trigger callback if bunch of changes...
|
||||
opt.to = setTimeout(function(){ // but schedule the callback to fire soon!
|
||||
if(!opt.any){ return }
|
||||
opt.any.call(opt.at.$, opt.doc, opt.key, opt, opt.eve); // call it.
|
||||
if(opt.off){ // check for unsubscribing.
|
||||
opt.eve.off();
|
||||
opt.any = null;
|
||||
}
|
||||
}, opt.wait || 9);
|
||||
opt.at = opt.at || ctx; // opt.at will always be the first context it finds.
|
||||
opt.key = opt.key || key;
|
||||
opt.eve.s[this._.id] = eve; // collect all the events together.
|
||||
if(true === Gun.valid(data)){ // if primitive value...
|
||||
if(!at){
|
||||
opt.doc = data;
|
||||
} else {
|
||||
at[key] = data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
var tmp = this; // else if a sub-object, CPU schedule loop over properties to do recursion.
|
||||
setTimeout.each(Object.keys(data), function(key, val){
|
||||
if('_' === key && !opt.meta){ return }
|
||||
val = data[key];
|
||||
var doc = at || opt.doc, id; // first pass this becomes the root of open, then at is passed below, and will be the parent for each sub-document/object.
|
||||
if(!doc){ return } // if no "parent"
|
||||
if('string' !== typeof (id = Gun.valid(val))){ // if primitive...
|
||||
doc[key] = val;
|
||||
return;
|
||||
}
|
||||
if(opt.ids[id]){ // if we've already seen this sub-object/document
|
||||
doc[key] = opt.ids[id]; // link to itself, our already in-memory one, not a new copy.
|
||||
return;
|
||||
}
|
||||
if(opt.depth <= depth){ // stop recursive open at max depth.
|
||||
doc[key] = doc[key] || val; // show link so app can load it if need.
|
||||
return;
|
||||
} // now open up the recursion of sub-documents!
|
||||
tmp.get(key).open(opt.any, opt, opt.ids[id] = doc[key] = {}, depth+1); // 3rd param is now where we are "at".
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
|
||||
|
||||
Gun.chain.path = function(field, opt){
|
||||
var back = this, gun = back, tmp;
|
||||
if(typeof field === 'string'){
|
||||
tmp = field.split(opt || '.');
|
||||
if(1 === tmp.length){
|
||||
gun = back.get(field);
|
||||
return gun;
|
||||
}
|
||||
field = tmp;
|
||||
}
|
||||
if(field instanceof Array){
|
||||
if(field.length > 1){
|
||||
gun = back;
|
||||
var i = 0, l = field.length;
|
||||
for(i; i < l; i++){
|
||||
//gun = gun.get(field[i], (i+1 === l)? cb : null, opt);
|
||||
gun = gun.get(field[i]);
|
||||
}
|
||||
} else {
|
||||
gun = back.get(field[0]);
|
||||
}
|
||||
return gun;
|
||||
}
|
||||
if(!field && 0 != field){
|
||||
return back;
|
||||
}
|
||||
gun = back.get(''+field);
|
||||
return gun;
|
||||
}
|
||||
7
assets/static/pouchdb.min.js
vendored
Normal file
BIN
assets/static/printer2.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
1033
assets/static/qrcode/barcode.js
Normal file
1
assets/static/qrcode/html5-qrcode.min.js
vendored
Normal file
2
assets/static/qrcode/qrcode.min.js
vendored
Normal file
@@ -1,606 +0,0 @@
|
||||
;(function(){
|
||||
|
||||
function Radisk(opt){
|
||||
|
||||
opt = opt || {};
|
||||
opt.log = opt.log || console.log;
|
||||
opt.file = String(opt.file || 'radata');
|
||||
var has = (Radisk.has || (Radisk.has = {}))[opt.file];
|
||||
if(has){ return has }
|
||||
|
||||
opt.max = opt.max || (opt.memory? (opt.memory * 999 * 999) : 300000000) * 0.3;
|
||||
opt.until = opt.until || opt.wait || 250;
|
||||
opt.batch = opt.batch || (10 * 1000);
|
||||
opt.chunk = opt.chunk || (1024 * 1024 * 1); // 1MB
|
||||
opt.code = opt.code || {};
|
||||
opt.code.from = opt.code.from || '!';
|
||||
opt.jsonify = true;
|
||||
|
||||
|
||||
function ename(t){ return encodeURIComponent(t).replace(/\*/g, '%2A') } // TODO: Hash this also, but allow migration!
|
||||
function atomic(v){ return u !== v && (!v || 'object' != typeof v) }
|
||||
var timediate = (''+u === typeof setImmediate)? setTimeout : setImmediate;
|
||||
var puff = setTimeout.turn || timediate, u;
|
||||
var map = Radix.object;
|
||||
var ST = 0;
|
||||
|
||||
if(!opt.store){
|
||||
return opt.log("ERROR: Radisk needs `opt.store` interface with `{get: fn, put: fn (, list: fn)}`!");
|
||||
}
|
||||
if(!opt.store.put){
|
||||
return opt.log("ERROR: Radisk needs `store.put` interface with `(file, data, cb)`!");
|
||||
}
|
||||
if(!opt.store.get){
|
||||
return opt.log("ERROR: Radisk needs `store.get` interface with `(file, cb)`!");
|
||||
}
|
||||
if(!opt.store.list){
|
||||
//opt.log("WARNING: `store.list` interface might be needed!");
|
||||
}
|
||||
|
||||
if(''+u != typeof require){ require('./yson') }
|
||||
var parse = JSON.parseAsync || function(t,cb,r){ var u; try{ cb(u, JSON.parse(t,r)) }catch(e){ cb(e) } }
|
||||
var json = JSON.stringifyAsync || function(v,cb,r,s){ var u; try{ cb(u, JSON.stringify(v,r,s)) }catch(e){ cb(e) } }
|
||||
/*
|
||||
Any and all storage adapters should...
|
||||
1. Because writing to disk takes time, we should batch data to disk. This improves performance, and reduces potential disk corruption.
|
||||
2. If a batch exceeds a certain number of writes, we should immediately write to disk when physically possible. This caps total performance, but reduces potential loss.
|
||||
*/
|
||||
var r = function(key, data, cb, tag, DBG){
|
||||
if('function' === typeof data){
|
||||
var o = cb || {};
|
||||
cb = data;
|
||||
r.read(key, cb, o, DBG || tag);
|
||||
return;
|
||||
}
|
||||
//var tmp = (tmp = r.batch = r.batch || {})[key] = tmp[key] || {};
|
||||
//var tmp = (tmp = r.batch = r.batch || {})[key] = data;
|
||||
r.save(key, data, cb, tag, DBG);
|
||||
}
|
||||
r.save = function(key, data, cb, tag, DBG){
|
||||
var s = {key: key}, tags, f, d, q;
|
||||
s.find = function(file){ var tmp;
|
||||
s.file = file || (file = opt.code.from);
|
||||
DBG && (DBG = DBG[file] = DBG[file] || {});
|
||||
DBG && (DBG.sf = DBG.sf || +new Date);
|
||||
//console.only.i && console.log('found', file);
|
||||
if(tmp = r.disk[file]){ s.mix(u, tmp); return }
|
||||
r.parse(file, s.mix, u, DBG);
|
||||
}
|
||||
s.mix = function(err, disk){
|
||||
DBG && (DBG.sml = +new Date);
|
||||
DBG && (DBG.sm = DBG.sm || +new Date);
|
||||
if(s.err = err || s.err){ cb(err); return } // TODO: HANDLE BATCH EMIT
|
||||
var file = s.file = (disk||'').file || s.file, tmp;
|
||||
if(!disk && file !== opt.code.from){ // corrupt file?
|
||||
r.find.bad(file); // remove from dir list
|
||||
r.save(key, data, cb, tag); // try again
|
||||
return;
|
||||
}
|
||||
(disk = r.disk[file] || (r.disk[file] = disk || Radix())).file || (disk.file = file);
|
||||
if(opt.compare){
|
||||
data = opt.compare(disk(key), data, key, file);
|
||||
if(u === data){ cb(err, -1); return } // TODO: HANDLE BATCH EMIT
|
||||
}
|
||||
(s.disk = disk)(key, data);
|
||||
if(tag){
|
||||
(tmp = (tmp = disk.tags || (disk.tags = {}))[tag] || (tmp[tag] = r.tags[tag] || (r.tags[tag] = {})))[file] || (tmp[file] = r.one[tag] || (r.one[tag] = cb));
|
||||
cb = null;
|
||||
}
|
||||
DBG && (DBG.st = DBG.st || +new Date);
|
||||
//console.only.i && console.log('mix', disk.Q);
|
||||
if(disk.Q){ cb && disk.Q.push(cb); return } disk.Q = (cb? [cb] : []);
|
||||
disk.to = setTimeout(s.write, opt.until);
|
||||
}
|
||||
s.write = function(){
|
||||
DBG && (DBG.sto = DBG.sto || +new Date);
|
||||
var file = f = s.file, disk = d = s.disk;
|
||||
q = s.q = disk.Q;
|
||||
tags = s.tags = disk.tags;
|
||||
delete disk.Q;
|
||||
delete r.disk[file];
|
||||
delete disk.tags;
|
||||
//console.only.i && console.log('write', file, disk, 'was saving:', key, data);
|
||||
r.write(file, disk, s.ack, u, DBG);
|
||||
}
|
||||
s.ack = function(err, ok){
|
||||
DBG && (DBG.sa = DBG.sa || +new Date);
|
||||
DBG && (DBG.sal = q.length);
|
||||
var ack, tmp;
|
||||
// TODO!!!! CHANGE THIS INTO PUFF!!!!!!!!!!!!!!!!
|
||||
for(var id in r.tags){
|
||||
if(!r.tags.hasOwnProperty(id)){ continue } var tag = r.tags[id];
|
||||
if((tmp = r.disk[f]) && (tmp = tmp.tags) && tmp[tag]){ continue }
|
||||
ack = tag[f];
|
||||
delete tag[f];
|
||||
var ne; for(var k in tag){ if(tag.hasOwnProperty(k)){ ne = true; break } } // is not empty?
|
||||
if(ne){ continue } //if(!obj_empty(tag)){ continue }
|
||||
delete r.tags[tag];
|
||||
ack && ack(err, ok);
|
||||
}
|
||||
!q && (q = '');
|
||||
var l = q.length, i = 0;
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
// TODO: PERF: Why is acks so slow, what work do they do??? CHECK THIS!!
|
||||
var S = +new Date;
|
||||
for(;i < l; i++){ (ack = q[i]) && ack(err, ok) }
|
||||
console.STAT && console.STAT(S, +new Date - S, 'rad acks', ename(s.file));
|
||||
console.STAT && console.STAT(S, q.length, 'rad acks #', ename(s.file));
|
||||
}
|
||||
cb || (cb = function(err, ok){ // test delete!
|
||||
if(!err){ return }
|
||||
});
|
||||
//console.only.i && console.log('save', key);
|
||||
r.find(key, s.find);
|
||||
}
|
||||
r.disk = {};
|
||||
r.one = {};
|
||||
r.tags = {};
|
||||
|
||||
/*
|
||||
Any storage engine at some point will have to do a read in order to write.
|
||||
This is true of even systems that use an append only log, if they support updates.
|
||||
Therefore it is unavoidable that a read will have to happen,
|
||||
the question is just how long you delay it.
|
||||
*/
|
||||
var RWC = 0;
|
||||
r.write = function(file, rad, cb, o, DBG){
|
||||
if(!rad){ cb('No radix!'); return }
|
||||
o = ('object' == typeof o)? o : {force: o};
|
||||
var f = function Fractal(){}, a, b;
|
||||
f.text = '';
|
||||
f.file = file = rad.file || (rad.file = file);
|
||||
if(!file){ cb('What file?'); return }
|
||||
f.write = function(){
|
||||
var text = rad.raw = f.text;
|
||||
r.disk[file = rad.file || f.file || file] = rad;
|
||||
var S = +new Date;
|
||||
DBG && (DBG.wd = S);
|
||||
//console.only.i && console.log('add', file);
|
||||
r.find.add(file, function add(err){
|
||||
DBG && (DBG.wa = +new Date);
|
||||
if(err){ cb(err); return }
|
||||
//console.only.i && console.log('disk', file, text);
|
||||
opt.store.put(ename(file), text, function safe(err, ok){
|
||||
DBG && (DBG.wp = +new Date);
|
||||
console.STAT && console.STAT(S, ST = +new Date - S, "wrote disk", JSON.stringify(file), ++RWC, 'total all writes.');
|
||||
//console.only.i && console.log('done', err, ok || 1, cb);
|
||||
cb(err, ok || 1);
|
||||
if(!rad.Q){ delete r.disk[file] } // VERY IMPORTANT! Clean up memory, but not if there is already queued writes on it!
|
||||
});
|
||||
});
|
||||
}
|
||||
f.split = function(){
|
||||
var S = +new Date;
|
||||
DBG && (DBG.wf = S);
|
||||
f.text = '';
|
||||
if(!f.count){ f.count = 0;
|
||||
Radix.map(rad, function count(){ f.count++ }); // TODO: Perf? Any faster way to get total length?
|
||||
}
|
||||
DBG && (DBG.wfc = f.count);
|
||||
f.limit = Math.ceil(f.count/2);
|
||||
var SC = f.count;
|
||||
f.count = 0;
|
||||
DBG && (DBG.wf1 = +new Date);
|
||||
f.sub = Radix();
|
||||
Radix.map(rad, f.slice, {reverse: 1}); // IMPORTANT: DO THIS IN REVERSE, SO LAST HALF OF DATA MOVED TO NEW FILE BEFORE DROPPING FROM CURRENT FILE.
|
||||
DBG && (DBG.wf2 = +new Date);
|
||||
r.write(f.end, f.sub, f.both, o);
|
||||
DBG && (DBG.wf3 = +new Date);
|
||||
f.hub = Radix();
|
||||
Radix.map(rad, f.stop);
|
||||
DBG && (DBG.wf4 = +new Date);
|
||||
r.write(rad.file, f.hub, f.both, o);
|
||||
DBG && (DBG.wf5 = +new Date);
|
||||
console.STAT && console.STAT(S, +new Date - S, "rad split", ename(rad.file), SC);
|
||||
return true;
|
||||
}
|
||||
f.slice = function(val, key){
|
||||
f.sub(f.end = key, val);
|
||||
if(f.limit <= (++f.count)){ return true }
|
||||
}
|
||||
f.stop = function(val, key){
|
||||
if(key >= f.end){ return true }
|
||||
f.hub(key, val);
|
||||
}
|
||||
f.both = function(err, ok){
|
||||
DBG && (DBG.wfd = +new Date);
|
||||
if(b){ cb(err || b); return }
|
||||
if(a){ cb(err, ok); return }
|
||||
a = true;
|
||||
b = err;
|
||||
}
|
||||
f.each = function(val, key, k, pre){
|
||||
if(u !== val){ f.count++ }
|
||||
if(opt.max <= (val||'').length){ return cb("Data too big!"), true }
|
||||
var enc = Radisk.encode(pre.length) +'#'+ Radisk.encode(k) + (u === val? '' : ':'+ Radisk.encode(val)) +'\n';
|
||||
if((opt.chunk < f.text.length + enc.length) && (1 < f.count) && !o.force){
|
||||
return f.split();
|
||||
}
|
||||
f.text += enc;
|
||||
}
|
||||
//console.only.i && console.log('writing');
|
||||
if(opt.jsonify){ r.write.jsonify(f, rad, cb, o, DBG); return } // temporary testing idea
|
||||
if(!Radix.map(rad, f.each, true)){ f.write() }
|
||||
}
|
||||
|
||||
r.write.jsonify = function(f, rad, cb, o, DBG){
|
||||
var raw;
|
||||
var S = +new Date;
|
||||
DBG && (DBG.w = S);
|
||||
try{raw = JSON.stringify(rad.$);
|
||||
}catch(e){ cb("Cannot radisk!"); return }
|
||||
DBG && (DBG.ws = +new Date);
|
||||
console.STAT && console.STAT(S, +new Date - S, "rad stringified JSON");
|
||||
if(opt.chunk < raw.length && !o.force){
|
||||
var c = 0;
|
||||
Radix.map(rad, function(){
|
||||
if(c++){ return true } // more than 1 item
|
||||
});
|
||||
if(c > 1){
|
||||
return f.split();
|
||||
}
|
||||
}
|
||||
f.text = raw;
|
||||
f.write();
|
||||
}
|
||||
|
||||
r.range = function(tree, o){
|
||||
if(!tree || !o){ return }
|
||||
if(u === o.start && u === o.end){ return tree }
|
||||
if(atomic(tree)){ return tree }
|
||||
var sub = Radix();
|
||||
Radix.map(tree, function(v,k){ sub(k,v) }, o); // ONLY PLACE THAT TAKES TREE, maybe reduce API for better perf?
|
||||
return sub('');
|
||||
}
|
||||
|
||||
;(function(){
|
||||
r.read = function(key, cb, o, DBG){
|
||||
o = o || {};
|
||||
var g = {key: key};
|
||||
g.find = function(file){ var tmp;
|
||||
g.file = file || (file = opt.code.from);
|
||||
DBG && (DBG = DBG[file] = DBG[file] || {});
|
||||
DBG && (DBG.rf = DBG.rf || +new Date);
|
||||
if(tmp = r.disk[g.file = file]){ g.check(u, tmp); return }
|
||||
r.parse(file, g.check, u, DBG);
|
||||
}
|
||||
g.get = function(err, disk, info){
|
||||
DBG && (DBG.rgl = +new Date);
|
||||
DBG && (DBG.rg = DBG.rg || +new Date);
|
||||
if(g.err = err || g.err){ cb(err); return }
|
||||
var file = g.file = (disk||'').file || g.file;
|
||||
if(!disk && file !== opt.code.from){ // corrupt file?
|
||||
r.find.bad(file); // remove from dir list
|
||||
r.read(key, cb, o); // try again
|
||||
return;
|
||||
}
|
||||
disk = r.disk[file] || (r.disk[file] = disk);
|
||||
if(!disk){ cb(file === opt.code.from? u : "No file!"); return }
|
||||
disk.file || (disk.file = file);
|
||||
var data = r.range(disk(key), o);
|
||||
DBG && (DBG.rr = +new Date);
|
||||
o.unit = disk.unit;
|
||||
o.chunks = (o.chunks || 0) + 1;
|
||||
o.parsed = (o.parsed || 0) + ((info||'').parsed||(o.chunks*opt.chunk));
|
||||
o.more = 1;
|
||||
o.next = u;
|
||||
Radix.map(r.list, function next(v,f){
|
||||
if(!v || file === f){ return }
|
||||
o.next = f;
|
||||
return 1;
|
||||
}, o.reverse? {reverse: 1, end: file} : {start: file});
|
||||
DBG && (DBG.rl = +new Date);
|
||||
if(!o.next){ o.more = 0 }
|
||||
if(o.next){
|
||||
if(!o.reverse && ((key < o.next && 0 != o.next.indexOf(key)) || (u !== o.end && (o.end || '\uffff') < o.next))){ o.more = 0 }
|
||||
if(o.reverse && ((key > o.next && 0 != key.indexOf(o.next)) || ((u !== o.start && (o.start || '') > o.next && file <= o.start)))){ o.more = 0 }
|
||||
}
|
||||
//console.log(5, process.memoryUsage().heapUsed);
|
||||
if(!o.more){ cb(g.err, data, o); return }
|
||||
if(data){ cb(g.err, data, o) }
|
||||
if(o.parsed >= o.limit){ return }
|
||||
var S = +new Date;
|
||||
DBG && (DBG.rm = S);
|
||||
var next = o.next;
|
||||
timediate(function(){
|
||||
console.STAT && console.STAT(S, +new Date - S, 'rad more');
|
||||
r.parse(next, g.check);
|
||||
},0);
|
||||
}
|
||||
g.check = function(err, disk, info){
|
||||
//console.log(4, process.memoryUsage().heapUsed);
|
||||
g.get(err, disk, info);
|
||||
if(!disk || disk.check){ return } disk.check = 1;
|
||||
var S = +new Date;
|
||||
(info || (info = {})).file || (info.file = g.file);
|
||||
Radix.map(disk, function(val, key){
|
||||
// assume in memory for now, since both write/read already call r.find which will init it.
|
||||
r.find(key, function(file){
|
||||
if((file || (file = opt.code.from)) === info.file){ return }
|
||||
var id = (''+Math.random()).slice(-3);
|
||||
puff(function(){
|
||||
r.save(key, val, function ack(err, ok){
|
||||
if(err){ r.save(key, val, ack); return } // ad infinitum???
|
||||
// TODO: NOTE!!! Mislocated data could be because of a synchronous `put` from the `g.get(` other than perf shouldn't we do the check first before acking?
|
||||
console.STAT && console.STAT("MISLOCATED DATA CORRECTED", id, ename(key), ename(info.file), ename(file));
|
||||
});
|
||||
},0);
|
||||
})
|
||||
});
|
||||
console.STAT && console.STAT(S, +new Date - S, "rad check");
|
||||
}
|
||||
r.find(key || (o.reverse? (o.end||'') : (o.start||'')), g.find);
|
||||
}
|
||||
function rev(a,b){ return b }
|
||||
var revo = {reverse: true};
|
||||
}());
|
||||
|
||||
;(function(){
|
||||
/*
|
||||
Let us start by assuming we are the only process that is
|
||||
changing the directory or bucket. Not because we do not want
|
||||
to be multi-process/machine, but because we want to experiment
|
||||
with how much performance and scale we can get out of only one.
|
||||
Then we can work on the harder problem of being multi-process.
|
||||
*/
|
||||
var RPC = 0;
|
||||
var Q = {}, s = String.fromCharCode(31);
|
||||
r.parse = function(file, cb, raw, DBG){ var q;
|
||||
if(!file){ return cb(); }
|
||||
if(q = Q[file]){ q.push(cb); return } q = Q[file] = [cb];
|
||||
var p = function Parse(){}, info = {file: file};
|
||||
(p.disk = Radix()).file = file;
|
||||
p.read = function(err, data){ var tmp;
|
||||
DBG && (DBG.rpg = +new Date);
|
||||
console.STAT && console.STAT(S, +new Date - S, 'read disk', JSON.stringify(file), ++RPC, 'total all parses.');
|
||||
//console.log(2, process.memoryUsage().heapUsed);
|
||||
if((p.err = err) || (p.not = !data)){
|
||||
delete Q[file];
|
||||
p.map(q, p.ack);
|
||||
return;
|
||||
}
|
||||
if('string' !== typeof data){
|
||||
try{
|
||||
if(opt.max <= data.length){
|
||||
p.err = "Chunk too big!";
|
||||
} else {
|
||||
data = data.toString(); // If it crashes, it crashes here. How!?? We check size first!
|
||||
}
|
||||
}catch(e){ p.err = e }
|
||||
if(p.err){
|
||||
delete Q[file];
|
||||
p.map(q, p.ack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
info.parsed = data.length;
|
||||
DBG && (DBG.rpl = info.parsed);
|
||||
DBG && (DBG.rpa = q.length);
|
||||
S = +new Date;
|
||||
if(!(opt.jsonify || '{' === data[0])){
|
||||
p.radec(err, data);
|
||||
return;
|
||||
}
|
||||
parse(data, function(err, tree){
|
||||
//console.log(3, process.memoryUsage().heapUsed);
|
||||
if(!err){
|
||||
delete Q[file];
|
||||
p.disk.$ = tree;
|
||||
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'rad parsed JSON');
|
||||
DBG && (DBG.rpd = +new Date);
|
||||
p.map(q, p.ack); // hmmm, v8 profiler can't see into this cause of try/catch?
|
||||
return;
|
||||
}
|
||||
if('{' === data[0]){
|
||||
delete Q[file];
|
||||
p.err = tmp || "JSON error!";
|
||||
p.map(q, p.ack);
|
||||
return;
|
||||
}
|
||||
p.radec(err, data);
|
||||
});
|
||||
}
|
||||
p.map = function(){ // switch to setTimeout.each now?
|
||||
if(!q || !q.length){ return }
|
||||
//var i = 0, l = q.length, ack;
|
||||
var S = +new Date;
|
||||
var err = p.err, data = p.not? u : p.disk;
|
||||
var i = 0, ack; while(i < 9 && (ack = q[i++])){ ack(err, data, info) } // too much?
|
||||
console.STAT && console.STAT(S, +new Date - S, 'rad packs', ename(file));
|
||||
console.STAT && console.STAT(S, i, 'rad packs #', ename(file));
|
||||
if(!(q = q.slice(i)).length){ return }
|
||||
puff(p.map, 0);
|
||||
}
|
||||
p.ack = function(cb){
|
||||
if(!cb){ return }
|
||||
if(p.err || p.not){
|
||||
cb(p.err, u, info);
|
||||
return;
|
||||
}
|
||||
cb(u, p.disk, info);
|
||||
}
|
||||
p.radec = function(err, data){
|
||||
delete Q[file];
|
||||
S = +new Date;
|
||||
var tmp = p.split(data), pre = [], i, k, v;
|
||||
if(!tmp || 0 !== tmp[1]){
|
||||
p.err = "File '"+file+"' does not have root radix! ";
|
||||
p.map(q, p.ack);
|
||||
return;
|
||||
}
|
||||
while(tmp){
|
||||
k = v = u;
|
||||
i = tmp[1];
|
||||
tmp = p.split(tmp[2])||'';
|
||||
if('#' == tmp[0]){
|
||||
k = tmp[1];
|
||||
pre = pre.slice(0,i);
|
||||
if(i <= pre.length){
|
||||
pre.push(k);
|
||||
}
|
||||
}
|
||||
tmp = p.split(tmp[2])||'';
|
||||
if('\n' == tmp[0]){ continue }
|
||||
if('=' == tmp[0] || ':' == tmp[0]){ v = tmp[1] }
|
||||
if(u !== k && u !== v){ p.disk(pre.join(''), v) }
|
||||
tmp = p.split(tmp[2]);
|
||||
}
|
||||
console.STAT && console.STAT(S, +new Date - S, 'parsed RAD');
|
||||
p.map(q, p.ack);
|
||||
};
|
||||
p.split = function(t){
|
||||
if(!t){ return }
|
||||
var l = [], o = {}, i = -1, a = '', b, c;
|
||||
i = t.indexOf(s);
|
||||
if(!t[i]){ return }
|
||||
a = t.slice(0, i);
|
||||
l[0] = a;
|
||||
l[1] = b = Radisk.decode(t.slice(i), o);
|
||||
l[2] = t.slice(i + o.i);
|
||||
return l;
|
||||
}
|
||||
if(r.disk){ raw || (raw = (r.disk[file]||'').raw) }
|
||||
var S = +new Date, SM, SL;
|
||||
DBG && (DBG.rp = S);
|
||||
if(raw){ return puff(function(){ p.read(u, raw) }, 0) }
|
||||
opt.store.get(ename(file), p.read);
|
||||
// TODO: What if memory disk gets filled with updates, and we get an old one back?
|
||||
}
|
||||
}());
|
||||
|
||||
;(function(){
|
||||
var dir, f = String.fromCharCode(28), Q;
|
||||
r.find = function(key, cb){
|
||||
if(!dir){
|
||||
if(Q){ Q.push([key, cb]); return } Q = [[key, cb]];
|
||||
r.parse(f, init);
|
||||
return;
|
||||
}
|
||||
Radix.map(r.list = dir, function(val, key){
|
||||
if(!val){ return }
|
||||
return cb(key) || true;
|
||||
}, {reverse: 1, end: key}) || cb(opt.code.from);
|
||||
}
|
||||
r.find.add = function(file, cb){
|
||||
var has = dir(file);
|
||||
if(has || file === f){ cb(u, 1); return }
|
||||
dir(file, 1);
|
||||
cb.found = (cb.found || 0) + 1;
|
||||
r.write(f, dir, function(err, ok){
|
||||
if(err){ cb(err); return }
|
||||
cb.found = (cb.found || 0) - 1;
|
||||
if(0 !== cb.found){ return }
|
||||
cb(u, 1);
|
||||
}, true);
|
||||
}
|
||||
r.find.bad = function(file, cb){
|
||||
dir(file, 0);
|
||||
r.write(f, dir, cb||noop);
|
||||
}
|
||||
function init(err, disk){
|
||||
if(err){
|
||||
opt.log('list', err);
|
||||
setTimeout(function(){ r.parse(f, init) }, 1000);
|
||||
return;
|
||||
}
|
||||
if(disk){ drain(disk); return }
|
||||
dir = dir || disk || Radix();
|
||||
if(!opt.store.list){ drain(dir); return }
|
||||
// import directory.
|
||||
opt.store.list(function(file){
|
||||
if(!file){ drain(dir); return }
|
||||
r.find.add(file, noop);
|
||||
});
|
||||
}
|
||||
function drain(rad, tmp){
|
||||
dir = dir || rad;
|
||||
dir.file = f;
|
||||
tmp = Q; Q = null;
|
||||
map(tmp, function(arg){
|
||||
r.find(arg[0], arg[1]);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
||||
try{ !Gun.window && require('./radmigtmp')(r) }catch(e){}
|
||||
|
||||
var noop = function(){}, RAD, u;
|
||||
Radisk.has[opt.file] = r;
|
||||
return r;
|
||||
}
|
||||
|
||||
;(function(){
|
||||
var _ = String.fromCharCode(31), u;
|
||||
Radisk.encode = function(d, o, s){ s = s || _;
|
||||
var t = s, tmp;
|
||||
if(typeof d == 'string'){
|
||||
var i = d.indexOf(s);
|
||||
while(i != -1){ t += s; i = d.indexOf(s, i+1) }
|
||||
return t + '"' + d + s;
|
||||
} else
|
||||
if(d && d['#'] && 1 == Object.keys(d).length){
|
||||
return t + '#' + tmp + t;
|
||||
} else
|
||||
if('number' == typeof d){
|
||||
return t + '+' + (d||0) + t;
|
||||
} else
|
||||
if(null === d){
|
||||
return t + ' ' + t;
|
||||
} else
|
||||
if(true === d){
|
||||
return t + '+' + t;
|
||||
} else
|
||||
if(false === d){
|
||||
return t + '-' + t;
|
||||
}// else
|
||||
//if(binary){}
|
||||
}
|
||||
Radisk.decode = function(t, o, s){ s = s || _;
|
||||
var d = '', i = -1, n = 0, c, p;
|
||||
if(s !== t[0]){ return }
|
||||
while(s === t[++i]){ ++n }
|
||||
p = t[c = n] || true;
|
||||
while(--n >= 0){ i = t.indexOf(s, i+1) }
|
||||
if(i == -1){ i = t.length }
|
||||
d = t.slice(c+1, i);
|
||||
if(o){ o.i = i+1 }
|
||||
if('"' === p){
|
||||
return d;
|
||||
} else
|
||||
if('#' === p){
|
||||
return {'#':d};
|
||||
} else
|
||||
if('+' === p){
|
||||
if(0 === d.length){
|
||||
return true;
|
||||
}
|
||||
return parseFloat(d);
|
||||
} else
|
||||
if(' ' === p){
|
||||
return null;
|
||||
} else
|
||||
if('-' === p){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
if(typeof window !== "undefined"){
|
||||
var Gun = window.Gun;
|
||||
var Radix = window.Radix;
|
||||
window.Radisk = Radisk;
|
||||
} else {
|
||||
var Gun = require('../gun');
|
||||
var Radix = require('./radix');
|
||||
//var Radix = require('./radix2'); Radisk = require('./radisk2');
|
||||
try{ module.exports = Radisk }catch(e){}
|
||||
}
|
||||
|
||||
Radisk.Radix = Radix;
|
||||
|
||||
}());
|
||||
@@ -1,124 +0,0 @@
|
||||
;(function(){
|
||||
|
||||
function Radix(){
|
||||
var radix = function(key, val, t){
|
||||
radix.unit = 0;
|
||||
if(!t && u !== val){
|
||||
radix.last = (''+key < radix.last)? radix.last : ''+key;
|
||||
delete (radix.$||{})[_];
|
||||
}
|
||||
t = t || radix.$ || (radix.$ = {});
|
||||
if(!key && Object.keys(t).length){ return t }
|
||||
key = ''+key;
|
||||
var i = 0, l = key.length-1, k = key[i], at, tmp;
|
||||
while(!(at = t[k]) && i < l){
|
||||
k += key[++i];
|
||||
}
|
||||
if(!at){
|
||||
if(!each(t, function(r, s){
|
||||
var ii = 0, kk = '';
|
||||
if((s||'').length){ while(s[ii] == key[ii]){
|
||||
kk += s[ii++];
|
||||
} }
|
||||
if(kk){
|
||||
if(u === val){
|
||||
if(ii <= l){ return }
|
||||
(tmp || (tmp = {}))[s.slice(ii)] = r;
|
||||
//(tmp[_] = function $(){ $.sort = Object.keys(tmp).sort(); return $ }()); // get rid of this one, cause it is on read?
|
||||
return r;
|
||||
}
|
||||
var __ = {};
|
||||
__[s.slice(ii)] = r;
|
||||
ii = key.slice(ii);
|
||||
('' === ii)? (__[''] = val) : ((__[ii] = {})[''] = val);
|
||||
//(__[_] = function $(){ $.sort = Object.keys(__).sort(); return $ }());
|
||||
t[kk] = __;
|
||||
if(Radix.debug && 'undefined' === ''+kk){ console.log(0, kk); debugger }
|
||||
delete t[s];
|
||||
//(t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }());
|
||||
return true;
|
||||
}
|
||||
})){
|
||||
if(u === val){ return; }
|
||||
(t[k] || (t[k] = {}))[''] = val;
|
||||
if(Radix.debug && 'undefined' === ''+k){ console.log(1, k); debugger }
|
||||
//(t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }());
|
||||
}
|
||||
if(u === val){
|
||||
return tmp;
|
||||
}
|
||||
} else
|
||||
if(i == l){
|
||||
//if(u === val){ return (u === (tmp = at['']))? at : tmp } // THIS CODE IS CORRECT, below is
|
||||
if(u === val){ return (u === (tmp = at['']))? at : ((radix.unit = 1) && tmp) } // temporary help??
|
||||
at[''] = val;
|
||||
//(at[_] = function $(){ $.sort = Object.keys(at).sort(); return $ }());
|
||||
} else {
|
||||
if(u !== val){ delete at[_] }
|
||||
//at && (at[_] = function $(){ $.sort = Object.keys(at).sort(); return $ }());
|
||||
return radix(key.slice(++i), val, at || (at = {}));
|
||||
}
|
||||
}
|
||||
return radix;
|
||||
};
|
||||
|
||||
Radix.map = function rap(radix, cb, opt, pre){
|
||||
try {
|
||||
pre = pre || []; // TODO: BUG: most out-of-memory crashes come from here.
|
||||
var t = ('function' == typeof radix)? radix.$ || {} : radix;
|
||||
//!opt && console.log("WHAT IS T?", JSON.stringify(t).length);
|
||||
if(!t){ return }
|
||||
if('string' == typeof t){ if(Radix.debug){ throw ['BUG:', radix, cb, opt, pre] } return; }
|
||||
var keys = (t[_]||no).sort || (t[_] = function $(){ $.sort = Object.keys(t).sort(); return $ }()).sort, rev; // ONLY 17% of ops are pre-sorted!
|
||||
//var keys = Object.keys(t).sort();
|
||||
opt = (true === opt)? {branch: true} : (opt || {});
|
||||
if(rev = opt.reverse){ keys = keys.slice(0).reverse() }
|
||||
var start = opt.start, end = opt.end, END = '\uffff';
|
||||
var i = 0, l = keys.length;
|
||||
for(;i < l; i++){ var key = keys[i], tree = t[key], tmp, p, pt;
|
||||
if(!tree || '' === key || _ === key || 'undefined' === key){ continue }
|
||||
p = pre.slice(0); p.push(key);
|
||||
pt = p.join('');
|
||||
if(u !== start && pt < (start||'').slice(0,pt.length)){ continue }
|
||||
if(u !== end && (end || END) < pt){ continue }
|
||||
if(rev){ // children must be checked first when going in reverse.
|
||||
tmp = rap(tree, cb, opt, p);
|
||||
if(u !== tmp){ return tmp }
|
||||
}
|
||||
if(u !== (tmp = tree[''])){
|
||||
var yes = 1;
|
||||
if(u !== start && pt < (start||'')){ yes = 0 }
|
||||
if(u !== end && pt > (end || END)){ yes = 0 }
|
||||
if(yes){
|
||||
tmp = cb(tmp, pt, key, pre);
|
||||
if(u !== tmp){ return tmp }
|
||||
}
|
||||
} else
|
||||
if(opt.branch){
|
||||
tmp = cb(u, pt, key, pre);
|
||||
if(u !== tmp){ return tmp }
|
||||
}
|
||||
pre = p;
|
||||
if(!rev){
|
||||
tmp = rap(tree, cb, opt, pre);
|
||||
if(u !== tmp){ return tmp }
|
||||
}
|
||||
pre.pop();
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
if(typeof window !== "undefined"){
|
||||
window.Radix = Radix;
|
||||
} else {
|
||||
try{ module.exports = Radix }catch(e){}
|
||||
}
|
||||
var each = Radix.object = function(o, f, r){
|
||||
for(var k in o){
|
||||
if(!o.hasOwnProperty(k)){ continue }
|
||||
if((r = f(o[k], k)) !== u){ return r }
|
||||
}
|
||||
}, no = {}, u;
|
||||
var _ = String.fromCharCode(24);
|
||||
|
||||
}());
|
||||
@@ -1,79 +0,0 @@
|
||||
;(function(){
|
||||
/* // from @jabis
|
||||
if (navigator.storage && navigator.storage.estimate) {
|
||||
const quota = await navigator.storage.estimate();
|
||||
// quota.usage -> Number of bytes used.
|
||||
// quota.quota -> Maximum number of bytes available.
|
||||
const percentageUsed = (quota.usage / quota.quota) * 100;
|
||||
console.log(`You've used ${percentageUsed}% of the available storage.`);
|
||||
const remaining = quota.quota - quota.usage;
|
||||
console.log(`You can write up to ${remaining} more bytes.`);
|
||||
}
|
||||
*/
|
||||
function Store(opt){
|
||||
opt = opt || {};
|
||||
opt.file = String(opt.file || 'radata');
|
||||
var store = Store[opt.file], db = null, u;
|
||||
|
||||
if(store){
|
||||
console.log("Warning: reusing same IndexedDB store and options as 1st.");
|
||||
return Store[opt.file];
|
||||
}
|
||||
store = Store[opt.file] = function(){};
|
||||
|
||||
try{opt.indexedDB = opt.indexedDB || Store.indexedDB || indexedDB}catch(e){}
|
||||
try{if(!opt.indexedDB || 'file:' == location.protocol){
|
||||
var s = store.d || (store.d = {});
|
||||
store.put = function(f, d, cb){ s[f] = d; setTimeout(function(){ cb(null, 1) },250) };
|
||||
store.get = function(f, cb){ setTimeout(function(){ cb(null, s[f] || u) },5) };
|
||||
console.log('Warning: No indexedDB exists to persist data to!');
|
||||
return store;
|
||||
}}catch(e){}
|
||||
|
||||
|
||||
store.start = function(){
|
||||
var o = indexedDB.open(opt.file, 1);
|
||||
o.onupgradeneeded = function(eve){ (eve.target.result).createObjectStore(opt.file) }
|
||||
o.onsuccess = function(){ db = o.result }
|
||||
o.onerror = function(eve){ console.log(eve||1); }
|
||||
}; store.start();
|
||||
|
||||
store.put = function(key, data, cb){
|
||||
if(!db){ setTimeout(function(){ store.put(key, data, cb) },1); return }
|
||||
var tx = db.transaction([opt.file], 'readwrite');
|
||||
var obj = tx.objectStore(opt.file);
|
||||
var req = obj.put(data, ''+key);
|
||||
req.onsuccess = obj.onsuccess = tx.onsuccess = function(){ cb(null, 1) }
|
||||
req.onabort = obj.onabort = tx.onabort = function(eve){ cb(eve||'put.tx.abort') }
|
||||
req.onerror = obj.onerror = tx.onerror = function(eve){ cb(eve||'put.tx.error') }
|
||||
}
|
||||
|
||||
store.get = function(key, cb){
|
||||
if(!db){ setTimeout(function(){ store.get(key, cb) },9); return }
|
||||
var tx = db.transaction([opt.file], 'readonly');
|
||||
var obj = tx.objectStore(opt.file);
|
||||
var req = obj.get(''+key);
|
||||
req.onsuccess = function(){ cb(null, req.result) }
|
||||
req.onabort = function(eve){ cb(eve||4) }
|
||||
req.onerror = function(eve){ cb(eve||5) }
|
||||
}
|
||||
setInterval(function(){ db && db.close(); db = null; store.start() }, 1000 * 15); // reset webkit bug?
|
||||
return store;
|
||||
}
|
||||
|
||||
if(typeof window !== "undefined"){
|
||||
(Store.window = window).RindexedDB = Store;
|
||||
Store.indexedDB = window.indexedDB; // safari bug
|
||||
} else {
|
||||
try{ module.exports = Store }catch(e){}
|
||||
}
|
||||
|
||||
try{
|
||||
var Gun = Store.window.Gun || require('../gun');
|
||||
Gun.on('create', function(root){
|
||||
this.to.next(root);
|
||||
root.opt.store = root.opt.store || Store(root.opt);
|
||||
});
|
||||
}catch(e){}
|
||||
|
||||
}());
|
||||
1537
assets/static/sea.js
7
assets/static/simplemde.min.css
vendored
15
assets/static/simplemde.min.js
vendored
@@ -1,150 +0,0 @@
|
||||
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
|
||||
|
||||
Gun.on('create', function(root){
|
||||
if(Gun.TESTING){ root.opt.file = 'radatatest' }
|
||||
this.to.next(root);
|
||||
var opt = root.opt, empty = {}, u;
|
||||
if(false === opt.rad || false === opt.radisk){ return }
|
||||
if((u+'' != typeof process) && 'false' === ''+(process.env||'').RAD){ return }
|
||||
var Radisk = (Gun.window && Gun.window.Radisk) || require('./radisk');
|
||||
var Radix = Radisk.Radix;
|
||||
var dare = Radisk(opt), esc = String.fromCharCode(27);
|
||||
var ST = 0;
|
||||
|
||||
root.on('put', function(msg){
|
||||
this.to.next(msg);
|
||||
if((msg._||'').rad){ return } // don't save what just came from a read.
|
||||
//if(msg['@']){ return } // WHY DID I NOT ADD THIS?
|
||||
var id = msg['#'], put = msg.put, soul = put['#'], key = put['.'], val = put[':'], state = put['>'], tmp;
|
||||
var DBG = (msg._||'').DBG; DBG && (DBG.sp = DBG.sp || +new Date);
|
||||
//var lot = (msg._||'').lot||''; count[id] = (count[id] || 0) + 1;
|
||||
var S = (msg._||'').RPS || ((msg._||'').RPS = +new Date);
|
||||
//console.log("PUT ------->>>", soul,key, val, state);
|
||||
//dare(soul+esc+key, {':': val, '>': state}, dare.one[id] || function(err, ok){
|
||||
dare(soul+esc+key, {':': val, '>': state}, function(err, ok){
|
||||
//console.log("<<<------- PAT", soul,key, val, state, 'in', +new Date - S);
|
||||
DBG && (DBG.spd = DBG.spd || +new Date);
|
||||
console.STAT && console.STAT(S, +new Date - S, 'put');
|
||||
//if(!err && count[id] !== lot.s){ console.log(err = "Disk count not same as ram count."); console.STAT && console.STAT(+new Date, lot.s - count[id], 'put ack != count') } delete count[id];
|
||||
if(err){ root.on('in', {'@': id, err: err, DBG: DBG}); return }
|
||||
root.on('in', {'@': id, ok: ok, DBG: DBG});
|
||||
//}, id, DBG && (DBG.r = DBG.r || {}));
|
||||
}, false && id, DBG && (DBG.r = DBG.r || {}));
|
||||
DBG && (DBG.sps = DBG.sps || +new Date);
|
||||
});
|
||||
var count = {}, obj_empty = Object.empty;
|
||||
|
||||
root.on('get', function(msg){
|
||||
this.to.next(msg);
|
||||
var ctx = msg._||'', DBG = ctx.DBG = msg.DBG; DBG && (DBG.sg = +new Date);
|
||||
var id = msg['#'], get = msg.get, soul = msg.get['#'], has = msg.get['.']||'', o = {}, graph, lex, key, tmp, force;
|
||||
if('string' == typeof soul){
|
||||
key = soul;
|
||||
} else
|
||||
if(soul){
|
||||
if(u !== (tmp = soul['*'])){ o.limit = force = 1 }
|
||||
if(u !== soul['>']){ o.start = soul['>'] }
|
||||
if(u !== soul['<']){ o.end = soul['<'] }
|
||||
key = force? (''+tmp) : tmp || soul['='];
|
||||
force = null;
|
||||
}
|
||||
if(key && !o.limit){ // a soul.has must be on a soul, and not during soul*
|
||||
if('string' == typeof has){
|
||||
key = key+esc+(o.atom = has);
|
||||
} else
|
||||
if(has){
|
||||
if(u !== has['>']){ o.start = has['>']; o.limit = 1 }
|
||||
if(u !== has['<']){ o.end = has['<']; o.limit = 1 }
|
||||
if(u !== (tmp = has['*'])){ o.limit = force = 1 }
|
||||
if(key){ key = key+esc + (force? (''+(tmp||'')) : tmp || (o.atom = has['='] || '')) }
|
||||
}
|
||||
}
|
||||
if((tmp = get['%']) || o.limit){
|
||||
o.limit = (tmp <= (o.pack || (1000 * 100)))? tmp : 1;
|
||||
}
|
||||
if(has['-'] || (soul||{})['-'] || get['-']){ o.reverse = true }
|
||||
if((tmp = (root.next||'')[soul]) && tmp.put){
|
||||
if(o.atom){
|
||||
tmp = (tmp.next||'')[o.atom] ;
|
||||
if(tmp && tmp.root && tmp.root.graph && tmp.root.graph[soul] && tmp.root.graph[soul][o.atom]){ return }
|
||||
} else
|
||||
if(tmp && tmp.rad){ return }
|
||||
}
|
||||
var now = Gun.state();
|
||||
var S = (+new Date), C = 0, SPT = 0; // STATS!
|
||||
DBG && (DBG.sgm = S);
|
||||
//var GID = String.random(3); console.log("GET ------->>>", GID, key, o, '?', get);
|
||||
dare(key||'', function(err, data, info){
|
||||
//console.log("<<<------- GOT", GID, +new Date - S, err, data);
|
||||
DBG && (DBG.sgr = +new Date);
|
||||
DBG && (DBG.sgi = info);
|
||||
try{opt.store.stats.get.time[statg % 50] = (+new Date) - S; ++statg;
|
||||
opt.store.stats.get.count++;
|
||||
if(err){ opt.store.stats.get.err = err }
|
||||
}catch(e){} // STATS!
|
||||
//if(u === data && info.chunks > 1){ return } // if we already sent a chunk, ignore ending empty responses. // this causes tests to fail.
|
||||
console.STAT && console.STAT(S, +new Date - S, 'got', JSON.stringify(key)); S = +new Date;
|
||||
info = info || '';
|
||||
var va, ve;
|
||||
if(info.unit && data && u !== (va = data[':']) && u !== (ve = data['>'])){ // new format
|
||||
var tmp = key.split(esc), so = tmp[0], ha = tmp[1];
|
||||
(graph = graph || {})[so] = Gun.state.ify(graph[so], ha, ve, va, so);
|
||||
root.$.get(so).get(ha)._.rad = now;
|
||||
// REMEMBER TO ADD _rad TO NODE/SOUL QUERY!
|
||||
} else
|
||||
if(data){ // old code path
|
||||
if(typeof data !== 'string'){
|
||||
if(o.atom){
|
||||
data = u;
|
||||
} else {
|
||||
Radix.map(data, each, o); // IS A RADIX TREE, NOT FUNCTION!
|
||||
}
|
||||
}
|
||||
if(!graph && data){ each(data, '') }
|
||||
// TODO: !has what about soul lookups?
|
||||
if(!o.atom && !has & 'string' == typeof soul && !o.limit && !o.more){
|
||||
root.$.get(soul)._.rad = now;
|
||||
}
|
||||
}
|
||||
DBG && (DBG.sgp = +new Date);
|
||||
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
|
||||
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
|
||||
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
|
||||
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
|
||||
// TODO: PERF NOTES! This is like 0.2s, but for each ack, or all? Can you cache these preps?
|
||||
// Or benchmark by reusing first start date.
|
||||
if(console.STAT && (ST = +new Date - S) > 9){ console.STAT(S, ST, 'got prep time'); console.STAT(S, C, 'got prep #') } SPT += ST; C = 0; S = +new Date;
|
||||
var faith = function(){}; faith.faith = true; faith.rad = get; // HNPERF: We're testing performance improvement by skipping going through security again, but this should be audited.
|
||||
root.on('in', {'@': id, put: graph, '%': info.more? 1 : u, err: err? err : u, _: faith, DBG: DBG});
|
||||
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'got emit', Object.keys(graph||{}).length);
|
||||
graph = u; // each is outside our scope, we have to reset graph to nothing!
|
||||
}, o, DBG && (DBG.r = DBG.r || {}));
|
||||
DBG && (DBG.sgd = +new Date);
|
||||
console.STAT && (ST = +new Date - S) > 9 && console.STAT(S, ST, 'get call'); // TODO: Perf: this was half a second??????
|
||||
function each(val, has, a,b){ // TODO: THIS CODE NEEDS TO BE FASTER!!!!
|
||||
C++;
|
||||
if(!val){ return }
|
||||
has = (key+has).split(esc);
|
||||
var soul = has.slice(0,1)[0];
|
||||
has = has.slice(-1)[0];
|
||||
if(o.limit && o.limit <= o.count){ return true }
|
||||
var va, ve, so = soul, ha = has;
|
||||
//if(u !== (va = val[':']) && u !== (ve = val['>'])){ // THIS HANDLES NEW CODE!
|
||||
if('string' != typeof val){ // THIS HANDLES NEW CODE!
|
||||
va = val[':']; ve = val['>'];
|
||||
(graph = graph || {})[so] = Gun.state.ify(graph[so], ha, ve, va, so);
|
||||
//root.$.get(so).get(ha)._.rad = now;
|
||||
o.count = (o.count || 0) + ((va||'').length || 9);
|
||||
return;
|
||||
}
|
||||
o.count = (o.count || 0) + val.length;
|
||||
var tmp = val.lastIndexOf('>');
|
||||
var state = Radisk.decode(val.slice(tmp+1), null, esc);
|
||||
val = Radisk.decode(val.slice(0,tmp), null, esc);
|
||||
(graph = graph || {})[soul] = Gun.state.ify(graph[soul], has, state, val, soul);
|
||||
}
|
||||
});
|
||||
var val_is = Gun.valid;
|
||||
(opt.store||{}).stats = {get:{time:{}, count:0}, put: {time:{}, count:0}}; // STATS!
|
||||
var statg = 0, statp = 0; // STATS!
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
(function (env) {
|
||||
var Gun;
|
||||
if(typeof module !== "undefined" && module.exports){ Gun = require('gun/gun') }
|
||||
if(typeof window !== "undefined"){ Gun = window.Gun }
|
||||
|
||||
Gun.chain.sync = function (obj, opt, cb, o) {
|
||||
var gun = this;
|
||||
if (!Gun.obj.is(obj)) {
|
||||
console.log('First param is not an object');
|
||||
return gun;
|
||||
}
|
||||
if (Gun.bi.is(opt)) {
|
||||
opt = {
|
||||
meta: opt
|
||||
};
|
||||
}
|
||||
if(Gun.fn.is(opt)){
|
||||
cb = opt;
|
||||
opt = null;
|
||||
}
|
||||
cb = cb || function(){};
|
||||
opt = opt || {};
|
||||
opt.ctx = opt.ctx || {};
|
||||
gun.on(function (change, field) {
|
||||
Gun.obj.map(change, function (val, field) {
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
if (field === '_' || field === '#') {
|
||||
if (opt.meta) {
|
||||
obj[field] = val;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Gun.obj.is(val)) {
|
||||
var soul = Gun.val.rel.is(val);
|
||||
if (opt.ctx[soul + field]) {
|
||||
// don't re-subscribe.
|
||||
return;
|
||||
}
|
||||
// unique subscribe!
|
||||
opt.ctx[soul + field] = true;
|
||||
this.path(field).sync(
|
||||
obj[field] = (obj[field] || {}),
|
||||
Gun.obj.copy(opt),
|
||||
cb,
|
||||
o || obj
|
||||
);
|
||||
return;
|
||||
}
|
||||
obj[field] = val;
|
||||
}, this);
|
||||
cb(o || obj);
|
||||
});
|
||||
return gun;
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -1,134 +0,0 @@
|
||||
;(function(){
|
||||
var GUN = (typeof window !== "undefined")? window.Gun : require('../gun');
|
||||
GUN.on('opt', function(root){
|
||||
this.to.next(root);
|
||||
var opt = root.opt;
|
||||
if(root.once){ return }
|
||||
if(!GUN.Mesh){ return }
|
||||
if(false === opt.RTCPeerConnection){ return }
|
||||
|
||||
var env;
|
||||
if(typeof window !== "undefined"){ env = window }
|
||||
if(typeof global !== "undefined"){ env = global }
|
||||
env = env || {};
|
||||
|
||||
var rtcpc = opt.RTCPeerConnection || env.RTCPeerConnection || env.webkitRTCPeerConnection || env.mozRTCPeerConnection;
|
||||
var rtcsd = opt.RTCSessionDescription || env.RTCSessionDescription || env.webkitRTCSessionDescription || env.mozRTCSessionDescription;
|
||||
var rtcic = opt.RTCIceCandidate || env.RTCIceCandidate || env.webkitRTCIceCandidate || env.mozRTCIceCandidate;
|
||||
if(!rtcpc || !rtcsd || !rtcic){ return }
|
||||
opt.RTCPeerConnection = rtcpc;
|
||||
opt.RTCSessionDescription = rtcsd;
|
||||
opt.RTCIceCandidate = rtcic;
|
||||
opt.rtc = opt.rtc || {'iceServers': [
|
||||
{urls: 'stun:stun.l.google.com:19302'},
|
||||
{urls: 'stun:stun.cloudflare.com:3478'}/*,
|
||||
{urls: "stun:stun.sipgate.net:3478"},
|
||||
{urls: "stun:stun.stunprotocol.org"},
|
||||
{urls: "stun:stun.sipgate.net:10000"},
|
||||
{urls: "stun:217.10.68.152:10000"},
|
||||
{urls: 'stun:stun.services.mozilla.com'}*/
|
||||
]};
|
||||
// TODO: Select the most appropriate stuns.
|
||||
// FIXME: Find the wire throwing ICE Failed
|
||||
// The above change corrects at least firefox RTC Peer handler where it **throws** on over 6 ice servers, and updates url: to urls: removing deprecation warning
|
||||
opt.rtc.dataChannel = opt.rtc.dataChannel || {ordered: false, maxRetransmits: 2};
|
||||
opt.rtc.sdp = opt.rtc.sdp || {mandatory: {OfferToReceiveAudio: false, OfferToReceiveVideo: false}};
|
||||
opt.rtc.max = opt.rtc.max || 55; // is this a magic number? // For Future WebRTC notes: Chrome 500 max limit, however 256 likely - FF "none", webtorrent does 55 per torrent.
|
||||
opt.rtc.room = opt.rtc.room || GUN.window && (window.rtcRoom || location.hash.slice(1) || location.pathname.slice(1));
|
||||
opt.announce = function(to){
|
||||
opt.rtc.start = +new Date; // handle room logic:
|
||||
root.$.get('/RTC/'+opt.rtc.room+'<?99').get('+').put(opt.pid, function(ack){
|
||||
if(!ack.ok || !ack.ok.rtc){ return }
|
||||
plan(ack);
|
||||
}, {acks: opt.rtc.max}).on(function(last,key, msg){
|
||||
if(last === opt.pid || opt.rtc.start > msg.put['>']){ return }
|
||||
plan({'#': ''+msg['#'], ok: {rtc: {id: last}}});
|
||||
});
|
||||
};
|
||||
|
||||
var mesh = opt.mesh = opt.mesh || GUN.Mesh(root), wired = mesh.wire;
|
||||
mesh.hear['rtc'] = plan;
|
||||
mesh.wire = function(media){ try{ wired && wired(media);
|
||||
if(!(media instanceof MediaStream)){ return }
|
||||
(open.media = open.media||{})[media.id] = media;
|
||||
for(var p in opt.peers){ p = opt.peers[p]||'';
|
||||
p.addTrack && media.getTracks().forEach(track => {
|
||||
p.addTrack(track, media);
|
||||
});
|
||||
p.createOffer && p.createOffer(function(offer){
|
||||
p.setLocalDescription(offer);
|
||||
mesh.say({'#': root.ask(plan), dam: 'rtc', ok: {rtc: {offer: offer, id: opt.pid}}}, p);
|
||||
}, function(){}, opt.rtc.sdp);
|
||||
}
|
||||
} catch(e){console.log(e)} }
|
||||
root.on('create', function(at){
|
||||
this.to.next(at);
|
||||
setTimeout(opt.announce, 1);
|
||||
});
|
||||
|
||||
function plan(msg){
|
||||
if(!msg.ok){ return }
|
||||
var rtc = msg.ok.rtc, peer, tmp;
|
||||
if(!rtc || !rtc.id || rtc.id === opt.pid){ return }
|
||||
peer = open(msg, rtc);
|
||||
if(tmp = rtc.candidate){
|
||||
return peer.addIceCandidate(new opt.RTCIceCandidate(tmp));
|
||||
}
|
||||
if(tmp = rtc.answer){
|
||||
tmp.sdp = tmp.sdp.replace(/\\r\\n/g, '\r\n');
|
||||
return peer.setRemoteDescription(peer.remoteSet = new opt.RTCSessionDescription(tmp));
|
||||
}
|
||||
if(tmp = rtc.offer){
|
||||
rtc.offer.sdp = rtc.offer.sdp.replace(/\\r\\n/g, '\r\n');
|
||||
peer.setRemoteDescription(new opt.RTCSessionDescription(tmp));
|
||||
return peer.createAnswer(function(answer){
|
||||
peer.setLocalDescription(answer);
|
||||
root.on('out', {'@': msg['#'], ok: {rtc: {answer: answer, id: opt.pid}}});
|
||||
}, function(){}, opt.rtc.sdp);
|
||||
}
|
||||
}
|
||||
function open(msg, rtc, peer){
|
||||
if(peer = opt.peers[rtc.id] || open[rtc.id]){ return peer }
|
||||
(peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id;
|
||||
var wire = peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel);
|
||||
function rtceve(eve){ eve.peer = peer; gun.on('rtc', eve) }
|
||||
peer.$ = gun;
|
||||
open[rtc.id] = peer;
|
||||
peer.ontrack = rtceve;
|
||||
peer.onremovetrack = rtceve;
|
||||
peer.onconnectionstatechange = rtceve;
|
||||
wire.to = setTimeout(function(){delete open[rtc.id]},1000*60);
|
||||
wire.onclose = function(){ mesh.bye(peer) };
|
||||
wire.onerror = function(err){ };
|
||||
wire.onopen = function(e){
|
||||
delete open[rtc.id];
|
||||
mesh.hi(peer);
|
||||
}
|
||||
wire.onmessage = function(msg){
|
||||
if(!msg){ return }
|
||||
mesh.hear(msg.data || msg, peer);
|
||||
};
|
||||
peer.onicecandidate = function(e){ rtceve(e);
|
||||
if(!e.candidate){ return }
|
||||
root.on('out', {'@': (msg||'')['#'], '#': root.ask(plan), ok: {rtc: {candidate: e.candidate, id: opt.pid}}});
|
||||
}
|
||||
peer.ondatachannel = function(e){ rtceve(e);
|
||||
var rc = e.channel;
|
||||
rc.onmessage = wire.onmessage;
|
||||
rc.onopen = wire.onopen;
|
||||
rc.onclose = wire.onclose;
|
||||
}
|
||||
if(rtc.offer){ return peer }
|
||||
for(var m in open.media){ m = open.media[m];
|
||||
m.getTracks().forEach(track => {
|
||||
peer.addTrack(track, m);
|
||||
});
|
||||
}
|
||||
peer.createOffer(function(offer){
|
||||
peer.setLocalDescription(offer);
|
||||
root.on('out', {'@': (msg||'')['#'], '#': root.ask(plan), ok: {rtc: {offer: offer, id: opt.pid}}});
|
||||
}, function(){}, opt.rtc.sdp);
|
||||
return peer;
|
||||
}
|
||||
});
|
||||
}());
|
||||
@@ -1,244 +0,0 @@
|
||||
;(function(){
|
||||
// JSON: JavaScript Object Notation
|
||||
// YSON: Yielding javaScript Object Notation
|
||||
var yson = {}, u, sI = setTimeout.turn || (typeof setImmediate != ''+u && setImmediate) || setTimeout;
|
||||
|
||||
yson.parseAsync = function(text, done, revive, M){
|
||||
if('string' != typeof text){ try{ done(u,JSON.parse(text)) }catch(e){ done(e) } return }
|
||||
var ctx = {i: 0, text: text, done: done, l: text.length, up: []};
|
||||
//M = 1024 * 1024 * 100;
|
||||
//M = M || 1024 * 64;
|
||||
M = M || 1024 * 32;
|
||||
parse();
|
||||
function parse(){
|
||||
//var S = +new Date;
|
||||
var s = ctx.text;
|
||||
var i = ctx.i, l = ctx.l, j = 0;
|
||||
var w = ctx.w, b, tmp;
|
||||
while(j++ < M){
|
||||
var c = s[i++];
|
||||
if(i > l){
|
||||
ctx.end = true;
|
||||
break;
|
||||
}
|
||||
if(w){
|
||||
i = s.indexOf('"', i-1); c = s[i];
|
||||
tmp = 0; while('\\' == s[i-(++tmp)]){}; tmp = !(tmp % 2);//tmp = ('\\' == s[i-1]); // json is stupid
|
||||
b = b || tmp;
|
||||
if('"' == c && !tmp){
|
||||
w = u;
|
||||
tmp = ctx.s;
|
||||
if(ctx.a){
|
||||
tmp = s.slice(ctx.sl, i);
|
||||
if(b || (1+tmp.indexOf('\\'))){ tmp = JSON.parse('"'+tmp+'"') } // escape + unicode :( handling
|
||||
if(ctx.at instanceof Array){
|
||||
ctx.at.push(ctx.s = tmp);
|
||||
} else {
|
||||
if(!ctx.at){ ctx.end = j = M; tmp = u }
|
||||
(ctx.at||{})[ctx.s] = ctx.s = tmp;
|
||||
}
|
||||
ctx.s = u;
|
||||
} else {
|
||||
ctx.s = s.slice(ctx.sl, i);
|
||||
if(b || (1+ctx.s.indexOf('\\'))){ ctx.s = JSON.parse('"'+ctx.s+'"'); } // escape + unicode :( handling
|
||||
}
|
||||
ctx.a = b = u;
|
||||
}
|
||||
++i;
|
||||
} else {
|
||||
switch(c){
|
||||
case '"':
|
||||
ctx.sl = i;
|
||||
w = true;
|
||||
break;
|
||||
case ':':
|
||||
ctx.ai = i;
|
||||
ctx.a = true;
|
||||
break;
|
||||
case ',':
|
||||
if(ctx.a || ctx.at instanceof Array){
|
||||
if(tmp = s.slice(ctx.ai, i-1)){
|
||||
if(u !== (tmp = value(tmp))){
|
||||
if(ctx.at instanceof Array){
|
||||
ctx.at.push(tmp);
|
||||
} else {
|
||||
ctx.at[ctx.s] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.a = u;
|
||||
if(ctx.at instanceof Array){
|
||||
ctx.a = true;
|
||||
ctx.ai = i;
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
ctx.up.push(ctx.at||(ctx.at = {}));
|
||||
if(ctx.at instanceof Array){
|
||||
ctx.at.push(ctx.at = {});
|
||||
} else
|
||||
if(u !== (tmp = ctx.s)){
|
||||
ctx.at[tmp] = ctx.at = {};
|
||||
}
|
||||
ctx.a = u;
|
||||
break;
|
||||
case '}':
|
||||
if(ctx.a){
|
||||
if(tmp = s.slice(ctx.ai, i-1)){
|
||||
if(u !== (tmp = value(tmp))){
|
||||
if(ctx.at instanceof Array){
|
||||
ctx.at.push(tmp);
|
||||
} else {
|
||||
if(!ctx.at){ ctx.end = j = M; tmp = u }
|
||||
(ctx.at||{})[ctx.s] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.a = u;
|
||||
ctx.at = ctx.up.pop();
|
||||
break;
|
||||
case '[':
|
||||
if(u !== (tmp = ctx.s)){
|
||||
ctx.up.push(ctx.at);
|
||||
ctx.at[tmp] = ctx.at = [];
|
||||
} else
|
||||
if(!ctx.at){
|
||||
ctx.up.push(ctx.at = []);
|
||||
}
|
||||
ctx.a = true;
|
||||
ctx.ai = i;
|
||||
break;
|
||||
case ']':
|
||||
if(ctx.a){
|
||||
if(tmp = s.slice(ctx.ai, i-1)){
|
||||
if(u !== (tmp = value(tmp))){
|
||||
if(ctx.at instanceof Array){
|
||||
ctx.at.push(tmp);
|
||||
} else {
|
||||
ctx.at[ctx.s] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.a = u;
|
||||
ctx.at = ctx.up.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.s = u;
|
||||
ctx.i = i;
|
||||
ctx.w = w;
|
||||
if(ctx.end){
|
||||
tmp = ctx.at;
|
||||
if(u === tmp){
|
||||
try{ tmp = JSON.parse(text)
|
||||
}catch(e){ return ctx.done(e) }
|
||||
}
|
||||
ctx.done(u, tmp);
|
||||
} else {
|
||||
sI(parse);
|
||||
}
|
||||
}
|
||||
}
|
||||
function value(s){
|
||||
var n = parseFloat(s);
|
||||
if(!isNaN(n)){
|
||||
return n;
|
||||
}
|
||||
s = s.trim();
|
||||
if('true' == s){
|
||||
return true;
|
||||
}
|
||||
if('false' == s){
|
||||
return false;
|
||||
}
|
||||
if('null' == s){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
yson.stringifyAsync = function(data, done, replacer, space, ctx){
|
||||
//try{done(u, JSON.stringify(data, replacer, space))}catch(e){done(e)}return;
|
||||
ctx = ctx || {};
|
||||
ctx.text = ctx.text || "";
|
||||
ctx.up = [ctx.at = {d: data}];
|
||||
ctx.done = done;
|
||||
ctx.i = 0;
|
||||
var j = 0;
|
||||
ify();
|
||||
function ify(){
|
||||
var at = ctx.at, data = at.d, add = '', tmp;
|
||||
if(at.i && (at.i - at.j) > 0){ add += ',' }
|
||||
if(u !== (tmp = at.k)){ add += JSON.stringify(tmp) + ':' } //'"'+tmp+'":' } // only if backslash
|
||||
switch(typeof data){
|
||||
case 'boolean':
|
||||
add += ''+data;
|
||||
break;
|
||||
case 'string':
|
||||
add += JSON.stringify(data); //ctx.text += '"'+data+'"';//JSON.stringify(data); // only if backslash
|
||||
break;
|
||||
case 'number':
|
||||
add += (isNaN(data)? 'null' : data);
|
||||
break;
|
||||
case 'object':
|
||||
if(!data){
|
||||
add += 'null';
|
||||
break;
|
||||
}
|
||||
if(data instanceof Array){
|
||||
add += '[';
|
||||
at = {i: -1, as: data, up: at, j: 0};
|
||||
at.l = data.length;
|
||||
ctx.up.push(ctx.at = at);
|
||||
break;
|
||||
}
|
||||
if('function' != typeof (data||'').toJSON){
|
||||
add += '{';
|
||||
at = {i: -1, ok: Object.keys(data).sort(), as: data, up: at, j: 0};
|
||||
at.l = at.ok.length;
|
||||
ctx.up.push(ctx.at = at);
|
||||
break;
|
||||
}
|
||||
if(tmp = data.toJSON()){
|
||||
add += tmp;
|
||||
break;
|
||||
}
|
||||
// let this & below pass into default case...
|
||||
case 'function':
|
||||
if(at.as instanceof Array){
|
||||
add += 'null';
|
||||
break;
|
||||
}
|
||||
default: // handle wrongly added leading `,` if previous item not JSON-able.
|
||||
add = '';
|
||||
at.j++;
|
||||
}
|
||||
ctx.text += add;
|
||||
while(1+at.i >= at.l){
|
||||
ctx.text += (at.ok? '}' : ']');
|
||||
at = ctx.at = at.up;
|
||||
}
|
||||
if(++at.i < at.l){
|
||||
if(tmp = at.ok){
|
||||
at.d = at.as[at.k = tmp[at.i]];
|
||||
} else {
|
||||
at.d = at.as[at.i];
|
||||
}
|
||||
if(++j < 9){ return ify() } else { j = 0 }
|
||||
sI(ify);
|
||||
return;
|
||||
}
|
||||
ctx.done(u, ctx.text);
|
||||
}
|
||||
}
|
||||
if(typeof window != ''+u){ window.YSON = yson }
|
||||
try{ if(typeof module != ''+u){ module.exports = yson } }catch(e){}
|
||||
if(typeof JSON != ''+u){
|
||||
JSON.parseAsync = yson.parseAsync;
|
||||
JSON.stringifyAsync = yson.stringifyAsync;
|
||||
}
|
||||
|
||||
}());
|
||||
42
build.py
@@ -1,24 +1,50 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
def get_all_files(directory):
|
||||
files = []
|
||||
for root, _, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
# Convert to relative path and normalize separators
|
||||
rel_path = os.path.relpath(path, directory)
|
||||
files.append(rel_path.replace('\\', '/'))
|
||||
return files
|
||||
|
||||
PREFETCH = ""
|
||||
VERSIONCO = "2025-07-30_3"
|
||||
HANDLEPARSE = os.listdir("src/")
|
||||
ASSETS = json.load(open("_assets.json", "r")) + HANDLEPARSE
|
||||
VERSIONCO = "2026-02-23_" + time.strftime("%Y%m%d%H%M%S")
|
||||
HANDLEPARSE = get_all_files("src")
|
||||
TITLE = os.environ.get("TELESEC_TITLE", "TeleSec")
|
||||
HOSTER = os.environ.get("TELESEC_HOSTER", "EuskadiTech")
|
||||
# Combine assets from JSON and recursively found files
|
||||
ASSETS = get_all_files("assets")
|
||||
|
||||
for asset in ASSETS:
|
||||
if asset != "sw.js":
|
||||
PREFETCH += f'<link rel="prefetch" href="{asset}" />\n'
|
||||
for src in HANDLEPARSE:
|
||||
if src != "sw.js":
|
||||
PREFETCH += f'<link rel="prefetch" href="{src}" />\n'
|
||||
|
||||
if os.path.exists("dist"):
|
||||
shutil.rmtree("dist")
|
||||
shutil.copytree("assets","dist", dirs_exist_ok=True)
|
||||
|
||||
os.system("rm -r dist ; mkdir -p dist ; cp -r assets/* dist/")
|
||||
def replace_handles(string):
|
||||
string = string.replace("%%PREFETCH%%", PREFETCH)
|
||||
string = string.replace("%%VERSIONCO%%", VERSIONCO)
|
||||
string = string.replace("%%TITLE%%", TITLE)
|
||||
string = string.replace("%%HOSTER%%", HOSTER)
|
||||
string = string.replace("%%ASSETSJSON%%", json.dumps(ASSETS, ensure_ascii=False))
|
||||
return string
|
||||
|
||||
|
||||
for file in HANDLEPARSE:
|
||||
with open("src/" + file, "r") as f:
|
||||
out = replace_handles(f.read())
|
||||
with open("dist/" + file, "w") as f:
|
||||
f.write(out)
|
||||
print(file)
|
||||
with open("src/" + file, "r", encoding="utf-8") as f1:
|
||||
out = replace_handles(f1.read())
|
||||
with open("dist/" + file, "w", encoding="utf-8") as f2:
|
||||
f2.write(out)
|
||||
|
||||
175
decrypt.js
Normal file
@@ -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
|
After Width: | Height: | Size: 103 KiB |
@@ -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>
|
||||
273
python_sdk/telesec_couchdb.py
Normal file
@@ -0,0 +1,273 @@
|
||||
import base64
|
||||
import email.utils
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
|
||||
class TeleSecCryptoError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TeleSecCouchDBError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
|
||||
pad_len = block_size - (len(data) % block_size)
|
||||
return data + bytes([pad_len]) * pad_len
|
||||
|
||||
|
||||
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
|
||||
if not data or len(data) % block_size != 0:
|
||||
raise TeleSecCryptoError("Invalid padded data length")
|
||||
pad_len = data[-1]
|
||||
if pad_len < 1 or pad_len > block_size:
|
||||
raise TeleSecCryptoError("Invalid PKCS7 padding")
|
||||
if data[-pad_len:] != bytes([pad_len]) * pad_len:
|
||||
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
|
||||
return data[:-pad_len]
|
||||
|
||||
|
||||
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
|
||||
d = b""
|
||||
prev = b""
|
||||
while len(d) < key_len + iv_len:
|
||||
prev = hashlib.md5(prev + passphrase + salt).digest()
|
||||
d += prev
|
||||
return d[:key_len], d[key_len : key_len + iv_len]
|
||||
|
||||
|
||||
def _json_dumps_like_js(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def ts_encrypt(input_value: Any, secret: str) -> str:
|
||||
"""
|
||||
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
|
||||
wrapped as RSA{<ciphertext>}.
|
||||
"""
|
||||
if secret is None or secret == "":
|
||||
if isinstance(input_value, str):
|
||||
return input_value
|
||||
return _json_dumps_like_js(input_value)
|
||||
|
||||
payload = input_value
|
||||
if not isinstance(input_value, str):
|
||||
try:
|
||||
payload = _json_dumps_like_js(input_value)
|
||||
except Exception:
|
||||
payload = str(input_value)
|
||||
|
||||
payload_bytes = payload.encode("utf-8")
|
||||
salt = os.urandom(8)
|
||||
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
|
||||
openssl_blob = b"Salted__" + salt + encrypted
|
||||
b64 = base64.b64encode(openssl_blob).decode("utf-8")
|
||||
return f"RSA{{{b64}}}"
|
||||
|
||||
|
||||
def ts_encrypt(input_value: Any, secret: str) -> str:
|
||||
if not isinstance(input_value, str):
|
||||
payload = json.dumps(input_value, separators=(",", ":"), ensure_ascii=False)
|
||||
else:
|
||||
payload = input_value
|
||||
|
||||
payload_bytes = payload.encode("utf-8")
|
||||
salt = os.urandom(8)
|
||||
|
||||
# OpenSSL EVP_BytesToKey (MD5)
|
||||
dx = b""
|
||||
salted = b""
|
||||
while len(salted) < 48: # 32 key + 16 iv
|
||||
dx = hashlib.md5(dx + secret.encode() + salt).digest()
|
||||
salted += dx
|
||||
|
||||
key = salted[:32]
|
||||
iv = salted[32:48]
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
|
||||
|
||||
openssl_blob = b"Salted__" + salt + encrypted
|
||||
b64 = base64.b64encode(openssl_blob).decode("utf-8")
|
||||
|
||||
return f"RSA{{{b64}}}"
|
||||
|
||||
@dataclass
|
||||
class TeleSecDoc:
|
||||
id: str
|
||||
data: Any
|
||||
raw: Dict[str, Any]
|
||||
|
||||
|
||||
class TeleSecCouchDB:
|
||||
"""
|
||||
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
|
||||
No local replication layer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server_url: str,
|
||||
dbname: str,
|
||||
secret: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
timeout: int = 30,
|
||||
session: Optional[requests.Session] = None,
|
||||
) -> None:
|
||||
self.server_url = server_url.rstrip("/")
|
||||
self.dbname = dbname
|
||||
self.secret = secret or ""
|
||||
self.timeout = timeout
|
||||
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
|
||||
self.session = session or requests.Session()
|
||||
self.session.headers.update({"Accept": "application/json"})
|
||||
if username is not None:
|
||||
self.session.auth = (username, password or "")
|
||||
|
||||
def _iso_now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
def _doc_id(self, table: str, item_id: str) -> str:
|
||||
return f"{table}:{item_id}"
|
||||
|
||||
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
|
||||
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
res = self.session.request(method=method, url=url, **kwargs)
|
||||
return res
|
||||
|
||||
def get_server_datetime(self) -> datetime:
|
||||
"""
|
||||
Returns server datetime using HTTP Date header from CouchDB.
|
||||
Avoids reliance on local machine clock.
|
||||
"""
|
||||
candidates = [
|
||||
("HEAD", self.base_url),
|
||||
("GET", self.base_url),
|
||||
("HEAD", self.server_url),
|
||||
("GET", self.server_url),
|
||||
]
|
||||
for method, url in candidates:
|
||||
try:
|
||||
res = self.session.request(method=method, url=url, timeout=self.timeout)
|
||||
date_header = res.headers.get("Date")
|
||||
if not date_header:
|
||||
continue
|
||||
dt = email.utils.parsedate_to_datetime(date_header)
|
||||
if dt is None:
|
||||
continue
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
except Exception:
|
||||
continue
|
||||
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
|
||||
|
||||
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
|
||||
now = self.get_server_datetime()
|
||||
target = now.timestamp() + (minutes * 60)
|
||||
out = datetime.fromtimestamp(target, tz=timezone.utc)
|
||||
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
def check_connection(self) -> Dict[str, Any]:
|
||||
res = self._request("GET")
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
|
||||
return res.json()
|
||||
|
||||
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
|
||||
res = self._request("GET", quote(doc_id, safe=""))
|
||||
if res.status_code == 404:
|
||||
return None
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
|
||||
return res.json()
|
||||
|
||||
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if "_id" not in doc:
|
||||
raise ValueError("Document must include _id")
|
||||
res = self._request(
|
||||
"PUT",
|
||||
quote(doc["_id"], safe=""),
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=_json_dumps_like_js(doc).encode("utf-8"),
|
||||
)
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
|
||||
return res.json()
|
||||
|
||||
def delete_raw(self, doc_id: str) -> bool:
|
||||
doc = self.get_raw(doc_id)
|
||||
if not doc:
|
||||
return False
|
||||
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
|
||||
return True
|
||||
|
||||
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
|
||||
doc_id = self._doc_id(table, item_id)
|
||||
|
||||
if data is None:
|
||||
self.delete_raw(doc_id)
|
||||
return {"ok": True, "id": doc_id, "deleted": True}
|
||||
|
||||
existing = self.get_raw(doc_id)
|
||||
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
|
||||
|
||||
to_store = data
|
||||
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
|
||||
if encrypt and self.secret and not is_encrypted_string:
|
||||
to_store = ts_encrypt(data, self.secret)
|
||||
|
||||
doc["data"] = to_store
|
||||
doc["table"] = table
|
||||
doc["ts"] = self._iso_now()
|
||||
|
||||
return self.put_raw(doc)
|
||||
|
||||
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
|
||||
doc_id = self._doc_id(table, item_id)
|
||||
doc = self.get_raw(doc_id)
|
||||
if not doc:
|
||||
return None
|
||||
value = doc.get("data")
|
||||
if decrypt:
|
||||
return ts_decrypt(value, self.secret)
|
||||
return value
|
||||
|
||||
def delete(self, table: str, item_id: str) -> bool:
|
||||
return self.delete_raw(self._doc_id(table, item_id))
|
||||
|
||||
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
|
||||
params = {
|
||||
"include_docs": "true",
|
||||
"startkey": f'"{table}:"',
|
||||
"endkey": f'"{table}:\uffff"',
|
||||
}
|
||||
res = self._request("GET", "_all_docs", params=params)
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
|
||||
|
||||
rows = res.json().get("rows", [])
|
||||
out: List[TeleSecDoc] = []
|
||||
for row in rows:
|
||||
doc = row.get("doc") or {}
|
||||
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
|
||||
value = doc.get("data")
|
||||
if decrypt:
|
||||
value = ts_decrypt(value, self.secret)
|
||||
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
|
||||
return out
|
||||
545
python_sdk/windows_agent.py
Normal file
@@ -0,0 +1,545 @@
|
||||
import argparse
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional, List
|
||||
|
||||
import psutil
|
||||
import base64
|
||||
import email.utils
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
|
||||
class TeleSecCryptoError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TeleSecCouchDBError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
|
||||
pad_len = block_size - (len(data) % block_size)
|
||||
return data + bytes([pad_len]) * pad_len
|
||||
|
||||
|
||||
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
|
||||
if not data or len(data) % block_size != 0:
|
||||
raise TeleSecCryptoError("Invalid padded data length")
|
||||
pad_len = data[-1]
|
||||
if pad_len < 1 or pad_len > block_size:
|
||||
raise TeleSecCryptoError("Invalid PKCS7 padding")
|
||||
if data[-pad_len:] != bytes([pad_len]) * pad_len:
|
||||
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
|
||||
return data[:-pad_len]
|
||||
|
||||
|
||||
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
|
||||
d = b""
|
||||
prev = b""
|
||||
while len(d) < key_len + iv_len:
|
||||
prev = hashlib.md5(prev + passphrase + salt).digest()
|
||||
d += prev
|
||||
return d[:key_len], d[key_len : key_len + iv_len]
|
||||
|
||||
|
||||
def _json_dumps_like_js(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def ts_encrypt(input_value: Any, secret: str) -> str:
|
||||
"""
|
||||
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
|
||||
wrapped as RSA{<ciphertext>}.
|
||||
"""
|
||||
if secret is None or secret == "":
|
||||
if isinstance(input_value, str):
|
||||
return input_value
|
||||
return _json_dumps_like_js(input_value)
|
||||
|
||||
payload = input_value
|
||||
if not isinstance(input_value, str):
|
||||
try:
|
||||
payload = _json_dumps_like_js(input_value)
|
||||
except Exception:
|
||||
payload = str(input_value)
|
||||
|
||||
payload_bytes = payload.encode("utf-8")
|
||||
salt = os.urandom(8)
|
||||
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
|
||||
openssl_blob = b"Salted__" + salt + encrypted
|
||||
b64 = base64.b64encode(openssl_blob).decode("utf-8")
|
||||
return f"RSA{{{b64}}}"
|
||||
|
||||
|
||||
|
||||
def ts_decrypt(input_value: Any, secret: str) -> Any:
|
||||
"""
|
||||
Compatible with JS TS_decrypt behavior:
|
||||
- If not string: return as-is.
|
||||
- If RSA{...}: decrypt AES(CryptoJS passphrase mode), parse JSON when possible.
|
||||
- If plain string JSON: parse JSON.
|
||||
- Else: return raw string.
|
||||
"""
|
||||
if not isinstance(input_value, str):
|
||||
return input_value
|
||||
|
||||
is_wrapped = input_value.startswith("RSA{") and input_value.endswith("}")
|
||||
if is_wrapped:
|
||||
if not secret:
|
||||
raise TeleSecCryptoError("Secret is required to decrypt RSA payload")
|
||||
b64 = input_value[4:-1]
|
||||
try:
|
||||
raw = base64.b64decode(b64)
|
||||
except Exception as exc:
|
||||
raise TeleSecCryptoError("Invalid base64 payload") from exc
|
||||
|
||||
if len(raw) < 16 or not raw.startswith(b"Salted__"):
|
||||
raise TeleSecCryptoError("Unsupported encrypted payload format")
|
||||
|
||||
salt = raw[8:16]
|
||||
ciphertext = raw[16:]
|
||||
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
decrypted = cipher.decrypt(ciphertext)
|
||||
decrypted = _pkcs7_unpad(decrypted, 16)
|
||||
|
||||
try:
|
||||
text = decrypted.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
text = decrypted.decode("latin-1")
|
||||
|
||||
try:
|
||||
return json.loads(text)
|
||||
except Exception:
|
||||
return text
|
||||
|
||||
try:
|
||||
return json.loads(input_value)
|
||||
except Exception:
|
||||
return input_value
|
||||
|
||||
@dataclass
|
||||
class TeleSecDoc:
|
||||
id: str
|
||||
data: Any
|
||||
raw: Dict[str, Any]
|
||||
|
||||
|
||||
class TeleSecCouchDB:
|
||||
"""
|
||||
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
|
||||
No local replication layer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server_url: str,
|
||||
dbname: str,
|
||||
secret: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
timeout: int = 30,
|
||||
session: Optional[requests.Session] = None,
|
||||
) -> None:
|
||||
self.server_url = server_url.rstrip("/")
|
||||
self.dbname = dbname
|
||||
self.secret = secret or ""
|
||||
self.timeout = timeout
|
||||
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
|
||||
self.session = session or requests.Session()
|
||||
self.session.headers.update({"Accept": "application/json"})
|
||||
if username is not None:
|
||||
self.session.auth = (username, password or "")
|
||||
|
||||
def _iso_now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
def _doc_id(self, table: str, item_id: str) -> str:
|
||||
return f"{table}:{item_id}"
|
||||
|
||||
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
|
||||
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
res = self.session.request(method=method, url=url, **kwargs)
|
||||
return res
|
||||
|
||||
def get_server_datetime(self) -> datetime:
|
||||
"""
|
||||
Returns server datetime using HTTP Date header from CouchDB.
|
||||
Avoids reliance on local machine clock.
|
||||
"""
|
||||
candidates = [
|
||||
("HEAD", self.base_url),
|
||||
("GET", self.base_url),
|
||||
("HEAD", self.server_url),
|
||||
("GET", self.server_url),
|
||||
]
|
||||
for method, url in candidates:
|
||||
try:
|
||||
res = self.session.request(method=method, url=url, timeout=self.timeout)
|
||||
date_header = res.headers.get("Date")
|
||||
if not date_header:
|
||||
continue
|
||||
dt = email.utils.parsedate_to_datetime(date_header)
|
||||
if dt is None:
|
||||
continue
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
except Exception:
|
||||
continue
|
||||
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
|
||||
|
||||
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
|
||||
now = self.get_server_datetime()
|
||||
target = now.timestamp() + (minutes * 60)
|
||||
out = datetime.fromtimestamp(target, tz=timezone.utc)
|
||||
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
def check_connection(self) -> Dict[str, Any]:
|
||||
res = self._request("GET")
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
|
||||
return res.json()
|
||||
|
||||
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
|
||||
res = self._request("GET", quote(doc_id, safe=""))
|
||||
if res.status_code == 404:
|
||||
return None
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
|
||||
return res.json()
|
||||
|
||||
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if "_id" not in doc:
|
||||
raise ValueError("Document must include _id")
|
||||
res = self._request(
|
||||
"PUT",
|
||||
quote(doc["_id"], safe=""),
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=_json_dumps_like_js(doc).encode("utf-8"),
|
||||
)
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
|
||||
return res.json()
|
||||
|
||||
def delete_raw(self, doc_id: str) -> bool:
|
||||
doc = self.get_raw(doc_id)
|
||||
if not doc:
|
||||
return False
|
||||
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
|
||||
return True
|
||||
|
||||
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
|
||||
doc_id = self._doc_id(table, item_id)
|
||||
|
||||
if data is None:
|
||||
self.delete_raw(doc_id)
|
||||
return {"ok": True, "id": doc_id, "deleted": True}
|
||||
|
||||
existing = self.get_raw(doc_id)
|
||||
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
|
||||
|
||||
to_store = data
|
||||
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
|
||||
if encrypt and self.secret and not is_encrypted_string:
|
||||
to_store = ts_encrypt(data, self.secret)
|
||||
|
||||
doc["data"] = to_store
|
||||
doc["table"] = table
|
||||
doc["ts"] = self._iso_now()
|
||||
|
||||
return self.put_raw(doc)
|
||||
|
||||
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
|
||||
doc_id = self._doc_id(table, item_id)
|
||||
doc = self.get_raw(doc_id)
|
||||
if not doc:
|
||||
return None
|
||||
value = doc.get("data")
|
||||
if decrypt:
|
||||
return ts_decrypt(value, self.secret)
|
||||
return value
|
||||
|
||||
def delete(self, table: str, item_id: str) -> bool:
|
||||
return self.delete_raw(self._doc_id(table, item_id))
|
||||
|
||||
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
|
||||
params = {
|
||||
"include_docs": "true",
|
||||
"startkey": f'"{table}:"',
|
||||
"endkey": f'"{table}:\uffff"',
|
||||
}
|
||||
res = self._request("GET", "_all_docs", params=params)
|
||||
if res.status_code >= 400:
|
||||
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
|
||||
|
||||
rows = res.json().get("rows", [])
|
||||
out: List[TeleSecDoc] = []
|
||||
for row in rows:
|
||||
doc = row.get("doc") or {}
|
||||
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
|
||||
value = doc.get("data")
|
||||
if decrypt:
|
||||
value = ts_decrypt(value, self.secret)
|
||||
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
|
||||
return out
|
||||
|
||||
def utcnow_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def parse_iso(value: str) -> Optional[datetime]:
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
v = value.strip().replace("Z", "+00:00")
|
||||
dt = datetime.fromisoformat(v)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_current_username() -> str:
|
||||
try:
|
||||
return psutil.Process().username() or os.getlogin()
|
||||
except Exception:
|
||||
try:
|
||||
return os.getlogin()
|
||||
except Exception:
|
||||
return os.environ.get("USERNAME", "")
|
||||
|
||||
|
||||
def _window_title(hwnd: int) -> str:
|
||||
buf_len = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
|
||||
if buf_len <= 0:
|
||||
return ""
|
||||
buf = ctypes.create_unicode_buffer(buf_len + 1)
|
||||
ctypes.windll.user32.GetWindowTextW(hwnd, buf, buf_len + 1)
|
||||
return buf.value or ""
|
||||
|
||||
|
||||
def get_active_app() -> Dict[str, str]:
|
||||
exe = ""
|
||||
title = ""
|
||||
try:
|
||||
hwnd = ctypes.windll.user32.GetForegroundWindow()
|
||||
if hwnd:
|
||||
title = _window_title(hwnd)
|
||||
pid = ctypes.c_ulong()
|
||||
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||
if pid.value:
|
||||
try:
|
||||
proc = psutil.Process(pid.value)
|
||||
exe = proc.name() or ""
|
||||
except Exception:
|
||||
exe = ""
|
||||
except Exception:
|
||||
pass
|
||||
return {"exe": exe, "title": title}
|
||||
|
||||
|
||||
def build_payload(machine_id: str) -> Dict[str, Any]:
|
||||
app = get_active_app()
|
||||
return {
|
||||
"Hostname": machine_id,
|
||||
"UsuarioActual": get_current_username(),
|
||||
"AppActualEjecutable": app.get("exe", ""),
|
||||
"AppActualTitulo": app.get("title", ""),
|
||||
# campo local diagnóstico (no se usa para decisión remota)
|
||||
"AgentLocalSeenAt": utcnow_iso(),
|
||||
}
|
||||
|
||||
|
||||
def should_shutdown(data: Dict[str, Any], server_now: datetime) -> bool:
|
||||
target = parse_iso(str(data.get("ShutdownBeforeDate", "") or ""))
|
||||
if not target:
|
||||
return False
|
||||
return server_now <= target
|
||||
|
||||
|
||||
def execute_shutdown(dry_run: bool = False) -> None:
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Ejecutaría: shutdown /s /t 0 /f")
|
||||
return
|
||||
subprocess.run(["shutdown", "/s", "/t", "0", "/f"], check=False)
|
||||
|
||||
|
||||
def run_once(client: TeleSecCouchDB, machine_id: str, dry_run: bool = False) -> None:
|
||||
server_now = client.get_server_datetime()
|
||||
server_now_iso = server_now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
raw = client.get(table="aulas_ordenadores", item_id=machine_id, decrypt=False)
|
||||
current: Dict[str, Any] = {}
|
||||
if raw is not None:
|
||||
current = ts_decrypt(raw, client.secret)
|
||||
if not isinstance(current, dict):
|
||||
current = {}
|
||||
|
||||
update = build_payload(machine_id)
|
||||
update["LastSeenAt"] = server_now_iso
|
||||
|
||||
for key in ["ShutdownBeforeDate", "ShutdownRequestedAt", "ShutdownRequestedBy"]:
|
||||
if key in current:
|
||||
update[key] = current.get(key)
|
||||
|
||||
client.put(table="aulas_ordenadores", item_id=machine_id, data=update, encrypt=True)
|
||||
|
||||
if should_shutdown(update, server_now):
|
||||
print(f"[{server_now_iso}] ShutdownBeforeDate alcanzado. Apagando {machine_id}...")
|
||||
execute_shutdown(dry_run=dry_run)
|
||||
else:
|
||||
print(
|
||||
f"[{server_now_iso}] Reportado {machine_id} user={update.get('UsuarioActual','')} "
|
||||
f"exe={update.get('AppActualEjecutable','')} title={update.get('AppActualTitulo','')}"
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
|
||||
parser.add_argument("--server", default="", help="CouchDB server URL, ej. https://couch.example")
|
||||
parser.add_argument("--db", default="", help="Database name")
|
||||
parser.add_argument("--user", default="", help="CouchDB username")
|
||||
parser.add_argument("--password", default="", help="CouchDB password")
|
||||
parser.add_argument("--secret", default="", help="TeleSec secret para cifrado")
|
||||
parser.add_argument("--machine-id", default="", help="ID de máquina (default: hostname)")
|
||||
parser.add_argument("--interval", type=int, default=15, help="Intervalo en segundos")
|
||||
parser.add_argument("--once", action="store_true", help="Ejecutar una sola iteración")
|
||||
parser.add_argument("--dry-run", action="store_true", help="No apagar realmente, solo log")
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default="",
|
||||
help="Ruta de config JSON (default: ~/.telesec/windows_agent.json)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _default_config_path() -> str:
|
||||
return os.path.join(os.path.expanduser("~"), ".telesec", "windows_agent.json")
|
||||
|
||||
|
||||
def _load_or_init_config(path: str) -> Dict[str, Any]:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
default_cfg = {
|
||||
"server": "https://tu-couchdb",
|
||||
"db": "telesec",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"secret": "",
|
||||
"machine_id": "",
|
||||
"interval": 15,
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(default_cfg, f, ensure_ascii=False, indent=2)
|
||||
return default_cfg
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
return {}
|
||||
|
||||
|
||||
def _save_config(path: str, data: Dict[str, Any]) -> None:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _pick(cli_value: Any, cfg_value: Any, default_value: Any = None) -> Any:
|
||||
if cli_value is None:
|
||||
return cfg_value if cfg_value not in [None, ""] else default_value
|
||||
if isinstance(cli_value, str):
|
||||
if cli_value.strip() == "":
|
||||
return cfg_value if cfg_value not in [None, ""] else default_value
|
||||
return cli_value
|
||||
return cli_value
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
config_path = args.config or _default_config_path()
|
||||
try:
|
||||
cfg = _load_or_init_config(config_path)
|
||||
except Exception as exc:
|
||||
print(f"No se pudo cargar/crear config en {config_path}: {exc}", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
server = _pick(args.server, cfg.get("server"), "")
|
||||
db = _pick(args.db, cfg.get("db"), "telesec")
|
||||
user = _pick(args.user, cfg.get("user"), "")
|
||||
password = _pick(args.password, cfg.get("password"), "")
|
||||
secret = _pick(args.secret, cfg.get("secret"), "")
|
||||
machine_id = _pick(args.machine_id, cfg.get("machine_id"), "")
|
||||
interval = _pick(args.interval, cfg.get("interval"), 15)
|
||||
|
||||
machine_id = (machine_id or socket.gethostname() or "unknown-host").strip()
|
||||
|
||||
if not server or not secret:
|
||||
print(
|
||||
"Falta configuración obligatoria. Edita el JSON en: " + config_path,
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 4
|
||||
|
||||
# Persist effective parameters for next runs
|
||||
try:
|
||||
persistent_cfg = {
|
||||
"server": server,
|
||||
"db": db,
|
||||
"user": user,
|
||||
"password": password,
|
||||
"secret": secret,
|
||||
"machine_id": machine_id,
|
||||
"interval": int(interval),
|
||||
}
|
||||
_save_config(config_path, persistent_cfg)
|
||||
except Exception as exc:
|
||||
print(f"No se pudo guardar config en {config_path}: {exc}", file=sys.stderr)
|
||||
|
||||
client = TeleSecCouchDB(
|
||||
server_url=server,
|
||||
dbname=db,
|
||||
secret=secret,
|
||||
username=user or None,
|
||||
password=password or None,
|
||||
)
|
||||
|
||||
try:
|
||||
client.check_connection()
|
||||
except TeleSecCouchDBError as exc:
|
||||
print(f"Error de conexión CouchDB: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.once:
|
||||
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
|
||||
return 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
|
||||
except Exception as exc:
|
||||
print(f"Error en iteración agente: {exc}", file=sys.stderr)
|
||||
time.sleep(max(5, int(interval)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
requests
|
||||
pycryptodome
|
||||
psutil
|
||||
204
src/app_logic.js
@@ -1,95 +1,82 @@
|
||||
function fixfloat(number) {
|
||||
return parseFloat(number).toPrecision(8);
|
||||
}
|
||||
|
||||
function tableScroll(query) {
|
||||
$(query).doubleScroll();
|
||||
}
|
||||
//var secretTokenEl = document.getElementById("secretToken");
|
||||
var groupIdEl = document.getElementById("groupId");
|
||||
var container = document.getElementById("container");
|
||||
function LinkAccount(LinkAccount_group, LinkAccount_secret, refresh = false) {
|
||||
GROUPID = LinkAccount_group.toUpperCase();
|
||||
SECRET = LinkAccount_secret.toUpperCase();
|
||||
var container = document.getElementById('container');
|
||||
|
||||
localStorage.setItem("TELESEC_AUTO", "YES");
|
||||
localStorage.setItem("TELESEC_groupid", GROUPID);
|
||||
localStorage.setItem("TELESEC_secret", SECRET);
|
||||
|
||||
TABLE = GROUPID + ":telesec.tech.eus";
|
||||
//secretTokenEl.innerText = SECRET;
|
||||
groupIdEl.innerText = GROUPID;
|
||||
document.getElementById("LinkAccount_details").open = false;
|
||||
if (refresh == true) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
if (localStorage.getItem("TELESEC_AUTO") == "YES") {
|
||||
LinkAccount(
|
||||
localStorage.getItem("TELESEC_groupid"),
|
||||
localStorage.getItem("TELESEC_secret")
|
||||
);
|
||||
}
|
||||
if (urlParams.get("login") != null) {
|
||||
LinkAccount(
|
||||
urlParams.get("login").split(":")[0],
|
||||
urlParams.get("login").split(":")[1]
|
||||
);
|
||||
//location.search = "";
|
||||
}
|
||||
function open_page(params) {
|
||||
if (params == "") {
|
||||
params = "index";
|
||||
// Clear stored event listeners and timers
|
||||
EventListeners.GunJS = [];
|
||||
EventListeners.Timeout.forEach((ev) => clearTimeout(ev));
|
||||
EventListeners.Timeout = [];
|
||||
EventListeners.Interval.forEach((ev) => clearInterval(ev));
|
||||
EventListeners.Interval = [];
|
||||
EventListeners.QRScanner.forEach((ev) => ev.clear());
|
||||
EventListeners.QRScanner = [];
|
||||
EventListeners.Custom.forEach((ev) => ev());
|
||||
EventListeners.Custom = [];
|
||||
EventListeners.DB.forEach((ev) => DB.unlisten(ev));
|
||||
EventListeners.DB = [];
|
||||
|
||||
if (SUB_LOGGED_IN != true && params != 'login,setup' && !params.startsWith('login,onboarding')) {
|
||||
PAGES['login'].index();
|
||||
return;
|
||||
}
|
||||
var path = params.split(",");
|
||||
if (params == '') {
|
||||
params = 'index';
|
||||
}
|
||||
var path = params.split(',');
|
||||
var app = path[0];
|
||||
if (!PAGES[app]) {
|
||||
toastr.error('La app solicitada no existe.');
|
||||
setUrlHash('index');
|
||||
return;
|
||||
}
|
||||
if (path[1] == undefined) {
|
||||
PAGES[app].index();
|
||||
return;
|
||||
}
|
||||
PAGES[app].edit(path[1]);
|
||||
PAGES[app].edit(path.slice(1).join(','));
|
||||
}
|
||||
|
||||
function setUrlHash(hash) {
|
||||
location.hash = "#" + hash;
|
||||
location.hash = '#' + hash;
|
||||
|
||||
// Handle quick search transfer
|
||||
if (hash === 'buscar') {
|
||||
const quickSearchInput = document.getElementById('quickSearchInput');
|
||||
if (quickSearchInput && quickSearchInput.value.trim()) {
|
||||
// Store the search term temporarily
|
||||
sessionStorage.setItem('telesec_quick_search', quickSearchInput.value.trim());
|
||||
quickSearchInput.value = ''; // Clear the input
|
||||
}
|
||||
}
|
||||
}
|
||||
window.onhashchange = () => {
|
||||
try {
|
||||
if (EVENTLISTENER != null) {
|
||||
try {
|
||||
EVENTLISTENER.off();
|
||||
EVENTLISTENER = null;
|
||||
EVENTLISTENER2.off();
|
||||
EVENTLISTENER2 = null;
|
||||
// TypeError: Cannot read properties of null (reading 'off')
|
||||
} catch (error) {
|
||||
if (!error.name == "TypeError") {
|
||||
console.debug("EVENTLISTENER error", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("EVENTLISTENER onhashchange", e);
|
||||
}
|
||||
|
||||
open_page(location.hash.replace("#", ""));
|
||||
open_page(location.hash.replace('#', '').split("?")[0]);
|
||||
};
|
||||
function download(filename, text) {
|
||||
var element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:application/octet-stream;charset=utf-8," + encodeURIComponent(text)
|
||||
);
|
||||
element.setAttribute("download", filename);
|
||||
|
||||
element.style.display = "none";
|
||||
function download(filename, text) {
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute(
|
||||
'href',
|
||||
'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text)
|
||||
);
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
function resizeInputImage(
|
||||
file,
|
||||
callback,
|
||||
targetHeight = 256,
|
||||
targetQuality = 0.75
|
||||
) {
|
||||
|
||||
function resizeInputImage(file, callback, targetHeight = 256, targetQuality = 0.75) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (event) {
|
||||
@@ -98,19 +85,19 @@ function resizeInputImage(
|
||||
const aspectRatio = img.width / img.height;
|
||||
const targetWidth = targetHeight * aspectRatio;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
||||
|
||||
// Get resized image as Blob
|
||||
const dataURL = canvas.toDataURL("image/jpeg", targetQuality);
|
||||
const dataURL = canvas.toDataURL('image/jpeg', targetQuality);
|
||||
callback(dataURL);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
@@ -118,43 +105,56 @@ function resizeInputImage(
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function CurrentISODate() {
|
||||
return new Date().toISOString().split("T")[0].replace("T", " ");
|
||||
return new Date().toISOString().split('T')[0].replace('T', ' ');
|
||||
}
|
||||
|
||||
function CurrentISOTime() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function fixGunLocalStorage() {
|
||||
localStorage.removeItem("radata");
|
||||
localStorage.removeItem('radata');
|
||||
removeCache();
|
||||
location.reload();
|
||||
}
|
||||
function betterGunPut(ref, data) {
|
||||
ref.put(data, (ack) => {
|
||||
if (ack.err) {
|
||||
toastr.error(
|
||||
ack.err + "<br>Pulsa aqui para reiniciar la app",
|
||||
"Error al guardar",
|
||||
{ onclick: () => fixGunLocalStorage() }
|
||||
);
|
||||
} else {
|
||||
console.debug("Guardado correctamente");
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
ref.put(data);
|
||||
}, 250);
|
||||
setTimeout(() => {
|
||||
ref.put(data);
|
||||
}, 500);
|
||||
|
||||
// Heartbeat: store a small "last seen" doc locally and replicate to remote when available
|
||||
// setInterval(() => {
|
||||
// if (typeof DB !== 'undefined') {
|
||||
// DB.put('heartbeat', getDBName() || 'heartbeat', 'heartbeat-' + CurrentISOTime());
|
||||
// }
|
||||
// }, 5000);
|
||||
|
||||
function betterSorter(a, b) {
|
||||
// 1. Fecha (ascending)
|
||||
if (a.Fecha && b.Fecha && a.Fecha !== b.Fecha) {
|
||||
return a.Fecha > b.Fecha ? -1 : 1;
|
||||
}
|
||||
// 2. Region (ascending, from SC_Personas if Persona exists)
|
||||
const regionA =
|
||||
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Region || '' : a.Region || '';
|
||||
const regionB =
|
||||
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Region || '' : b.Region || '';
|
||||
if (regionA !== regionB) {
|
||||
return regionA.toLowerCase() < regionB.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
// 3. Persona (Nombre, ascending, from SC_Personas if Persona exists)
|
||||
const nombrePersonaA =
|
||||
a.Persona && SC_Personas[a.Persona] ? SC_Personas[a.Persona].Nombre || '' : '';
|
||||
const nombrePersonaB =
|
||||
b.Persona && SC_Personas[b.Persona] ? SC_Personas[b.Persona].Nombre || '' : '';
|
||||
if (nombrePersonaA !== nombrePersonaB) {
|
||||
return nombrePersonaA.toLowerCase() < nombrePersonaB.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
// 4. Nombre (ascending, from a.Nombre/b.Nombre)
|
||||
if (a.Nombre && b.Nombre && a.Nombre !== b.Nombre) {
|
||||
return a.Nombre.toLowerCase() < b.Nombre.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
// 5. Asunto (ascending, from a.Asunto/b.Asunto)
|
||||
if (a.Asunto && b.Asunto && a.Asunto !== b.Asunto) {
|
||||
return a.Asunto.toLowerCase() < b.Asunto.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
setInterval(() => {
|
||||
betterGunPut(
|
||||
gun.get(TABLE).get("heartbeat"),
|
||||
"heartbeat-" + CurrentISOTime()
|
||||
);
|
||||
gun.get(TABLE).get("heartbeat").load(console.debug);
|
||||
}, 5000);
|
||||
gun.get(TABLE).on((data) => {
|
||||
var e = true;
|
||||
});
|
||||
|
||||
2459
src/app_modules.js
166
src/config.js
@@ -1,22 +1,146 @@
|
||||
var EVENTLISTENER = null;
|
||||
var EVENTLISTENER2 = null;
|
||||
var urlParams = new URLSearchParams(location.search);
|
||||
if (urlParams.get("hidenav") == "yes") {
|
||||
document.getElementById("header_hide_query").style.display = "none";
|
||||
// Syntax helper for HTML template literals (e.g. html`<div>${content}</div>`)
|
||||
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
|
||||
|
||||
// Global Event Listeners registry for cleanup on logout or other events. Each category can be used to track different types of listeners (e.g., GunJS events, timeouts, intervals, QRScanner events, custom events).
|
||||
var EventListeners = {
|
||||
GunJS: [],
|
||||
Timeout: [],
|
||||
Interval: [],
|
||||
QRScanner: [],
|
||||
Custom: [],
|
||||
DB: [],
|
||||
};
|
||||
|
||||
// Safe UUID for html element IDs: generates a unique identifier with a specified prefix, ensuring it is safe for use in HTML element IDs. It uses crypto.randomUUID if available, with a fallback to a random string generation method for environments that do not support it. The generated ID is prefixed to avoid collisions and ensure uniqueness across the application.
|
||||
function safeuuid(prefix = 'AXLUID_') {
|
||||
if (!crypto.randomUUID) {
|
||||
// Fallback for environments without crypto.randomUUID()
|
||||
const randomPart = Math.random().toString(36).substring(2, 10);
|
||||
return prefix + randomPart;
|
||||
}
|
||||
return prefix + crypto.randomUUID().split('-')[4];
|
||||
}
|
||||
var GROUPID = "";
|
||||
// const PUBLIC_KEY = "~cppGiuA4UFUPGTDoC-4r2izVC3F7MfpaCmF3iZdESN4.vntmjgbAVUpF_zfinYY6EKVFuuTYxh5xOrL4KmtdTmc"
|
||||
var TABLE = GROUPID + ":telesec.tech.eus";
|
||||
const RELAYS = [
|
||||
"https://gun-es01.tech.eus/gun",
|
||||
"https://gun-es02.tech.eus/gun",
|
||||
"https://gun-es03.tech.eus/gun",
|
||||
"https://gun-es04.tech.eus/gun",
|
||||
"https://gun-es05.tech.eus/gun",
|
||||
"https://gun-es06.tech.eus/gun",
|
||||
// "https://gun-es07.tech.eus/gun", // No he podido instalar el nodo.
|
||||
"https://gun-manhattan.herokuapp.com/gun",
|
||||
"https://peer.wallie.io/gun",
|
||||
"https://gun.defucc.me/gun",
|
||||
];
|
||||
var SECRET = "";
|
||||
|
||||
function parseURL(input) {
|
||||
try {
|
||||
return new URL(input);
|
||||
} catch (e) {
|
||||
try {
|
||||
return new URL('https://' + input);
|
||||
} catch (e2) {
|
||||
return { hostname: '', username: '', password: '', pathname: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
var urlParams = new URLSearchParams(location.search);
|
||||
var AC_BYPASS = false;
|
||||
if (urlParams.get('ac_bypass') == 'yes') {
|
||||
AC_BYPASS = true;
|
||||
}
|
||||
if (urlParams.get('hidenav') != undefined) {
|
||||
document.getElementById('header_hide_query').style.display = 'none';
|
||||
}
|
||||
// CouchDB URI generator from components: host, user, pass, dbname. Host can include protocol or not, but will be normalized to just hostname in the display. If host is empty, returns empty string.
|
||||
function makeCouchURLDisplay(host, user, pass, dbname) {
|
||||
if (!host) return '';
|
||||
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
|
||||
return display;
|
||||
}
|
||||
// Auto-configure CouchDB from ?couch=<uri> parameter
|
||||
if (urlParams.get('couch') != null) {
|
||||
try {
|
||||
var couchURI = urlParams.get('couch');
|
||||
// Normalize URL: add https:// if no protocol specified
|
||||
var normalizedUrl = couchURI;
|
||||
if (!/^https?:\/\//i.test(couchURI)) {
|
||||
normalizedUrl = 'https://' + couchURI;
|
||||
}
|
||||
var URL_PARSED = parseURL(normalizedUrl);
|
||||
var user = URL_PARSED.username || '';
|
||||
var pass = URL_PARSED.password || '';
|
||||
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
|
||||
var host = URL_PARSED.hostname || normalizedUrl;
|
||||
|
||||
// Extract secret from ?secret= parameter if provided
|
||||
var secret = urlParams.get('secret') || '';
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
|
||||
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||
localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||
localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||
if (secret) {
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
}
|
||||
|
||||
// Mark onboarding as complete since we have server config
|
||||
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
|
||||
|
||||
// Clean URL by removing the couch parameter
|
||||
urlParams.delete('couch');
|
||||
urlParams.delete('secret');
|
||||
history.replaceState(
|
||||
null,
|
||||
'',
|
||||
location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '') + location.hash.split("?")[0]
|
||||
);
|
||||
|
||||
console.log('CouchDB auto-configured from URL parameter');
|
||||
} catch (e) {
|
||||
console.error('Error auto-configuring CouchDB from URL:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// getDBName: prefer explicit CouchDB dbname from settings. Single-group model: default to 'telesec'
|
||||
function getDBName() {
|
||||
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || '';
|
||||
if (dbname && dbname.trim() !== '') return dbname.trim();
|
||||
return 'telesec';
|
||||
}
|
||||
var SECRET = '';
|
||||
var SUB_LOGGED_IN = false;
|
||||
var SUB_LOGGED_IN_DETAILS = false;
|
||||
var SUB_LOGGED_IN_ID = false;
|
||||
var SAVE_WAIT = 500;
|
||||
var SC_Personas = {};
|
||||
var PeerConnectionInterval = 5000;
|
||||
if (urlParams.get('sublogin') != null) {
|
||||
SUB_LOGGED_IN = true;
|
||||
SUB_LOGGED_IN_ID = urlParams.get('sublogin');
|
||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||
var sli = 15;
|
||||
var slii = setInterval(() => {
|
||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||
sli -= 1;
|
||||
if (sli < 0) {
|
||||
clearInterval(slii);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
// Logout function for sublogin: clears sublogin state and reloads the page without the sublogin parameter
|
||||
function LogOutTeleSec() {
|
||||
SUB_LOGGED_IN = false;
|
||||
SUB_LOGGED_IN_DETAILS = false;
|
||||
SUB_LOGGED_IN_ID = false;
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
//Remove sublogin from URL and reload
|
||||
urlParams.delete('sublogin');
|
||||
history.replaceState(null, '', '?' + urlParams.toString());
|
||||
location.reload();
|
||||
}
|
||||
var TTS_RATE = parseFloat(urlParams.get('tts_rate')) || 0.75;
|
||||
function TS_SayTTS(msg) {
|
||||
try {
|
||||
if (window.speechSynthesis) {
|
||||
let utterance = new SpeechSynthesisUtterance(msg);
|
||||
utterance.rate = TTS_RATE;
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
} catch { console.warn('TTS error'); }
|
||||
}
|
||||
|
||||
function createElementFromHTML(htmlString) {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = htmlString.trim();
|
||||
return div.firstChild;
|
||||
}
|
||||
417
src/db.js
Normal file
@@ -0,0 +1,417 @@
|
||||
// Simple PouchDB wrapper for TeleSec
|
||||
// - Uses PouchDB for local storage and optional replication to a CouchDB server
|
||||
// - Stores records as docs with _id = "<table>:<id>" and field `data` containing either plain object or encrypted string
|
||||
// - Exposes: init, put, get, del, map, list, replicate
|
||||
|
||||
var DB = (function () {
|
||||
let local = null;
|
||||
let remote = null;
|
||||
let changes = null;
|
||||
let repPush = null;
|
||||
let repPull = null;
|
||||
let callbacks = {}; // table -> [{ id, cb }]
|
||||
let callbackSeq = 0;
|
||||
let docCache = {}; // _id -> last data snapshot (stringified)
|
||||
|
||||
function ensureLocal() {
|
||||
if (local) return;
|
||||
try {
|
||||
const localName = 'telesec';
|
||||
local = new PouchDB(localName);
|
||||
if (changes) {
|
||||
try {
|
||||
changes.cancel();
|
||||
} catch (e) {}
|
||||
}
|
||||
changes = local
|
||||
.changes({ live: true, since: 'now', include_docs: true })
|
||||
.on('change', onChange);
|
||||
} catch (e) {
|
||||
console.warn('ensureLocal error', e);
|
||||
}
|
||||
}
|
||||
|
||||
function makeId(table, id) {
|
||||
return table + ':' + id;
|
||||
}
|
||||
|
||||
function makeCallbackId(table) {
|
||||
callbackSeq += 1;
|
||||
return table + '#' + callbackSeq;
|
||||
}
|
||||
|
||||
function init(opts) {
|
||||
const localName = 'telesec';
|
||||
try {
|
||||
if (opts && opts.secret) {
|
||||
SECRET = opts.secret;
|
||||
try {
|
||||
localStorage.setItem('TELESEC_SECRET', SECRET);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
local = new PouchDB(localName);
|
||||
|
||||
if (opts.remoteServer) {
|
||||
try {
|
||||
const server = opts.remoteServer.replace(/\/$/, '');
|
||||
const dbname = encodeURIComponent(opts.dbname || localName);
|
||||
let authPart = '';
|
||||
if (opts.username) authPart = opts.username + ':' + (opts.password || '') + '@';
|
||||
const remoteUrl = server.replace(/https?:\/\//, (m) => m) + '/' + dbname;
|
||||
if (opts.username) remote = new PouchDB(remoteUrl.replace(/:\/\//, '://' + authPart));
|
||||
else remote = new PouchDB(remoteUrl);
|
||||
replicateToRemote();
|
||||
} catch (e) {
|
||||
console.warn('Remote DB init error', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (changes) changes.cancel();
|
||||
changes = local
|
||||
.changes({ live: true, since: 'now', include_docs: true })
|
||||
.on('change', onChange);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function replicateToRemote() {
|
||||
ensureLocal();
|
||||
if (!local || !remote) return;
|
||||
try {
|
||||
if (repPush && repPush.cancel) repPush.cancel();
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (repPull && repPull.cancel) repPull.cancel();
|
||||
} catch (e) {}
|
||||
|
||||
repPush = PouchDB.replicate(local, remote, { live: true, retry: true }).on(
|
||||
'error',
|
||||
function (err) {
|
||||
console.warn('Replication push error', err);
|
||||
}
|
||||
);
|
||||
repPull = PouchDB.replicate(remote, local, { live: true, retry: true }).on(
|
||||
'error',
|
||||
function (err) {
|
||||
console.warn('Replication pull error', err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.addEventListener) {
|
||||
window.addEventListener('online', function () {
|
||||
try {
|
||||
setTimeout(replicateToRemote, 1000);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
function onChange(change) {
|
||||
const doc = change.doc;
|
||||
if (!doc || !doc._id) return;
|
||||
try {
|
||||
window.TELESEC_LAST_SYNC = Date.now();
|
||||
// derive a stable color from the last record's data hash
|
||||
let payload = '';
|
||||
try {
|
||||
payload = typeof doc.data === 'string' ? doc.data : JSON.stringify(doc.data || {});
|
||||
} catch (e) {
|
||||
payload = String(doc._id || '');
|
||||
}
|
||||
let hash = 0;
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
hash = (hash * 31 + payload.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
const hue = hash % 360;
|
||||
window.TELESEC_LAST_SYNC_COLOR = `hsl(${hue}, 70%, 50%)`;
|
||||
updateStatusOrb();
|
||||
} catch (e) {}
|
||||
const [table, id] = doc._id.split(':');
|
||||
|
||||
// handle deletes
|
||||
if (change.deleted || doc._deleted) {
|
||||
delete docCache[doc._id];
|
||||
if (callbacks[table]) {
|
||||
callbacks[table].forEach((listener) => {
|
||||
const cb = listener.cb;
|
||||
try {
|
||||
cb(null, id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// handle insert/update
|
||||
try {
|
||||
const now = typeof doc.data === 'string' ? doc.data : JSON.stringify(doc.data);
|
||||
const prev = docCache[doc._id];
|
||||
if (prev === now) return; // no meaningful change
|
||||
docCache[doc._id] = now;
|
||||
} catch (e) {
|
||||
/* ignore cache errors */
|
||||
}
|
||||
|
||||
if (callbacks[table]) {
|
||||
callbacks[table].forEach((listener) => {
|
||||
const cb = listener.cb;
|
||||
try {
|
||||
cb(doc.data, id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function put(table, id, data) {
|
||||
ensureLocal();
|
||||
const _id = makeId(table, id);
|
||||
try {
|
||||
const existing = await local.get(_id).catch(() => null);
|
||||
if (data === null) {
|
||||
if (existing) await local.remove(existing);
|
||||
return;
|
||||
}
|
||||
const doc = existing || { _id: _id };
|
||||
var toStore = data;
|
||||
try {
|
||||
var isEncryptedString =
|
||||
typeof data === 'string' && data.startsWith('RSA{') && data.endsWith('}');
|
||||
if (
|
||||
!isEncryptedString &&
|
||||
typeof TS_encrypt === 'function' &&
|
||||
typeof SECRET !== 'undefined' &&
|
||||
SECRET
|
||||
) {
|
||||
toStore = await new Promise((resolve) => {
|
||||
try {
|
||||
TS_encrypt(data, SECRET, (enc) => resolve(enc));
|
||||
} catch (e) {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toStore = data;
|
||||
}
|
||||
doc.data = toStore;
|
||||
doc.table = table;
|
||||
doc.ts = new Date().toISOString();
|
||||
if (existing) doc._rev = existing._rev;
|
||||
await local.put(doc);
|
||||
|
||||
// FIX: manually trigger map() callbacks for local update
|
||||
// onChange will update docCache and notify all subscribers
|
||||
onChange({ doc: doc });
|
||||
} catch (e) {
|
||||
console.error('DB.put error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function get(table, id) {
|
||||
ensureLocal();
|
||||
const _id = makeId(table, id);
|
||||
try {
|
||||
const doc = await local.get(_id);
|
||||
return doc.data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function del(table, id) {
|
||||
return put(table, id, null);
|
||||
}
|
||||
|
||||
async function list(table) {
|
||||
ensureLocal();
|
||||
try {
|
||||
const res = await local.allDocs({
|
||||
include_docs: true,
|
||||
startkey: table + ':',
|
||||
endkey: table + ':\uffff',
|
||||
});
|
||||
return res.rows.map((r) => {
|
||||
const id = r.id.split(':')[1];
|
||||
try {
|
||||
docCache[r.id] = typeof r.doc.data === 'string' ? r.doc.data : JSON.stringify(r.doc.data);
|
||||
} catch (e) {}
|
||||
return { id: id, data: r.doc.data };
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function dataURLtoBlob(dataurl) {
|
||||
const arr = dataurl.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)[1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
while (n--) u8arr[n] = bstr.charCodeAt(n);
|
||||
return new Blob([u8arr], { type: mime });
|
||||
}
|
||||
|
||||
async function putAttachment(table, id, name, dataUrlOrBlob, contentType) {
|
||||
ensureLocal();
|
||||
const _id = makeId(table, id);
|
||||
try {
|
||||
let doc = await local.get(_id).catch(() => null);
|
||||
if (!doc) {
|
||||
await local.put({ _id: _id, table: table, ts: new Date().toISOString(), data: {} });
|
||||
doc = await local.get(_id);
|
||||
}
|
||||
let blob = dataUrlOrBlob;
|
||||
if (typeof dataUrlOrBlob === 'string' && dataUrlOrBlob.indexOf('data:') === 0)
|
||||
blob = dataURLtoBlob(dataUrlOrBlob);
|
||||
const type = contentType || (blob && blob.type) || 'application/octet-stream';
|
||||
await local.putAttachment(_id, name, doc._rev, blob, type);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('putAttachment error', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAttachment(table, id, name) {
|
||||
ensureLocal();
|
||||
const _id = makeId(table, id);
|
||||
try {
|
||||
const blob = await local.getAttachment(_id, name);
|
||||
if (!blob) return null;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(e.target.result);
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function listAttachments(table, id) {
|
||||
ensureLocal();
|
||||
const _id = makeId(table, id);
|
||||
try {
|
||||
const doc = await local.get(_id, { attachments: true });
|
||||
if (!doc || !doc._attachments) return [];
|
||||
const out = [];
|
||||
for (const name of Object.keys(doc._attachments)) {
|
||||
try {
|
||||
const att = doc._attachments[name];
|
||||
if (att && att.data) {
|
||||
const content_type = att.content_type || 'application/octet-stream';
|
||||
const durl = 'data:' + content_type + ';base64,' + att.data;
|
||||
out.push({ name: name, dataUrl: durl, content_type: content_type });
|
||||
continue;
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const durl = await getAttachment(table, id, name);
|
||||
out.push({ name: name, dataUrl: durl, content_type: null });
|
||||
} catch (e) {
|
||||
out.push({ name: name, dataUrl: null, content_type: null });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
try {
|
||||
const doc = await local.get(_id).catch(() => null);
|
||||
if (!doc || !doc._attachments) return [];
|
||||
const out = [];
|
||||
for (const name of Object.keys(doc._attachments)) {
|
||||
try {
|
||||
const durl = await getAttachment(table, id, name);
|
||||
out.push({ name: name, dataUrl: durl, content_type: null });
|
||||
} catch (e) {
|
||||
out.push({ name: name, dataUrl: null, content_type: null });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
} catch (e2) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAttachment(table, id, name) {
|
||||
ensureLocal();
|
||||
const _id = makeId(table, id);
|
||||
try {
|
||||
const doc = await local.get(_id);
|
||||
if (!doc || !doc._attachments || !doc._attachments[name]) return false;
|
||||
delete doc._attachments[name];
|
||||
await local.put(doc);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('deleteAttachment error', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function map(table, cb) {
|
||||
ensureLocal();
|
||||
const callbackId = makeCallbackId(table);
|
||||
callbacks[table] = callbacks[table] || [];
|
||||
callbacks[table].push({ id: callbackId, cb: cb });
|
||||
list(table).then((rows) => {
|
||||
const stillListening = (callbacks[table] || []).some((listener) => listener.id === callbackId);
|
||||
if (!stillListening) return;
|
||||
rows.forEach((r) => cb(r.data, r.id));
|
||||
});
|
||||
return callbackId;
|
||||
}
|
||||
|
||||
function unlisten(callbackId) {
|
||||
if (!callbackId) return false;
|
||||
for (const table of Object.keys(callbacks)) {
|
||||
const before = callbacks[table].length;
|
||||
callbacks[table] = callbacks[table].filter((listener) => listener.id !== callbackId);
|
||||
if (callbacks[table].length !== before) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
put,
|
||||
get,
|
||||
del,
|
||||
list,
|
||||
map,
|
||||
unlisten,
|
||||
replicateToRemote,
|
||||
listAttachments,
|
||||
deleteAttachment,
|
||||
putAttachment,
|
||||
getAttachment,
|
||||
_internal: { local },
|
||||
};
|
||||
})();
|
||||
|
||||
window.DB = DB;
|
||||
|
||||
// Auto-initialize DB on startup using saved settings (non-blocking)
|
||||
(function autoInitDB() {
|
||||
try {
|
||||
const remoteServer = localStorage.getItem('TELESEC_COUCH_URL') || '';
|
||||
const username = localStorage.getItem('TELESEC_COUCH_USER') || '';
|
||||
const password = localStorage.getItem('TELESEC_COUCH_PASS') || '';
|
||||
const dbname = localStorage.getItem('TELESEC_COUCH_DBNAME') || undefined;
|
||||
try {
|
||||
SECRET = localStorage.getItem('TELESEC_SECRET') || '';
|
||||
} catch (e) {
|
||||
SECRET = '';
|
||||
}
|
||||
DB.init({ remoteServer, username, password, dbname }).catch((e) =>
|
||||
console.warn('DB.autoInit error', e)
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('DB.autoInit unexpected error', e);
|
||||
}
|
||||
})();
|
||||
@@ -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.');
|
||||
|
||||
251
src/index.html
@@ -1,154 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>TeleSec</title>
|
||||
<link rel="icon" type="image/png" href="static/TeleSec.jpg" />
|
||||
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
|
||||
<link href="static/toastr.min.css" rel="stylesheet" />
|
||||
|
||||
%%PREFETCH%%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<details class="supermesh-indicator">
|
||||
<summary>
|
||||
<b>SuperMesh</b><br />
|
||||
<br /><small id="peerPID" style="font-family: monospace"
|
||||
>PID ??????????</small
|
||||
>
|
||||
</summary>
|
||||
<ul id="peerList"></ul>
|
||||
<i>Todos los datos están encriptados.</i>
|
||||
</details>
|
||||
<main>
|
||||
<header class="no_print" id="header_hide_query">
|
||||
<details id="LinkAccount_details" open>
|
||||
<summary>
|
||||
<b
|
||||
>TeleSec - <span id="groupId">???</span> - (<span id="peerCount"
|
||||
>?</span
|
||||
>
|
||||
nodos)</b
|
||||
>
|
||||
</summary>
|
||||
<fieldset id="auth_fieldSet">
|
||||
<legend>Credenciales</legend>
|
||||
<br />
|
||||
<label
|
||||
>Codigo de grupo:<br />
|
||||
<input type="text" id="LinkAccount_group"
|
||||
/></label>
|
||||
<br />
|
||||
<br />
|
||||
<label
|
||||
>Clave secreta:<br />
|
||||
<input type="text" id="LinkAccount_secret"
|
||||
/></label>
|
||||
<br /><br />
|
||||
<button
|
||||
type="button"
|
||||
onclick='LinkAccount(document.getElementById("LinkAccount_group").value, document.getElementById("LinkAccount_secret").value, true)'
|
||||
>
|
||||
Iniciar sesión
|
||||
</button>
|
||||
</fieldset>
|
||||
</details>
|
||||
<!-- <button onclick="displayPost('index')">Ir a la pagina de inicio</button> -->
|
||||
<div id="appendApps">
|
||||
<!--<a class="button nav-supercafe nav-disabled" disabled>SuperCafé</a>
|
||||
<a class="button nav-comedor nav-disabled" disabled>Menú Comedor</a>
|
||||
<a class="button nav-recetas nav-disabled" disabled>Recetas</a>-->
|
||||
</div>
|
||||
<hr />
|
||||
</header>
|
||||
<div id="container"></div>
|
||||
<!-- <br><br><br>
|
||||
<footer>
|
||||
<hr>
|
||||
<details>
|
||||
<summary><b>Apps SuperMesh</b></summary>
|
||||
<button type="button">
|
||||
<img src="static/TeleSec.jpg" alt="" width="100px">
|
||||
<br>TeleSec
|
||||
</button>
|
||||
</details>
|
||||
</footer> -->
|
||||
<img
|
||||
id="connectStatus"
|
||||
style="bottom: 15px; right: 15px; position: fixed; width: 50px"
|
||||
/>
|
||||
</main>
|
||||
<img
|
||||
id="actionStatus"
|
||||
src="static/ico/statusok.png"
|
||||
style="
|
||||
z-index: 2048;
|
||||
margin: 0px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: none;
|
||||
"
|
||||
/>
|
||||
<div id="snackbar">
|
||||
Hay una nueva versión de TeleSec.<br /><a id="reload"
|
||||
>Pulsa aqui para actualizar.</a
|
||||
>
|
||||
</div>
|
||||
<script src="static/showdown.min.js"></script>
|
||||
<script src="static/jquery.js"></script>
|
||||
<script src="static/gun.js"></script>
|
||||
<script src="static/webrtc.js"></script>
|
||||
<script src="static/sea.js"></script>
|
||||
<script src="static/yson.js"></script>
|
||||
<script src="static/radix.js"></script>
|
||||
<!-- <script src="static/radisk.js"></script> -->
|
||||
<!-- <script src="static/store.js"></script> -->
|
||||
<script src="static/rindexed.js"></script>
|
||||
<script src="static/path.js"></script>
|
||||
<script src="static/open.js"></script>
|
||||
<script src="static/load.js"></script>
|
||||
<!--<script src="static/synchronous.js"></script>-->
|
||||
<!--<script src="static/axe.js"></script>-->
|
||||
<script src="static/toastr.min.js"></script>
|
||||
<script src="static/doublescroll.js"></script>
|
||||
<!--<script src="static/simplemde.min.js"></script>-->
|
||||
<script async>
|
||||
async function getQuota(cb = () => {}) {
|
||||
if (navigator.storage && navigator.storage.estimate) {
|
||||
const quota = await navigator.storage.estimate();
|
||||
// quota.usage -> Number of bytes used.
|
||||
// quota.quota -> Maximum number of bytes available.
|
||||
const percentageUsed = (quota.usage / quota.quota) * 100;
|
||||
console.log(
|
||||
`You've used ${percentageUsed}% of the available storage.`
|
||||
);
|
||||
const remaining = quota.quota - quota.usage;
|
||||
cb(percentageUsed, remaining);
|
||||
console.log(`You can write up to ${remaining} more bytes.`);
|
||||
}
|
||||
}
|
||||
getQuota();
|
||||
</script>
|
||||
<script src="pwa.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="gun_init.js"></script>
|
||||
<script src="app_logic.js"></script>
|
||||
<script src="app_modules.js"></script>
|
||||
<script src="page__index.js"></script>
|
||||
<script src="page__importar.js"></script>
|
||||
<script src="page__exportar.js"></script>
|
||||
<script src="page__materiales.js"></script>
|
||||
<script src="page__resumen_diario.js"></script>
|
||||
<script src="page__personas.js"></script>
|
||||
<script src="page__supercafe.js"></script>
|
||||
<script src="page__notificaciones.js"></script>
|
||||
<script src="page__comedor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>%%TITLE%%</title>
|
||||
<link rel="icon" type="image/png" href="static/logo.jpg" />
|
||||
<link href="static/euskaditech-css/simple.css" rel="stylesheet" />
|
||||
<link href="static/toastr.min.css" rel="stylesheet" />
|
||||
%%PREFETCH%%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="ribbon no_print" id="header_hide_query">
|
||||
<img class="ribbon-orb" id="connectStatus" src="static/logo.jpg" style="cursor: pointer;" />
|
||||
|
||||
<div class="ribbon-content" id="ribbon-content" style="display: none;">
|
||||
<div class="ribbon-tabs">
|
||||
<div class="ribbon-tab active" data-tab="modulos">Modulos</div>
|
||||
<div class="ribbon-tab" data-tab="buscar">Buscar</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Modulos -->
|
||||
<details id="tab-modulos" open>
|
||||
<summary hidden>Modulos</summary>
|
||||
<div class="ribbon-panel" id="appendApps2">
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Tab: Buscar -->
|
||||
<details id="tab-buscar">
|
||||
<summary hidden>Buscar</summary>
|
||||
<div class="ribbon-panel">
|
||||
<input type="text" id="quickSearchInput" placeholder="Búsqueda rápida..."
|
||||
onkeypress="if(event.key==='Enter') document.getElementById('quickSearchBtn').click()">
|
||||
<button id="quickSearchBtn" onclick="setUrlHash('buscar')" class="btn5">
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<small style="margin-top:10px;">Base de datos: <b id="peerLink">(no configurado)</b></small>
|
||||
</div>
|
||||
<div class="ribbon-content" id="ribbon-content-alternative">
|
||||
<h2 style="margin: 0;">TeleSec, muy seguro. </h2>
|
||||
<i><small>(pulsa el icono para mostrar el menú)</small></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img id="loading" src="load.gif" style="display: block; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 50px); max-width: 400px;" />
|
||||
<main id="container"></main>
|
||||
<img id="actionStatus" src="static/ico/statusok.png" style="
|
||||
z-index: 2048;
|
||||
margin: 0px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: none;
|
||||
" />
|
||||
<div id="snackbar">
|
||||
Hay una nueva versión de %%TITLE%%.<br /><a id="reload">Pulsa aqui para actualizar.</a>
|
||||
</div>
|
||||
<script src="static/aes.js"></script>
|
||||
<script src="static/showdown.min.js"></script>
|
||||
<script src="static/qrcode/html5-qrcode.min.js"></script>
|
||||
<script src="static/qrcode/barcode.js"></script>
|
||||
<script src="static/qrcode/qrcode.min.js"></script>
|
||||
<script src="static/jquery.js"></script>
|
||||
<script src="static/pouchdb.min.js"></script>
|
||||
<script src="static/toastr.min.js"></script>
|
||||
<script src="static/doublescroll.js"></script>
|
||||
<script src="static/chart.umd.min.js"></script>
|
||||
<script src="pwa.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="db.js"></script>
|
||||
<script src="app_logic.js"></script>
|
||||
<script src="app_modules.js"></script>
|
||||
<script src="page/login.js"></script>
|
||||
<script src="page/index.js"></script>
|
||||
<script src="page/dataman.js"></script>
|
||||
<script src="page/aulas.js"></script>
|
||||
<script src="page/materiales.js"></script>
|
||||
<script src="page/personas.js"></script>
|
||||
<script src="page/supercafe.js"></script>
|
||||
<script src="page/comedor.js"></script>
|
||||
<script src="page/notas.js"></script>
|
||||
<script src="page/panel.js"></script>
|
||||
<script src="page/buscar.js"></script>
|
||||
<script src="page/pagos.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
28
src/manifest.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"theme_color": "#23365e",
|
||||
"background_color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"purpose": "maskable",
|
||||
"sizes": "512x512",
|
||||
"src": "icon512_maskable.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"purpose": "any",
|
||||
"sizes": "512x512",
|
||||
"src": "icon512_rounded.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"dir": "auto",
|
||||
"lang": "es-ES",
|
||||
"start_url": "index.html",
|
||||
"scope": "/",
|
||||
"description": "%%TITLE%% - Comunicación Hipersegura",
|
||||
"id": "telesec.tech.eus",
|
||||
"name": "%%TITLE%%",
|
||||
"short_name": "%%TITLE%%"
|
||||
}
|
||||
1212
src/page/aulas.js
Normal file
265
src/page/buscar.js
Normal file
@@ -0,0 +1,265 @@
|
||||
PAGES.buscar = {
|
||||
navcss: 'btn1',
|
||||
icon: 'static/appico/view.svg',
|
||||
Title: 'Buscar',
|
||||
AccessControl: true,
|
||||
Esconder: true,
|
||||
|
||||
index: function () {
|
||||
const searchInput = safeuuid();
|
||||
const resultsContainer = safeuuid();
|
||||
const searchButton = safeuuid();
|
||||
const recentSearches = safeuuid();
|
||||
const moduleFilter = safeuuid();
|
||||
|
||||
container.innerHTML = html`
|
||||
<h1>🔍 Búsqueda Global</h1>
|
||||
<p>Busca en todos los módulos: personas, materiales, café, comedor, notas y avisos</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Opciones de búsqueda</legend>
|
||||
<input
|
||||
type="text"
|
||||
id="${searchInput}"
|
||||
placeholder="Escribe aquí para buscar..."
|
||||
onkeypress="if(event.key==='Enter') document.getElementById('${searchButton}').click()"
|
||||
/>
|
||||
<select id="${moduleFilter}">
|
||||
<option value="">Todos los módulos</option>
|
||||
<!-- Options will be populated dynamically based on user permissions -->
|
||||
</select>
|
||||
<button id="${searchButton}" class="btn5">Buscar</button>
|
||||
</fieldset>
|
||||
|
||||
<div id="${recentSearches}"></div>
|
||||
|
||||
<div id="${resultsContainer}">
|
||||
<fieldset>
|
||||
<legend>Resultados</legend>
|
||||
<div>🔍 Introduce un término de búsqueda para comenzar</div>
|
||||
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
|
||||
|
||||
<details>
|
||||
<summary>💡 Consejos de búsqueda</summary>
|
||||
<ul>
|
||||
<li><strong>Busca por nombres:</strong> "María", "García"</li>
|
||||
<li><strong>Busca por fechas:</strong> "2024-10-01" o "01/10/2024"</li>
|
||||
<li><strong>Busca por ubicación:</strong> "aula", "laboratorio"</li>
|
||||
<li><strong>Usa filtros:</strong> selecciona un módulo específico</li>
|
||||
<li><strong>Atajos de teclado:</strong> Ctrl+F para buscar, Esc para limpiar</li>
|
||||
</ul>
|
||||
</details>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize global search
|
||||
const globalSearch = GlobalSearch();
|
||||
globalSearch.loadAllData();
|
||||
|
||||
// Get accessible modules for the current user
|
||||
const accessibleModules = globalSearch.getAccessibleModules();
|
||||
|
||||
const searchInputEl = document.getElementById(searchInput);
|
||||
const resultsEl = document.getElementById(resultsContainer);
|
||||
const searchButtonEl = document.getElementById(searchButton);
|
||||
const recentSearchesEl = document.getElementById(recentSearches);
|
||||
const moduleFilterEl = document.getElementById(moduleFilter);
|
||||
|
||||
// Populate module filter dropdown with only accessible modules
|
||||
function populateModuleFilter() {
|
||||
// Clear existing options except "Todos los módulos"
|
||||
moduleFilterEl.innerHTML = '<option value="">Todos los módulos</option>';
|
||||
|
||||
// Add only accessible modules
|
||||
accessibleModules.forEach((module) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = module.key;
|
||||
option.textContent = `${getModuleIcon(module.key)} ${module.title}`;
|
||||
moduleFilterEl.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get module icons (fallback for older module mappings)
|
||||
function getModuleIcon(moduleKey) {
|
||||
const iconMap = {
|
||||
personas: '👤',
|
||||
materiales: '📦',
|
||||
supercafe: '☕',
|
||||
comedor: '🍽️',
|
||||
avisos: '🔔',
|
||||
aulas: '🏫',
|
||||
resumen_diario: '📊',
|
||||
};
|
||||
return iconMap[moduleKey] || '📋';
|
||||
}
|
||||
|
||||
// Load recent searches from localStorage
|
||||
function loadRecentSearches() {
|
||||
const recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
|
||||
if (recent.length > 0) {
|
||||
recentSearchesEl.innerHTML = html`
|
||||
<fieldset>
|
||||
<legend>Búsquedas recientes</legend>
|
||||
${recent
|
||||
.map(
|
||||
(term) => `
|
||||
<button onclick="document.getElementById('${searchInput}').value='${term}'; document.getElementById('${searchButton}').click();" class="btn4">
|
||||
${term}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
<button
|
||||
onclick="localStorage.removeItem('telesec_recent_searches'); this.parentElement.style.display='none';"
|
||||
class="rojo"
|
||||
>
|
||||
Limpiar
|
||||
</button>
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the module filter dropdown
|
||||
populateModuleFilter();
|
||||
|
||||
// Save search term to recent searches
|
||||
function saveToRecent(term) {
|
||||
if (!term || term.length < 2) return;
|
||||
|
||||
let recent = JSON.parse(localStorage.getItem('telesec_recent_searches') || '[]');
|
||||
recent = recent.filter((t) => t !== term); // Remove if exists
|
||||
recent.unshift(term); // Add to beginning
|
||||
recent = recent.slice(0, 5); // Keep only 5 most recent
|
||||
|
||||
localStorage.setItem('telesec_recent_searches', JSON.stringify(recent));
|
||||
loadRecentSearches();
|
||||
}
|
||||
|
||||
// Perform search
|
||||
function performSearch() {
|
||||
const searchTerm = searchInputEl.value.trim();
|
||||
const selectedModule = moduleFilterEl.value;
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
resultsEl.innerHTML = html`
|
||||
<fieldset>
|
||||
<legend>Error</legend>
|
||||
<div>⚠️ Por favor, introduce al menos 2 caracteres para buscar</div>
|
||||
</fieldset>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading
|
||||
resultsEl.innerHTML = html`
|
||||
<fieldset>
|
||||
<legend>Buscando...</legend>
|
||||
<div>⏳ Procesando búsqueda...</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
// Add small delay to show loading state
|
||||
setTimeout(() => {
|
||||
let results = globalSearch.performSearch(searchTerm);
|
||||
|
||||
// Filter by module if selected
|
||||
if (selectedModule) {
|
||||
results = results.filter((result) => result._module === selectedModule);
|
||||
}
|
||||
|
||||
globalSearch.renderResults(results, resultsEl);
|
||||
saveToRecent(searchTerm);
|
||||
|
||||
// Add stats
|
||||
if (results.length > 0) {
|
||||
const statsDiv = document.createElement('fieldset');
|
||||
const legend = document.createElement('legend');
|
||||
legend.textContent = 'Estadísticas';
|
||||
statsDiv.appendChild(legend);
|
||||
|
||||
let filterText = selectedModule
|
||||
? ` en ${moduleFilterEl.options[moduleFilterEl.selectedIndex].text}`
|
||||
: '';
|
||||
const content = document.createElement('div');
|
||||
content.innerHTML = html`📊 Se encontraron <strong>${results.length}</strong> resultados
|
||||
para "<strong>${searchTerm}</strong>"${filterText}`;
|
||||
statsDiv.appendChild(content);
|
||||
|
||||
resultsEl.insertBefore(statsDiv, resultsEl.firstChild);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
searchButtonEl.onclick = performSearch;
|
||||
|
||||
// Filter change listener
|
||||
moduleFilterEl.onchange = () => {
|
||||
if (searchInputEl.value.trim().length >= 2) {
|
||||
performSearch();
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-search as user types (with debounce)
|
||||
let searchTimeout;
|
||||
searchInputEl.oninput = () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (searchInputEl.value.trim().length >= 2) {
|
||||
performSearch();
|
||||
}
|
||||
}, 1501);
|
||||
};
|
||||
|
||||
// Focus on search input
|
||||
searchInputEl.focus();
|
||||
|
||||
// Add keyboard shortcuts
|
||||
document.addEventListener('keydown', function (e) {
|
||||
// Ctrl+F or Cmd+F to focus search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||
e.preventDefault();
|
||||
searchInputEl.focus();
|
||||
searchInputEl.select();
|
||||
}
|
||||
|
||||
// Escape to clear search
|
||||
if (e.key === 'Escape') {
|
||||
searchInputEl.value = '';
|
||||
searchInputEl.focus();
|
||||
resultsEl.innerHTML = html`
|
||||
<fieldset>
|
||||
<legend>Resultados</legend>
|
||||
<div>🔍 Introduce un término de búsqueda para comenzar</div>
|
||||
<p>Puedes buscar por nombres, referencias, fechas, ubicaciones...</p>
|
||||
|
||||
<details>
|
||||
<summary>💡 Consejos de búsqueda</summary>
|
||||
<ul>
|
||||
<li><strong>Busca por nombres:</strong> "María", "García"</li>
|
||||
<li><strong>Busca por fechas:</strong> "2024-10-01" o "01/10/2024"</li>
|
||||
<li><strong>Busca por ubicación:</strong> "aula", "laboratorio"</li>
|
||||
<li><strong>Usa filtros:</strong> selecciona un módulo específico</li>
|
||||
<li><strong>Atajos de teclado:</strong> Ctrl+F para buscar, Esc para limpiar</li>
|
||||
</ul>
|
||||
</details>
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Check for quick search term from header
|
||||
const quickSearchTerm = sessionStorage.getItem('telesec_quick_search');
|
||||
if (quickSearchTerm) {
|
||||
searchInputEl.value = quickSearchTerm;
|
||||
sessionStorage.removeItem('telesec_quick_search');
|
||||
// Perform search automatically
|
||||
setTimeout(performSearch, 100);
|
||||
}
|
||||
|
||||
// Load recent searches
|
||||
loadRecentSearches();
|
||||
},
|
||||
};
|
||||
297
src/page/comedor.js
Normal file
@@ -0,0 +1,297 @@
|
||||
PERMS['comedor'] = 'Comedor';
|
||||
PERMS['comedor:edit'] = '> Editar';
|
||||
PAGES.comedor = {
|
||||
navcss: 'btn6',
|
||||
icon: 'static/appico/apple.png',
|
||||
AccessControl: true,
|
||||
Title: 'Comedor',
|
||||
__cleanupOldMenus: async function () {
|
||||
try {
|
||||
var rows = await DB.list('comedor');
|
||||
var now = new Date();
|
||||
var todayUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
var removed = 0;
|
||||
|
||||
function parseISODateToUTC(value) {
|
||||
if (!value || typeof value !== 'string') return null;
|
||||
var match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
var y = parseInt(match[1], 10);
|
||||
var m = parseInt(match[2], 10) - 1;
|
||||
var d = parseInt(match[3], 10);
|
||||
return Date.UTC(y, m, d);
|
||||
}
|
||||
|
||||
async function getFechaFromRow(row) {
|
||||
var data = row.data;
|
||||
if (typeof data === 'string') {
|
||||
return await new Promise((resolve) => {
|
||||
TS_decrypt(
|
||||
data,
|
||||
SECRET,
|
||||
(decrypted) => {
|
||||
if (decrypted && typeof decrypted === 'object') {
|
||||
resolve(decrypted.Fecha || row.id.split(',')[0] || '');
|
||||
} else {
|
||||
resolve(row.id.split(',')[0] || '');
|
||||
}
|
||||
},
|
||||
'comedor',
|
||||
row.id
|
||||
);
|
||||
});
|
||||
}
|
||||
if (data && typeof data === 'object') {
|
||||
return data.Fecha || row.id.split(',')[0] || '';
|
||||
}
|
||||
return row.id.split(',')[0] || '';
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
var fecha = await getFechaFromRow(row);
|
||||
var rowUTC = parseISODateToUTC(fecha);
|
||||
if (rowUTC == null) continue;
|
||||
var ageDays = Math.floor((todayUTC - rowUTC) / 86400000);
|
||||
if (ageDays >= 30) {
|
||||
await DB.del('comedor', row.id);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
toastr.info('Limpieza automática: ' + removed + ' menús antiguos eliminados.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Comedor cleanup error', e);
|
||||
}
|
||||
},
|
||||
edit: function (mid) {
|
||||
if (!checkRole('comedor:edit')) {
|
||||
setUrlHash('comedor');
|
||||
return;
|
||||
}
|
||||
var nameh1 = safeuuid();
|
||||
var field_fecha = safeuuid();
|
||||
var field_tipo = safeuuid();
|
||||
var field_primero = safeuuid();
|
||||
var field_segundo = safeuuid();
|
||||
var field_postre = safeuuid();
|
||||
var btn_picto_primero = safeuuid();
|
||||
var btn_picto_segundo = safeuuid();
|
||||
var btn_picto_postre = safeuuid();
|
||||
var debounce_picto = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Entrada del menú <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: left;">
|
||||
<legend>Valores</legend>
|
||||
<label>
|
||||
Fecha<br />
|
||||
<input type="date" id="${field_fecha}" value="" /><br /><br />
|
||||
</label>
|
||||
<label>
|
||||
Tipo<br />
|
||||
<input type="text" id="${field_tipo}" value="" /><br /><br />
|
||||
</label>
|
||||
<label>
|
||||
Primero<br />
|
||||
<input type="text" id="${field_primero}" value="" /><br />
|
||||
<div class="picto" id="${btn_picto_primero}"></div>
|
||||
</label>
|
||||
<label>
|
||||
Segundo<br />
|
||||
<input type="text" id="${field_segundo}" value="" /><br />
|
||||
<div class="picto" id="${btn_picto_segundo}"></div>
|
||||
</label>
|
||||
<label>
|
||||
Postre<br />
|
||||
<input type="text" id="${field_postre}" value="" /><br />
|
||||
<div class="picto" id="${btn_picto_postre}"></div>
|
||||
</label>
|
||||
<button class="saveico" id="${btn_guardar}">
|
||||
<img src="static/floppy_disk_green.png" />
|
||||
<br>Guardar
|
||||
</button>
|
||||
<button class="delico" id="${btn_borrar}">
|
||||
<img src="static/garbage.png" />
|
||||
<br>Borrar
|
||||
</button>
|
||||
<button class="opicon" onclick="setUrlHash('comedor')" style="float: right;"> <!-- Align to the right -->
|
||||
<img src="static/exit.png" />
|
||||
<br>Salir
|
||||
</button>
|
||||
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||
<img src="static/printer2.png" />
|
||||
<br>Imprimir
|
||||
</button>
|
||||
</fieldset>
|
||||
`;
|
||||
const pictogramSelector = TS_CreateArasaacSelector({
|
||||
modal: true,
|
||||
debounceId: debounce_picto,
|
||||
onPick: (context, item) => {
|
||||
TS_applyPictoValue(context.pictoId, {
|
||||
text: item.label,
|
||||
arasaacId: String(item.id),
|
||||
});
|
||||
},
|
||||
});
|
||||
document.getElementById(btn_picto_primero).onclick = () =>
|
||||
pictogramSelector.open({ pictoId: btn_picto_primero });
|
||||
document.getElementById(btn_picto_segundo).onclick = () =>
|
||||
pictogramSelector.open({ pictoId: btn_picto_segundo });
|
||||
document.getElementById(btn_picto_postre).onclick = () =>
|
||||
pictogramSelector.open({ pictoId: btn_picto_postre });
|
||||
DB.get('comedor', mid).then((data) => {
|
||||
function load_data(data, ENC = '') {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_fecha).value = data['Fecha'] || mid || CurrentISODate();
|
||||
document.getElementById(field_tipo).value = data['Tipo'] || '';
|
||||
document.getElementById(field_primero).value = data['Primero'] || '';
|
||||
document.getElementById(field_segundo).value = data['Segundo'] || '';
|
||||
document.getElementById(field_postre).value = data['Postre'] || '';
|
||||
TS_applyPictoValue(btn_picto_primero, data['Primero_Picto'] || '');
|
||||
TS_applyPictoValue(btn_picto_segundo, data['Segundo_Picto'] || '');
|
||||
TS_applyPictoValue(btn_picto_postre, data['Postre_Picto'] || '');
|
||||
}
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(
|
||||
data,
|
||||
SECRET,
|
||||
(data, wasEncrypted) => {
|
||||
load_data(data, '%E');
|
||||
},
|
||||
'comedor',
|
||||
mid
|
||||
);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = '0.5';
|
||||
|
||||
const newDate = document.getElementById(field_fecha).value;
|
||||
const newTipo = document.getElementById(field_tipo).value.trim();
|
||||
var data = {
|
||||
Fecha: newDate,
|
||||
Tipo: newTipo,
|
||||
Primero: document.getElementById(field_primero).value.trim(),
|
||||
Segundo: document.getElementById(field_segundo).value.trim(),
|
||||
Postre: document.getElementById(field_postre).value.trim(),
|
||||
Primero_Picto: TS_getPictoValue(btn_picto_primero),
|
||||
Segundo_Picto: TS_getPictoValue(btn_picto_segundo),
|
||||
Postre_Picto: TS_getPictoValue(btn_picto_postre),
|
||||
};
|
||||
|
||||
// If the date has changed, we need to delete the old entry
|
||||
if (mid !== newDate + "," + newTipo && mid !== '') {
|
||||
DB.del('comedor', mid);
|
||||
}
|
||||
|
||||
document.getElementById('actionStatus').style.display = 'block';
|
||||
DB.put('comedor', newDate + "," + newTipo, data)
|
||||
.then(() => {
|
||||
toastr.success('Guardado!');
|
||||
setTimeout(() => {
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
setUrlHash('comedor');
|
||||
}, SAVE_WAIT);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = '1';
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
toastr.error('Error al guardar el menú');
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm('¿Quieres borrar esta entrada?') == true) {
|
||||
DB.del('comedor', mid).then(() => {
|
||||
toastr.error('Borrado!');
|
||||
setTimeout(() => {
|
||||
setUrlHash('comedor');
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
if (!checkRole('comedor')) {
|
||||
setUrlHash('index');
|
||||
return;
|
||||
}
|
||||
const cont = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Menú del comedor</h1>
|
||||
<button id="${btn_new}">Nueva entrada</button>
|
||||
<div id="${cont}"></div>
|
||||
`;
|
||||
var renderList = () => {
|
||||
TS_IndexElement(
|
||||
'comedor',
|
||||
[
|
||||
{
|
||||
key: 'Fecha',
|
||||
type: 'raw',
|
||||
default: '',
|
||||
label: 'Fecha',
|
||||
},
|
||||
{
|
||||
key: 'Tipo',
|
||||
type: 'raw',
|
||||
default: '',
|
||||
label: 'Tipo',
|
||||
},
|
||||
{
|
||||
key: 'Primero_Picto',
|
||||
type: 'picto',
|
||||
default: '',
|
||||
label: 'Primero',
|
||||
labelkey: 'Primero',
|
||||
},
|
||||
{
|
||||
key: 'Segundo_Picto',
|
||||
type: 'picto',
|
||||
default: '',
|
||||
label: 'Segundo',
|
||||
labelkey: 'Segundo',
|
||||
},
|
||||
{
|
||||
key: 'Postre_Picto',
|
||||
type: 'picto',
|
||||
default: '',
|
||||
label: 'Postre',
|
||||
labelkey: 'Postre',
|
||||
},
|
||||
],
|
||||
'comedor',
|
||||
document.getElementById(cont),
|
||||
(data, new_tr) => {
|
||||
// new_tr.style.backgroundColor = "#FFCCCB";
|
||||
if (data.Fecha == CurrentISODate()) {
|
||||
new_tr.style.backgroundColor = 'lightgreen';
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
PAGES.comedor.__cleanupOldMenus().finally(renderList);
|
||||
|
||||
if (!checkRole('comedor:edit')) {
|
||||
document.getElementById(btn_new).style.display = 'none';
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash('comedor,' + safeuuid(''));
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
300
src/page/dataman.js
Normal file
@@ -0,0 +1,300 @@
|
||||
PAGES.dataman = {
|
||||
navcss: 'btn1',
|
||||
icon: 'static/appico/gear_edit.png',
|
||||
AccessControl: true,
|
||||
Title: 'Ajustes',
|
||||
edit: function (mid) {
|
||||
switch (mid) {
|
||||
case 'export':
|
||||
PAGES.dataman.__export();
|
||||
break;
|
||||
case 'import':
|
||||
PAGES.dataman.__import();
|
||||
break;
|
||||
case 'config':
|
||||
PAGES.dataman.__config();
|
||||
break;
|
||||
case 'labels':
|
||||
PAGES.dataman.__labels();
|
||||
break;
|
||||
case 'precios':
|
||||
PAGES.dataman.__precios();
|
||||
break;
|
||||
default:
|
||||
// Tab to edit
|
||||
}
|
||||
},
|
||||
__config: function () {
|
||||
var form = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Ajustes</h1>
|
||||
<h2>No disponible</h2>
|
||||
<form id="${form}">
|
||||
<label>
|
||||
<input type="checkbox" name="block_add_account" value="yes" />
|
||||
<b>Bloquear crear cuenta de administrador?</b>
|
||||
</label>
|
||||
<button type="submit">Aplicar ajustes</button>
|
||||
</form>
|
||||
`;
|
||||
document.getElementById(form).onsubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
var ford = new FormData(document.getElementById(form));
|
||||
if (ford.get('block_add_account') == 'yes') {
|
||||
config['block_add_account'] = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
__export: function () {
|
||||
var button_export_local = safeuuid();
|
||||
var button_export_safe = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Exportar Datos</h1>
|
||||
<fieldset>
|
||||
<legend>Exportar datos</legend>
|
||||
<em>Al pulsar, Espera hasta que salga una notificacion verde.</em>
|
||||
<br />
|
||||
<br />
|
||||
<button id="${button_export_local}" type="button">Exportar sin cifrar</button>
|
||||
<button id="${button_export_safe}" type="button">Exportar con cifrado</button>
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(button_export_local).onclick = () => {
|
||||
var data_export = {};
|
||||
var output = {
|
||||
materiales: {},
|
||||
personas: {},
|
||||
};
|
||||
(async () => {
|
||||
const materiales = await DB.list('materiales');
|
||||
materiales.forEach((entry) => {
|
||||
const key = entry.id;
|
||||
const value = entry.data;
|
||||
if (value != null) {
|
||||
if (typeof value == 'string') {
|
||||
TS_decrypt(
|
||||
value,
|
||||
SECRET,
|
||||
(data, wasEncrypted) => {
|
||||
output.materiales[key] = data;
|
||||
},
|
||||
'materiales',
|
||||
key
|
||||
);
|
||||
} else {
|
||||
output.materiales[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
const personas = await DB.list('personas');
|
||||
personas.forEach((entry) => {
|
||||
const key = entry.id;
|
||||
const value = entry.data;
|
||||
if (value != null) {
|
||||
if (typeof value == 'string') {
|
||||
TS_decrypt(
|
||||
value,
|
||||
SECRET,
|
||||
(data, wasEncrypted) => {
|
||||
output.personas[key] = data;
|
||||
},
|
||||
'personas',
|
||||
key
|
||||
);
|
||||
} else {
|
||||
output.personas[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
toastr.success('Exportado todo, descargando!');
|
||||
download(`Export %%TITLE%% ${getDBName()}.json.txt`, JSON.stringify(output));
|
||||
})();
|
||||
};
|
||||
document.getElementById(button_export_safe).onclick = () => {
|
||||
(async () => {
|
||||
const result = { materiales: {}, personas: {} };
|
||||
const materiales = await DB.list('materiales');
|
||||
materiales.forEach((entry) => {
|
||||
result.materiales[entry.id] = entry.data;
|
||||
});
|
||||
const personas = await DB.list('personas');
|
||||
personas.forEach((entry) => {
|
||||
result.personas[entry.id] = entry.data;
|
||||
});
|
||||
toastr.success('Exportado todo, descargado!');
|
||||
download(`Export %%TITLE%% Encriptado ${getDBName()}.json.txt`, JSON.stringify(result));
|
||||
})();
|
||||
};
|
||||
},
|
||||
__import: function () {
|
||||
var select_type = safeuuid();
|
||||
var textarea_content = safeuuid();
|
||||
var button_import = safeuuid();
|
||||
var button_clear = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Importar Datos</h1>
|
||||
<fieldset>
|
||||
<legend>Importar datos</legend>
|
||||
<em>Espera hasta que se vacien todas las notificaciones.</em>
|
||||
<select id="${select_type}">
|
||||
<option value="" disabled selected>Tipo de archivo...</option>
|
||||
<option value="comedor">Galileo - db.comedor.axd</option>
|
||||
<option value="recetas">Galileo - db.recetas.axd</option>
|
||||
<option value="materiales">Galileo - db.materiales.axd</option>
|
||||
<option value="personas">Galileo - db.personas.axd</option>
|
||||
<option value="comandas">Galileo - db.cafe.comandas.axd</option>
|
||||
<option value="%telesec">TeleSec Exportado (encriptado o no)</option>
|
||||
</select>
|
||||
<textarea
|
||||
id="${textarea_content}"
|
||||
style="height: 100px;"
|
||||
placeholder="Introduce el contenido del archivo"
|
||||
></textarea>
|
||||
<button id="${button_import}" type="button">Importar</button>
|
||||
<button id="${button_clear}" type="button">Vaciar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
document.getElementById(button_import).onclick = () => {
|
||||
toastr.info('Importando datos...');
|
||||
var val = document.getElementById(textarea_content).value;
|
||||
var sel = document.getElementById(select_type).value;
|
||||
if (sel == '%telesec') {
|
||||
// legacy import, store entire payload as-is
|
||||
// for each top-level key, store their items in DB
|
||||
var parsed = JSON.parse(val);
|
||||
Object.entries(parsed).forEach((section) => {
|
||||
const sectionName = section[0];
|
||||
const sectionData = section[1];
|
||||
Object.entries(sectionData).forEach((entry) => {
|
||||
DB.put(sectionName, entry[0], entry[1]).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.entries(JSON.parse(val)['data']).forEach((entry) => {
|
||||
DB.put(sel, entry[0], entry[1]).catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
});
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
toastr.info('Importado todo!');
|
||||
|
||||
if (sel == '%telesec') {
|
||||
setUrlHash('inicio');
|
||||
} else {
|
||||
setUrlHash(sel);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
},
|
||||
__labels: function (mid) {
|
||||
var div_materiales = safeuuid();
|
||||
container.innerHTML = html` <h1>Imprimir Etiquetas QR</h1>
|
||||
<button onclick="print()">Imprimir</button>
|
||||
<h2>Materiales</h2>
|
||||
<div id="${div_materiales}"></div>
|
||||
<br /><br />`;
|
||||
div_materiales = document.getElementById(div_materiales);
|
||||
DB.map('materiales', (data, key) => {
|
||||
function add_row(data, key) {
|
||||
if (data != null) {
|
||||
div_materiales.innerHTML += BuildQR('materiales,' + key, data['Nombre'] || key);
|
||||
}
|
||||
}
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(data, SECRET, (data) => {
|
||||
add_row(data, key);
|
||||
});
|
||||
} else {
|
||||
add_row(data, key);
|
||||
}
|
||||
});
|
||||
},
|
||||
__precios: function () {
|
||||
var form = safeuuid();
|
||||
|
||||
// Cargar precios actuales desde DB
|
||||
DB.get('config', 'precios_cafe').then((raw) => {
|
||||
TS_decrypt(raw, SECRET, (precios) => {
|
||||
container.innerHTML = html`
|
||||
<h1>Configuración de Precios del Café</h1>
|
||||
<form id="${form}">
|
||||
<fieldset>
|
||||
<legend>Precios Base (en céntimos)</legend>
|
||||
<label>
|
||||
<b>Servicio base:</b>
|
||||
<input type="number" name="servicio_base" value="${precios.servicio_base || 10}" min="0" step="1" />
|
||||
céntimos
|
||||
</label>
|
||||
<br><br>
|
||||
<label>
|
||||
<b>Leche pequeña:</b>
|
||||
<input type="number" name="leche_pequena" value="${precios.leche_pequena || 15}" min="0" step="1" />
|
||||
céntimos
|
||||
</label>
|
||||
<br><br>
|
||||
<label>
|
||||
<b>Leche grande:</b>
|
||||
<input type="number" name="leche_grande" value="${precios.leche_grande || 25}" min="0" step="1" />
|
||||
céntimos
|
||||
</label>
|
||||
<br><br>
|
||||
<label>
|
||||
<b>Café:</b>
|
||||
<input type="number" name="cafe" value="${precios.cafe || 25}" min="0" step="1" />
|
||||
céntimos
|
||||
</label>
|
||||
<br><br>
|
||||
<label>
|
||||
<b>ColaCao:</b>
|
||||
<input type="number" name="colacao" value="${precios.colacao || 25}" min="0" step="1" />
|
||||
céntimos
|
||||
</label>
|
||||
</fieldset>
|
||||
<br>
|
||||
<button type="submit">💾 Guardar precios</button>
|
||||
<button type="button" onclick="setUrlHash('dataman')">🔙 Volver</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
document.getElementById(form).onsubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
var formData = new FormData(document.getElementById(form));
|
||||
var nuevosPrecios = {
|
||||
servicio_base: parseInt(formData.get('servicio_base')) || 10,
|
||||
leche_pequena: parseInt(formData.get('leche_pequena')) || 15,
|
||||
leche_grande: parseInt(formData.get('leche_grande')) || 25,
|
||||
cafe: parseInt(formData.get('cafe')) || 25,
|
||||
colacao: parseInt(formData.get('colacao')) || 25,
|
||||
};
|
||||
|
||||
DB.put('config', 'precios_cafe', nuevosPrecios).then(() => {
|
||||
toastr.success('Precios guardados correctamente');
|
||||
// Actualizar variable global
|
||||
if (window.PRECIOS_CAFE) {
|
||||
Object.assign(window.PRECIOS_CAFE, nuevosPrecios);
|
||||
}
|
||||
setTimeout(() => setUrlHash('dataman'), 1000);
|
||||
}).catch((e) => {
|
||||
toastr.error('Error al guardar precios: ' + e.message);
|
||||
});
|
||||
};
|
||||
});
|
||||
}).catch(() => {
|
||||
// Si no hay precios guardados, usar valores por defecto
|
||||
PAGES.dataman.__precios();
|
||||
});
|
||||
},
|
||||
index: function () {
|
||||
container.innerHTML = html`
|
||||
<h1>Administración de datos</h1>
|
||||
<a class="button" href="#dataman,import">Importar datos</a>
|
||||
<a class="button" href="#dataman,export">Exportar datos</a>
|
||||
<a class="button" href="#dataman,labels">Imprimir etiquetas</a>
|
||||
<a class="button" href="#dataman,precios">⚙️ Precios del café</a>
|
||||
<a class="button" href="#dataman,config">Ajustes</a>
|
||||
`;
|
||||
},
|
||||
};
|
||||
327
src/page/index.js
Normal file
@@ -0,0 +1,327 @@
|
||||
PAGES.index = {
|
||||
//navcss: "btn1",
|
||||
Title: 'Inicio',
|
||||
icon: 'static/appico/house.png',
|
||||
index: function () {
|
||||
var div_stats = safeuuid();
|
||||
|
||||
container.innerHTML = html`
|
||||
<h1>¡Hola, ${SUB_LOGGED_IN_DETAILS.Nombre}!<br />Bienvenidx a %%TITLE%%</h1>
|
||||
<h2>
|
||||
Tienes ${parseFloat(SUB_LOGGED_IN_DETAILS.Monedero_Balance).toPrecision(2)} € en el
|
||||
monedero.
|
||||
</h2>
|
||||
<details style="border: 2px solid black; padding: 15px; border-radius: 10px;">
|
||||
<summary>Estadisticas</summary>
|
||||
<div
|
||||
id="${div_stats}"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; margin-bottom: 20px;"
|
||||
></div>
|
||||
</details>
|
||||
<em>Utiliza el menú superior para abrir un modulo</em>
|
||||
<br /><br />
|
||||
<button class="btn1" onclick="ActualizarProgramaTeleSec()">Actualizar programa</button>
|
||||
<button class="btn1" onclick="LogOutTeleSec()">Cerrar sesión</button>
|
||||
`;
|
||||
|
||||
if (checkRole('pagos')) {
|
||||
var total_ingresos = safeuuid();
|
||||
var total_gastos = safeuuid();
|
||||
var balance_total = safeuuid();
|
||||
var total_ingresos_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #2ed573, #26d063); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">Pagos</span><br>
|
||||
<h3 style="margin: 0;">Total Ingresos</h3>
|
||||
<div id="${total_ingresos}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0.00€
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
var total_gastos_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #ff4757, #ff3838); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">Pagos</span><br>
|
||||
<h3 style="margin: 0;">Total Gastos</h3>
|
||||
<div id="${total_gastos}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0.00€
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
var balance_total_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #667eea, #764ba2); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">Pagos</span><br>
|
||||
<h3 style="margin: 0;">Balance Total</h3>
|
||||
<div id="${balance_total}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0.00€
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(total_ingresos_srcel));
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(total_gastos_srcel));
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(balance_total_srcel));
|
||||
|
||||
let totalData = {
|
||||
ingresos: {},
|
||||
gastos: {},
|
||||
};
|
||||
|
||||
EventListeners.DB.push(
|
||||
DB.map('pagos', (data, key) => {
|
||||
function applyData(row) {
|
||||
if (!row || typeof row !== 'object') {
|
||||
delete totalData.ingresos[key];
|
||||
delete totalData.gastos[key];
|
||||
} else {
|
||||
const monto = parseFloat(row.Monto || 0) || 0;
|
||||
const tipo = row.Tipo;
|
||||
|
||||
if (tipo === 'Ingreso') {
|
||||
if (row.Origen != 'Promo Bono') {
|
||||
totalData.gastos[key] = 0;
|
||||
totalData.ingresos[key] = monto;
|
||||
}
|
||||
} else if (tipo === 'Gasto') {
|
||||
totalData.ingresos[key] = 0;
|
||||
totalData.gastos[key] = monto;
|
||||
} else {
|
||||
totalData.ingresos[key] = 0;
|
||||
totalData.gastos[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const totalIngresos = Object.values(totalData.ingresos).reduce((a, b) => a + b, 0);
|
||||
const totalGastos = Object.values(totalData.gastos).reduce((a, b) => a + b, 0);
|
||||
|
||||
document.getElementById(total_ingresos).innerText = totalIngresos.toFixed(2) + '€';
|
||||
document.getElementById(total_gastos).innerText = totalGastos.toFixed(2) + '€';
|
||||
}
|
||||
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(data, SECRET, (decoded) => {
|
||||
applyData(decoded);
|
||||
});
|
||||
} else {
|
||||
applyData(data);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
EventListeners.Interval.push(
|
||||
setInterval(() => {
|
||||
var balanceReal = 0;
|
||||
Object.values(SC_Personas).forEach((persona) => {
|
||||
balanceReal += parseFloat(persona.Monedero_Balance || 0);
|
||||
});
|
||||
document.getElementById(balance_total).innerText = balanceReal.toFixed(2) + '€';
|
||||
document.getElementById(balance_total).style.color =
|
||||
balanceReal >= 0 ? 'white' : '#ffcccc';
|
||||
}, 1000)
|
||||
);
|
||||
}
|
||||
|
||||
if (checkRole('mensajes')) {
|
||||
var mensajes_sin_leer = safeuuid();
|
||||
var mensajes_sin_leer_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #66380d, #a5570d); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">Mensajes</span><br>
|
||||
<h3 style="margin: 0;">Sin leer</h3>
|
||||
<div id="${mensajes_sin_leer}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(mensajes_sin_leer_srcel));
|
||||
|
||||
var unreadById = {};
|
||||
|
||||
EventListeners.DB.push(
|
||||
DB.map('mensajes', (data, key) => {
|
||||
function applyUnread(row) {
|
||||
if (!row || typeof row !== 'object') {
|
||||
delete unreadById[key];
|
||||
} else {
|
||||
var estado = String(row.Estado || '').trim().toLowerCase();
|
||||
var isRead = estado === 'leido' || estado === 'leído';
|
||||
unreadById[key] = isRead ? 0 : 1;
|
||||
}
|
||||
|
||||
var totalUnread = Object.values(unreadById).reduce((a, b) => a + b, 0);
|
||||
document.getElementById(mensajes_sin_leer).innerText = String(totalUnread);
|
||||
}
|
||||
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(data, SECRET, (decoded) => {
|
||||
applyUnread(decoded);
|
||||
});
|
||||
} else {
|
||||
applyUnread(data);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (checkRole('supercafe')) {
|
||||
var comandas_en_deuda = safeuuid();
|
||||
var comandas_en_deuda_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #8e44ad, #6c3483); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">SuperCafé</span><br>
|
||||
<h3 style="margin: 0;">Comandas en deuda</h3>
|
||||
<div id="${comandas_en_deuda}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(comandas_en_deuda_srcel));
|
||||
|
||||
var deudaById = {};
|
||||
|
||||
EventListeners.DB.push(
|
||||
DB.map('supercafe', (data, key) => {
|
||||
function applyDeuda(row) {
|
||||
if (!row || typeof row !== 'object') {
|
||||
delete deudaById[key];
|
||||
} else {
|
||||
var estado = String(row.Estado || '').trim().toLowerCase();
|
||||
deudaById[key] = estado === 'deuda' ? 1 : 0;
|
||||
}
|
||||
|
||||
var totalDeuda = Object.values(deudaById).reduce((a, b) => a + b, 0);
|
||||
document.getElementById(comandas_en_deuda).innerText = String(totalDeuda);
|
||||
}
|
||||
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(data, SECRET, (decoded) => {
|
||||
applyDeuda(decoded);
|
||||
});
|
||||
} else {
|
||||
applyDeuda(data);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (checkRole('materiales')) {
|
||||
var materiales_comprar = safeuuid();
|
||||
var materiales_revisar = safeuuid();
|
||||
|
||||
var materiales_comprar_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #e67e22, #d35400); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">Almacén</span><br>
|
||||
<h3 style="margin: 0;">Por comprar</h3>
|
||||
<div id="${materiales_comprar}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var materiales_revisar_srcel = html`
|
||||
<div
|
||||
style="background: linear-gradient(135deg, #2980b9, #1f6391); padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<span style="font-size: 16px;">Almacén</span><br>
|
||||
<h3 style="margin: 0;">Por revisar</h3>
|
||||
<div id="${materiales_revisar}" style="font-size: 32px; font-weight: bold; margin-top: 10px;">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(materiales_comprar_srcel));
|
||||
document.getElementById(div_stats).appendChild(createElementFromHTML(materiales_revisar_srcel));
|
||||
|
||||
var comprarById = {};
|
||||
var revisarById = {};
|
||||
|
||||
EventListeners.DB.push(
|
||||
DB.map('materiales', (data, key) => {
|
||||
function applyMaterialStats(row) {
|
||||
if (!row || typeof row !== 'object') {
|
||||
delete comprarById[key];
|
||||
delete revisarById[key];
|
||||
} else {
|
||||
var cantidad = parseFloat(row.Cantidad);
|
||||
var cantidadMinima = parseFloat(row.Cantidad_Minima);
|
||||
var lowStock = !isNaN(cantidad) && !isNaN(cantidadMinima) && cantidad < cantidadMinima;
|
||||
comprarById[key] = lowStock ? 1 : 0;
|
||||
|
||||
var revision = String(row.Revision || '?').trim();
|
||||
var needsReview = false;
|
||||
|
||||
if (revision === '?' || revision === '' || revision === '-') {
|
||||
needsReview = true;
|
||||
} else {
|
||||
var match = revision.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
needsReview = true;
|
||||
} else {
|
||||
var y = parseInt(match[1], 10);
|
||||
var m = parseInt(match[2], 10) - 1;
|
||||
var d = parseInt(match[3], 10);
|
||||
var revisionMs = Date.UTC(y, m, d);
|
||||
var now = new Date();
|
||||
var todayMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
var diffDays = Math.floor((todayMs - revisionMs) / 86400000);
|
||||
needsReview = diffDays >= 90;
|
||||
}
|
||||
}
|
||||
|
||||
revisarById[key] = needsReview ? 1 : 0;
|
||||
}
|
||||
|
||||
var totalComprar = Object.values(comprarById).reduce((a, b) => a + b, 0);
|
||||
var totalRevisar = Object.values(revisarById).reduce((a, b) => a + b, 0);
|
||||
|
||||
document.getElementById(materiales_comprar).innerText = String(totalComprar);
|
||||
document.getElementById(materiales_revisar).innerText = String(totalRevisar);
|
||||
}
|
||||
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(data, SECRET, (decoded) => {
|
||||
applyMaterialStats(decoded);
|
||||
});
|
||||
} else {
|
||||
applyMaterialStats(data);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
edit: function (mid) {
|
||||
switch (mid) {
|
||||
case 'qr':
|
||||
PAGES.index.__scan();
|
||||
break;
|
||||
}
|
||||
},
|
||||
__scan: function (mid) {
|
||||
var qrscan = safeuuid();
|
||||
container.innerHTML = html` <h1>Escanear Codigo QR</h1>
|
||||
<div style="max-width: 400px;" id="${qrscan}"></div>
|
||||
<br /><br />`;
|
||||
var html5QrcodeScanner = new Html5QrcodeScanner(qrscan, { fps: 10, qrbox: 250 });
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
html5QrcodeScanner.clear();
|
||||
// Handle on success condition with the decoded text or result.
|
||||
// alert(`Scan result: ${decodedText}`, decodedResult);
|
||||
setUrlHash(decodedText);
|
||||
// ...
|
||||
|
||||
// ^ this will stop the scanner (video feed) and clear the scan area.
|
||||
}
|
||||
|
||||
html5QrcodeScanner.render(onScanSuccess);
|
||||
EventListeners.QRScanner.push(html5QrcodeScanner);
|
||||
},
|
||||
};
|
||||
461
src/page/login.js
Normal file
@@ -0,0 +1,461 @@
|
||||
function makeCouchURLDisplay(host, user, pass, dbname) {
|
||||
if (!host) return '';
|
||||
var display = user + ':' + pass + '@' + host.replace(/^https?:\/\//, '') + '/' + dbname;
|
||||
return display;
|
||||
}
|
||||
PAGES.login = {
|
||||
Esconder: true,
|
||||
Title: 'Login',
|
||||
onboarding: function (step) {
|
||||
// Multi-step onboarding flow
|
||||
step = step || 'config';
|
||||
|
||||
if (step === 'config') {
|
||||
// Step 1: "Configuración de datos"
|
||||
var field_couch = safeuuid();
|
||||
var field_secret = safeuuid();
|
||||
var btn_existing_server = safeuuid();
|
||||
var btn_new_server = safeuuid();
|
||||
var btn_skip = safeuuid();
|
||||
var div_server_config = safeuuid();
|
||||
|
||||
container.innerHTML = html`
|
||||
<h1>¡Bienvenido a TeleSec! 🎉</h1>
|
||||
<h2>Paso 1: Configuración de datos</h2>
|
||||
<p>Para comenzar, elige cómo quieres configurar tu base de datos:</p>
|
||||
<fieldset>
|
||||
<button id="${btn_existing_server}" class="btn5">Conectar a CouchDB existente</button>
|
||||
<button id="${btn_new_server}" class="btn2">Solicitar un nuevo CouchDB</button>
|
||||
<button id="${btn_skip}" class="btn3">No sincronizar (no recomendado)</button>
|
||||
</fieldset>
|
||||
<div id="${div_server_config}" style="display:none;margin-top:20px;">
|
||||
<h3>Configuración del servidor CouchDB</h3>
|
||||
<fieldset>
|
||||
<label
|
||||
>Origen CouchDB (ej: usuario:contraseña@servidor/basededatos)
|
||||
<input
|
||||
type="text"
|
||||
id="${field_couch}"
|
||||
value="${makeCouchURLDisplay(
|
||||
localStorage.getItem('TELESEC_COUCH_URL'),
|
||||
localStorage.getItem('TELESEC_COUCH_USER'),
|
||||
localStorage.getItem('TELESEC_COUCH_PASS'),
|
||||
localStorage.getItem('TELESEC_COUCH_DBNAME')
|
||||
)}"
|
||||
/><br /><br />
|
||||
</label>
|
||||
<label
|
||||
>Clave de encriptación <span style="color: red;">*</span>
|
||||
<input
|
||||
type="password"
|
||||
id="${field_secret}"
|
||||
value="${localStorage.getItem('TELESEC_SECRET') || ''}"
|
||||
required
|
||||
/><br /><br />
|
||||
</label>
|
||||
<button id="${btn_skip}-save" class="btn5">Guardar y Continuar</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById(btn_existing_server).onclick = () => {
|
||||
document.getElementById(div_server_config).style.display = 'block';
|
||||
};
|
||||
|
||||
document.getElementById(btn_new_server).onclick = () => {
|
||||
window.open('https://tech.eus/telesec-signup.php', '_blank');
|
||||
toastr.info(
|
||||
'Una vez creado el servidor, vuelve aquí y conéctate usando el botón "Conectar a un servidor existente"'
|
||||
);
|
||||
};
|
||||
|
||||
document.getElementById(btn_skip).onclick = () => {
|
||||
// Continue to persona creation without server config
|
||||
// Check if personas already exist (shouldn't happen but safety check)
|
||||
var hasPersonas = Object.keys(SC_Personas).length > 0;
|
||||
if (hasPersonas) {
|
||||
toastr.info('Ya existen personas. Saltando creación de cuenta.');
|
||||
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
|
||||
open_page('login');
|
||||
setUrlHash('login');
|
||||
} else {
|
||||
open_page('login,onboarding-persona');
|
||||
setUrlHash('login,onboarding-persona');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById(btn_skip + '-save').onclick = () => {
|
||||
var url = document.getElementById(field_couch).value.trim();
|
||||
var secret = document.getElementById(field_secret).value.trim();
|
||||
|
||||
if (!url) {
|
||||
toastr.error('Por favor ingresa un servidor CouchDB');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
toastr.error('La clave de encriptación es obligatoria');
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize URL: add https:// if no protocol specified
|
||||
var normalizedUrl = url;
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
normalizedUrl = 'https://' + url;
|
||||
}
|
||||
var URL_PARSED = parseURL(normalizedUrl);
|
||||
var user = URL_PARSED.username || '';
|
||||
var pass = URL_PARSED.password || '';
|
||||
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
|
||||
var host = URL_PARSED.hostname || normalizedUrl;
|
||||
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
|
||||
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||
localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||
localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
SECRET = secret.toUpperCase();
|
||||
|
||||
try {
|
||||
DB.init({
|
||||
secret: SECRET,
|
||||
remoteServer: 'https://' + host,
|
||||
username: user,
|
||||
password: pass,
|
||||
dbname: dbname || undefined,
|
||||
});
|
||||
toastr.success('Servidor configurado correctamente');
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
function waitForReplicationIdle(maxWaitMs, idleMs) {
|
||||
var startTime = Date.now();
|
||||
var lastSeenSync = window.TELESEC_LAST_SYNC || 0;
|
||||
return new Promise((resolve) => {
|
||||
var interval = setInterval(() => {
|
||||
var now = Date.now();
|
||||
var currentSync = window.TELESEC_LAST_SYNC || 0;
|
||||
if (currentSync > lastSeenSync) {
|
||||
lastSeenSync = currentSync;
|
||||
}
|
||||
var lastActivity = Math.max(lastSeenSync, startTime);
|
||||
var idleLongEnough = now - lastActivity >= idleMs;
|
||||
var timedOut = now - startTime >= maxWaitMs;
|
||||
if (idleLongEnough || timedOut) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait until replication goes idle or timeout
|
||||
waitForReplicationIdle(10000, 2500).then(() => {
|
||||
// Check if personas were replicated from server
|
||||
var hasPersonas = Object.keys(SC_Personas).length > 0;
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
if (hasPersonas) {
|
||||
// Personas found from server, skip persona creation step
|
||||
toastr.info('Se encontraron personas en el servidor. Saltando creación de cuenta.');
|
||||
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
|
||||
open_page('login');
|
||||
setUrlHash('login');
|
||||
} else {
|
||||
// No personas found, continue to persona creation
|
||||
open_page('login,onboarding-persona');
|
||||
setUrlHash('login,onboarding-persona');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
toastr.error('Error al configurar el servidor: ' + (e.message || e));
|
||||
}
|
||||
};
|
||||
} else if (step === 'persona') {
|
||||
// Step 2: "Crea una persona"
|
||||
var field_nombre = safeuuid();
|
||||
var btn_crear = safeuuid();
|
||||
|
||||
// Check if personas already exist
|
||||
var hasPersonas = Object.keys(SC_Personas).length > 0;
|
||||
if (hasPersonas) {
|
||||
toastr.info('Se detectaron personas existentes. Redirigiendo al login.');
|
||||
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
|
||||
open_page('login');
|
||||
setUrlHash('login');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = html`
|
||||
<h1>¡Bienvenido a TeleSec! 🎉</h1>
|
||||
<h2>Paso 2: Crea tu cuenta de administrador</h2>
|
||||
<p>Para continuar, necesitas crear una cuenta personal con permisos de administrador.</p>
|
||||
<fieldset>
|
||||
<label
|
||||
>Tu nombre:
|
||||
<input
|
||||
type="text"
|
||||
id="${field_nombre}"
|
||||
placeholder="Ej: Juan Pérez"
|
||||
autofocus
|
||||
/><br /><br />
|
||||
</label>
|
||||
<p>
|
||||
<small
|
||||
>ℹ️ Esta cuenta tendrá todos los permisos de administrador y podrás gestionar la
|
||||
aplicación completamente.</small
|
||||
>
|
||||
</p>
|
||||
<button id="${btn_crear}" class="btn5">Crear cuenta y empezar</button>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
document.getElementById(btn_crear).onclick = () => {
|
||||
var nombre = document.getElementById(field_nombre).value.trim();
|
||||
if (!nombre) {
|
||||
toastr.error('Por favor ingresa tu nombre');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button to prevent duplicate creation
|
||||
var btnElement = document.getElementById(btn_crear);
|
||||
btnElement.disabled = true;
|
||||
btnElement.style.opacity = '0.5';
|
||||
btnElement.innerText = 'Creando...';
|
||||
|
||||
// Create persona with all admin permissions from PERMS object
|
||||
var allPerms = Object.keys(PERMS).join(',') + ',';
|
||||
var personaId = safeuuid('admin-');
|
||||
var persona = {
|
||||
Nombre: nombre,
|
||||
Roles: allPerms,
|
||||
Region: '',
|
||||
Monedero_Balance: 0,
|
||||
markdown: 'Cuenta de administrador creada durante el onboarding',
|
||||
};
|
||||
|
||||
DB.put('personas', personaId, persona)
|
||||
.then(() => {
|
||||
toastr.success('¡Cuenta creada exitosamente! 🎉');
|
||||
localStorage.setItem('TELESEC_ONBOARDING_COMPLETE', 'true');
|
||||
localStorage.setItem('TELESEC_ADMIN_ID', personaId);
|
||||
|
||||
// Auto-login
|
||||
SUB_LOGGED_IN_ID = personaId;
|
||||
SUB_LOGGED_IN_DETAILS = persona;
|
||||
SUB_LOGGED_IN = true;
|
||||
SetPages();
|
||||
|
||||
setTimeout(() => {
|
||||
open_page('index');
|
||||
setUrlHash('index');
|
||||
}, 500);
|
||||
})
|
||||
.catch((e) => {
|
||||
toastr.error('Error creando cuenta: ' + (e.message || e));
|
||||
// Re-enable button on error
|
||||
btnElement.disabled = false;
|
||||
btnElement.style.opacity = '1';
|
||||
btnElement.innerText = 'Crear cuenta y empezar';
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
edit: function (mid) {
|
||||
// Handle onboarding routes
|
||||
if (mid === 'onboarding-config') {
|
||||
PAGES.login.onboarding('config');
|
||||
return;
|
||||
}
|
||||
if (mid === 'onboarding-persona') {
|
||||
PAGES.login.onboarding('persona');
|
||||
return;
|
||||
}
|
||||
// Setup form to configure CouchDB remote and initial group/secret
|
||||
var field_couch = safeuuid();
|
||||
var field_secret = safeuuid();
|
||||
var btn_save = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Configuración del servidor CouchDB</h1>
|
||||
<b
|
||||
>Aviso: Después de guardar, la aplicación intentará sincronizar con el servidor CouchDB en
|
||||
segundo plano. Puede que falten registros hasta que se termine. Tenga paciencia.</b
|
||||
>
|
||||
<fieldset>
|
||||
<label
|
||||
>Origen CouchDB (ej: usuario:contraseña@servidor/basededatos)
|
||||
<input
|
||||
type="text"
|
||||
id="${field_couch}"
|
||||
value="${makeCouchURLDisplay(
|
||||
localStorage.getItem('TELESEC_COUCH_URL'),
|
||||
localStorage.getItem('TELESEC_COUCH_USER'),
|
||||
localStorage.getItem('TELESEC_COUCH_PASS'),
|
||||
localStorage.getItem('TELESEC_COUCH_DBNAME')
|
||||
)}"
|
||||
/><br /><br />
|
||||
</label>
|
||||
<label
|
||||
>Clave de encriptación (opcional) - usada para cifrar datos en reposo
|
||||
<input
|
||||
type="password"
|
||||
id="${field_secret}"
|
||||
value="${localStorage.getItem('TELESEC_SECRET') || ''}"
|
||||
/><br /><br />
|
||||
</label>
|
||||
<button id="${btn_save}" class="btn5">Guardar y Conectar</button>
|
||||
<button onclick="setUrlHash('login');" class="btn3">Cancelar</button>
|
||||
</fieldset>
|
||||
<p>
|
||||
Después de guardar, el navegador intentará sincronizar en segundo plano con el servidor.
|
||||
</p>
|
||||
`;
|
||||
// Helper: normalize and apply config object
|
||||
function applyConfig(cfg) {
|
||||
try {
|
||||
if (!cfg) throw new Error('JSON vacío');
|
||||
var url = cfg.server || cfg.couch || cfg.url || cfg.host || cfg.hostname || cfg.server_url;
|
||||
var dbname = cfg.dbname || cfg.database || cfg.db || cfg.name;
|
||||
var user = cfg.username || cfg.user || cfg.u;
|
||||
var pass = cfg.password || cfg.pass || cfg.p;
|
||||
var secret = (cfg.secret || cfg.key || cfg.secretKey || cfg.SECRET || '').toString();
|
||||
if (!url) throw new Error('Falta campo "server" en JSON');
|
||||
var URL_PARSED = parseURL(url);
|
||||
var host = URL_PARSED.hostname || url;
|
||||
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
|
||||
if (dbname) localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||
if (user) localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||
if (pass) localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||
if (secret) {
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
SECRET = secret.toUpperCase();
|
||||
}
|
||||
DB.init({
|
||||
secret: SECRET,
|
||||
remoteServer: 'https://' + url.replace(/^https?:\/\//, ''),
|
||||
username: user,
|
||||
password: pass,
|
||||
dbname: dbname || undefined,
|
||||
});
|
||||
toastr.success('Configuración aplicada e iniciando sincronización');
|
||||
location.hash = '#login';
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 400);
|
||||
} catch (e) {
|
||||
toastr.error('Error aplicando configuración: ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
document.getElementById(btn_save).onclick = () => {
|
||||
var url = document.getElementById(field_couch).value.trim();
|
||||
var secret = document.getElementById(field_secret).value.trim();
|
||||
var URL_PARSED = parseURL(url);
|
||||
var host = URL_PARSED.hostname || url;
|
||||
var user = URL_PARSED.username || '';
|
||||
var pass = URL_PARSED.password || '';
|
||||
var dbname = URL_PARSED.pathname ? URL_PARSED.pathname.replace(/^\//, '') : '';
|
||||
localStorage.setItem('TELESEC_COUCH_URL', 'https://' + host);
|
||||
localStorage.setItem('TELESEC_COUCH_DBNAME', dbname);
|
||||
localStorage.setItem('TELESEC_COUCH_USER', user);
|
||||
localStorage.setItem('TELESEC_COUCH_PASS', pass);
|
||||
localStorage.setItem('TELESEC_SECRET', secret.toUpperCase());
|
||||
SECRET = secret.toUpperCase();
|
||||
try {
|
||||
DB.init({
|
||||
secret: SECRET,
|
||||
remoteServer: 'https://' + host,
|
||||
username: user,
|
||||
password: pass,
|
||||
dbname: dbname || undefined,
|
||||
});
|
||||
toastr.success('Iniciando sincronización con CouchDB');
|
||||
location.hash = '#login';
|
||||
//location.reload();
|
||||
} catch (e) {
|
||||
toastr.error('Error al iniciar sincronización: ' + e.message);
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function (mid) {
|
||||
// Check if onboarding is needed
|
||||
var onboardingComplete = localStorage.getItem('TELESEC_ONBOARDING_COMPLETE');
|
||||
var hasPersonas = Object.keys(SC_Personas).length > 0;
|
||||
|
||||
// If no personas exist and onboarding not complete, redirect to onboarding
|
||||
if (!hasPersonas && !onboardingComplete && !AC_BYPASS) {
|
||||
open_page('login,onboarding-config');
|
||||
setUrlHash('login,onboarding-config');
|
||||
return;
|
||||
}
|
||||
|
||||
var field_persona = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_reload = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Iniciar sesión</h1>
|
||||
<fieldset>
|
||||
<legend>Valores</legend>
|
||||
<input type="hidden" id="${field_persona}" />
|
||||
<div id="${div_actions}"></div>
|
||||
<button class="btn5" id="${btn_guardar}">Acceder</button>
|
||||
<button class="btn3" id="${btn_reload}">Recargar lista</button>
|
||||
<a class="button btn1" href="#login,setup">Configurar base de datos</a>
|
||||
</fieldset>
|
||||
`;
|
||||
var divact = document.getElementById(div_actions);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
'',
|
||||
(value) => {
|
||||
document.getElementById(field_persona).value = value;
|
||||
},
|
||||
'¿Quién eres?',
|
||||
true,
|
||||
"- Pulsa recargar o rellena los credenciales abajo, si quieres crear un nuevo grupo, pulsa el boton 'Desde cero' -"
|
||||
);
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
if (document.getElementById(field_persona).value == '') {
|
||||
alert('Tienes que elegir tu cuenta!');
|
||||
return;
|
||||
}
|
||||
SUB_LOGGED_IN_ID = document.getElementById(field_persona).value;
|
||||
SUB_LOGGED_IN_DETAILS = SC_Personas[SUB_LOGGED_IN_ID];
|
||||
SUB_LOGGED_IN = true;
|
||||
SetPages();
|
||||
if (location.hash.replace('#', '').split("?")[0].startsWith('login')) {
|
||||
open_page('index');
|
||||
setUrlHash('index');
|
||||
} else {
|
||||
open_page(location.hash.replace('#', '').split("?")[0]);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById(btn_reload).onclick = () => {
|
||||
open_page('login');
|
||||
};
|
||||
|
||||
// AC_BYPASS: allow creating a local persona from the login screen
|
||||
if (AC_BYPASS) {
|
||||
var btn_bypass_create = safeuuid();
|
||||
divact.innerHTML += `<button id="${btn_bypass_create}" class="btn2" style="margin-left:10px;">Crear persona local (bypass)</button>`;
|
||||
document.getElementById(btn_bypass_create).onclick = () => {
|
||||
var name = prompt('Nombre de la persona (ej: Admin):');
|
||||
if (!name) return;
|
||||
var id = 'bypass-' + Date.now();
|
||||
var persona = { Nombre: name, Roles: 'ADMIN,' };
|
||||
DB.put('personas', id, persona)
|
||||
.then(() => {
|
||||
toastr.success('Persona creada: ' + id);
|
||||
localStorage.setItem('TELESEC_BYPASS_ID', id);
|
||||
SUB_LOGGED_IN_ID = id;
|
||||
SUB_LOGGED_IN_DETAILS = persona;
|
||||
SUB_LOGGED_IN = true;
|
||||
SetPages();
|
||||
open_page('index');
|
||||
})
|
||||
.catch((e) => {
|
||||
toastr.error('Error creando persona: ' + (e && e.message ? e.message : e));
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
652
src/page/materiales.js
Normal file
@@ -0,0 +1,652 @@
|
||||
PERMS['materiales'] = 'Almacén';
|
||||
PERMS['materiales:edit'] = '> Editar';
|
||||
PAGES.materiales = {
|
||||
navcss: 'btn2',
|
||||
icon: 'static/appico/shelf.png',
|
||||
AccessControl: true,
|
||||
Title: 'Almacén',
|
||||
edit: function (mid) {
|
||||
if (!checkRole('materiales:edit')) {
|
||||
setUrlHash('materiales');
|
||||
return;
|
||||
}
|
||||
var nameh1 = safeuuid();
|
||||
var field_nombre = safeuuid();
|
||||
var field_revision = safeuuid();
|
||||
var field_cantidad = safeuuid();
|
||||
var field_unidad = safeuuid();
|
||||
var field_cantidad_min = safeuuid();
|
||||
var field_ubicacion = safeuuid();
|
||||
var field_notas = safeuuid();
|
||||
var mov_tipo = safeuuid();
|
||||
var mov_cantidad = safeuuid();
|
||||
var mov_nota = safeuuid();
|
||||
var mov_btn = safeuuid();
|
||||
var mov_registro = safeuuid();
|
||||
var mov_chart = safeuuid();
|
||||
var mov_chart_canvas = safeuuid();
|
||||
var mov_open_modal_btn = safeuuid();
|
||||
var btn_print_chart = safeuuid();
|
||||
var mov_modal = safeuuid();
|
||||
var mov_modal_close = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var FECHA_ISO = new Date().toISOString().split('T')[0];
|
||||
var movimientos = [];
|
||||
var movimientosChartInstance = null;
|
||||
|
||||
function parseNum(v, fallback = 0) {
|
||||
var n = parseFloat(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function buildMaterialData() {
|
||||
return {
|
||||
Nombre: document.getElementById(field_nombre).value,
|
||||
Unidad: document.getElementById(field_unidad).value,
|
||||
Cantidad: document.getElementById(field_cantidad).value,
|
||||
Cantidad_Minima: document.getElementById(field_cantidad_min).value,
|
||||
Ubicacion: document.getElementById(field_ubicacion).value,
|
||||
Revision: document.getElementById(field_revision).value,
|
||||
Notas: document.getElementById(field_notas).value,
|
||||
Movimientos: movimientos,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMovimientos() {
|
||||
var el = document.getElementById(mov_registro);
|
||||
if (!el) return;
|
||||
if (!movimientos.length) {
|
||||
el.innerHTML = '<small>Sin movimientos registrados.</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = movimientos
|
||||
.map((mov) => {
|
||||
var fecha = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
|
||||
return html`<tr>
|
||||
<td>${fecha}</td>
|
||||
<td>${mov.Tipo || '-'}</td>
|
||||
<td>${mov.Cantidad ?? '-'}</td>
|
||||
<td>${mov.Antes ?? '-'}</td>
|
||||
<td>${mov.Despues ?? '-'}</td>
|
||||
<td>${mov.Nota || ''}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
el.innerHTML = html`
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Tipo</th>
|
||||
<th>Cantidad</th>
|
||||
<th>Antes</th>
|
||||
<th>Después</th>
|
||||
<th>Nota</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMovimientosChart() {
|
||||
var el = document.getElementById(mov_chart);
|
||||
if (!el) return;
|
||||
|
||||
if (movimientosChartInstance) {
|
||||
movimientosChartInstance.destroy();
|
||||
movimientosChartInstance = null;
|
||||
}
|
||||
|
||||
if (!movimientos.length) {
|
||||
el.innerHTML = html`
|
||||
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
|
||||
<small>Sin datos para graficar.</small>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
var ordered = [...movimientos].sort((a, b) => {
|
||||
return new Date(a.Fecha || 0).getTime() - new Date(b.Fecha || 0).getTime();
|
||||
});
|
||||
|
||||
var deltas = [];
|
||||
var labelsShort = [];
|
||||
|
||||
ordered.forEach((mov) => {
|
||||
var cantidad = parseNum(mov.Cantidad, 0);
|
||||
var delta = 0;
|
||||
if (mov.Tipo === 'Entrada') {
|
||||
delta = cantidad;
|
||||
} else if (mov.Tipo === 'Salida') {
|
||||
delta = -cantidad;
|
||||
} else {
|
||||
var antes = parseNum(mov.Antes, 0);
|
||||
var despues = parseNum(mov.Despues, antes);
|
||||
delta = despues - antes;
|
||||
}
|
||||
deltas.push(Number(delta.toFixed(2)));
|
||||
|
||||
var fechaTxt = mov.Fecha ? new Date(mov.Fecha).toLocaleString('es-ES') : '-';
|
||||
labelsShort.push(fechaTxt);
|
||||
});
|
||||
|
||||
var currentStock = parseNum(document.getElementById(field_cantidad)?.value, 0);
|
||||
var totalNeto = deltas.reduce((acc, n) => acc + n, 0);
|
||||
var stockInicialInferido = currentStock - totalNeto;
|
||||
|
||||
if (ordered.length > 0 && Number.isFinite(parseNum(ordered[0].Antes, NaN))) {
|
||||
stockInicialInferido = parseNum(ordered[0].Antes, stockInicialInferido);
|
||||
}
|
||||
|
||||
var acumulado = stockInicialInferido;
|
||||
var values = deltas.map((neto) => {
|
||||
acumulado += neto;
|
||||
return Number(acumulado.toFixed(2));
|
||||
});
|
||||
|
||||
el.innerHTML = html`
|
||||
<h3 style="margin: 0 0 8px 0;">Historial de movimientos por fecha</h3>
|
||||
<small style="display: block;margin-bottom: 6px;">Stock por fecha (cierre diario)</small>
|
||||
<canvas id="${mov_chart_canvas}" style="width: 100%;height: 280px;"></canvas>
|
||||
`;
|
||||
|
||||
if (typeof Chart === 'undefined') {
|
||||
el.innerHTML += '<small>No se pudo cargar la librería de gráficos.</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
var chartCanvasEl = document.getElementById(mov_chart_canvas);
|
||||
if (!chartCanvasEl) return;
|
||||
|
||||
movimientosChartInstance = new Chart(chartCanvasEl, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labelsShort,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Stock diario',
|
||||
data: values,
|
||||
borderColor: '#2d7ef7',
|
||||
backgroundColor: 'rgba(45,126,247,0.16)',
|
||||
fill: true,
|
||||
tension: 0.25,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Stock',
|
||||
},
|
||||
grace: '10%',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
container.innerHTML = html`
|
||||
<h1>Material <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="width: 100%;max-width: 980px;box-sizing: border-box;">
|
||||
<div style="display: flex;flex-wrap: wrap;gap: 10px 16px;align-items: flex-end;">
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||
<label for="${field_revision}">Fecha Revisión</label>
|
||||
<input type="date" id="${field_revision}" style="flex: 1;" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||
<label for="${field_nombre}">Nombre</label>
|
||||
<input type="text" id="${field_nombre}" style="flex: 1;" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||
<label for="${field_ubicacion}">Ubicación</label>
|
||||
<input
|
||||
type="text"
|
||||
id="${field_ubicacion}"
|
||||
value="-"
|
||||
list="${field_ubicacion}_list"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
<datalist id="${field_ubicacion}_list"></datalist>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||
<label for="${field_unidad}">Unidad</label>
|
||||
<select id="${field_unidad}" style="flex: 1;">
|
||||
<option value="unidad(es)">unidad(es)</option>
|
||||
<option value="paquete(s)">paquete(s)</option>
|
||||
<option value="caja(s)">caja(s)</option>
|
||||
<option value="rollo(s)">rollo(s)</option>
|
||||
<option value="bote(s)">bote(s)</option>
|
||||
|
||||
<option value="metro(s)">metro(s)</option>
|
||||
<option value="litro(s)">litro(s)</option>
|
||||
<option value="kg">kg</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||
<label for="${field_cantidad}">Cantidad Actual</label>
|
||||
<input type="number" step="0.5" id="${field_cantidad}" style="flex: 1;" disabled />
|
||||
</div>
|
||||
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 220px;flex: 1 1 280px;">
|
||||
<label for="${field_cantidad_min}">Cantidad Minima</label>
|
||||
<input type="number" step="0.5" id="${field_cantidad_min}" style="flex: 1;" />
|
||||
</div>
|
||||
|
||||
<label style="display: flex;flex-direction: column;gap: 6px;min-width: 220px;flex: 1 1 100%;">
|
||||
Notas
|
||||
<textarea id="${field_notas}"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="${mov_modal}"
|
||||
style="display: none;position: fixed;z-index: 9999;left: 0;top: 0;width: 100%;height: 100%;overflow: auto;background: rgba(0,0,0,0.45);"
|
||||
>
|
||||
<div
|
||||
style="background: #fff;margin: 2vh auto;padding: 14px;border: 1px solid #888;width: min(960px, 96vw);max-height: 94vh;overflow: auto;border-radius: 8px;box-sizing: border-box;"
|
||||
>
|
||||
<div style="display: flex;justify-content: space-between;align-items: center;gap: 10px;">
|
||||
<h3 style="margin: 0;">Realizar movimiento</h3>
|
||||
<button type="button" id="${mov_modal_close}" class="rojo">Cerrar</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 12px;display: flex;gap: 10px;align-items: flex-end;flex-wrap: wrap;">
|
||||
<div style="display: flex;flex-wrap: wrap;gap: 10px 12px;align-items: flex-end;flex: 1 1 420px;">
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
||||
<label for="${mov_tipo}">Tipo</label>
|
||||
<select id="${mov_tipo}" style="flex: 1;">
|
||||
<option value="Entrada">Entrada - Meter al almacen</option>
|
||||
<option value="Salida">Salida - Sacar del almacen</option>
|
||||
<option value="Ajuste">Ajuste - Existencias actuales</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
||||
<label for="${mov_cantidad}">Cantidad</label>
|
||||
<input type="number" step="0.5" id="${mov_cantidad}" style="flex: 1;" />
|
||||
</div>
|
||||
<div style="display: flex;flex-direction: column;align-items: stretch;gap: 6px;min-width: 180px;flex: 1 1 220px;">
|
||||
<label for="${mov_nota}">Nota</label>
|
||||
<input type="text" id="${mov_nota}" style="flex: 1;" placeholder="Motivo del movimiento" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;justify-content: flex-end;flex: 1 1 120px;min-width: 120px;">
|
||||
<button type="button" class="saveico" id="${mov_btn}">
|
||||
<img src="static/floppy_disk_green.png" />
|
||||
<br>Guardar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 14px 0 6px 0;">Registro de movimientos</h4>
|
||||
<div id="${mov_registro}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<button class="saveico" id="${btn_guardar}">
|
||||
<img src="static/floppy_disk_green.png" />
|
||||
<br>Guardar
|
||||
</button>
|
||||
<button class="delico" id="${btn_borrar}">
|
||||
<img src="static/garbage.png" />
|
||||
<br>Borrar
|
||||
</button>
|
||||
<button class="opicon" id="${mov_open_modal_btn}">
|
||||
<img src="static/exchange.png" />
|
||||
<br>Movimientos
|
||||
</button>
|
||||
<button class="opicon" onclick="setUrlHash('materiales')" style="float: right;"> <!-- Align to the right -->
|
||||
<img src="static/exit.png" />
|
||||
<br>Salir
|
||||
</button>
|
||||
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||
<img src="static/printer2.png" />
|
||||
<br>Imprimir
|
||||
</button>
|
||||
</fieldset>
|
||||
<div id="${mov_chart}" style="max-width: 980px;width: 100%;margin-top: 14px;min-height: 260px;height: min(400px, 52vh);"></div>
|
||||
`;
|
||||
// Cargar ubicaciones existentes para autocompletar
|
||||
DB.map('materiales', (data) => {
|
||||
if (!data) return;
|
||||
function addUbicacion(d) {
|
||||
const ubicacion = d.Ubicacion || '-';
|
||||
const datalist = document.getElementById(`${field_ubicacion}_list`);
|
||||
if (!datalist) {
|
||||
console.warn(`Element with ID "${field_ubicacion}_list" not found.`);
|
||||
return;
|
||||
}
|
||||
const optionExists = Array.from(datalist.options).some((opt) => opt.value === ubicacion);
|
||||
if (!optionExists) {
|
||||
const option = document.createElement('option');
|
||||
option.value = ubicacion;
|
||||
datalist.appendChild(option);
|
||||
}
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
TS_decrypt(
|
||||
data,
|
||||
SECRET,
|
||||
(data, wasEncrypted) => {
|
||||
if (data && typeof data === 'object') {
|
||||
addUbicacion(data);
|
||||
}
|
||||
},
|
||||
'materiales',
|
||||
mid
|
||||
);
|
||||
} else {
|
||||
addUbicacion(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar datos del material
|
||||
DB.get('materiales', mid).then((data) => {
|
||||
function load_data(data, ENC = '') {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_nombre).value = data['Nombre'] || '';
|
||||
document.getElementById(field_unidad).value = data['Unidad'] || 'unidad(es)';
|
||||
document.getElementById(field_cantidad).value = data['Cantidad'] || '';
|
||||
document.getElementById(field_cantidad_min).value = data['Cantidad_Minima'] || '';
|
||||
document.getElementById(field_ubicacion).value = data['Ubicacion'] || '-';
|
||||
document.getElementById(field_revision).value = data['Revision'] || '-';
|
||||
document.getElementById(field_notas).value = data['Notas'] || '';
|
||||
movimientos = Array.isArray(data['Movimientos']) ? data['Movimientos'] : [];
|
||||
renderMovimientos();
|
||||
renderMovimientosChart();
|
||||
}
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(
|
||||
data,
|
||||
SECRET,
|
||||
(data, wasEncrypted) => {
|
||||
load_data(data, '%E');
|
||||
},
|
||||
'materiales',
|
||||
mid
|
||||
);
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById(mov_open_modal_btn).onclick = () => {
|
||||
document.getElementById(mov_modal).style.display = 'block';
|
||||
renderMovimientos();
|
||||
};
|
||||
|
||||
document.getElementById(mov_modal_close).onclick = () => {
|
||||
document.getElementById(mov_modal).style.display = 'none';
|
||||
};
|
||||
|
||||
document.getElementById(mov_modal).onclick = (evt) => {
|
||||
if (evt.target.id === mov_modal) {
|
||||
document.getElementById(mov_modal).style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById(mov_btn).onclick = () => {
|
||||
var btn = document.getElementById(mov_btn);
|
||||
if (btn.disabled) return;
|
||||
|
||||
var tipo = document.getElementById(mov_tipo).value;
|
||||
var cantidadMov = parseNum(document.getElementById(mov_cantidad).value, NaN);
|
||||
var nota = document.getElementById(mov_nota).value || '';
|
||||
var actual = parseNum(document.getElementById(field_cantidad).value, 0);
|
||||
|
||||
if ((!Number.isFinite(cantidadMov) || cantidadMov <= 0) && tipo !== 'Ajuste') {
|
||||
toastr.warning('Indica una cantidad válida para el movimiento');
|
||||
return;
|
||||
}
|
||||
|
||||
var nuevaCantidad = actual;
|
||||
if (tipo === 'Entrada') {
|
||||
nuevaCantidad = actual + cantidadMov;
|
||||
} else if (tipo === 'Salida') {
|
||||
nuevaCantidad = actual - cantidadMov;
|
||||
} else if (tipo === 'Ajuste') {
|
||||
nuevaCantidad = cantidadMov;
|
||||
}
|
||||
|
||||
movimientos.unshift({
|
||||
Fecha: new Date().toISOString(),
|
||||
Tipo: tipo,
|
||||
Cantidad: cantidadMov,
|
||||
Antes: actual,
|
||||
Despues: nuevaCantidad,
|
||||
Nota: nota,
|
||||
});
|
||||
|
||||
document.getElementById(field_cantidad).value = nuevaCantidad;
|
||||
document.getElementById(field_revision).value = FECHA_ISO;
|
||||
document.getElementById(mov_cantidad).value = '';
|
||||
document.getElementById(mov_nota).value = '';
|
||||
renderMovimientos();
|
||||
renderMovimientosChart();
|
||||
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.5';
|
||||
document.getElementById('actionStatus').style.display = 'block';
|
||||
DB.put('materiales', mid, buildMaterialData())
|
||||
.then(() => {
|
||||
toastr.success('Movimiento registrado');
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
toastr.error('Error al guardar el movimiento');
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = '0.5';
|
||||
|
||||
var data = buildMaterialData();
|
||||
document.getElementById('actionStatus').style.display = 'block';
|
||||
DB.put('materiales', mid, data)
|
||||
.then(() => {
|
||||
toastr.success('Guardado!');
|
||||
setTimeout(() => {
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
setUrlHash('materiales');
|
||||
}, SAVE_WAIT);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = '1';
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
toastr.error('Error al guardar el material');
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm('¿Quieres borrar este material?') == true) {
|
||||
DB.del('materiales', mid).then(() => {
|
||||
toastr.error('Borrado!');
|
||||
setTimeout(() => {
|
||||
setUrlHash('materiales');
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
if (!checkRole('materiales')) {
|
||||
setUrlHash('index');
|
||||
return;
|
||||
}
|
||||
var btn_new = safeuuid();
|
||||
var select_ubicacion = safeuuid();
|
||||
var check_lowstock = safeuuid();
|
||||
var tableContainer = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Materiales del Almacén</h1>
|
||||
<label>
|
||||
<b>Solo lo que falta:</b>
|
||||
<input type="checkbox" id="${check_lowstock}" style="height: 25px;width: 25px;" /> </label
|
||||
><br />
|
||||
<label
|
||||
>Filtrar por ubicación:
|
||||
<select id="${select_ubicacion}">
|
||||
<option value="">(Todas)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="${btn_new}">Nuevo Material</button>
|
||||
<div id="${tableContainer}"></div>
|
||||
`;
|
||||
|
||||
const config = [
|
||||
{ key: 'Revision', label: 'Ult. Revisión', type: 'fecha-diff', default: '' },
|
||||
{ key: 'Nombre', label: 'Nombre', type: 'text', default: '' },
|
||||
{ key: 'Ubicacion', label: 'Ubicación', type: 'text', default: '--' },
|
||||
{
|
||||
key: 'Cantidad',
|
||||
label: 'Cantidad',
|
||||
type: 'template',
|
||||
template: (data, element) => {
|
||||
const min = parseFloat(data.Cantidad_Minima);
|
||||
const act = parseFloat(data.Cantidad);
|
||||
const sma = act < min ? `<small>- min. ${data.Cantidad_Minima || '?'}</small>` : '';
|
||||
element.innerHTML = html`${data.Cantidad || '?'} ${data.Unidad || '?'} ${sma}`;
|
||||
},
|
||||
default: '?',
|
||||
},
|
||||
{ key: 'Notas', label: 'Notas', type: 'text', default: '' },
|
||||
];
|
||||
|
||||
// Obtener todas las ubicaciones únicas y poblar el <select>, desencriptando si es necesario
|
||||
DB.map('materiales', (data, key) => {
|
||||
try {
|
||||
if (!data) return;
|
||||
|
||||
function addUbicacion(d) {
|
||||
const ubicacion = d.Ubicacion || '-';
|
||||
const select = document.getElementById(select_ubicacion);
|
||||
|
||||
if (!select) {
|
||||
console.warn(`Element with ID "${select_ubicacion}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const optionExists = Array.from(select.options).some((opt) => opt.value === ubicacion);
|
||||
if (!optionExists) {
|
||||
const option = document.createElement('option');
|
||||
option.value = ubicacion;
|
||||
option.textContent = ubicacion;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
TS_decrypt(
|
||||
data,
|
||||
SECRET,
|
||||
(dec, wasEncrypted) => {
|
||||
if (dec && typeof dec === 'object') {
|
||||
addUbicacion(dec);
|
||||
}
|
||||
},
|
||||
'materiales',
|
||||
key
|
||||
);
|
||||
} else {
|
||||
addUbicacion(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error processing ubicacion:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Función para renderizar la tabla filtrada
|
||||
function renderTable(filtroUbicacion) {
|
||||
TS_IndexElement(
|
||||
'materiales',
|
||||
config,
|
||||
'materiales',
|
||||
document.getElementById(tableContainer),
|
||||
function (data, new_tr) {
|
||||
if (parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima)) {
|
||||
new_tr.style.background = '#fcfcb0';
|
||||
}
|
||||
if (parseFloat(data.Cantidad) <= 0) {
|
||||
new_tr.style.background = '#ffc0c0';
|
||||
}
|
||||
if ((data.Cantidad || '?') == '?') {
|
||||
new_tr.style.background = '#d0d0ff';
|
||||
}
|
||||
if ((data.Revision || '?') == '?') {
|
||||
new_tr.style.background = '#d0d0ff';
|
||||
}
|
||||
},
|
||||
function (data) {
|
||||
var is_low_stock =
|
||||
!document.getElementById(check_lowstock).checked ||
|
||||
parseFloat(data.Cantidad) < parseFloat(data.Cantidad_Minima);
|
||||
|
||||
var is_region = filtroUbicacion === '' || data.Ubicacion === filtroUbicacion;
|
||||
|
||||
return !(is_low_stock && is_region);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Inicializar tabla sin filtro
|
||||
renderTable('');
|
||||
|
||||
// Evento para filtrar por ubicación
|
||||
document.getElementById(select_ubicacion).onchange = function () {
|
||||
renderTable(this.value);
|
||||
};
|
||||
// Recargar al cambiar filtro
|
||||
document.getElementById(check_lowstock).onchange = function () {
|
||||
renderTable(document.getElementById(select_ubicacion).value);
|
||||
};
|
||||
|
||||
if (!checkRole('materiales:edit')) {
|
||||
document.getElementById(btn_new).style.display = 'none';
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash('materiales,' + safeuuid(''));
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
312
src/page/notas.js
Normal file
@@ -0,0 +1,312 @@
|
||||
PERMS['notas'] = 'Notas';
|
||||
PERMS['notas:edit'] = '> Editar';
|
||||
PAGES.notas = {
|
||||
navcss: 'btn5',
|
||||
icon: 'static/appico/edit.png',
|
||||
AccessControl: true,
|
||||
Title: 'Notas',
|
||||
edit: function (mid) {
|
||||
if (!checkRole('notas:edit')) {
|
||||
setUrlHash('notas');
|
||||
return;
|
||||
}
|
||||
var nameh1 = safeuuid();
|
||||
var field_asunto = safeuuid();
|
||||
var field_contenido = safeuuid();
|
||||
var field_autor = safeuuid();
|
||||
var field_files = safeuuid();
|
||||
var attachments_list = safeuuid();
|
||||
var btn_guardar = safeuuid();
|
||||
var btn_borrar = safeuuid();
|
||||
var div_actions = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Nota <code id="${nameh1}"></code></h1>
|
||||
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
|
||||
<legend>Valores</legend>
|
||||
<div style="max-width: 400px;">
|
||||
<label>
|
||||
Asunto<br />
|
||||
<input type="text" id="${field_asunto}" value="" /><br /><br />
|
||||
</label>
|
||||
<input type="hidden" id="${field_autor}" value="" />
|
||||
<div id="${div_actions}"></div>
|
||||
</div>
|
||||
<label>
|
||||
Contenido<br />
|
||||
<textarea
|
||||
id="${field_contenido}"
|
||||
style="width: calc(100% - 15px); height: 400px;"
|
||||
></textarea
|
||||
><br /><br />
|
||||
</label>
|
||||
<label>
|
||||
Adjuntos (Fotos o archivos)<br />
|
||||
<input type="file" id="${field_files}" multiple /><br /><br />
|
||||
<div id="${attachments_list}"></div>
|
||||
</label>
|
||||
<hr />
|
||||
<button class="saveico" id="${btn_guardar}">
|
||||
<img src="static/floppy_disk_green.png" />
|
||||
<br>Guardar
|
||||
</button>
|
||||
<button class="delico" id="${btn_borrar}">
|
||||
<img src="static/garbage.png" />
|
||||
<br>Borrar
|
||||
</button>
|
||||
<button class="opicon" onclick="setUrlHash('notas')" style="float: right;"> <!-- Align to the right -->
|
||||
<img src="static/exit.png" />
|
||||
<br>Salir
|
||||
</button>
|
||||
<button class="opicon" onclick="window.print()" style="float: right;"> <!-- Align to the right -->
|
||||
<img src="static/printer2.png" />
|
||||
<br>Imprimir
|
||||
</button>
|
||||
</fieldset>
|
||||
`;
|
||||
var divact = document.getElementById(div_actions);
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
SUB_LOGGED_IN_ID,
|
||||
(value) => {
|
||||
document.getElementById(field_autor).value = value;
|
||||
},
|
||||
'Autor'
|
||||
);
|
||||
DB.get('notas', mid).then((data) => {
|
||||
function load_data(data, ENC = '') {
|
||||
document.getElementById(nameh1).innerText = mid;
|
||||
document.getElementById(field_asunto).value = data['Asunto'] || '';
|
||||
document.getElementById(field_contenido).value = data['Contenido'] || '';
|
||||
document.getElementById(field_autor).value = data['Autor'] || SUB_LOGGED_IN_ID || '';
|
||||
|
||||
// Persona select
|
||||
divact.innerHTML = '';
|
||||
addCategory_Personas(
|
||||
divact,
|
||||
SC_Personas,
|
||||
data['Autor'] || SUB_LOGGED_IN_ID || '',
|
||||
(value) => {
|
||||
document.getElementById(field_autor).value = value;
|
||||
},
|
||||
'Autor'
|
||||
);
|
||||
// Mostrar adjuntos existentes (si los hay).
|
||||
// No confiar en `data._attachments` porque `DB.get` devuelve solo `doc.data`.
|
||||
const attachContainer = document.getElementById(attachments_list);
|
||||
attachContainer.innerHTML = '';
|
||||
// Usar API de DB para listar attachments (no acceder a internals desde la UI)
|
||||
DB.listAttachments('notas', mid)
|
||||
.then((list) => {
|
||||
if (!list || !Array.isArray(list)) return;
|
||||
list.forEach((att) => {
|
||||
addAttachmentRow(att.name, att.dataUrl);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('listAttachments error', e);
|
||||
});
|
||||
}
|
||||
if (typeof data == 'string') {
|
||||
TS_decrypt(data, SECRET, (data) => {
|
||||
load_data(data, '%E');
|
||||
});
|
||||
} else {
|
||||
load_data(data || {});
|
||||
}
|
||||
});
|
||||
// gestión de archivos seleccionados antes de guardar
|
||||
const attachmentsToUpload = [];
|
||||
function addAttachmentRow(name, url) {
|
||||
const attachContainer = document.getElementById(attachments_list);
|
||||
const idRow = safeuuid();
|
||||
const isImage = url && url.indexOf('data:image') === 0;
|
||||
const preview = isImage
|
||||
? `<img src="${url}" height="80" style="margin-right:8px;">`
|
||||
: `<a href="${url}" target="_blank">${name}</a>`;
|
||||
const html = `
|
||||
<div id="${idRow}" style="display:flex;align-items:center;margin:6px 0;border:1px solid #ddd;padding:6px;border-radius:6px;">
|
||||
<div style="flex:1">${preview}<strong style="margin-left:8px">${name}</strong></div>
|
||||
<div><button type="button" class="rojo" data-name="${name}">Borrar</button></div>
|
||||
</div>`;
|
||||
attachContainer.insertAdjacentHTML('beforeend', html);
|
||||
attachContainer.querySelectorAll(`button[data-name="${name}"]`).forEach((btn) => {
|
||||
btn.onclick = () => {
|
||||
if (!confirm('¿Borrar este adjunto?')) return;
|
||||
// Usar API pública en DB para borrar metadata del attachment
|
||||
DB.deleteAttachment('notas', mid, name)
|
||||
.then((ok) => {
|
||||
if (ok) {
|
||||
document.getElementById(idRow).remove();
|
||||
toastr.error('Adjunto borrado');
|
||||
} else {
|
||||
toastr.error('No se pudo borrar el adjunto');
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('deleteAttachment error', e);
|
||||
toastr.error('Error borrando adjunto');
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById(field_files).addEventListener('change', function (e) {
|
||||
const files = Array.from(e.target.files || []);
|
||||
files.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
const dataUrl = ev.target.result;
|
||||
attachmentsToUpload.push({
|
||||
name: file.name,
|
||||
data: dataUrl,
|
||||
type: file.type || 'application/octet-stream',
|
||||
});
|
||||
// mostrar preview temporal
|
||||
addAttachmentRow(file.name, dataUrl);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
// limpiar input para permitir re-subidas del mismo archivo
|
||||
e.target.value = '';
|
||||
});
|
||||
document.getElementById(btn_guardar).onclick = () => {
|
||||
// Disable button to prevent double-clicking
|
||||
var guardarBtn = document.getElementById(btn_guardar);
|
||||
if (guardarBtn.disabled) return;
|
||||
|
||||
guardarBtn.disabled = true;
|
||||
guardarBtn.style.opacity = '0.5';
|
||||
|
||||
var data = {
|
||||
Autor: document.getElementById(field_autor).value,
|
||||
Contenido: document.getElementById(field_contenido).value,
|
||||
Asunto: document.getElementById(field_asunto).value,
|
||||
};
|
||||
document.getElementById('actionStatus').style.display = 'block';
|
||||
DB.put('notas', mid, data)
|
||||
.then(() => {
|
||||
// subir attachments si los hay
|
||||
const uploadPromises = [];
|
||||
attachmentsToUpload.forEach((att) => {
|
||||
if (DB.putAttachment) {
|
||||
uploadPromises.push(
|
||||
DB.putAttachment('notas', mid, att.name, att.data, att.type).catch((e) => {
|
||||
console.warn('putAttachment error', e);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
Promise.all(uploadPromises)
|
||||
.then(() => {
|
||||
// limpiar lista temporal y recargar attachments
|
||||
attachmentsToUpload.length = 0;
|
||||
try {
|
||||
// recargar lista actual sin salir
|
||||
const pouchId = 'notas:' + mid;
|
||||
if (DB && DB._internal && DB._internal.local) {
|
||||
DB._internal.local
|
||||
.get(pouchId, { attachments: true })
|
||||
.then((doc) => {
|
||||
const attachContainer = document.getElementById(attachments_list);
|
||||
attachContainer.innerHTML = '';
|
||||
if (doc && doc._attachments) {
|
||||
Object.keys(doc._attachments).forEach((name) => {
|
||||
try {
|
||||
const att = doc._attachments[name];
|
||||
if (att && att.data) {
|
||||
const durl =
|
||||
'data:' +
|
||||
(att.content_type || 'application/octet-stream') +
|
||||
';base64,' +
|
||||
att.data;
|
||||
addAttachmentRow(name, durl);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
DB.getAttachment('notas', mid, name)
|
||||
.then((durl) => {
|
||||
addAttachmentRow(name, durl);
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* ignore reload errors */
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
toastr.success('Guardado!');
|
||||
setTimeout(() => {
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
setUrlHash('notas');
|
||||
}, SAVE_WAIT);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Attachment upload error', e);
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = '1';
|
||||
toastr.error('Error al guardar los adjuntos');
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('DB.put error', e);
|
||||
document.getElementById('actionStatus').style.display = 'none';
|
||||
guardarBtn.disabled = false;
|
||||
guardarBtn.style.opacity = '1';
|
||||
toastr.error('Error al guardar la nota');
|
||||
});
|
||||
};
|
||||
document.getElementById(btn_borrar).onclick = () => {
|
||||
if (confirm('¿Quieres borrar esta nota?') == true) {
|
||||
DB.del('notas', mid).then(() => {
|
||||
toastr.error('Borrado!');
|
||||
setTimeout(() => {
|
||||
setUrlHash('notas');
|
||||
}, SAVE_WAIT);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
index: function () {
|
||||
if (!checkRole('notas')) {
|
||||
setUrlHash('index');
|
||||
return;
|
||||
}
|
||||
const tablebody = safeuuid();
|
||||
var btn_new = safeuuid();
|
||||
container.innerHTML = html`
|
||||
<h1>Notas</h1>
|
||||
<button id="${btn_new}">Nueva nota</button>
|
||||
<div id="cont"></div>
|
||||
`;
|
||||
TS_IndexElement(
|
||||
'notas',
|
||||
[
|
||||
{
|
||||
key: 'Autor',
|
||||
type: 'persona-nombre',
|
||||
default: '',
|
||||
label: 'Autor',
|
||||
},
|
||||
{
|
||||
key: 'Asunto',
|
||||
type: 'raw',
|
||||
default: '',
|
||||
label: 'Asunto',
|
||||
},
|
||||
],
|
||||
'notas',
|
||||
document.querySelector('#cont')
|
||||
);
|
||||
if (!checkRole('notas:edit')) {
|
||||
document.getElementById(btn_new).style.display = 'none';
|
||||
} else {
|
||||
document.getElementById(btn_new).onclick = () => {
|
||||
setUrlHash('notas,' + safeuuid(''));
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||