├── .eslintrc.js ├── .github └── workflows │ └── gitlab.yml ├── .gitignore ├── .gitlab-ci.yml ├── LICENSE.md ├── README.md ├── index.html ├── package.json ├── screenshot.png ├── src ├── AggregatedChart.tsx ├── CustomyChart.tsx ├── GranularityChoosingChart.tsx ├── config.ts ├── db.ts ├── gui.tsx ├── lazy.ts ├── main.tsx └── util.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { 5 | tsconfigRootDir: __dirname, 6 | project: ["./tsconfig.json"], 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 13 | "prettier", 14 | ], 15 | rules: { 16 | "@typescript-eslint/no-unused-vars": "off", 17 | "@typescript-eslint/no-this-alias": "warn", 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/gitlab.yml: -------------------------------------------------------------------------------- 1 | name: Run GitLab CI on GitHub ( ͡° ͜ʖ ͡°) 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | # Steps represent a sequence of tasks that will be executed as part of the job 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: install gitlab runner 17 | run: | 18 | curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash 19 | sudo apt install gitlab-runner 20 | - name: typescript 21 | run: | 22 | ls -al 23 | gitlab-runner exec docker typescript 24 | - name: eslint 25 | run: gitlab-runner exec docker lint 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | logs 3 | node_modules 4 | dist 5 | .cache 6 | .vscode -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:alpine 2 | 3 | # This folder is cached between builds 4 | # http://docs.gitlab.com/ce/ci/yaml/README.html#cache 5 | cache: 6 | paths: 7 | - node_modules/ 8 | 9 | typescript: 10 | script: 11 | - yarn install --frozen-lockfile 12 | - yarn tsc 13 | 14 | lint: 15 | script: 16 | - yarn install --frozen-lockfile 17 | - yarn lint 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | I like the concept of giving back, so I settled on the AGPL as the 2 | default license for all my personal projects. 3 | 4 | This isn't set in stone, so feel free to write me at 5 | `phireskyde+git@gmail.com` if you need something else. 6 | 7 | --- 8 | 9 | ### GNU AFFERO GENERAL PUBLIC LICENSE 10 | 11 | Version 3, 19 November 2007 12 | 13 | Copyright © 2007 Free Software Foundation, Inc. 14 | <> 15 | 16 | Everyone is permitted to copy and distribute verbatim copies of this 17 | license document, but changing it is not allowed. 18 | 19 | ### Preamble 20 | 21 | The GNU Affero General Public License is a free, copyleft license for 22 | software and other kinds of works, specifically designed to ensure 23 | cooperation with the community in the case of network server software. 24 | 25 | The licenses for most software and other practical works are designed to 26 | take away your freedom to share and change the works. By contrast, our 27 | General Public Licenses are intended to guarantee your freedom to share 28 | and change all versions of a program--to make sure it remains free 29 | software for all its users. 30 | 31 | When we speak of free software, we are referring to freedom, not price. 32 | Our General Public Licenses are designed to make sure that you have the 33 | freedom to distribute copies of free software (and charge for them if 34 | you wish), that you receive source code or can get it if you want it, 35 | that you can change the software or use pieces of it in new free 36 | programs, and that you know you can do these things. 37 | 38 | Developers that use our General Public Licenses protect your rights with 39 | two steps: (1) assert copyright on the software, and (2) offer you this 40 | License which gives you legal permission to copy, distribute and/or 41 | modify the software. 42 | 43 | A secondary benefit of defending all users' freedom is that improvements 44 | made in alternate versions of the program, if they receive widespread 45 | use, become available for other developers to incorporate. Many 46 | developers of free software are heartened and encouraged by the 47 | resulting cooperation. However, in the case of software used on network 48 | servers, this result may fail to come about. The GNU General Public 49 | License permits making a modified version and letting the public access 50 | it on a server without ever releasing its source code to the public. 51 | 52 | The GNU Affero General Public License is designed specifically to ensure 53 | that, in such cases, the modified source code becomes available to the 54 | community. It requires the operator of a network server to provide the 55 | source code of the modified version running there to the users of that 56 | server. Therefore, public use of a modified version, on a publicly 57 | accessible server, gives the public access to the source code of the 58 | modified version. 59 | 60 | An older license, called the Affero General Public License and published 61 | by Affero, was designed to accomplish similar goals. This is a different 62 | license, not a version of the Affero GPL, but Affero has released a new 63 | version of the Affero GPL which permits relicensing under this license. 64 | 65 | The precise terms and conditions for copying, distribution and 66 | modification follow. 67 | 68 | ### TERMS AND CONDITIONS 69 | 70 | #### 0. Definitions. 71 | 72 | "This License" refers to version 3 of the GNU Affero General Public 73 | License. 74 | 75 | "Copyright" also means copyright-like laws that apply to other kinds of 76 | works, such as semiconductor masks. 77 | 78 | "The Program" refers to any copyrightable work licensed under this 79 | License. Each licensee is addressed as "you". "Licensees" and 80 | "recipients" may be individuals or organizations. 81 | 82 | To "modify" a work means to copy from or adapt all or part of the work 83 | in a fashion requiring copyright permission, other than the making of an 84 | exact copy. The resulting work is called a "modified version" of the 85 | earlier work or a work "based on" the earlier work. 86 | 87 | A "covered work" means either the unmodified Program or a work based on 88 | the Program. 89 | 90 | To "propagate" a work means to do anything with it that, without 91 | permission, would make you directly or secondarily liable for 92 | infringement under applicable copyright law, except executing it on a 93 | computer or modifying a private copy. Propagation includes copying, 94 | distribution (with or without modification), making available to the 95 | public, and in some countries other activities as well. 96 | 97 | To "convey" a work means any kind of propagation that enables other 98 | parties to make or receive copies. Mere interaction with a user through 99 | a computer network, with no transfer of a copy, is not conveying. 100 | 101 | An interactive user interface displays "Appropriate Legal Notices" to 102 | the extent that it includes a convenient and prominently visible feature 103 | that (1) displays an appropriate copyright notice, and (2) tells the 104 | user that there is no warranty for the work (except to the extent that 105 | warranties are provided), that licensees may convey the work under this 106 | License, and how to view a copy of this License. If the interface 107 | presents a list of user commands or options, such as a menu, a prominent 108 | item in the list meets this criterion. 109 | 110 | #### 1. Source Code. 111 | 112 | The "source code" for a work means the preferred form of the work for 113 | making modifications to it. "Object code" means any non-source form of a 114 | work. 115 | 116 | A "Standard Interface" means an interface that either is an official 117 | standard defined by a recognized standards body, or, in the case of 118 | interfaces specified for a particular programming language, one that is 119 | widely used among developers working in that language. 120 | 121 | The "System Libraries" of an executable work include anything, other 122 | than the work as a whole, that (a) is included in the normal form of 123 | packaging a Major Component, but which is not part of that Major 124 | Component, and (b) serves only to enable use of the work with that Major 125 | Component, or to implement a Standard Interface for which an 126 | implementation is available to the public in source code form. A "Major 127 | Component", in this context, means a major essential component (kernel, 128 | window system, and so on) of the specific operating system (if any) on 129 | which the executable work runs, or a compiler used to produce the work, 130 | or an object code interpreter used to run it. 131 | 132 | The "Corresponding Source" for a work in object code form means all the 133 | source code needed to generate, install, and (for an executable work) 134 | run the object code and to modify the work, including scripts to control 135 | those activities. However, it does not include the work's System 136 | Libraries, or general-purpose tools or generally available free programs 137 | which are used unmodified in performing those activities but which are 138 | not part of the work. For example, Corresponding Source includes 139 | interface definition files associated with source files for the work, 140 | and the source code for shared libraries and dynamically linked 141 | subprograms that the work is specifically designed to require, such as 142 | by intimate data communication or control flow between those subprograms 143 | and other parts of the work. 144 | 145 | The Corresponding Source need not include anything that users can 146 | regenerate automatically from other parts of the Corresponding Source. 147 | 148 | The Corresponding Source for a work in source code form is that same 149 | work. 150 | 151 | #### 2. Basic Permissions. 152 | 153 | All rights granted under this License are granted for the term of 154 | copyright on the Program, and are irrevocable provided the stated 155 | conditions are met. This License explicitly affirms your unlimited 156 | permission to run the unmodified Program. The output from running a 157 | covered work is covered by this License only if the output, given its 158 | content, constitutes a covered work. This License acknowledges your 159 | rights of fair use or other equivalent, as provided by copyright law. 160 | 161 | You may make, run and propagate covered works that you do not convey, 162 | without conditions so long as your license otherwise remains in force. 163 | You may convey covered works to others for the sole purpose of having 164 | them make modifications exclusively for you, or provide you with 165 | facilities for running those works, provided that you comply with the 166 | terms of this License in conveying all material for which you do not 167 | control copyright. Those thus making or running the covered works for 168 | you must do so exclusively on your behalf, under your direction and 169 | control, on terms that prohibit them from making any copies of your 170 | copyrighted material outside their relationship with you. 171 | 172 | Conveying under any other circumstances is permitted solely under the 173 | conditions stated below. Sublicensing is not allowed; section 10 makes 174 | it unnecessary. 175 | 176 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 177 | 178 | No covered work shall be deemed part of an effective technological 179 | measure under any applicable law fulfilling obligations under article 11 180 | of the WIPO copyright treaty adopted on 20 December 1996, or similar 181 | laws prohibiting or restricting circumvention of such measures. 182 | 183 | When you convey a covered work, you waive any legal power to forbid 184 | circumvention of technological measures to the extent such circumvention 185 | is effected by exercising rights under this License with respect to the 186 | covered work, and you disclaim any intention to limit operation or 187 | modification of the work as a means of enforcing, against the work's 188 | users, your or third parties' legal rights to forbid circumvention of 189 | technological measures. 190 | 191 | #### 4. Conveying Verbatim Copies. 192 | 193 | You may convey verbatim copies of the Program's source code as you 194 | receive it, in any medium, provided that you conspicuously and 195 | appropriately publish on each copy an appropriate copyright notice; keep 196 | intact all notices stating that this License and any non-permissive 197 | terms added in accord with section 7 apply to the code; keep intact all 198 | notices of the absence of any warranty; and give all recipients a copy 199 | of this License along with the Program. 200 | 201 | You may charge any price or no price for each copy that you convey, and 202 | you may offer support or warranty protection for a fee. 203 | 204 | #### 5. Conveying Modified Source Versions. 205 | 206 | You may convey a work based on the Program, or the modifications to 207 | produce it from the Program, in the form of source code under the terms 208 | of section 4, provided that you also meet all of these conditions: 209 | 210 | - a\) The work must carry prominent notices stating that you modified it, 211 | and giving a relevant date. 212 | - b\) The work must carry prominent notices stating that it is released 213 | under this License and any conditions added under section 7. This 214 | requirement modifies the requirement in section 4 to "keep intact 215 | all notices". 216 | - c\) You must license the entire work, as a whole, under this License to 217 | anyone who comes into possession of a copy. This License will therefore 218 | apply, along with any applicable section 7 additional terms, to the 219 | whole of the work, and all its parts, regardless of how they 220 | are packaged. This License gives no permission to license the work in 221 | any other way, but it does not invalidate such permission if you have 222 | separately received it. 223 | - d\) If the work has interactive user interfaces, each must display 224 | Appropriate Legal Notices; however, if the Program has interactive 225 | interfaces that do not display Appropriate Legal Notices, your work need 226 | not make them do so. 227 | 228 | A compilation of a covered work with other separate and independent 229 | works, which are not by their nature extensions of the covered work, and 230 | which are not combined with it such as to form a larger program, in or 231 | on a volume of a storage or distribution medium, is called an 232 | "aggregate" if the compilation and its resulting copyright are not used 233 | to limit the access or legal rights of the compilation's users beyond 234 | what the individual works permit. Inclusion of a covered work in an 235 | aggregate does not cause this License to apply to the other parts of the 236 | aggregate. 237 | 238 | #### 6. Conveying Non-Source Forms. 239 | 240 | You may convey a covered work in object code form under the terms of 241 | sections 4 and 5, provided that you also convey the machine-readable 242 | Corresponding Source under the terms of this License, in one of these 243 | ways: 244 | 245 | - a\) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by the 247 | Corresponding Source fixed on a durable physical medium customarily used 248 | for software interchange. 249 | - b\) Convey the object code in, or embodied in, a physical product 250 | (including a physical distribution medium), accompanied by a written 251 | offer, valid for at least three years and valid for as long as you offer 252 | spare parts or customer support for that product model, to give anyone 253 | who possesses the object code either (1) a copy of the Corresponding 254 | Source for all the software in the product that is covered by this 255 | License, on a durable physical medium customarily used for software 256 | interchange, for a price no more than your reasonable cost of physically 257 | performing this conveying of source, or (2) access to copy the 258 | Corresponding Source from a network server at no charge. 259 | - c\) Convey individual copies of the object code with a copy of the 260 | written offer to provide the Corresponding Source. This alternative is 261 | allowed only occasionally and noncommercially, and only if you received 262 | the object code with such an offer, in accord with subsection 6b. 263 | - d\) Convey the object code by offering access from a designated place 264 | (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to copy 268 | the object code is a network server, the Corresponding Source may be on 269 | a different server (operated by you or a third party) that supports 270 | equivalent copying facilities, provided you maintain clear directions 271 | next to the object code saying where to find the Corresponding Source. 272 | Regardless of what server hosts the Corresponding Source, you remain 273 | obligated to ensure that it is available for as long as needed to 274 | satisfy these requirements. 275 | - e\) Convey the object code using peer-to-peer transmission, provided you 276 | inform other peers where the object code and Corresponding Source of the 277 | work are being offered to the general public at no charge under 278 | subsection 6d. 279 | 280 | A separable portion of the object code, whose source code is excluded 281 | from the Corresponding Source as a System Library, need not be included 282 | in conveying the object code work. 283 | 284 | A "User Product" is either (1) a "consumer product", which means any 285 | tangible personal property which is normally used for personal, family, 286 | or household purposes, or (2) anything designed or sold for 287 | incorporation into a dwelling. In determining whether a product is a 288 | consumer product, doubtful cases shall be resolved in favor of coverage. 289 | For a particular product received by a particular user, "normally used" 290 | refers to a typical or common use of that class of product, regardless 291 | of the status of the particular user or of the way in which the 292 | particular user actually uses, or expects or is expected to use, the 293 | product. A product is a consumer product regardless of whether the 294 | product has substantial commercial, industrial or non-consumer uses, 295 | unless such uses represent the only significant mode of use of the 296 | product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product 301 | from a modified version of its Corresponding Source. The information 302 | must suffice to ensure that the continued functioning of the modified 303 | object code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied by 312 | the Installation Information. But this requirement does not apply if 313 | neither you nor any third party retains the ability to install modified 314 | object code on the User Product (for example, the work has been 315 | installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, in 326 | accord with this section must be in a format that is publicly documented 327 | (and with an implementation available to the public in source code 328 | form), and must require no special password or key for unpacking, 329 | reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by this 340 | License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option remove 343 | any additional permissions from that copy, or from any part of it. 344 | (Additional permissions may be written to require their own removal in 345 | certain cases when you modify the work.) You may place additional 346 | permissions on material, added by you to a covered work, for which you 347 | have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a\) Disclaiming warranty or limiting liability differently from the terms 354 | of sections 15 and 16 of this License; or 355 | - b\) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal Notices 357 | displayed by works containing it; or 358 | - c\) Prohibiting misrepresentation of the origin of that material, or 359 | requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d\) Limiting the use for publicity purposes of names of licensors or 362 | authors of the material; or 363 | - e\) Declining to grant rights under trademark law for use of some trade 364 | names, trademarks, or service marks; or 365 | - f\) Requiring indemnification of licensors and authors of that material 366 | by anyone who conveys the material (or modified versions of it) with 367 | contractual assumptions of liability to the recipient, for any liability 368 | that these contractual assumptions directly impose on those licensors 369 | and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains a 376 | further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms of 378 | that license document, provided that the further restriction does not 379 | survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you must 382 | place, in the relevant source files, a statement of the additional terms 383 | that apply to those files, or a notice indicating where to find the 384 | applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the above 388 | requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally terminates 401 | your license, and (b) permanently, if the copyright holder fails to 402 | notify you of the violation by some reasonable means prior to 60 days 403 | after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is reinstated 406 | permanently if the copyright holder notifies you of the violation by 407 | some reasonable means, this is the first time you have received notice 408 | of violation of this License (for any work) from that copyright holder, 409 | and you cure the violation prior to 30 days after your receipt of the 410 | notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run a 421 | copy of the Program. Ancillary propagation of a covered work occurring 422 | solely as a consequence of using peer-to-peer transmission to receive a 423 | copy likewise does not require acceptance. However, nothing other than 424 | this License grants you permission to propagate or modify any covered 425 | work. These actions infringe copyright if you do not accept this 426 | License. Therefore, by modifying or propagating a covered work, you 427 | indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered work 439 | results from an entity transaction, each party to that transaction who 440 | receives a copy of the work also receives whatever licenses to the work 441 | the party's predecessor in interest had or could give under the previous 442 | paragraph, plus a right to possession of the Corresponding Source of the 443 | work from the predecessor in interest, if the predecessor has it or can 444 | get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may not 448 | impose a license fee, royalty, or other charge for exercise of rights 449 | granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that any 451 | patent claim is infringed by making, using, selling, offering for sale, 452 | or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The work 458 | thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned or 461 | controlled by the contributor, whether already acquired or hereafter 462 | acquired, that would be infringed by some manner, permitted by this 463 | License, of making, using, or selling its contributor version, but do 464 | not include claims that would be infringed only as a consequence of 465 | further modification of the contributor version. For purposes of this 466 | definition, "control" includes the right to grant patent sublicenses in 467 | a manner consistent with the requirements of this License. 468 | 469 | Each contributor grants you a non-exclusive, worldwide, royalty-free 470 | patent license under the contributor's essential patent claims, to make, 471 | use, sell, offer for sale, import and otherwise run, modify and 472 | propagate the contents of its contributor version. 473 | 474 | In the following three paragraphs, a "patent license" is any express 475 | agreement or commitment, however denominated, not to enforce a patent 476 | (such as an express permission to practice a patent or covenant not to 477 | sue for patent infringement). To "grant" such a patent license to a 478 | party means to make such an agreement or commitment not to enforce a 479 | patent against the party. 480 | 481 | If you convey a covered work, knowingly relying on a patent license, and 482 | the Corresponding Source of the work is not available for anyone to 483 | copy, free of charge and under the terms of this License, through a 484 | publicly available network server or other readily accessible means, 485 | then you must either (1) cause the Corresponding Source to be so 486 | available, or (2) arrange to deprive yourself of the benefit of the 487 | patent license for this particular work, or (3) arrange, in a manner 488 | consistent with the requirements of this License, to extend the patent 489 | license to downstream recipients. "Knowingly relying" means you have 490 | actual knowledge that, but for the patent license, your conveying the 491 | covered work in a country, or your recipient's use of the covered work 492 | in a country, would infringe one or more identifiable patents in that 493 | country that you have reason to believe are valid. 494 | 495 | If, pursuant to or in connection with a single transaction or 496 | arrangement, you convey, or propagate by procuring conveyance of, a 497 | covered work, and grant a patent license to some of the parties 498 | receiving the covered work authorizing them to use, propagate, modify or 499 | convey a specific copy of the covered work, then the patent license you 500 | grant is automatically extended to all recipients of the covered work 501 | and works based on it. 502 | 503 | A patent license is "discriminatory" if it does not include within the 504 | scope of its coverage, prohibits the exercise of, or is conditioned on 505 | the non-exercise of one or more of the rights that are specifically 506 | granted under this License. You may not convey a covered work if you are 507 | a party to an arrangement with a third party that is in the business of 508 | distributing software, under which you make payment to the third party 509 | based on the extent of your activity of conveying the work, and under 510 | which the third party grants, to any of the parties who would receive 511 | the covered work from you, a discriminatory patent license (a) in 512 | connection with copies of the covered work conveyed by you (or copies 513 | made from those copies), or (b) primarily for and in connection with 514 | specific products or compilations that contain the covered work, unless 515 | you entered into that arrangement, or that patent license was granted, 516 | prior to 28 March 2007. 517 | 518 | Nothing in this License shall be construed as excluding or limiting any 519 | implied license or other defenses to infringement that may otherwise be 520 | available to you under applicable patent law. 521 | 522 | #### 12. No Surrender of Others' Freedom. 523 | 524 | If conditions are imposed on you (whether by court order, agreement or 525 | otherwise) that contradict the conditions of this License, they do not 526 | excuse you from the conditions of this License. If you cannot convey a 527 | covered work so as to satisfy simultaneously your obligations under this 528 | License and any other pertinent obligations, then as a consequence you 529 | may not convey it at all. For example, if you agree to terms that 530 | obligate you to collect a royalty for further conveying from those to 531 | whom you convey the Program, the only way you could satisfy both those 532 | terms and this License would be to refrain entirely from conveying the 533 | Program. 534 | 535 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 536 | 537 | Notwithstanding any other provision of this License, if you modify the 538 | Program, your modified version must prominently offer all users 539 | interacting with it remotely through a computer network (if your version 540 | supports such interaction) an opportunity to receive the Corresponding 541 | Source of your version by providing access to the Corresponding Source 542 | from a network server at no charge, through some standard or customary 543 | means of facilitating copying of software. This Corresponding Source 544 | shall include the Corresponding Source for any work covered by version 3 545 | of the GNU General Public License that is incorporated pursuant to the 546 | following paragraph. 547 | 548 | Notwithstanding any other provision of this License, you have permission 549 | to link or combine any covered work with a work licensed under version 3 550 | of the GNU General Public License into a single combined work, and to 551 | convey the resulting work. The terms of this License will continue to 552 | apply to the part which is the covered work, but the work with which it 553 | is combined will remain governed by version 3 of the GNU General Public 554 | License. 555 | 556 | #### 14. Revised Versions of this License. 557 | 558 | The Free Software Foundation may publish revised and/or new versions of 559 | the GNU Affero General Public License from time to time. Such new 560 | versions will be similar in spirit to the present version, but may 561 | differ in detail to address new problems or concerns. 562 | 563 | Each version is given a distinguishing version number. If the Program 564 | specifies that a certain numbered version of the GNU Affero General 565 | Public License "or any later version" applies to it, you have the option 566 | of following the terms and conditions either of that numbered version or 567 | of any later version published by the Free Software Foundation. If the 568 | Program does not specify a version number of the GNU Affero General 569 | Public License, you may choose any version ever published by the Free 570 | Software Foundation. 571 | 572 | If the Program specifies that a proxy can decide which future versions 573 | of the GNU Affero General Public License can be used, that proxy's 574 | public statement of acceptance of a version permanently authorizes you 575 | to choose that version for the Program. 576 | 577 | Later license versions may give you additional or different permissions. 578 | However, no additional obligations are imposed on any author or 579 | copyright holder as a result of your choosing to follow a later version. 580 | 581 | #### 15. Disclaimer of Warranty. 582 | 583 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 584 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 585 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 586 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 587 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 588 | PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF 589 | THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME 590 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 591 | 592 | #### 16. Limitation of Liability. 593 | 594 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 595 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 596 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 597 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 598 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 599 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES 600 | SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE 601 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN 602 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 603 | 604 | #### 17. Interpretation of Sections 15 and 16. 605 | 606 | If the disclaimer of warranty and limitation of liability provided above 607 | cannot be given local legal effect according to their terms, reviewing 608 | courts shall apply local law that most closely approximates an absolute 609 | waiver of all civil liability in connection with the Program, unless a 610 | warranty or assumption of liability accompanies a copy of the Program in 611 | return for a fee. 612 | 613 | END OF TERMS AND CONDITIONS 614 | 615 | ### How to Apply These Terms to Your New Programs 616 | 617 | If you develop a new program, and you want it to be of the greatest 618 | possible use to the public, the best way to achieve this is to make it 619 | free software which everyone can redistribute and change under these 620 | terms. 621 | 622 | To do so, attach the following notices to the program. It is safest to 623 | attach them to the start of each source file to most effectively state 624 | the exclusion of warranty; and each file should have at least the 625 | "copyright" line and a pointer to where the full notice is found. 626 | 627 | 628 | Copyright (C) 629 | 630 | This program is free software: you can redistribute it and/or modify 631 | it under the terms of the GNU Affero General Public License as 632 | published by the Free Software Foundation, either version 3 of the 633 | License, or (at your option) any later version. 634 | 635 | This program is distributed in the hope that it will be useful, 636 | but WITHOUT ANY WARRANTY; without even the implied warranty of 637 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 638 | GNU Affero General Public License for more details. 639 | 640 | You should have received a copy of the GNU Affero General Public License 641 | along with this program. If not, see . 642 | 643 | Also add information on how to contact you by electronic and paper mail. 644 | 645 | If your software can interact with users remotely through a computer 646 | network, you should also make sure that it provides a way for users to 647 | get its source. For example, if your program is a web application, its 648 | interface could display a "Source" link that leads users to an archive 649 | of the code. There are many ways you could offer source, and different 650 | solutions will be better for different programs; see section 13 for the 651 | specific requirements. 652 | 653 | You should also get your employer (if you work as a programmer) or 654 | school, if any, to sign a "copyright disclaimer" for the program, if 655 | necessary. For more information on this, and how to apply and follow the 656 | GNU AGPL, see <>. 657 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nmap-log-parse 2 | 3 | Logs which devices are in your local network and draws graphs 4 | 5 | Screenshot: 6 | 7 | ![Screenshot](screenshot.png) 8 | 9 | ## Setup 10 | 11 | 1. Clone this repository and checkout the precompiled branch 12 | 13 | ```bash 14 | git clone https://github.com/phiresky/nmap-log-parse 15 | cd nmap-log-parse 16 | git checkout gh-pages 17 | ``` 18 | 19 | 2. Add this line to your **_root_** crontab on your raspberry pi (or other device that is always on): 20 | 21 | `*/10 * * * * nmap -sn '192.168.178.*' -oX - >> /var/www/nmap-logs/$(date -I).xml` 22 | 23 | You can edit the root crontab by running something like `sudo EDITOR=nano crontab -e` 24 | 25 | Replace `'192.168.178.*'` with your network range and `/var/www/nmap-logs/` with the target location. 26 | 27 | This takes about ~10MB of storage per month. 28 | 29 | **The above command _must_ be put into the root crontab!** 30 | Otherwise, nmap can't read mac-addresses and the output will be wrong. 31 | 32 | 3. create a `config.json` file in the same folder as the built `index.html` is in (dist), 33 | overriding any of the values in the [default config](./config.ts). 34 | 35 | for example: 36 | 37 | **config.json** 38 | 39 | ```json 40 | { 41 | "logFilesPath": "/nmap/logs/", 42 | "staticLogFiles": ["logs/oldlogs"], 43 | "deviceNames": { 44 | "AB:CD:EF:01:23": "John's PC" 45 | } 46 | } 47 | ``` 48 | 49 | Use `staticLogFiles` only if you have an undated log file from an older version. 50 | 51 | The logFilesPath is either relative to the root dirctory of the server (if starting with /) or the built index.html file (otherwise) 52 | 53 | 4. Wait more than an hour. Make sure the first log file (in the form 2019-06-29.xml) exists and contains XML documents. 54 | 5. Open `index.html` in a browser. Open it from a server (like a minimal `python3 -m http.server`) instead of from the filesystem because of Cross-Domain security. 55 | 56 | ## Contributing 57 | 58 | Made with [React], the [Dexie.js] database and [Highcharts]. 59 | 60 | Get the dependencies using `yarn install`, then run `yarn build` to build the JS into the /dist directory. For development, run `yarn dev` to start the dev server. 61 | 62 | This project is written in [TypeScript], which is basically JavaScript (ES6), but strongly typed. I can recommend [Visual Studio Code][vsc] (on linux) for IntelliSense and Refactoring support. 63 | 64 | If you have ideas to make this better please open an issue, or even better, send a pull request. 65 | 66 | [typescript]: https://www.typescriptlang.org 67 | [react]: https://facebook.github.io/react/ 68 | [dexie.js]: http://dexie.org/ 69 | [highcharts]: http://www.highcharts.com/ 70 | [vsc]: https://code.visualstudio.com/ 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Who's in my network? 5 | 6 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nmap-log-parse", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "none", 6 | "scripts": { 7 | "lint": "eslint . --ext .ts,.tsx", 8 | "dev": "parcel watch --public-url . index.html", 9 | "build": "parcel build --public-url . index.html", 10 | "setup-pages": "git worktree add -b gh-pages dist", 11 | "pages": "rm -r dist/* && yarn build && cd dist && git add -A . && git commit -m'update binaries'" 12 | }, 13 | "dependencies": { 14 | "@babel/core": "^7.4.4", 15 | "@babel/preset-env": "^7.4.4", 16 | "@types/lodash": "^4.14.129", 17 | "@types/react": "^16.8.17", 18 | "@types/react-dom": "^16.8.4", 19 | "bootstrap": "^4.3.1", 20 | "dexie": "^3.0.1", 21 | "highcharts": "^8.1.2", 22 | "highcharts-react-official": "^3.0.0", 23 | "lodash": "^4.17.11", 24 | "mobx": "^5.9.4", 25 | "mobx-react": "^6.2.2", 26 | "mobx-react-lite": "^2.0.7", 27 | "parcel-bundler": "^1.12.3", 28 | "react": "^16.8.6", 29 | "react-dom": "^16.8.6", 30 | "sax-wasm": "^1.4.5", 31 | "whatwg-fetch": "^3.0.0" 32 | }, 33 | "devDependencies": { 34 | "@typescript-eslint/eslint-plugin": "^3.4.0", 35 | "@typescript-eslint/parser": "^3.4.0", 36 | "eslint": "^7.3.1", 37 | "eslint-config-prettier": "^6.11.0", 38 | "husky": ">=1", 39 | "lint-staged": ">=8", 40 | "prettier": "^2.0.5", 41 | "typescript": "^3.9.5" 42 | }, 43 | "author": "phiresky", 44 | "license": "AGPL-3.0", 45 | "browserslist": [ 46 | "last 1 Chrome version" 47 | ], 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "lint-staged": { 54 | "*.{ts,tsx,js,css,scss,json,md}": [ 55 | "prettier --write" 56 | ] 57 | }, 58 | "prettier": { 59 | "semi": true, 60 | "tabWidth": 4, 61 | "useTabs": true, 62 | "trailingComma": "all", 63 | "printWidth": 80, 64 | "endOfLine": "lf" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phiresky/nmap-log-parse/c60ba0655d1088bc904f182ed869115e1559f68b/screenshot.png -------------------------------------------------------------------------------- /src/AggregatedChart.tsx: -------------------------------------------------------------------------------- 1 | import * as Highcharts from "highcharts"; 2 | import _, { minBy } from "lodash"; 3 | import * as React from "react"; 4 | import { Config } from "./config"; 5 | import { DeviceInfo, NmapLog } from "./db"; 6 | import { ReactChart } from "./gui"; 7 | import { observable } from "mobx"; 8 | import { lazy } from "./lazy"; 9 | import { DateRounder, levelInvert, uptimePart } from "./util"; 10 | import { observer } from "mobx-react"; 11 | type AggregatedChartData = SingleChartData & { rounder: DateRounder }; 12 | 13 | export type CommonChartData = { 14 | data: NmapLog[]; 15 | config: Config; 16 | deviceInfos: Map; 17 | }; 18 | export type SingleChartData = CommonChartData & { 19 | title: string; 20 | highchartsOptions?: (o: Highcharts.Options) => void; 21 | }; 22 | function aggregate( 23 | datas: NmapLog[], 24 | rounder: DateRounder, 25 | ): Map> { 26 | const map = new Map>(); 27 | datas 28 | .flatMap((log) => { 29 | const rounded = rounder(new Date(log.time)); 30 | return rounded 31 | ? [ 32 | { 33 | time: rounded.getTime(), 34 | devices: log.devices, 35 | }, 36 | ] 37 | : []; 38 | }) 39 | .sort((a, b) => a.time - b.time) 40 | .forEach((data) => { 41 | if (!map.has(data.time)) map.set(data.time, new Map()); 42 | const map2 = map.get(data.time)!; 43 | for (const dev of data.devices) 44 | map2.set(dev, (map2.get(dev) || 0) + 1); 45 | }); 46 | return map; 47 | } 48 | 49 | @observer 50 | export class AggregatedChart extends React.Component { 51 | @observable 52 | chartOptions: Highcharts.Options; 53 | 54 | constructor(props: AggregatedChartData) { 55 | super(props); 56 | this.chartOptions = { title: { text: props.title + ": Loading..." } }; 57 | } 58 | 59 | chart: Highcharts.Chart | null = null; 60 | componentDidMount(): void { 61 | this.init(); 62 | } 63 | 64 | componentDidUpdate( 65 | oldProps: AggregatedChartData, 66 | _oldState: { 67 | options: Highcharts.Options; 68 | }, 69 | ): void { 70 | if (oldProps !== this.props) this.init(); 71 | } 72 | init(): void { 73 | const agg = levelInvert( 74 | aggregate(this.props.data, this.props.rounder), 75 | 0, 76 | ); 77 | const meUptime = agg.get(this.props.config.selfMacAddress); 78 | if (!meUptime) { 79 | this.setState({ 80 | options: { title: { text: this.props.title + ": No Data." } }, 81 | }); 82 | return; 83 | } 84 | // exclude data of device calling nmap 85 | agg.delete(this.props.config.selfMacAddress); 86 | const totalMeUptime = lazy(meUptime.values()).sum(); 87 | const logIntervalMS = 1000 * 60 * this.props.config.logIntervalMinutes; 88 | let minDistance = Infinity; 89 | 90 | const hncounts = _([...this.props.deviceInfos.values()]) 91 | .flatMap((p) => p.hostnames) 92 | .countBy((z) => z) 93 | .value(); 94 | const ipcounts = _([...this.props.deviceInfos.values()]) 95 | .flatMap((p) => p.ips) 96 | .countBy((z) => z) 97 | .value(); 98 | 99 | function getNiceHostname(info: DeviceInfo) { 100 | if (info.displayname) return info.displayname; 101 | if (info.hostnames.length > 0) 102 | return minBy(info.hostnames, (p) => hncounts[p]); 103 | if (info.ips.length > 0) return minBy(info.ips, (p) => ipcounts[p]); 104 | return null; 105 | } 106 | 107 | const data = [...agg.entries()] 108 | .filter( 109 | ([_mac, vals]) => 110 | lazy(vals.values()).sum() >= 111 | totalMeUptime * this.props.config.minimumUptime, 112 | ) 113 | .map(([mac, map]) => { 114 | const info = this.props.deviceInfos.get(mac); 115 | const footer = ` 116 |

MAC: ${mac} ${info?.vendor?.join(", ") || "???"}


117 |

Hostnames:
${ 118 | info?.hostnames?.join("
") || "???" 119 | }


120 |

IPs:
${info?.ips?.join("
") || "???"}

121 | `; 122 | return { 123 | name: (info && getNiceHostname(info)) || mac, 124 | type: "line", 125 | tooltip: { 126 | footerFormat: footer, 127 | }, 128 | data: lazy(map) 129 | .mapToTuple(([time, amount]) => [ 130 | time, 131 | Math.round( 132 | 100 * uptimePart(amount, meUptime.get(time)), 133 | ), 134 | ]) 135 | .intersplice((left, right) => { 136 | const distance = right[0] - left[0]; 137 | if (distance < minDistance) minDistance = distance; 138 | if (distance >= minDistance * 2) 139 | return [ 140 | [left[0] + logIntervalMS, null], 141 | [right[0] - logIntervalMS, null], 142 | ] as [number, number | null][]; 143 | else return []; 144 | }) 145 | .collect(), 146 | }; 147 | }); 148 | data.sort((a, b) => (a.name || "").localeCompare(b.name || "")); 149 | let showHideFlag = false; 150 | const co: Highcharts.Options = { 151 | chart: { type: "line", zoomType: "x" }, 152 | title: { text: this.props.title }, 153 | time: { useUTC: true }, 154 | xAxis: { 155 | type: "datetime", 156 | }, 157 | tooltip: { 158 | valueSuffix: "%", 159 | }, 160 | plotOptions: { 161 | line: { marker: { enabled: false }, animation: false }, 162 | series: { 163 | events: { 164 | legendItemClick() { 165 | const chart = this.chart; 166 | const series = chart.series; 167 | if (this.index === 0) { 168 | if (!showHideFlag) { 169 | series.forEach((series) => series.hide()); 170 | } else { 171 | series.forEach((series) => series.show()); 172 | } 173 | showHideFlag = !showHideFlag; 174 | this.hide(); 175 | } 176 | }, 177 | }, 178 | }, 179 | }, 180 | yAxis: { 181 | title: { text: "Online" }, 182 | labels: { format: "{value:%.0f}%" }, 183 | min: 0, 184 | max: 100, 185 | }, 186 | /*legend: { 187 | layout: "vertical", 188 | },*/ 189 | // tooltip: {}, 190 | series: [ 191 | { 192 | name: "Show/Hide all", 193 | visible: true, 194 | type: "line", 195 | }, 196 | ...data, 197 | ], 198 | }; 199 | if (this.props.highchartsOptions) this.props.highchartsOptions(co); 200 | this.chartOptions = co; 201 | } 202 | render(): React.ReactElement { 203 | return ( 204 | (this.chart = chart)} 207 | /> 208 | ); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/CustomyChart.tsx: -------------------------------------------------------------------------------- 1 | import { observer, useLocalStore } from "mobx-react-lite"; 2 | import * as React from "react"; 3 | import { CommonChartData } from "./AggregatedChart"; 4 | import { 5 | Granularities, 6 | GranularityChoosingChart, 7 | } from "./GranularityChoosingChart"; 8 | import { DateRounder, setHighchartsOptionsForPreset } from "./util"; 9 | 10 | export const presets = { 11 | weekly: { 12 | title: "Weekly", 13 | headerFormat: `{point.key:%A %H:%M}
`, 14 | xAxisLabels: { format: "{value:%a. %H:%M}" }, 15 | offset: (d: Date): Date => { 16 | d.setFullYear(1970); 17 | d.setMonth(0, d.getDay() + 5); 18 | return d; 19 | }, 20 | }, 21 | daily: { 22 | title: "Daily", 23 | headerFormat: `{point.key:%H:%M}
`, 24 | xAxisLabels: { format: "{value:%H:%M}" }, 25 | offset: (d: Date): Date => { 26 | d.setFullYear(1970); 27 | d.setMonth(0, 1); 28 | return d; 29 | }, 30 | }, 31 | }; 32 | export type Preset = typeof presets["weekly"]; 33 | 34 | function useTwoWay( 35 | initialValue: V, 36 | get: (t: T) => V, 37 | set: (v: V) => Partial, 38 | ) { 39 | const store = useLocalStore(() => ({ value: initialValue })); 40 | return { 41 | value: store.value, 42 | props: { 43 | onChange: (e: React.ChangeEvent) => 44 | (store.value = get(e.currentTarget)), 45 | ...set(store.value), 46 | }, 47 | }; 48 | } 49 | function useTwoWayDate(initialDate: Date) { 50 | return useTwoWay< 51 | Pick, 52 | Date | null 53 | >( 54 | initialDate, 55 | (e) => e.valueAsDate, 56 | (v) => ({ value: v ? v.toISOString().slice(0, 10) : "" }), 57 | ); 58 | } 59 | const defaultOldDate = new Date(); 60 | defaultOldDate.setDate(defaultOldDate.getDate() - 31); 61 | 62 | function _CustomyChart({ 63 | granularities, 64 | ...props 65 | }: { granularities: Granularities } & CommonChartData) { 66 | const fromDate = useTwoWayDate(defaultOldDate); 67 | const toDate = useTwoWayDate(new Date()); 68 | 69 | type AggChoice = "none" | keyof typeof presets; 70 | const aggregationChoices = ["none", ...Object.keys(presets)] as AggChoice[]; 71 | const data = useLocalStore(() => ({ 72 | aggregator: aggregationChoices[0], 73 | offsetter: ((d: Date) => d) as DateRounder, 74 | active: false, 75 | title: "Custom Chart", 76 | preset: null as null | typeof presets.daily, 77 | rerender() { 78 | this.active = true; 79 | this.preset = 80 | data.aggregator !== "none" ? presets[data.aggregator] : null; 81 | 82 | this.offsetter = (d: Date) => { 83 | if ( 84 | (fromDate.value && d < fromDate.value) || 85 | (toDate.value && d > toDate.value) 86 | ) 87 | return null; 88 | return this.preset ? this.preset.offset(d) : d; 89 | }; 90 | }, 91 | })); 92 | return ( 93 |
94 |

Custom Filter

95 | From To{" "} 96 | 97 | Aggregator{" "} 98 | 110 | 111 | {data.active && ( 112 | 127 | )} 128 |
129 | ); 130 | } 131 | export const CustomyChart = observer(_CustomyChart); 132 | -------------------------------------------------------------------------------- /src/GranularityChoosingChart.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { AggregatedChart, SingleChartData } from "./AggregatedChart"; 3 | import { DateRounder } from "./util"; 4 | 5 | export type Granularities = [string, DateRounder][]; 6 | 7 | export type GranularityChoosingChartData = SingleChartData & { 8 | initialGranularity: string; 9 | granularities: Granularities; 10 | offsetter?: DateRounder; 11 | }; 12 | 13 | export class GranularityChoosingChart extends React.Component< 14 | GranularityChoosingChartData, 15 | { 16 | granularity: string; 17 | } 18 | > { 19 | constructor(props: GranularityChoosingChartData) { 20 | super(props); 21 | this.state = { granularity: props.initialGranularity }; 22 | } 23 | render(): React.ReactElement { 24 | const rounder = this.props.granularities.find( 25 | (k) => k[0] === this.state.granularity, 26 | )?.[1]; 27 | if (!rounder) return <>no rounder found; 28 | let rounder2 = rounder; 29 | const offsetter = this.props.offsetter; 30 | if (offsetter) { 31 | rounder2 = (date) => { 32 | const rounded = rounder(date); 33 | if (!rounded) return null; 34 | else return offsetter(rounded); 35 | }; 36 | } 37 | return ( 38 |
39 | 40 | Granularity:{" "} 41 | 55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const defaultConfig = { 2 | // path of the log files relative to the index.html file 3 | logFilesPath: "./logs/", 4 | // scan backwards for 7 days before giving up when data is missing 5 | maxMissingDays: 7, 6 | // only get this many days before stopping 7 | dayGetLimit: Infinity, 8 | // must be set to the interval with which nmap is run in your crontab, 9 | // e.g. 10 if your crontab entry is "*/10 * * * ..." 10 | logIntervalMinutes: 10, 11 | // hide devices that are up less than 2% of the time 12 | minimumUptime: 0.02, 13 | selfMacAddress: "00:00:00:00:00:00", 14 | staticLogFiles: [] as string[], 15 | deviceNames: { 16 | ["00:00:00:00:00:00"]: "me", 17 | } as { [mac: string]: string | undefined }, 18 | }; 19 | export type Config = typeof defaultConfig; 20 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import Dexie from "dexie"; 2 | import { Config } from "./config"; 3 | import { parseXML } from "./util"; 4 | import { lazy } from "./lazy"; 5 | 6 | export interface NmapLog { 7 | time: number; 8 | devices: Set; // mac addresses 9 | } 10 | interface GottenFiles { 11 | filename: string; 12 | result: "404" | "success"; 13 | } 14 | export interface MacToInfo { 15 | mac: string; 16 | type: "ip" | "hostname" | "vendor"; 17 | info: string; 18 | } 19 | export interface DeviceInfo { 20 | displayname: string | undefined; 21 | vendor: string[]; 22 | hostnames: string[]; 23 | ips: string[]; 24 | } 25 | export class Database extends Dexie { 26 | nmapLogs!: Dexie.Table; 27 | gottenFiles!: Dexie.Table; 28 | macToInfo!: Dexie.Table; 29 | constructor(private config: Config) { 30 | super("NmapLogDatabase"); 31 | this.version(1).stores({ 32 | nmapLogs: "time", 33 | gottenFiles: "filename", 34 | macToInfo: "[mac+type+info], mac, info", 35 | }); 36 | } 37 | async getDeviceInfo(mac: string): Promise { 38 | const infos = await this.macToInfo.where("mac").equals(mac).toArray(); 39 | return { 40 | displayname: this.config.deviceNames[mac], 41 | vendor: infos 42 | .filter((info) => info.type === "vendor") 43 | .map((info) => info.info) 44 | .filter((x) => x.length > 0), 45 | hostnames: infos 46 | .filter((info) => info.type === "hostname") 47 | .map((info) => info.info), 48 | ips: infos 49 | .filter((info) => info.type === "ip") 50 | .map((info) => info.info), 51 | }; 52 | } 53 | /** 54 | * @return the data for the given date, null if no data was recorded on that day 55 | */ 56 | async getForDate(date: Date): Promise<"404" | "success"> { 57 | date.setUTCHours(0, 0, 0, 0); 58 | const recentDate = new Date(); 59 | recentDate.setDate(recentDate.getDate() - 3); 60 | const dateFormatted = date.toISOString().substr(0, 10); 61 | 62 | const filename = this.config.logFilesPath + dateFormatted + ".xml"; 63 | return await this.getForFile(filename, undefined, date >= recentDate); 64 | } 65 | async getForFile( 66 | filename: string, 67 | statusCallback?: (state: string, done: number, total: number) => void, 68 | forceFetch = false, 69 | ): Promise<"404" | "success"> { 70 | const gotDate = await this.gottenFiles 71 | .get(filename) 72 | .catch((_e) => null); 73 | if (!forceFetch && gotDate) return gotDate.result; 74 | 75 | const response = await fetch(filename, { credentials: "include" }); 76 | if (response.status == 404) { 77 | await this.gottenFiles.put({ filename, result: "404" }); 78 | return "404"; 79 | } 80 | if (response.status >= 300) { 81 | throw Error( 82 | `Request Error: ${response.status}: ${response.statusText}`, 83 | ); 84 | } 85 | 86 | const rawXMLs = (await response.text()).split(" 91 | statusCallback 92 | ? [ 93 | new Promise((resolve) => { 94 | statusCallback("loading", i, total); 95 | setTimeout(resolve, 0); 96 | }), 97 | ] 98 | : []; 99 | await lazy(rawXMLs) 100 | .chunk(200) 101 | .map( 102 | async (rawXMLs) => 103 | await this.transaction( 104 | "rw", 105 | [this.gottenFiles, this.nmapLogs, this.macToInfo], 106 | () => { 107 | for (const rawXML of rawXMLs) { 108 | if (rawXML.length == 0) continue; 109 | const scan = parseXML( 110 | this.config, 111 | filename, 112 | " void): Promise { 130 | const current = new Date(); 131 | let failures = 0; 132 | let gotten = 0; 133 | while (current > new Date(2010, 0, 1)) { 134 | const resp = await this.getForDate(current); 135 | if (resp === "404") { 136 | failures++; 137 | if (failures >= this.config.maxMissingDays) break; 138 | } else { 139 | failures = 0; 140 | gotten++; 141 | progressCallback(gotten); 142 | if (gotten >= this.config.dayGetLimit) break; 143 | } 144 | current.setUTCDate(current.getUTCDate() - 1); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/gui.tsx: -------------------------------------------------------------------------------- 1 | import * as Highcharts from "highcharts"; 2 | import HighchartsReact from "highcharts-react-official"; 3 | import * as React from "react"; 4 | import { CommonChartData } from "./AggregatedChart"; 5 | import { CustomyChart, presets } from "./CustomyChart"; 6 | import { DeviceInfo } from "./db"; 7 | import { 8 | Granularities, 9 | GranularityChoosingChart, 10 | } from "./GranularityChoosingChart"; 11 | import { roundDate, uptimePart, setHighchartsOptionsForPreset } from "./util"; 12 | 13 | export class ReactChart extends React.Component<{ 14 | options: Highcharts.Options; 15 | callback: (chart: Highcharts.Chart) => void; 16 | }> { 17 | chart: Highcharts.Chart | null = null; 18 | render(): React.ReactElement { 19 | return ( 20 | 25 | ); 26 | } 27 | } 28 | 29 | const Ul = ({ list }: { list: string[] }) => ( 30 |
    31 | {list.map((str, i) => ( 32 |
  • {str}
  • 33 | ))} 34 |
35 | ); 36 | const HostInfoLine = ( 37 | info: DeviceInfo & { mac: string; upTime: number; upRelative: number }, 38 | ) => ( 39 | 40 | {[ 41 | info.displayname, 42 | `${info.mac} ${ 43 | info.vendor.length > 0 ? `(${info.vendor.join(", ")})` : "" 44 | }`, 45 |
    , 46 |
      , 47 | info.upTime.toFixed(0) + "h", 48 | `${(info.upRelative * 100) | 0}%`, 49 | ].map((x, i) => ( 50 | {x} 51 | ))} 52 | 53 | ); 54 | 55 | export const GuiContainer: React.FunctionComponent<{ 56 | dataUsage?: number; 57 | children?: React.ReactNode; 58 | }> = ({ dataUsage = 0, children = {} }) => ( 59 |
      60 |
      61 |

      Who's in my network?

      {" "} 62 | 63 | Source Code on GitHub 64 | 65 |
      66 | {children} 67 |
      68 |
      69 | 87 |
      88 |
      89 | ); 90 | export const ProgressGui: React.FunctionComponent<{ 91 | progress: number; 92 | total?: number; 93 | prefix: string; 94 | suffix: string; 95 | }> = (props) => ( 96 | 97 |
      98 |
      111 | {props.prefix} 112 | {props.progress} 113 | {props.suffix} 114 |
      115 |
      116 |
      117 | ); 118 | 119 | export class Gui extends React.Component< 120 | CommonChartData & { dataUsage: number } 121 | > { 122 | granularities: Granularities = [ 123 | ["Weekly", (date: Date): Date => roundDate(date, 7, 24)], 124 | ["Daily", (date: Date): Date => roundDate(date, 1, 24)], 125 | ["3 hourly", (date: Date): Date => roundDate(date, 1, 3)], 126 | ["hourly", (date: Date): Date => roundDate(date, 1, 1)], 127 | ["20 minutes", (date: Date): Date => roundDate(date, 1, 1, 20)], 128 | ]; 129 | render(): React.ReactElement { 130 | const me = this.props.deviceInfos.get(this.props.config.selfMacAddress); 131 | const meUptime = me?.upCount; 132 | return ( 133 | 134 | 140 | Date.now() - date.getTime() < 1000 * 60 * 60 * 24 * 7 141 | ? date 142 | : null 143 | } 144 | /> 145 |
      146 | 152 | Date.now() - date.getTime() < 1000 * 60 * 60 * 24 * 31 153 | ? date 154 | : null 155 | } 156 | /> 157 |
      158 | 164 |
      165 | 176 |
      177 | 188 |
      189 | 193 |
      194 |

      Totals

      195 |
      196 | 197 | 198 | 199 | {"Name,Mac,Hostnames,IPs,Recorded Uptime,Average Uptime" 200 | .split(",") 201 | .map((x) => ( 202 | 203 | ))} 204 | 205 | 206 | 207 | {[...this.props.deviceInfos] 208 | .sort( 209 | ([_a, i1], [_b, i2]) => 210 | i2.upCount - i1.upCount, 211 | ) 212 | .map(([mac, h]) => ( 213 | 228 | ))} 229 | 230 |
      {x}
      231 |
      232 |
      233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/lazy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for functional operations on lazily evaluated ECMAScript iterators / generators 3 | * 4 | * TODO: replace with lodash 5 | * 6 | * 7 | * For example, 8 | * 9 | * lazy([1,2,3,4,5]).map(x => x * x).filter(x => x > 5).forEach(x => console.log(x)) 10 | * 11 | * creates the same output as 12 | * 13 | * [1,2,3,4,5].map(x => x * x).filter(x => x > 5).forEach(x => console.log(x)) 14 | * 15 | * but without creating any intermediary arrays. 16 | */ 17 | class Lazy implements Iterable { 18 | constructor(private iterable: Iterable) {} 19 | 20 | map(mapper: (t: T) => U): Lazy { 21 | const self = this; 22 | return lazy( 23 | (function* () { 24 | for (const element of self) yield mapper(element); 25 | })(), 26 | ); 27 | } 28 | 29 | mapToTuple(mapper: (t: T) => [A, B]): Lazy<[A, B]> { 30 | return this.map(mapper); 31 | } 32 | 33 | /** 34 | * Replace every element with any number of new elements. 35 | * 36 | * @example 37 | * ```ts 38 | * lazy.range(0, 5).flatMap(i => lazy.generate(i, () => i)) 39 | * // => [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] 40 | * ``` 41 | */ 42 | flatMap(mapper: (t: T) => Iterable) { 43 | const self = this; 44 | return lazy( 45 | (function* () { 46 | for (const element of self) yield* mapper(element); 47 | })(), 48 | ); 49 | } 50 | 51 | /** 52 | * Create an ECMAScript Map extracting a custom key and value from each element. 53 | * 54 | * @example 55 | * ```ts 56 | * const map = lazy([{name: "John", age: 24}, {name: "Jane", age: 22}]) 57 | * .toMapKeyed(e => e.name, e => e); 58 | * map.get("John"); // {name: "John", age: 24} 59 | * ``` 60 | */ 61 | toMapKeyed( 62 | keyGetter: (t: T) => K, 63 | valueGetter: (t: T) => V, 64 | ): Map { 65 | return this.mapToTuple((t) => [keyGetter(t), valueGetter(t)]).toMap(); 66 | } 67 | 68 | /** 69 | * Retain only elements matching the filter function. 70 | */ 71 | filter(filter: (t: T) => boolean) { 72 | const self = this; 73 | return lazy( 74 | (function* () { 75 | for (const element of self) if (filter(element)) yield element; 76 | })(), 77 | ); 78 | } 79 | 80 | /** 81 | * Calls the specified callback function for all the elements in an array. 82 | * The return value of the callback function is the accumulated result, and 83 | * is provided as an argument in the next call to the callback function. 84 | */ 85 | reduce( 86 | callbackfn: (previousValue: U, currentValue: T) => U, 87 | initialValue: U, 88 | ): U { 89 | let previous = initialValue; 90 | for (const element of this) { 91 | previous = callbackfn(previous, element); 92 | } 93 | return previous; 94 | } 95 | 96 | /** 97 | * Sort the elements of this lazy in ascending order according to `keyGetter`. 98 | * 99 | * Forces complete evaluation. 100 | */ 101 | sort(keyGetter: (t: T) => number | string) { 102 | return lazy( 103 | [...this].sort((a, b) => { 104 | const ak = keyGetter(a), 105 | bk = keyGetter(b); 106 | return ak < bk ? -1 : ak > bk ? 1 : 0; 107 | }), 108 | ); 109 | } 110 | 111 | /** 112 | * Split this Lazy into multiple lazies of length `limit`. 113 | * The last chunk may be shorter if the length is not dividable by `limit`. 114 | * 115 | * Forces partial evaluation of `limit` elements at a time. 116 | */ 117 | chunk(limit: number): Lazy> { 118 | const self = this; 119 | return lazy( 120 | (function* (): Iterable> { 121 | // must be cached in an array, otherwise it's impossible to know if the outer lazy 122 | // is finished yet when the inner lazy has not been consumed 123 | let cache = [] as T[]; 124 | for (const element of self) { 125 | cache.push(element); 126 | if (cache.length === limit) { 127 | yield lazy(cache); 128 | cache = []; 129 | } 130 | } 131 | if (cache.length > 0) yield lazy(cache); 132 | })(), 133 | ); 134 | } 135 | 136 | /** 137 | * Insert new elements between every pair of existing elements 138 | * 139 | * @example 140 | * ```ts 141 | * lazy("testing").intersplice(() => [":"]) // "t:e:s:t:i:n:g" 142 | * ``` 143 | */ 144 | intersplice(between: (left: T, right: T) => Iterable): Lazy { 145 | const self = this; 146 | const nothing: T = {} as T; 147 | return lazy( 148 | (function* (): Iterable { 149 | let last = nothing; 150 | for (const element of self) { 151 | if (last !== nothing) yield* between(last, element); 152 | last = element; 153 | yield element; 154 | } 155 | })(), 156 | ); 157 | } 158 | 159 | zipWith(other: Iterable, combinator: (t: T, u: U) => V) { 160 | const self = this; 161 | return lazy( 162 | (function* (): Iterable { 163 | const selfIt = self[Symbol.iterator](); 164 | const otherIt = other[Symbol.iterator](); 165 | while (true) { 166 | const selfNext = selfIt.next(); 167 | const otherNext = otherIt.next(); 168 | if (selfNext.done || otherNext.done) return; 169 | yield combinator(selfNext.value, otherNext.value); 170 | } 171 | })(), 172 | ); 173 | } 174 | 175 | unique() { 176 | return lazy(new Set(this)); 177 | } 178 | 179 | collect() { 180 | return [...this]; 181 | } 182 | 183 | first(): T { 184 | return this[Symbol.iterator]().next().value as T; 185 | } 186 | 187 | some(predicate: (t: T) => boolean) { 188 | for (const t of this) if (predicate(t)) return true; 189 | return false; 190 | } 191 | 192 | every(predicate: (t: T) => boolean) { 193 | for (const t of this) if (!predicate(t)) return false; 194 | return true; 195 | } 196 | 197 | forEach(consumer: (t: T) => void) { 198 | for (const element of this) consumer(element); 199 | } 200 | 201 | concat(other: Iterable) { 202 | return concat(this, other); 203 | } 204 | 205 | /** 206 | * Retrieve the given number of future elements before they are consumed. Useful for promises. 207 | * 208 | * @example The following will fetch the files {file1, file2, …, file10} three at a time. 209 | * ```ts 210 | * lazy.range(0,10) 211 | * .map(x => fetch("file" + x).then(response => response.text())) 212 | * .caching(3) 213 | * .awaitSequential() 214 | * .then(texts => texts.forEach(text => console.log(text))) 215 | * ``` 216 | */ 217 | caching(count: number) { 218 | const self = this; 219 | return lazy( 220 | (function* () { 221 | const arr: T[] = []; 222 | for (const element of self) { 223 | arr.push(element); 224 | if (arr.length >= count) yield arr.shift()!; 225 | } 226 | yield* arr; 227 | })(), 228 | ); 229 | } 230 | 231 | [Symbol.iterator]() { 232 | const it = this.iterable[Symbol.iterator](); 233 | this[Symbol.iterator] = () => { 234 | throw Error("this lazy was already consumed"); 235 | }; 236 | return it; 237 | } 238 | 239 | // the following functions only work on some specific Lazy types 240 | 241 | toMap(this: Lazy<[K, V]>): Map { 242 | return new Map(this); 243 | } 244 | 245 | join(this: Lazy) { 246 | return [...this].join(""); 247 | } 248 | 249 | sum(this: Lazy) { 250 | return this.reduce((a, b) => a + b, 0); 251 | } 252 | 253 | count() { 254 | let i = 0; 255 | for (const _ of this) i++; 256 | return i; 257 | } 258 | 259 | /** 260 | * convert a Lazy> to a Promise>, starting all the promises in parallel. 261 | * 262 | * @example 263 | * ```ts 264 | * lazy.range(0,10) 265 | * .map(x => fetch("file" + x).then(response => response.text())) 266 | * .awaitParallel() 267 | * .then(texts => texts.forEach(text => console.log(text))) 268 | * ``` 269 | * would fetch the files "file1, file2, …, file10" at the same time 270 | */ 271 | async awaitParallel(this: Lazy>): Promise> { 272 | return lazy(await Promise.all([...this])); 273 | } 274 | /** 275 | * convert a Lazy> to a Promise>, waiting for each promise to finish before starting the next one 276 | * 277 | * @example 278 | * ```ts 279 | * lazy.range(0,10) 280 | * .map(x => fetch("file" + x)) 281 | * .awaitSequential() 282 | * .then(response => console.log(response)) 283 | * ``` 284 | * would fetch the files "file1, file2, …, file10" one after another 285 | */ 286 | async awaitSequential(this: Lazy>): Promise> { 287 | const results: U[] = []; 288 | for (const promise of this) results.push(await promise); 289 | return lazy(results); 290 | } 291 | } 292 | 293 | export function lazy(iterable: Iterable): Lazy { 294 | return new Lazy(iterable); 295 | } 296 | 297 | function concat(...iterables: Iterable[]) { 298 | return lazy( 299 | (function* () { 300 | for (const it of iterables) yield* it; 301 | })(), 302 | ); 303 | } 304 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import "whatwg-fetch"; 4 | import "../node_modules/bootstrap/dist/css/bootstrap.css"; 5 | import { Config, defaultConfig } from "./config"; 6 | import { Database, DeviceInfo, NmapLog } from "./db"; 7 | import { ProgressGui, Gui, GuiContainer } from "./gui"; 8 | React; 9 | const target = document.getElementById("root"); 10 | 11 | async function fetchDeviceInfos(db: Database, data: NmapLog[]) { 12 | const stats = new Map(); 13 | for (const log of data) { 14 | for (const dev of log.devices) { 15 | let info = stats.get(dev); 16 | if (!info) 17 | stats.set( 18 | dev, 19 | (info = Object.assign(await db.getDeviceInfo(dev), { 20 | upCount: 0, 21 | })), 22 | ); 23 | info.upCount++; 24 | } 25 | } 26 | return stats; 27 | } 28 | async function run() { 29 | const userConfig = (await fetch("config.json", { 30 | credentials: "include", 31 | }).then((resp) => { 32 | if (!resp.ok) { 33 | console.warn(resp); 34 | throw Error(`${resp.status} ${resp.statusText}`); 35 | } 36 | return resp.json(); 37 | })) as Config; 38 | const config = Object.assign({}, defaultConfig, userConfig); 39 | config.deviceNames = Object.assign( 40 | {}, 41 | defaultConfig.deviceNames, 42 | userConfig.deviceNames, 43 | ); 44 | 45 | const db = new Database(config); 46 | console.time("getDates"); 47 | await db.getAllDates((days) => 48 | ReactDOM.render( 49 | , 50 | target, 51 | ), 52 | ); 53 | console.timeEnd("getDates"); 54 | console.time("getStatic"); 55 | for (const filename of config.staticLogFiles) 56 | await db.getForFile(filename, (action, done, total) => 57 | ReactDOM.render( 58 | , 64 | target, 65 | ), 66 | ); 67 | console.timeEnd("getStatic"); 68 | const data = await db.nmapLogs.toArray(); 69 | console.time("fetchDeviceInfos"); 70 | const deviceInfos = await fetchDeviceInfos(db, data); 71 | console.timeEnd("fetchDeviceInfos"); 72 | let usage = 0; 73 | try { 74 | usage = (await navigator.storage.estimate()).usage || 0; 75 | } catch (e) { 76 | console.log("could not get usage", e); 77 | } 78 | 79 | ReactDOM.render( 80 | , 86 | target, 87 | ); 88 | } 89 | run().catch((e) => { 90 | ReactDOM.render( 91 | 92 |
       93 | 				Error: {String(e)}.
      94 | See F12 console for more details. 95 |
      96 |
      , 97 | target, 98 | ); 99 | console.error("run error", e); 100 | }); 101 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from "./lazy"; 2 | import { NmapLog, MacToInfo } from "./db"; 3 | import { Config } from "./config"; 4 | import type { Preset } from "./CustomyChart"; 5 | 6 | export function levelInvert( 7 | map: Map>, 8 | defaultValue: D, 9 | ): Map> { 10 | const bs = lazy(map.values()) 11 | .flatMap((innerMap) => innerMap.keys()) 12 | .unique() 13 | .collect(); 14 | const as = [...map.keys()]; 15 | const innerMap = (b: B) => 16 | lazy(as).toMapKeyed( 17 | (a) => a, 18 | (a) => map.get(a)!.get(b) || defaultValue, 19 | ); 20 | return new Map>(lazy(bs).toMapKeyed((b) => b, innerMap)); 21 | } 22 | 23 | export function roundDate( 24 | date: Date, 25 | weekday: number, 26 | hours: number, 27 | minutes = 60, 28 | ): Date { 29 | date.setDate(date.getDate() - (date.getDay() % weekday)); 30 | date.setHours(date.getHours() - (date.getHours() % hours)); 31 | date.setMinutes(date.getMinutes() - (date.getMinutes() % minutes), 0, 0); 32 | return date; 33 | } 34 | 35 | export function setHighchartsOptionsForPreset( 36 | preset: Preset, 37 | o: Highcharts.Options, 38 | ): void { 39 | if (!o.tooltip) o.tooltip = {}; 40 | o.tooltip.headerFormat = preset.headerFormat; 41 | if (!o.xAxis) o.xAxis = {}; 42 | if (Array.isArray(o.xAxis)) throw Error("can't handle multiple x"); 43 | o.xAxis.labels = preset.xAxisLabels; 44 | } 45 | 46 | export type DateRounder = (d: Date) => Date | null; 47 | function assumeNonNull(t: T | null | undefined, varname = "var"): T { 48 | if (t == null || t === undefined) 49 | throw Error(`${varname} can't be ${String(t)}`); 50 | return t; 51 | } 52 | interface parseXMLReturn { 53 | online: NmapLog; 54 | newInfos: MacToInfo[]; 55 | } 56 | function hasChildren(obj: T): obj is T & { children: HTMLCollection } { 57 | return !!(obj as { children?: HTMLCollection }).children; 58 | } 59 | 60 | export function uptimePart(t: number, meUptime: number | undefined): number { 61 | if (!meUptime) { 62 | console.warn("could not get own uptime"); 63 | // don't know how long we were up or meUptime = 0 64 | return t; 65 | } else return t / meUptime; 66 | } 67 | 68 | export function parseXML( 69 | config: Config, 70 | filename: string, 71 | xml: string, 72 | ): parseXMLReturn | null { 73 | /*const sax1 = await import('sax-wasm'); 74 | const sax = await import('sax-wasm/lib/sax-wasm.wasm'); 75 | console.log({sax1, sax});*/ 76 | const parser = new DOMParser(); 77 | const doc = parser.parseFromString(xml, "application/xml").documentElement; 78 | const time = new Date(1000 * +doc.getAttribute("start")!); 79 | if (time < new Date(2000, 0)) 80 | // RTC has not been set, ignore scan 81 | return null; 82 | const scan: parseXMLReturn = { 83 | online: { time: time.getTime(), devices: new Set() }, 84 | newInfos: [], 85 | }; 86 | for (const h of Array.from(doc.children)) { 87 | if (h.nodeName === "parsererror") { 88 | console.warn(`parsing error in ${filename}`); 89 | return null; 90 | } 91 | if (h.nodeName !== "host") { 92 | if ( 93 | ["verbose", "debugging", "runstats", "target"].indexOf( 94 | h.nodeName, 95 | ) >= 0 96 | ) 97 | continue; 98 | else throw Error("unexpected " + h.nodeName); 99 | } 100 | 101 | let mac = ""; 102 | if (!hasChildren(h)) throw Error("no children"); 103 | const ips: string[] = [], 104 | hostnames: string[] = []; 105 | for (const a of Array.from(h.children)) { 106 | if (a.nodeName === "status") { 107 | if (a.getAttribute("reason") === "localhost-response") 108 | mac = config.selfMacAddress; 109 | } 110 | if (a.nodeName === "address") { 111 | const type = a.getAttribute("addrtype"); 112 | if (type === "mac") { 113 | mac = assumeNonNull(a.getAttribute("addr"), "mac"); 114 | scan.newInfos.push({ 115 | mac, 116 | type: "vendor", 117 | info: a.getAttribute("vendor") || "", 118 | }); 119 | } else if (type === "ipv4") { 120 | ips.push(assumeNonNull(a.getAttribute("addr"), "ipv4")); 121 | } 122 | } else if (a.nodeName === "hostnames") { 123 | if (!hasChildren(a)) throw Error("no children"); 124 | for (const child of Array.from(a.children)) 125 | if (child.tagName === "hostname") 126 | hostnames.push( 127 | assumeNonNull( 128 | child.getAttribute("name"), 129 | "hostname", 130 | ), 131 | ); 132 | } 133 | } 134 | if (!mac) { 135 | console.warn("no mac address found", doc, h); 136 | continue; 137 | } 138 | scan.online.devices.add(mac); 139 | scan.newInfos.push( 140 | ...ips.map((ip) => ({ mac, type: "ip", info: ip } as MacToInfo)), 141 | ); 142 | scan.newInfos.push( 143 | ...hostnames.map( 144 | (hostname) => 145 | ({ mac, type: "hostname", info: hostname } as MacToInfo), 146 | ), 147 | ); 148 | } 149 | return scan; 150 | } 151 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "noImplicitAny": true, 8 | "noEmit": true, 9 | "isolatedModules": true, 10 | "sourceMap": true, 11 | "jsx": "react", 12 | "strict": true, 13 | "noImplicitThis": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "outDir": "bin", 17 | "allowSyntheticDefaultImports": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------