Compare commits

...

321 Commits

Author SHA1 Message Date
dependabot[bot] 52014199f1
Bump glob-parent from 5.1.0 to 5.1.2 (#237)
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.0 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.0...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-03 23:17:46 +02:00
dependabot[bot] 18de86b061
Bump lodash from 4.17.19 to 4.17.21 (#235)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 22:56:20 +02:00
Maciej Winnicki 44ff51a618
feat: produce Windows exe 2021-03-17 16:17:42 +01:00
Ethan Dye 514c16e488
chore: update socket.io (#232)
Signed-off-by: Ethan Dye <mrtops03@gmail.com>
2021-03-17 15:49:33 +01:00
EvertEt 6610661b26
chore: fix typo (#229)
Related to #163
2021-03-09 10:49:09 +01:00
Maciej Winnicki 9d14ac51a8
Release 4.9.2 2021-02-17 23:03:05 +01:00
dependabot[bot] ab66d83efc
Bump dot-prop from 4.2.0 to 4.2.1 (#224)
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-17 22:59:38 +01:00
Aaron Dewes a5582dc34f
feat: use Debian buster (#221) 2021-02-17 22:59:19 +01:00
Maciej Winnicki cd871ad361
chore: use explicit modules versions (#225) 2021-02-17 22:57:35 +01:00
dependabot[bot] 38022f7a0a
Bump socket.io from 2.2.0 to 2.4.0 (#223)
Bumps [socket.io](https://github.com/socketio/socket.io) from 2.2.0 to 2.4.0.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/2.4.0/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/2.2.0...2.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-04 08:54:43 +01:00
dependabot[bot] ea330b8ef9
Bump lodash from 4.17.15 to 4.17.19 (#213)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-20 13:37:15 +02:00
Maciej Winnicki fb1da7bd18
chore: update pkg 2020-04-14 09:37:00 +02:00
Maciej Winnicki 5dad99106e
fix: disable usage stats param not passed when in daemon mode. Closes #188 2020-04-13 21:24:54 +02:00
Maciej Winnicki 94e2c46ee6
chore: prettier standard 2020-04-13 21:21:19 +02:00
Maciej Winnicki 0ceb864693
fix: ellipsis in title 2020-04-13 21:03:18 +02:00
Maciej Winnicki 39f8410a2a
chore: fix lint 2020-03-31 20:25:47 +02:00
DomExpire 6c4561a8d7
fix of the approximate value of window.pageYOffset on chrome android (#202) 2020-03-31 20:19:08 +02:00
Maciej Winnicki d43976f3a5
Release 4.9.1 2020-03-26 15:26:52 +01:00
Maciej Winnicki 7f506ae5bf
fix: use different base image to support rpi 2020-03-26 15:24:39 +01:00
dependabot[bot] 0fcd6278fd
Bump acorn from 5.7.3 to 5.7.4 (#199)
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-03-14 21:40:11 +01:00
donnie 0aad9dd2dc
Fix websocket error during handshake (#195)
See [this SO issue](https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respo)
2020-02-19 19:55:30 +01:00
Maciej Winnicki c12a1878de
Update Release guide 2020-02-03 13:26:32 +01:00
Maciej Winnicki 25f3b23c86
Release 4.9.0 2020-02-03 13:19:55 +01:00
Maciej Winnicki 2b3a64dca2
Fix publish step 2020-02-03 13:19:23 +01:00
Maciej Winnicki 9e6e7ee5dc
Fix publish command 2020-02-03 13:02:09 +01:00
Maciej Winnicki 1a9ce68c4a
Fix publish step 2020-02-03 12:56:45 +01:00
Maciej Winnicki 06fd619a40
Fix GH action 2020-02-03 12:51:20 +01:00
Maciej Winnicki b80fc8fd0c
Fix GH action 2020-02-03 12:39:12 +01:00
Maciej Winnicki 34d5893694
Fix GH action 2020-02-03 12:37:48 +01:00
Maciej Winnicki 18c4603016
Fix GH actions config 2020-02-03 12:35:50 +01:00
Maciej Winnicki a63e6dbd87
Lint fixes 2020-02-03 12:23:47 +01:00
Alexander Wunschik 0eac303527
Add windows support (#194)
* fix: replace native tail with fs-tail-stream
fixes #111

* fix: use options.buffer as start parameter

* feat: add additional windows support

* feat: add additional windows support
2020-02-03 12:02:21 +01:00
Maciej Winnicki 1357793a28
Make CI step conditional 2019-09-13 21:34:33 +02:00
Maciej Winnicki 7ff68040c3
Typo 2019-09-13 21:21:44 +02:00
Maciej Winnicki b31de76d33
converted main.workflow to Actions V2 yml files (#183) 2019-09-13 21:19:15 +02:00
Alexander Wunschik 09acdc62af update commander to 3.0.1 (#182)
see also #176
2019-08-30 13:20:11 +02:00
Maciej Winnicki 3b390a0d54
Release 4.8.0 2019-08-29 16:42:53 +02:00
Pavlo Bashynskyi 01afdc8cdd Add pause button (#163) Closes #113. Closes #137. 2019-08-27 11:24:25 +02:00
Maciej Winnicki 4d5f29b209
Track --number param 2019-08-26 22:03:27 +02:00
Maciej Winnicki 29d660623f
Release 4.7.0 2019-08-26 20:17:38 +02:00
Maciej Winnicki 2150761fa2
Fix auto-scrolling (#180) 2019-08-26 20:14:30 +02:00
dependabot[bot] 1cacc260f4 Bump eslint-utils from 1.3.1 to 1.4.2 (#181)
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-26 20:10:31 +02:00
Maciej Winnicki f8de308820
Update pkg 2019-08-08 21:29:44 +02:00
Maciej Winnicki 7c8c9cab98
Fix CI issues (#179) 2019-08-08 21:22:14 +02:00
dependabot[bot] 62c0d67370 Bump extend from 3.0.1 to 3.0.2 (#178)
Bumps [extend](https://github.com/justmoon/node-extend) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/justmoon/node-extend/releases)
- [Changelog](https://github.com/justmoon/node-extend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/justmoon/node-extend/compare/v3.0.1...v3.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-06 19:41:48 +02:00
Alexander Wunschik 364f68b9c1 chore(docs): fix help option (#176)
fixes #53
2019-08-06 19:39:40 +02:00
Alexander Wunschik bc8c1f8d00 chore(docs): fix web interface url (#177) 2019-08-06 16:25:33 +02:00
Ryan Hunt e6cc5f4dd1 keep empty lines (#174) 2019-07-22 21:03:10 +02:00
dependabot[bot] 95d8cd5dcc Bump lodash from 4.17.4 to 4.17.15 (#173)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.4 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.4...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-21 22:14:59 +02:00
Maciej Winnicki 54101c8fad
Release 4.6.0 2019-07-03 22:58:25 +02:00
Maciej Winnicki 5bf80592cc
Support running frontial Docker on ARM by switching base image to the official one. Closes #168 2019-07-03 22:56:00 +02:00
dependabot[bot] f5c86cd3d7 Bump stringstream from 0.0.5 to 0.0.6 (#167)
Bumps [stringstream](https://github.com/mhart/StringStream) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/mhart/StringStream/releases)
- [Commits](https://github.com/mhart/StringStream/compare/v0.0.5...v0.0.6)

Signed-off-by: dependabot[bot] <support@github.com>
2019-06-30 19:02:31 +02:00
dependabot[bot] 4f4693b623 Bump js-yaml from 3.12.0 to 3.13.1 (#166)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)
2019-06-05 10:03:23 +02:00
Maciej Winnicki e9e16fa578
Release 4.5.4 2019-02-21 23:17:30 +01:00
Maciej Winnicki a869307ecc
Fix proper signal handling. Closes #158 #160 2019-02-21 23:15:17 +01:00
Maciej Winnicki 205cdbf696
Cleanup unused files (#157) 2019-01-18 22:43:51 +01:00
Maciej Winnicki 658a0914bf
Revert "Temporary remove GH workflow because of https://github.com/actions/bin/issues/13"
This reverts commit 26c3957457.
2019-01-18 22:24:18 +01:00
Maciej Winnicki b2a4f4b4af
Release 4.5.3 2019-01-09 22:04:34 +01:00
Maciej Winnicki da920b4c04
Update npm token 2019-01-09 22:03:53 +01:00
Maciej Winnicki 955bf9d436
Release 4.5.2 2019-01-09 22:00:28 +01:00
Maciej Winnicki 57367a2b0d
Fix highligthing multiple word. Closes #151 (#155) 2019-01-09 21:58:01 +01:00
Maciej Winnicki 26c3957457
Temporary remove GH workflow because of https://github.com/actions/bin/issues/13 2019-01-09 21:53:10 +01:00
Maciej Winnicki 953c6b7dbe
Add GH actions workflow 2019-01-09 21:45:21 +01:00
Maciej Winnicki 8ea0f65760
test2 2019-01-07 21:53:05 +01:00
Maciej Winnicki 6c77313a6a
test 2019-01-07 21:50:38 +01:00
Maciej Winnicki 2cfff5caf7
Update deps (#154) 2018-12-18 12:14:26 +01:00
Maciej Winnicki ef8c14bb9c
Release 4.5.1 2018-11-28 10:17:37 +01:00
Maciej Winnicki c35287ce7a
Fix runtime stats sending 2018-11-28 10:13:32 +01:00
Maciej Winnicki 28de6d18dc
Update mocha dependency 2018-11-28 09:50:19 +01:00
Maciej Winnicki 274e48731f
Release 4.5.0 2018-11-22 12:11:15 +01:00
Maciej Winnicki 4a87fa6bdf
Add usage stats gathering 2018-11-22 12:09:51 +01:00
Maciej Winnicki 90d1428449
Release 4.4.0 2018-10-30 08:52:40 +01:00
watanabe takanobu 75d31aafd6 Support OpenBSD (#146) 2018-10-30 08:39:21 +01:00
Maciej Winnicki a99286fa0d
Update Docker image link 2018-10-24 14:07:09 +02:00
Maciej Winnicki 4687fa0698
Release 4.3.3 2018-10-24 13:51:57 +02:00
Maciej Winnicki ab97b1bc36
Add Dockerfile. Closes #125 (#145) 2018-10-24 13:42:49 +02:00
Maciej Winnicki 71f2114127
Add title attribute with full path. Closes #85 2018-10-23 12:12:45 +02:00
Maciej Winnicki 6faeac0e06
Fix text overflow in navbar. Closes #128 (#144) 2018-10-23 12:07:41 +02:00
Maciej Winnicki 8547d3d241
Update presets lit 2018-10-12 10:31:08 +02:00
Maciej Winnicki 1edcfac40e
Fix filename size (#142) 2018-10-12 10:23:12 +02:00
novahawk13 3d8b93b2d2 Fixes issue where navbar was overlaying text (#139)
* Edited webpage and css to prevent navbar from ever overlaying over text
2018-10-12 10:15:33 +02:00
Maciej Winnicki 751a2b9db7
Release 4.3.2 2018-10-11 13:40:50 +02:00
Maciej Winnicki b1eada0b0e
Update eslint (#141) 2018-10-11 12:53:53 +02:00
Krishna 6e758d8e34 Basic Python logging preset (#140) 2018-10-11 11:57:40 +02:00
Pavlo Bashynskyi 7e7f8e59e8 Set filter in URL (#138)
* Set filter in URL

* fix: jsdom > v11 broke with node4

* Updating URL from filter input
2018-10-01 22:04:14 +02:00
Maciej Winnicki fd735d87c0
Update connect dependencies. Closes #133 2018-08-14 11:10:10 +02:00
Maciej Winnicki 446d144b21
Update deps 2018-08-14 10:15:06 +02:00
Maciej Winnicki 54d293e9f5
Add infor about not running on Windows 2018-08-14 10:09:10 +02:00
Krzysztof Starzyk 1fb8d5be97 add npmlog style preset (#135) 2018-08-14 09:43:45 +02:00
Maciej Winnicki 710551da7a
Release 4.2.2 2018-07-25 10:03:29 +02:00
Maciej Winnicki a6c98e46d8
Update deps 2018-07-25 10:00:00 +02:00
Maciej Winnicki 500c505137
Update TravisCI node versions 2018-07-25 09:48:24 +02:00
Maciej Winnicki c8b02c6246
Add relesing guide 2018-07-25 09:46:08 +02:00
Maciej Winnicki c2f98d5554
Release v4.2.1 2018-07-25 09:37:23 +02:00
Maciej Winnicki b19ea8ae1d
Publish to NPM via Travis 2018-07-25 09:33:33 +02:00
Andrew Wright 1e54a7786c Add viewport width to support responsive design (#121)
The UI is built to respond to the width of the screen it is being
displayed on and provide the best experience across device. When I tried
to view logs streaming on my phone (Apple iPhone 6s), site was
attempting to render the "bigger-screen" version.

This is because, by default, mobile browsers advertise a bigger "view
port" than screen size, to allow compability with non-responsive sites.
In order to benefit from the responsiveness provided the viewport meta
tag needs to be overwritten. More on that tag can be found at
(mdn)[https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag].
2018-04-11 23:24:08 +02:00
Maciej Winnicki 0623cd91aa
4.2.0 2018-04-07 19:39:59 +02:00
Maciej Winnicki 30e8eb04c3
add pkg to package.json 2018-04-07 19:38:39 +02:00
Maciej Winnicki 35f7e0e80a
add --url-path option. Closes #108 (#120)
* Added configuration to fix #108

* Conflicting option

* Updated README

* Fix for web sockets behind nginx

* make url path working without nginx

* update readme
2018-04-07 14:16:20 +02:00
Maciej Winnicki 9be6486c92
release 4.1.1 2018-02-07 22:57:40 +01:00
Maciej Winnicki 6ad045f9e0
replace git repo dependency with forked package. Closes #110 2018-02-07 22:57:04 +01:00
Maciej Winnicki 0f0e8d5044
update readme with info about binary files 2018-01-03 14:10:18 +01:00
Maciej Winnicki baa9e3de7b
release 4.1.0 2018-01-03 14:00:16 +01:00
Maciej Winnicki 19b15685b9
update socket.io dep. Closes #103 2018-01-03 13:56:52 +01:00
Maciej Winnicki 6cf8a3391f
prepare to be used as a standalone binary package with pkg 2018-01-03 13:49:37 +01:00
Maciej Winnicki 87bfcff493
remove codesponso 2017-11-27 19:38:52 +01:00
Todsaporn Banjerdkit 6d3f7c2439 Update README.md (#102) 2017-10-25 22:16:21 +02:00
Maciej Winnicki 98a3003902 add codesponsor banner 2017-09-29 14:46:24 +02:00
Maciej Winnicki 439194393f
add docker download badge 2017-09-26 16:22:49 +02:00
Maciej Winnicki 200f8f8fbf
release 4.0.4 2017-08-22 16:36:26 +02:00
Maciej Winnicki 756cbee2a5
use byline also for stdin 2017-08-22 16:28:21 +02:00
Maciej Winnicki bc8eb6cada add gif to the README file 2017-08-22 16:26:40 +02:00
Maciej Winnicki de39dc6ca1
release 4.0.3 2017-08-21 12:24:23 +02:00
Maciej Winnicki 71dc8ceb83
use byline for supporting large files 2017-08-21 12:22:15 +02:00
Maciej Winnicki be1eefcd1e
fix deamon error. Closes #100 2017-08-21 11:48:48 +02:00
Maciej Winnicki 2b6923a9b1 Merge pull request #96 from yagop/patch-1
Check if cookies[sessionKey] exists
2017-07-03 11:41:22 +02:00
Yago fcd8ee906e Check if cookies[sessionKey] exists 2017-06-29 16:16:33 +02:00
Maciej Winnicki 16b064c545 update screenshot 2017-04-10 11:35:20 +02:00
Maciej Winnicki 9994060700
fix updating favico every line 2017-02-27 15:30:13 +01:00
Maciej Winnicki ce7aadcd4e
fix typo 2017-01-31 10:49:23 +01:00
Maciej Winnicki d9748a8133 Merge pull request #87 from mindflowers/form-enter
Disable enter in filter form
2017-01-31 10:49:15 +01:00
mindflowers 8d78f2e7f5 Disable enter in filter form 2017-01-31 11:50:23 +05:30
Maciej Winnicki ece7dc9c2a update README 2016-10-06 10:35:25 +02:00
Maciej Winnicki 73a667c0d6
fix showing empty lines. Closes #78. Closes #79 2016-10-05 16:16:43 +02:00
Maciej Winnicki 31e25acce6
update coding standard 2016-10-05 15:57:53 +02:00
Maciej Winnicki 432716822d
update readme 2016-10-05 09:54:58 +02:00
Maciej Winnicki 9dbe9b2f51
support stdin 2016-10-05 09:51:57 +02:00
Maciej Winnicki fb6560e0e1
remove SSH feature 2016-10-04 19:49:18 +02:00
Maciej Winnicki 7007f92715
release 3.1.2 2016-09-04 18:55:10 +02:00
Maciej Winnicki fc6128160c
remove server side sanitization (it's done by ansi_up in frontend). Closes #75 2016-09-04 18:30:57 +02:00
Maciej Winnicki b74dfc805e
update readme 2016-09-04 18:01:43 +02:00
Maciej Winnicki 70ef84434f
release v3.1.1 2016-09-04 17:46:54 +02:00
Maciej Winnicki a304ac098b
fix lint 2016-09-04 17:44:25 +02:00
Maciej Winnicki ba362a4100
version bump 2016-09-04 17:31:53 +02:00
Maciej Winnicki 5c582d4b2d
update socket.io lib 2016-09-04 17:31:35 +02:00
Maciej Winnicki 4949ca416d Migration to Node v4. Closes #69 2016-05-05 09:58:03 +02:00
Maciej Winnicki 837901debb replace jshint jscs with eslint 2016-05-03 19:12:15 +02:00
Maciej Winnicki 658ccb7a23 Lint; v2.3.0 2016-03-30 19:23:15 +02:00
Maciej Winnicki a8f6942b5c Merge pull request #65 from MAD-GooZe/master
Displaying terminal ansi color characters
2016-03-30 19:10:37 +02:00
Alexey Gusev 0c6068b30e enable html escaping && load ansi_up.js in tests 2016-03-30 19:29:29 +03:00
Alexey Gusev 7c8f63d6c1 ansi_up.js && version bump 2016-03-30 19:06:10 +03:00
Maciej Winnicki c38de1be51 fix preset loading. v2.2.1 2016-01-29 13:36:21 +01:00
Maciej Winnicki b5865d5423 v bump 2016-01-25 12:57:05 +01:00
Maciej Winnicki 3e1c620b24 Typo 2016-01-25 12:55:51 +01:00
Maciej Winnicki b025ff6dbd Merge pull request #61 from andresol/master
Fix for tail'ing over network
2016-01-25 12:54:52 +01:00
andresol 5e96521809 Fix for tail'ing over network
Do not kill tail on error. Just log. File trunkcated is common on
tailing over windows.
2016-01-25 12:24:52 +01:00
Maciej Winnicki 2ac11eb0ac Stylezz 2016-01-11 10:58:52 +01:00
Maciej Winnicki a7cd6897ad Properly handle signals. Closes #57 2016-01-11 10:53:52 +01:00
Maciej Winnicki 2bff6b68de Update README.md 2015-10-07 10:08:44 +02:00
Maciej Winnicki 3df000920c Info about tailing multiple files 2015-09-17 10:05:53 +02:00
Maciej Winnicki 054eb70c46 Test escaping HTML fix #50 2015-09-16 23:54:14 +02:00
Maciej Winnicki 3879a6eb79 Update README.md
[ci skip]
2015-07-07 23:13:34 +02:00
Maciej Winnicki 2bfb862258 Version bump [ci skip] 2015-07-06 19:36:28 +02:00
Maciej Winnicki a1eef74c73 Merge pull request #49 from matteocontrini/master
Escape HTML tags
2015-07-06 19:31:44 +02:00
Matteo Contrini a10c35018a Code style 2015-07-05 13:07:48 +02:00
Matteo Contrini 99c37d6d99 Escape HTML tags 2015-07-05 13:00:22 +02:00
Maciej Winnicki 95671f0164 Info about docker 2015-04-23 20:05:06 +02:00
Maciej Winnicki d50b6d4177 Version bump [ci skip] 2015-03-27 12:14:15 +01:00
Maciej Winnicki a321a09bf3 Test against 0.12 2015-03-27 12:07:07 +01:00
Maciej Winnicki 511a33e0d3 Handle multiple files 2015-03-27 12:06:06 +01:00
Maciej Winnicki a65f5f4104 Doc update 2015-03-03 23:49:31 +01:00
Maciej Winnicki 019ce4acd9 2.0.0 release 2015-03-03 23:45:09 +01:00
Maciej Winnicki 654a62c522 Fixed highlighting in deamon mode (fix #44) 2015-03-03 23:44:09 +01:00
Maciej Winnicki 80607e1237 New option --ui-higlight-preset 2015-03-03 23:43:36 +01:00
Maciej Winnicki b85c8284bc Code refactor + test for ssh 2015-02-23 21:27:09 +01:00
Maciej Winnicki 4711e72e48 Simpler notation 2015-02-23 20:56:10 +01:00
Maciej Winnicki 15f1c39d15 Merge pull request #42 from mdelapenya/ssh-daemonize
[#41] Pass SSH parameters to daemon
2015-02-23 20:45:04 +01:00
Manuel de la Peña c0b796c5b0 [#42] Add another test: without SSH configuration 2015-02-23 20:04:35 +01:00
Manuel de la Peña d7ec19d5a4 [#42] Update test with doSSH approach 2015-02-23 20:04:00 +01:00
Manuel de la Peña 1d8fb184c3 [#42] Follow doSecure approach for SSH connections 2015-02-23 20:03:25 +01:00
Manuel de la Peña 3909011c1c [#41] Source formatting (Travis-ci complained about line length) 2015-02-20 14:55:03 +01:00
Manuel de la Peña bcfbd599bc [#41] Add a test to verify the issue 2015-02-20 14:49:45 +01:00
Manuel de la Peña d45568a11d [#41] Pass SSH parameters to daemon 2015-02-20 14:12:51 +01:00
Maciej Winnicki 77bddbe17e README update with info about ssh. Version bump. 2015-02-10 00:30:16 +01:00
Maciej Winnicki 3b1b976c87 Merge pull request #39 from RoCat/master
add remote tail over ssh
2015-02-10 00:23:46 +01:00
rcatoio 2bfb6b1c51 simplify error catching 2015-02-04 17:13:34 +01:00
rcatoio 82618c8cc9 fix codestyle 2015-02-04 16:45:03 +01:00
rcatoio d8f9b491ea add remote tail over ssh 2015-02-04 14:22:35 +01:00
Maciej Winnicki a5d7b9e797 Update README.md 2014-11-28 11:25:45 +01:00
Maciej Winnicki ebe8d0abba New NPM badge 2014-11-14 20:48:34 +01:00
Maciej Winnicki 70b5c7c61f New version 2014-10-08 00:12:29 +02:00
Maciej Winnicki 9fc775997f Updated readme with info about highlight 2014-10-08 00:11:38 +02:00
Maciej Winnicki c4caed9a58 Redundant code moved to before each 2014-10-07 23:58:30 +02:00
Maciej Winnicki 79f63d39fb Some parameters moved from jshint to jscs 2014-10-07 23:40:55 +02:00
Maciej Winnicki 6223033bb0 Merge pull request #36 from mthenw/highlights
--ui-highlight
2014-10-06 22:59:45 +02:00
Maciej Winnicki 3435c986aa Tests and README update 2014-10-06 22:55:51 +02:00
Maciej Winnicki 38c256a9f3 Merge branch 'master' into highlights 2014-10-05 15:02:06 +02:00
Maciej Winnicki 14bbc135db temp package update because of travis build failure 2014-10-05 14:57:07 +02:00
Maciej Winnicki a74a7b75fe jscs check 2014-10-05 14:33:25 +02:00
Maciej Winnicki d0b1cdcc98 npm scripts instead of Makefile 2014-10-05 13:11:02 +02:00
Patrik Johnson 4d658ed940 Apply css stylings to either lines or individual words, if matching strings are found in a supplied configuration file 2014-10-02 00:38:16 +03:00
Maciej Winnicki b57fa79588 Missing ; 2014-07-13 02:45:15 +02:00
Maciej Winnicki 78076c1b8e Buffer lines on start (fixes #33) 2014-07-13 02:42:21 +02:00
Maciej Winnicki bd21fe14c7 Travis don't suppor 0.9 2014-05-29 01:29:16 +02:00
Maciej Winnicki b439f5ff90 Updated travis worker to stable version 2014-05-29 01:26:41 +02:00
Maciej Winnicki cbde520735 Updated travis worker's node version 2014-05-29 01:23:19 +02:00
Maciej Winnicki 9ace721324 daemonize with all cli parameters 2014-05-29 01:14:05 +02:00
Maciej Winnicki c3fbf35568 New description 2014-04-26 12:45:09 +02:00
Maciej Winnicki f573abe064 Not needed info 2014-03-29 21:47:26 +01:00
Maciej Winnicki bd6abfd8ef Forgot about lint 2014-03-29 21:44:40 +01:00
Maciej Winnicki a32ee45021 Two UI flags: hide topbar & no indent 2014-03-29 21:37:56 +01:00
Maciej Winnicki c58955c627 Readme fixes 2014-03-06 23:20:12 +01:00
Maciej Winnicki 662cfdc43b npm package version bumped 2014-03-01 00:36:30 +01:00
Maciej Winnicki f5363719be Screenshot updated 2014-03-01 00:28:03 +01:00
Maciej Winnicki f51f522ec5 Merge pull request #31 from mthenw/frontend-tested
Tests for browser app
2014-02-28 23:44:43 +01:00
Maciej Winnicki cd67c512d1 not needed new lines 2014-02-28 23:42:37 +01:00
Maciej Winnicki 9dc749cb67 event method extracted 2014-02-28 23:12:19 +01:00
Maciej Winnicki 55ef83fc00 jQuery not needed 2014-02-24 00:43:59 +01:00
Maciej Winnicki 0a2911dd3e Basic test for browser app 2014-02-23 22:59:50 +01:00
Maciej Winnicki c9d03a2635 twtr bootstrap from local file 2014-02-21 20:17:24 +01:00
Maciej Winnicki 9fdcbf4a7c test-watch task to Makefile 2014-02-21 20:11:41 +01:00
Maciej Winnicki d8355db7f4 Merge pull request #29 from mthenw/flat-design
New colours based on twtr bootstrap
2014-02-15 14:43:51 +01:00
Maciej Winnicki 903f33d5ad New colours based on twtr bootstrap 2014-02-15 14:40:14 +01:00
Maciej Winnicki 2e46b3dc0b Updated screenshot 2014-01-10 15:53:05 +01:00
Maciej Winnicki 0f392b35ed Version bump 2014-01-10 15:48:42 +01:00
Maciej Winnicki ec1bd2b138 Merge pull request #28 from mthenw/pr/22
Dynamic Favicon shows number of new lines
2014-01-10 06:46:23 -08:00
Maciej Winnicki ef5a6d6843 Info about unreaded counter 2014-01-10 15:40:59 +01:00
Maciej Winnicki 868621eba1 Lint fixes 2014-01-10 15:34:34 +01:00
Maciej Winnicki af0759bac2 code fixes; new icon 2014-01-10 15:30:15 +01:00
Maciej Winnicki 007ee800b7 Merge branch 'master' into pr/22 2014-01-10 14:29:12 +01:00
Maciej Winnicki 317240d02f One level down 2013-11-24 13:55:47 +01:00
Maciej Winnicki 4588a7a611 License in separate file 2013-11-24 12:26:44 +01:00
Maciej Winnicki 7bb3c4dd1d Help message updated 2013-11-06 19:53:28 +01:00
Maciej Winnicki a418fed728 Too long lines 2013-11-06 19:52:08 +01:00
Maciej Winnicki 4efa0cf4a7 --help cleanup 2013-11-06 19:41:02 +01:00
Maciej Winnicki 283c486ac3 v0.6.0 2013-11-06 19:31:48 +01:00
Maciej Winnicki d8b31ff5f6 Merge pull request #23 from mthenw/file-as-ns
Files as socket.io namespace
2013-11-06 10:30:33 -08:00
Maciej Winnicki 46a4ce21cb Cleaner args 2013-11-06 18:54:50 +01:00
Maciej Winnicki 42abdf2b0b Merge branch 'master' into file-as-ns
Conflicts:
	index.js
	test/connect_builder.js
2013-11-06 18:34:36 +01:00
Maciej Winnicki 4eb92b67f2 supertest 2013-11-06 18:00:57 +01:00
Maciej Winnicki 437986a49e server builder 2013-11-05 23:02:17 +01:00
Maciej Winnicki 24c2098edb should 2013-11-05 17:32:22 +01:00
Maciej Winnicki 146c53500c Files on socket.io namespace 2013-11-05 00:19:23 +01:00
Maciej Winnicki fcbef4a122 Test cleanup 2013-11-04 23:26:36 +01:00
Maciej Winnicki 222a93ed3c npm module badge 2013-11-03 13:34:20 +01:00
Maciej Winnicki 33d15c1408 New screenshot in readme; v0.5.0 2013-11-01 22:54:49 +01:00
lexx 5508492850 lint fixes 2013-11-01 23:36:45 +02:00
lexx 2ae3eb9b6c merger master 2013-11-01 23:24:25 +02:00
Maciej Winnicki 5b26b6cd26 Theme as another param 2013-11-01 22:17:52 +01:00
Maciej Winnicki a3aead7036 Merge pull request #21 from lexx27/master
Give the option change theme
2013-11-01 14:04:46 -07:00
lexx 5907a8922c lint fix 2013-11-01 19:14:47 +02:00
lexx 8a7d5f6c88 lint fixes 2013-11-01 19:06:19 +02:00
lexx a82636f8db fixing tests 2013-11-01 18:38:54 +02:00
lexx 784857d521 updated readme 2013-10-31 20:19:06 +02:00
lexx 0972c6fd0a added style setting in daemon mode and passed theme option to html with the new logic 2013-10-31 20:13:41 +02:00
Maciej Winnicki aa3992edaa Connection builder returns app with index 2013-10-31 16:41:49 +01:00
Maciej Winnicki f92de2f7c2 Missing lint 2013-10-31 00:39:52 +01:00
Maciej Winnicki a6fe17462f Connect builder 2013-10-31 00:26:30 +01:00
lexx c4d3c5faa9 move code away from io event 2013-10-30 00:53:05 +02:00
lexx 5aa40fe448 favicon show new lines 2013-10-30 00:11:51 +02:00
lexx 54897e009d merge master 2013-10-29 23:44:57 +02:00
lexx 01e7745aec remove css file 2013-10-29 22:57:02 +02:00
lexx 584402fbf4 minor changes 2013-10-29 22:56:39 +02:00
lexx 49b3b7e742 dynamic favicon 2013-10-29 22:42:22 +02:00
Maciej Winnicki 2c98b8c587 Listening host fix #19 2013-10-29 19:51:42 +01:00
Maciej Winnicki 8ea077dcb9 Travis status [ci skip] 2013-10-29 19:40:47 +01:00
Maciej Winnicki 22ea4908b6 Tests via Travis 2013-10-29 19:31:37 +01:00
lexx 3e93cac335 let's be consistent with naming 2013-10-29 06:40:24 +02:00
lexx db96f9dd2a User can select css style as option 2013-10-29 06:38:59 +02:00
Maciej Winnicki c2d27a3b01 Tail tests 2013-10-25 16:56:55 +02:00
Maciej Winnicki bab90d4b60 Basic version 2013-10-23 21:54:24 +02:00
Maciej Winnicki c7fbe1ac19 travis ci 2013-09-21 12:09:17 +02:00
Maciej Winnicki 69fb952ce0 typo 2013-09-21 12:08:10 +02:00
Maciej Winnicki 021e5c6955 lint 2013-09-21 12:00:38 +02:00
Maciej Winnicki c9736566b0 0.4.2 2013-08-22 17:51:54 +02:00
Maciej Winnicki 8dc0267997 package.json shortened 2013-08-22 17:47:20 +02:00
Maciej Winnicki 17255399b9 Moved to index.js 2013-08-22 17:46:35 +02:00
Maciej Winnicki ac0f163901 Packages updated 2013-08-22 17:33:53 +02:00
Maciej Winnicki 3fc98aca3c to lower 2013-07-16 18:44:06 +02:00
Maciej Winnicki 62a24e48b4 Update README.md 2013-07-07 22:17:32 +02:00
Maciej Winnicki ca8f2e4ee4 0.4.1 2013-06-20 19:14:20 +02:00
Maciej Winnicki 0efceb288c Merge pull request #17 from ardavis/master
Add HTTPS Support fix #15 #17
2013-06-20 10:06:46 -07:00
Andrew Davis 801b9e05a0 Add HTTPS ability and update README 2013-06-20 12:19:07 -04:00
Maciej Winnicki 5ad7f19c66 Update README.md 2013-06-20 08:38:36 +03:00
Maciej Winnicki e8c8193d42 Options help fixed 2013-06-20 07:37:32 +02:00
Maciej Winnicki 5b0c537387 Update README.md 2013-06-01 23:54:23 +02:00
Maciej Winnicki d8473a564a No test no travis 2013-05-26 11:27:38 +02:00
Maciej Winnicki 987f285ead Basic Auth - closes #14 2013-05-26 11:24:22 +02:00
Maciej Winnicki 5d3506b172 README proper order 2013-05-20 12:30:18 +02:00
Maciej Winnicki 63d5daa43f Cleanup 2013-04-03 18:45:13 +02:00
Maciej Winnicki 335ab564d5 Version bumped 2013-03-28 17:11:02 +01:00
Maciej Winnicki 2962711cc3 daemon instead of forever 2013-03-28 17:10:37 +01:00
Maciej Winnicki 8098631076 -l info added 2013-02-27 23:41:54 +01:00
Maciej Winnicki 9b0de5ed21 Limit lines in frontend; fixes #11 2013-02-27 23:40:20 +01:00
Maciej Winnicki c481564261 Use forever instead of node-daemon 2013-02-27 00:27:09 +01:00
Maciej Winnicki 668a3d67f2 Version bumped 2013-02-15 16:01:12 +01:00
Maciej Winnicki 4db7df502a Merge pull request #9 from skarnecki/z-data-sanitized
Sanitize log data
2013-02-15 06:58:28 -08:00
skarnecki 6913af8d9b Update lib/frontail.js 2013-02-15 15:55:38 +01:00
Szymon Karnecki 135055dbae Wrong file in commit - removed 2013-02-15 15:52:52 +01:00
Szymon Karnecki 503e9803be Sanitize log data 2013-02-15 15:46:52 +01:00
Maciej Winnicki 12bc47efff v0.2.12 2012-09-27 11:41:26 +02:00
Maciej Winnicki 210b83a413 v0.2.11 2012-09-27 11:40:04 +02:00
Maciej Winnicki fd155faf1f v0.2.10 2012-09-27 11:33:36 +02:00
Maciej Winnicki 883d02bc0d v0.2.9; frontail avaiable on older node versions 2012-09-27 11:16:15 +02:00
Maciej Winnicki fb40f0a3d4 Merge pull request #8 from travis4all/clean
The repo your repo could be like!
2012-08-23 08:09:41 -07:00
travis4all 8bf1ff5ac0 💎 Travis CI image/link in readme 💎 2012-08-23 12:07:09 +00:00
travis4all 855aacef50 💎 Added travis.yml file 💎 2012-08-23 12:07:09 +00:00
Maciej Winnicki 8ac8aed7f8 v0.2.8 2012-08-22 21:19:22 +02:00
Maciej Winnicki 7943632363 Filter value can be regex pattern. fix #6 2012-08-21 01:19:53 +02:00
Maciej Winnicki 37e7c1bf59 JShinted 2012-07-24 20:01:18 +02:00
Maciej Winnicki 8ba3de12b7 Version bumped 2012-07-24 19:37:12 +02:00
Maciej Winnicki 95b9454166 dependencies bumped 2012-07-24 19:35:48 +02:00
Maciej Winnicki 01dd23f16c Bumped commander version; --version is taken from package.json 2012-07-24 19:26:21 +02:00
Maciej Winnicki 09bb6bf4f0 Missing files 2012-05-15 22:46:35 +02:00
Maciej Winnicki 5aa113df46 Styles and app script in separate files 2012-05-15 22:46:12 +02:00
Maciej Winnicki 12a9aa6058 v0.2.6 2012-05-06 22:10:26 +02:00
Maciej Winnicki 42b6ff6279 Fixed args passing while tailing multiple files. fixes #4 2012-05-06 22:05:42 +02:00
Maciej Winnicki 9885e9f70d Merge pull request #1 from gabrys/master
Better indent
2012-04-07 01:37:31 -07:00
Piotr Gabryjeluk f97759b86d Adjusting the indent to match character width. 2012-04-06 21:33:36 +03:00
Maciej Winnicki 2112fba4c3 Filtering affects incoming logs; v0.2.5 2012-04-05 20:50:34 +02:00
Maciej Winnicki 5b0961b958 New screen 2012-04-05 09:44:57 +02:00
Maciej Winnicki dde1185be6 New screen 2012-04-05 09:33:06 +02:00
Maciej Winnicki 56571fd891 Readme another update 2012-04-04 22:02:10 +02:00
Maciej Winnicki 65c5d8ea64 Readme update 2012-04-04 21:50:18 +02:00
Maciej Winnicki 1d80d2719a Installation instruction 2012-04-04 21:23:50 +02:00
Maciej Winnicki 3cea5976e2 v0.2.4 2012-04-04 21:19:09 +02:00
Maciej Winnicki 518a00f5c0 Readme update 2012-04-04 21:16:04 +02:00
Maciej Winnicki 1abc1b422c Selecting lines feature 2012-04-04 21:15:52 +02:00
Maciej Winnicki 5ad285b74a v0.2.3 2012-04-04 19:35:31 +02:00
Maciej Winnicki ec423ac6a8 Readme update 2012-04-04 19:33:28 +02:00
Maciej Winnicki db58d44697 Disable socket.io logging 2012-04-04 19:25:55 +02:00
43 changed files with 6481 additions and 347 deletions

20
.github/workflows/push.yml vendored Normal file
View File

@ -0,0 +1,20 @@
on: push
name: Build, Lint, Test, and Publish
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm install
- run: npm test
- run: npm run lint
- uses: primer/publish@master
if: github.ref == 'refs/heads/master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
NPM_REGISTRY_URL: registry.npmjs.org

4
.gitignore vendored
View File

@ -1 +1,5 @@
node_modules
test/fixtures/*.pem
npm-debug.log
dist
.DS_Store

16
.travis.yml Normal file
View File

@ -0,0 +1,16 @@
language: node_js
node_js:
- 6
- 8
- 10
script:
- npm run lint
- npm test
deploy:
provider: npm
email: maciej.winnicki@gmail.com
api_key:
secure: OuE8bOpsq/XN2AL6z0SWqHtpfKB5Qg3wug+hZVka+AdWaU6Gm0uvs4yA7B/uw9o5gH6KKgM+CP50Lq1xhC12CZbKLjkAUuFJQb9FrU/7jEAp+t+DKpoVBcru8bBjTxxx4wO1zXFcbJZ9zb4c4h76j2nfaNVuPTBB+mDF/HDd914=
on:
tags: true
repo: mthenw/frontail

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:12-buster-slim
WORKDIR /frontail
ADD . .
RUN npm install --production
ENTRYPOINT ["/frontail/docker-entrypoint.sh"]
EXPOSE 9001
CMD ["--help"]

31
LICENSE
View File

@ -1,21 +1,20 @@
Copyright 2012 Maciej Winnicki http://maciejwinnicki.pl
The MIT License (MIT)
This project is free software released under the MIT/X11 license:
Copyright (c) 2017 Maciej Winnicki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

130
README.md
View File

@ -1,8 +1,36 @@
# frontail tail -F output in browser
# frontail streaming logs to the browser
## Introduction
`frontail` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI.
frontail is node.js application for serving `tail -F` output to browser using [socket.io](http://socket.io/).
![frontial](https://user-images.githubusercontent.com/455261/29570317-660c8122-8756-11e7-9d2f-8fea19e05211.gif)
[![Docker Pulls](https://img.shields.io/docker/pulls/mthenw/frontail.svg)](https://hub.docker.com/r/mthenw/frontail/)
## Quick start
- `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
- `frontail /var/log/syslog`
- visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
## Features
- log rotation (not on Windows)
- auto-scrolling
- marking logs
- pausing logs
- number of unread logs in favicon
- themes (default, dark)
- [highlighting](#highlighting)
- search (`Tab` to focus, `Esc` to clear)
- set filter from url parameter `filter`
- tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
- basic authentication
## Installation options
- download a binary file from [Releases](https://github.com/mthenw/frontail/releases) pagegit st
- using [npm package](https://www.npmjs.com/package/frontail): `npm i frontail -g`
- using [Docker image](https://cloud.docker.com/repository/docker/mthenw/frontail): `docker run -d -P -v /var/log:/log mthenw/frontail /log/syslog`
## Usage
@ -10,7 +38,95 @@ frontail is node.js application for serving `tail -F` output to browser using [s
Options:
-h, --help output usage information
-V, --version output the version number
-p, --port <port> server port, default 9001
-n, --number <number> starting lines number, default 10
-V, --version output the version number
-h, --host <host> listening host, default 0.0.0.0
-p, --port <port> listening port, default 9001
-n, --number <number> starting lines number, default 10
-l, --lines <lines> number on lines stored in browser, default 2000
-t, --theme <theme> name of the theme (default, dark)
-d, --daemonize run as daemon
-U, --user <username> Basic Authentication username, option works only along with -P option
-P, --password <password> Basic Authentication password, option works only along with -U option
-k, --key <key.pem> Private Key for HTTPS, option works only along with -c option
-c, --certificate <cert.pem> Certificate for HTTPS, option works only along with -k option
--pid-path <path> if run as daemon file that will store the process id, default /var/run/frontail.pid
--log-path <path> if run as daemon file that will be used as a log, default /dev/null
--url-path <path> URL path for the browser application, default /
--ui-hide-topbar hide topbar (log file name and search box)
--ui-no-indent don't indent log lines
--ui-highlight highlight words or lines if defined string found in logs, default preset
--ui-highlight-preset <path> custom preset for highlighting (see ./preset/default.json)
--path <path> prefix path for the running application, default /
--disable-usage-stats disable gathering usage statistics
--help output usage information
Web interface runs on **http://[host]:[port]**.
### Tailing multiple files
`[file ...]` accepts multiple paths, `*`, `?` and other shell special characters([Wildcards, Quotes, Back Quotes and Apostrophes in shell commands](http://www.codecoffee.com/tipsforlinux/articles/26-1.html)).
### stdin
Use `-` for streaming stdin:
./server | frontail -
### Highlighting
`--ui-highlight` option turns on highlighting in UI. By default preset from `./preset/default.json` is used:
```
{
"words": {
"err": "color: red;"
},
"lines": {
"err": "font-weight: bold;"
}
}
```
which means that every "err" string will be in red and every line containing "err" will be bolded.
_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._
Available presets:
- default
- npmlog
- python
### Running behind nginx
Using the `--url-path` option `frontail` can run behind nginx with the example configuration
Using `frontail` with `--url-path /frontail`
```
events {
worker_connections 1024;
}
http {
server {
listen 8080;
server_name localhost;
location /frontail {
proxy_pass http://127.0.0.1:9001/frontail;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
```
### Usage statistics
`frontail` by default (from `v4.5.0`) gathers **anonymous** usage statistics in Google Analytics. It can be disabled with
`--disable-usage-stats`.
The data is used to help me understand how `frontail` is used and I can make it better.

40
RELEASING.md Normal file
View File

@ -0,0 +1,40 @@
# Releasing Frontail
After all [pull requests](https://github.com/mthenw/frontail/pulls) for a release have been merged and all [Travis CI builds](https://travis-ci.org/mthenw/frontail) are green, you may create a release as follows:
1. If you haven't already, switch to the master branch, ensure that you have no changes, and pull from origin.
```sh
$ git checkout master
$ git status
$ git pull <remote> master --rebase
```
1. Edit the `package.json` file changing `version` field to your new release version and run `npm i`.
1. Commit your changes.
```sh
$ git commit -am "Release <version>"
```
1. Push the commit.
```sh
$ git push origin head
```
1. GitHub action will publish new version to NPM and push new tag.
1. Publish new release on GitHub with [`release`](https://github.com/zeit/release) package.
```sh
$ git pull
$ npx release -P
```
1. Upload binaries
```sh
$ npm run pkg
```

View File

@ -1,2 +1,2 @@
#!/usr/bin/env node
require('./../lib/frontail');
require('./../index');

5
docker-entrypoint.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
exec ./bin/frontail $@

171
index.js Normal file
View File

@ -0,0 +1,171 @@
'use strict';
const cookie = require('cookie');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const path = require('path');
const { Server } = require('socket.io');
const fs = require('fs');
const untildify = require('untildify');
const tail = require('./lib/tail');
const connectBuilder = require('./lib/connect_builder');
const program = require('./lib/options_parser');
const serverBuilder = require('./lib/server_builder');
const daemonize = require('./lib/daemonize');
const usageStats = require('./lib/stats');
/**
* Parse args
*/
program.parse(process.argv);
if (program.args.length === 0) {
console.error('Arguments needed, use --help');
process.exit();
}
/**
* Init usage statistics
*/
const stats = usageStats(!program.disableUsageStats, program);
stats.track('runtime', 'init');
stats.time('runtime', 'runtime');
/**
* Validate params
*/
const doAuthorization = !!(program.user && program.password);
const doSecure = !!(program.key && program.certificate);
const sessionSecret = String(+new Date()) + Math.random();
const files = program.args.join(' ');
const filesNamespace = crypto.createHash('md5').update(files).digest('hex');
const urlPath = program.urlPath.replace(/\/$/, ''); // remove trailing slash
if (program.daemonize) {
daemonize(__filename, program, {
doAuthorization,
doSecure,
});
} else {
/**
* HTTP(s) server setup
*/
const appBuilder = connectBuilder(urlPath);
if (doAuthorization) {
appBuilder.session(sessionSecret);
appBuilder.authorize(program.user, program.password);
}
appBuilder
.static(path.join(__dirname, 'web', 'assets'))
.index(
path.join(__dirname, 'web', 'index.html'),
files,
filesNamespace,
program.theme
);
const builder = serverBuilder();
if (doSecure) {
builder.secure(program.key, program.certificate);
}
const server = builder
.use(appBuilder.build())
.port(program.port)
.host(program.host)
.build();
/**
* socket.io setup
*/
const io = new Server({ path: `${urlPath}/socket.io` });
io.attach(server);
if (doAuthorization) {
io.use((socket, next) => {
const handshakeData = socket.request;
if (handshakeData.headers.cookie) {
const cookies = cookie.parse(handshakeData.headers.cookie);
const sessionIdEncoded = cookies['connect.sid'];
if (!sessionIdEncoded) {
return next(new Error('Session cookie not provided'), false);
}
const sessionId = cookieParser.signedCookie(
sessionIdEncoded,
sessionSecret
);
if (sessionId) {
return next(null);
}
return next(new Error('Invalid cookie'), false);
}
return next(new Error('No cookie in header'), false);
});
}
/**
* Setup UI highlights
*/
let highlightConfig;
if (program.uiHighlight) {
let presetPath;
if (!program.uiHighlightPreset) {
presetPath = path.join(__dirname, 'preset', 'default.json');
} else {
presetPath = path.resolve(untildify(program.uiHighlightPreset));
}
if (fs.existsSync(presetPath)) {
highlightConfig = JSON.parse(fs.readFileSync(presetPath));
} else {
throw new Error(`Preset file ${presetPath} doesn't exists`);
}
}
/**
* When connected send starting data
*/
const tailer = tail(program.args, {
buffer: program.number,
});
const filesSocket = io.of(`/${filesNamespace}`).on('connection', (socket) => {
socket.emit('options:lines', program.lines);
if (program.uiHideTopbar) {
socket.emit('options:hide-topbar');
}
if (!program.uiIndent) {
socket.emit('options:no-indent');
}
if (program.uiHighlight) {
socket.emit('options:highlightConfig', highlightConfig);
}
tailer.getBuffer().forEach((line) => {
socket.emit('line', line);
});
});
/**
* Send incoming data
*/
tailer.on('line', (line) => {
filesSocket.emit('line', line);
});
stats.track('runtime', 'started');
/**
* Handle signals
*/
const cleanExit = () => {
stats.timeEnd('runtime', 'runtime', () => {
process.exit();
});
};
process.on('SIGINT', cleanExit);
process.on('SIGTERM', cleanExit);
}

75
lib/connect_builder.js Normal file
View File

@ -0,0 +1,75 @@
'use strict';
const connect = require('connect');
const fs = require('fs');
const serveStatic = require('serve-static');
const expressSession = require('express-session');
const basicAuth = require('basic-auth-connect');
function ConnectBuilder(urlPath) {
this.app = connect();
this.urlPath = urlPath;
}
ConnectBuilder.prototype.authorize = function authorize(user, pass) {
this.app.use(
this.urlPath,
basicAuth(
(incomingUser, incomingPass) =>
user === incomingUser && pass === incomingPass
)
);
return this;
};
ConnectBuilder.prototype.build = function build() {
return this.app;
};
ConnectBuilder.prototype.index = function index(
path,
files,
filesNamespace,
themeOpt
) {
const theme = themeOpt || 'default';
this.app.use(this.urlPath, (req, res) => {
fs.readFile(path, (err, data) => {
res.writeHead(200, {
'Content-Type': 'text/html',
});
res.end(
data
.toString('utf-8')
.replace(/__TITLE__/g, files)
.replace(/__THEME__/g, theme)
.replace(/__NAMESPACE__/g, filesNamespace)
.replace(/__PATH__/g, this.urlPath),
'utf-8'
);
});
});
return this;
};
ConnectBuilder.prototype.session = function sessionf(secret) {
this.app.use(
this.urlPath,
expressSession({
secret,
resave: false,
saveUninitialized: true,
})
);
return this;
};
ConnectBuilder.prototype.static = function staticf(path) {
this.app.use(this.urlPath, serveStatic(path));
return this;
};
module.exports = (urlPath) => new ConnectBuilder(urlPath);

69
lib/daemonize.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const daemon = require('daemon-fix41');
const fs = require('fs');
const defaultOptions = {
doAuthorization: false,
doSecure: false,
};
module.exports = (script, params, opts) => {
const options = opts || defaultOptions;
const logFile = fs.openSync(params.logPath, 'a');
let args = [
'-h',
params.host,
'-p',
params.port,
'-n',
params.number,
'-l',
params.lines,
'-t',
params.theme,
];
if (options.doAuthorization) {
args.push('-U', params.user, '-P', params.password);
}
if (options.doSecure) {
args.push('-k', params.key, '-c', params.certificate);
}
if (params.uiHideTopbar) {
args.push('--ui-hide-topbar');
}
if (params.urlPath) {
args.push('--url-path', params.urlPath);
}
if (!params.uiIndent) {
args.push('--ui-no-indent');
}
if (params.uiHighlight) {
args.push('--ui-highlight');
}
if (params.uiHighlightPreset) {
args.push('--ui-highlight-preset', params.uiHighlightPreset);
}
if (params.disableUsageStats) {
args.push('--disable-usage-stats', params.disableUsageStats);
}
args = args.concat(params.args);
const proc = daemon.daemon(script, args, {
stdout: logFile,
stderr: logFile,
});
fs.writeFileSync(params.pidPath, proc.pid);
};

View File

@ -1,100 +0,0 @@
var program = require('commander')
, http = require('http')
, fs = require('fs')
, socketio = require('socket.io')
, spawn = require('child_process').spawn
, daemon = require('daemon');
/**
* Parse arg
*/
program
.version('0.2.0')
.usage('[options] [file ...]')
.option('-p, --port <port>', 'server port, default 9001', Number, 9001)
.option('-n, --number <number>', 'starting lines number, default 10', Number, 10)
.option('-d, --daemonize', 'run as daemon')
.option('--pid-path <path>', 'if run as deamon file that will store the process ID, default /var/run/frontail.pid',
String, '/var/run/frontail.pid')
.option('--log-path <path>', 'if run as deamon file that will be used as a log, default /dev/null',
String, '/dev/null')
.parse(process.argv);
/**
* Validate args
*/
if (program.args.length === 0) {
console.error('Arguments needed, use --help');
process.exit();
} else {
var files = program.args.join(" ");
}
if (program.daemonize) {
try {
fs.writeFileSync(program.pidPath, '');
fs.writeFileSync(program.logPath, '');
} catch (err) {
console.error(err.message);
process.exit();
}
}
/**
* Server setup
*/
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/index.html', function (err, data) {
if (err) {
res.writeHead(500, {'Content-Type': 'text/plain'});
res.end('Interal error');
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(data.toString('utf-8').replace(/__TITLE__/g, 'tail -F ' + files), 'utf-8');
}
});
});
server.listen(program.port);
/**
* socket.io setup
*/
var io = socketio.listen(server);
io.set('log level', 2);
/**
* Connect to socket and send starting data
*/
io.sockets.on('connection', function (socket) {
var tail = spawn('tail', ['-n', program.number, files]);
tail.stdout.on('data', function (data) {
socket.emit('lines', data.toString('utf-8').split('\n'));
});
});
/*
* Send incoming data
*/
var tail = spawn('tail', ['-F', files]);
tail.stdout.on('data', function (data) {
io.sockets.emit('lines', data.toString('utf-8').split('\n'));
});
/*
* Daemon
*/
if (program.daemonize) {
daemon.daemonize(program.logPath, program.pidPath, function (err, pid) {
if (err) {
return console.log('Error starting daemon: ' + err);
}
console.log('Daemon started successfully with pid: ' + pid);
});
}

View File

@ -1,195 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>__TITLE__</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
/* Styles mainly from http://twitter.github.com/bootstrap/ */
* {
padding: 0;
margin: 0;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
line-height: 18px;
color: #333;
padding: 45px 0 10px;
}
.navbar {
background-color: #2C2C2C;
background-image: -webkit-linear-gradient(top, #333, #222);
background-repeat: repeat-x;
box-shadow: rgba(0, 0, 0, 0.246094) 0px 1px 3px 0px, rgba(0, 0, 0, 0.0976563) 0px -1px 0px 0px inset;
position: fixed;
left: 0px;
right: 0px;
top: 0px;
color: #fff;
display: block;
height: 24px;
font-size: 20px;
font-weight: 200;
padding: 8px 50px;
}
.navbar .query {
background-color: #626262;
border-color: #151515;
border-radius: 14px 14px 14px 14px;
border-style: solid;
border-width: 1px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.098) inset, 0 1px 0 0 rgba(255, 255, 255, 0.15);
color: white;
display: inline-block;
float: right;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 13px;
height: 18px;
line-height: 13px;
margin-top: -2px;
padding: 4px 9px;
width: 160px;
}
.navbar .query:focus,
.navbar .query.focused {
padding: 5px 10px;
color: #333333;
text-shadow: 0 1px 0 #ffffff;
background-color: #ffffff;
border: 0;
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
outline: 0;
}
.log {
white-space: pre-wrap;
}
.log .line {
padding: 0 50px;
margin-left: 10em;
text-indent: -10em;
}
</style>
</head>
<body>
<div class="navbar">
<span class="brand">__TITLE__</span>
<input type="text" class="query" placeholder="Filter" tabindex="1">
</div>
<pre class="log"></pre>
<script src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
var Frontail = (function() {
/**
* @type {Object}
* @private
*/
var _socket;
/**
* @type {HTMLElement}
* @private
*/
var _logContainer;
/**
* @type {HTMLElement}
* @private
*/
var _filterInput;
/**
* @type {String}
* @private
*/
var _filterValue = '';
/**
* Filter logs based on _filterValue
*
* @function
* @private
*/
var _filterLogs = function() {
var collection = _logContainer.childNodes;
var i = collection.length;
if (i == 0) return;
while (i-=1) {
var e = collection[i-1];
if (e.textContent.indexOf(_filterValue) === -1) {
e.style.display = 'none';
} else {
e.style.display = '';
}
}
window.scrollTo(0, document.body.scrollHeight);
};
return {
/**
* Init socket.io communication and log container
*
* @param {Object} opts options
*/
init: function(opts) {
var that = this;
// socket.io init
_socket = new io.connect();
_socket
.on('lines', function(lines) {
for (var i = 0; i < lines.length; i+=1) {
that.log(lines[i]);
}
});
// Elements
_logContainer = opts.container;
_filterInput = opts.filterInput;
_filterInput.focus();
// Filter input bind
_filterInput.addEventListener('keyup', function(e) {
if (e.keyCode === 27) { //esc
this.value = '';
_filterValue = '';
} else {
_filterValue = this.value;
}
_filterLogs();
});
},
/**
* Log data
*
* @param {string} data data to log
*/
log: function(data) {
var p = document.createElement('p');
p.className = 'line';
p.innerHTML = data;
_logContainer.appendChild(p);
window.scrollTo(0, document.body.scrollHeight);
}
}
})();
window.load = Frontail.init({
container: document.getElementsByClassName('log')[0],
filterInput: document.getElementsByClassName('query')[0]
});
</script>
</body>
</html>

87
lib/options_parser.js Normal file
View File

@ -0,0 +1,87 @@
const program = require('commander');
program
.version(require('../package.json').version)
.usage('[options] [file ...]')
.helpOption('--help')
.option(
'-h, --host <host>',
'listening host, default 0.0.0.0',
String,
'0.0.0.0'
)
.option('-p, --port <port>', 'listening port, default 9001', Number, 9001)
.option(
'-n, --number <number>',
'starting lines number, default 10',
Number,
10
)
.option(
'-l, --lines <lines>',
'number on lines stored in browser, default 2000',
Number,
2000
)
.option(
'-t, --theme <theme>',
'name of the theme (default, dark)',
String,
'default'
)
.option('-d, --daemonize', 'run as daemon')
.option(
'-U, --user <username>',
'Basic Authentication username, option works only along with -P option',
String,
false
)
.option(
'-P, --password <password>',
'Basic Authentication password, option works only along with -U option',
String,
false
)
.option(
'-k, --key <key.pem>',
'Private Key for HTTPS, option works only along with -c option',
String,
false
)
.option(
'-c, --certificate <cert.pem>',
'Certificate for HTTPS, option works only along with -k option',
String,
false
)
.option(
'--pid-path <path>',
'if run as daemon file that will store the process id, default /var/run/frontail.pid',
String,
'/var/run/frontail.pid'
)
.option(
'--log-path <path>',
'if run as daemon file that will be used as a log, default /dev/null',
String,
'/dev/null'
)
.option(
'--url-path <path>',
'URL path for the browser application, default /',
String,
'/'
)
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
.option('--ui-no-indent', "don't indent log lines")
.option(
'--ui-highlight',
'highlight words or lines if defined string found in logs, default preset'
)
.option(
'--ui-highlight-preset <path>',
'custom preset for highlighting (see ./preset/default.json)'
)
.option('--disable-usage-stats', 'disable gathering usage statistics');
module.exports = program;

54
lib/server_builder.js Normal file
View File

@ -0,0 +1,54 @@
/* eslint no-underscore-dangle: off */
'use strict';
const fs = require('fs');
const http = require('http');
const https = require('https');
function ServerBuilder() {
this._host = null;
this._port = 9001;
}
ServerBuilder.prototype.build = function build() {
if (this._key && this._cert) {
const options = {
key: this._key,
cert: this._cert,
};
return https
.createServer(options, this._callback)
.listen(this._port, this._host);
}
return http.createServer(this._callback).listen(this._port, this._host);
};
ServerBuilder.prototype.host = function hostf(host) {
this._host = host;
return this;
};
ServerBuilder.prototype.port = function portf(port) {
this._port = port;
return this;
};
ServerBuilder.prototype.secure = function secure(keyPath, certPath) {
try {
this._key = fs.readFileSync(keyPath);
this._cert = fs.readFileSync(certPath);
} catch (e) {
throw new Error('No key or certificate file found');
}
return this;
};
ServerBuilder.prototype.use = function use(callback) {
this._callback = callback;
return this;
};
module.exports = () => new ServerBuilder();

69
lib/stats.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const ua = require('universal-analytics');
const isDocker = require('is-docker');
const Configstore = require('configstore');
const uuidv4 = require('uuid/v4');
const pkg = require('../package.json');
const trackingID = 'UA-129582046-1';
// Usage stats
function Stats(enabled, opts) {
this.timer = {};
if (enabled === true) {
const config = new Configstore(pkg.name);
let clientID = uuidv4();
if (config.has('clientID')) {
clientID = config.get('clientID');
} else {
config.set('clientID', clientID);
}
const tracker = ua(trackingID, clientID);
tracker.set('aip', 1); // Anonymize IP
tracker.set('an', 'frontail'); // Application Name
tracker.set('av', pkg.version); // Application Version
tracker.set('ds', 'app'); // Data Source
tracker.set('cd1', process.platform); // platform
tracker.set('cd2', process.arch); // arch
tracker.set('cd3', process.version.match(/^v(\d+\.\d+)/)[1]); // Node version
tracker.set('cd4', isDocker()); // is Docker
tracker.set('cd5', opts.number !== 10); // is --number parameter set
this.tracker = tracker;
}
return this;
}
Stats.prototype.track = function track(category, action) {
if (!this.tracker) {
return;
}
this.tracker.event(category, action).send();
};
Stats.prototype.time = function time(category, action) {
if (!this.tracker) {
return;
}
if (!this.timer[category]) {
this.timer[category] = {};
}
this.timer[category][action] = Date.now();
};
Stats.prototype.timeEnd = function timeEnd(category, action, cb) {
if (!this.tracker) {
cb();
return;
}
this.tracker
.timing(category, action, Date.now() - this.timer[category][action])
.send(cb);
};
module.exports = (enabled, opts) => new Stats(enabled, opts);

72
lib/tail.js Normal file
View File

@ -0,0 +1,72 @@
/* eslint no-underscore-dangle: off */
'use strict';
const events = require('events');
const childProcess = require('child_process');
const tailStream = require('fs-tail-stream');
const util = require('util');
const CBuffer = require('CBuffer');
const byline = require('byline');
const commandExistsSync = require('command-exists').sync;
function Tail(path, opts) {
events.EventEmitter.call(this);
const options = opts || {
buffer: 0,
};
this._buffer = new CBuffer(options.buffer);
let stream;
if (path[0] === '-') {
stream = process.stdin;
} else {
/* Check if this os provides the `tail` command. */
const hasTailCommand = commandExistsSync('tail');
if (hasTailCommand) {
let followOpt = '-F';
if (process.platform === 'openbsd') {
followOpt = '-f';
}
const cp = childProcess.spawn(
'tail',
['-n', options.buffer, followOpt].concat(path)
);
cp.stderr.on('data', (data) => {
// If there is any important error then display it in the console. Tail will keep running.
// File can be truncated over network.
if (data.toString().indexOf('file truncated') === -1) {
console.error(data.toString());
}
});
stream = cp.stdout;
process.on('exit', () => {
cp.kill();
});
} else {
/* This is used if the os does not support the `tail`command. */
stream = tailStream.createReadStream(path.join(), {
encoding: 'utf8',
start: options.buffer,
tail: true,
});
}
}
byline(stream, { keepEmptyLines: true }).on('data', (line) => {
const str = line.toString();
this._buffer.push(str);
this.emit('line', str);
});
}
util.inherits(Tail, events.EventEmitter);
Tail.prototype.getBuffer = function getBuffer() {
return this._buffer.toArray();
};
module.exports = (path, options) => new Tail(path, options);

3785
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,94 @@
{
"name": "frontail",
"version": "0.2.0",
"description": "tail -F output in browser",
"homepage": "https://github.com/mthenw/frontail",
"contributors": {
"name": "Maciej Winnicki",
"email": "maciej.winnicki@gmail.com"
},
"license": "MIT",
"bin": {
"frontail": "./bin/frontail"
},
"dependencies": {
"commander": ">=0.5.2",
"socket.io": ">=0.9.3",
"daemon": ">=0.4.1"
},
"repository": {
"type": "git",
"url": "http://github.com/mthenw/frontail.git"
},
"keywords": [
"tail",
"syslog"
"name": "frontail",
"version": "4.9.2",
"description": "streaming logs to the browser",
"homepage": "https://github.com/mthenw/frontail",
"author": "Maciej Winnicki <maciej.winnicki@gmail.com>",
"license": "MIT",
"bin": "./bin/frontail",
"dependencies": {
"CBuffer": "0.1.4",
"basic-auth-connect": "1.0.0",
"byline": "5.0.0",
"command-exists": "1.2.8",
"commander": "3.0.1",
"configstore": "4.0.0",
"connect": "3.7.0",
"cookie": "0.1.0",
"cookie-parser": "1.4.5",
"daemon-fix41": "1.1.2",
"express-session": "1.15.6",
"fs-tail-stream": "1.1.0",
"is-docker": "1.1.0",
"serve-static": "1.14.1",
"socket.io": "3.1.2",
"universal-analytics": "0.4.23",
"untildify": "3.0.2",
"uuid": "3.3.2"
},
"devDependencies": {
"eslint": "~6.8.0",
"eslint-config-airbnb-base": "~14.0.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "~2.20.0",
"jsdom": "~11.12.0",
"mocha": "~5.2.0",
"pkg": "~4.4.7",
"should": "~3.3.2",
"sinon": "~1.7.3",
"supertest": "^3.3.0",
"temp": "~0.8.1"
},
"scripts": {
"lint": "eslint .",
"test": "mocha -r should --exit test/*.js",
"pkg": "pkg --out-path=dist ."
},
"pkg": {
"assets": [
"preset/*.json",
"web/**/*"
],
"engine": {
"node": ">=0.6"
"targets": [
"node12-alpine-x64",
"node12-linux-x64",
"node12-macos-x64",
"node12-windows-x64"
]
},
"eslintConfig": {
"extends": [
"airbnb-base",
"prettier"
],
"rules": {
"no-console": "off",
"strict": "off",
"implicit-arrow-linebreak": "off"
},
"preferGlobal": true
}
"env": {
"node": true
},
"ignorePatterns": [
"web/assets/tinycon.min.js",
"web/assets/ansi_up.js"
]
},
"prettier": {
"singleQuote": true,
"arrowParens": "always"
},
"repository": {
"type": "git",
"url": "http://github.com/mthenw/frontail.git"
},
"keywords": [
"tail",
"syslog",
"realtime",
"log",
"devops"
],
"main": "index",
"preferGlobal": true
}

8
preset/default.json Normal file
View File

@ -0,0 +1,8 @@
{
"words": {
"err": "color: red;"
},
"lines": {
"err": "font-weight: bold;"
}
}

15
preset/npmlog.json Normal file
View File

@ -0,0 +1,15 @@
{
"words": {
"verb": "color: blue; background-color: black;",
"info": "color: green;",
"http": "color: green; background-color: black;",
"WARN": "color: black; background-color: yellow; font-style: normal;",
"error": "color: red;",
"ERR!": "color: red; background-color: black;"
},
"lines": {
"WARN": "font-style: italic;",
"ERR!": "font-weight: bold;"
}
}

12
preset/python.json Normal file
View File

@ -0,0 +1,12 @@
{
"words": {
"TODO": "color: purple"
},
"lines": {
"CRITICAL": "color: Red; font-weight: bold",
"ERROR": "color: Crimson; font-weight: bold",
"WARN": "color: Orange; font-style: italic",
"INFO": "color: DeepSkyBlue;",
"DEBUG": "color: LightGrey;"
}
}

9
test/.eslintrc.json Normal file
View File

@ -0,0 +1,9 @@
{
"rules": {
"import/no-extraneous-dependencies": "off",
"no-unused-expressions": "off"
},
"env": {
"mocha": true
}
}

242
test/app.js Normal file
View File

@ -0,0 +1,242 @@
'use strict';
const fs = require('fs');
const jsdom = require('jsdom/lib/old-api.js');
const events = require('events');
describe('browser application', () => {
let io;
let window;
function initApp() {
window.App.init({
socket: io,
container: window.document.querySelector('.log'),
filterInput: window.document.querySelector('#filter'),
pauseBtn: window.document.querySelector('#pauseBtn'),
topbar: window.document.querySelector('.topbar'),
body: window.document.querySelector('body'),
});
}
function clickOnElement(line) {
const click = window.document.createEvent('MouseEvents');
click.initMouseEvent(
'click',
true,
true,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
line.dispatchEvent(click);
}
beforeEach((done) => {
io = new events.EventEmitter();
const html =
'<title></title><body><div class="topbar"></div>' +
'<div class="log"></div><button type="button" id="pauseBtn"></button>' +
'<input type="test" id="filter"/></body>';
const ansiup = fs.readFileSync('./web/assets/ansi_up.js', 'utf-8');
const src = fs.readFileSync('./web/assets/app.js', 'utf-8');
jsdom.env({
html,
url: 'http://localhost?filter=line.*',
src: [ansiup, src],
onload: (domWindow) => {
window = domWindow;
initApp();
done();
},
});
});
it('should show lines from socket.io', () => {
io.emit('line', 'test');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(1);
log.childNodes[0].textContent.should.be.equal('test');
log.childNodes[0].className.should.be.equal('line');
log.childNodes[0].tagName.should.be.equal('DIV');
log.childNodes[0].innerHTML.should.be.equal(
'<p class="inner-line">test</p>'
);
});
it('should select line when clicked', () => {
io.emit('line', 'test');
const line = window.document.querySelector('.line');
clickOnElement(line);
line.className.should.be.equal('line-selected');
});
it('should deselect line when selected line clicked', () => {
io.emit('line', 'test');
const line = window.document.querySelector('.line');
clickOnElement(line);
clickOnElement(line);
line.className.should.be.equal('line');
});
it('should limit number of lines in browser', () => {
io.emit('options:lines', 2);
io.emit('line', 'line1');
io.emit('line', 'line2');
io.emit('line', 'line3');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(2);
log.childNodes[0].textContent.should.be.equal('line2');
log.childNodes[1].textContent.should.be.equal('line3');
});
it('should hide topbar', () => {
io.emit('options:hide-topbar');
const topbar = window.document.querySelector('.topbar');
topbar.className.should.match(/hide/);
const body = window.document.querySelector('body');
body.className.should.match(/no-topbar/);
});
it('should not indent log lines', () => {
io.emit('options:no-indent');
const log = window.document.querySelector('.log');
log.className.should.match(/no-indent/);
});
it('should highlight word', () => {
io.emit('options:highlightConfig', {
words: {
foo: 'background: black',
bar: 'background: black',
},
});
io.emit('line', 'foo bar');
const line = window.document.querySelector('.line');
line.innerHTML.should.containEql(
'<span style="background: black">foo</span> <span style="background: black">bar</span>'
);
});
it('should highlight line', () => {
io.emit('options:highlightConfig', {
lines: {
line: 'background: black',
},
});
io.emit('line', 'line1');
const line = window.document.querySelector('.line');
line.parentNode.innerHTML.should.equal(
'<div class="line" style="background: black"><p class="inner-line">line1</p></div>'
);
});
it('should escape HTML', () => {
io.emit('line', '<a/>');
const line = window.document.querySelector('.line');
line.innerHTML.should.equal('<p class="inner-line">&lt;a/&gt;</p>');
});
it('should work filter from URL', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
const filterInput = window.document.querySelector('#filter');
filterInput.value.should.be.equal('line.*');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(3);
log.childNodes[0].style.display.should.be.equal('');
log.childNodes[1].style.display.should.be.equal('none');
log.childNodes[2].style.display.should.be.equal('');
window.location.href.should.containEql('filter=line.*');
});
it('should clean filter', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
const filterInput = window.document.querySelector('#filter');
const event = new window.KeyboardEvent('keyup', { keyCode: 27 });
filterInput.dispatchEvent(event);
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(3);
log.childNodes[0].style.display.should.be.equal('');
log.childNodes[1].style.display.should.be.equal('');
log.childNodes[2].style.display.should.be.equal('');
window.location.href.should.be.equal('http://localhost/');
});
it('should change filter', () => {
io.emit('line', 'line1');
io.emit('line', 'another');
io.emit('line', 'line2');
const log = window.document.querySelector('.log');
const filterInput = window.document.querySelector('#filter');
filterInput.value = 'other';
const event = new window.KeyboardEvent('keyup', { keyCode: 13 });
filterInput.dispatchEvent(event);
log.childNodes.length.should.be.equal(3);
log.childNodes[0].style.display.should.be.equal('none');
log.childNodes[1].style.display.should.be.equal('');
log.childNodes[2].style.display.should.be.equal('none');
window.location.href.should.containEql('filter=other');
});
it('should pause', () => {
io.emit('line', 'line1');
const btn = window.document.querySelector('#pauseBtn');
const event = window.document.createEvent('Event');
event.initEvent('mouseup', true, true);
btn.dispatchEvent(event);
io.emit('line', 'line2');
io.emit('line', 'line3');
btn.className.should.containEql('play');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('==> SKIPPED: 2 <==');
});
it('should play', () => {
const btn = window.document.querySelector('#pauseBtn');
const event = window.document.createEvent('Event');
event.initEvent('mouseup', true, true);
btn.dispatchEvent(event);
io.emit('line', 'line1');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(1);
log.lastChild.textContent.should.be.equal('==> SKIPPED: 1 <==');
btn.className.should.containEql('play');
btn.dispatchEvent(event);
io.emit('line', 'line2');
btn.className.should.not.containEql('play');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('line2');
});
});

123
test/connect_builder.js Normal file
View File

@ -0,0 +1,123 @@
'use strict';
const request = require('supertest');
const path = require('path');
const connectBuilder = require('../lib/connect_builder');
describe('connectBuilder', () => {
it('should build connect app', () => {
connectBuilder('/').build().should.have.property('use');
connectBuilder().build().should.have.property('listen');
});
it('should build app requiring authorized user', (done) => {
const app = connectBuilder('/').authorize('user', 'pass').build();
request(app)
.get('/')
.expect('www-authenticate', 'Basic realm="Authorization Required"')
.expect(401, done);
});
it('should build app allowing user to login', (done) => {
const app = connectBuilder('/').authorize('user', 'pass').build();
app.use((req, res) => {
res.end('secret!');
});
request(app)
.get('/')
.set('Authorization', 'Basic dXNlcjpwYXNz')
.expect(200, 'secret!', done);
});
it('should build app that setup session', (done) => {
const app = connectBuilder('/').session('secret').build();
app.use((req, res) => {
res.end();
});
request(app)
.get('/')
.expect('set-cookie', /^connect.sid/, done);
});
it('should build app that serve static files', (done) => {
const app = connectBuilder('/')
.static(path.join(__dirname, 'fixtures'))
.build();
request(app).get('/foo.txt').expect('bar', done);
});
it('should build app that serve index file', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
.build();
request(app).get('/').expect(200).expect('Content-Type', 'text/html', done);
});
it('should build app that serve index file on specified path', (done) => {
const app = connectBuilder('/test')
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
.build();
request(app)
.get('/test')
.expect(200)
.expect('Content-Type', 'text/html', done);
});
it('should build app that replace index title', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
.build();
request(app).get('/').expect('<head><title>/testfile</title></head>', done);
});
it('should build app that sets socket.io namespace based on files', (done) => {
const app = connectBuilder('/')
.index(
path.join(__dirname, 'fixtures/index_with_ns'),
'/testfile',
'ns',
'dark'
)
.build();
request(app).get('/').expect('ns', done);
});
it('should build app that sets theme', (done) => {
const app = connectBuilder('/')
.index(
path.join(__dirname, '/fixtures/index_with_theme'),
'/testfile',
'ns',
'dark'
)
.build();
request(app)
.get('/')
.expect(
'<head><title>/testfile</title><link href="dark.css" rel="stylesheet" type="text/css"/></head>',
done
);
});
it('should build app that sets default theme', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile')
.build();
request(app)
.get('/')
.expect(
'<head><title>/testfile</title><link href="default.css" rel="stylesheet" type="text/css"/></head>',
done
);
});
});

267
test/daemonize.js Normal file
View File

@ -0,0 +1,267 @@
'use strict';
const daemon = require('daemon-fix41');
const sinon = require('sinon');
const fs = require('fs');
const optionsParser = require('../lib/options_parser');
const daemonize = require('../lib/daemonize');
describe('daemonize', () => {
beforeEach(() => {
sinon.stub(daemon, 'daemon');
daemon.daemon.returns({
pid: 1000,
});
sinon.stub(fs, 'writeFileSync');
sinon.stub(fs, 'openSync');
});
afterEach(() => {
daemon.daemon.restore();
fs.writeFileSync.restore();
fs.openSync.restore();
});
describe('should daemon', () => {
it('current script', () => {
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[0].should.match('script');
});
it('with hostname', () => {
optionsParser.parse(['node', '/path/to/frontail', '-h', '127.0.0.1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-h', '127.0.0.1']);
});
it('with port', () => {
optionsParser.parse(['node', '/path/to/frontail', '-p', '80']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-p', 80]);
});
it('with lines number', () => {
optionsParser.parse(['node', '/path/to/frontail', '-n', '1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-n', 1]);
});
it('with lines stored in browser', () => {
optionsParser.parse(['node', '/path/to/frontail', '-l', '1']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-l', 1]);
});
it('with theme', () => {
optionsParser.parse(['node', '/path/to/frontail', '-t', 'dark']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['-t', 'dark']);
});
it('with authorization', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-U',
'user',
'-P',
'passw0rd',
]);
daemonize('script', optionsParser, {
doAuthorization: true,
});
daemon.daemon.lastCall.args[1].should.containDeep([
'-U',
'user',
'-P',
'passw0rd',
]);
});
it('without authorization if option doAuthorization not passed', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-U',
'user',
'-P',
'passw0rd',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.not.containDeep([
'-U',
'user',
'-P',
'passw0rd',
]);
});
it('with secure connection', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-k',
'key.file',
'-c',
'cert.file',
]);
daemonize('script', optionsParser, {
doSecure: true,
});
daemon.daemon.lastCall.args[1].should.containDeep([
'-k',
'key.file',
'-c',
'cert.file',
]);
});
it('without secure connection if option doSecure not passed', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'-k',
'key.file',
'-c',
'cert.file',
]);
daemonize('script', optionsParser, {
doSecure: true,
});
daemon.daemon.lastCall.args[1].should.containDeep([
'-k',
'key.file',
'-c',
'cert.file',
]);
});
it('with url-path option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--url-path', '/test']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--url-path',
'/test',
]);
});
it('with hide-topbar option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-hide-topbar']);
});
it('with no-indent option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--ui-no-indent']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-no-indent']);
});
it('with highlight option', () => {
optionsParser.parse(['node', '/path/to/frontail', '--ui-highlight']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight']);
});
it('with highlight preset option', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--ui-highlight',
'--ui-highlight-preset',
'test.json',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--ui-highlight-preset',
'test.json',
]);
});
it('with disable usage stats', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--disable-usage-stats',
'test.json',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--disable-usage-stats',
'test.json',
]);
});
it('with file to tail', () => {
optionsParser.parse(['node', '/path/to/frontail', '/path/to/file']);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['/path/to/file']);
});
});
it('should write pid to pidfile', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--pid-path',
'/path/to/pid',
]);
daemonize('script', optionsParser);
fs.writeFileSync.lastCall.args[0].should.be.equal('/path/to/pid');
fs.writeFileSync.lastCall.args[1].should.be.equal(1000);
});
it('should log to file', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--log-path',
'/path/to/log',
]);
fs.openSync.returns('file');
daemonize('script', optionsParser);
fs.openSync.lastCall.args[0].should.equal('/path/to/log');
fs.openSync.lastCall.args[1].should.equal('a');
daemon.daemon.lastCall.args[2].should.eql({
stdout: 'file',
stderr: 'file',
});
});
});

1
test/fixtures/foo.txt vendored Normal file
View File

@ -0,0 +1 @@
bar

1
test/fixtures/index vendored Normal file
View File

@ -0,0 +1 @@
index

1
test/fixtures/index_with_ns vendored Normal file
View File

@ -0,0 +1 @@
__NAMESPACE__

1
test/fixtures/index_with_theme vendored Normal file
View File

@ -0,0 +1 @@
<head><title>__TITLE__</title><link href="__THEME__.css" rel="stylesheet" type="text/css"/></head>

1
test/fixtures/index_with_title vendored Normal file
View File

@ -0,0 +1 @@
<head><title>__TITLE__</title></head>

127
test/server_builder.js Normal file
View File

@ -0,0 +1,127 @@
'use strict';
const fs = require('fs');
const http = require('http');
const https = require('https');
const sinon = require('sinon');
const serverBuilder = require('../lib/server_builder');
describe('serverBuilder', () => {
describe('http server', () => {
let httpServer;
let createServer;
beforeEach(() => {
httpServer = sinon.createStubInstance(http.Server);
httpServer.listen.returns(httpServer);
createServer = sinon.stub(http, 'createServer').returns(httpServer);
});
afterEach(() => {
createServer.restore();
});
it('should build server', () => {
const server = serverBuilder().build();
createServer.calledOnce.should.equal(true);
server.should.be.an.instanceof(http.Server);
});
it('should build server accepting requests', () => {
const callback = () => {};
serverBuilder().use(callback).build();
createServer.calledWith(callback).should.equal(true);
});
it('should build listening server', () => {
serverBuilder().build();
httpServer.listen.calledOnce.should.equal(true);
});
it('should build server listening on specified port', () => {
serverBuilder().port(6666).build();
httpServer.listen.calledWith(6666).should.equal(true);
});
it('should build server listening on default port', () => {
serverBuilder().build();
httpServer.listen.calledWith(9001).should.equal(true);
});
it('should build server listening on specified host', () => {
serverBuilder().host('127.0.0.1').build();
httpServer.listen.calledWith(9001, '127.0.0.1').should.equal(true);
});
it('should build server listening on default host', () => {
serverBuilder().build();
httpServer.listen.calledWith(9001, null).should.equal(true);
});
});
describe('https server', () => {
let httpsServer;
let createHttpsServer;
let readFileSyncStub;
beforeEach(() => {
httpsServer = sinon.createStubInstance(https.Server);
httpsServer.listen.returns(httpsServer);
createHttpsServer = sinon
.stub(https, 'createServer')
.returns(httpsServer);
readFileSyncStub = sinon.stub(fs, 'readFileSync');
readFileSyncStub.withArgs('key.pem').returns('testkey');
readFileSyncStub.withArgs('cert.pem').returns('testcert');
});
afterEach(() => {
createHttpsServer.restore();
readFileSyncStub.restore();
});
it('should build server', () => {
const server = serverBuilder().secure('key.pem', 'cert.pem').build();
server.should.be.an.instanceof(https.Server);
createHttpsServer
.calledWith({
key: 'testkey',
cert: 'testcert',
})
.should.equal(true);
});
it('should build server accepting requests', () => {
const callback = () => {};
serverBuilder().use(callback).secure('key.pem', 'cert.pem').build();
createHttpsServer
.calledWith(
{
key: 'testkey',
cert: 'testcert',
},
callback
)
.should.equal(true);
});
it('should throw error if key or cert not provided', () => {
readFileSyncStub.restore();
(() => {
serverBuilder().secure('nofile', 'nofile');
}).should.throw('No key or certificate file found');
});
});
});

60
test/tail.js Normal file
View File

@ -0,0 +1,60 @@
'use strict';
const fs = require('fs');
const temp = require('temp');
const tail = require('../lib/tail');
const TEMP_FILE_PROFIX = '';
const SPAWN_DELAY = 10;
function writeLines(fd, count) {
for (let i = 0; i < count; i += 1) {
fs.writeSync(
fd,
`line${i}
`
);
}
fs.closeSync(fd);
}
describe('tail', () => {
temp.track();
it('calls event line if new line appear in file', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
tail(info.path).on('line', (line) => {
line.should.equal('line0');
done();
});
setTimeout(writeLines, SPAWN_DELAY, info.fd, 1);
});
});
it('buffers lines on start', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
writeLines(info.fd, 20);
const tailer = tail(info.path, {
buffer: 2,
});
setTimeout(() => {
tailer.getBuffer().should.be.eql(['line18', 'line19']);
done();
}, SPAWN_DELAY);
});
});
it('buffers no lines on start by default', (done) => {
temp.open(TEMP_FILE_PROFIX, (err, info) => {
writeLines(info.fd, 3);
const tailer = tail(info.path);
setTimeout(() => {
tailer.getBuffer().should.be.empty;
done();
}, SPAWN_DELAY);
});
});
});

13
web/assets/.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"rules": {
"no-var": "off",
"no-underscore-dangle": "off",
"prefer-arrow-callback": "off",
"func-names": "off",
"space-before-function-paren": "off",
"prefer-template": "off"
},
"env": {
"browser": true
}
}

327
web/assets/ansi_up.js Normal file
View File

@ -0,0 +1,327 @@
// ansi_up.js
// version : 1.3.0
// author : Dru Nelson
// license : MIT
// http://github.com/drudru/ansi_up
(function (Date, undefined) {
var ansi_up,
VERSION = "1.3.0",
// check for nodeJS
hasModule = (typeof module !== 'undefined'),
// Normal and then Bright
ANSI_COLORS = [
[
{ color: "0, 0, 0", 'class': "ansi-black" },
{ color: "187, 0, 0", 'class': "ansi-red" },
{ color: "0, 187, 0", 'class': "ansi-green" },
{ color: "187, 187, 0", 'class': "ansi-yellow" },
{ color: "0, 0, 187", 'class': "ansi-blue" },
{ color: "187, 0, 187", 'class': "ansi-magenta" },
{ color: "0, 187, 187", 'class': "ansi-cyan" },
{ color: "255,255,255", 'class': "ansi-white" }
],
[
{ color: "85, 85, 85", 'class': "ansi-bright-black" },
{ color: "255, 85, 85", 'class': "ansi-bright-red" },
{ color: "0, 255, 0", 'class': "ansi-bright-green" },
{ color: "255, 255, 85", 'class': "ansi-bright-yellow" },
{ color: "85, 85, 255", 'class': "ansi-bright-blue" },
{ color: "255, 85, 255", 'class': "ansi-bright-magenta" },
{ color: "85, 255, 255", 'class': "ansi-bright-cyan" },
{ color: "255, 255, 255", 'class': "ansi-bright-white" }
]
],
// 256 Colors Palette
PALETTE_COLORS;
function Ansi_Up() {
this.fg = this.bg = this.fg_truecolor = this.bg_truecolor = null;
this.bright = 0;
}
Ansi_Up.prototype.setup_palette = function() {
PALETTE_COLORS = [];
// Index 0..15 : System color
(function() {
var i, j;
for (i = 0; i < 2; ++i) {
for (j = 0; j < 8; ++j) {
PALETTE_COLORS.push(ANSI_COLORS[i][j]['color']);
}
}
})();
// Index 16..231 : RGB 6x6x6
// https://gist.github.com/jasonm23/2868981#file-xterm-256color-yaml
(function() {
var levels = [0, 95, 135, 175, 215, 255];
var format = function (r, g, b) { return levels[r] + ', ' + levels[g] + ', ' + levels[b] };
var r, g, b;
for (r = 0; r < 6; ++r) {
for (g = 0; g < 6; ++g) {
for (b = 0; b < 6; ++b) {
PALETTE_COLORS.push(format.call(this, r, g, b));
}
}
}
})();
// Index 232..255 : Grayscale
(function() {
var level = 8;
var format = function(level) { return level + ', ' + level + ', ' + level };
var i;
for (i = 0; i < 24; ++i, level += 10) {
PALETTE_COLORS.push(format.call(this, level));
}
})();
};
Ansi_Up.prototype.escape_for_html = function (txt) {
return txt.replace(/[&<>]/gm, function(str) {
if (str == "&") return "&amp;";
if (str == "<") return "&lt;";
if (str == ">") return "&gt;";
});
};
Ansi_Up.prototype.linkify = function (txt) {
return txt.replace(/(https?:\/\/[^\s]+)/gm, function(str) {
return "<a href=\"" + str + "\">" + str + "</a>";
});
};
Ansi_Up.prototype.ansi_to_html = function (txt, options) {
return this.process(txt, options, true);
};
Ansi_Up.prototype.ansi_to_text = function (txt) {
var options = {};
return this.process(txt, options, false);
};
Ansi_Up.prototype.process = function (txt, options, markup) {
var self = this;
var raw_text_chunks = txt.split(/\033\[/);
var first_chunk = raw_text_chunks.shift(); // the first chunk is not the result of the split
var color_chunks = raw_text_chunks.map(function (chunk) {
return self.process_chunk(chunk, options, markup);
});
color_chunks.unshift(first_chunk);
return color_chunks.join('');
};
Ansi_Up.prototype.process_chunk = function (text, options, markup) {
// Are we using classes or styles?
options = typeof options == 'undefined' ? {} : options;
var use_classes = typeof options.use_classes != 'undefined' && options.use_classes;
var key = use_classes ? 'class' : 'color';
// Each 'chunk' is the text after the CSI (ESC + '[') and before the next CSI/EOF.
//
// This regex matches four groups within a chunk.
//
// The first and third groups match code type.
// We supported only SGR command. It has empty first group and 'm' in third.
//
// The second group matches all of the number+semicolon command sequences
// before the 'm' (or other trailing) character.
// These are the graphics or SGR commands.
//
// The last group is the text (including newlines) that is colored by
// the other group's commands.
var matches = text.match(/^([!\x3c-\x3f]*)([\d;]*)([\x20-\x2c]*[\x40-\x7e])([\s\S]*)/m);
if (!matches) return text;
var orig_txt = matches[4];
var nums = matches[2].split(';');
// We currently support only "SGR" (Select Graphic Rendition)
// Simply ignore if not a SGR command.
if (matches[1] !== '' || matches[3] !== 'm') {
return orig_txt;
}
if (!markup) {
return orig_txt;
}
var self = this;
while (nums.length > 0) {
var num_str = nums.shift();
var num = parseInt(num_str);
if (isNaN(num) || num === 0) {
self.fg = self.bg = null;
self.bright = 0;
} else if (num === 1) {
self.bright = 1;
} else if (num == 39) {
self.fg = null;
} else if (num == 49) {
self.bg = null;
} else if ((num >= 30) && (num < 38)) {
self.fg = ANSI_COLORS[self.bright][(num % 10)][key];
} else if ((num >= 90) && (num < 98)) {
self.fg = ANSI_COLORS[1][(num % 10)][key];
} else if ((num >= 40) && (num < 48)) {
self.bg = ANSI_COLORS[0][(num % 10)][key];
} else if ((num >= 100) && (num < 108)) {
self.bg = ANSI_COLORS[1][(num % 10)][key];
} else if (num === 38 || num === 48) { // extend color (38=fg, 48=bg)
(function() {
var is_foreground = (num === 38);
if (nums.length >= 1) {
var mode = nums.shift();
if (mode === '5' && nums.length >= 1) { // palette color
var palette_index = parseInt(nums.shift());
if (palette_index >= 0 && palette_index <= 255) {
if (!use_classes) {
if (!PALETTE_COLORS) {
self.setup_palette.call(self);
}
if (is_foreground) {
self.fg = PALETTE_COLORS[palette_index];
} else {
self.bg = PALETTE_COLORS[palette_index];
}
} else {
var klass = (palette_index >= 16)
? ('ansi-palette-' + palette_index)
: ANSI_COLORS[palette_index > 7 ? 1 : 0][palette_index % 8]['class'];
if (is_foreground) {
self.fg = klass;
} else {
self.bg = klass;
}
}
}
} else if(mode === '2' && nums.length >= 3) { // true color
var r = parseInt(nums.shift());
var g = parseInt(nums.shift());
var b = parseInt(nums.shift());
if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
var color = r + ', ' + g + ', ' + b;
if (!use_classes) {
if (is_foreground) {
self.fg = color;
} else {
self.bg = color;
}
} else {
if (is_foreground) {
self.fg = 'ansi-truecolor';
self.fg_truecolor = color;
} else {
self.bg = 'ansi-truecolor';
self.bg_truecolor = color;
}
}
}
}
}
})();
}
}
if ((self.fg === null) && (self.bg === null)) {
return orig_txt;
} else {
var styles = [];
var classes = [];
var data = {};
var render_data = function (data) {
var fragments = [];
var key;
for (key in data) {
if (data.hasOwnProperty(key)) {
fragments.push('data-' + key + '="' + this.escape_for_html(data[key]) + '"');
}
}
return fragments.length > 0 ? ' ' + fragments.join(' ') : '';
};
if (self.fg) {
if (use_classes) {
classes.push(self.fg + "-fg");
if (self.fg_truecolor !== null) {
data['ansi-truecolor-fg'] = self.fg_truecolor;
self.fg_truecolor = null;
}
} else {
styles.push("color:rgb(" + self.fg + ")");
}
}
if (self.bg) {
if (use_classes) {
classes.push(self.bg + "-bg");
if (self.bg_truecolor !== null) {
data['ansi-truecolor-bg'] = self.bg_truecolor;
self.bg_truecolor = null;
}
} else {
styles.push("background-color:rgb(" + self.bg + ")");
}
}
if (use_classes) {
return '<span class="' + classes.join(' ') + '"' + render_data.call(self, data) + '>' + orig_txt + '</span>';
} else {
return '<span style="' + styles.join(';') + '"' + render_data.call(self, data) + '>' + orig_txt + '</span>';
}
}
};
// Module exports
ansi_up = {
escape_for_html: function (txt) {
var a2h = new Ansi_Up();
return a2h.escape_for_html(txt);
},
linkify: function (txt) {
var a2h = new Ansi_Up();
return a2h.linkify(txt);
},
ansi_to_html: function (txt, options) {
var a2h = new Ansi_Up();
return a2h.ansi_to_html(txt, options);
},
ansi_to_text: function (txt) {
var a2h = new Ansi_Up();
return a2h.ansi_to_text(txt);
},
ansi_to_html_obj: function () {
return new Ansi_Up();
}
};
// CommonJS module is defined
if (hasModule) {
module.exports = ansi_up;
}
/*global ender:false */
if (typeof window !== 'undefined' && typeof ender === 'undefined') {
window.ansi_up = ansi_up;
}
/*global define:false */
if (typeof define === "function" && define.amd) {
define("ansi_up", [], function () {
return ansi_up;
});
}
})(Date);

345
web/assets/app.js Normal file
View File

@ -0,0 +1,345 @@
/* global Tinycon:false, ansi_up:false */
window.App = (function app(window, document) {
'use strict';
/**
* @type {Object}
* @private
*/
var _socket;
/**
* @type {HTMLElement}
* @private
*/
var _logContainer;
/**
* @type {HTMLElement}
* @private
*/
var _filterInput;
/**
* @type {String}
* @private
*/
var _filterValue = '';
/**
* @type {HTMLElement}
* @private
*/
var _pauseBtn;
/**
* @type {boolean}
* @private
*/
var _isPaused = false;
/**
* @type {number}
* @private
*/
var _skipCounter = 0;
/**
* @type {HTMLElement}
* @private
*/
var _topbar;
/**
* @type {HTMLElement}
* @private
*/
var _body;
/**
* @type {number}
* @private
*/
var _linesLimit = Math.Infinity;
/**
* @type {number}
* @private
*/
var _newLinesCount = 0;
/**
* @type {boolean}
* @private
*/
var _isWindowFocused = true;
/**
* @type {object}
* @private
*/
var _highlightConfig;
/**
* Hide element if doesn't contain filter value
*
* @param {Object} element
* @private
*/
var _filterElement = function(elem) {
var pattern = new RegExp(_filterValue, 'i');
var element = elem;
if (pattern.test(element.textContent)) {
element.style.display = '';
} else {
element.style.display = 'none';
}
};
/**
* Filter logs based on _filterValue
*
* @function
* @private
*/
var _filterLogs = function() {
var collection = _logContainer.childNodes;
var i = collection.length;
if (i === 0) {
return;
}
while (i) {
_filterElement(collection[i - 1]);
i -= 1;
}
window.scrollTo(0, document.body.scrollHeight);
};
/**
* Set _filterValue from URL parameter `filter`
*
* @function
* @private
*/
var _setFilterValueFromURL = function(filterInput, uri) {
var _url = new URL(uri);
var _filterValueFromURL = _url.searchParams.get('filter');
if (typeof _filterValueFromURL !== 'undefined' && _filterValueFromURL !== null) {
_filterValue = _filterValueFromURL;
filterInput.value = _filterValue; // eslint-disable-line
}
};
/**
* Set parameter `filter` in URL
*
* @function
* @private
*/
var _setFilterParam = function(value, uri) {
var _url = new URL(uri);
var _params = new URLSearchParams(_url.search.slice(1));
if (value === '') {
_params.delete('filter');
} else {
_params.set('filter', value);
}
_url.search = _params.toString();
window.history.replaceState(null, document.title, _url.toString());
};
/**
* @return void
* @private
*/
var _faviconReset = function() {
_newLinesCount = 0;
Tinycon.setBubble(0);
};
/**
* @return void
* @private
*/
var _updateFaviconCounter = function() {
if (_isWindowFocused || _isPaused) {
return;
}
if (_newLinesCount < 99) {
_newLinesCount += 1;
Tinycon.setBubble(_newLinesCount);
}
};
/**
* @return String
* @private
*/
var _highlightWord = function(line) {
var output = line;
if (_highlightConfig && _highlightConfig.words) {
Object.keys(_highlightConfig.words).forEach((wordCheck) => {
output = output.replace(
wordCheck,
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>',
);
});
}
return output;
};
/**
* @return HTMLElement
* @private
*/
var _highlightLine = function(line, container) {
if (_highlightConfig && _highlightConfig.lines) {
Object.keys(_highlightConfig.lines).forEach((lineCheck) => {
if (line.indexOf(lineCheck) !== -1) {
container.setAttribute('style', _highlightConfig.lines[lineCheck]);
}
});
}
return container;
};
return {
/**
* Init socket.io communication and log container
*
* @param {Object} opts options
*/
init: function init(opts) {
var self = this;
// Elements
_logContainer = opts.container;
_filterInput = opts.filterInput;
_filterInput.focus();
_pauseBtn = opts.pauseBtn;
_topbar = opts.topbar;
_body = opts.body;
_setFilterValueFromURL(_filterInput, window.location.toString());
// Filter input bind
_filterInput.addEventListener('keyup', function(e) {
// ESC
if (e.keyCode === 27) {
this.value = '';
_filterValue = '';
} else {
_filterValue = this.value;
}
_setFilterParam(_filterValue, window.location.toString());
_filterLogs();
});
// Pause button bind
_pauseBtn.addEventListener('mouseup', function() {
_isPaused = !_isPaused;
if (_isPaused) {
this.className += ' play';
} else {
_skipCounter = 0;
this.classList.remove('play');
}
});
// Favicon counter bind
window.addEventListener(
'blur',
function() {
_isWindowFocused = false;
},
true,
);
window.addEventListener(
'focus',
function() {
_isWindowFocused = true;
_faviconReset();
},
true,
);
// socket.io init
_socket = opts.socket;
_socket
.on('options:lines', function(limit) {
_linesLimit = limit;
})
.on('options:hide-topbar', function() {
_topbar.className += ' hide';
_body.className = 'no-topbar';
})
.on('options:no-indent', function() {
_logContainer.className += ' no-indent';
})
.on('options:highlightConfig', function(highlightConfig) {
_highlightConfig = highlightConfig;
})
.on('line', function(line) {
if (_isPaused) {
_skipCounter += 1;
self.log('==> SKIPPED: ' + _skipCounter + ' <==', (_skipCounter > 1));
} else {
self.log(line);
}
});
},
/**
* Log data
*
* @param {string} data data to log
*/
log: function log(data, replace = false) {
var wasScrolledBottom = window.innerHeight + Math.ceil(window.pageYOffset + 1)
>= document.body.offsetHeight;
var div = document.createElement('div');
var p = document.createElement('p');
p.className = 'inner-line';
// convert ansi color codes to html && escape HTML tags
data = ansi_up.escape_for_html(data); // eslint-disable-line
data = ansi_up.ansi_to_html(data); // eslint-disable-line
p.innerHTML = _highlightWord(data);
div.className = 'line';
div = _highlightLine(data, div);
div.addEventListener('click', function click() {
if (this.className.indexOf('selected') === -1) {
this.className = 'line-selected';
} else {
this.className = 'line';
}
});
div.appendChild(p);
_filterElement(div);
if (replace) {
_logContainer.replaceChild(div, _logContainer.lastChild);
} else {
_logContainer.appendChild(div);
}
if (_logContainer.children.length > _linesLimit) {
_logContainer.removeChild(_logContainer.children[0]);
}
if (wasScrolledBottom) {
window.scrollTo(0, document.body.scrollHeight);
}
_updateFaviconCounter();
},
};
}(window, document));

BIN
web/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

6
web/assets/styles/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,67 @@
@import 'default.css';
body {
padding-top: 4em;
background-color: #2f3238;
}
.no-topbar {
padding-top: 10px;
}
.navbar {
background-color: #26292e;
border: 0;
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-brand:hover {
color: #999;
}
.btn-pause {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%237f8289' viewBox='0 0 8 8'><path d='M1 1v6h2v-6h-2zm4 0v6h2v-6h-2z'></path></svg>") no-repeat center center;
background-color: #2f3238;
border: 1px solid transparent;
}
.btn-pause.play {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%237f8289' viewBox='0 0 8 8'><path d='M1 1v6l6-3-6-3z'></path></svg>") no-repeat center center;
background-color: #2f3238;
}
.form-control {
border: 0;
color: #7f8289;
background-color: #2f3238;
}
.log {
white-space: pre-wrap;
color: #7f8289;
font-size: 0.85em;
background: inherit;
border: 0;
padding: 0;
}
.log .inner-line {
padding: 0 15px;
margin-left: 84pt;
text-indent: -84pt;
margin-bottom: 0;
}
.log .inner-line:empty::after {
content: '.';
visibility: hidden;
}
.log.no-indent .inner-line {
margin-left: 0;
text-indent: 0;
}
.log .line-selected {
background-color: #302436;
}

View File

@ -0,0 +1,84 @@
@import 'bootstrap.min.css';
body {
padding-top: 60px;
}
@media screen and (max-width: 768px) {
body {
padding-top: 120px;
}
}
.navbar-brand {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: inline-block;
width: 100%;
}
.btn-pause {
margin: 8px 0 8px;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 8 8'><path d='M1 1v6h2v-6h-2zm4 0v6h2v-6h-2z'></path></svg>")
no-repeat center center;
background-color: #e2e6ea;
background-size: contain;
cursor: pointer;
display: inline-block;
height: 34px;
width: 34px;
border: 1px solid #ccc;
}
.btn-pause.play {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 8 8'><path d='M1 1v6l6-3-6-3z'></path></svg>")
no-repeat center center;
background-color: #e2e6ea;
}
.navbar-form-custom {
padding: 8px 0;
}
.no-horiz-padding {
padding-left: 0px;
padding-right: 0px;
}
.no-topbar {
padding-top: 10px;
}
.navbar-inverse .navbar-brand {
color: white;
}
.log {
white-space: pre-wrap;
color: black;
font-size: 0.85em;
background: inherit;
border: 0;
padding: 0;
}
.log .inner-line {
padding: 0 15px;
margin-left: 84pt;
text-indent: -84pt;
margin-bottom: 0;
}
.log .inner-line:empty::after {
content: '.';
visibility: hidden;
}
.log.no-indent .inner-line {
margin-left: 0;
text-indent: 0;
}
.log .line-selected {
background-color: #ffb2b0;
}

8
web/assets/tinycon.min.js vendored Normal file
View File

@ -0,0 +1,8 @@
/*!
Tinycon - A small library for manipulating the Favicon
Tom Moor, http://tommoor.com
Copyright (c) 2012 Tom Moor
MIT Licensed
@version 0.6.1
*/
!function(){var a={},b=null,c=null,d=document.title,e=null,f=null,g={},h=window.devicePixelRatio||1,i=16*h,j={width:7,height:9,font:10*h+"px arial",colour:"#ffffff",background:"#F03D25",fallback:!0,crossOrigin:!0,abbreviate:!0},k=function(){var a=navigator.userAgent.toLowerCase();return function(b){return-1!==a.indexOf(b)}}(),l={ie:k("msie"),chrome:k("chrome"),webkit:k("chrome")||k("safari"),safari:k("safari")&&!k("chrome"),mozilla:k("mozilla")&&!k("chrome")&&!k("safari")},m=function(){for(var a=document.getElementsByTagName("link"),b=0,c=a.length;c>b;b++)if((a[b].getAttribute("rel")||"").match(/\bicon\b/))return a[b];return!1},n=function(){for(var a=document.getElementsByTagName("link"),b=document.getElementsByTagName("head")[0],c=0,d=a.length;d>c;c++){var e="undefined"!=typeof a[c];e&&(a[c].getAttribute("rel")||"").match(/\bicon\b/)&&b.removeChild(a[c])}},o=function(){if(!c||!b){var a=m();c=b=a?a.getAttribute("href"):"/favicon.ico"}return b},p=function(){return f||(f=document.createElement("canvas"),f.width=i,f.height=i),f},q=function(a){n();var b=document.createElement("link");b.type="image/x-icon",b.rel="icon",b.href=a,document.getElementsByTagName("head")[0].appendChild(b)},s=function(a,b){if(!p().getContext||l.ie||l.safari||"force"===g.fallback)return t(a);var c=p().getContext("2d"),b=b||"#000000",d=o();e=document.createElement("img"),e.onload=function(){c.clearRect(0,0,i,i),c.drawImage(e,0,0,e.width,e.height,0,0,i,i),(a+"").length>0&&u(c,a,b),v()},!d.match(/^data/)&&g.crossOrigin&&(e.crossOrigin="anonymous"),e.src=d},t=function(a){g.fallback&&(document.title=(a+"").length>0?"("+a+") "+d:d)},u=function(a,b){"number"==typeof b&&b>99&&g.abbreviate&&(b=w(b));var d=(b+"").length-1,e=g.width*h+6*h*d,f=g.height*h,j=i-f,k=i-e-h,m=16*h,n=16*h,o=2*h;a.font=(l.webkit?"bold ":"")+g.font,a.fillStyle=g.background,a.strokeStyle=g.background,a.lineWidth=h,a.beginPath(),a.moveTo(k+o,j),a.quadraticCurveTo(k,j,k,j+o),a.lineTo(k,m-o),a.quadraticCurveTo(k,m,k+o,m),a.lineTo(n-o,m),a.quadraticCurveTo(n,m,n,m-o),a.lineTo(n,j+o),a.quadraticCurveTo(n,j,n-o,j),a.closePath(),a.fill(),a.beginPath(),a.strokeStyle="rgba(0,0,0,0.3)",a.moveTo(k+o/2,m),a.lineTo(n-o/2,m),a.stroke(),a.fillStyle=g.colour,a.textAlign="right",a.textBaseline="top",a.fillText(b,2===h?29:15,l.mozilla?7*h:6*h)},v=function(){p().getContext&&q(p().toDataURL())},w=function(a){for(var b=[["G",1e9],["M",1e6],["k",1e3]],c=0;c<b.length;++c)if(a>=b[c][1]){a=x(a/b[c][1])+b[c][0];break}return a},x=function(a,b){var c=new Number(a);return c.toFixed(b)};a.setOptions=function(a){g={};for(var b in j)g[b]=a.hasOwnProperty(b)?a[b]:j[b];return this},a.setImage=function(a){return b=a,v(),this},a.setBubble=function(a,b){return a=a||"",s(a,b),this},a.reset=function(){q(c)},a.setOptions(j),window.Tinycon=a}();

57
web/index.html Normal file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>tail -f __TITLE__</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="__PATH__/styles/__THEME__.css">
<link rel="icon" href="__PATH__/favicon.ico">
</head>
<body>
<nav class="topbar navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8">
<span class="navbar-brand text-overflow" title="__TITLE__">tail -f __TITLE__</span>
</div>
<div class="col-sm-1 text-right">
<button type="button" class="btn btn-light btn-pause" data-toggle="button" aria-pressed="false" autocomplete="off"></button>
</div>
<div class="col-sm-3">
<form class="navbar-form-custom" role="search" onkeypress="return event.keyCode != 13;">
<input type="text" class="form-control query" placeholder="Filter" tabindex="1">
</form>
</div>
</div>
</div>
</nav>
<div class="container-fluid no-horiz-padding">
<pre class="log"></pre>
</div>
<script src="__PATH__/socket.io/socket.io.js"></script>
<script src="__PATH__/tinycon.min.js"></script>
<script src="__PATH__/ansi_up.js"></script>
<script src="__PATH__/app.js"></script>
<script type="text/javascript">
var socket = new io.connect('/__NAMESPACE__', {
path: '__PATH__/socket.io',
transports: ['websocket']
});
window.load = App.init({
socket: socket,
container: document.getElementsByClassName('log')[0],
filterInput: document.getElementsByClassName('query')[0],
pauseBtn: document.getElementsByClassName('btn-pause')[0],
topbar: document.getElementsByClassName('topbar')[0],
body: document.getElementsByTagName('body')[0]
});
</script>
</body>
</html>