190 | 191 | 192 | Port : {{selectedPi.getPort()}} 193 | 194 |
├── LICENSE ├── README.md ├── package.json ├── pi-Communicator.js ├── server.js ├── settings.js └── ui ├── css ├── messenger-theme-air.css ├── messenger.css ├── orka-style.css ├── semantic.min.css └── themes │ ├── basic │ └── assets │ │ └── fonts │ │ ├── icons.eot │ │ ├── icons.svg │ │ ├── icons.ttf │ │ └── icons.woff │ └── default │ └── assets │ ├── fonts │ ├── icons.eot │ ├── icons.otf │ ├── icons.svg │ ├── icons.ttf │ ├── icons.woff │ └── icons.woff2 │ └── images │ └── flags.png ├── index.html ├── js ├── angular.easypiechart.min.js ├── angular.min.js ├── jquery.address.js ├── jquery.min.js ├── logger │ └── orka-ui-logger.js ├── messenger.min.js ├── notifications │ ├── orka-flock-notifier.js │ └── orka-ui-notifier.js ├── orka-IPC-bridge.js ├── orka-angular-controller.js ├── orka-generator-service.js ├── orka-notification-service.js ├── orka-schedular.js └── semantic.min.js └── test.html /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Karthik K 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 | # Orka-Server 2 |
Lists are useful when you want to control a particular group of Pi. You can send commands, run tasks for the particular lists.
43 | 1. Choose Manage Lists from the Home Menu. 44 | 2. Give the name for the list and choose the Pi which has to added to the list in the Modal window. 45 | 3. click add to create the List 46 | 47 | ##### Batch Execute Command 48 | 1. Choose Batch Execute Command from the Home Menu. 49 | 2. Choose either you want to broadcast the command to all the connected Pi or to particular client or lists. 50 | 3. Type the command you want to execute in the command window. 51 | 4. Start. The commands will be send to the clients and the command output is stored at the notification panel. 52 | 53 | ##### Tasks 54 |Tasks are very much useful when you want to run command at particular interval. It can be either a single-shot or repeatative.
55 | 1. Choose Manage Tasks from the Home Menu. 56 | 2. Give a name and interval for the task. 57 | 3. Choose the type either Timeout(single-shot) or Interval (repeatative) 58 | 4. Choose the clients. The clients can be either a single client or a group of clients(lists) 59 | 5. Specify the command to execute 60 | 6. Click Create Task. 61 |The task will not be started unless until it is started from the Task Panel.
62 | 63 | ## Why Orka ? 64 | I am aware that there are lot of tools available to control multiple clients. Most of them are command line tools. I wanted to create a one click access Gui tool to control Multiple clients. And that's why Orka was born. Peace!.... 65 | 66 | ### Caution! 67 | Orka is in Aplha Stage. Many features(like authentication) are yet to be merged. Kindly avoid using it in production environment. 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orka", 3 | "productName": "Orka Server", 4 | "description": "Orka Monitor and Controller", 5 | "version": "0.1.0", 6 | "author": "Karthik K", 7 | "copyright": "MIT License", 8 | "main": "server.js", 9 | "dependencies": { 10 | "electron-settings": "^2.1.1", 11 | "request": "^2.74.0", 12 | "socket.io": "^1.4.8", 13 | "socket.io-client": "^1.4.8" 14 | }, 15 | "scripts": { 16 | "start": "electron .", 17 | "build": "electron-packager . --out=dist --asar --overwrite --all" 18 | }, 19 | "devDependencies": { 20 | "electron-packager": "^8.0.0", 21 | "electron": "^1.0.1", 22 | "eslint": "^3.3.1", 23 | "eslint-config-standard": "^6.0.0-beta.3", 24 | "eslint-plugin-import": "^1.14.0", 25 | "eslint-plugin-promise": "^2.0.1", 26 | "eslint-plugin-react": "^6.1.2", 27 | "eslint-plugin-standard": "^2.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pi-Communicator.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************ 2 | * pi-Communicator.js 3 | * maps socket id to client names 4 | * Singleton class 5 | ***********************************************************************************************/ 6 | 'use strict' 7 | 8 | let PiHTTPHelper = (function () { 9 | const request = require('request') 10 | let instance = null 11 | 12 | // inner class 13 | function initialize () { 14 | var socketIdMap = {} // Map to hold socket id and names 15 | 16 | /** 17 | * connects to remote pi and passes the (reverse) connection information as post data 18 | * @param {string} ip ip to connect 19 | * @param {number} port port 20 | * @param {object} data information about the server to conenct back and monitoring settings 21 | * @return {none} none 22 | */ 23 | function connectToPi (ip, port, data) { 24 | request.post({ 25 | url: `http://${ip}:${port}/connect`, 26 | method: 'POST', 27 | json: true, 28 | body: data 29 | }, function (err) { 30 | console.warn(err) 31 | }) 32 | } 33 | 34 | /** 35 | * converts socket id into name 36 | * @param {string} id [socket id] 37 | * @return {string} name [name associated with the socket id] 38 | */ 39 | function getNameFromSocketId (id) { 40 | for (var socket in socketIdMap) { 41 | if (socketIdMap[socket] === id) { 42 | return socket 43 | } 44 | } 45 | return null 46 | } 47 | 48 | /** 49 | * converts name into socket id 50 | * @param {string} name [name of client] 51 | * @return {string} socket_id [socket id associated with the name] 52 | */ 53 | function getSocketIdFromName (name) { 54 | if (isAvailable(name)) { 55 | return socketIdMap[name] 56 | } 57 | else { 58 | return null 59 | } 60 | } 61 | 62 | function addSocket (name, id) { 63 | socketIdMap[name] = id 64 | } 65 | 66 | function removeSocket (id) { 67 | delete socketIdMap[getNameFromSocketId(id)] 68 | } 69 | 70 | function isAvailable (name) { 71 | return socketIdMap[name] ? true : false 72 | } 73 | 74 | function getAllSocketsName () { 75 | return Object.keys(socketIdMap) 76 | } 77 | 78 | return { 79 | connectToPi, 80 | 81 | getNameFromSocketId, 82 | getSocketIdFromName, 83 | 84 | addSocket, 85 | removeSocket, 86 | 87 | isAvailable, 88 | getAllSocketsName 89 | } 90 | } 91 | 92 | // return Singleton object 93 | return { 94 | getInstance: function () { 95 | if (instance == null) { 96 | instance = initialize() 97 | } 98 | return instance 99 | } 100 | } 101 | 102 | })(); 103 | 104 | module.exports = PiHTTPHelper.getInstance(); 105 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************ 2 | * ORKA Server 3 | * ----------- 4 | * Entry point of orka 5 | * loads ui and opens sockets for client communication. Acts as a bridge between ui and the clients 6 | * 7 | * 8 | ***********************************************************************************************/ 9 | 10 | 'use strict' 11 | 12 | // const electron = require('electron') 13 | const {app, BrowserWindow} = require('electron') 14 | const io = require('socket.io')() 15 | const {ipcMain} = require('electron') 16 | 17 | const settings = require('./settings.js') // Persistent settings storage 18 | const port = settings.getServerOptions().port || 8000 19 | const piSocketManager = require('./pi-Communicator.js') // maps socket id into name and vice versa 20 | 21 | let win = null 22 | let contents = null 23 | 24 | function init () { 25 | win = new BrowserWindow({width: 800, height: 500, show: false}) 26 | win.loadURL(`file://${__dirname}/ui/index.html`) 27 | 28 | win.webContents.openDevTools() 29 | // win.maximize(); 30 | win.setFullScreen(true) 31 | 32 | win.once('closed', () => { 33 | win = null 34 | }) 35 | // wait for electron to load the page 36 | win.once('ready-to-show', () => { 37 | win.show() 38 | }) 39 | contents = win.webContents 40 | } 41 | 42 | (function () { 43 | app.once('ready', init) 44 | io.listen(port) 45 | })() 46 | 47 | app.on('window-all-closed', () => { 48 | app.quit() 49 | }) 50 | io.on('connection', function (socket) { 51 | var clientName = socket.handshake.query.client_name // Client name should be passed during connection 52 | if (clientName === undefined) { 53 | socket.disconnect() 54 | } else { 55 | piSocketManager.addSocket(clientName, socket.id) 56 | 57 | contents.send('setPiConnectionStatus', { 58 | name: clientName, 59 | connected: true 60 | }) 61 | } 62 | 63 | socket.on('disconnect', function () { 64 | if (piSocketManager.getNameFromSocketId(socket.id) != null) { 65 | contents.send('setPiConnectionStatus', { 66 | name: piSocketManager.getNameFromSocketId(socket.id), 67 | connected: false 68 | }) 69 | } 70 | piSocketManager.removeSocket(socket.id) 71 | }) 72 | 73 | /************************************************************************************************ 74 | * 75 | * The following are the events supported by Orka. These events should in sync with orka-IPC-bridge.js. 76 | * 77 | * <- denotes the events coming from client to Orka Server 78 | * -> denotes the events going to the client from the Orka Server 79 | * -- denotes the events which flows through the Orka UI and Orka Server 80 | * 81 | * 82 | ***********************************************************************************************/ 83 | 84 | /** 85 | * Event <- stats; statistics received from client 86 | * @type object 87 | */ 88 | socket.on('stats', function (data) { 89 | var name = piSocketManager.getNameFromSocketId(socket.id) // convert socket id into name 90 | if (!name) { 91 | console.error('un-registered client is sending stats') 92 | } 93 | if (name != null) { 94 | contents.send('stats', { 95 | 'name': name, 96 | 'data': data 97 | }) 98 | } else { 99 | socket.disconnect() 100 | } 101 | }) 102 | 103 | /** 104 | * Event <- output; output of executed command from client. see TaskSchedular and Batch Execution 105 | * @type object 106 | */ 107 | socket.on('output', function (data) { 108 | contents.send('output', { 109 | 'name': piSocketManager.getNameFromSocketId(socket.id), 110 | output: data 111 | }) 112 | }) 113 | 114 | /** 115 | * Event <- alert;generated by the client when the statistics threashold is surpassed 116 | * @type string 117 | */ 118 | socket.on('alert', (data) => { 119 | console.log(data) 120 | contents.send('alert', { 121 | name: piSocketManager.getNameFromSocketId(socket.id), 122 | message: data 123 | }) 124 | }) 125 | 126 | /** 127 | * Event <- systemInfo; basic system information from the client. Obtained at the connecting phase 128 | * @type Object 129 | */ 130 | socket.on('systemInfo', (data) => { 131 | contents.send('systemInfo', { 132 | name: piSocketManager.getNameFromSocketId(socket.id), 133 | data 134 | }) 135 | }) 136 | }) 137 | 138 | /** 139 | * Event -> Command; the command sent to client and executed 140 | * @type string 141 | */ 142 | ipcMain.on('Command', (event, name, command) => { 143 | var socketId = piSocketManager.getSocketIdFromName(name) 144 | if (socketId != null) { 145 | io.sockets.connected[socketId].emit('Command', command) 146 | } 147 | }) 148 | 149 | ipcMain.on('piRemoved', function (event, item) { 150 | var sock_id = piSocketManager.getSocketIdFromName(item) 151 | var client_sock = io.sockets.connected[sock_id] 152 | 153 | // remove from both memory and storage 154 | piSocketManager.removeSocket(sock_id) 155 | settings.removeClient(item) 156 | if (client_sock != undefined) 157 | client_sock.disconnect() 158 | }) 159 | 160 | ipcMain.on('piAdded', function (event, args) { 161 | // when added , try to connect the client 162 | var param = {} 163 | param['name'] = args.name 164 | // client connection settings defines ip,name and port for the client to connect to 165 | param['settings'] = settings.getClientConnectionSettings() 166 | piSocketManager.connectToPi(args.ip, args.port, param) 167 | settings.addClient(args.name, { 168 | ip: args.ip, 169 | port: args.port 170 | }) 171 | }) 172 | 173 | ipcMain.on('listCreated', (event, listName, args) => { 174 | settings.addList(listName, args) 175 | }) 176 | ipcMain.on('listRemoved', (event, listName) => { 177 | settings.removeList(listName) 178 | }) 179 | ipcMain.on('clientAddedToList', (event, listName, clients) => { 180 | settings.addClientsToList(listName, clients) 181 | }) 182 | ipcMain.on('clientRemovedFromList', (event, listName, clients) => { 183 | settings.removeClientFromList(listName, clients) 184 | }) 185 | ipcMain.on('taskCreated', (event, taskName, args) => { 186 | settings.addTask(taskName, args) 187 | }) 188 | ipcMain.on('taskDeleted', (event, taskname) => { 189 | settings.removeTask(taskname) 190 | }) 191 | ipcMain.on('disconnect', (event, name) => { 192 | var sock_id = piSocketManager.getSocketIdFromName(name) 193 | var client_sock = io.sockets.connected[sock_id] 194 | 195 | if (client_sock != undefined) { 196 | client_sock.disconnect() 197 | } 198 | }) 199 | 200 | ipcMain.on('connect', (event, args) => { 201 | var param = {} 202 | param['name'] = args.name 203 | param['settings'] = settings.getClientConnectionSettings() 204 | piSocketManager.connectToPi(args.ip, args.port, param) 205 | }) 206 | 207 | ipcMain.on('quit', () => { 208 | var pi = piSocketManager.getAllSocketsName() 209 | 210 | for (var name in pi) { 211 | io.sockets.connected[piSocketManager.getSocketIdFromName(pi[name])].disconnect() 212 | } 213 | 214 | setTimeout(function () { 215 | app.quit() 216 | }, 100) 217 | }) 218 | 219 | /** 220 | * Event -- open-url; opens URL in a new electron window 221 | * @type {BrowserWindow} 222 | */ 223 | ipcMain.on('open-url', (event, clientname, hostname) => { 224 | let shell = new BrowserWindow({width: 600, height: 600, title: clientname}) 225 | 226 | // the url is passed as parameter to the html file, which load the URL into webview. 227 | // bypass cross-origin-policy 228 | shell.loadURL(`file://${__dirname}/ui/test.html?${hostname}`) 229 | shell.on('closed', () => { 230 | shell = null 231 | }) 232 | }) 233 | 234 | ipcMain.on('minimize', () => win.minimize()) 235 | 236 | /** 237 | * events -- *; to read settings from secondary storage 238 | */ 239 | 240 | ipcMain.on('client-info-settings', function (event) { 241 | event.returnValue = settings.getClients() || {} 242 | }) 243 | ipcMain.on('lists-info-settings', function (event) { 244 | event.returnValue = settings.getAllLists() || {} 245 | }) 246 | ipcMain.on('tasks-info-settings', function (event) { 247 | event.returnValue = settings.getAllTasks() || {} 248 | }) 249 | ipcMain.on('client-connection-settings', function (event) { 250 | event.returnValue = settings.getClientConnectionSettings() || {} 251 | }) 252 | ipcMain.on('notification-settings', function (event) { 253 | event.returnValue = settings.getNotificationStatus() || {} 254 | }) 255 | ipcMain.on('toggle-notification-status', function (event, type, state) { 256 | settings.toggleNotificationStatus(type, state) 257 | }) 258 | ipcMain.on('set-server-options', function (event, port) { 259 | settings.setServerOptions(port) 260 | }) 261 | ipcMain.on('get-server-options', function (event) { 262 | event.returnValue = port 263 | }) 264 | ipcMain.on('set-client-connection-settings', function (event, options) { 265 | settings.setClientConnectionSettings(options) 266 | }) 267 | ipcMain.on('set-flock-webhook', function (event, webhook) { 268 | settings.setFlockWebHook(webhook) 269 | }) 270 | ipcMain.on('reset-default-settings', function (event) { 271 | settings.resetSettings() 272 | contents.send('settings-restored-to-default') 273 | }) 274 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************ 2 | * Settings.js 3 | * manages reading and writing settings from disk 4 | * Singleton class 5 | ***********************************************************************************************/ 6 | 7 | 'use strict' 8 | 9 | ;(function () { 10 | let piSettings = (function () { 11 | const settings = require('electron-settings') 12 | let instance = null 13 | 14 | function initialize () { 15 | // default settings of orka. loaded at first startup and after settings restored to default 16 | let defaultSettings = { 17 | server: { 18 | port: 8000 19 | }, 20 | clients: { 21 | 22 | }, 23 | lists: { 24 | 25 | }, 26 | tasks: { 27 | 28 | }, 29 | notification: { 30 | 'systemNotification': true, 31 | 'uiNotification': true, 32 | 'flockNotification': true, 33 | 'flockWebHook': undefined 34 | }, 35 | logs: { 36 | 'logging': true 37 | }, 38 | clientConnectionSettings: { 39 | ip: '127.0.0.1', 40 | port: 8000, 41 | interval: 5000, 42 | threshold: { 43 | temperature: { 44 | value: 50, 45 | command: '', 46 | notify: true 47 | }, 48 | ram: { 49 | value: 90, 50 | command: '', 51 | notify: true 52 | }, 53 | disk: { 54 | value: 99, 55 | command: '', 56 | notify: false 57 | }, 58 | io: { 59 | value: 0, 60 | command: '', 61 | notify: true 62 | } 63 | } 64 | } 65 | } 66 | 67 | let resetSettings = () => { 68 | settings.resetToDefaults() 69 | } 70 | 71 | let getServerOptions = () => { 72 | return settings.getSync('server') 73 | } 74 | 75 | let setServerOptions = (port) => { 76 | if (port % 1 === 0) { 77 | settings.setSync('server.port', port) 78 | settings.setSync(`clientConnectionSettings.port`, port) 79 | } 80 | } 81 | 82 | let getClientConnectionSettings = () => { 83 | return settings.getSync('clientConnectionSettings') 84 | } 85 | let setClientConnectionSettings = (options) => { 86 | settings.setSync(`clientConnectionSettings`, JSON.parse(options)) 87 | } 88 | 89 | let getClients = () => { 90 | return settings.getSync('clients') 91 | } 92 | 93 | let addClient = (name, param) => { 94 | if (!isAvailable(`clients.${name}`)) { 95 | settings.setSync(`clients.${name}`, param) 96 | } 97 | else { 98 | console.log(name + ' already present in settings') 99 | } 100 | } 101 | 102 | let removeClient = (name) => { 103 | if (isAvailable(`clients.${name}`)) { 104 | settings.deleteSync(`clients.${name}`) 105 | } 106 | else { 107 | console.log(`${name} not present in settings`) 108 | } 109 | } 110 | 111 | let getAllLists = () => { 112 | if (isAvailable('lists')) { 113 | return settings.getSync('lists') 114 | } 115 | else { 116 | return {} 117 | } 118 | } 119 | 120 | let getList = (list) => { 121 | if (isAvailable(`lists.${list}`)) { 122 | return settings.getSync(`lists.${list}`) 123 | } 124 | } 125 | 126 | let addList = (list, args) => { 127 | if (!isListAvailable(list)) { 128 | settings.setSync(`lists.${list}`, args) 129 | } 130 | else { 131 | console.log(`${list} already available`) 132 | } 133 | } 134 | 135 | let removeList = (list) => { 136 | if (isListAvailable(list)) { 137 | settings.deleteSync(`lists.${list}`) 138 | } 139 | else { 140 | console.log(`${list} not available`); 141 | } 142 | } 143 | 144 | let addClientsToList = (list, clients) => { 145 | if (isListAvailable(list)) { 146 | settings.setSync(`lists.${list}`, getList(list).concat([...clients])) 147 | } 148 | } 149 | 150 | // TO-DO Implement this 151 | let removeClientFromList = (list, client) => { 152 | if (isListAvailable(list)) { 153 | settings.setSync(`lists.${list}`, getList(list).filter(item => item !== client)) 154 | } 155 | else { 156 | console.log(`${list} not available`) 157 | } 158 | } 159 | 160 | let addTask = (taskname, taskOptions) => { 161 | if (!isTaskAvailable(taskname)) { 162 | settings.setSync(`tasks.${taskname}`, taskOptions) 163 | } 164 | } 165 | 166 | let removeTask = (taskname) => { 167 | if (isTaskAvailable(taskname)) { 168 | settings.deleteSync(`tasks.${taskname}`) 169 | } 170 | } 171 | 172 | let getAllTasks = () => { 173 | if (isAvailable('tasks')) { 174 | return settings.getSync('tasks') 175 | } 176 | } 177 | 178 | let getTask = (taskname) => { 179 | if (isTaskAvailable(taskname)) { 180 | return settings.getSync(`tasks.${taskname}`) 181 | } 182 | } 183 | 184 | let isTaskAvailable = (taskname) => { 185 | return isAvailable(`tasks.${taskname}`) 186 | } 187 | 188 | let isAvailable = (setting) => { 189 | return settings.hasSync(setting) 190 | } 191 | 192 | let isListAvailable = (list) => { 193 | return settings.hasSync(`lists.${list}`) 194 | } 195 | 196 | let getNotificationStatus = () => { 197 | let status = settings.getSync(`notification`) 198 | status['logs'] = settings.getSync('logs') 199 | return status 200 | } 201 | 202 | let toggleNotificationStatus = (type, state) => { 203 | let updatePath = type === 'logging' ? 'logs' : 'notification' 204 | updatePath += `.${type}` 205 | console.log(updatePath) 206 | settings.setSync(updatePath, state) 207 | } 208 | 209 | let setFlockWebHook = (webhook) => { 210 | settings.setSync(`notification.flockWebHook`, webhook) 211 | } 212 | 213 | settings.defaults(defaultSettings) 214 | 215 | settings.configure({ 216 | pretiffy: true 217 | }) 218 | /* Sanity Check.. If settings file is corrupted or it is not present, then create a new one */ 219 | if (getServerOptions() === undefined || 220 | getServerOptions().port === {} || 221 | getServerOptions().port === undefined) { /* we need atleast port number to run */ 222 | resetSettings() 223 | } 224 | return { 225 | resetSettings, 226 | 227 | getServerOptions, 228 | setServerOptions, 229 | 230 | getClientConnectionSettings, 231 | setClientConnectionSettings, 232 | 233 | getClients, 234 | addClient, 235 | removeClient, 236 | 237 | getList, 238 | getAllLists, 239 | addList, 240 | removeList, 241 | addClientsToList, 242 | removeClientFromList, 243 | 244 | addTask, 245 | removeTask, 246 | getTask, 247 | getAllTasks, 248 | 249 | toggleNotificationStatus, 250 | getNotificationStatus, 251 | 252 | setFlockWebHook 253 | } 254 | } 255 | 256 | return { 257 | getInstance: function () { 258 | if (instance == null) { 259 | instance = initialize() 260 | } 261 | return instance 262 | } 263 | } 264 | })() 265 | module.exports = piSettings.getInstance() 266 | })() 267 | -------------------------------------------------------------------------------- /ui/css/messenger-theme-air.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Raleway:400"); 2 | @-webkit-keyframes ui-spinner-rotate-right { 3 | 0% { 4 | -webkit-transform: rotate(0deg); 5 | } 6 | 25% { 7 | -webkit-transform: rotate(180deg); 8 | } 9 | 50% { 10 | -webkit-transform: rotate(180deg); 11 | } 12 | 75% { 13 | -webkit-transform: rotate(360deg); 14 | } 15 | 100% { 16 | -webkit-transform: rotate(360deg); 17 | } 18 | } 19 | @-webkit-keyframes ui-spinner-rotate-left { 20 | 0% { 21 | -webkit-transform: rotate(0deg); 22 | } 23 | 25% { 24 | -webkit-transform: rotate(0deg); 25 | } 26 | 50% { 27 | -webkit-transform: rotate(180deg); 28 | } 29 | 75% { 30 | -webkit-transform: rotate(180deg); 31 | } 32 | 100% { 33 | -webkit-transform: rotate(360deg); 34 | } 35 | } 36 | @-moz-keyframes ui-spinner-rotate-right { 37 | 0% { 38 | -moz-transform: rotate(0deg); 39 | } 40 | 25% { 41 | -moz-transform: rotate(180deg); 42 | } 43 | 50% { 44 | -moz-transform: rotate(180deg); 45 | } 46 | 75% { 47 | -moz-transform: rotate(360deg); 48 | } 49 | 100% { 50 | -moz-transform: rotate(360deg); 51 | } 52 | } 53 | @-moz-keyframes ui-spinner-rotate-left { 54 | 0% { 55 | -moz-transform: rotate(0deg); 56 | } 57 | 25% { 58 | -moz-transform: rotate(0deg); 59 | } 60 | 50% { 61 | -moz-transform: rotate(180deg); 62 | } 63 | 75% { 64 | -moz-transform: rotate(180deg); 65 | } 66 | 100% { 67 | -moz-transform: rotate(360deg); 68 | } 69 | } 70 | @keyframes ui-spinner-rotate-right { 71 | 0% { 72 | transform: rotate(0deg); 73 | } 74 | 25% { 75 | transform: rotate(180deg); 76 | } 77 | 50% { 78 | transform: rotate(180deg); 79 | } 80 | 75% { 81 | transform: rotate(360deg); 82 | } 83 | 100% { 84 | transform: rotate(360deg); 85 | } 86 | } 87 | @keyframes ui-spinner-rotate-left { 88 | 0% { 89 | transform: rotate(0deg); 90 | } 91 | 25% { 92 | transform: rotate(0deg); 93 | } 94 | 50% { 95 | transform: rotate(180deg); 96 | } 97 | 75% { 98 | transform: rotate(180deg); 99 | } 100 | 100% { 101 | transform: rotate(360deg); 102 | } 103 | } 104 | /* line 118, ../../src/sass/messenger-spinner.scss */ 105 | .messenger-spinner { 106 | position: relative; 107 | border-radius: 100%; 108 | } 109 | /* line 122, ../../src/sass/messenger-spinner.scss */ 110 | ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { 111 | display: block; 112 | } 113 | /* line 126, ../../src/sass/messenger-spinner.scss */ 114 | .messenger-spinner .messenger-spinner-side { 115 | width: 50%; 116 | height: 100%; 117 | overflow: hidden; 118 | position: absolute; 119 | } 120 | /* line 132, ../../src/sass/messenger-spinner.scss */ 121 | .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { 122 | border-radius: 999px; 123 | position: absolute; 124 | width: 100%; 125 | height: 100%; 126 | -moz-animation-iteration-count: infinite; 127 | -webkit-animation-iteration-count: infinite; 128 | animation-iteration-count: infinite; 129 | -moz-animation-timing-function: linear; 130 | -webkit-animation-timing-function: linear; 131 | animation-timing-function: linear; 132 | } 133 | /* line 142, ../../src/sass/messenger-spinner.scss */ 134 | .messenger-spinner .messenger-spinner-side-left { 135 | left: 0; 136 | } 137 | /* line 145, ../../src/sass/messenger-spinner.scss */ 138 | .messenger-spinner .messenger-spinner-side-left .messenger-spinner-fill { 139 | left: 100%; 140 | border-top-left-radius: 0; 141 | border-bottom-left-radius: 0; 142 | -moz-animation-name: ui-spinner-rotate-left; 143 | -webkit-animation-name: ui-spinner-rotate-left; 144 | animation-name: ui-spinner-rotate-left; 145 | -moz-transform-origin: 0 50% 50%; 146 | -ms-transform-origin: 0 50% 50%; 147 | -webkit-transform-origin: 0 50% 50%; 148 | transform-origin: 0 50% 50%; 149 | } 150 | /* line 154, ../../src/sass/messenger-spinner.scss */ 151 | .messenger-spinner .messenger-spinner-side-right { 152 | left: 50%; 153 | } 154 | /* line 157, ../../src/sass/messenger-spinner.scss */ 155 | .messenger-spinner .messenger-spinner-side-right .messenger-spinner-fill { 156 | left: -100%; 157 | border-top-right-radius: 0; 158 | border-bottom-right-radius: 0; 159 | -moz-animation-name: ui-spinner-rotate-right; 160 | -webkit-animation-name: ui-spinner-rotate-right; 161 | animation-name: ui-spinner-rotate-right; 162 | -moz-transform-origin: 100% 50% 50%; 163 | -ms-transform-origin: 100% 50% 50%; 164 | -webkit-transform-origin: 100% 50% 50%; 165 | transform-origin: 100% 50% 50%; 166 | } 167 | 168 | /* line 16, ../../src/sass/messenger-theme-air.sass */ 169 | ul.messenger-theme-air { 170 | -moz-user-select: none; 171 | -webkit-user-select: none; 172 | -o-user-select: none; 173 | user-select: none; 174 | font-family: "Raleway", sans-serif; 175 | } 176 | /* line 20, ../../src/sass/messenger-theme-air.sass */ 177 | ul.messenger-theme-air .messenger-message { 178 | -moz-transition: background-color 0.4s; 179 | -o-transition: background-color 0.4s; 180 | -webkit-transition: background-color 0.4s; 181 | transition: background-color 0.4s; 182 | -moz-border-radius: 5px; 183 | -webkit-border-radius: 5px; 184 | border-radius: 5px; 185 | -moz-box-shadow: inset 0 0 0 1px #fff, inset 0 2px #fff, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px rgba(0, 0, 0, 0.2); 186 | -webkit-box-shadow: inset 0 0 0 1px #fff, inset 0 2px #fff, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px rgba(0, 0, 0, 0.2); 187 | box-shadow: inset 0 0 0 1px #fff, inset 0 2px #fff, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px rgba(0, 0, 0, 0.2); 188 | border: 0px; 189 | background-color: rgba(255, 255, 255, 0.8); 190 | position: relative; 191 | margin-bottom: 1em; 192 | font-size: 13px; 193 | color: #666; 194 | font-weight: 500; 195 | padding: 10px 30px 11px 46px; 196 | } 197 | /* line 33, ../../src/sass/messenger-theme-air.sass */ 198 | ul.messenger-theme-air .messenger-message:hover { 199 | background-color: white; 200 | } 201 | /* line 36, ../../src/sass/messenger-theme-air.sass */ 202 | ul.messenger-theme-air .messenger-message .messenger-close { 203 | position: absolute; 204 | top: 0px; 205 | right: 0px; 206 | color: #888; 207 | opacity: 1; 208 | font-weight: bold; 209 | display: block; 210 | font-size: 20px; 211 | line-height: 20px; 212 | padding: 8px 10px 7px 7px; 213 | cursor: pointer; 214 | background: transparent; 215 | border: 0; 216 | -webkit-appearance: none; 217 | } 218 | /* line 52, ../../src/sass/messenger-theme-air.sass */ 219 | ul.messenger-theme-air .messenger-message .messenger-close:hover { 220 | color: #444; 221 | } 222 | /* line 55, ../../src/sass/messenger-theme-air.sass */ 223 | ul.messenger-theme-air .messenger-message .messenger-close:active { 224 | color: #222; 225 | } 226 | /* line 58, ../../src/sass/messenger-theme-air.sass */ 227 | ul.messenger-theme-air .messenger-message .messenger-actions { 228 | float: none; 229 | margin-top: 10px; 230 | } 231 | /* line 62, ../../src/sass/messenger-theme-air.sass */ 232 | ul.messenger-theme-air .messenger-message .messenger-actions a { 233 | -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); 234 | -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); 235 | box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); 236 | -moz-border-radius: 4px; 237 | -webkit-border-radius: 4px; 238 | border-radius: 4px; 239 | text-decoration: none; 240 | display: inline-block; 241 | padding: 10px; 242 | color: #888; 243 | margin-right: 10px; 244 | padding: 3px 10px 5px; 245 | text-transform: capitalize; 246 | } 247 | /* line 73, ../../src/sass/messenger-theme-air.sass */ 248 | ul.messenger-theme-air .messenger-message .messenger-actions a:hover { 249 | -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15); 250 | -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15); 251 | box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15); 252 | color: #444; 253 | } 254 | /* line 77, ../../src/sass/messenger-theme-air.sass */ 255 | ul.messenger-theme-air .messenger-message .messenger-actions a:active { 256 | -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.18), inset 0px 1px rgba(0, 0, 0, 0.05); 257 | -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.18), inset 0px 1px rgba(0, 0, 0, 0.05); 258 | box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.18), inset 0px 1px rgba(0, 0, 0, 0.05); 259 | background: rgba(0, 0, 0, 0.04); 260 | color: #444; 261 | } 262 | /* line 82, ../../src/sass/messenger-theme-air.sass */ 263 | ul.messenger-theme-air .messenger-message .messenger-actions .messenger-phrase { 264 | display: none; 265 | } 266 | /* line 85, ../../src/sass/messenger-theme-air.sass */ 267 | ul.messenger-theme-air .messenger-message .messenger-message-inner:before { 268 | -moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); 269 | -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); 270 | box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); 271 | -moz-border-radius: 50%; 272 | -webkit-border-radius: 50%; 273 | border-radius: 50%; 274 | position: absolute; 275 | left: 17px; 276 | display: block; 277 | content: " "; 278 | top: 50%; 279 | margin-top: -8px; 280 | height: 13px; 281 | width: 13px; 282 | z-index: 20; 283 | } 284 | /* line 99, ../../src/sass/messenger-theme-air.sass */ 285 | ul.messenger-theme-air .messenger-message.alert-success .messenger-message-inner:before { 286 | background-color: #5fca4a; 287 | } 288 | /* line 34, ../../src/sass/messenger-spinner.scss */ 289 | ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { 290 | width: 24px; 291 | height: 24px; 292 | background: transparent; 293 | } 294 | /* line 39, ../../src/sass/messenger-spinner.scss */ 295 | ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { 296 | background: #dd6a45; 297 | -moz-animation-duration: 20s; 298 | -webkit-animation-duration: 20s; 299 | animation-duration: 20s; 300 | opacity: 1; 301 | } 302 | /* line 47, ../../src/sass/messenger-spinner.scss */ 303 | ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { 304 | content: ""; 305 | background: #fff; 306 | position: absolute; 307 | width: 19px; 308 | height: 19px; 309 | border-radius: 50%; 310 | top: 2px; 311 | left: 2px; 312 | display: block; 313 | } 314 | /* line 34, ../../src/sass/messenger-spinner.scss */ 315 | ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-later .messenger-spinner { 316 | width: 24px; 317 | height: 24px; 318 | background: transparent; 319 | } 320 | /* line 39, ../../src/sass/messenger-spinner.scss */ 321 | ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { 322 | background: #dd6a45; 323 | -moz-animation-duration: 600s; 324 | -webkit-animation-duration: 600s; 325 | animation-duration: 600s; 326 | opacity: 1; 327 | } 328 | /* line 47, ../../src/sass/messenger-spinner.scss */ 329 | ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { 330 | content: ""; 331 | background: #fff; 332 | position: absolute; 333 | width: 19px; 334 | height: 19px; 335 | border-radius: 50%; 336 | top: 2px; 337 | left: 2px; 338 | display: block; 339 | } 340 | /* line 109, ../../src/sass/messenger-theme-air.sass */ 341 | ul.messenger-theme-air .messenger-message.alert-error .messenger-message-inner:before { 342 | background-color: #dd6a45; 343 | } 344 | /* line 113, ../../src/sass/messenger-theme-air.sass */ 345 | ul.messenger-theme-air .messenger-message.alert-info .messenger-message-inner:before { 346 | background-color: #61c4b8; 347 | } 348 | /* line 116, ../../src/sass/messenger-theme-air.sass */ 349 | ul.messenger-theme-air .messenger-spinner { 350 | display: block; 351 | position: absolute; 352 | left: 12px; 353 | top: 50%; 354 | margin-top: -13px; 355 | z-index: 999; 356 | height: 24px; 357 | width: 24px; 358 | z-index: 10; 359 | } 360 | -------------------------------------------------------------------------------- /ui/css/messenger.css: -------------------------------------------------------------------------------- 1 | /* line 4, ../../src/sass/messenger.sass */ 2 | ul.messenger { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | /* line 8, ../../src/sass/messenger.sass */ 7 | ul.messenger > li { 8 | list-style: none; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | /* line 14, ../../src/sass/messenger.sass */ 13 | ul.messenger.messenger-empty { 14 | display: none; 15 | } 16 | /* line 17, ../../src/sass/messenger.sass */ 17 | ul.messenger .messenger-message { 18 | overflow: hidden; 19 | *zoom: 1; 20 | } 21 | /* line 20, ../../src/sass/messenger.sass */ 22 | ul.messenger .messenger-message.messenger-hidden { 23 | display: none; 24 | } 25 | /* line 23, ../../src/sass/messenger.sass */ 26 | ul.messenger .messenger-message .messenger-phrase, ul.messenger .messenger-message .messenger-actions a { 27 | padding-right: 5px; 28 | } 29 | /* line 26, ../../src/sass/messenger.sass */ 30 | ul.messenger .messenger-message .messenger-actions { 31 | float: right; 32 | } 33 | /* line 29, ../../src/sass/messenger.sass */ 34 | ul.messenger .messenger-message .messenger-actions a { 35 | cursor: pointer; 36 | text-decoration: underline; 37 | } 38 | /* line 33, ../../src/sass/messenger.sass */ 39 | ul.messenger .messenger-message ul, ul.messenger .messenger-message ol { 40 | margin: 10px 18px 0; 41 | } 42 | /* line 36, ../../src/sass/messenger.sass */ 43 | ul.messenger.messenger-fixed { 44 | position: fixed; 45 | z-index: 10000; 46 | } 47 | /* line 40, ../../src/sass/messenger.sass */ 48 | ul.messenger.messenger-fixed .messenger-message { 49 | min-width: 0; 50 | -moz-box-sizing: border-box; 51 | -webkit-box-sizing: border-box; 52 | box-sizing: border-box; 53 | } 54 | /* line 45, ../../src/sass/messenger.sass */ 55 | ul.messenger.messenger-fixed .message .messenger-actions { 56 | float: left; 57 | } 58 | /* line 48, ../../src/sass/messenger.sass */ 59 | ul.messenger.messenger-fixed.messenger-on-top { 60 | top: 20px; 61 | } 62 | /* line 51, ../../src/sass/messenger.sass */ 63 | ul.messenger.messenger-fixed.messenger-on-bottom { 64 | bottom: 20px; 65 | } 66 | /* line 54, ../../src/sass/messenger.sass */ 67 | ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { 68 | left: 50%; 69 | width: 800px; 70 | margin-left: -400px; 71 | } 72 | @media (max-width: 960px) { 73 | /* line 54, ../../src/sass/messenger.sass */ 74 | ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { 75 | left: 10%; 76 | width: 80%; 77 | margin-left: 0px; 78 | } 79 | } 80 | /* line 64, ../../src/sass/messenger.sass */ 81 | ul.messenger.messenger-fixed.messenger-on-top.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-right { 82 | right: 20px; 83 | left: auto; 84 | } 85 | /* line 68, ../../src/sass/messenger.sass */ 86 | ul.messenger.messenger-fixed.messenger-on-top.messenger-on-left, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-left { 87 | left: 20px; 88 | margin-left: 0px; 89 | } 90 | /* line 72, ../../src/sass/messenger.sass */ 91 | ul.messenger.messenger-fixed.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-left { 92 | width: 350px; 93 | } 94 | /* line 75, ../../src/sass/messenger.sass */ 95 | ul.messenger.messenger-fixed.messenger-on-right .messenger-actions, ul.messenger.messenger-fixed.messenger-on-left .messenger-actions { 96 | float: left; 97 | } 98 | /* line 78, ../../src/sass/messenger.sass */ 99 | ul.messenger .messenger-spinner { 100 | display: none; 101 | } 102 | /* line 81, ../../src/sass/messenger.sass */ 103 | ul.messenger .messenger-clickable { 104 | cursor: pointer; 105 | } 106 | -------------------------------------------------------------------------------- /ui/css/orka-style.css: -------------------------------------------------------------------------------- 1 | 2 | body{ 3 | padding-right: 1px; 4 | padding-left: 1px; 5 | margin-top: 5px; 6 | overflow: hidden; 7 | } 8 | .ui.three.wide.column { 9 | width: auto; 10 | float:left; 11 | } 12 | .ui.demo.menu { 13 | height: 93%; 14 | width: 100% !important; 15 | border-color: #d4d4d4; 16 | } 17 | .ui.demo.menu .item { 18 | font-size: 1.1em; 19 | } 20 | .ui.raised.segment { 21 | height: 93%; 22 | position:relative; 23 | } 24 | #content { 25 | width:100% ; 26 | height:93%; 27 | } 28 | #displayArea { 29 | width:100% !important; 30 | height:calc(100% - 150px); 31 | overflow: auto; 32 | } 33 | #notificationDisplay{ 34 | height:calc(100% - 150px); 35 | } 36 | #notificationTable{ 37 | height:100%; 38 | } 39 | td{ 40 | text-overflow: ellipsis; 41 | height:10px; 42 | } 43 | #header { 44 | font-family: sans-serif; 45 | } 46 | .chart { 47 | position: relative; 48 | display: inline-block; 49 | width: 110px; 50 | height: 110px; 51 | margin-top: 50px; 52 | margin-bottom: 50px; 53 | text-align: center; 54 | } 55 | .chart canvas { 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | } 60 | .percent { 61 | display: inline-block; 62 | line-height: 110px; 63 | z-index: 2; 64 | } 65 | .percent:after { 66 | content: '%'; 67 | margin-left: 0.1em; 68 | font-size: .8em; 69 | } 70 | -------------------------------------------------------------------------------- /ui/css/themes/basic/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/basic/assets/fonts/icons.eot -------------------------------------------------------------------------------- /ui/css/themes/basic/assets/fonts/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 451 | -------------------------------------------------------------------------------- /ui/css/themes/basic/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/basic/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /ui/css/themes/basic/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/basic/assets/fonts/icons.woff -------------------------------------------------------------------------------- /ui/css/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /ui/css/themes/default/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/default/assets/fonts/icons.otf -------------------------------------------------------------------------------- /ui/css/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /ui/css/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /ui/css/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /ui/css/themes/default/assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haikarthikssk/Orka-Server/ba6cb0a0c76bfba73f656e8164b8d3bc171e6cfb/ui/css/themes/default/assets/images/flags.png -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |