Compare commits

...

123 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
41 changed files with 4965 additions and 410 deletions

View File

@ -1,2 +0,0 @@
lib/web/assets/tinycon.min.js
lib/web/assets/ansi_up.js

View File

@ -1,10 +0,0 @@
{
"extends": "airbnb-base",
"rules": {
"no-console": "off",
"strict": "off"
},
"env": {
"node": true
}
}

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

2
.gitignore vendored
View File

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

View File

@ -1,8 +1,16 @@
language: node_js
node_js:
- 4
- 5
- 6
- 6
- 8
- 10
script:
- npm run lint
- npm test
- 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"]

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 Maciej Winnicki
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

View File

@ -1,35 +1,36 @@
# frontail streaming logs to the browser
```frontail``` is a Node.js application for streaming logs to the browser.
`frontail` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI.
[![Build Status](https://img.shields.io/travis/mthenw/frontail.svg?style=flat)](https://travis-ci.org/mthenw/frontail)
[![Version](http://img.shields.io/npm/v/frontail.svg?style=flat)](https://www.npmjs.org/package/frontail)
![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`
- `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
* auto-scrolling
* marking logs
* number of unread logs in favicon
* themes (default, dark)
* [highlighting](#highlighting)
* search (```Tab``` to focus, ```Esc``` to clear)
* tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
* basic authentication
- 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
## Installation options
npm i frontail -g
or use [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/)
docker run -d -P -v /var/log:/log mthenw/frontail /log/syslog
- 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
@ -37,7 +38,6 @@ or use [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/)
Options:
-h, --help output usage information
-V, --version output the version number
-h, --host <host> listening host, default 0.0.0.0
-p, --port <port> listening port, default 9001
@ -51,12 +51,16 @@ or use [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/)
-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://127.0.0.1:[port]**.
Web interface runs on **http://[host]:[port]**.
### Tailing multiple files
@ -70,7 +74,7 @@ Use `-` for streaming stdin:
### Highlighting
```--ui-highlight``` option turns on highlighting in UI. By default preset from ```./preset/defatult.json``` is used:
`--ui-highlight` option turns on highlighting in UI. By default preset from `./preset/default.json` is used:
```
{
@ -83,10 +87,46 @@ Use `-` for streaming stdin:
}
```
which means that every "err" string will be in red and every line with "err" will be bolded. Custom preset can be provided by
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 your, please create PR with json file.*
_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._
## Screenshot
Available presets:
![screenshot1](https://dl.dropboxusercontent.com/u/3101412/frontail1.0.png)
- 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
```

5
docker-entrypoint.sh Executable file
View File

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

View File

@ -1,15 +1,18 @@
'use strict';
const connect = require('connect');
const cookieParser = require('cookie');
const cookie = require('cookie');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const path = require('path');
const socketio = require('socket.io');
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
@ -20,15 +23,22 @@ if (program.args.length === 0) {
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 sessionKey = 'sid';
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, {
@ -39,14 +49,19 @@ if (program.daemonize) {
/**
* HTTP(s) server setup
*/
const appBuilder = connectBuilder();
const appBuilder = connectBuilder(urlPath);
if (doAuthorization) {
appBuilder.session(sessionSecret, sessionKey);
appBuilder.session(sessionSecret);
appBuilder.authorize(program.user, program.password);
}
appBuilder
.static(path.join(__dirname, 'lib/web/assets'))
.index(path.join(__dirname, 'lib/web/index.html'), files, filesNamespace, program.theme);
.static(path.join(__dirname, 'web', 'assets'))
.index(
path.join(__dirname, 'web', 'index.html'),
files,
filesNamespace,
program.theme
);
const builder = serverBuilder();
if (doSecure) {
@ -61,16 +76,22 @@ if (program.daemonize) {
/**
* socket.io setup
*/
const io = socketio.listen(server, {
log: false,
});
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 cookie = cookieParser.parse(handshakeData.headers.cookie);
const sessionId = connect.utils.parseSignedCookie(cookie[sessionKey], sessionSecret);
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);
}
@ -86,7 +107,19 @@ if (program.daemonize) {
*/
let highlightConfig;
if (program.uiHighlight) {
highlightConfig = require(path.resolve(__dirname, program.uiHighlightPreset)); // eslint-disable-line
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`);
}
}
/**
@ -123,11 +156,15 @@ if (program.daemonize) {
filesSocket.emit('line', line);
});
stats.track('runtime', 'started');
/**
* Handle signals
*/
const cleanExit = () => {
process.exit();
stats.timeEnd('runtime', 'runtime', () => {
process.exit();
});
};
process.on('SIGINT', cleanExit);
process.on('SIGTERM', cleanExit);

View File

@ -2,15 +2,23 @@
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() {
function ConnectBuilder(urlPath) {
this.app = connect();
this.urlPath = urlPath;
}
ConnectBuilder.prototype.authorize = function authorize(user, pass) {
this.app.use(
connect.basicAuth(
(incomingUser, incomingPass) => user === incomingUser && pass === incomingPass));
this.urlPath,
basicAuth(
(incomingUser, incomingPass) =>
user === incomingUser && pass === incomingPass
)
);
return this;
};
@ -19,18 +27,26 @@ ConnectBuilder.prototype.build = function build() {
return this.app;
};
ConnectBuilder.prototype.index = function index(path, files, filesNamespace, themeOpt) {
ConnectBuilder.prototype.index = function index(
path,
files,
filesNamespace,
themeOpt
) {
const theme = themeOpt || 'default';
this.app.use((req, res) => {
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),
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'
);
});
@ -39,18 +55,21 @@ ConnectBuilder.prototype.index = function index(path, files, filesNamespace, the
return this;
};
ConnectBuilder.prototype.session = function session(secret, key) {
this.app.use(connect.cookieParser());
this.app.use(connect.session({
secret,
key,
}));
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(connect.static(path));
this.app.use(this.urlPath, serveStatic(path));
return this;
};
module.exports = () => new ConnectBuilder();
module.exports = (urlPath) => new ConnectBuilder(urlPath);

View File

@ -1,6 +1,6 @@
'use strict';
const daemon = require('daemon');
const daemon = require('daemon-fix41');
const fs = require('fs');
const defaultOptions = {
@ -14,37 +14,48 @@ module.exports = (script, params, opts) => {
const logFile = fs.openSync(params.logPath, 'a');
let args = [
'-h', params.host,
'-p', params.port,
'-n', params.number,
'-l', params.lines,
'-t', params.theme,
'-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
);
args.push('-U', params.user, '-P', params.password);
}
if (options.doSecure) {
args.push(
'-k', params.key,
'-c', params.certificate
);
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', '--ui-highlight-preset', params.uiHighlightPreset);
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);

View File

@ -3,28 +3,85 @@ const program = require('commander');
program
.version(require('../package.json').version)
.usage('[options] [file ...]')
.option('-h, --host <host>', 'listening host, default 0.0.0.0', String, '0.0.0.0')
.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(
'-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(
'-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)', String,
'./preset/default.json');
.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;

View File

@ -17,7 +17,9 @@ ServerBuilder.prototype.build = function build() {
key: this._key,
cert: this._cert,
};
return https.createServer(options, this._callback).listen(this._port, this._host);
return https
.createServer(options, this._callback)
.listen(this._port, this._host);
}
return http.createServer(this._callback).listen(this._port, this._host);

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);

View File

@ -2,54 +2,68 @@
'use strict';
const EventEmitter = require('events').EventEmitter;
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) {
EventEmitter.call(this);
events.EventEmitter.call(this);
const options = opts || {
buffer: 0,
};
this._buffer = new CBuffer(options.buffer);
let stream;
if (path[0] === '-') {
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
const line = process.stdin.read();
if (line !== null) {
this.emit('line', line);
}
});
stream = process.stdin;
} else {
const tail = childProcess.spawn('tail', ['-n', options.buffer, '-F'].concat(path));
tail.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());
/* Check if this os provides the `tail` command. */
const hasTailCommand = commandExistsSync('tail');
if (hasTailCommand) {
let followOpt = '-F';
if (process.platform === 'openbsd') {
followOpt = '-f';
}
});
tail.stdout.on('data', (data) => {
const lines = data.toString('utf-8').split('\n');
lines.pop();
lines.forEach((line) => {
this._buffer.push(line);
this.emit('line', line);
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', () => {
tail.kill();
});
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, EventEmitter);
util.inherits(Tail, events.EventEmitter);
Tail.prototype.getBuffer = function getBuffer() {
return this._buffer.toArray();

File diff suppressed because one or more lines are too long

View File

@ -1,51 +0,0 @@
@import "bootstrap.min.css";
body {
padding-top: 4em;
background-color: #2F3238;
}
.no-topbar {
padding-top: 10px;
}
.navbar {
background-color: #26292E;
border: 0;
}
.form-control {
border: 0;
color: #7F8289;
background-color: #2F3238;
}
.log {
white-space: pre-wrap;
color: #7F8289;
font-size: .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

@ -1,43 +0,0 @@
@import "bootstrap.min.css";
body {
padding-top: 4em;
}
.no-topbar {
padding-top: 10px;
}
.navbar-inverse .navbar-brand {
color: white;
}
.log {
white-space: pre-wrap;
color: black;
font-size: .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;
}

View File

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>tail -F __TITLE__</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="/styles/__THEME__.css">
<link rel="icon" href="/favicon.ico">
</head>
<body>
<nav class="topbar navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<span class="navbar-brand" href="#">tail -F __TITLE__</span>
<form class="navbar-form navbar-right" role="search">
<div class="form-group">
<input type="text" class="form-control query" placeholder="Filter" tabindex="1">
</div>
</form>
</div>
</nav>
<pre class="log"></pre>
<script src="/socket.io/socket.io.js"></script>
<script src="/tinycon.min.js"></script>
<script src="/ansi_up.js"></script>
<script src="/app.js"></script>
<script type="text/javascript">
var socket = new io.connect('/' + '__NAMESPACE__');
window.load = App.init({
socket: socket,
container: document.getElementsByClassName('log')[0],
filterInput: document.getElementsByClassName('query')[0],
topbar: document.getElementsByClassName('topbar')[0],
body: document.getElementsByTagName('body')[0]
});
</script>
</body>
</html>

3785
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "frontail",
"version": "4.0.1",
"version": "4.9.2",
"description": "streaming logs to the browser",
"homepage": "https://github.com/mthenw/frontail",
"author": "Maciej Winnicki <maciej.winnicki@gmail.com>",
@ -8,26 +8,75 @@
"bin": "./bin/frontail",
"dependencies": {
"CBuffer": "0.1.4",
"commander": "1.3.2",
"connect": "2.11.0",
"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",
"daemon": "1.1.0",
"socket.io": "1.4.8"
"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": "^3.6.0",
"eslint-config-airbnb-base": "^8.0.0",
"eslint-plugin-import": "^1.16.0",
"jsdom": "^8.5.0",
"mocha": "~2.3.2",
"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": "~0.8.1",
"supertest": "^3.3.0",
"temp": "~0.8.1"
},
"scripts": {
"lint": "eslint .",
"test": "mocha -r should --reporter spec test/*.js"
"test": "mocha -r should --exit test/*.js",
"pkg": "pkg --out-path=dist ."
},
"pkg": {
"assets": [
"preset/*.json",
"web/**/*"
],
"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"
},
"env": {
"node": true
},
"ignorePatterns": [
"web/assets/tinycon.min.js",
"web/assets/ansi_up.js"
]
},
"prettier": {
"singleQuote": true,
"arrowParens": "always"
},
"repository": {
"type": "git",

View File

@ -1,8 +1,8 @@
{
"words": {
"err": "color: red;"
},
"lines": {
"err": "font-weight: bold;"
}
"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;"
}
}

View File

@ -1,8 +1,8 @@
'use strict';
const fs = require('fs');
const jsdom = require('jsdom');
const EventEmitter = require('events').EventEmitter;
const jsdom = require('jsdom/lib/old-api.js');
const events = require('events');
describe('browser application', () => {
let io;
@ -13,6 +13,7 @@ describe('browser application', () => {
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'),
});
@ -20,19 +21,38 @@ describe('browser application', () => {
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);
click.initMouseEvent(
'click',
true,
true,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
line.dispatchEvent(click);
}
beforeEach((done) => {
io = new EventEmitter();
const html = '<title></title><body><div class="topbar"></div>' +
'<div class="log"></div><input type="test" id="filter"/></body>';
const ansiup = fs.readFileSync('./lib/web/assets/ansi_up.js', 'utf-8');
const src = fs.readFileSync('./lib/web/assets/app.js', 'utf-8');
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;
@ -51,7 +71,9 @@ describe('browser application', () => {
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>');
log.childNodes[0].innerHTML.should.be.equal(
'<p class="inner-line">test</p>'
);
});
it('should select line when clicked', () => {
@ -104,13 +126,16 @@ describe('browser application', () => {
it('should highlight word', () => {
io.emit('options:highlightConfig', {
words: {
line: 'background: black',
foo: 'background: black',
bar: 'background: black',
},
});
io.emit('line', 'line1');
io.emit('line', 'foo bar');
const line = window.document.querySelector('.line');
line.innerHTML.should.containEql('<span style="background: black">line</span>');
line.innerHTML.should.containEql(
'<span style="background: black">foo</span> <span style="background: black">bar</span>'
);
});
it('should highlight line', () => {
@ -133,4 +158,85 @@ describe('browser application', () => {
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');
});
});

View File

@ -1,17 +1,17 @@
'use strict';
const connectBuilder = require('../lib/connect_builder');
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('use');
connectBuilder().build().should.have.property('listen');
});
it('should build app requiring authorized user', (done) => {
const app = connectBuilder().authorize('user', 'pass').build();
const app = connectBuilder('/').authorize('user', 'pass').build();
request(app)
.get('/')
@ -20,7 +20,7 @@ describe('connectBuilder', () => {
});
it('should build app allowing user to login', (done) => {
const app = connectBuilder().authorize('user', 'pass').build();
const app = connectBuilder('/').authorize('user', 'pass').build();
app.use((req, res) => {
res.end('secret!');
});
@ -32,56 +32,72 @@ describe('connectBuilder', () => {
});
it('should build app that setup session', (done) => {
const app = connectBuilder().session('secret', 'sessionkey').build();
const app = connectBuilder('/').session('secret').build();
app.use((req, res) => {
res.end();
});
request(app)
.get('/')
.expect('set-cookie', /^sessionkey/, done);
.expect('set-cookie', /^connect.sid/, done);
});
it('should build app that serve static files', (done) => {
const app = connectBuilder().static(path.join(__dirname, 'fixtures')).build();
const app = connectBuilder('/')
.static(path.join(__dirname, 'fixtures'))
.build();
request(app)
.get('/foo')
.expect('bar', done);
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();
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('/')
.get('/test')
.expect(200)
.expect('Content-Type', 'text/html', done);
});
it('should build app that replace index title', (done) => {
const app = connectBuilder()
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
.build();
request(app)
.get('/')
.expect('<head><title>/testfile</title></head>', done);
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')
const app = connectBuilder('/')
.index(
path.join(__dirname, 'fixtures/index_with_ns'),
'/testfile',
'ns',
'dark'
)
.build();
request(app)
.get('/')
.expect('ns', done);
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')
const app = connectBuilder('/')
.index(
path.join(__dirname, '/fixtures/index_with_theme'),
'/testfile',
'ns',
'dark'
)
.build();
request(app)
@ -89,11 +105,11 @@ describe('connectBuilder', () => {
.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()
const app = connectBuilder('/')
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile')
.build();
@ -102,6 +118,6 @@ describe('connectBuilder', () => {
.expect(
'<head><title>/testfile</title><link href="default.css" rel="stylesheet" type="text/css"/></head>',
done
);
);
});
});

View File

@ -1,10 +1,10 @@
'use strict';
const daemon = require('daemon');
const optionsParser = require('../lib/options_parser');
const daemonize = require('../lib/daemonize');
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(() => {
@ -70,41 +70,100 @@ describe('daemonize', () => {
});
it('with authorization', () => {
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
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']);
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']);
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']);
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']);
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']);
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']);
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']);
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', () => {
@ -129,7 +188,39 @@ describe('daemonize', () => {
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight']);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight-preset', './preset/default.json']);
});
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', () => {
@ -142,7 +233,12 @@ describe('daemonize', () => {
});
it('should write pid to pidfile', () => {
optionsParser.parse(['node', '/path/to/frontail', '--pid-path', '/path/to/pid']);
optionsParser.parse([
'node',
'/path/to/frontail',
'--pid-path',
'/path/to/pid',
]);
daemonize('script', optionsParser);
@ -151,7 +247,12 @@ describe('daemonize', () => {
});
it('should log to file', () => {
optionsParser.parse(['node', '/path/to/frontail', '--log-path', '/path/to/log']);
optionsParser.parse([
'node',
'/path/to/frontail',
'--log-path',
'/path/to/log',
]);
fs.openSync.returns('file');
daemonize('script', optionsParser);

View File

@ -3,8 +3,8 @@
const fs = require('fs');
const http = require('http');
const https = require('https');
const serverBuilder = require('../lib/server_builder');
const sinon = require('sinon');
const serverBuilder = require('../lib/server_builder');
describe('serverBuilder', () => {
describe('http server', () => {
@ -29,8 +29,7 @@ describe('serverBuilder', () => {
});
it('should build server accepting requests', () => {
const callback = () => {
};
const callback = () => {};
serverBuilder().use(callback).build();
@ -76,7 +75,9 @@ describe('serverBuilder', () => {
beforeEach(() => {
httpsServer = sinon.createStubInstance(https.Server);
httpsServer.listen.returns(httpsServer);
createHttpsServer = sinon.stub(https, 'createServer').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');
@ -91,22 +92,28 @@ describe('serverBuilder', () => {
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);
createHttpsServer
.calledWith({
key: 'testkey',
cert: 'testcert',
})
.should.equal(true);
});
it('should build server accepting requests', () => {
const callback = () => {
};
const callback = () => {};
serverBuilder().use(callback).secure('key.pem', 'cert.pem').build();
createHttpsServer.calledWith({
key: 'testkey',
cert: 'testcert',
}, callback).should.equal(true);
createHttpsServer
.calledWith(
{
key: 'testkey',
cert: 'testcert',
},
callback
)
.should.equal(true);
});
it('should throw error if key or cert not provided', () => {

View File

@ -1,16 +1,19 @@
'use strict';
const fs = require('fs');
const tail = require('../lib/tail');
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.writeSync(
fd,
`line${i}
`
);
}
fs.closeSync(fd);
}

View File

@ -27,6 +27,24 @@ window.App = (function app(window, document) {
*/
var _filterValue = '';
/**
* @type {HTMLElement}
* @private
*/
var _pauseBtn;
/**
* @type {boolean}
* @private
*/
var _isPaused = false;
/**
* @type {number}
* @private
*/
var _skipCounter = 0;
/**
* @type {HTMLElement}
* @private
@ -101,14 +119,36 @@ window.App = (function app(window, document) {
};
/**
* @return {Boolean}
* Set _filterValue from URL parameter `filter`
*
* @function
* @private
*/
var _isScrolledBottom = function() {
var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
var totalHeight = document.body.offsetHeight;
var clientHeight = document.documentElement.clientHeight;
return totalHeight <= currentScroll + clientHeight;
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());
};
/**
@ -125,15 +165,12 @@ window.App = (function app(window, document) {
* @private
*/
var _updateFaviconCounter = function() {
if (_isWindowFocused) {
if (_isWindowFocused || _isPaused) {
return;
}
_newLinesCount += 1;
if (_newLinesCount > 99) {
Tinycon.setBubble(99);
} else {
if (_newLinesCount < 99) {
_newLinesCount += 1;
Tinycon.setBubble(_newLinesCount);
}
};
@ -143,20 +180,18 @@ window.App = (function app(window, document) {
* @private
*/
var _highlightWord = function(line) {
if (_highlightConfig) {
if (_highlightConfig.words) {
for (var wordCheck in _highlightConfig.words) { // eslint-disable-line
if (_highlightConfig.words.hasOwnProperty(wordCheck)) { // eslint-disable-line
line = line.replace( // eslint-disable-line
wordCheck,
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>'
);
}
}
}
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 line;
return output;
};
/**
@ -164,14 +199,12 @@ window.App = (function app(window, document) {
* @private
*/
var _highlightLine = function(line, container) {
if (_highlightConfig) {
if (_highlightConfig.lines) {
for (var lineCheck in _highlightConfig.lines) { // eslint-disable-line
if (line.indexOf(lineCheck) !== -1) { // eslint-disable-line
container.setAttribute('style', _highlightConfig.lines[lineCheck]);
}
if (_highlightConfig && _highlightConfig.lines) {
Object.keys(_highlightConfig.lines).forEach((lineCheck) => {
if (line.indexOf(lineCheck) !== -1) {
container.setAttribute('style', _highlightConfig.lines[lineCheck]);
}
}
});
}
return container;
@ -190,9 +223,12 @@ window.App = (function app(window, document) {
_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
@ -202,17 +238,37 @@ window.App = (function app(window, document) {
} 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);
window.addEventListener(
'blur',
function() {
_isWindowFocused = false;
},
true,
);
window.addEventListener(
'focus',
function() {
_isWindowFocused = true;
_faviconReset();
},
true,
);
// socket.io init
_socket = opts.socket;
@ -231,7 +287,12 @@ window.App = (function app(window, document) {
_highlightConfig = highlightConfig;
})
.on('line', function(line) {
self.log(line);
if (_isPaused) {
_skipCounter += 1;
self.log('==> SKIPPED: ' + _skipCounter + ' <==', (_skipCounter > 1));
} else {
self.log(line);
}
});
},
@ -240,8 +301,9 @@ window.App = (function app(window, document) {
*
* @param {string} data data to log
*/
log: function log(data) {
var wasScrolledBottom = _isScrolledBottom();
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';
@ -263,7 +325,11 @@ window.App = (function app(window, document) {
div.appendChild(p);
_filterElement(div);
_logContainer.appendChild(div);
if (replace) {
_logContainer.replaceChild(div, _logContainer.lastChild);
} else {
_logContainer.appendChild(div);
}
if (_logContainer.children.length > _linesLimit) {
_logContainer.removeChild(_logContainer.children[0]);

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

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;
}

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>