├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── JavaScript ├── api │ ├── about.js │ ├── contacts.js │ ├── fields.js │ ├── links.js │ ├── stack.js │ └── team.js ├── server.js └── static │ ├── 404.html │ ├── console.css │ ├── console.js │ ├── favicon.ico │ ├── favicon.png │ ├── index.html │ ├── manifest.json │ ├── metarhia.png │ ├── metarhia.svg │ └── worker.js ├── LICENSE └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.js,*.mjs,*.ts,*.json,*.yml}] 11 | indent_size = 2 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | }, 11 | "globals": { 12 | "BigInt": true 13 | }, 14 | "rules": { 15 | "indent": [ 16 | "error", 17 | 2 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "no-loop-func": [ 32 | "error" 33 | ], 34 | "block-spacing": [ 35 | "error", 36 | "always" 37 | ], 38 | "camelcase": [ 39 | "error" 40 | ], 41 | "eqeqeq": [ 42 | "error", 43 | "always" 44 | ], 45 | "strict": [ 46 | "error", 47 | "global" 48 | ], 49 | "brace-style": [ 50 | "error", 51 | "1tbs", 52 | { 53 | "allowSingleLine": true 54 | } 55 | ], 56 | "comma-style": [ 57 | "error", 58 | "last" 59 | ], 60 | "comma-spacing": [ 61 | "error", 62 | { 63 | "before": false, 64 | "after": true 65 | } 66 | ], 67 | "eol-last": [ 68 | "error" 69 | ], 70 | "func-call-spacing": [ 71 | "error", 72 | "never" 73 | ], 74 | "key-spacing": [ 75 | "error", 76 | { 77 | "beforeColon": false, 78 | "afterColon": true, 79 | "mode": "minimum" 80 | } 81 | ], 82 | "keyword-spacing": [ 83 | "error", 84 | { 85 | "before": true, 86 | "after": true, 87 | "overrides": { 88 | "function": { 89 | "after": false 90 | } 91 | } 92 | } 93 | ], 94 | "max-len": [ 95 | "error", 96 | { 97 | "code": 80, 98 | "ignoreUrls": true 99 | } 100 | ], 101 | "max-nested-callbacks": [ 102 | "error", 103 | { 104 | "max": 7 105 | } 106 | ], 107 | "new-cap": [ 108 | "error", 109 | { 110 | "newIsCap": true, 111 | "capIsNew": false, 112 | "properties": true 113 | } 114 | ], 115 | "new-parens": [ 116 | "error" 117 | ], 118 | "no-lonely-if": [ 119 | "error" 120 | ], 121 | "no-trailing-spaces": [ 122 | "error" 123 | ], 124 | "no-unneeded-ternary": [ 125 | "error" 126 | ], 127 | "no-whitespace-before-property": [ 128 | "error" 129 | ], 130 | "object-curly-spacing": [ 131 | "error", 132 | "always" 133 | ], 134 | "operator-assignment": [ 135 | "error", 136 | "always" 137 | ], 138 | "operator-linebreak": [ 139 | "error", 140 | "after" 141 | ], 142 | "semi-spacing": [ 143 | "error", 144 | { 145 | "before": false, 146 | "after": true 147 | } 148 | ], 149 | "space-before-blocks": [ 150 | "error", 151 | "always" 152 | ], 153 | "space-before-function-paren": [ 154 | "error", 155 | { 156 | "anonymous": "never", 157 | "named": "never", 158 | "asyncArrow": "always" 159 | } 160 | ], 161 | "space-in-parens": [ 162 | "error", 163 | "never" 164 | ], 165 | "space-infix-ops": [ 166 | "error" 167 | ], 168 | "space-unary-ops": [ 169 | "error", 170 | { 171 | "words": true, 172 | "nonwords": false, 173 | "overrides": { 174 | "typeof": false 175 | } 176 | } 177 | ], 178 | "no-unreachable": [ 179 | "error" 180 | ], 181 | "no-global-assign": [ 182 | "error" 183 | ], 184 | "no-self-compare": [ 185 | "error" 186 | ], 187 | "no-unmodified-loop-condition": [ 188 | "error" 189 | ], 190 | "no-constant-condition": [ 191 | "error", 192 | { 193 | "checkLoops": false 194 | } 195 | ], 196 | "no-console": [ 197 | "off" 198 | ], 199 | "no-useless-concat": [ 200 | "error" 201 | ], 202 | "no-useless-escape": [ 203 | "error" 204 | ], 205 | "no-shadow-restricted-names": [ 206 | "error" 207 | ], 208 | "no-use-before-define": [ 209 | "error", 210 | { 211 | "functions": false 212 | } 213 | ], 214 | "arrow-parens": [ 215 | "error", 216 | "always" 217 | ], 218 | "arrow-body-style": [ 219 | "error", 220 | "as-needed" 221 | ], 222 | "arrow-spacing": [ 223 | "error" 224 | ], 225 | "no-confusing-arrow": [ 226 | "error", 227 | { 228 | "allowParens": true 229 | } 230 | ], 231 | "no-useless-computed-key": [ 232 | "error" 233 | ], 234 | "no-useless-rename": [ 235 | "error" 236 | ], 237 | "no-var": [ 238 | "error" 239 | ], 240 | "object-shorthand": [ 241 | "error", 242 | "always" 243 | ], 244 | "prefer-arrow-callback": [ 245 | "error" 246 | ], 247 | "prefer-const": [ 248 | "error" 249 | ], 250 | "prefer-numeric-literals": [ 251 | "error" 252 | ], 253 | "prefer-rest-params": [ 254 | "error" 255 | ], 256 | "prefer-spread": [ 257 | "error" 258 | ], 259 | "rest-spread-spacing": [ 260 | "error", 261 | "never" 262 | ], 263 | "template-curly-spacing": [ 264 | "error", 265 | "never" 266 | ], 267 | "consistent-return": [ 268 | "error", 269 | { "treatUndefinedAsUnspecified": true } 270 | ] 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: tshemsedinov 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /JavaScript/api/about.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async () => [ 4 | 'Metarhia is a Community and Technology Stack', 5 | 'for Distributed Highload Applications and Data Storage', 6 | '', 7 | 'Activities:', 8 | '• Academic fields: Research, Education and Open Lectures', 9 | '• Open Source Contribution e.g. Node.js, Impress, Metasync, etc.', 10 | '• Services and Products', 11 | '', 12 | 'Metarhia provides following services:', 13 | '• Software Development', 14 | '• Software Audit, Quality Control and Code Review', 15 | '• Business Processes Analysis', 16 | '• Architecture Solutions and Consulting', 17 | '• Database structure and technical specification', 18 | '• Project planning: time and cost estimation', 19 | '• Education, Training, Team building and Recruiting', 20 | '', 21 | 'Metarhia is a group of IT professionals, located in Kiev (Ukraine)', 22 | 'and working together in software development, internet solutions', 23 | 'and production automation. We are experienced in development and', 24 | 'system integration, ours architects are over 20 years in information', 25 | 'technologies. Ours software developers have practical knowledge in', 26 | 'programming including C, C++, JavaScript, Rust, Go, Swift, Java,', 27 | 'Objective-C, Kotlin, C#, Delphi, Assembler, Python, Haskell, etc.', 28 | 'We provide solutions for Unix/Linux, Windows, OSX, Android, Internet', 29 | 'solutions and Embedded systems.' 30 | ]; 31 | -------------------------------------------------------------------------------- /JavaScript/api/contacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async () => [ 4 | 'Timur Shemsedinov', 5 | 'tshemsedinov@github', 6 | 'timur.shemsedinov@gmail.com', 7 | 'tshemsedinov@facebook', 8 | 'marcusaurelius@habrahabr', 9 | ]; 10 | -------------------------------------------------------------------------------- /JavaScript/api/fields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async () => [ 4 | 'Software fields:', 5 | '• Highload applications, Scaling and Performance optimization', 6 | '• Corporate applications, Databases and Information Systems', 7 | '• Systems Architecture and System Integration', 8 | '• Interactive applications, games and second screen TV solutions', 9 | '• Clusterware and massively parallel distributed cluster/cloud', 10 | '• Big-data and big-memory solutions', 11 | '• Deep learning, Neural networks, Data analysis', 12 | '• Production automation, cybernetics, telemetry', 13 | '• Network applications and protocols', 14 | '• Mobile, Desktop and Web GUI Applications', 15 | '• Embedded systems, Hardware and System Programming', 16 | '', 17 | 'Applied fields:', 18 | '• eGovernance Information Systems', 19 | '• Health Information Systems', 20 | '• Financial analytics and Trading Systems', 21 | '• Asset tracking solutions, RFID & GPS navigation', 22 | '• RFID (Radio Frequency IDentification)', 23 | '• Oil and gas transporting automation and telemetry', 24 | '• Social networking and Messaging solutions', 25 | '• Document flow automation, Timing and Planning solutions', 26 | '• Expert systems and CAD/CAM software', 27 | ]; 28 | -------------------------------------------------------------------------------- /JavaScript/api/links.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async () => [ 4 | 'Links:', 5 | '• Open Source code https://github.com/metarhia', 6 | '• Educational code Examples https://github.com/HowProgrammingWorks', 7 | '', 8 | 'Meetup Groups:', 9 | '• https://www.meetup.com/HowProgrammingWorks', 10 | '• https://www.meetup.com/NodeUA', 11 | '• https://www.meetup.com/KievNodeJS', 12 | '', 13 | 'Telegram Channels:', 14 | '• https://t.me/HowProgrammingWorks', 15 | '• https://t.me/metarhia', 16 | '', 17 | 'Telegram Groups:', 18 | '• https://t.me/MetarhiaHPW', 19 | '• https://t.me/nodeua', 20 | ]; 21 | -------------------------------------------------------------------------------- /JavaScript/api/stack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async () => [ 4 | 'Metarhia Technology Stack Key Ideas:', 5 | '• Unification: API, data, contracts', 6 | '• Homogeneity of server infrastructure', 7 | '• No back compatibility (no legacy support)', 8 | '• Open Source & no vendor lock', 9 | '• Architectural decisions (single plan)', 10 | '• Community and trainings', 11 | '', 12 | 'Impress Application Server', 13 | 'for highload clusters and private clouds with Node.js', 14 | 'https://github.com/metarhia/impress', 15 | '', 16 | 'JSTP protocol', 17 | 'RPC, event bus, db sync on TCP, TLS, WebSocket and JSON5;', 18 | 'with SDK for mobile and desktop: JavaScript, Java, Swift,', 19 | 'C++, Haskell, Python, Objective-C, PHP, Golang, C#', 20 | 'https://github.com/metarhia/jstp', 21 | '', 22 | 'GlobalStorage', 23 | 'Distributed reactive in-memory DBMS, work in progress', 24 | 'https://github.com/metarhia/globalstorage', 25 | '', 26 | 'Metasync: asynchronous programming abstractions', 27 | 'https://github.com/metarhia/metasync', 28 | '', 29 | 'Maojian: testing framework', 30 | 'https://github.com/metarhia/maojian', 31 | ]; 32 | -------------------------------------------------------------------------------- /JavaScript/api/team.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async () => [ 4 | 'Metarhia core team:', 5 | '@tshemsedinov @aqrln @belochub @nechaido @GYFK @lidaamber', 6 | '@lundibundi @GreatAndPowerfulKing @grimelion @DzyubSpirit', 7 | '@RayGron @alinkedd @j-martyn @johnbizokk @bugagashenkj', 8 | '@Kowalski0805 @dimanadko @kuvichkamaksim @mille-nium', 9 | '@o-rumiantsev @Tariod', 10 | ]; 11 | -------------------------------------------------------------------------------- /JavaScript/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs'); 4 | const http = require('node:http'); 5 | const path = require('node:path'); 6 | 7 | const PORT = 8000; 8 | 9 | const STATIC_PATH = path.join(process.cwd(), './static'); 10 | const API_PATH = './api/'; 11 | 12 | const MIME_TYPES = { 13 | default: 'application/octet-stream', 14 | html: 'text/html; charset=UTF-8', 15 | js: 'application/javascript; charset=UTF-8', 16 | json: 'application/json', 17 | css: 'text/css', 18 | png: 'image/png', 19 | jpg: 'image/jpg', 20 | gif: 'image/gif', 21 | ico: 'image/x-icon', 22 | svg: 'image/svg+xml', 23 | }; 24 | 25 | const toBool = [() => true, () => false]; 26 | 27 | const prepareFile = async (url) => { 28 | const paths = [STATIC_PATH, url]; 29 | if (url.endsWith('/')) paths.push('index.html'); 30 | const filePath = path.join(...paths); 31 | const pathTraversal = !filePath.startsWith(STATIC_PATH); 32 | const exists = await fs.promises.access(filePath).then(...toBool); 33 | const found = !pathTraversal && exists; 34 | const streamPath = found ? filePath : STATIC_PATH + '/404.html'; 35 | const ext = path.extname(streamPath).substring(1).toLowerCase(); 36 | const stream = fs.createReadStream(streamPath); 37 | return { found, ext, stream }; 38 | }; 39 | 40 | const api = new Map(); 41 | 42 | const receiveArgs = async (req) => { 43 | const buffers = []; 44 | for await (const chunk of req) buffers.push(chunk); 45 | const data = Buffer.concat(buffers).toString(); 46 | return JSON.parse(data); 47 | }; 48 | 49 | const cacheFile = (name) => { 50 | const filePath = API_PATH + name; 51 | const key = path.basename(filePath, '.js'); 52 | try { 53 | const libPath = require.resolve(filePath); 54 | delete require.cache[libPath]; 55 | } catch { 56 | return; 57 | } 58 | try { 59 | const method = require(filePath); 60 | api.set(key, method); 61 | } catch { 62 | api.delete(name); 63 | } 64 | }; 65 | 66 | const cacheFolder = (path) => { 67 | fs.readdir(path, (err, files) => { 68 | if (!err) files.forEach(cacheFile); 69 | }); 70 | }; 71 | 72 | const watch = (path) => { 73 | fs.watch(path, (event, file) => { 74 | cacheFile(file); 75 | }); 76 | }; 77 | 78 | cacheFolder(API_PATH); 79 | watch(API_PATH); 80 | 81 | const httpError = (res, status, message) => { 82 | res.statusCode = status; 83 | res.end(`"${message}"`); 84 | }; 85 | 86 | http.createServer(async (req, res) => { 87 | const [first, second] = req.url.substring(1).split('/'); 88 | if (first === 'api') { 89 | const method = api.get(second); 90 | const args = await receiveArgs(req); 91 | try { 92 | const result = await method(...args); 93 | if (!result) { 94 | httpError(res, 500, 'Server error'); 95 | return; 96 | } 97 | res.end(JSON.stringify(result)); 98 | } catch (err) { 99 | console.dir({ err }); 100 | httpError(res, 500, 'Server error'); 101 | } 102 | } else { 103 | const file = await prepareFile(req.url); 104 | const statusCode = file.found ? 200 : 404; 105 | const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default; 106 | res.writeHead(statusCode, { 'Content-Type': mimeType }); 107 | file.stream.pipe(res); 108 | console.log(`${req.method} ${req.url} ${statusCode}`); 109 | } 110 | }).listen(PORT); 111 | 112 | console.log(`Server running at http://127.0.0.1:${PORT}/`); 113 | -------------------------------------------------------------------------------- /JavaScript/static/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error 404: File not found 6 | 7 | 8 |

Error 404: File not found

9 | 10 | 11 | -------------------------------------------------------------------------------- /JavaScript/static/console.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | outline: none; 5 | cursor: default; 6 | -moz-user-select: inherit; 7 | } 8 | a, a * { 9 | cursor: pointer; 10 | } 11 | html, body { 12 | padding: 0; 13 | margin: 0; 14 | font-family: 'Share Tech Mono', monospace; 15 | font-size: 11pt; 16 | overflow: hidden; 17 | height: 100%; 18 | background: #000; 19 | color: #FFF; 20 | } 21 | body { 22 | height: 100%; 23 | } 24 | input, textarea { 25 | -webkit-border-radius: 0; 26 | -webkit-touch-callout: text; 27 | -webkit-user-select: text; 28 | -khtml-user-select: text; 29 | -moz-user-select: text; 30 | -ms-user-select: text; 31 | user-select: text; 32 | } 33 | ul,ol { 34 | padding: 0 0 0 30px; 35 | } 36 | #screenConsole { 37 | height: 100%; 38 | } 39 | #screenConsole > div { 40 | overflow: hidden; 41 | } 42 | #panelColors { 43 | height: 100%; 44 | width: 8px; 45 | float: left; 46 | background: #FF0000; 47 | } 48 | .colorA { background: #AA0077; height: 40px; } 49 | .colorB { background: #552277; } 50 | .colorC { background: #2222BB; } 51 | .colorD { background: #0033DD; } 52 | .colorE { background: #006699; } 53 | .colorF { background: #009933; } 54 | .colorG { background: #7FD900; } 55 | .colorH { background: #F2F200; } 56 | .colorI { background: #FFBF00; } 57 | .colorJ { background: #FF8C00; } 58 | .colorK { background: #FF3300; } 59 | .colorL { background: #FF0000; height: 40px; } 60 | #panelLogo { 61 | margin: 4px; 62 | position: absolute; 63 | top: 0; 64 | left: 8px; 65 | right: 8px; 66 | background: #000; 67 | } 68 | #panelConsole { 69 | position: absolute; 70 | left: 8px; 71 | right: 8px; 72 | top: 8px; 73 | bottom: 0; 74 | } 75 | #panelScroll { 76 | height: 100%; 77 | width: 8px; 78 | float: right; 79 | background: #262626; 80 | } 81 | #controlScroll { 82 | background: #009933; 83 | position: absolute; 84 | width: 8px; 85 | height: 25px; 86 | bottom: 0px; 87 | } 88 | #panelColors > div { 89 | height: 25pt; 90 | } 91 | #controlShadow { 92 | position: absolute; 93 | top: -10px; 94 | left: 0; 95 | right: 0; 96 | height: 10px; 97 | box-shadow: 0px 0px 20px 6px rgba(0,0,0,1); 98 | z-index: 1; 99 | } 100 | #controlBrowse { 101 | padding: 4px; 102 | color: #009933; 103 | overflow-x: hidden; 104 | overflow-y: scroll; 105 | position: absolute; 106 | left: 0; 107 | right: 0; 108 | top: 0; 109 | bottom: 0; 110 | } 111 | #controlBrowse td { 112 | padding: 2px 4px; 113 | } 114 | #controlBrowse th { 115 | padding: 2px 4px; 116 | font-weight: bold; 117 | color: #FFBF00; 118 | } 119 | #controlBrowse tr:nth-child(even) { 120 | background: #101010; 121 | } 122 | #controlBrowse tr:nth-child(odd) { 123 | background: #262626; 124 | } 125 | #controlInput { 126 | color: #009933; 127 | } 128 | #controlInput span { 129 | animation: blinker 1s ease-out infinite; 130 | margin-left: 2px; 131 | } 132 | @keyframes blinker { 133 | 50% { opacity: 0.0; } 134 | } 135 | #controlKeyboard { 136 | background: #000; 137 | position: absolute; 138 | bottom: 0; 139 | right: 0; 140 | left: 0; 141 | } 142 | #controlKeyboard .key { 143 | font-size: 1.4em; 144 | background: #262626; 145 | display: inline-block; 146 | text-align: center; 147 | width: 10%; 148 | height: 25px; 149 | } 150 | .caps { 151 | text-transform: uppercase; 152 | } 153 | ::-webkit-scrollbar { 154 | display: none; 155 | } 156 | #controlBrowseSpacer { 157 | height: 100%; 158 | } 159 | -------------------------------------------------------------------------------- /JavaScript/static/console.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('JavaScript has loaded'); 4 | 5 | const registerServiceWorker = () => { 6 | if (!Reflect.has(navigator, 'serviceWorker')) { 7 | console.log('Service workers are not supported'); 8 | return; 9 | } 10 | const { serviceWorker } = navigator; 11 | serviceWorker.register('/worker.js').then((registration) => { 12 | if (registration.installing) { 13 | console.log('Service worker installing'); 14 | console.log(registration.installing); 15 | return; 16 | } 17 | if (registration.waiting) { 18 | console.log('Service worker installed'); 19 | console.log(registration.waiting); 20 | return; 21 | } 22 | if (registration.active) { 23 | console.log('Service worker active'); 24 | console.log(registration.active); 25 | return; 26 | } 27 | }).catch((error) => { 28 | console.log('Registration failed'); 29 | console.log(error); 30 | }); 31 | }; 32 | 33 | window.addEventListener('load', () => { 34 | console.log('The page has loaded'); 35 | registerServiceWorker(); 36 | }); 37 | 38 | window.addEventListener('beforeinstallprompt', (event) => { 39 | console.log('Installing PWA'); 40 | console.dir({ beforeinstallprompt: event }); 41 | }); 42 | 43 | window.addEventListener('appinstalled', (event) => { 44 | console.log('PWA installed'); 45 | console.dir({ appinstalled: event }); 46 | }); 47 | 48 | // AJAX API Builder 49 | 50 | const buildAPI = (methods) => { 51 | const api = {}; 52 | for (const method of methods) { 53 | api[method] = (...args) => new Promise((resolve, reject) => { 54 | const url = `/api/${method}`; 55 | console.log(url, args); 56 | fetch(url, { 57 | method: 'POST', 58 | headers: { 'Content-Type': 'application/json' }, 59 | body: JSON.stringify(args), 60 | }).then((res) => { 61 | const { status } = res; 62 | if (status !== 200) { 63 | reject(new Error(`Status Code: ${status}`)); 64 | return; 65 | } 66 | resolve(res.json()); 67 | }); 68 | }); 69 | } 70 | return api; 71 | }; 72 | 73 | // Console Emulation 74 | 75 | const ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 76 | const ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'; 77 | const ALPHA = ALPHA_UPPER + ALPHA_LOWER; 78 | const DIGIT = '0123456789'; 79 | const CHARS = ALPHA + DIGIT; 80 | const TIME_LINE = 300; 81 | const TIME_CHAR = 20; 82 | 83 | const KEY_CODE = { 84 | BACKSPACE: 8, TAB: 9, ENTER: 13, PAUSE: 19, ESC: 27, SPACE: 32, 85 | PGUP: 33, PGDN: 34, END: 35, HOME: 36, 86 | LT: 37, UP: 38, RT: 39, DN: 40, INS: 45, DEL: 46, 87 | F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, 88 | F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123, 89 | ACCENT: 192, 90 | }; 91 | 92 | const KEY_NAME = {}; 93 | for (const keyName in KEY_CODE) KEY_NAME[KEY_CODE[keyName]] = keyName; 94 | 95 | let controlKeyboard, panelScroll; 96 | let controlInput, controlBrowse, controlScroll; 97 | 98 | const api = buildAPI(['about']); 99 | 100 | const pad = (padChar, length) => new Array(length + 1).join(padChar); 101 | 102 | const isMobile = () => ( 103 | navigator.userAgent.match(/Android/i) || 104 | navigator.userAgent.match(/webOS/i) || 105 | navigator.userAgent.match(/iPhone/i) || 106 | navigator.userAgent.match(/iPad/i) || 107 | navigator.userAgent.match(/iPod/i) || 108 | navigator.userAgent.match(/BlackBerry/i) || 109 | navigator.userAgent.match(/Windows Phone/i) 110 | ); 111 | 112 | let viewportHeight, viewableRatio; 113 | let contentHeight, scrollHeight; 114 | let thumbHeight, thumbPosition; 115 | 116 | const refreshScroll = () => { 117 | viewportHeight = controlBrowse.offsetHeight; 118 | contentHeight = controlBrowse.scrollHeight; 119 | viewableRatio = viewportHeight / contentHeight; 120 | scrollHeight = panelScroll.offsetHeight; 121 | thumbHeight = scrollHeight * viewableRatio; 122 | thumbPosition = controlBrowse.scrollTop * thumbHeight / viewportHeight; 123 | controlScroll.style.top = thumbPosition + 'px'; 124 | controlScroll.style.height = thumbHeight + 'px'; 125 | }; 126 | 127 | const scrollBottom = () => { 128 | refreshScroll(); 129 | controlBrowse.scrollTop = controlBrowse.scrollHeight; 130 | }; 131 | 132 | const initScroll = () => { 133 | controlBrowse.scrollTop = controlBrowse.scrollHeight; 134 | controlBrowse.addEventListener('scroll', refreshScroll); 135 | window.addEventListener('orientationchange', () => { 136 | setTimeout(scrollBottom, 0); 137 | }); 138 | }; 139 | 140 | const showKeyboard = () => { 141 | if (!isMobile()) return; 142 | controlKeyboard.style.display = 'block'; 143 | controlBrowse.style.bottom = controlKeyboard.offsetHeight + 'px'; 144 | }; 145 | 146 | const inputSetValue = (value) => { 147 | controlInput.inputValue = value; 148 | if (controlInput.inputType === 'masked') { 149 | value = pad('*', value.length); 150 | } 151 | value = value.replace(/ /g, ' '); 152 | controlInput.innerHTML = ( 153 | controlInput.inputPrompt + value + '' 154 | ); 155 | }; 156 | 157 | const input = (type, prompt, callback) => { 158 | showKeyboard(); 159 | controlInput.style.display = 'none'; 160 | controlBrowse.removeChild(controlInput); 161 | controlInput.inputActive = true; 162 | controlInput.inputPrompt = prompt; 163 | inputSetValue(''); 164 | controlInput.inputType = type; 165 | controlInput.inputCallback = callback; 166 | controlBrowse.appendChild(controlInput); 167 | controlInput.style.display = 'block'; 168 | setTimeout(scrollBottom, 0); 169 | }; 170 | 171 | const clear = () => { 172 | const elements = controlBrowse.children; 173 | let element; 174 | for (let i = elements.length - 2; i > 1; i--) { 175 | element = elements[i]; 176 | controlBrowse.removeChild(element); 177 | } 178 | }; 179 | 180 | const print = (s) => { 181 | const list = Array.isArray(s); 182 | let line = list ? s.shift() : s; 183 | if (!line) line = ''; 184 | const element = document.createElement('div'); 185 | if (!line) line = '\xa0'; 186 | if (line.charAt(0) === '<') { 187 | element.innerHTML += line; 188 | } else { 189 | const timer = setInterval(() => { 190 | const char = line.charAt(0); 191 | element.innerHTML += char; 192 | line = line.substr(1); 193 | if (!line) clearInterval(timer); 194 | controlBrowse.scrollTop = controlBrowse.scrollHeight; 195 | scrollBottom(); 196 | }, TIME_CHAR); 197 | } 198 | if (list && s.length) setTimeout(print, TIME_LINE, s); 199 | controlBrowse.insertBefore(element, controlInput); 200 | controlBrowse.scrollTop = controlBrowse.scrollHeight; 201 | scrollBottom(); 202 | }; 203 | 204 | const enterKey = () => { 205 | input('masked', 'Key: ', (err, key) => { 206 | api.signin({ key }, (err, data) => { 207 | if (data.result === 'ok') { 208 | print('You are logged in'); 209 | } else { 210 | print('Incorect key'); 211 | enterKey(); 212 | } 213 | }); 214 | }); 215 | }; 216 | 217 | const format = (obj) => { 218 | let res = ''; 219 | let key, val; 220 | for (key in obj) { 221 | val = obj[key]; 222 | res += ``; 223 | } 224 | return res + '
ParameterValue
${key}${val}
'; 225 | }; 226 | 227 | const inputKeyboardEvents = { 228 | ESC() { 229 | inputSetValue(''); 230 | }, 231 | BACKSPACE() { 232 | let value = controlInput.inputValue; 233 | value = value.slice(0, -1); 234 | inputSetValue(value); 235 | }, 236 | ENTER() { 237 | const result = controlInput.inputValue; 238 | let value = result; 239 | if (controlInput.inputType === 'masked') { 240 | value = pad('*', value.length); 241 | } 242 | print(controlInput.inputPrompt + value); 243 | controlInput.style.display = 'none'; 244 | controlInput.inputActive = false; 245 | controlInput.inputCallback(null, value); 246 | }, 247 | CAPS() { 248 | if (controlKeyboard.className === 'caps') { 249 | controlKeyboard.className = ''; 250 | } else { 251 | controlKeyboard.className = 'caps'; 252 | } 253 | }, 254 | KEY(char) { // Alpha or Digit 255 | if (controlKeyboard.className === 'caps') { 256 | char = char.toUpperCase(); 257 | } 258 | let value = controlInput.inputValue; 259 | value += char; 260 | inputSetValue(value); 261 | } 262 | }; 263 | 264 | const makeKeyboardClick = (char) => (e) => { 265 | char = e.target.inputChar; 266 | if (char === '_') char = ' '; 267 | let keyName = 'KEY'; 268 | if (char === '<') keyName = 'BACKSPACE'; 269 | if (char === '>') keyName = 'ENTER'; 270 | if (char === '^') keyName = 'CAPS'; 271 | const fn = inputKeyboardEvents[keyName]; 272 | if (fn) fn(char); 273 | e.stopPropagation(); 274 | return false; 275 | }; 276 | 277 | const initKeyboard = () => { 278 | if (!isMobile()) return; 279 | controlKeyboard.style.display = 'block'; 280 | const KEYBOARD_LAYOUT = [ 281 | '1234567890', 282 | 'qwertyuiop', 283 | 'asdfghjkl<', 284 | '^zxcvbnm_>' 285 | ]; 286 | let i, j, char, keyboardClick; 287 | let keyboardLine, elementKey, elementLine; 288 | for (i = 0; i < KEYBOARD_LAYOUT.length; i++) { 289 | keyboardLine = KEYBOARD_LAYOUT[i]; 290 | elementLine = document.createElement('div'); 291 | controlKeyboard.appendChild(elementLine); 292 | for (j = 0; j < keyboardLine.length; j++) { 293 | char = keyboardLine[j]; 294 | if (char === ' ') char = ' '; 295 | elementKey = document.createElement('div'); 296 | elementKey.innerHTML = char; 297 | elementKey.inputChar = char; 298 | elementKey.className = 'key'; 299 | elementKey.style.opacity = ((i + j) % 2) ? 0.8 : 1; 300 | keyboardClick = makeKeyboardClick(char); 301 | elementKey.addEventListener('click', keyboardClick); 302 | elementLine.appendChild(elementKey); 303 | } 304 | } 305 | controlBrowse.style.bottom = controlKeyboard.offsetHeight + 'px'; 306 | }; 307 | 308 | document.onkeydown = (event) => { 309 | let keyName, fn; 310 | if (controlInput.inputActive) { 311 | keyName = KEY_NAME[event.keyCode]; 312 | fn = inputKeyboardEvents[keyName]; 313 | if (fn) { 314 | fn(); 315 | return false; 316 | } 317 | } 318 | }; 319 | 320 | document.onkeypress = (event) => { 321 | if (controlInput.inputActive) { 322 | const fn = inputKeyboardEvents['KEY']; 323 | const char = String.fromCharCode(event.keyCode); 324 | if (CHARS.includes(char) && fn) { 325 | fn(char); 326 | return false; 327 | } 328 | } 329 | }; 330 | 331 | const commandLoop = () => { 332 | input('command', '.', (err, line) => { 333 | exec(line); 334 | commandLoop(); 335 | }); 336 | }; 337 | 338 | const commands = {}; 339 | 340 | const help = [ 341 | '', 'Commands: about, fields, team, links, stack, contacts' 342 | ]; 343 | 344 | const exec = async (line) => { 345 | const args = line.split(' '); 346 | const cmd = args.shift(); 347 | const data = await api[cmd](args); 348 | print(data); 349 | commandLoop(); 350 | }; 351 | 352 | window.addEventListener('load', () => { 353 | panelScroll = document.getElementById('panelScroll'); 354 | controlInput = document.getElementById('controlInput'); 355 | controlKeyboard = document.getElementById('controlKeyboard'); 356 | controlBrowse = document.getElementById('controlBrowse'); 357 | controlScroll = document.getElementById('controlScroll'); 358 | initKeyboard(); 359 | initScroll(); 360 | const path = window.location.pathname.substring(1); 361 | print([ 362 | 'Metarhia/KPI is a Research & Development Center', 363 | 'in Kiev Polytechnic Institute (ICT faculty)', 364 | ].concat(help)); 365 | if (path) { 366 | setTimeout(() => { 367 | exec('contacts ' + path); 368 | window.history.replaceState(null, '', '/'); 369 | }, TIME_LINE * 3); 370 | } 371 | commandLoop(); 372 | }); 373 | -------------------------------------------------------------------------------- /JavaScript/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowProgrammingWorks/ServiceWorker/95c326cb2d28995ee98822484b4faee21288a0e4/JavaScript/static/favicon.ico -------------------------------------------------------------------------------- /JavaScript/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowProgrammingWorks/ServiceWorker/95c326cb2d28995ee98822484b4faee21288a0e4/JavaScript/static/favicon.png -------------------------------------------------------------------------------- /JavaScript/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Metarhia Console 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /JavaScript/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Metarhia", 3 | "name": "Metarhia console", 4 | "icons": [ 5 | { 6 | "src": "/favicon.png", 7 | "type": "image/png", 8 | "sizes": "128x128" 9 | }, 10 | { 11 | "src": "/metarhia.png", 12 | "type": "image/png", 13 | "sizes": "256x256" 14 | } 15 | ], 16 | "start_url": "/", 17 | "background_color": "#552277", 18 | "display": "standalone", 19 | "scope": "/", 20 | "theme_color": "#009933" 21 | } 22 | -------------------------------------------------------------------------------- /JavaScript/static/metarhia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowProgrammingWorks/ServiceWorker/95c326cb2d28995ee98822484b4faee21288a0e4/JavaScript/static/metarhia.png -------------------------------------------------------------------------------- /JavaScript/static/metarhia.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Metarhia 4 | ultimate automation 5 | -------------------------------------------------------------------------------- /JavaScript/static/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const files = [ 4 | '/', 5 | '/console.css', 6 | '/console.js', 7 | '/favicon.ico', 8 | '/favicon.png', 9 | '/manifest.json', 10 | '/metarhia.png', 11 | '/metarhia.svg', 12 | ]; 13 | 14 | self.addEventListener('install', (event) => event.waitUntil( 15 | caches.open('v1').then((cache) => cache.addAll(files)) 16 | )); 17 | 18 | self.addEventListener('fetch', (event) => { 19 | event.respondWith(caches.match(event.request).then((response) => { 20 | if (response !== undefined) return response; 21 | return fetch(event.request).then((response) => { 22 | const responseClone = response.clone(); 23 | caches.open('v1').then((cache) => { 24 | cache.put(event.request, responseClone); 25 | }); 26 | return response; 27 | }).catch((error) => { 28 | throw error; 29 | }); 30 | })); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 How.Programming.Works contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Workers for PWA caching, proxy and offline 2 | 3 | [![Progressive Web Applications PWA и ServiceWorkers](https://img.youtube.com/vi/s7AIwZMTVPs/0.jpg)](https://youtu.be/s7AIwZMTVPs) 4 | --------------------------------------------------------------------------------