' + line.html + '') 126 | } else { 127 | // for log lines use ansi format 128 | line.html = escapeHtml(line.content) 129 | line.html = ansi_up.ansi_to_html(line.html, { use_classes: true }) 130 | line.html = $sce.trustAsHtml(line.html) 131 | } 132 | 133 | return line 134 | } 135 | 136 | // https://github.com/component/escape-html/blob/master/index.js#L22 137 | function escapeHtml(html) { 138 | return String(html) 139 | .replace(/&/g, '&') 140 | .replace(/"/g, '"') 141 | .replace(/'/g, ''') 142 | .replace(//g, '>'); 144 | } 145 | 146 | ctrl.activeStreamFilter = function activeStreamFilter(line) { 147 | try { 148 | return (new RegExp(ctrl.activeStreamRegExp)).test(line.content) 149 | } catch (err) { 150 | return true 151 | } 152 | } 153 | 154 | ctrl.resume = function resume() { 155 | if (!ctrl.paused) return 156 | ctrl.paused = false 157 | ctrl.socket.emit('select stream', ctrl.activeStream) 158 | $streamLines.on('wheel', ctrl.pause) 159 | } 160 | 161 | ctrl.pause = function pause() { 162 | if (ctrl.paused) return 163 | ctrl.paused = true 164 | ctrl.socket.emit('select stream') 165 | $streamLines.off('wheel', ctrl.pause) 166 | $scope.$apply() 167 | } 168 | 169 | $streamLines.on('wheel', ctrl.pause) 170 | 171 | /*! 172 | * settings and preferences 173 | */ 174 | ctrl.toggleFavorite = function toggleFavorite(stream) { 175 | if (ctrl.favorites[stream]) { 176 | delete ctrl.favorites[stream] 177 | } else { 178 | ctrl.favorites[stream] = true 179 | } 180 | 181 | $localForage.setItem('favorites', ctrl.favorites) 182 | } 183 | 184 | ctrl.toggleTimestamp = function toggleTimestamp(stream) { 185 | if (ctrl.hiddenTimestamps[stream]) { 186 | delete ctrl.hiddenTimestamps[stream] 187 | } else { 188 | ctrl.hiddenTimestamps[stream] = true 189 | } 190 | 191 | $localForage.setItem('hiddenTimestamps', ctrl.hiddenTimestamps) 192 | } 193 | 194 | ctrl.setTheme = function setTheme(theme) { 195 | ctrl.theme = theme 196 | $localForage.setItem('theme', theme) 197 | } 198 | 199 | ctrl.setFontFamily = function setFontFamily(fontFamily) { 200 | ctrl.fontFamily = fontFamily 201 | $localForage.setItem('fontFamily', fontFamily) 202 | } 203 | 204 | ctrl.incFontSize = function incFontSize() { 205 | ctrl.fontSize = Math.min(7, ctrl.fontSize + 1) 206 | $localForage.setItem('fontSize', ctrl.fontSize) 207 | } 208 | 209 | ctrl.resetFontSize = function resetFontSize() { 210 | ctrl.fontSize = 4 211 | $localForage.setItem('fontSize', ctrl.fontSize) 212 | } 213 | 214 | ctrl.decFontSize = function decFontSize(fontSize) { 215 | ctrl.fontSize = Math.max(1, ctrl.fontSize - 1) 216 | $localForage.setItem('fontSize', fontSize) 217 | } 218 | 219 | ctrl.setStreamDirection = function setStreamDirection(streamDirection) { 220 | ctrl.streamDirection = streamDirection 221 | $localForage.setItem('streamDirection', streamDirection) 222 | } 223 | 224 | /*! 225 | * load storage in memory and boot 226 | */ 227 | $localForage.getItem('sidebarWidth').then(function (sidebarWidth) { 228 | ctrl.sidebarWidth = sidebarWidth 229 | }) 230 | 231 | $localForage.getItem('theme').then(function (theme) { 232 | ctrl.theme = theme || 'dark' 233 | }) 234 | 235 | $localForage.getItem('fontFamily').then(function (fontFamily) { 236 | ctrl.fontFamily = fontFamily || 1 237 | }) 238 | 239 | $localForage.getItem('fontSize').then(function (fontSize) { 240 | ctrl.fontSize = fontSize || 4 241 | }) 242 | 243 | $localForage.getItem('favorites').then(function (favorites) { 244 | ctrl.favorites = favorites || {} 245 | }) 246 | 247 | $localForage.getItem('hiddenTimestamps').then(function (hiddenTimestamps) { 248 | ctrl.hiddenTimestamps = hiddenTimestamps || {} 249 | }) 250 | 251 | $localForage.getItem('timestampFormat').then(function (timestampFormat) { 252 | ctrl.timestampFormat = timestampFormat || 'MM/DD/YY hh:mm:ss' 253 | }) 254 | 255 | $localForage.getItem('activeStream').then(function (activeStream) { 256 | if (!activeStream || ('streams' === $state.current.name && $stateParams.stream)) return 257 | console.info('%s: restoring session', activeStream) 258 | $state.go('streams', { stream: activeStream }) 259 | }) 260 | 261 | $localForage.getItem('streamDirection').then(function (streamDirection) { 262 | ctrl.streamDirection = undefined === streamDirection ? true : streamDirection 263 | }) 264 | 265 | /*! 266 | * respond to url change 267 | */ 268 | $rootScope.$on('$stateChangeStart', function (e, toState, toParams) { 269 | if ('streams' !== toState.name) return 270 | ctrl.selectStream(toParams.stream) 271 | }) 272 | 273 | /*! 274 | * tell UI we're ready to roll 275 | */ 276 | ctrl.loaded = true 277 | }) 278 | -------------------------------------------------------------------------------- /app/scss/main.scss: -------------------------------------------------------------------------------- 1 | // ** 2 | // variables 3 | // ** 4 | 5 | $spacing: 20px; 6 | $border-radius: 5px; 7 | $font-family: 'Nunito'; 8 | 9 | // ** 10 | // imports 11 | // ** 12 | 13 | @import 'fonts'; 14 | 15 | // ** 16 | // reset 17 | // ** 18 | 19 | * { 20 | background: none; 21 | border: 0; 22 | box-sizing: border-box; 23 | color: inherit; 24 | margin: 0; 25 | padding: 0; 26 | text-decoration: none; 27 | 28 | &:focus { 29 | outline: none; 30 | } 31 | } 32 | 33 | ol, 34 | ul { 35 | list-style: none; 36 | } 37 | 38 | pre { 39 | font-family: inherit !important; 40 | } 41 | 42 | input { 43 | font: inherit !important; 44 | } 45 | 46 | // ** 47 | // styles 48 | // ** 49 | 50 | body { 51 | align-items: stretch; 52 | display: flex; 53 | flex-direction: column; 54 | font-family: $font-family; 55 | font-size: 16px; 56 | height: 100vh; 57 | 58 | &.resizing { 59 | user-select: none; 60 | } 61 | } 62 | 63 | header { 64 | align-items: center; 65 | display: flex; 66 | flex-direction: row; 67 | height: 58px; 68 | 69 | .rtail-logo { 70 | background-position: 50% 50%; 71 | background-repeat: no-repeat; 72 | background-size: 53px 13px; 73 | height: 100px; 74 | width: 94px; 75 | } 76 | 77 | .btn { 78 | background-position: 50% 50%; 79 | background-repeat: no-repeat; 80 | background-size: 100%; 81 | cursor: pointer; 82 | height: 19px; 83 | margin-right: $spacing; 84 | opacity: .4; 85 | width: 19px; 86 | 87 | &.btn-info { 88 | margin-left: auto; 89 | } 90 | } 91 | } 92 | 93 | .split-pane { 94 | display: flex; 95 | flex: 1; 96 | flex-direction: row; 97 | min-height: 0; 98 | } 99 | 100 | .sidebar { 101 | display: flex; 102 | flex-direction: column; 103 | font-size: 16px; 104 | position: relative; 105 | width: 230px; 106 | 107 | .resize-handler { 108 | cursor: col-resize; 109 | height: 100%; 110 | position: absolute; 111 | right: -5px; 112 | top: 0; 113 | width: 10px; 114 | } 115 | 116 | .search-box { 117 | background-position: $spacing 50%; 118 | background-repeat: no-repeat; 119 | background-size: 16px 16px; 120 | display: flex; 121 | height: 57px; 122 | 123 | input { 124 | flex: 1; 125 | font-size: 14px; 126 | height: 57px; 127 | margin-left: 56px; 128 | margin-right: $spacing; 129 | } 130 | } 131 | 132 | .stream-sections { 133 | flex: 1; 134 | height: 0; 135 | overflow-y: auto; 136 | 137 | .stream-section { 138 | flex: 1; 139 | 140 | h4 { 141 | align-items: center; 142 | display: flex; 143 | font-size: 16px; 144 | font-weight: normal; 145 | height: 32px; 146 | margin-top: $spacing; 147 | padding-left: $spacing; 148 | } 149 | 150 | a { 151 | align-items: center; 152 | cursor: pointer; 153 | display: flex; 154 | height: 32px; 155 | padding-left: 40px; 156 | 157 | &.selected { 158 | border-left: 2px solid; 159 | padding-left: 38px; 160 | } 161 | 162 | span { 163 | text-overflow: ellipsis; 164 | white-space: nowrap; 165 | overflow: hidden; 166 | } 167 | 168 | i { 169 | border-radius: 50%; 170 | height: 8px; 171 | margin-left: auto; 172 | margin-right: $spacing; 173 | width: 8px; 174 | } 175 | } 176 | } 177 | } 178 | } 179 | 180 | .stream-view { 181 | position: relative; 182 | display: flex; 183 | flex: 1; 184 | flex-direction: column; 185 | min-width: 0; 186 | 187 | .stream-header { 188 | align-items: center; 189 | display: flex; 190 | font-size: 16px; 191 | height: 57px; 192 | 193 | .stream-title { 194 | align-items: center; 195 | display: flex; 196 | } 197 | 198 | .stream-title-favorite { 199 | background-repeat: no-repeat; 200 | background-size: 15px 15px; 201 | display: inline-block; 202 | height: 15px; 203 | margin: 0 $spacing; 204 | width: 15px; 205 | } 206 | 207 | .filter-box { 208 | background-position: 0 50%; 209 | background-repeat: no-repeat; 210 | background-size: 14px 14px; 211 | display: flex; 212 | font-size: 14px; 213 | height: 30px; 214 | margin-left: auto; 215 | margin-right: $spacing; 216 | opacity: .7; 217 | width: 210px; 218 | } 219 | 220 | input { 221 | flex: 1; 222 | margin-left: 24px; 223 | } 224 | } 225 | 226 | .stream-lines { 227 | flex: 1; 228 | font-size: 12px; 229 | height: 0; 230 | overflow: auto; 231 | padding: 2px 0; 232 | 233 | .stream-line { 234 | display: flex; 235 | line-height: .5em; 236 | 237 | pre { 238 | line-height: 1.2; 239 | } 240 | 241 | .stream-line-timestamp { 242 | border-right: 1px solid; 243 | flex-shrink: 0; 244 | padding-right: $spacing; 245 | } 246 | 247 | .stream-line-content { 248 | white-space: pre; 249 | } 250 | 251 | .stream-line-timestamp, 252 | .stream-line-content { 253 | align-items: center; 254 | display: flex; 255 | padding-bottom: 5px; 256 | padding-left: $spacing; 257 | padding-top: 5px; 258 | } 259 | } 260 | } 261 | 262 | .btn-toggle-timestamp { 263 | position: absolute; 264 | bottom: 15px; 265 | width: 20px; 266 | height: 20px; 267 | border-radius: $border-radius; 268 | cursor: pointer; 269 | transform: translateX(-50%); 270 | 271 | &.closed { 272 | transform: translateX(-50%) rotate(180deg); 273 | } 274 | } 275 | 276 | .btn-resume { 277 | background-position: 10px 50%; 278 | background-repeat: no-repeat; 279 | background-size: 8px 10px; 280 | border-radius: $border-radius; 281 | bottom: $spacing; 282 | cursor: pointer; 283 | font-size: 14px; 284 | height: 30px; 285 | padding-left: 25px; 286 | position: absolute; 287 | right: $spacing; 288 | text-align: left; 289 | width: 85px; 290 | } 291 | } 292 | 293 | .popover { 294 | position: absolute; 295 | 296 | .popover-content { 297 | border-radius: 5px; 298 | display: flex; 299 | flex-direction: column; 300 | font-size: 12px; 301 | margin-top: $spacing * 2; 302 | overflow: visible !important; 303 | position: relative; 304 | width: 156px; 305 | 306 | &:before { 307 | border-bottom: $spacing / 2 solid transparent; 308 | border-left: $spacing / 2 solid transparent; 309 | border-right: $spacing / 2 solid transparent; 310 | content: ''; 311 | height: 0; 312 | left: 50%; 313 | position: absolute; 314 | top: 0; 315 | transform: translate(-50%, -100%); 316 | width: 0; 317 | } 318 | 319 | .btn { 320 | border-radius: $border-radius; 321 | border-style: solid; 322 | border-width: 1px; 323 | } 324 | } 325 | 326 | .popover-info { 327 | align-items: center; 328 | height: 265px; 329 | 330 | .rtail-logo { 331 | background-position: 50% 50%; 332 | background-repeat: no-repeat; 333 | background-size: 53px 13px; 334 | border-radius: $border-radius $border-radius 0 0; 335 | height: 48px; 336 | width: 100%; 337 | } 338 | 339 | .version { 340 | margin: 15px 0; 341 | } 342 | 343 | .btn { 344 | align-items: center; 345 | display: flex; 346 | height: 30px; 347 | justify-content: center; 348 | margin-bottom: 5px; 349 | width: 87px; 350 | } 351 | 352 | .lukibear-logo { 353 | background-position: 50% 50%; 354 | background-repeat: no-repeat; 355 | background-size: 30%; 356 | border-radius: 0 0 $border-radius $border-radius; 357 | flex: 1; 358 | margin-top: 10px; 359 | width: 100%; 360 | } 361 | } 362 | 363 | .popover-settings { 364 | left: -60px; 365 | padding: 15px; 366 | 367 | &:before { 368 | transform: translate(48px, -100%); 369 | } 370 | 371 | h4 { 372 | font-weight: normal; 373 | } 374 | 375 | .btn-group { 376 | display: flex; 377 | flex-direction: row; 378 | flex-wrap: wrap; 379 | margin: 10px 0; 380 | 381 | &:last-of-type { 382 | margin-bottom: 0; 383 | } 384 | 385 | .btn { 386 | background-position: 50% 50%; 387 | background-repeat: no-repeat; 388 | background-size: auto 50%; 389 | display: block; 390 | font-size: 16px; 391 | height: 30px; 392 | width: 42px; 393 | 394 | &:nth-child(1) { 395 | border-radius: $border-radius 0 0 $border-radius; 396 | border-right: 0; 397 | } 398 | 399 | + .btn { 400 | border-radius: 0; 401 | } 402 | 403 | &:nth-child(3) { 404 | border-left: 0; 405 | border-radius: 0 $border-radius 0 0; 406 | } 407 | 408 | &:nth-child(4) { 409 | border-radius: 0 0 0 $border-radius; 410 | border-right: 0; 411 | border-top: 0; 412 | } 413 | 414 | &:last-child { 415 | border-radius: 0 $border-radius $border-radius 0; 416 | } 417 | 418 | &:nth-child(5) { 419 | border-top: 0; 420 | } 421 | 422 | &:nth-child(6) { 423 | border-left: 0; 424 | border-radius: 0 0 $border-radius; 425 | border-top: 0; 426 | } 427 | } 428 | 429 | &.six-grid { 430 | .btn:nth-child(1) { 431 | border-radius: $border-radius 0 0; 432 | border-right: 0; 433 | } 434 | 435 | .btn:nth-child(n + 4) { 436 | border-top: 0; 437 | } 438 | } 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `rtail(1)` 2 | 3 | [](https://app.wercker.com/project/bykey/54b073dac5b9156509c26031c78c98d4) 4 | [](https://coveralls.io/r/kilianc/rtail) 5 | [](https://www.npmjs.com/package/rtail) 6 | [](https://www.npmjs.com/package/rtail) 7 | [](https://github.com/kilianc/rtail) 8 | [](https://www.npmjs.com/package/rtail) 9 | [](https://gitter.im/kilianc/rtail?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | 11 | ## Terminal output to the browser in seconds, using UNIX pipes. 12 | 13 | `rtail` is a command line utility that grabs every line in `stdin` and broadcasts it over **UDP**. That's it. Nothing fancy. Nothing complicated. Tail log files, app output, or whatever you wish, using `rtail` broadcasting to an `rtail-server` – See multiple streams in the browser, in realtime. 14 | 15 | ## Installation 16 | 17 | $ npm install -g rtail 18 | 19 | ## Web app 20 | 21 |  22 | 23 |  24 | 25 | ## Rationale 26 | 27 | Whether you deploy your code on remote servers using multiple environments or simply have multiple projects, **you must `ssh` to each machine running your code, in order to monitor the logs in realtime**. 28 | 29 | There are many log aggregation tools out there, but few of them are realtime. **Most other tools require you to change your application source code to support their logging protocol/transport**. 30 | 31 | `rtail` is meant to be a replacement of [logio](https://github.com/NarrativeScience/Log.io/commits/master), which isn't actively maintained anymore, doesn't support node v0.12., and uses *TCP. (TCP requires strict client / server handshaking, is resource-hungry, and very difficult to scale.)* 32 | 33 | **The `rtail` approach is very simple:** 34 | * pipe something into `rtail` using [UNIX I/O redirection](http://www.westwind.com/reference/os-x/commandline/pipes.html) [[2]](http://www.codecoffee.com/tipsforlinux/articles2/042.html) 35 | * broadcast every line using UDP 36 | * `rtail-server`, **if listening**, will dispatch the stream into your browser, using [socket.io](http://socket.io/). 37 | 38 | `rtail` is a realtime debugging and monitoring tool, which can display multiple aggregate streams via a modern web interface. **There is no persistent layer, nor does the tool store any data**. If you need a persistent layer, use something like [loggly](https://www.loggly.com/). 39 | 40 | ## Examples 41 | 42 | In your app init script: 43 | 44 | $ node server.js 2>&1 | rtail --id "api.myproject.com" 45 | 46 | $ mycommand | rtail > server.log 47 | 48 | $ node server.js 2>&1 | rtail --mute 49 | 50 | Supports JSON5 lines: 51 | 52 | $ while true; do echo [1, 2, 3, "hello"]; sleep 1; done | rtail 53 | $ echo { "foo": "bar" } | rtail 54 | $ echo { format: 'JSON5' } | rtail 55 | 56 | Using log files (log rotate safe!): 57 | 58 | $ node server.js 2>&1 > log.txt 59 | $ tail -F log.txt | rtail 60 | 61 | For fun and debugging: 62 | 63 | $ cat ~/myfile.txt | rtail 64 | $ echo "Server rebooted!" | rtail --id `hostname` 65 | 66 | ## Params 67 | 68 | $ rtail --help 69 | Usage: cmd | rtail [OPTIONS] 70 | 71 | Options: 72 | --host, -h The server host [string] [default: "127.0.0.1"] 73 | --port, -p The server port [string] [default: 9999] 74 | --id, --name The log stream id [string] [default: (moniker)] 75 | --mute, -m Don't pipe stdin with stdout [boolean] 76 | --tty Keeps ansi colors [boolean] [default: true] 77 | --parse-date Looks for dates to use as timestamp [boolean] [default: true] 78 | --help Show help [boolean] 79 | --version, -v Show version number [boolean] 80 | 81 | Examples: 82 | server | rtail > server.log localhost + file 83 | server | rtail --id api.domain.com Name the log stream 84 | server | rtail --host example.com Sends to example.com 85 | server | rtail --port 43567 Uses custom port 86 | server | rtail --mute No stdout 87 | server | rtail --no-tty Strips ansi colors 88 | server | rtail --no-date-parse Disable date parsing/stripping 89 | 90 | 91 | ## `rtail-server(1)` 92 | 93 | `rtail-server` receives all messages broadcast from every `rtail` client, displaying all incoming log streams in a realtime web view. **Under the hood, the server uses [socket.io](http://socket.io) to pipe every incoming UDP message to the browser.** 94 | 95 | There is little to no configuration – The default UDP/HTTP ports can be changed, but that's it. 96 | 97 | ## Examples 98 | 99 | Use default values: 100 | 101 | $ rtail-server 102 | 103 | Always use latest, stable webapp: 104 | 105 | $ rtail-server --web-version stable 106 | 107 | Use custom ports: 108 | 109 | $ rtail-server --web-port 8080 --udp-port 9090 110 | 111 | Set debugging on: 112 | 113 | $ DEBUG=rtail:* rtail-server 114 | 115 | Open your browser and start tailing logs! 116 | 117 | ## Params 118 | 119 | $ rtail-server --help 120 | Usage: rtail-server [OPTIONS] 121 | 122 | Options: 123 | --udp-host, --uh The listening UDP hostname [default: "127.0.0.1"] 124 | --udp-port, --up The listening UDP port [default: 9999] 125 | --web-host, --wh The listening HTTP hostname [default: "127.0.0.1"] 126 | --web-port, --wp The listening HTTP port [default: 8888] 127 | --web-version Define web app version to serve [string] 128 | --help, -h Show help [boolean] 129 | --version, -v Show version number [boolean] 130 | 131 | Examples: 132 | rtail-server --web-port 8080 Use custom HTTP port 133 | rtail-server --udp-port 8080 Use custom UDP port 134 | rtail-server --web-version stable Always uses latest stable webapp 135 | rtail-server --web-version unstable Always uses latest develop webapp 136 | rtail-server --web-version 0.1.3 Use webapp v0.1.3 137 | 138 | ## UDP Broadcasting 139 | 140 | To scale and broadcast on multiple servers, instruct the `rtail` client to stream to the broadcast address. Every message will then be delivered to all servers in your subnet. 141 | 142 | ## Authentication layer 143 | 144 | For the time being, the webapp doesn't have an authentication layer; it assumes that you will run it behind a VPN or reverse proxy, with a simple `Authorization` header check. 145 | 146 | # How to contribute 147 | 148 | This project follows the awesome [Vincent Driessen](http://nvie.com/about/) [branching model](http://nvie.com/posts/a-successful-git-branching-model/). 149 | 150 | * You must add a new feature on its own branch 151 | * You must contribute to hot-fixing, directly into the master branch (and pull-request to it) 152 | 153 | This project uses JSCS to enforce a consistent code style. Your contribution must be pass jscs validation. 154 | 155 | The test suite is written on top of [mochajs/mocha](http://mochajs.org/). Use the tests to check if your contribution breaks some part of the library and be sure to add new tests for each new feature. 156 | 157 | $ npm test 158 | 159 | ## Contributors 160 | 161 | * [Kilian Ciuffolo](https://github.com/kilianc) 162 | * [Luca Orio](https://www.behance.net/lucaorio) 163 | * [Sandaruwan Silva](https://github.com/s-silva) 164 | * [Sorel Mihai](https://dribbble.com/sorelmihai) 165 | * [Tim Riot](https://www.linkedin.com/in/timriot) 166 | 167 | ## Roadmap (aka where you can help) 168 | 169 | * Write a rock solid test suite 170 | * Allow use of DTLS (waiting for node to support this https://github.com/joyent/node/pull/6704) 171 | * Add GitHub OAuth and basic auth for teams (join proposal convo here: https://github.com/kilianc/rtail/issues/44) 172 | * Implement infinite-scroll like behavior in the webapp to support bigger backlogs and make it future proof. 173 | * Publish base rtail docker image to DockerHub 174 | * Create a catch all docker logs image 175 | * Rewrite webapp using ng2 176 | 177 | ## Sponsors 178 | ❤ rTail? Consider sponsoring this project to keep it alive and free for the community. 179 | 180 | * Lukibear (domain) 181 | * ? (wildcard TLS cert) 182 | * ? (.io domain) 183 | 184 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=info%40rtail%2eorg&lc=US&item_name=rtail&item_number=rtail¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) 185 | 186 | Professional support or ad-hoc is also available. 187 | 188 | ## License 189 | 190 | _This software is released under the MIT license cited below_. 191 | 192 | Copyright (c) 2014 Kilian Ciuffolo, me@nailik.org. All Rights Reserved. 193 | 194 | Permission is hereby granted, free of charge, to any person 195 | obtaining a copy of this software and associated documentation 196 | files (the 'Software'), to deal in the Software without 197 | restriction, including without limitation the rights to use, 198 | copy, modify, merge, publish, distribute, sublicense, and/or sell 199 | copies of the Software, and to permit persons to whom the 200 | Software is furnished to do so, subject to the following 201 | conditions: 202 | 203 | The above copyright notice and this permission notice shall be 204 | included in all copies or substantial portions of the Software. 205 | 206 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 207 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 208 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 209 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 210 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 211 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 212 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 213 | OTHER DEALINGS IN THE SOFTWARE. 214 | -------------------------------------------------------------------------------- /app/images/dark/logo-lukibear.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/images/light/logo-lukibear.svg: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------