diff --git a/.gitignore b/.gitignore index 0518e9f..355b4d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -node_modules/ -deploy-config.json -.env +node_modules/ +deploy-config.json +.env diff --git a/LICENSE b/LICENSE index f288702..3877ae0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,674 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/PROXMOX_DEPLOY_TEMPLATE.md b/PROXMOX_DEPLOY_TEMPLATE.md index 5d21676..fde4dfb 100644 --- a/PROXMOX_DEPLOY_TEMPLATE.md +++ b/PROXMOX_DEPLOY_TEMPLATE.md @@ -1,211 +1,211 @@ -# 🚀 Proxmox Deployment Template (TurnKey Node.js) - -**Use this guide to deploy ANY Node.js application to a TurnKey Linux LXC Container.** - ---- - -## 📋 Prerequisites - -1. **Project**: A Node.js application (Express, Next.js, etc.) in a Git repository. -2. **Server**: A Proxmox TurnKey Node.js Container. -3. **Access**: Root SSH password for the container. -4. **Domain (Optional)**: If using Cloudflare Tunnel. - ---- - -## 🛠️ Step 1: Prepare Your Project - -Ensure your project is ready for production: - -1. **Port Configuration**: Ensure your app listens on a configurable port or a fixed internal port (e.g., `4001`). - ```javascript - // server.js - const PORT = process.env.PORT || 4001; - app.listen(PORT, ...); - ``` - -2. **Git Ignore**: Ensure `node_modules` and config files with secrets are ignored. - ```gitignore - node_modules/ - .env - config.json - ``` - ---- - -## 🖥️ Step 2: One-Time Server Setup - -SSH into your new container: -```bash -ssh root@ -``` - -Run these commands to prepare the environment: - -### 1. Install Essentials -```bash -apt-get update && apt-get install -y git -``` - -### 2. Prepare Directory -```bash -# Standard web directory -mkdir -p /var/www/ -cd /var/www/ - -# Clone your repo (Use Basic Auth with Token if private) -# Format: https://:@github.com//.git -git clone . - -# Install dependencies -npm install -``` - -### 3. Setup Permissions -```bash -# Give ownership to www-data (Nginx user) -chown -R www-data:www-data /var/www/ -``` - ---- - -## ⚙️ Step 3: Application Configuration - -### 1. Systemd Service -Create a service file to keep your app running. - -Create `/etc/systemd/system/.service`: -```ini -[Unit] -Description= Service -After=network.target - -[Service] -Type=simple -User=root -# OR use 'www-data' if app doesn't need root ports -# User=www-data -WorkingDirectory=/var/www/ -ExecStart=/usr/local/bin/node server.js -Restart=always -Environment=NODE_ENV=production -Environment=PORT=4001 - -[Install] -WantedBy=multi-user.target -``` - -Enable and start: -```bash -systemctl daemon-reload -systemctl enable -systemctl start -``` - -### 2. Nginx Reverse Proxy -Configure Nginx to forward port 80 to your app (Port 4001). - -Create `/etc/nginx/sites-available/`: -```nginx -server { - listen 80; - server_name _; - - root /var/www/; - index index.html; - - # Serve static files (Optional) - location / { - try_files $uri $uri/ =404; - } - - # Proxy API/Dynamic requests - location /api { - proxy_pass http://localhost:4001; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } -} -``` - -Enable site: -```bash -# Remove defaults -rm -f /etc/nginx/sites-enabled/default -rm -f /etc/nginx/sites-enabled/nodejs - -# Link new site -ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ - -# Reload -nginx -t && systemctl reload nginx -``` - ---- - -## ☁️ Step 4: Cloudflare Tunnel (Secure Access) - -Expose your app securely without opening router ports. - -### 1. Install Cloudflared -```bash -# Add Key -mkdir -p --mode=0755 /usr/share/keyrings -curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null - -# Add Repo -echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list - -# Install -apt-get update && apt-get install -y cloudflared -``` - -### 2. Create Tunnel -```bash -cloudflared tunnel login -cloudflared tunnel create -# Follow on-screen instructions to map domain -> http://localhost:4001 -``` - ---- - -## 🔄 Step 5: Automated Updates (PowerShell) - -Create a script `deploy-remote.ps1` in your project root to automate updates. - -**Pre-requisite**: Create `deploy-config.json` (Add to .gitignore!): -```json -{ - "host": "", - "username": "root", - "password": "", - "remotePath": "/var/www/" -} -``` - -**Script `deploy-remote.ps1`**: -```powershell -# Reads config and updates remote server -$Config = Get-Content "deploy-config.json" | ConvertFrom-Json -$User = $Config.username; $HostName = $Config.host; $Pass = $Config.password -$RemotePath = $Config.remotePath - -# Commands to run remotely -$Cmds = " - cd $RemotePath - echo '⬇️ Pulling code...' - git pull - echo '📦 Installing deps...' - npm install - echo '🚀 Restarting service...' - systemctl restart - systemctl status --no-pager -" - -echo y | plink -ssh -t -pw $Pass "$User@$HostName" $Cmds -``` - -**Usage**: Just run `./deploy-remote.ps1` to deploy! +# 🚀 Proxmox Deployment Template (TurnKey Node.js) + +**Use this guide to deploy ANY Node.js application to a TurnKey Linux LXC Container.** + +--- + +## 📋 Prerequisites + +1. **Project**: A Node.js application (Express, Next.js, etc.) in a Git repository. +2. **Server**: A Proxmox TurnKey Node.js Container. +3. **Access**: Root SSH password for the container. +4. **Domain (Optional)**: If using Cloudflare Tunnel. + +--- + +## 🛠️ Step 1: Prepare Your Project + +Ensure your project is ready for production: + +1. **Port Configuration**: Ensure your app listens on a configurable port or a fixed internal port (e.g., `4001`). + ```javascript + // server.js + const PORT = process.env.PORT || 4001; + app.listen(PORT, ...); + ``` + +2. **Git Ignore**: Ensure `node_modules` and config files with secrets are ignored. + ```gitignore + node_modules/ + .env + config.json + ``` + +--- + +## 🖥️ Step 2: One-Time Server Setup + +SSH into your new container: +```bash +ssh root@ +``` + +Run these commands to prepare the environment: + +### 1. Install Essentials +```bash +apt-get update && apt-get install -y git +``` + +### 2. Prepare Directory +```bash +# Standard web directory +mkdir -p /var/www/ +cd /var/www/ + +# Clone your repo (Use Basic Auth with Token if private) +# Format: https://:@github.com//.git +git clone . + +# Install dependencies +npm install +``` + +### 3. Setup Permissions +```bash +# Give ownership to www-data (Nginx user) +chown -R www-data:www-data /var/www/ +``` + +--- + +## ⚙️ Step 3: Application Configuration + +### 1. Systemd Service +Create a service file to keep your app running. + +Create `/etc/systemd/system/.service`: +```ini +[Unit] +Description= Service +After=network.target + +[Service] +Type=simple +User=root +# OR use 'www-data' if app doesn't need root ports +# User=www-data +WorkingDirectory=/var/www/ +ExecStart=/usr/local/bin/node server.js +Restart=always +Environment=NODE_ENV=production +Environment=PORT=4001 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: +```bash +systemctl daemon-reload +systemctl enable +systemctl start +``` + +### 2. Nginx Reverse Proxy +Configure Nginx to forward port 80 to your app (Port 4001). + +Create `/etc/nginx/sites-available/`: +```nginx +server { + listen 80; + server_name _; + + root /var/www/; + index index.html; + + # Serve static files (Optional) + location / { + try_files $uri $uri/ =404; + } + + # Proxy API/Dynamic requests + location /api { + proxy_pass http://localhost:4001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +Enable site: +```bash +# Remove defaults +rm -f /etc/nginx/sites-enabled/default +rm -f /etc/nginx/sites-enabled/nodejs + +# Link new site +ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ + +# Reload +nginx -t && systemctl reload nginx +``` + +--- + +## ☁️ Step 4: Cloudflare Tunnel (Secure Access) + +Expose your app securely without opening router ports. + +### 1. Install Cloudflared +```bash +# Add Key +mkdir -p --mode=0755 /usr/share/keyrings +curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null + +# Add Repo +echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list + +# Install +apt-get update && apt-get install -y cloudflared +``` + +### 2. Create Tunnel +```bash +cloudflared tunnel login +cloudflared tunnel create +# Follow on-screen instructions to map domain -> http://localhost:4001 +``` + +--- + +## 🔄 Step 5: Automated Updates (PowerShell) + +Create a script `deploy-remote.ps1` in your project root to automate updates. + +**Pre-requisite**: Create `deploy-config.json` (Add to .gitignore!): +```json +{ + "host": "", + "username": "root", + "password": "", + "remotePath": "/var/www/" +} +``` + +**Script `deploy-remote.ps1`**: +```powershell +# Reads config and updates remote server +$Config = Get-Content "deploy-config.json" | ConvertFrom-Json +$User = $Config.username; $HostName = $Config.host; $Pass = $Config.password +$RemotePath = $Config.remotePath + +# Commands to run remotely +$Cmds = " + cd $RemotePath + echo '⬇️ Pulling code...' + git pull + echo '📦 Installing deps...' + npm install + echo '🚀 Restarting service...' + systemctl restart + systemctl status --no-pager +" + +echo y | plink -ssh -t -pw $Pass "$User@$HostName" $Cmds +``` + +**Usage**: Just run `./deploy-remote.ps1` to deploy! diff --git a/README.md b/README.md index a126859..1d8224d 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,125 @@ -# Beyond Cloud Technology - Website Stress Test - -![Website Stress Test Dashboard](screenshots/hero.png) - -## 🌐 Public Access -**Live URL:** [https://website-stress-test.beyondcloud.technology/](https://website-stress-test.beyondcloud.technology/) - ---- - -## 🚀 Overview -The **Website Stress Test** is a professional-grade load testing tool designed to simulate realistic traffic patterns on your web applications. It helps developers and QA engineers identify bottlenecks, test scalability, and ensure production readiness. - -Built with a **modern, high-performance architecture**, it includes a custom NodeJS proxy server to bypass CORS restrictions and allow testing of any target URL. - -## ✨ Key Features - -### 🛠️ Core Functionality -* **Custom HTTP Methods**: Support for GET, POST, PUT, DELETE, and PATCH. -* **Configurable Load**: Adjust concurrent users (up to 5000) and test duration. -* **Traffic Patterns**: - * **Steady**: Constant load. - * **Burst**: Sudden spikes to test resilience. - * **Ramp-up**: Gradual increase to find breaking points. - * **Random**: Simulate unpredictable real-world traffic. -* **Crawler Mode**: Automatically crawls the target website to test multiple pages and paths, not just the entry point. - -### 📊 Real-Time Analytics -* **Interactive Charts**: Live visualization of Requests Per Second (RPS) and Response Times. -* **Detailed Metrics**: Track Active Users, Bandwidth, Success Rates, and Error breakdown (4xx, 5xx, Timeouts). -* **Percentiles**: Monitor P50, P95, and P99 latency metrics. - -### 🎨 User Experience -* **Modern UI**: Sleek, glassmorphism-inspired design with Light/Dark mode support. -* **Git Versioning**: Automatic display of the current Git Commit ID and deployment age in the UI. -* **Responsive Design**: Fully functional on desktop and tablet devices. - ---- - -## 📦 Installation & Setup - -### Prerequisites -* Node.js (v18 or higher) -* Nginx (for production deployment) -* PM2 (for process management) - -### 💻 Local Development -1. **Clone the Repository** - ```bash - git clone https://github.com/DeNNiiInc/Website-Stress-Test.git - cd Website-Stress-Test - ``` - -2. **Install Dependencies** - ```bash - npm install - ``` - -3. **Start the Proxy Server** - ```bash - npm start - ``` - The server will start on `http://localhost:3000`. - -4. **Open the Application** - Open `index.html` in your browser or serve it using a static file server (e.g., Live Server). - ---- - -## 🚀 Deployment Guide (Proxmox / Ubuntu) - -This project includes automated deployment scripts for Proxmox/Ubuntu environments. - -### 1. Configuration -Copy `deploy-config.example.json` to `deploy-config.json` and update with your server details: -```json -{ - "host": "YOUR_SERVER_IP", - "username": "root", - "password": "YOUR_PASSWORD", - "remotePath": "/var/www/website-stress-test", - "repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git", - "githubToken": "YOUR_GITHUB_TOKEN", - "appName": "website-stress-test" -} -``` - -### 2. Auto-Deployment -Run the PowerShell deployment script: -```powershell -./start-deployment.ps1 -``` -This script will: -* Connect to your server via SSH. -* Install Nginx and Node.js if missing. -* Clone/Pull the latest code. -* Configure Nginx as a reverse proxy. -* Set up a Cron job for auto-updates. - -### 3. Auto-Sync -The system automatically checks for Git updates every 5 minutes. If changes are detected, it pulls the code, installs dependencies, and restarts the backend process without downtime. - -**Manual Update Trigger:** -```bash -/var/www/website-stress-test/auto-sync.sh -``` - ---- - -## 🔧 Architecture - -### Backend (`proxy-server.js`) -* **Role**: Handles CORS requests and authenticates traffic. -* **Port**: 3000 (Internal). -* **Endpoints**: - * `/proxy`: Forwards stress test requests. - * `/git-info`: Returns current commit hash and deployment date. - -### Frontend (`index.html` + `script.js`) -* **Technology**: Vanilla JS + Chart.js. -* **Communication**: Fetch API to the Proxy Server. - ---- - -## 📝 License -MIT License - Copyright (c) 2025 Beyond Cloud Technology. +# Beyond Cloud Technology - Website Stress Test + +![Website Stress Test Dashboard](screenshots/hero.png) + +## 🌐 Public Access +**Live URL:** [https://website-stress-test.beyondcloud.technology/](https://website-stress-test.beyondcloud.technology/) + +--- + +## 🚀 Overview +The **Website Stress Test** is a professional-grade load testing tool designed to simulate realistic traffic patterns on your web applications. It helps developers and QA engineers identify bottlenecks, test scalability, and ensure production readiness. + +Built with a **modern, high-performance architecture**, it includes a custom NodeJS proxy server to bypass CORS restrictions and allow testing of any target URL. + +## ✨ Key Features + +### 🛠️ Core Functionality +* **Custom HTTP Methods**: Support for GET, POST, PUT, DELETE, and PATCH. +* **Configurable Load**: Adjust concurrent users (up to 5000) and test duration. +* **Traffic Patterns**: + * **Steady**: Constant load. + * **Burst**: Sudden spikes to test resilience. + * **Ramp-up**: Gradual increase to find breaking points. + * **Random**: Simulate unpredictable real-world traffic. +* **Crawler Mode**: Automatically crawls the target website to test multiple pages and paths, not just the entry point. + +### 📊 Real-Time Analytics +* **Interactive Charts**: Live visualization of Requests Per Second (RPS) and Response Times. +* **Detailed Metrics**: Track Active Users, Bandwidth, Success Rates, and Error breakdown (4xx, 5xx, Timeouts). +* **Percentiles**: Monitor P50, P95, and P99 latency metrics. + +### 🎨 User Experience +* **Modern UI**: Sleek, glassmorphism-inspired design with Light/Dark mode support. +* **Git Versioning**: Automatic display of the current Git Commit ID and deployment age in the UI. +* **Responsive Design**: Fully functional on desktop and tablet devices. + +--- + +## 📦 Installation & Setup + +### Prerequisites +* Node.js (v18 or higher) +* Nginx (for production deployment) +* PM2 (for process management) + +### 💻 Local Development +1. **Clone the Repository** + ```bash + git clone https://github.com/DeNNiiInc/Website-Stress-Test.git + cd Website-Stress-Test + ``` + +2. **Install Dependencies** + ```bash + npm install + ``` + +3. **Start the Proxy Server** + ```bash + npm start + ``` + The server will start on `http://localhost:3000`. + +4. **Open the Application** + Open `index.html` in your browser or serve it using a static file server (e.g., Live Server). + +--- + +## 🚀 Deployment Guide (Proxmox / Ubuntu) + +This project includes automated deployment scripts for Proxmox/Ubuntu environments. + +### 1. Configuration +Copy `deploy-config.example.json` to `deploy-config.json` and update with your server details: +```json +{ + "host": "YOUR_SERVER_IP", + "username": "root", + "password": "YOUR_PASSWORD", + "remotePath": "/var/www/website-stress-test", + "repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git", + "githubToken": "YOUR_GITHUB_TOKEN", + "appName": "website-stress-test" +} +``` + +### 2. Auto-Deployment +Run the PowerShell deployment script: +```powershell +./start-deployment.ps1 +``` +This script will: +* Connect to your server via SSH. +* Install Nginx and Node.js if missing. +* Clone/Pull the latest code. +* Configure Nginx as a reverse proxy. +* Set up a Cron job for auto-updates. + +### 3. Auto-Sync +The system automatically checks for Git updates every 5 minutes. If changes are detected, it pulls the code, installs dependencies, and restarts the backend process without downtime. + +**Manual Update Trigger:** +```bash +/var/www/website-stress-test/auto-sync.sh +``` + +--- + +## 🔧 Architecture + +### Backend (`proxy-server.js`) +* **Role**: Handles CORS requests and authenticates traffic. +* **Port**: 3000 (Internal). +* **Endpoints**: + * `/proxy`: Forwards stress test requests. + * `/git-info`: Returns current commit hash and deployment date. + +### Frontend (`index.html` + `script.js`) +* **Technology**: Vanilla JS + Chart.js. +* **Communication**: Fetch API to the Proxy Server. + +--- + +## 📝 License +MIT License - Copyright (c) 2025 Beyond Cloud Technology. diff --git a/deploy-config.example.json b/deploy-config.example.json index a900523..f92b3df 100644 --- a/deploy-config.example.json +++ b/deploy-config.example.json @@ -1,9 +1,9 @@ -{ - "host": "YOUR_SERVER_IP", - "username": "root", - "password": "YOUR_SSH_PASSWORD", - "remotePath": "/var/www/website-stress-test", - "repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git", - "githubToken": "YOUR_GITHUB_TOKEN", - "appName": "website-stress-test" -} +{ + "host": "YOUR_SERVER_IP", + "username": "root", + "password": "YOUR_SSH_PASSWORD", + "remotePath": "/var/www/website-stress-test", + "repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git", + "githubToken": "YOUR_GITHUB_TOKEN", + "appName": "website-stress-test" +} diff --git a/index.html b/index.html index c24d5ed..03aef6e 100644 --- a/index.html +++ b/index.html @@ -1,512 +1,512 @@ - - - - - - - Beyond Cloud Technology - Website Stress Test - - - - - - -
- -
-
-

- BCT Logo - Beyond Cloud Technology - Website Stress Test -

-

- Simulate realistic traffic patterns to test your production websites -

- - - - - Watch on YouTube @beyondcloudtechnology - -
-
- -
-
- - -
-
-
-

Quick Start

-
-
- - -
-
- Keyboard Shortcuts: S = Start | P = Pause/Resume | - X = Stop -
-
- - -
- -
-
-
⚙️
-

Configuration

-
- -
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - Randomly navigate through website links like real users -
- - - - - -
- -
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - - Delay between requests per user -
-
-
-
-
- - -
-
-
🎮
-

Control Panel

-
- - -
- Idle -
- - -
-
-
- - -
- - - -
- - -
-
-
Elapsed Time
-
0s
-
-
-
Remaining
-
0s
-
-
-
-
- - -
-
-
📊
-

Live Statistics

-
- -
-
-
Active Users
-
0
-
-
-
Total Requests
-
0
-
-
-
Requests/Sec
-
0
-
-
-
Success Rate
-
0%
-
-
-
Failed Requests
-
0
-
-
-
Avg Response Time
-
0ms
-
-
- - -
-

Response Time Percentiles

-
-
-
P50 (Median)
-
0ms
-
-
-
P95
-
0ms
-
-
-
P99
-
0ms
-
-
-
- - -
-

Error Breakdown

-
-
-
4xx Errors
-
0
-
-
-
5xx Errors
-
0
-
-
-
Timeout Errors
-
0
-
-
-
Network Errors
-
0
-
-
-
- - -
-

Bandwidth Usage

-
-
Total Bandwidth
-
0 B
-
-
- - -
-
- -
-
- -
-
- - -
- -
-
- - -
-
-
📝
-

Request History (Last 100)

-
- -
- - - - - - - - - - - - -
TimeURLStatusResponse Time
-
-
- - - -
- -
-

© 2025 Beyond Cloud Technology. All rights reserved.

-
- - - - - - - - + + + + + + + Beyond Cloud Technology - Website Stress Test + + + + + + +
+ +
+
+

+ BCT Logo + Beyond Cloud Technology - Website Stress Test +

+

+ Simulate realistic traffic patterns to test your production websites +

+ + + + + Watch on YouTube @beyondcloudtechnology + +
+
+ +
+
+ + +
+
+
+

Quick Start

+
+
+ + +
+
+ Keyboard Shortcuts: S = Start | P = Pause/Resume | + X = Stop +
+
+ + +
+ +
+
+
⚙️
+

Configuration

+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + Randomly navigate through website links like real users +
+ + + + + +
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + Delay between requests per user +
+
+
+
+
+ + +
+
+
🎮
+

Control Panel

+
+ + +
+ Idle +
+ + +
+
+
+ + +
+ + + +
+ + +
+
+
Elapsed Time
+
0s
+
+
+
Remaining
+
0s
+
+
+
+
+ + +
+
+
📊
+

Live Statistics

+
+ +
+
+
Active Users
+
0
+
+
+
Total Requests
+
0
+
+
+
Requests/Sec
+
0
+
+
+
Success Rate
+
0%
+
+
+
Failed Requests
+
0
+
+
+
Avg Response Time
+
0ms
+
+
+ + +
+

Response Time Percentiles

+
+
+
P50 (Median)
+
0ms
+
+
+
P95
+
0ms
+
+
+
P99
+
0ms
+
+
+
+ + +
+

Error Breakdown

+
+
+
4xx Errors
+
0
+
+
+
5xx Errors
+
0
+
+
+
Timeout Errors
+
0
+
+
+
Network Errors
+
0
+
+
+
+ + +
+

Bandwidth Usage

+
+
Total Bandwidth
+
0 B
+
+
+ + +
+
+ +
+
+ +
+
+ + +
+ +
+
+ + +
+
+
📝
+

Request History (Last 100)

+
+ +
+ + + + + + + + + + + + +
TimeURLStatusResponse Time
+
+
+ + + +
+ +
+

© 2025 Beyond Cloud Technology. All rights reserved.

+
+ + + + + + + + diff --git a/package.json b/package.json index 7002a11..e5ca53b 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ -{ - "name": "stress-testing-tool", - "version": "1.0.0", - "description": "Website stress testing tool with CORS proxy", - "main": "proxy-server.js", - "scripts": { - "start": "node proxy-server.js", - "proxy": "node proxy-server.js" - }, - "keywords": [ - "stress-testing", - "load-testing", - "cors-proxy" - ], - "author": "", - "license": "MIT" -} +{ + "name": "stress-testing-tool", + "version": "1.0.0", + "description": "Website stress testing tool with CORS proxy", + "main": "proxy-server.js", + "scripts": { + "start": "node proxy-server.js", + "proxy": "node proxy-server.js" + }, + "keywords": [ + "stress-testing", + "load-testing", + "cors-proxy" + ], + "author": "", + "license": "MIT" +} diff --git a/proxy-server.js b/proxy-server.js index a2b89c6..dddd3a2 100644 --- a/proxy-server.js +++ b/proxy-server.js @@ -1,273 +1,291 @@ -// =================================== -// CORS PROXY SERVER -// =================================== -// This proxy server allows the stress testing tool to test -// production websites without CORS restrictions. - -const http = require('http'); -const https = require('https'); -const url = require('url'); - -const PORT = 3000; - -// Configuration -const CONFIG = { - // Maximum request timeout (30 seconds) - timeout: 30000, - - // Allowed origins (restrict to your stress testing tool's domain) - // Use '*' for development, specific domain for production - allowedOrigins: '*', - - // Maximum concurrent connections - maxConnections: 5000, - - // User agents for rotation - userAgents: [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - ] -}; - -// Get random user agent -function getRandomUserAgent() { - return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)]; -} - -const { exec } = require('child_process'); - -// Helper to get git info -const getGitInfo = () => { - return new Promise((resolve) => { - exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => { - if (err) { - console.error('Error fetching git info:', err); - resolve({ commit: 'Unknown', date: 'Unknown' }); - return; - } - const parts = stdout.trim().split('\n'); - resolve({ - commit: parts[0] || 'Unknown', - date: parts[1] || 'Unknown' - }); - }); - }); -}; - -// Create the proxy server -const server = http.createServer((req, res) => { - // Handle CORS preflight requests - if (req.method === 'OPTIONS') { - handleCORS(res); - res.writeHead(200); - res.end(); - return; - } - - // Handle Git Info request - // Nginx proxy_pass might result in double slashes (//git-info) - if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') { - handleCORS(res); - getGitInfo().then(info => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(info)); - }); - return; - } - - // Only allow POST requests to the proxy - if (req.method !== 'POST') { - res.writeHead(405, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' })); - return; - } - - // Parse request body - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - - req.on('end', () => { - try { - const proxyRequest = JSON.parse(body); - handleProxyRequest(proxyRequest, res); - } catch (error) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - error: 'Invalid JSON', - message: error.message - })); - } - }); -}); - -// Handle the actual proxy request -function handleProxyRequest(proxyRequest, clientRes) { - const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest; - - // Validate target URL - if (!targetUrl) { - clientRes.writeHead(400, { 'Content-Type': 'application/json' }); - clientRes.end(JSON.stringify({ error: 'targetUrl is required' })); - return; - } - - let parsedUrl; - try { - parsedUrl = new URL(targetUrl); - } catch (error) { - clientRes.writeHead(400, { 'Content-Type': 'application/json' }); - clientRes.end(JSON.stringify({ error: 'Invalid URL' })); - return; - } - - // Determine if we need http or https - const protocol = parsedUrl.protocol === 'https:' ? https : http; - - // Prepare request options with random user agent - const options = { - hostname: parsedUrl.hostname, - port: parsedUrl.port, - path: parsedUrl.pathname + parsedUrl.search, - method: method, - headers: { - ...headers, - 'User-Agent': getRandomUserAgent(), - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate, br', - 'DNT': '1', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' - }, - timeout: CONFIG.timeout - }; - - const startTime = Date.now(); - - // Make the request to the target server - const proxyReq = protocol.request(options, (proxyRes) => { - const responseTime = Date.now() - startTime; - - // Collect response data - let responseData = ''; - let responseSize = 0; - const maxBodySize = 500000; // 500KB limit for crawler - - proxyRes.on('data', chunk => { - responseSize += chunk.length; - // Only collect body if under size limit (for crawler) - if (responseSize < maxBodySize) { - responseData += chunk.toString(); - } - }); - - proxyRes.on('end', () => { - // Send response back to client with CORS headers - handleCORS(clientRes); - clientRes.writeHead(200, { 'Content-Type': 'application/json' }); - - clientRes.end(JSON.stringify({ - success: true, - statusCode: proxyRes.statusCode, - statusMessage: proxyRes.statusMessage, - responseTime: responseTime, - headers: proxyRes.headers, - body: responseData, // Full body for crawler link extraction - bodySize: responseSize - })); - }); - }); - - // Handle request errors - proxyReq.on('error', (error) => { - const responseTime = Date.now() - startTime; - - handleCORS(clientRes); - clientRes.writeHead(200, { 'Content-Type': 'application/json' }); - - clientRes.end(JSON.stringify({ - success: false, - error: error.message, - responseTime: responseTime, - statusCode: 0 - })); - }); - - // Handle timeout - proxyReq.on('timeout', () => { - proxyReq.destroy(); - const responseTime = Date.now() - startTime; - - handleCORS(clientRes); - clientRes.writeHead(200, { 'Content-Type': 'application/json' }); - - clientRes.end(JSON.stringify({ - success: false, - error: 'Request timeout', - responseTime: responseTime, - statusCode: 0 - })); - }); - - // Send request body if present - if (body && method !== 'GET' && method !== 'HEAD') { - proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body)); - } - - proxyReq.end(); -} - -// Add CORS headers to response -function handleCORS(res) { - res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins); - res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); -} - -// Start the server -server.listen(PORT, () => { - console.log(` -╔════════════════════════════════════════════════════════════╗ -║ CORS Proxy Server for Stress Testing Tool ║ -╚════════════════════════════════════════════════════════════╝ - -✅ Server running on: http://localhost:${PORT} -✅ Max connections: ${CONFIG.maxConnections} -✅ Request timeout: ${CONFIG.timeout}ms - -📝 Usage: - POST to http://localhost:${PORT} with JSON body: - { - "targetUrl": "https://example.com", - "method": "GET", - "headers": {}, - "body": null - } - -🔒 Security Note: - For production, update CONFIG.allowedOrigins to your - stress testing tool's domain (not '*') - -Press Ctrl+C to stop the server - `); -}); - -// Handle server errors -server.on('error', (error) => { - console.error('❌ Server error:', error.message); - process.exit(1); -}); - -// Graceful shutdown -process.on('SIGINT', () => { - console.log('\n\n🛑 Shutting down proxy server...'); - server.close(() => { - console.log('✅ Server closed'); - process.exit(0); - }); -}); +// =================================== +// CORS PROXY SERVER +// =================================== +// This proxy server allows the stress testing tool to test +// production websites without CORS restrictions. + +const http = require('http'); +const https = require('https'); +const url = require('url'); +const cluster = require('cluster'); +const numCPUs = require('os').cpus().length; + +const PORT = process.env.PORT || 3000; + +// Configuration +const CONFIG = { + // Maximum request timeout (30 seconds) + timeout: 30000, + + // Allowed origins (restrict to your stress testing tool's domain) + // Use '*' for development, specific domain for production + allowedOrigins: '*', + + // Maximum concurrent connections + maxConnections: 10000, // Increased for cluster + + // User agents for rotation + userAgents: [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + ] +}; + +// Global agents for connection pooling +const httpAgent = new http.Agent({ keepAlive: true, maxSockets: Infinity }); +const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: Infinity }); + +// Get random user agent +function getRandomUserAgent() { + return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)]; +} + +const { exec } = require('child_process'); + +// Helper to get git info +const getGitInfo = () => { + return new Promise((resolve) => { + exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => { + if (err) { + resolve({ commit: 'Unknown', date: 'Unknown' }); + return; + } + const parts = stdout.trim().split('\n'); + resolve({ + commit: parts[0] || 'Unknown', + date: parts[1] || 'Unknown' + }); + }); + }); +}; + +if (cluster.isMaster) { + console.log(`Master ${process.pid} is running`); + console.log(`Spawning ${numCPUs} workers...`); + + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('exit', (worker, code, signal) => { + console.log(`Worker ${worker.process.pid} died. Respawning...`); + cluster.fork(); + }); + + // Master process only listens for SIGINT to gracefully shut down workers + process.on('SIGINT', () => { + console.log('\n\n🛑 Shutting down proxy server (master)...'); + for (const id in cluster.workers) { + cluster.workers[id].kill(); + } + process.exit(0); + }); + +} else { + // Create the proxy server + const server = http.createServer((req, res) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + handleCORS(res); + res.writeHead(200); + res.end(); + return; + } + + // Health check + if (req.url === '/health' || req.url === '//health') { + handleCORS(res); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok', worker: process.pid })); + return; + } + + // Handle Git Info request + // Nginx proxy_pass might result in double slashes (//git-info) + if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') { + handleCORS(res); + getGitInfo().then(info => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(info)); + }); + return; + } + + // Only allow POST requests to the proxy + if (req.method !== 'POST') { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' })); + return; + } + + // Parse request body + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + + req.on('end', () => { + try { + const proxyRequest = JSON.parse(body); + handleProxyRequest(proxyRequest, res); + } catch (error) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + error: 'Invalid JSON', + message: error.message + })); + } + }); + }); + + // Handle the actual proxy request + function handleProxyRequest(proxyRequest, clientRes) { + const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest; + + // Validate target URL + if (!targetUrl) { + clientRes.writeHead(400, { 'Content-Type': 'application/json' }); + clientRes.end(JSON.stringify({ error: 'targetUrl is required' })); + return; + } + + let parsedUrl; + try { + parsedUrl = new URL(targetUrl); + } catch (error) { + clientRes.writeHead(400, { 'Content-Type': 'application/json' }); + clientRes.end(JSON.stringify({ error: 'Invalid URL' })); + return; + } + + // Determine if we need http or https and which agent to use + const isHttps = parsedUrl.protocol === 'https:'; + const protocol = isHttps ? https : http; + const agent = isHttps ? httpsAgent : httpAgent; + + // Prepare request options with random user agent + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname + parsedUrl.search, + method: method, + agent: agent, // Use the global agent for connection pooling + headers: { + ...headers, + 'User-Agent': getRandomUserAgent(), + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1' + }, + timeout: CONFIG.timeout + }; + + const startTime = Date.now(); + + // Make the request to the target server + const proxyReq = protocol.request(options, (proxyRes) => { + const responseTime = Date.now() - startTime; + + // Collect response data + let responseData = ''; + let responseSize = 0; + const maxBodySize = 500000; // 500KB limit for crawler + + proxyRes.on('data', chunk => { + responseSize += chunk.length; + // Only collect body if under size limit (for crawler) + if (responseSize < maxBodySize) { + responseData += chunk.toString(); + } + }); + + proxyRes.on('end', () => { + // Send response back to client with CORS headers + handleCORS(clientRes); + clientRes.writeHead(200, { 'Content-Type': 'application/json' }); + + clientRes.end(JSON.stringify({ + success: true, + statusCode: proxyRes.statusCode, + statusMessage: proxyRes.statusMessage, + responseTime: responseTime, + headers: proxyRes.headers, + body: responseData, // Full body for crawler link extraction + bodySize: responseSize, + proxyWorker: process.pid // Add worker ID for debugging + })); + }); + }); + + // Handle request errors + proxyReq.on('error', (error) => { + const responseTime = Date.now() - startTime; + + handleCORS(clientRes); + clientRes.writeHead(200, { 'Content-Type': 'application/json' }); + + clientRes.end(JSON.stringify({ + success: false, + error: error.message, + responseTime: responseTime, + statusCode: 0 + })); + }); + + // Handle timeout + proxyReq.on('timeout', () => { + proxyReq.destroy(); + const responseTime = Date.now() - startTime; + + handleCORS(clientRes); + clientRes.writeHead(200, { 'Content-Type': 'application/json' }); + + clientRes.end(JSON.stringify({ + success: false, + error: 'Request timeout', + responseTime: responseTime, + statusCode: 0 + })); + }); + + // Send request body if present + if (body && method !== 'GET' && method !== 'HEAD') { + proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body)); + } + + proxyReq.end(); + } + + // Add CORS headers to response + function handleCORS(res) { + res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins); + res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + } + + // Start the server + server.listen(PORT, () => { + console.log(`Worker ${process.pid} running on http://localhost:${PORT}`); + }); + + // Handle server errors + server.on('error', (error) => { + console.error(`❌ Worker ${process.pid} server error:`, error.message); + process.exit(1); + }); + + // Graceful shutdown for workers + process.on('SIGINT', () => { + console.log(`\n\n🛑 Worker ${process.pid} shutting down...`); + server.close(() => { + console.log(`✅ Worker ${process.pid} closed`); + process.exit(0); + }); + }); +} diff --git a/script.js b/script.js index 75a6e11..f8640a8 100644 --- a/script.js +++ b/script.js @@ -1,1490 +1,1272 @@ -// =================================== -// STRESS TESTING TOOL - MAIN SCRIPT -// Enhanced with Crawler & Advanced Features -// =================================== - -// =================================== -// WEBSITE CRAWLER CLASS -// =================================== -class WebsiteCrawler { - constructor() { - this.visitedUrls = new Set(); - this.urlQueue = []; - } - - extractLinks(html, baseUrl) { - const links = []; - const parser = new DOMParser(); - const doc = parser.parseFromString(html, "text/html"); - const anchorTags = doc.querySelectorAll("a[href]"); - - const baseUrlObj = new URL(baseUrl); - - anchorTags.forEach((anchor) => { - try { - const href = anchor.getAttribute("href"); - if ( - !href || - href.startsWith("#") || - href.startsWith("javascript:") || - href.startsWith("mailto:") - ) { - return; - } - - const absoluteUrl = new URL(href, baseUrl); - - // Only include links from the same domain if configured - if (absoluteUrl.hostname === baseUrlObj.hostname) { - const urlString = absoluteUrl.href; - if (!this.visitedUrls.has(urlString)) { - links.push(urlString); - } - } - } catch (e) { - // Invalid URL, skip - } - }); - - return links; - } - - getNextUrl(currentUrl, html, config) { - // Extract links from current page - const links = this.extractLinks(html, currentUrl); - - // Add new links to queue (limit per page) - const linksToAdd = links.slice(0, config.linksPerPage || 10); - linksToAdd.forEach((link) => { - if (!this.visitedUrls.has(link) && this.urlQueue.length < 100) { - this.urlQueue.push(link); - } - }); - - // Mark current URL as visited - this.visitedUrls.add(currentUrl); - - // Get next URL from queue - if (this.urlQueue.length > 0) { - const randomIndex = Math.floor(Math.random() * this.urlQueue.length); - return this.urlQueue.splice(randomIndex, 1)[0]; - } - - return null; - } - - reset() { - this.visitedUrls.clear(); - this.urlQueue = []; - } -} - -// =================================== -// UTILITY FUNCTIONS -// =================================== -function calculatePercentile(arr, percentile) { - if (arr.length === 0) return 0; - const sorted = [...arr].sort((a, b) => a - b); - const index = Math.ceil((percentile / 100) * sorted.length) - 1; - return sorted[Math.max(0, index)]; -} - -function categorizeError(statusCode, errorMessage) { - if (statusCode >= 400 && statusCode < 500) return "4xx"; - if (statusCode >= 500) return "5xx"; - if (errorMessage && errorMessage.includes("timeout")) return "timeout"; - return "network"; -} - -function formatBytes(bytes) { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; -} - -// =================================== -// MAIN STRESS TESTING TOOL CLASS -// =================================== -class StressTestingTool { - constructor() { - this.config = { - targetUrl: "", - userCount: 100, - duration: 60, - trafficPattern: "steady", - httpMethod: "GET", - customHeaders: {}, - requestBody: null, - thinkTime: 1000, - proxyUrl: - window.location.protocol === "file:" || - window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1" - ? "http://localhost:3000" - : "/proxy", - - // Crawler settings - crawlerEnabled: false, - crawlDepth: 2, - linksPerPage: 10, - stayOnDomain: true, - }; - - this.state = { - status: "idle", - startTime: null, - pauseTime: null, - elapsedTime: 0, - activeUsers: 0, - totalRequests: 0, - successfulRequests: 0, - failedRequests: 0, - responseTimes: [], - requestsPerSecond: [], - workers: [], - updateInterval: null, - chartUpdateInterval: null, - userErrorData: [], - errorThreshold: null, - - // Enhanced metrics - errorsByCategory: { - "4xx": 0, - "5xx": 0, - timeout: 0, - network: 0, - }, - totalBytesSent: 0, - totalBytesReceived: 0, - requestHistory: [], - - // Percentile tracking - percentiles: { - p50: 0, - p95: 0, - p99: 0, - }, - }; - - this.crawler = new WebsiteCrawler(); - this.charts = { - rps: null, - responseTime: null, - userError: null, - }; - - // Test presets - this.presets = { - light: { userCount: 10, duration: 30, trafficPattern: "steady" }, - medium: { userCount: 100, duration: 60, trafficPattern: "random" }, - heavy: { userCount: 500, duration: 120, trafficPattern: "rampup" }, - spike: { userCount: 200, duration: 60, trafficPattern: "burst" }, - }; - - this.init(); - } - - init() { - this.bindElements(); - this.attachEventListeners(); - this.initializeCharts(); - this.loadTheme(); - this.loadSavedConfigs(); - this.setupKeyboardShortcuts(); - this.fetchGitInfo(); - } - - bindElements() { - // Form inputs - this.elements = { - targetUrl: document.getElementById("targetUrl"), - userCount: document.getElementById("userCount"), - userCountValue: document.getElementById("userCountValue"), - duration: document.getElementById("duration"), - durationValue: document.getElementById("durationValue"), - trafficPattern: document.getElementById("trafficPattern"), - httpMethod: document.getElementById("httpMethod"), - customHeaders: document.getElementById("customHeaders"), - requestBody: document.getElementById("requestBody"), - thinkTime: document.getElementById("thinkTime"), - thinkTimeValue: document.getElementById("thinkTimeValue"), - - // Crawler controls - crawlerEnabled: document.getElementById("crawlerEnabled"), - crawlDepth: document.getElementById("crawlDepth"), - crawlDepthValue: document.getElementById("crawlDepthValue"), - linksPerPage: document.getElementById("linksPerPage"), - linksPerPageValue: document.getElementById("linksPerPageValue"), - - // Controls - startBtn: document.getElementById("startBtn"), - pauseBtn: document.getElementById("pauseBtn"), - stopBtn: document.getElementById("stopBtn"), - statusBadge: document.getElementById("statusBadge"), - progressBar: document.getElementById("progressBar"), - - // Statistics - elapsedTime: document.getElementById("elapsedTime"), - remainingTime: document.getElementById("remainingTime"), - activeUsers: document.getElementById("activeUsers"), - totalRequests: document.getElementById("totalRequests"), - requestsPerSec: document.getElementById("requestsPerSec"), - successRate: document.getElementById("successRate"), - failedRequests: document.getElementById("failedRequests"), - avgResponseTime: document.getElementById("avgResponseTime"), - - // Enhanced metrics - p50ResponseTime: document.getElementById("p50ResponseTime"), - p95ResponseTime: document.getElementById("p95ResponseTime"), - p99ResponseTime: document.getElementById("p99ResponseTime"), - errors4xx: document.getElementById("errors4xx"), - errors5xx: document.getElementById("errors5xx"), - errorsTimeout: document.getElementById("errorsTimeout"), - errorsNetwork: document.getElementById("errorsNetwork"), - totalBandwidth: document.getElementById("totalBandwidth"), - - // Request history - requestHistoryBody: document.getElementById("requestHistoryBody"), - - // Results - resultsPanel: document.getElementById("resultsPanel"), - resultsTableBody: document.getElementById("resultsTableBody"), - exportJsonBtn: document.getElementById("exportJsonBtn"), - exportCsvBtn: document.getElementById("exportCsvBtn"), - - // Advanced options - advancedToggle: document.getElementById("advancedToggle"), - advancedContent: document.getElementById("advancedContent"), - - // Theme & presets - themeToggle: document.getElementById("themeToggle"), - presetSelect: document.getElementById("presetSelect"), - saveConfigBtn: document.getElementById("saveConfigBtn"), - - // Git Info - gitInfo: document.getElementById("gitInfo"), - gitCommit: document.getElementById("gitCommit"), - gitDate: document.getElementById("gitDate"), - }; - } - - attachEventListeners() { - // Range inputs - this.elements.userCount.addEventListener("input", (e) => { - this.elements.userCountValue.textContent = e.target.value; - }); - - this.elements.duration.addEventListener("input", (e) => { - this.elements.durationValue.textContent = e.target.value; - }); - - this.elements.thinkTime.addEventListener("input", (e) => { - this.elements.thinkTimeValue.textContent = e.target.value; - }); - - if (this.elements.crawlDepth) { - this.elements.crawlDepth.addEventListener("input", (e) => { - this.elements.crawlDepthValue.textContent = e.target.value; - }); - } - - if (this.elements.linksPerPage) { - this.elements.linksPerPage.addEventListener("input", (e) => { - this.elements.linksPerPageValue.textContent = e.target.value; - }); - } - - // Control buttons - this.elements.startBtn.addEventListener("click", () => this.startTest()); - this.elements.pauseBtn.addEventListener("click", () => this.pauseTest()); - this.elements.stopBtn.addEventListener("click", () => this.stopTest()); - - // Export buttons - this.elements.exportJsonBtn.addEventListener("click", () => - this.exportResults("json") - ); - this.elements.exportCsvBtn.addEventListener("click", () => - this.exportResults("csv") - ); - - // Advanced options accordion - this.elements.advancedToggle.addEventListener("click", () => { - this.elements.advancedToggle.classList.toggle("active"); - this.elements.advancedContent.classList.toggle("active"); - }); - - // Theme toggle - if (this.elements.themeToggle) { - this.elements.themeToggle.addEventListener("click", () => - this.toggleTheme() - ); - } - - // Preset selector - if (this.elements.presetSelect) { - this.elements.presetSelect.addEventListener("change", (e) => - this.loadPreset(e.target.value) - ); - } - - // Save config - if (this.elements.saveConfigBtn) { - this.elements.saveConfigBtn.addEventListener("click", () => - this.saveConfig() - ); - } - } - - setupKeyboardShortcuts() { - document.addEventListener("keydown", (e) => { - // Don't trigger if user is typing in an input - if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") - return; - - switch (e.key.toLowerCase()) { - case "s": - if (this.state.status === "idle") this.startTest(); - break; - case "p": - if (this.state.status === "running" || this.state.status === "paused") - this.pauseTest(); - break; - case "x": - if (this.state.status === "running" || this.state.status === "paused") - this.stopTest(); - break; - } - }); - } - - loadTheme() { - const savedTheme = localStorage.getItem("stressTestTheme") || "dark"; - document.documentElement.setAttribute("data-theme", savedTheme); - } - - toggleTheme() { - const currentTheme = - document.documentElement.getAttribute("data-theme") || "dark"; - const newTheme = currentTheme === "dark" ? "light" : "dark"; - document.documentElement.setAttribute("data-theme", newTheme); - localStorage.setItem("stressTestTheme", newTheme); - - // Update chart colors - this.updateChartTheme(); - } - - updateChartTheme() { - const isDark = - document.documentElement.getAttribute("data-theme") === "dark"; - const textColor = isDark ? "#94a3b8" : "#475569"; - const gridColor = isDark - ? "rgba(148, 163, 184, 0.1)" - : "rgba(148, 163, 184, 0.2)"; - - Object.values(this.charts).forEach((chart) => { - if (chart) { - chart.options.scales.x.ticks.color = textColor; - chart.options.scales.x.grid.color = gridColor; - chart.options.scales.y.ticks.color = textColor; - chart.options.scales.y.grid.color = gridColor; - if (chart.options.scales.y1) { - chart.options.scales.y1.ticks.color = textColor; - } - chart.update("none"); - } - }); - } - - loadSavedConfigs() { - const saved = localStorage.getItem("stressTestConfigs"); - if (saved) { - try { - const configs = JSON.parse(saved); - // Add to preset select if exists - if (this.elements.presetSelect) { - Object.keys(configs).forEach((name) => { - const option = document.createElement("option"); - option.value = `saved_${name}`; - option.textContent = `💾 ${name}`; - this.elements.presetSelect.appendChild(option); - }); - } - } catch (e) { - console.error("Failed to load saved configs:", e); - } - } - } - - loadPreset(presetName) { - if (!presetName) return; - - let config; - if (presetName.startsWith("saved_")) { - const saved = JSON.parse( - localStorage.getItem("stressTestConfigs") || "{}" - ); - config = saved[presetName.replace("saved_", "")]; - } else { - config = this.presets[presetName]; - } - - if (config) { - this.elements.userCount.value = config.userCount; - this.elements.userCountValue.textContent = config.userCount; - this.elements.duration.value = config.duration; - this.elements.durationValue.textContent = config.duration; - this.elements.trafficPattern.value = config.trafficPattern; - } - } - - saveConfig() { - const name = prompt("Enter a name for this configuration:"); - if (!name) return; - - const config = { - userCount: parseInt(this.elements.userCount.value), - duration: parseInt(this.elements.duration.value), - trafficPattern: this.elements.trafficPattern.value, - targetUrl: this.elements.targetUrl.value, - }; - - const saved = JSON.parse(localStorage.getItem("stressTestConfigs") || "{}"); - saved[name] = config; - localStorage.setItem("stressTestConfigs", JSON.stringify(saved)); - - alert(`Configuration "${name}" saved!`); - location.reload(); // Reload to update preset list - } - - async fetchGitInfo() { - try { - // Ensure we don't have double slashes if proxyUrl ends with slash (it shouldn't based on init logic) - const url = `${this.config.proxyUrl}/git-info`; - const response = await fetch(url); - if (response.ok) { - const data = await response.json(); - if (data.commit && data.date && data.commit !== 'Unknown') { - if (this.elements.gitCommit) this.elements.gitCommit.textContent = data.commit; - if (this.elements.gitDate) { - let dateStr = data.date; - // Shorten to match screenshot style (approximate) - dateStr = dateStr.replace(/ days? ago/, 'd ago') - .replace(/ hours? ago/, 'h ago') - .replace(/ minutes? ago/, 'm ago') - .replace(/ seconds? ago/, 's ago'); - this.elements.gitDate.textContent = dateStr; - } - if (this.elements.gitInfo) this.elements.gitInfo.style.display = 'flex'; - } - } - } catch (e) { - console.error('Failed to fetch git info:', e); - } - } - - initializeCharts() { - const isDark = - document.documentElement.getAttribute("data-theme") === "dark"; - const textColor = isDark ? "#94a3b8" : "#475569"; - const gridColor = isDark - ? "rgba(148, 163, 184, 0.1)" - : "rgba(148, 163, 184, 0.2)"; - - const chartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - }, - }, - scales: { - x: { - grid: { - color: gridColor, - }, - ticks: { - color: textColor, - }, - }, - y: { - grid: { - color: gridColor, - }, - ticks: { - color: textColor, - }, - beginAtZero: true, - }, - }, - }; - - // RPS Chart - const rpsCtx = document.getElementById("rpsChart").getContext("2d"); - this.charts.rps = new Chart(rpsCtx, { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "Requests per Second", - data: [], - borderColor: "#6366f1", - backgroundColor: "rgba(99, 102, 241, 0.1)", - borderWidth: 2, - fill: true, - tension: 0.4, - }, - ], - }, - options: { - ...chartOptions, - plugins: { - ...chartOptions.plugins, - title: { - display: true, - text: "Requests per Second", - color: textColor, - font: { - size: 14, - weight: 600, - }, - }, - }, - }, - }); - - // Response Time Chart - const responseTimeCtx = document - .getElementById("responseTimeChart") - .getContext("2d"); - this.charts.responseTime = new Chart(responseTimeCtx, { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "Average Response Time (ms)", - data: [], - borderColor: "#f59e0b", - backgroundColor: "rgba(245, 158, 11, 0.1)", - borderWidth: 2, - fill: true, - tension: 0.4, - }, - ], - }, - options: { - ...chartOptions, - plugins: { - ...chartOptions.plugins, - title: { - display: true, - text: "Average Response Time", - color: textColor, - font: { - size: 14, - weight: 600, - }, - }, - }, - }, - }); - - // User/Error Correlation Chart - const userErrorCtx = document - .getElementById("userErrorChart") - .getContext("2d"); - this.charts.userError = new Chart(userErrorCtx, { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "Active Users", - data: [], - borderColor: "#3b82f6", - backgroundColor: "rgba(59, 130, 246, 0.1)", - borderWidth: 2, - fill: true, - tension: 0.4, - yAxisID: "y", - }, - { - label: "Error Rate (%)", - data: [], - borderColor: "#ef4444", - backgroundColor: "rgba(239, 68, 68, 0.2)", - borderWidth: 2, - fill: true, - tension: 0.4, - yAxisID: "y1", - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - mode: "index", - intersect: false, - }, - plugins: { - legend: { - display: true, - labels: { - color: textColor, - font: { - size: 12, - weight: 600, - }, - }, - }, - title: { - display: true, - text: "User Load vs Error Rate", - color: textColor, - font: { - size: 14, - weight: 600, - }, - }, - }, - scales: { - x: { - grid: { - color: gridColor, - }, - ticks: { - color: textColor, - }, - }, - y: { - type: "linear", - display: true, - position: "left", - title: { - display: true, - text: "Active Users", - color: "#3b82f6", - font: { - size: 12, - weight: 600, - }, - }, - grid: { - color: gridColor, - }, - ticks: { - color: textColor, - }, - beginAtZero: true, - }, - y1: { - type: "linear", - display: true, - position: "right", - title: { - display: true, - text: "Error Rate (%)", - color: "#ef4444", - font: { - size: 12, - weight: 600, - }, - }, - grid: { - drawOnChartArea: false, - }, - ticks: { - color: textColor, - }, - beginAtZero: true, - max: 100, - }, - }, - }, - }); - } - - async startTest() { - if (!this.validateConfig()) { - return; - } - - this.gatherConfig(); - this.resetState(); - this.updateStatus("running"); - this.state.startTime = Date.now(); - - // Update UI - this.elements.startBtn.disabled = true; - this.elements.pauseBtn.disabled = false; - this.elements.stopBtn.disabled = false; - - // Start workers - this.startWorkers(); - - // Start update intervals - this.state.updateInterval = setInterval(() => this.updateStatistics(), 100); - this.state.chartUpdateInterval = setInterval( - () => this.updateCharts(), - 1000 - ); - } - - pauseTest() { - if (this.state.status === "running") { - this.updateStatus("paused"); - this.state.pauseTime = Date.now(); - this.stopWorkers(); - this.elements.pauseBtn.textContent = "▶️ Resume"; - } else if (this.state.status === "paused") { - this.updateStatus("running"); - const pauseDuration = Date.now() - this.state.pauseTime; - this.state.startTime += pauseDuration; - this.startWorkers(); - this.elements.pauseBtn.textContent = "⏸️ Pause"; - } - } - - stopTest() { - this.updateStatus("stopped"); - this.stopWorkers(); - clearInterval(this.state.updateInterval); - clearInterval(this.state.chartUpdateInterval); - - // Update UI - this.elements.startBtn.disabled = false; - this.elements.pauseBtn.disabled = true; - this.elements.stopBtn.disabled = true; - this.elements.pauseBtn.textContent = "⏸️ Pause"; - - // Calculate final percentiles - this.calculatePercentiles(); - - // Show results - this.displayResults(); - } - - validateConfig() { - const url = this.elements.targetUrl.value.trim(); - if (!url) { - alert("Please enter a target URL"); - return false; - } - - try { - new URL(url); - } catch (e) { - alert("Please enter a valid URL"); - return false; - } - - const headersText = this.elements.customHeaders.value.trim(); - if (headersText) { - try { - JSON.parse(headersText); - } catch (e) { - alert("Custom headers must be valid JSON"); - return false; - } - } - - const bodyText = this.elements.requestBody.value.trim(); - if (bodyText) { - try { - JSON.parse(bodyText); - } catch (e) { - alert("Request body must be valid JSON"); - return false; - } - } - - return true; - } - - gatherConfig() { - this.config.targetUrl = this.elements.targetUrl.value.trim(); - this.config.userCount = parseInt(this.elements.userCount.value); - this.config.duration = parseInt(this.elements.duration.value); - this.config.trafficPattern = this.elements.trafficPattern.value; - this.config.httpMethod = this.elements.httpMethod.value; - this.config.thinkTime = parseInt(this.elements.thinkTime.value); - - const headersText = this.elements.customHeaders.value.trim(); - this.config.customHeaders = headersText ? JSON.parse(headersText) : {}; - - const bodyText = this.elements.requestBody.value.trim(); - this.config.requestBody = bodyText ? JSON.parse(bodyText) : null; - - // Crawler config - if (this.elements.crawlerEnabled) { - this.config.crawlerEnabled = this.elements.crawlerEnabled.checked; - this.config.crawlDepth = parseInt(this.elements.crawlDepth?.value || 2); - this.config.linksPerPage = parseInt( - this.elements.linksPerPage?.value || 10 - ); - } - } - - resetState() { - this.state.elapsedTime = 0; - this.state.activeUsers = 0; - this.state.totalRequests = 0; - this.state.successfulRequests = 0; - this.state.failedRequests = 0; - this.state.responseTimes = []; - this.state.requestsPerSecond = []; - this.state.workers = []; - this.state.userErrorData = []; - this.state.errorThreshold = null; - this.state.errorsByCategory = { - "4xx": 0, - "5xx": 0, - timeout: 0, - network: 0, - }; - this.state.totalBytesSent = 0; - this.state.totalBytesReceived = 0; - this.state.requestHistory = []; - this.state.percentiles = { p50: 0, p95: 0, p99: 0 }; - - // Reset crawler - this.crawler.reset(); - if (this.config.crawlerEnabled) { - this.crawler.urlQueue.push(this.config.targetUrl); - } - - // Reset charts - this.charts.rps.data.labels = []; - this.charts.rps.data.datasets[0].data = []; - this.charts.responseTime.data.labels = []; - this.charts.responseTime.data.datasets[0].data = []; - this.charts.userError.data.labels = []; - this.charts.userError.data.datasets[0].data = []; - this.charts.userError.data.datasets[1].data = []; - this.charts.rps.update("none"); - this.charts.responseTime.update("none"); - this.charts.userError.update("none"); - - // Clear request history table - if (this.elements.requestHistoryBody) { - this.elements.requestHistoryBody.innerHTML = ""; - } - - // Hide results panel - this.elements.resultsPanel.style.display = "none"; - } - - startWorkers() { - const pattern = this.config.trafficPattern; - - switch (pattern) { - case "steady": - this.startSteadyLoad(); - break; - case "burst": - this.startBurstLoad(); - break; - case "rampup": - this.startRampUpLoad(); - break; - case "random": - this.startRandomLoad(); - break; - } - } - - startSteadyLoad() { - const delayBetweenUsers = 100; - - for (let i = 0; i < this.config.userCount; i++) { - setTimeout(() => { - if (this.state.status === "running") { - this.createWorker(i); - } - }, i * delayBetweenUsers); - } - } - - startBurstLoad() { - const burstSize = Math.ceil(this.config.userCount / 5); - const burstInterval = (this.config.duration * 1000) / 5; - - for (let burst = 0; burst < 5; burst++) { - setTimeout(() => { - if (this.state.status === "running") { - for (let i = 0; i < burstSize; i++) { - this.createWorker(burst * burstSize + i); - } - } - }, burst * burstInterval); - } - } - - startRampUpLoad() { - const totalTime = this.config.duration * 1000; - const timePerUser = totalTime / this.config.userCount; - - for (let i = 0; i < this.config.userCount; i++) { - setTimeout(() => { - if (this.state.status === "running") { - this.createWorker(i); - } - }, i * timePerUser); - } - } - - startRandomLoad() { - const maxDelay = (this.config.duration * 1000) / 2; - - for (let i = 0; i < this.config.userCount; i++) { - const randomDelay = Math.random() * maxDelay; - setTimeout(() => { - if (this.state.status === "running") { - this.createWorker(i); - } - }, randomDelay); - } - } - - createWorker(id) { - const worker = { - id: id, - active: true, - requestCount: 0, - currentUrl: this.config.targetUrl, - crawlDepth: 0, - }; - - this.state.workers.push(worker); - this.state.activeUsers++; - this.runWorker(worker); - } - - async runWorker(worker) { - const endTime = this.state.startTime + this.config.duration * 1000; - - while ( - worker.active && - this.state.status === "running" && - Date.now() < endTime - ) { - await this.makeRequest(worker); - - // Think time - if (this.config.thinkTime > 0) { - await this.sleep(this.config.thinkTime); - } - } - - // Worker finished - worker.active = false; - this.state.activeUsers--; - - // Check if all workers are done - if (this.state.activeUsers === 0 && this.state.status === "running") { - this.stopTest(); - } - } - - async makeRequest(worker) { - const startTime = performance.now(); - const requestUrl = worker.currentUrl; - - try { - const proxyPayload = { - targetUrl: requestUrl, - method: this.config.httpMethod, - headers: this.config.customHeaders, - body: this.config.requestBody, - }; - - // Estimate request size - const requestSize = JSON.stringify(proxyPayload).length; - this.state.totalBytesSent += requestSize; - - const response = await fetch(this.config.proxyUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(proxyPayload), - }); - - const endTime = performance.now(); - const responseTime = endTime - startTime; - - this.state.totalRequests++; - worker.requestCount++; - - const proxyResponse = await response.json(); - - // Track response size - if (proxyResponse.body) { - this.state.totalBytesReceived += proxyResponse.body.length; - } - - const isSuccess = - proxyResponse.success && - proxyResponse.statusCode >= 200 && - proxyResponse.statusCode < 400; - - if (isSuccess) { - this.state.successfulRequests++; - } else { - this.state.failedRequests++; - const category = categorizeError( - proxyResponse.statusCode, - proxyResponse.error - ); - this.state.errorsByCategory[category]++; - } - - const actualResponseTime = proxyResponse.responseTime || responseTime; - this.state.responseTimes.push(actualResponseTime); - - // Keep only last 1000 response times - if (this.state.responseTimes.length > 1000) { - this.state.responseTimes.shift(); - } - - // Add to request history - this.addToRequestHistory({ - url: requestUrl, - status: proxyResponse.statusCode, - responseTime: Math.round(actualResponseTime), - success: isSuccess, - timestamp: new Date().toLocaleTimeString(), - }); - - // Crawler: Get next URL if enabled - if ( - this.config.crawlerEnabled && - isSuccess && - proxyResponse.body && - worker.crawlDepth < this.config.crawlDepth - ) { - const nextUrl = this.crawler.getNextUrl( - requestUrl, - proxyResponse.body, - this.config - ); - if (nextUrl) { - worker.currentUrl = nextUrl; - worker.crawlDepth++; - } - } - } catch (error) { - const endTime = performance.now(); - const responseTime = endTime - startTime; - - this.state.totalRequests++; - this.state.failedRequests++; - this.state.responseTimes.push(responseTime); - this.state.errorsByCategory["network"]++; - worker.requestCount++; - - this.addToRequestHistory({ - url: requestUrl, - status: 0, - responseTime: Math.round(responseTime), - success: false, - timestamp: new Date().toLocaleTimeString(), - error: error.message, - }); - } - } - - addToRequestHistory(request) { - this.state.requestHistory.unshift(request); - - // Keep only last 100 - if (this.state.requestHistory.length > 100) { - this.state.requestHistory.pop(); - } - - // Update UI table - if (this.elements.requestHistoryBody) { - const row = document.createElement("tr"); - row.className = request.success ? "success-row" : "error-row"; - row.innerHTML = ` - ${request.timestamp} - ${this.truncateUrl( - request.url - )} - ${request.status} - ${request.responseTime}ms - `; - - this.elements.requestHistoryBody.insertBefore( - row, - this.elements.requestHistoryBody.firstChild - ); - - // Keep only 100 rows in DOM - while (this.elements.requestHistoryBody.children.length > 100) { - this.elements.requestHistoryBody.removeChild( - this.elements.requestHistoryBody.lastChild - ); - } - } - } - - truncateUrl(url) { - if (url.length > 50) { - return url.substring(0, 47) + "..."; - } - return url; - } - - stopWorkers() { - this.state.workers.forEach((worker) => { - worker.active = false; - }); - } - - calculatePercentiles() { - if (this.state.responseTimes.length > 0) { - this.state.percentiles.p50 = Math.round( - calculatePercentile(this.state.responseTimes, 50) - ); - this.state.percentiles.p95 = Math.round( - calculatePercentile(this.state.responseTimes, 95) - ); - this.state.percentiles.p99 = Math.round( - calculatePercentile(this.state.responseTimes, 99) - ); - } - } - - updateStatistics() { - const now = Date.now(); - const elapsed = Math.floor((now - this.state.startTime) / 1000); - const remaining = Math.max(0, this.config.duration - elapsed); - const progress = Math.min(100, (elapsed / this.config.duration) * 100); - - // Update time displays - this.elements.elapsedTime.textContent = `${elapsed}s`; - this.elements.remainingTime.textContent = `${remaining}s`; - this.elements.progressBar.style.width = `${progress}%`; - - // Update statistics - this.elements.activeUsers.textContent = this.state.activeUsers; - this.elements.totalRequests.textContent = - this.state.totalRequests.toLocaleString(); - this.elements.failedRequests.textContent = - this.state.failedRequests.toLocaleString(); - - // Calculate RPS - const rps = - elapsed > 0 ? Math.round(this.state.totalRequests / elapsed) : 0; - this.elements.requestsPerSec.textContent = rps; - - // Calculate success rate - const successRate = - this.state.totalRequests > 0 - ? ( - (this.state.successfulRequests / this.state.totalRequests) * - 100 - ).toFixed(1) - : 0; - this.elements.successRate.textContent = `${successRate}%`; - - // Calculate average response time - const avgResponseTime = - this.state.responseTimes.length > 0 - ? Math.round( - this.state.responseTimes.reduce((a, b) => a + b, 0) / - this.state.responseTimes.length - ) - : 0; - this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`; - - // Update enhanced metrics - if (this.elements.p50ResponseTime) { - const p50 = Math.round(calculatePercentile(this.state.responseTimes, 50)); - const p95 = Math.round(calculatePercentile(this.state.responseTimes, 95)); - const p99 = Math.round(calculatePercentile(this.state.responseTimes, 99)); - - this.elements.p50ResponseTime.textContent = `${p50}ms`; - this.elements.p95ResponseTime.textContent = `${p95}ms`; - this.elements.p99ResponseTime.textContent = `${p99}ms`; - } - - if (this.elements.errors4xx) { - this.elements.errors4xx.textContent = this.state.errorsByCategory["4xx"]; - this.elements.errors5xx.textContent = this.state.errorsByCategory["5xx"]; - this.elements.errorsTimeout.textContent = - this.state.errorsByCategory["timeout"]; - this.elements.errorsNetwork.textContent = - this.state.errorsByCategory["network"]; - } - - if (this.elements.totalBandwidth) { - const totalBytes = - this.state.totalBytesSent + this.state.totalBytesReceived; - this.elements.totalBandwidth.textContent = formatBytes(totalBytes); - } - } - - updateCharts() { - const now = Date.now(); - const elapsed = Math.floor((now - this.state.startTime) / 1000); - - // Calculate current RPS - const currentRps = - this.state.totalRequests > 0 && elapsed > 0 - ? Math.round(this.state.totalRequests / elapsed) - : 0; - - // Calculate current average response time - const recentResponseTimes = this.state.responseTimes.slice(-100); - const currentAvgResponseTime = - recentResponseTimes.length > 0 - ? Math.round( - recentResponseTimes.reduce((a, b) => a + b, 0) / - recentResponseTimes.length - ) - : 0; - - // Update RPS chart - this.charts.rps.data.labels.push(`${elapsed}s`); - this.charts.rps.data.datasets[0].data.push(currentRps); - - if (this.charts.rps.data.labels.length > 60) { - this.charts.rps.data.labels.shift(); - this.charts.rps.data.datasets[0].data.shift(); - } - - this.charts.rps.update("none"); - - // Update Response Time chart - this.charts.responseTime.data.labels.push(`${elapsed}s`); - this.charts.responseTime.data.datasets[0].data.push(currentAvgResponseTime); - - if (this.charts.responseTime.data.labels.length > 60) { - this.charts.responseTime.data.labels.shift(); - this.charts.responseTime.data.datasets[0].data.shift(); - } - - this.charts.responseTime.update("none"); - - // Calculate current error rate - const currentErrorRate = - this.state.totalRequests > 0 - ? ( - (this.state.failedRequests / this.state.totalRequests) * - 100 - ).toFixed(1) - : 0; - - // Update User/Error chart - this.charts.userError.data.labels.push(`${elapsed}s`); - this.charts.userError.data.datasets[0].data.push(this.state.activeUsers); - this.charts.userError.data.datasets[1].data.push( - parseFloat(currentErrorRate) - ); - - // Track user/error data - this.state.userErrorData.push({ - time: elapsed, - users: this.state.activeUsers, - errorRate: parseFloat(currentErrorRate), - failedRequests: this.state.failedRequests, - }); - - // Detect error threshold - if ( - this.state.errorThreshold === null && - this.state.failedRequests > 0 && - this.state.activeUsers > 0 - ) { - this.state.errorThreshold = { - users: this.state.activeUsers, - time: elapsed, - errorRate: parseFloat(currentErrorRate), - }; - } - - if (this.charts.userError.data.labels.length > 60) { - this.charts.userError.data.labels.shift(); - this.charts.userError.data.datasets[0].data.shift(); - this.charts.userError.data.datasets[1].data.shift(); - } - - this.charts.userError.update("none"); - } - - updateStatus(status) { - this.state.status = status; - const badge = this.elements.statusBadge; - - badge.className = "status-badge"; - - switch (status) { - case "idle": - badge.classList.add("status-idle"); - badge.textContent = "Idle"; - break; - case "running": - badge.classList.add("status-running"); - badge.textContent = "Running"; - break; - case "paused": - badge.classList.add("status-paused"); - badge.textContent = "Paused"; - break; - case "stopped": - badge.classList.add("status-idle"); - badge.textContent = "Completed"; - break; - } - } - - displayResults() { - this.elements.resultsPanel.style.display = "block"; - - const results = this.calculateResults(); - const tbody = this.elements.resultsTableBody; - tbody.innerHTML = ""; - - // Populate results table - Object.entries(results).forEach(([key, value]) => { - const row = document.createElement("tr"); - row.innerHTML = ` - ${key} - ${value} - `; - tbody.appendChild(row); - }); - - // Scroll to results - this.elements.resultsPanel.scrollIntoView({ behavior: "smooth" }); - } - - calculateResults() { - const totalTime = - this.state.elapsedTime || - Math.floor((Date.now() - this.state.startTime) / 1000); - const successRate = - this.state.totalRequests > 0 - ? ( - (this.state.successfulRequests / this.state.totalRequests) * - 100 - ).toFixed(2) - : 0; - - const avgResponseTime = - this.state.responseTimes.length > 0 - ? Math.round( - this.state.responseTimes.reduce((a, b) => a + b, 0) / - this.state.responseTimes.length - ) - : 0; - - const minResponseTime = - this.state.responseTimes.length > 0 - ? Math.round(Math.min(...this.state.responseTimes)) - : 0; - - const maxResponseTime = - this.state.responseTimes.length > 0 - ? Math.round(Math.max(...this.state.responseTimes)) - : 0; - - const rps = - totalTime > 0 ? (this.state.totalRequests / totalTime).toFixed(2) : 0; - - const results = { - "Target URL": this.config.targetUrl, - "Test Duration": `${totalTime} seconds`, - "Concurrent Users": this.config.userCount, - "Traffic Pattern": this.config.trafficPattern, - "Crawler Mode": this.config.crawlerEnabled ? "Enabled" : "Disabled", - "Total Requests": this.state.totalRequests.toLocaleString(), - "Successful Requests": this.state.successfulRequests.toLocaleString(), - "Failed Requests": this.state.failedRequests.toLocaleString(), - "Success Rate": `${successRate}%`, - "Requests per Second": rps, - "Average Response Time": `${avgResponseTime}ms`, - "Min Response Time": `${minResponseTime}ms`, - "Max Response Time": `${maxResponseTime}ms`, - "P50 Response Time": `${this.state.percentiles.p50}ms`, - "P95 Response Time": `${this.state.percentiles.p95}ms`, - "P99 Response Time": `${this.state.percentiles.p99}ms`, - "4xx Errors": this.state.errorsByCategory["4xx"], - "5xx Errors": this.state.errorsByCategory["5xx"], - "Timeout Errors": this.state.errorsByCategory["timeout"], - "Network Errors": this.state.errorsByCategory["network"], - "Total Bandwidth": formatBytes( - this.state.totalBytesSent + this.state.totalBytesReceived - ), - "Data Sent": formatBytes(this.state.totalBytesSent), - "Data Received": formatBytes(this.state.totalBytesReceived), - "HTTP Method": this.config.httpMethod, - "Think Time": `${this.config.thinkTime}ms`, - "Error Threshold": this.state.errorThreshold - ? `${this.state.errorThreshold.users} users at ${this.state.errorThreshold.time}s (${this.state.errorThreshold.errorRate}% error rate)` - : "No errors detected", - }; - - if (this.config.crawlerEnabled) { - results["Unique URLs Visited"] = this.crawler.visitedUrls.size; - } - - return results; - } - - exportResults(format) { - const results = this.calculateResults(); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - - if (format === "json") { - const data = { - config: this.config, - results: results, - requestHistory: this.state.requestHistory.slice(0, 100), - timestamp: new Date().toISOString(), - }; - - const blob = new Blob([JSON.stringify(data, null, 2)], { - type: "application/json", - }); - this.downloadFile(blob, `stress-test-results-${timestamp}.json`); - } else if (format === "csv") { - let csv = "Metric,Value\n"; - Object.entries(results).forEach(([key, value]) => { - csv += `"${key}","${value}"\n`; - }); - - const blob = new Blob([csv], { type: "text/csv" }); - this.downloadFile(blob, `stress-test-results-${timestamp}.csv`); - } - } - - downloadFile(blob, filename) { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - - sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} - -// Initialize the application -document.addEventListener("DOMContentLoaded", () => { - new StressTestingTool(); -}); +// =================================== +// UTILITY FUNCTIONS +// =================================== +function calculatePercentile(arr, percentile) { + if (arr.length === 0) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const index = Math.ceil((percentile / 100) * sorted.length) - 1; + return sorted[Math.max(0, index)]; +} + +function categorizeError(statusCode, errorMessage) { + if (statusCode >= 400 && statusCode < 500) return "4xx"; + if (statusCode >= 500) return "5xx"; + if (errorMessage && errorMessage.includes("timeout")) return "timeout"; + return "network"; +} + +function formatBytes(bytes) { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; +} + +// =================================== +// MAIN STRESS TESTING TOOL CLASS +// =================================== +class StressTestingTool { + constructor() { + this.config = { + targetUrl: "", + userCount: 100, + duration: 60, + trafficPattern: "steady", + httpMethod: "GET", + customHeaders: {}, + requestBody: null, + thinkTime: 1000, + proxyUrl: + window.location.protocol === "file:" || + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" + ? "http://localhost:3000" + : "/proxy", + + // Crawler settings + crawlerEnabled: false, + crawlDepth: 2, + linksPerPage: 10, + stayOnDomain: true, + }; + + this.state = { + status: "idle", + startTime: null, + pauseTime: null, + elapsedTime: 0, + activeUsers: 0, + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + responseTimes: [], + requestsPerSecond: [], + workers: [], // Web Worker instances + workerStats: new Map(), // Stats per worker + updateInterval: null, + chartUpdateInterval: null, + userErrorData: [], + errorThreshold: null, + lastUiUpdate: 0, + + // Enhanced metrics + errorsByCategory: { + "4xx": 0, + "5xx": 0, + timeout: 0, + network: 0, + }, + totalBytesSent: 0, + totalBytesReceived: 0, + requestHistory: [], + + // Percentile tracking + percentiles: { + p50: 0, + p95: 0, + p99: 0, + }, + }; + + this.charts = { + rps: null, + responseTime: null, + userError: null, + }; + + // Test presets + this.presets = { + light: { userCount: 10, duration: 30, trafficPattern: "steady" }, + medium: { userCount: 100, duration: 60, trafficPattern: "random" }, + heavy: { userCount: 500, duration: 120, trafficPattern: "rampup" }, + spike: { userCount: 200, duration: 60, trafficPattern: "burst" }, + }; + + this.init(); + } + + init() { + this.bindElements(); + this.attachEventListeners(); + this.initializeCharts(); + this.loadTheme(); + this.loadSavedConfigs(); + this.setupKeyboardShortcuts(); + this.fetchGitInfo(); + } + + bindElements() { + // Form inputs + this.elements = { + targetUrl: document.getElementById("targetUrl"), + userCount: document.getElementById("userCount"), + userCountValue: document.getElementById("userCountValue"), + duration: document.getElementById("duration"), + durationValue: document.getElementById("durationValue"), + trafficPattern: document.getElementById("trafficPattern"), + httpMethod: document.getElementById("httpMethod"), + customHeaders: document.getElementById("customHeaders"), + requestBody: document.getElementById("requestBody"), + thinkTime: document.getElementById("thinkTime"), + thinkTimeValue: document.getElementById("thinkTimeValue"), + + // Crawler controls + crawlerEnabled: document.getElementById("crawlerEnabled"), + crawlDepth: document.getElementById("crawlDepth"), + crawlDepthValue: document.getElementById("crawlDepthValue"), + linksPerPage: document.getElementById("linksPerPage"), + linksPerPageValue: document.getElementById("linksPerPageValue"), + + // Controls + startBtn: document.getElementById("startBtn"), + pauseBtn: document.getElementById("pauseBtn"), + stopBtn: document.getElementById("stopBtn"), + statusBadge: document.getElementById("statusBadge"), + progressBar: document.getElementById("progressBar"), + + // Statistics + elapsedTime: document.getElementById("elapsedTime"), + remainingTime: document.getElementById("remainingTime"), + activeUsers: document.getElementById("activeUsers"), + totalRequests: document.getElementById("totalRequests"), + requestsPerSec: document.getElementById("requestsPerSec"), + successRate: document.getElementById("successRate"), + failedRequests: document.getElementById("failedRequests"), + avgResponseTime: document.getElementById("avgResponseTime"), + + // Enhanced metrics + p50ResponseTime: document.getElementById("p50ResponseTime"), + p95ResponseTime: document.getElementById("p95ResponseTime"), + p99ResponseTime: document.getElementById("p99ResponseTime"), + errors4xx: document.getElementById("errors4xx"), + errors5xx: document.getElementById("errors5xx"), + errorsTimeout: document.getElementById("errorsTimeout"), + errorsNetwork: document.getElementById("errorsNetwork"), + totalBandwidth: document.getElementById("totalBandwidth"), + + // Request history + requestHistoryBody: document.getElementById("requestHistoryBody"), + + // Results + resultsPanel: document.getElementById("resultsPanel"), + resultsTableBody: document.getElementById("resultsTableBody"), + exportJsonBtn: document.getElementById("exportJsonBtn"), + exportCsvBtn: document.getElementById("exportCsvBtn"), + + // Advanced options + advancedToggle: document.getElementById("advancedToggle"), + advancedContent: document.getElementById("advancedContent"), + + // Theme & presets + themeToggle: document.getElementById("themeToggle"), + presetSelect: document.getElementById("presetSelect"), + saveConfigBtn: document.getElementById("saveConfigBtn"), + + // Git Info + gitInfo: document.getElementById("gitInfo"), + gitCommit: document.getElementById("gitCommit"), + gitDate: document.getElementById("gitDate"), + }; + } + + attachEventListeners() { + // Range inputs + this.elements.userCount.addEventListener("input", (e) => { + this.elements.userCountValue.textContent = e.target.value; + }); + + this.elements.duration.addEventListener("input", (e) => { + this.elements.durationValue.textContent = e.target.value; + }); + + this.elements.thinkTime.addEventListener("input", (e) => { + this.elements.thinkTimeValue.textContent = e.target.value; + }); + + if (this.elements.crawlDepth) { + this.elements.crawlDepth.addEventListener("input", (e) => { + this.elements.crawlDepthValue.textContent = e.target.value; + }); + } + + if (this.elements.linksPerPage) { + this.elements.linksPerPage.addEventListener("input", (e) => { + this.elements.linksPerPageValue.textContent = e.target.value; + }); + } + + // Control buttons + this.elements.startBtn.addEventListener("click", () => this.startTest()); + this.elements.pauseBtn.addEventListener("click", () => this.pauseTest()); + this.elements.stopBtn.addEventListener("click", () => this.stopTest()); + + // Export buttons + this.elements.exportJsonBtn.addEventListener("click", () => + this.exportResults("json") + ); + this.elements.exportCsvBtn.addEventListener("click", () => + this.exportResults("csv") + ); + + // Advanced options accordion + this.elements.advancedToggle.addEventListener("click", () => { + this.elements.advancedToggle.classList.toggle("active"); + this.elements.advancedContent.classList.toggle("active"); + }); + + // Theme toggle + if (this.elements.themeToggle) { + this.elements.themeToggle.addEventListener("click", () => + this.toggleTheme() + ); + } + + // Preset selector + if (this.elements.presetSelect) { + this.elements.presetSelect.addEventListener("change", (e) => + this.loadPreset(e.target.value) + ); + } + + // Save config + if (this.elements.saveConfigBtn) { + this.elements.saveConfigBtn.addEventListener("click", () => + this.saveConfig() + ); + } + } + + setupKeyboardShortcuts() { + document.addEventListener("keydown", (e) => { + // Don't trigger if user is typing in an input + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") + return; + + switch (e.key.toLowerCase()) { + case "s": + if (this.state.status === "idle") this.startTest(); + break; + case "p": + if (this.state.status === "running" || this.state.status === "paused") + this.pauseTest(); + break; + case "x": + if (this.state.status === "running" || this.state.status === "paused") + this.stopTest(); + break; + } + }); + } + + loadTheme() { + const savedTheme = localStorage.getItem("stressTestTheme") || "dark"; + document.documentElement.setAttribute("data-theme", savedTheme); + } + + toggleTheme() { + const currentTheme = + document.documentElement.getAttribute("data-theme") || "dark"; + const newTheme = currentTheme === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme", newTheme); + localStorage.setItem("stressTestTheme", newTheme); + + // Update chart colors + this.updateChartTheme(); + } + + updateChartTheme() { + const isDark = + document.documentElement.getAttribute("data-theme") === "dark"; + const textColor = isDark ? "#94a3b8" : "#475569"; + const gridColor = isDark + ? "rgba(148, 163, 184, 0.1)" + : "rgba(148, 163, 184, 0.2)"; + + Object.values(this.charts).forEach((chart) => { + if (chart) { + chart.options.scales.x.ticks.color = textColor; + chart.options.scales.x.grid.color = gridColor; + chart.options.scales.y.ticks.color = textColor; + chart.options.scales.y.grid.color = gridColor; + if (chart.options.scales.y1) { + chart.options.scales.y1.ticks.color = textColor; + } + chart.update("none"); + } + }); + } + + loadSavedConfigs() { + const saved = localStorage.getItem("stressTestConfigs"); + if (saved) { + try { + const configs = JSON.parse(saved); + // Add to preset select if exists + if (this.elements.presetSelect) { + Object.keys(configs).forEach((name) => { + const option = document.createElement("option"); + option.value = `saved_${name}`; + option.textContent = `💾 ${name}`; + this.elements.presetSelect.appendChild(option); + }); + } + } catch (e) { + console.error("Failed to load saved configs:", e); + } + } + } + + loadPreset(presetName) { + if (!presetName) return; + + let config; + if (presetName.startsWith("saved_")) { + const saved = JSON.parse( + localStorage.getItem("stressTestConfigs") || "{}" + ); + config = saved[presetName.replace("saved_", "")]; + } else { + config = this.presets[presetName]; + } + + if (config) { + this.elements.userCount.value = config.userCount; + this.elements.userCountValue.textContent = config.userCount; + this.elements.duration.value = config.duration; + this.elements.durationValue.textContent = config.duration; + this.elements.trafficPattern.value = config.trafficPattern; + } + } + + saveConfig() { + const name = prompt("Enter a name for this configuration:"); + if (!name) return; + + const config = { + userCount: parseInt(this.elements.userCount.value), + duration: parseInt(this.elements.duration.value), + trafficPattern: this.elements.trafficPattern.value, + targetUrl: this.elements.targetUrl.value, + }; + + const saved = JSON.parse(localStorage.getItem("stressTestConfigs") || "{}"); + saved[name] = config; + localStorage.setItem("stressTestConfigs", JSON.stringify(saved)); + + alert(`Configuration "${name}" saved!`); + location.reload(); // Reload to update preset list + } + + async fetchGitInfo() { + try { + // Ensure we don't have double slashes if proxyUrl ends with slash (it shouldn't based on init logic) + const url = `${this.config.proxyUrl}/git-info`; + const response = await fetch(url); + if (response.ok) { + const data = await response.json(); + if (data.commit && data.date && data.commit !== 'Unknown') { + if (this.elements.gitCommit) this.elements.gitCommit.textContent = data.commit; + if (this.elements.gitDate) { + let dateStr = data.date; + // Shorten to match screenshot style (approximate) + dateStr = dateStr.replace(/ days? ago/, 'd ago') + .replace(/ hours? ago/, 'h ago') + .replace(/ minutes? ago/, 'm ago') + .replace(/ seconds? ago/, 's ago'); + this.elements.gitDate.textContent = dateStr; + } + if (this.elements.gitInfo) this.elements.gitInfo.style.display = 'flex'; + } + } + } catch (e) { + console.error('Failed to fetch git info:', e); + } + } + + initializeCharts() { + const isDark = + document.documentElement.getAttribute("data-theme") === "dark"; + const textColor = isDark ? "#94a3b8" : "#475569"; + const gridColor = isDark + ? "rgba(148, 163, 184, 0.1)" + : "rgba(148, 163, 184, 0.2)"; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + }, + scales: { + x: { + grid: { + color: gridColor, + }, + ticks: { + color: textColor, + }, + }, + y: { + grid: { + color: gridColor, + }, + ticks: { + color: textColor, + }, + beginAtZero: true, + }, + }, + }; + + // RPS Chart + const rpsCtx = document.getElementById("rpsChart").getContext("2d"); + this.charts.rps = new Chart(rpsCtx, { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "Requests per Second", + data: [], + borderColor: "#6366f1", + backgroundColor: "rgba(99, 102, 241, 0.1)", + borderWidth: 2, + fill: true, + tension: 0.4, + }, + ], + }, + options: { + ...chartOptions, + plugins: { + ...chartOptions.plugins, + title: { + display: true, + text: "Requests per Second", + color: textColor, + font: { + size: 14, + weight: 600, + }, + }, + }, + }, + }); + + // Response Time Chart + const responseTimeCtx = document + .getElementById("responseTimeChart") + .getContext("2d"); + this.charts.responseTime = new Chart(responseTimeCtx, { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "Average Response Time (ms)", + data: [], + borderColor: "#f59e0b", + backgroundColor: "rgba(245, 158, 11, 0.1)", + borderWidth: 2, + fill: true, + tension: 0.4, + }, + ], + }, + options: { + ...chartOptions, + plugins: { + ...chartOptions.plugins, + title: { + display: true, + text: "Average Response Time", + color: textColor, + font: { + size: 14, + weight: 600, + }, + }, + }, + }, + }); + + // User/Error Correlation Chart + const userErrorCtx = document + .getElementById("userErrorChart") + .getContext("2d"); + this.charts.userError = new Chart(userErrorCtx, { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "Active Users", + data: [], + borderColor: "#3b82f6", + backgroundColor: "rgba(59, 130, 246, 0.1)", + borderWidth: 2, + fill: true, + tension: 0.4, + yAxisID: "y", + }, + { + label: "Error Rate (%)", + data: [], + borderColor: "#ef4444", + backgroundColor: "rgba(239, 68, 68, 0.2)", + borderWidth: 2, + fill: true, + tension: 0.4, + yAxisID: "y1", + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: "index", + intersect: false, + }, + plugins: { + legend: { + display: true, + labels: { + color: textColor, + font: { + size: 12, + weight: 600, + }, + }, + }, + title: { + display: true, + text: "User Load vs Error Rate", + color: textColor, + font: { + size: 14, + weight: 600, + }, + }, + }, + scales: { + x: { + grid: { + color: gridColor, + }, + ticks: { + color: textColor, + }, + }, + y: { + type: "linear", + display: true, + position: "left", + title: { + display: true, + text: "Active Users", + color: "#3b82f6", + font: { + size: 12, + weight: 600, + }, + }, + grid: { + color: gridColor, + }, + ticks: { + color: textColor, + }, + beginAtZero: true, + }, + y1: { + type: "linear", + display: true, + position: "right", + title: { + display: true, + text: "Error Rate (%)", + color: "#ef4444", + font: { + size: 12, + weight: 600, + }, + }, + grid: { + drawOnChartArea: false, + }, + ticks: { + color: textColor, + }, + beginAtZero: true, + max: 100, + }, + }, + }, + }); + } + + async startTest() { + if (!this.validateConfig()) { + return; + } + + this.gatherConfig(); + this.resetState(); + this.updateStatus("running"); + this.state.startTime = Date.now(); + + // Update UI + this.elements.startBtn.disabled = true; + this.elements.pauseBtn.disabled = false; + this.elements.stopBtn.disabled = false; + + // Start workers + this.startWorkers(); + + // Start update intervals + this.state.updateInterval = setInterval(() => this.updateStatistics(), 100); + this.state.chartUpdateInterval = setInterval( + () => this.updateCharts(), + 1000 + ); + } + + pauseTest() { + if (this.state.status === "running") { + this.updateStatus("paused"); + this.state.pauseTime = Date.now(); + this.stopWorkers(); + this.elements.pauseBtn.textContent = "▶️ Resume"; + } else if (this.state.status === "paused") { + this.updateStatus("running"); + const pauseDuration = Date.now() - this.state.pauseTime; + this.state.startTime += pauseDuration; + this.startWorkers(); + this.elements.pauseBtn.textContent = "⏸️ Pause"; + } + } + + stopTest() { + this.updateStatus("stopped"); + this.stopWorkers(); + clearInterval(this.state.updateInterval); + clearInterval(this.state.chartUpdateInterval); + + // Update UI + this.elements.startBtn.disabled = false; + this.elements.pauseBtn.disabled = true; + this.elements.stopBtn.disabled = true; + this.elements.pauseBtn.textContent = "⏸️ Pause"; + + // Calculate final percentiles + this.calculatePercentiles(); + + // Show results + this.displayResults(); + } + + validateConfig() { + const url = this.elements.targetUrl.value.trim(); + if (!url) { + alert("Please enter a target URL"); + return false; + } + + try { + new URL(url); + } catch (e) { + alert("Please enter a valid URL"); + return false; + } + + const headersText = this.elements.customHeaders.value.trim(); + if (headersText) { + try { + JSON.parse(headersText); + } catch (e) { + alert("Custom headers must be valid JSON"); + return false; + } + } + + const bodyText = this.elements.requestBody.value.trim(); + if (bodyText) { + try { + JSON.parse(bodyText); + } catch (e) { + alert("Request body must be valid JSON"); + return false; + } + } + + return true; + } + + gatherConfig() { + this.config.targetUrl = this.elements.targetUrl.value.trim(); + this.config.userCount = parseInt(this.elements.userCount.value); + this.config.duration = parseInt(this.elements.duration.value); + this.config.trafficPattern = this.elements.trafficPattern.value; + this.config.httpMethod = this.elements.httpMethod.value; + this.config.thinkTime = parseInt(this.elements.thinkTime.value); + + const headersText = this.elements.customHeaders.value.trim(); + this.config.customHeaders = headersText ? JSON.parse(headersText) : {}; + + const bodyText = this.elements.requestBody.value.trim(); + this.config.requestBody = bodyText ? JSON.parse(bodyText) : null; + + // Crawler config + if (this.elements.crawlerEnabled) { + this.config.crawlerEnabled = this.elements.crawlerEnabled.checked; + this.config.crawlDepth = parseInt(this.elements.crawlDepth?.value || 2); + this.config.linksPerPage = parseInt( + this.elements.linksPerPage?.value || 10 + ); + } + } + + resetState() { + this.state.elapsedTime = 0; + this.state.activeUsers = 0; + this.state.totalRequests = 0; + this.state.successfulRequests = 0; + this.state.failedRequests = 0; + this.state.responseTimes = []; + this.state.requestsPerSecond = []; + this.state.workers = []; + this.state.userErrorData = []; + this.state.errorThreshold = null; + this.state.errorsByCategory = { + "4xx": 0, + "5xx": 0, + timeout: 0, + network: 0, + }; + this.state.totalBytesSent = 0; + this.state.totalBytesReceived = 0; + this.state.requestHistory = []; + this.state.percentiles = { p50: 0, p95: 0, p99: 0 }; + + // Reset crawler + this.crawler.reset(); + if (this.config.crawlerEnabled) { + this.crawler.urlQueue.push(this.config.targetUrl); + } + + // Reset charts + this.charts.rps.data.labels = []; + this.charts.rps.data.datasets[0].data = []; + this.charts.responseTime.data.labels = []; + this.charts.responseTime.data.datasets[0].data = []; + this.charts.userError.data.labels = []; + this.charts.userError.data.datasets[0].data = []; + this.charts.userError.data.datasets[1].data = []; + this.charts.rps.update("none"); + this.charts.responseTime.update("none"); + this.charts.userError.update("none"); + + // Clear request history table + if (this.elements.requestHistoryBody) { + this.elements.requestHistoryBody.innerHTML = ""; + } + + // Hide results panel + this.elements.resultsPanel.style.display = "none"; + } + + startWorkers() { + const totalUsers = this.config.userCount; + const workerCount = Math.min(Math.ceil(totalUsers / 100), navigator.hardwareConcurrency || 4); + const usersPerWorker = Math.ceil(totalUsers / workerCount); + + for (let i = 0; i < workerCount; i++) { + const worker = new Worker('worker.js'); + const startUser = i * usersPerWorker; + const endUser = Math.min((i + 1) * usersPerWorker, totalUsers); + const workerUsers = Array.from({ length: endUser - startUser }, (_, j) => startUser + j); + + worker.onmessage = (e) => this.handleWorkerMessage(i, e.data); + + worker.postMessage({ + type: 'INIT', + data: { config: this.config } + }); + + worker.postMessage({ + type: 'START', + data: { users: workerUsers } + }); + + this.state.workers.push(worker); + this.state.workerStats.set(i, { + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + bytesSent: 0, + bytesReceived: 0, + errorsByCategory: { "4xx": 0, "5xx": 0, "timeout": 0, "network": 0 }, + responseTimes: [] + }); + } + + this.state.activeUsers = totalUsers; + } + + handleWorkerMessage(workerId, message) { + if (message.type === 'STATS') { + this.state.workerStats.set(workerId, message.data); + this.aggregateStats(); + } else if (message.type === 'LOG') { + this.addToRequestHistory(message.data); + } + } + + aggregateStats() { + let totalRequests = 0; + let successfulRequests = 0; + let failedRequests = 0; + let bytesSent = 0; + let bytesReceived = 0; + let errors = { "4xx": 0, "5xx": 0, "timeout": 0, "network": 0 }; + let allResponseTimes = []; + + for (const stats of this.state.workerStats.values()) { + totalRequests += stats.totalRequests; + successfulRequests += stats.successfulRequests; + failedRequests += stats.failedRequests; + bytesSent += stats.bytesSent; + bytesReceived += stats.bytesReceived; + + errors["4xx"] += stats.errorsByCategory["4xx"]; + errors["5xx"] += stats.errorsByCategory["5xx"]; + errors["timeout"] += stats.errorsByCategory["timeout"]; + errors["network"] += stats.errorsByCategory["network"]; + + if (stats.responseTimes) { + allResponseTimes = allResponseTimes.concat(stats.responseTimes); + } + } + + this.state.totalRequests = totalRequests; + this.state.successfulRequests = successfulRequests; + this.state.failedRequests = failedRequests; + this.state.totalBytesSent = bytesSent; + this.state.totalBytesReceived = bytesReceived; + this.state.errorsByCategory = errors; + this.state.responseTimes = allResponseTimes.slice(-1000); // Sample for percentiles + } + + addToRequestHistory(request) { + this.state.requestHistory.unshift(request); + + // Keep only last 100 + if (this.state.requestHistory.length > 100) { + this.state.requestHistory.pop(); + } + + // Update UI table + if (this.elements.requestHistoryBody) { + const row = document.createElement("tr"); + row.className = request.success ? "success-row" : "error-row"; + row.innerHTML = ` + ${request.timestamp} + ${this.truncateUrl(request.url)} + ${request.status} + ${request.responseTime}ms + `; + + this.elements.requestHistoryBody.insertBefore(row, this.elements.requestHistoryBody.firstChild); + + // Keep only 100 rows in DOM + while (this.elements.requestHistoryBody.children.length > 100) { + this.elements.requestHistoryBody.removeChild(this.elements.requestHistoryBody.lastChild); + } + } + } + + truncateUrl(url) { + if (url.length > 50) { + return url.substring(0, 47) + "..."; + } + return url; + } + + stopWorkers() { + this.state.workers.forEach((worker) => { + worker.terminate(); + }); + this.state.workers = []; + this.state.workerStats.clear(); + } + + calculatePercentiles() { + if (this.state.responseTimes.length > 0) { + this.state.percentiles.p50 = Math.round( + calculatePercentile(this.state.responseTimes, 50) + ); + this.state.percentiles.p95 = Math.round( + calculatePercentile(this.state.responseTimes, 95) + ); + this.state.percentiles.p99 = Math.round( + calculatePercentile(this.state.responseTimes, 99) + ); + } + } + + updateStatistics() { + const now = Date.now(); + + // Check if enough time has passed for a UI update (1000ms throttled) + if (this.state.status === "running" && now - this.state.lastUiUpdate < 1000) { + return; + } + this.state.lastUiUpdate = now; + + const elapsed = Math.floor((now - this.state.startTime) / 1000); + const remaining = Math.max(0, this.config.duration - elapsed); + const progress = Math.min(100, (elapsed / this.config.duration) * 100); + + // Update time displays + this.elements.elapsedTime.textContent = `${elapsed}s`; + this.elements.remainingTime.textContent = `${remaining}s`; + this.elements.progressBar.style.width = `${progress}%`; + + // Update statistics + this.elements.activeUsers.textContent = this.state.activeUsers; + this.elements.totalRequests.textContent = + this.state.totalRequests.toLocaleString(); + this.elements.failedRequests.textContent = + this.state.failedRequests.toLocaleString(); + + // Calculate RPS + const rps = + elapsed > 0 ? Math.round(this.state.totalRequests / elapsed) : 0; + this.elements.requestsPerSec.textContent = rps; + + // Calculate success rate + const successRate = + this.state.totalRequests > 0 + ? ( + (this.state.successfulRequests / this.state.totalRequests) * + 100 + ).toFixed(1) + : 0; + this.elements.successRate.textContent = `${successRate}%`; + + // Calculate average response time + const avgResponseTime = + this.state.responseTimes.length > 0 + ? Math.round( + this.state.responseTimes.reduce((a, b) => a + b, 0) / + this.state.responseTimes.length + ) + : 0; + this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`; + + // Update enhanced metrics + if (this.elements.p50ResponseTime) { + const p50 = Math.round(calculatePercentile(this.state.responseTimes, 50)); + const p95 = Math.round(calculatePercentile(this.state.responseTimes, 95)); + const p99 = Math.round(calculatePercentile(this.state.responseTimes, 99)); + + this.elements.p50ResponseTime.textContent = `${p50}ms`; + this.elements.p95ResponseTime.textContent = `${p95}ms`; + this.elements.p99ResponseTime.textContent = `${p99}ms`; + } + + if (this.elements.errors4xx) { + this.elements.errors4xx.textContent = this.state.errorsByCategory["4xx"]; + this.elements.errors5xx.textContent = this.state.errorsByCategory["5xx"]; + this.elements.errorsTimeout.textContent = + this.state.errorsByCategory["timeout"]; + this.elements.errorsNetwork.textContent = + this.state.errorsByCategory["network"]; + } + + if (this.elements.totalBandwidth) { + const totalBytes = + this.state.totalBytesSent + this.state.totalBytesReceived; + this.elements.totalBandwidth.textContent = formatBytes(totalBytes); + } + } + + updateCharts() { + const now = Date.now(); + const elapsed = Math.floor((now - this.state.startTime) / 1000); + + // Calculate current RPS + const currentRps = + this.state.totalRequests > 0 && elapsed > 0 + ? Math.round(this.state.totalRequests / elapsed) + : 0; + + // Calculate current average response time + const recentResponseTimes = this.state.responseTimes.slice(-100); + const currentAvgResponseTime = + recentResponseTimes.length > 0 + ? Math.round( + recentResponseTimes.reduce((a, b) => a + b, 0) / + recentResponseTimes.length + ) + : 0; + + // Update RPS chart + this.charts.rps.data.labels.push(`${elapsed}s`); + this.charts.rps.data.datasets[0].data.push(currentRps); + + if (this.charts.rps.data.labels.length > 60) { + this.charts.rps.data.labels.shift(); + this.charts.rps.data.datasets[0].data.shift(); + } + + this.charts.rps.update("none"); + + // Update Response Time chart + this.charts.responseTime.data.labels.push(`${elapsed}s`); + this.charts.responseTime.data.datasets[0].data.push(currentAvgResponseTime); + + if (this.charts.responseTime.data.labels.length > 60) { + this.charts.responseTime.data.labels.shift(); + this.charts.responseTime.data.datasets[0].data.shift(); + } + + this.charts.responseTime.update("none"); + + // Calculate current error rate + const currentErrorRate = + this.state.totalRequests > 0 + ? ( + (this.state.failedRequests / this.state.totalRequests) * + 100 + ).toFixed(1) + : 0; + + // Update User/Error chart + this.charts.userError.data.labels.push(`${elapsed}s`); + this.charts.userError.data.datasets[0].data.push(this.state.activeUsers); + this.charts.userError.data.datasets[1].data.push( + parseFloat(currentErrorRate) + ); + + // Track user/error data + this.state.userErrorData.push({ + time: elapsed, + users: this.state.activeUsers, + errorRate: parseFloat(currentErrorRate), + failedRequests: this.state.failedRequests, + }); + + // Detect error threshold + if ( + this.state.errorThreshold === null && + this.state.failedRequests > 0 && + this.state.activeUsers > 0 + ) { + this.state.errorThreshold = { + users: this.state.activeUsers, + time: elapsed, + errorRate: parseFloat(currentErrorRate), + }; + } + + if (this.charts.userError.data.labels.length > 60) { + this.charts.userError.data.labels.shift(); + this.charts.userError.data.datasets[0].data.shift(); + this.charts.userError.data.datasets[1].data.shift(); + } + + this.charts.userError.update("none"); + } + + updateStatus(status) { + this.state.status = status; + const badge = this.elements.statusBadge; + + badge.className = "status-badge"; + + switch (status) { + case "idle": + badge.classList.add("status-idle"); + badge.textContent = "Idle"; + break; + case "running": + badge.classList.add("status-running"); + badge.textContent = "Running"; + break; + case "paused": + badge.classList.add("status-paused"); + badge.textContent = "Paused"; + break; + case "stopped": + badge.classList.add("status-idle"); + badge.textContent = "Completed"; + break; + } + } + + displayResults() { + this.elements.resultsPanel.style.display = "block"; + + const results = this.calculateResults(); + const tbody = this.elements.resultsTableBody; + tbody.innerHTML = ""; + + // Populate results table + Object.entries(results).forEach(([key, value]) => { + const row = document.createElement("tr"); + row.innerHTML = ` + ${key} + ${value} + `; + tbody.appendChild(row); + }); + + // Scroll to results + this.elements.resultsPanel.scrollIntoView({ behavior: "smooth" }); + } + + calculateResults() { + const totalTime = + this.state.elapsedTime || + Math.floor((Date.now() - this.state.startTime) / 1000); + const successRate = + this.state.totalRequests > 0 + ? ( + (this.state.successfulRequests / this.state.totalRequests) * + 100 + ).toFixed(2) + : 0; + + const avgResponseTime = + this.state.responseTimes.length > 0 + ? Math.round( + this.state.responseTimes.reduce((a, b) => a + b, 0) / + this.state.responseTimes.length + ) + : 0; + + const minResponseTime = + this.state.responseTimes.length > 0 + ? Math.round(Math.min(...this.state.responseTimes)) + : 0; + + const maxResponseTime = + this.state.responseTimes.length > 0 + ? Math.round(Math.max(...this.state.responseTimes)) + : 0; + + const rps = + totalTime > 0 ? (this.state.totalRequests / totalTime).toFixed(2) : 0; + + const results = { + "Target URL": this.config.targetUrl, + "Test Duration": `${totalTime} seconds`, + "Concurrent Users": this.config.userCount, + "Traffic Pattern": this.config.trafficPattern, + "Crawler Mode": this.config.crawlerEnabled ? "Enabled" : "Disabled", + "Total Requests": this.state.totalRequests.toLocaleString(), + "Successful Requests": this.state.successfulRequests.toLocaleString(), + "Failed Requests": this.state.failedRequests.toLocaleString(), + "Success Rate": `${successRate}%`, + "Requests per Second": rps, + "Average Response Time": `${avgResponseTime}ms`, + "Min Response Time": `${minResponseTime}ms`, + "Max Response Time": `${maxResponseTime}ms`, + "P50 Response Time": `${this.state.percentiles.p50}ms`, + "P95 Response Time": `${this.state.percentiles.p95}ms`, + "P99 Response Time": `${this.state.percentiles.p99}ms`, + "4xx Errors": this.state.errorsByCategory["4xx"], + "5xx Errors": this.state.errorsByCategory["5xx"], + "Timeout Errors": this.state.errorsByCategory["timeout"], + "Network Errors": this.state.errorsByCategory["network"], + "Total Bandwidth": formatBytes( + this.state.totalBytesSent + this.state.totalBytesReceived + ), + "Data Sent": formatBytes(this.state.totalBytesSent), + "Data Received": formatBytes(this.state.totalBytesReceived), + "HTTP Method": this.config.httpMethod, + "Think Time": `${this.config.thinkTime}ms`, + "Error Threshold": this.state.errorThreshold + ? `${this.state.errorThreshold.users} users at ${this.state.errorThreshold.time}s (${this.state.errorThreshold.errorRate}% error rate)` + : "No errors detected", + }; + + if (this.config.crawlerEnabled) { + results["Unique URLs Visited"] = this.crawler.visitedUrls.size; + } + + return results; + } + + exportResults(format) { + const results = this.calculateResults(); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + + if (format === "json") { + const data = { + config: this.config, + results: results, + requestHistory: this.state.requestHistory.slice(0, 100), + timestamp: new Date().toISOString(), + }; + + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: "application/json", + }); + this.downloadFile(blob, `stress-test-results-${timestamp}.json`); + } else if (format === "csv") { + let csv = "Metric,Value\n"; + Object.entries(results).forEach(([key, value]) => { + csv += `"${key}","${value}"\n`; + }); + + const blob = new Blob([csv], { type: "text/csv" }); + this.downloadFile(blob, `stress-test-results-${timestamp}.csv`); + } + } + + downloadFile(blob, filename) { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} + +// Initialize the application +document.addEventListener("DOMContentLoaded", () => { + new StressTestingTool(); +}); diff --git a/setup-server.sh b/setup-server.sh index 0974d8f..ed4da07 100644 --- a/setup-server.sh +++ b/setup-server.sh @@ -1,23 +1,25 @@ -#!/bin/bash - # setup-server.sh - Initial Setup Script -# 1. Install Global Dependencies +# 1. System Tuning for High Concurrency +echo "Tuning system limits..." +# Increase max open files for high connection counts +if ! grep -q "soft nofile 65535" /etc/security/limits.conf; then + echo "* soft nofile 65535" >> /etc/security/limits.conf + echo "* hard nofile 65535" >> /etc/security/limits.conf +fi +# Apply limits to current session (for the rest of this script) +ulimit -n 65535 + +# 2. Install Global Dependencies echo "Installing PM2..." npm install -g pm2 -# 2. Clone Repository -# Expects: REPO_URL, APP_DIR, GITHUB_TOKEN inside the script or env -# We'll use arguments passed to this script: $1=REPO_URL $2=APP_DIR $3=GITHUB_TOKEN - +# 3. Clone Repository +# ... (rest of cloning logic) REPO_URL="$1" APP_DIR="$2" GITHUB_TOKEN="$3" -# Construct URL with token for auth -# Extract host and path from REPO_URL (assuming https://github.com/user/repo.git) -# We need to insert token: https://TOKEN@github.com/user/repo.git -# Simple replacement: AUTH_REPO_URL="${REPO_URL/https:\/\//https:\/\/$GITHUB_TOKEN@}" echo "Preparing application directory: $APP_DIR" @@ -33,18 +35,20 @@ else cd "$APP_DIR" fi -# 3. Install App Dependencies +# 4. Install App Dependencies echo "Installing application dependencies..." npm install -# 4. Start Application with PM2 +# 5. Start Application with PM2 APP_NAME="website-stress-test" echo "Starting application with PM2 ($APP_NAME)..." -pm2 start proxy-server.js --name "$APP_NAME" --watch --ignore-watch="node_modules" +# Using Node built-in clustering, but PM2 monitors the master +pm2 stop "$APP_NAME" || true +pm2 start proxy-server.js --name "$APP_NAME" --max-memory-restart 1G pm2 save pm2 startup | tail -n 1 | bash # Setup startup script -# 5. Setup Cron Job for Auto-Sync +# 6. Setup Cron Job for Auto-Sync echo "Setting up Cron Job for auto-sync..." SCRIPT_PATH="$APP_DIR/auto-sync.sh" chmod +x "$SCRIPT_PATH" @@ -52,5 +56,5 @@ chmod +x "$SCRIPT_PATH" # Add to crontab if not exists (crontab -l 2>/dev/null; echo "*/5 * * * * $SCRIPT_PATH >> /var/log/app-sync.log 2>&1") | crontab - -echo "✅ Setup Complete! Application is running." +echo "✅ Setup Complete! Application is running with system optimizations." pm2 status diff --git a/start-deployment.ps1 b/start-deployment.ps1 index 53ed00d..23f9049 100644 --- a/start-deployment.ps1 +++ b/start-deployment.ps1 @@ -1,73 +1,73 @@ -# start-deployment.ps1 -# Automates the deployment by reading config, uploading scripts, and executing setup. - -$ErrorActionPreference = "Stop" - -$ConfigPath = "deploy-config.json" - -if (-not (Test-Path $ConfigPath)) { - Write-Error "Configuration file '$ConfigPath' not found. Please copy 'deploy-config.example.json' to '$ConfigPath' and fill in your details." -} - -$Config = Get-Content $ConfigPath | ConvertFrom-Json - -# Validate Config -$Required = @("host", "username", "password", "remotePath", "repoUrl", "githubToken") -foreach ($Key in $Required) { - if (-not $Config.$Key) { - Write-Error "Missing required config key: $Key" - } -} - -$User = $Config.username -$HostName = $Config.host -$Pass = $Config.password -# Note: Using password directly in script is tricky with standard ssh/scp without key. -# We will check if 'sshpass' or 'plink' is available, or guide user to use keys. -# Since the user specifically mentioned providing credentials, they might expect us to use them. -# The template used 'plink -pw $Pass'. We will stick to that if available, or warn. - -# Check for plink -if (Get-Command "plink.exe" -ErrorAction SilentlyContinue) { - Write-Host "Using plink for connection..." - $UsePlink = $true -} -else { - Write-Warning "plink.exe not found. Falling back to standard scp/ssh. You may be prompted for password multiple times." - $UsePlink = $false -} - -$RemoteTmp = "/tmp" -$SetupScript = "setup-server.sh" -$SyncScript = "auto-sync.sh" - -Write-Host "🚀 Starting Deployment to $HostName..." - -# 1. Upload Scripts -Write-Host "Uploading scripts..." -if ($UsePlink) { - echo y | pscp -P 22 -pw $Pass $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript" - echo y | pscp -P 22 -pw $Pass $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript" -} -else { - scp $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript" - scp $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript" -} - -# 2. Execute Setup -Write-Host "Executing setup on remote server..." -$AppDir = $Config.remotePath -$Repo = $Config.repoUrl -$Token = $Config.githubToken - -# Make scripts executable and run setup -$RemoteCmd = "chmod +x $RemoteTmp/$SetupScript $RemoteTmp/$SyncScript; $RemoteTmp/$SetupScript '$Repo' '$AppDir' '$Token'; rm $RemoteTmp/$SetupScript" - -if ($UsePlink) { - echo y | plink -ssh -P 22 -t -pw $Pass "$User@$HostName" $RemoteCmd -} -else { - ssh -t "$User@$HostName" $RemoteCmd -} - -Write-Host "🎉 Deployment command sent!" +# start-deployment.ps1 +# Automates the deployment by reading config, uploading scripts, and executing setup. + +$ErrorActionPreference = "Stop" + +$ConfigPath = "deploy-config.json" + +if (-not (Test-Path $ConfigPath)) { + Write-Error "Configuration file '$ConfigPath' not found. Please copy 'deploy-config.example.json' to '$ConfigPath' and fill in your details." +} + +$Config = Get-Content $ConfigPath | ConvertFrom-Json + +# Validate Config +$Required = @("host", "username", "password", "remotePath", "repoUrl", "githubToken") +foreach ($Key in $Required) { + if (-not $Config.$Key) { + Write-Error "Missing required config key: $Key" + } +} + +$User = $Config.username +$HostName = $Config.host +$Pass = $Config.password +# Note: Using password directly in script is tricky with standard ssh/scp without key. +# We will check if 'sshpass' or 'plink' is available, or guide user to use keys. +# Since the user specifically mentioned providing credentials, they might expect us to use them. +# The template used 'plink -pw $Pass'. We will stick to that if available, or warn. + +# Check for plink +if (Get-Command "plink.exe" -ErrorAction SilentlyContinue) { + Write-Host "Using plink for connection..." + $UsePlink = $true +} +else { + Write-Warning "plink.exe not found. Falling back to standard scp/ssh. You may be prompted for password multiple times." + $UsePlink = $false +} + +$RemoteTmp = "/tmp" +$SetupScript = "setup-server.sh" +$SyncScript = "auto-sync.sh" + +Write-Host "🚀 Starting Deployment to $HostName..." + +# 1. Upload Scripts +Write-Host "Uploading scripts..." +if ($UsePlink) { + echo y | pscp -P 22 -pw $Pass $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript" + echo y | pscp -P 22 -pw $Pass $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript" +} +else { + scp $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript" + scp $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript" +} + +# 2. Execute Setup +Write-Host "Executing setup on remote server..." +$AppDir = $Config.remotePath +$Repo = $Config.repoUrl +$Token = $Config.githubToken + +# Make scripts executable and run setup +$RemoteCmd = "chmod +x $RemoteTmp/$SetupScript $RemoteTmp/$SyncScript; $RemoteTmp/$SetupScript '$Repo' '$AppDir' '$Token'; rm $RemoteTmp/$SetupScript" + +if ($UsePlink) { + echo y | plink -ssh -P 22 -t -pw $Pass "$User@$HostName" $RemoteCmd +} +else { + ssh -t "$User@$HostName" $RemoteCmd +} + +Write-Host "🎉 Deployment command sent!" diff --git a/styles.css b/styles.css index 9e6cf7c..a3d9631 100644 --- a/styles.css +++ b/styles.css @@ -1,1101 +1,1101 @@ -/* =================================== - STRESS TESTING TOOL - DESIGN SYSTEM - Enhanced with Light Theme Support - =================================== */ - -/* Import Modern Typography */ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"); - -/* =================================== - CSS VARIABLES - DESIGN TOKENS - =================================== */ -:root { - /* Dark Theme Colors */ - --color-bg-primary: #0a0e1a; - --color-bg-secondary: #131829; - --color-bg-tertiary: #1a2035; - --color-bg-glass: rgba(26, 32, 53, 0.7); - --color-bg-glass-hover: rgba(26, 32, 53, 0.85); - - /* Accent Colors */ - --color-accent-primary: #6366f1; - --color-accent-secondary: #8b5cf6; - --color-accent-success: #10b981; - --color-accent-warning: #f59e0b; - --color-accent-danger: #ef4444; - --color-accent-info: #3b82f6; - - /* Gradients */ - --gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); - --gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%); - --gradient-danger: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); - --gradient-warning: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); - - /* Text Colors */ - --color-text-primary: #f8fafc; - --color-text-secondary: #cbd5e1; - --color-text-tertiary: #94a3b8; - --color-text-muted: #64748b; - - /* Border & Shadow */ - --color-border: rgba(148, 163, 184, 0.1); - --color-border-hover: rgba(148, 163, 184, 0.2); - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); - --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.3); - - /* Spacing */ - --spacing-xs: 0.5rem; - --spacing-sm: 0.75rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --spacing-2xl: 3rem; - - /* Border Radius */ - --radius-sm: 0.375rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; - - /* Typography */ - --font-primary: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", - sans-serif; - --font-mono: "JetBrains Mono", "Courier New", monospace; - - /* Transitions */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Light Theme */ -[data-theme="light"] { - --color-bg-primary: #f8fafc; - --color-bg-secondary: #f1f5f9; - --color-bg-tertiary: #e2e8f0; - --color-bg-glass: rgba(255, 255, 255, 0.8); - --color-bg-glass-hover: rgba(255, 255, 255, 0.95); - - --color-text-primary: #0f172a; - --color-text-secondary: #334155; - --color-text-tertiary: #475569; - --color-text-muted: #64748b; - - --color-border: rgba(148, 163, 184, 0.2); - --color-border-hover: rgba(148, 163, 184, 0.3); - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.15); -} - -/* =================================== - GLOBAL RESET & BASE STYLES - =================================== */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html { - font-size: 16px; - scroll-behavior: smooth; -} - -body { - font-family: var(--font-primary); - background: var(--color-bg-primary); - color: var(--color-text-primary); - line-height: 1.6; - min-height: 100vh; - overflow-x: hidden; - position: relative; - transition: background var(--transition-base), color var(--transition-base); -} - -/* Animated Background */ -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: radial-gradient( - circle at 20% 30%, - rgba(99, 102, 241, 0.1) 0%, - transparent 50% - ), - radial-gradient( - circle at 80% 70%, - rgba(139, 92, 246, 0.1) 0%, - transparent 50% - ); - pointer-events: none; - z-index: 0; -} - -/* =================================== - LAYOUT STRUCTURE - =================================== */ -.container { - max-width: 1400px; - margin: 0 auto; - padding: var(--spacing-xl); - position: relative; - z-index: 1; -} - -/* Header */ -.header { - text-align: center; - margin-bottom: var(--spacing-lg); - animation: fadeInDown 0.6s ease-out; - position: relative; - display: block; -} - -.header-content { - position: relative; - display: flex; - flex-direction: column; - align-items: center; -} - -.title { - font-size: 3rem; - font-weight: 700; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: var(--spacing-xs); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0; - line-height: 1.2; -} - -.title-icon { - width: 2.5rem; - height: 2.5rem; - stroke: url(#gradient); - /* Note: SVG needs defs for this, or we use currentColor */ - color: var(--color-accent-primary); - filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.5)); - margin-bottom: var(--spacing-xs); -} - -.subtitle { - font-size: 1.125rem; - color: var(--color-text-secondary); - font-weight: 300; - margin-bottom: var(--spacing-sm); -} - -.header-controls { - position: absolute; - top: 0; - right: 0; -} - -/* YouTube Link */ -.youtube-link { - display: inline-flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-md); - background: linear-gradient(135deg, #ff0000 0%, #cc0000 100%); - color: white; - text-decoration: none; - border-radius: var(--radius-md); - font-weight: 600; - font-size: 0.875rem; - box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3); - transition: all var(--transition-base); - animation: pulse-glow 2s ease-in-out infinite; - margin-top: var(--spacing-md); -} - -.youtube-link:hover { - transform: translateY(-2px) scale(1.03); - box-shadow: 0 4px 15px rgba(255, 0, 0, 0.5); - animation: none; - color: white; -} - -.youtube-icon { - width: 1.25rem; - height: 1.25rem; - flex-shrink: 0; -} - -@keyframes pulse-glow { - 0%, - 100% { - box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3); - } - - 50% { - box-shadow: 0 2px 15px rgba(255, 0, 0, 0.5); - } -} - -/* Footer */ -.footer { - text-align: center; - margin-top: var(--spacing-2xl); - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-border); - color: var(--color-text-muted); - font-size: 0.875rem; -} - -/* Main Grid Layout */ -.main-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-xl); - margin-bottom: var(--spacing-xl); -} - -/* =================================== - GLASS PANEL COMPONENT - =================================== */ -.panel { - background: var(--color-bg-glass); - backdrop-filter: blur(20px); - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - padding: var(--spacing-xl); - box-shadow: var(--shadow-md); - transition: all var(--transition-base); -} - -.panel:hover { - background: var(--color-bg-glass-hover); - border-color: var(--color-border-hover); - box-shadow: var(--shadow-lg); -} - -.panel-header { - display: flex; - align-items: center; - gap: var(--spacing-md); - margin-bottom: var(--spacing-lg); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--color-border); -} - -.panel-icon { - width: 2.5rem; - height: 2.5rem; - display: flex; - align-items: center; - justify-content: center; - background: var(--gradient-primary); - border-radius: var(--radius-md); - font-size: 1.25rem; -} - -.panel-title { - font-size: 1.5rem; - font-weight: 700; - color: var(--color-text-primary); -} - -.panel-section { - margin-top: var(--spacing-lg); -} - -.section-title { - font-size: 1.125rem; - font-weight: 600; - color: var(--color-text-secondary); - margin-bottom: var(--spacing-md); -} - -/* =================================== - PRESET CONTROLS - =================================== */ -.preset-controls { - display: flex; - gap: var(--spacing-md); - margin-bottom: var(--spacing-md); -} - -.preset-controls select { - flex: 1; -} - -.keyboard-shortcuts { - padding: var(--spacing-sm); - background: var(--color-bg-tertiary); - border-radius: var(--radius-md); - text-align: center; -} - -.keyboard-shortcuts small { - color: var(--color-text-tertiary); -} - -/* =================================== - FORM CONTROLS - =================================== */ -.form-group { - margin-bottom: var(--spacing-lg); -} - -.form-label { - display: block; - font-size: 0.875rem; - font-weight: 600; - color: var(--color-text-secondary); - margin-bottom: var(--spacing-xs); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.form-input, -.form-select, -.form-textarea { - width: 100%; - padding: 0.75rem 1rem; - background: var(--color-bg-tertiary); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - color: var(--color-text-primary); - font-family: var(--font-primary); - font-size: 0.9375rem; - transition: all var(--transition-base); -} - -.form-input:focus, -.form-select:focus, -.form-textarea:focus { - outline: none; - border-color: var(--color-accent-primary); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); -} - -.form-input::placeholder { - color: var(--color-text-muted); -} - -.form-textarea { - resize: vertical; - min-height: 100px; - font-family: var(--font-mono); - font-size: 0.875rem; -} - -.help-text { - display: block; - margin-top: var(--spacing-xs); - font-size: 0.8rem; - color: var(--color-text-muted); -} - -/* Checkbox */ -.checkbox-label { - display: flex; - align-items: center; - gap: var(--spacing-sm); - cursor: pointer; - font-weight: 500; -} - -.form-checkbox { - width: 1.25rem; - height: 1.25rem; - cursor: pointer; -} - -/* Crawler Section */ -.crawler-section { - background: var(--color-bg-tertiary); - padding: var(--spacing-md); - border-radius: var(--radius-md); - border: 1px solid var(--color-border); -} - -.crawler-settings { - margin-top: var(--spacing-md); - padding: var(--spacing-md); - background: var(--color-bg-secondary); - border-radius: var(--radius-md); - border: 1px dashed var(--color-border); -} - -/* Range Slider */ -.form-range { - width: 100%; - height: 6px; - background: var(--color-bg-tertiary); -} - -.form-range::-moz-range-thumb { - width: 20px; - height: 20px; - background: var(--gradient-primary); - border-radius: 50%; - cursor: pointer; - border: none; - box-shadow: var(--shadow-sm); - transition: all var(--transition-fast); -} - -.range-value { - display: inline-block; - margin-left: var(--spacing-sm); - padding: 0.25rem 0.75rem; - background: var(--color-bg-tertiary); - border-radius: var(--radius-sm); - font-weight: 600; - font-size: 0.875rem; - color: var(--color-accent-primary); -} - -/* =================================== - BUTTONS - =================================== */ -.btn { - padding: 0.875rem 1.75rem; - border: none; - border-radius: var(--radius-md); - font-family: var(--font-primary); - font-size: 0.9375rem; - font-weight: 600; - cursor: pointer; - transition: all var(--transition-base); - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-xs); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-sm { - padding: 0.5rem 1rem; - font-size: 0.875rem; -} - -.btn-primary { - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-sm); -} - -.btn-primary:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: var(--shadow-glow), var(--shadow-md); -} - -.btn-primary:active:not(:disabled) { - transform: translateY(0); -} - -.btn-success { - background: var(--gradient-success); - color: white; - box-shadow: var(--shadow-sm); -} - -.btn-success:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 0 20px rgba(16, 185, 129, 0.3), var(--shadow-md); -} - -.btn-danger { - background: var(--gradient-danger); - color: white; - box-shadow: var(--shadow-sm); -} - -.btn-danger:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 0 20px rgba(239, 68, 68, 0.3), var(--shadow-md); -} - -.btn-warning { - background: var(--gradient-warning); - color: white; - box-shadow: var(--shadow-sm); -} - -.btn-warning:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 0 20px rgba(245, 158, 11, 0.3), var(--shadow-md); -} - -.btn-secondary { - background: var(--color-bg-tertiary); - color: var(--color-text-primary); - border: 1px solid var(--color-border); -} - -.btn-secondary:hover:not(:disabled) { - background: var(--color-bg-glass); - border-color: var(--color-border-hover); -} - -/* Button Group */ -.btn-group { - display: flex; - gap: var(--spacing-md); - flex-wrap: wrap; -} - -/* =================================== - STATISTICS CARDS - =================================== */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--spacing-md); - margin-bottom: var(--spacing-xl); -} - -.stat-card { - background: var(--color-bg-tertiary); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - transition: all var(--transition-base); -} - -.stat-card:hover { - border-color: var(--color-border-hover); - transform: translateY(-2px); - box-shadow: var(--shadow-md); -} - -.stat-card-large { - background: var(--color-bg-tertiary); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: var(--spacing-xl); - text-align: center; -} - -.stat-label { - font-size: 0.75rem; - font-weight: 600; - color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: var(--spacing-xs); -} - -.stat-value { - font-size: 2rem; - font-weight: 800; - color: var(--color-text-primary); - line-height: 1; -} - -.stat-value.success { - color: var(--color-accent-success); -} - -.stat-value.danger { - color: var(--color-accent-danger); -} - -.stat-value.warning { - color: var(--color-accent-warning); -} - -.stat-value.info { - color: var(--color-accent-info); -} - -/* =================================== - CHARTS & VISUALIZATIONS - =================================== */ -.chart-container { - position: relative; - height: 300px; - margin-top: var(--spacing-lg); - padding: var(--spacing-md); - background: var(--color-bg-tertiary); - border-radius: var(--radius-lg); - border: 1px solid var(--color-border); -} - -/* =================================== - REQUEST HISTORY TABLE - =================================== */ -.request-history-container { - max-height: 400px; - overflow-y: auto; - border-radius: var(--radius-md); - border: 1px solid var(--color-border); -} - -.request-history-table { - width: 100%; - border-collapse: collapse; - font-size: 0.875rem; -} - -.request-history-table thead { - position: sticky; - top: 0; - background: var(--color-bg-tertiary); - z-index: 10; -} - -.request-history-table th { - padding: var(--spacing-md); - text-align: left; - font-weight: 600; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; - font-size: 0.75rem; - border-bottom: 2px solid var(--color-border); -} - -.request-history-table td { - padding: var(--spacing-sm) var(--spacing-md); - border-bottom: 1px solid var(--color-border); - color: var(--color-text-primary); -} - -.request-history-table tr.success-row { - background: rgba(16, 185, 129, 0.05); -} - -.request-history-table tr.error-row { - background: rgba(239, 68, 68, 0.05); -} - -.request-history-table tr:hover { - background: var(--color-bg-glass); -} - -.url-cell { - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.status-code { - display: inline-block; - padding: 0.25rem 0.5rem; - border-radius: var(--radius-sm); - font-weight: 600; - font-size: 0.75rem; -} - -.status-code.success { - background: rgba(16, 185, 129, 0.1); - color: var(--color-accent-success); -} - -.status-code.error { - background: rgba(239, 68, 68, 0.1); - color: var(--color-accent-danger); -} - -/* =================================== - STATUS INDICATORS - =================================== */ -.status-badge { - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); - padding: 0.375rem 0.875rem; - border-radius: var(--radius-lg); - font-size: 0.875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.status-badge::before { - content: ""; - width: 8px; - height: 8px; - border-radius: 50%; - animation: pulse 2s infinite; -} - -.status-idle { - background: rgba(148, 163, 184, 0.1); - color: var(--color-text-tertiary); -} - -.status-idle::before { - background: var(--color-text-tertiary); -} - -.status-running { - background: rgba(16, 185, 129, 0.1); - color: var(--color-accent-success); -} - -.status-running::before { - background: var(--color-accent-success); -} - -.status-paused { - background: rgba(245, 158, 11, 0.1); - color: var(--color-accent-warning); -} - -.status-paused::before { - background: var(--color-accent-warning); -} - -.status-error { - background: rgba(239, 68, 68, 0.1); - color: var(--color-accent-danger); -} - -.status-error::before { - background: var(--color-accent-danger); -} - -/* =================================== - PROGRESS BAR - =================================== */ -.progress-container { - width: 100%; - height: 8px; - background: var(--color-bg-tertiary); - border-radius: var(--radius-lg); - overflow: hidden; - margin-top: var(--spacing-md); -} - -.progress-bar { - height: 100%; - background: var(--gradient-primary); - border-radius: var(--radius-lg); - transition: width var(--transition-base); - position: relative; - overflow: hidden; -} - -.progress-bar::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.3), - transparent - ); - animation: shimmer 2s infinite; -} - -/* =================================== - RESULTS TABLE - =================================== */ -.results-table { - width: 100%; - border-collapse: collapse; - margin-top: var(--spacing-lg); - font-size: 0.875rem; -} - -.results-table th { - background: var(--color-bg-tertiary); - padding: var(--spacing-md); - text-align: left; - font-weight: 600; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; - font-size: 0.75rem; - border-bottom: 2px solid var(--color-border); -} - -.results-table td { - padding: var(--spacing-md); - border-bottom: 1px solid var(--color-border); - color: var(--color-text-primary); -} - -.results-table tr:hover { - background: var(--color-bg-tertiary); -} - -.results-table code { - font-family: var(--font-mono); - font-size: 0.8125rem; - color: var(--color-accent-primary); -} - -/* =================================== - ACCORDION - =================================== */ -.accordion { - margin-bottom: var(--spacing-md); -} - -.accordion-header { - width: 100%; - padding: var(--spacing-md); - background: var(--color-bg-tertiary); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - color: var(--color-text-primary); - font-family: var(--font-primary); - font-size: 0.9375rem; - font-weight: 600; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: all var(--transition-base); -} - -.accordion-header:hover { - background: var(--color-bg-glass); - border-color: var(--color-border-hover); -} - -.accordion-icon { - transition: transform var(--transition-base); -} - -.accordion-header.active .accordion-icon { - transform: rotate(180deg); -} - -.accordion-content { - max-height: 0; - overflow: hidden; - transition: max-height var(--transition-slow); -} - -.accordion-content.active { - max-height: 1000px; - padding-top: var(--spacing-md); -} - -/* =================================== - ANIMATIONS - =================================== */ -@keyframes pulse { - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0.5; - } -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - - 100% { - transform: translateX(100%); - } -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes fadeInDown { - from { - opacity: 0; - transform: translateY(-20px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.fade-in { - animation: fadeIn var(--transition-base) ease-out; -} - -/* =================================== - RESPONSIVE DESIGN - =================================== */ -@media (max-width: 1024px) { - .main-grid { - grid-template-columns: 1fr; - } - - .stats-grid { - grid-template-columns: repeat(2, 1fr); - } - - .header { - flex-direction: column; - align-items: flex-start; - gap: var(--spacing-md); - } - - .preset-controls { - flex-direction: column; - } -} - -@media (max-width: 640px) { - .container { - padding: var(--spacing-md); - } - - .header h1 { - font-size: 2rem; - } - - .header p { - font-size: 1rem; - } - - .panel { - padding: var(--spacing-md); - } - - .stats-grid { - grid-template-columns: 1fr; - } - - .btn-group { - flex-direction: column; - } - - .btn { - width: 100%; - } - - .request-history-table { - font-size: 0.75rem; - } - - .request-history-table th, - .request-history-table td { - padding: var(--spacing-xs); - } -} - -/* =================================== - UTILITY CLASSES - =================================== */ -.text-center { - text-align: center; -} - -.mt-1 { - margin-top: var(--spacing-xs); -} - -.mt-2 { - margin-top: var(--spacing-sm); -} - -.mt-3 { - margin-top: var(--spacing-md); -} - -.mt-4 { - margin-top: var(--spacing-lg); -} - -.mb-1 { - margin-bottom: var(--spacing-xs); -} - -.mb-2 { - margin-bottom: var(--spacing-sm); -} - -.mb-3 { - margin-bottom: var(--spacing-md); -} - -.mb-4 { - margin-bottom: var(--spacing-lg); -} - -.hidden { - display: none; -} - -.full-width { - grid-column: 1 / -1; -} - -/* =================================== - GIT INFO PILL - =================================== */ -.git-info-pill { - position: fixed; - bottom: 20px; - right: 20px; - background-color: rgba(10, 14, 26, 0.95); /* High opacity dark background */ - border: 1px solid var(--color-accent-primary); /* Distinct purple border */ - padding: 10px 20px; - border-radius: 9999px; - font-family: var(--font-mono); - font-size: 0.9rem; - color: white; /* Bright text */ - display: flex; - align-items: center; - gap: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Strong shadow */ - z-index: 99999; /* Maximum Z-index */ - backdrop-filter: blur(4px); - transition: all var(--transition-base); - cursor: default; -} - -.git-info-pill:hover { - background-color: rgba(10, 14, 26, 1); - border-color: var(--color-accent-secondary); - box-shadow: 0 0 15px rgba(99, 102, 241, 0.5); /* Glow on hover */ - transform: translateY(-2px); -} - -.git-commit { - color: var(--color-accent-primary); /* Purple highlight */ - font-weight: 700; - letter-spacing: 0.05em; -} - -.git-separator { - color: var(--color-text-tertiary); - opacity: 0.5; -} - -.git-date { - color: #cbd5e1; /* Lighter than muted */ - font-weight: 500; -} +/* =================================== + STRESS TESTING TOOL - DESIGN SYSTEM + Enhanced with Light Theme Support + =================================== */ + +/* Import Modern Typography */ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"); + +/* =================================== + CSS VARIABLES - DESIGN TOKENS + =================================== */ +:root { + /* Dark Theme Colors */ + --color-bg-primary: #0a0e1a; + --color-bg-secondary: #131829; + --color-bg-tertiary: #1a2035; + --color-bg-glass: rgba(26, 32, 53, 0.7); + --color-bg-glass-hover: rgba(26, 32, 53, 0.85); + + /* Accent Colors */ + --color-accent-primary: #6366f1; + --color-accent-secondary: #8b5cf6; + --color-accent-success: #10b981; + --color-accent-warning: #f59e0b; + --color-accent-danger: #ef4444; + --color-accent-info: #3b82f6; + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + --gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%); + --gradient-danger: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + --gradient-warning: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + + /* Text Colors */ + --color-text-primary: #f8fafc; + --color-text-secondary: #cbd5e1; + --color-text-tertiary: #94a3b8; + --color-text-muted: #64748b; + + /* Border & Shadow */ + --color-border: rgba(148, 163, 184, 0.1); + --color-border-hover: rgba(148, 163, 184, 0.2); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.3); + + /* Spacing */ + --spacing-xs: 0.5rem; + --spacing-sm: 0.75rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + /* Border Radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + + /* Typography */ + --font-primary: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + --font-mono: "JetBrains Mono", "Courier New", monospace; + + /* Transitions */ + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Light Theme */ +[data-theme="light"] { + --color-bg-primary: #f8fafc; + --color-bg-secondary: #f1f5f9; + --color-bg-tertiary: #e2e8f0; + --color-bg-glass: rgba(255, 255, 255, 0.8); + --color-bg-glass-hover: rgba(255, 255, 255, 0.95); + + --color-text-primary: #0f172a; + --color-text-secondary: #334155; + --color-text-tertiary: #475569; + --color-text-muted: #64748b; + + --color-border: rgba(148, 163, 184, 0.2); + --color-border-hover: rgba(148, 163, 184, 0.3); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.15); +} + +/* =================================== + GLOBAL RESET & BASE STYLES + =================================== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-primary); + background: var(--color-bg-primary); + color: var(--color-text-primary); + line-height: 1.6; + min-height: 100vh; + overflow-x: hidden; + position: relative; + transition: background var(--transition-base), color var(--transition-base); +} + +/* Animated Background */ +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient( + circle at 20% 30%, + rgba(99, 102, 241, 0.1) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 70%, + rgba(139, 92, 246, 0.1) 0%, + transparent 50% + ); + pointer-events: none; + z-index: 0; +} + +/* =================================== + LAYOUT STRUCTURE + =================================== */ +.container { + max-width: 1400px; + margin: 0 auto; + padding: var(--spacing-xl); + position: relative; + z-index: 1; +} + +/* Header */ +.header { + text-align: center; + margin-bottom: var(--spacing-lg); + animation: fadeInDown 0.6s ease-out; + position: relative; + display: block; +} + +.header-content { + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} + +.title { + font-size: 3rem; + font-weight: 700; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: var(--spacing-xs); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0; + line-height: 1.2; +} + +.title-icon { + width: 2.5rem; + height: 2.5rem; + stroke: url(#gradient); + /* Note: SVG needs defs for this, or we use currentColor */ + color: var(--color-accent-primary); + filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.5)); + margin-bottom: var(--spacing-xs); +} + +.subtitle { + font-size: 1.125rem; + color: var(--color-text-secondary); + font-weight: 300; + margin-bottom: var(--spacing-sm); +} + +.header-controls { + position: absolute; + top: 0; + right: 0; +} + +/* YouTube Link */ +.youtube-link { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + background: linear-gradient(135deg, #ff0000 0%, #cc0000 100%); + color: white; + text-decoration: none; + border-radius: var(--radius-md); + font-weight: 600; + font-size: 0.875rem; + box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3); + transition: all var(--transition-base); + animation: pulse-glow 2s ease-in-out infinite; + margin-top: var(--spacing-md); +} + +.youtube-link:hover { + transform: translateY(-2px) scale(1.03); + box-shadow: 0 4px 15px rgba(255, 0, 0, 0.5); + animation: none; + color: white; +} + +.youtube-icon { + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; +} + +@keyframes pulse-glow { + 0%, + 100% { + box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3); + } + + 50% { + box-shadow: 0 2px 15px rgba(255, 0, 0, 0.5); + } +} + +/* Footer */ +.footer { + text-align: center; + margin-top: var(--spacing-2xl); + padding: var(--spacing-xl) 0; + border-top: 1px solid var(--color-border); + color: var(--color-text-muted); + font-size: 0.875rem; +} + +/* Main Grid Layout */ +.main-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-xl); + margin-bottom: var(--spacing-xl); +} + +/* =================================== + GLASS PANEL COMPONENT + =================================== */ +.panel { + background: var(--color-bg-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + padding: var(--spacing-xl); + box-shadow: var(--shadow-md); + transition: all var(--transition-base); +} + +.panel:hover { + background: var(--color-bg-glass-hover); + border-color: var(--color-border-hover); + box-shadow: var(--shadow-lg); +} + +.panel-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.panel-icon { + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--gradient-primary); + border-radius: var(--radius-md); + font-size: 1.25rem; +} + +.panel-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-text-primary); +} + +.panel-section { + margin-top: var(--spacing-lg); +} + +.section-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-secondary); + margin-bottom: var(--spacing-md); +} + +/* =================================== + PRESET CONTROLS + =================================== */ +.preset-controls { + display: flex; + gap: var(--spacing-md); + margin-bottom: var(--spacing-md); +} + +.preset-controls select { + flex: 1; +} + +.keyboard-shortcuts { + padding: var(--spacing-sm); + background: var(--color-bg-tertiary); + border-radius: var(--radius-md); + text-align: center; +} + +.keyboard-shortcuts small { + color: var(--color-text-tertiary); +} + +/* =================================== + FORM CONTROLS + =================================== */ +.form-group { + margin-bottom: var(--spacing-lg); +} + +.form-label { + display: block; + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text-secondary); + margin-bottom: var(--spacing-xs); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: 0.75rem 1rem; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text-primary); + font-family: var(--font-primary); + font-size: 0.9375rem; + transition: all var(--transition-base); +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--color-accent-primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +.form-input::placeholder { + color: var(--color-text-muted); +} + +.form-textarea { + resize: vertical; + min-height: 100px; + font-family: var(--font-mono); + font-size: 0.875rem; +} + +.help-text { + display: block; + margin-top: var(--spacing-xs); + font-size: 0.8rem; + color: var(--color-text-muted); +} + +/* Checkbox */ +.checkbox-label { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; + font-weight: 500; +} + +.form-checkbox { + width: 1.25rem; + height: 1.25rem; + cursor: pointer; +} + +/* Crawler Section */ +.crawler-section { + background: var(--color-bg-tertiary); + padding: var(--spacing-md); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +.crawler-settings { + margin-top: var(--spacing-md); + padding: var(--spacing-md); + background: var(--color-bg-secondary); + border-radius: var(--radius-md); + border: 1px dashed var(--color-border); +} + +/* Range Slider */ +.form-range { + width: 100%; + height: 6px; + background: var(--color-bg-tertiary); +} + +.form-range::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--gradient-primary); + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: var(--shadow-sm); + transition: all var(--transition-fast); +} + +.range-value { + display: inline-block; + margin-left: var(--spacing-sm); + padding: 0.25rem 0.75rem; + background: var(--color-bg-tertiary); + border-radius: var(--radius-sm); + font-weight: 600; + font-size: 0.875rem; + color: var(--color-accent-primary); +} + +/* =================================== + BUTTONS + =================================== */ +.btn { + padding: 0.875rem 1.75rem; + border: none; + border-radius: var(--radius-md); + font-family: var(--font-primary); + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-base); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.btn-primary { + background: var(--gradient-primary); + color: white; + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: var(--shadow-glow), var(--shadow-md); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0); +} + +.btn-success { + background: var(--gradient-success); + color: white; + box-shadow: var(--shadow-sm); +} + +.btn-success:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 0 20px rgba(16, 185, 129, 0.3), var(--shadow-md); +} + +.btn-danger { + background: var(--gradient-danger); + color: white; + box-shadow: var(--shadow-sm); +} + +.btn-danger:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 0 20px rgba(239, 68, 68, 0.3), var(--shadow-md); +} + +.btn-warning { + background: var(--gradient-warning); + color: white; + box-shadow: var(--shadow-sm); +} + +.btn-warning:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 0 20px rgba(245, 158, 11, 0.3), var(--shadow-md); +} + +.btn-secondary { + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + border: 1px solid var(--color-border); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--color-bg-glass); + border-color: var(--color-border-hover); +} + +/* Button Group */ +.btn-group { + display: flex; + gap: var(--spacing-md); + flex-wrap: wrap; +} + +/* =================================== + STATISTICS CARDS + =================================== */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-md); + margin-bottom: var(--spacing-xl); +} + +.stat-card { + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + transition: all var(--transition-base); +} + +.stat-card:hover { + border-color: var(--color-border-hover); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.stat-card-large { + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + text-align: center; +} + +.stat-label { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: var(--spacing-xs); +} + +.stat-value { + font-size: 2rem; + font-weight: 800; + color: var(--color-text-primary); + line-height: 1; +} + +.stat-value.success { + color: var(--color-accent-success); +} + +.stat-value.danger { + color: var(--color-accent-danger); +} + +.stat-value.warning { + color: var(--color-accent-warning); +} + +.stat-value.info { + color: var(--color-accent-info); +} + +/* =================================== + CHARTS & VISUALIZATIONS + =================================== */ +.chart-container { + position: relative; + height: 300px; + margin-top: var(--spacing-lg); + padding: var(--spacing-md); + background: var(--color-bg-tertiary); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); +} + +/* =================================== + REQUEST HISTORY TABLE + =================================== */ +.request-history-container { + max-height: 400px; + overflow-y: auto; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +.request-history-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.request-history-table thead { + position: sticky; + top: 0; + background: var(--color-bg-tertiary); + z-index: 10; +} + +.request-history-table th { + padding: var(--spacing-md); + text-align: left; + font-weight: 600; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.75rem; + border-bottom: 2px solid var(--color-border); +} + +.request-history-table td { + padding: var(--spacing-sm) var(--spacing-md); + border-bottom: 1px solid var(--color-border); + color: var(--color-text-primary); +} + +.request-history-table tr.success-row { + background: rgba(16, 185, 129, 0.05); +} + +.request-history-table tr.error-row { + background: rgba(239, 68, 68, 0.05); +} + +.request-history-table tr:hover { + background: var(--color-bg-glass); +} + +.url-cell { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-code { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + font-weight: 600; + font-size: 0.75rem; +} + +.status-code.success { + background: rgba(16, 185, 129, 0.1); + color: var(--color-accent-success); +} + +.status-code.error { + background: rgba(239, 68, 68, 0.1); + color: var(--color-accent-danger); +} + +/* =================================== + STATUS INDICATORS + =================================== */ +.status-badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + padding: 0.375rem 0.875rem; + border-radius: var(--radius-lg); + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-badge::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + animation: pulse 2s infinite; +} + +.status-idle { + background: rgba(148, 163, 184, 0.1); + color: var(--color-text-tertiary); +} + +.status-idle::before { + background: var(--color-text-tertiary); +} + +.status-running { + background: rgba(16, 185, 129, 0.1); + color: var(--color-accent-success); +} + +.status-running::before { + background: var(--color-accent-success); +} + +.status-paused { + background: rgba(245, 158, 11, 0.1); + color: var(--color-accent-warning); +} + +.status-paused::before { + background: var(--color-accent-warning); +} + +.status-error { + background: rgba(239, 68, 68, 0.1); + color: var(--color-accent-danger); +} + +.status-error::before { + background: var(--color-accent-danger); +} + +/* =================================== + PROGRESS BAR + =================================== */ +.progress-container { + width: 100%; + height: 8px; + background: var(--color-bg-tertiary); + border-radius: var(--radius-lg); + overflow: hidden; + margin-top: var(--spacing-md); +} + +.progress-bar { + height: 100%; + background: var(--gradient-primary); + border-radius: var(--radius-lg); + transition: width var(--transition-base); + position: relative; + overflow: hidden; +} + +.progress-bar::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + animation: shimmer 2s infinite; +} + +/* =================================== + RESULTS TABLE + =================================== */ +.results-table { + width: 100%; + border-collapse: collapse; + margin-top: var(--spacing-lg); + font-size: 0.875rem; +} + +.results-table th { + background: var(--color-bg-tertiary); + padding: var(--spacing-md); + text-align: left; + font-weight: 600; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.75rem; + border-bottom: 2px solid var(--color-border); +} + +.results-table td { + padding: var(--spacing-md); + border-bottom: 1px solid var(--color-border); + color: var(--color-text-primary); +} + +.results-table tr:hover { + background: var(--color-bg-tertiary); +} + +.results-table code { + font-family: var(--font-mono); + font-size: 0.8125rem; + color: var(--color-accent-primary); +} + +/* =================================== + ACCORDION + =================================== */ +.accordion { + margin-bottom: var(--spacing-md); +} + +.accordion-header { + width: 100%; + padding: var(--spacing-md); + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text-primary); + font-family: var(--font-primary); + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + transition: all var(--transition-base); +} + +.accordion-header:hover { + background: var(--color-bg-glass); + border-color: var(--color-border-hover); +} + +.accordion-icon { + transition: transform var(--transition-base); +} + +.accordion-header.active .accordion-icon { + transform: rotate(180deg); +} + +.accordion-content { + max-height: 0; + overflow: hidden; + transition: max-height var(--transition-slow); +} + +.accordion-content.active { + max-height: 1000px; + padding-top: var(--spacing-md); +} + +/* =================================== + ANIMATIONS + =================================== */ +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(100%); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn var(--transition-base) ease-out; +} + +/* =================================== + RESPONSIVE DESIGN + =================================== */ +@media (max-width: 1024px) { + .main-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .header { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + } + + .preset-controls { + flex-direction: column; + } +} + +@media (max-width: 640px) { + .container { + padding: var(--spacing-md); + } + + .header h1 { + font-size: 2rem; + } + + .header p { + font-size: 1rem; + } + + .panel { + padding: var(--spacing-md); + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .btn-group { + flex-direction: column; + } + + .btn { + width: 100%; + } + + .request-history-table { + font-size: 0.75rem; + } + + .request-history-table th, + .request-history-table td { + padding: var(--spacing-xs); + } +} + +/* =================================== + UTILITY CLASSES + =================================== */ +.text-center { + text-align: center; +} + +.mt-1 { + margin-top: var(--spacing-xs); +} + +.mt-2 { + margin-top: var(--spacing-sm); +} + +.mt-3 { + margin-top: var(--spacing-md); +} + +.mt-4 { + margin-top: var(--spacing-lg); +} + +.mb-1 { + margin-bottom: var(--spacing-xs); +} + +.mb-2 { + margin-bottom: var(--spacing-sm); +} + +.mb-3 { + margin-bottom: var(--spacing-md); +} + +.mb-4 { + margin-bottom: var(--spacing-lg); +} + +.hidden { + display: none; +} + +.full-width { + grid-column: 1 / -1; +} + +/* =================================== + GIT INFO PILL + =================================== */ +.git-info-pill { + position: fixed; + bottom: 20px; + right: 20px; + background-color: rgba(10, 14, 26, 0.95); /* High opacity dark background */ + border: 1px solid var(--color-accent-primary); /* Distinct purple border */ + padding: 10px 20px; + border-radius: 9999px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: white; /* Bright text */ + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Strong shadow */ + z-index: 99999; /* Maximum Z-index */ + backdrop-filter: blur(4px); + transition: all var(--transition-base); + cursor: default; +} + +.git-info-pill:hover { + background-color: rgba(10, 14, 26, 1); + border-color: var(--color-accent-secondary); + box-shadow: 0 0 15px rgba(99, 102, 241, 0.5); /* Glow on hover */ + transform: translateY(-2px); +} + +.git-commit { + color: var(--color-accent-primary); /* Purple highlight */ + font-weight: 700; + letter-spacing: 0.05em; +} + +.git-separator { + color: var(--color-text-tertiary); + opacity: 0.5; +} + +.git-date { + color: #cbd5e1; /* Lighter than muted */ + font-weight: 500; +} diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..4396983 --- /dev/null +++ b/worker.js @@ -0,0 +1,237 @@ +// =================================== +// STRESS TESTING TOOL - WEB WORKER +// Handles request loops for a group of users +// =================================== + +let config = {}; +let state = { + active: false, + users: [], + startTime: 0, + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + responseTimes: [], + bytesSent: 0, + bytesReceived: 0, + errorsByCategory: { + "4xx": 0, + "5xx": 0, + "timeout": 0, + "network": 0 + } +}; + +// Listen for messages from the main thread +self.onmessage = function (e) { + const { type, data } = e.data; + + switch (type) { + case 'INIT': + config = data.config; + break; + case 'START': + state.active = true; + state.startTime = Date.now(); + startUsers(data.users); + break; + case 'STOP': + state.active = false; + break; + } +}; + +async function startUsers(userIndices) { + const pattern = config.trafficPattern; + const totalDuration = config.duration * 1000; + + for (const index of userIndices) { + if (!state.active) break; + + const delay = calculateStartDelay(index, userIndices.length, pattern, totalDuration); + + setTimeout(() => { + if (state.active) { + runUser(index); + } + }, delay); + } + + // Start reporting results periodically + const reportInterval = setInterval(() => { + if (!state.active) { + clearInterval(reportInterval); + return; + } + reportResults(); + }, 500); +} + +function calculateStartDelay(index, count, pattern, duration) { + switch (pattern) { + case 'steady': + return (index % count) * 100; + case 'burst': + const burstIndex = Math.floor((index % count) / (count / 5)); + return burstIndex * (duration / 5); + case 'rampup': + return (index % count) * (duration / count); + case 'random': + return Math.random() * (duration / 2); + default: + return 0; + } +} + +async function runUser(id) { + const endTime = state.startTime + config.duration * 1000; + let currentUrl = config.targetUrl; + let crawlDepth = 0; + + while (state.active && Date.now() < endTime) { + const result = await makeRequest(currentUrl); + + // Report individual request for history log (sampled) + if (Math.random() < 0.1 || config.userCount < 50) { + self.postMessage({ + type: 'LOG', + data: { + url: currentUrl, + status: result.status, + responseTime: result.responseTime, + success: result.success, + timestamp: new Date().toLocaleTimeString() + } + }); + } + + // Logic for crawler (simplified for worker) + if (config.crawlerEnabled && result.success && result.body && crawlDepth < config.crawlDepth) { + const nextUrl = extractRandomLink(result.body, currentUrl); + if (nextUrl) { + currentUrl = nextUrl; + crawlDepth++; + } + } + + // Think time with jitter + const jitter = 0.5 + Math.random(); // 50% to 150% + const sleepTime = config.thinkTime * jitter; + await new Promise(resolve => setTimeout(resolve, sleepTime)); + } +} + +async function makeRequest(targetUrl) { + const startTime = performance.now(); + let result = { + success: false, + status: 0, + responseTime: 0, + body: null + }; + + try { + const payload = { + targetUrl: targetUrl, + method: config.httpMethod, + headers: config.customHeaders, + body: config.requestBody + }; + + const payloadStr = JSON.stringify(payload); + state.bytesSent += payloadStr.length; + + const response = await fetch(config.proxyUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: payloadStr + }); + + const proxyResponse = await response.json(); + const endTime = performance.now(); + + result.responseTime = proxyResponse.responseTime || (endTime - startTime); + result.status = proxyResponse.statusCode; + result.success = proxyResponse.success && result.status >= 200 && result.status < 400; + result.body = proxyResponse.body; + + if (result.body) { + state.bytesReceived += result.body.length; + } + + updateStats(result); + + } catch (error) { + result.responseTime = performance.now() - startTime; + state.failedRequests++; + state.errorsByCategory["network"]++; + } + + return result; +} + +function updateStats(result) { + state.totalRequests++; + if (result.success) { + state.successfulRequests++; + } else { + state.failedRequests++; + const category = categorizeError(result.status); + state.errorsByCategory[category]++; + } + state.responseTimes.push(result.responseTime); + + // Keep response times capped in worker to save memory + if (state.responseTimes.length > 500) { + state.responseTimes.shift(); + } +} + +function categorizeError(status) { + if (status >= 400 && status < 500) return "4xx"; + if (status >= 500) return "5xx"; + return "network"; +} + +function reportResults() { + self.postMessage({ + type: 'STATS', + data: { + totalRequests: state.totalRequests, + successfulRequests: state.successfulRequests, + failedRequests: state.failedRequests, + bytesSent: state.bytesSent, + bytesReceived: state.bytesReceived, + errorsByCategory: state.errorsByCategory, + responseTimes: state.responseTimes // Sampled + } + }); + + // Clear local counters that are cumulative but reported incrementally if needed + // Actually, state object above is cumulative. Main thread will track totals. +} + +function extractRandomLink(html, baseUrl) { + try { + const linkRegex = /href=["'](https?:\/\/[^"']+|(?:\/[^"']+))["']/gi; + const links = []; + let match; + const baseUrlObj = new URL(baseUrl); + + while ((match = linkRegex.exec(html)) !== null) { + let href = match[1]; + try { + const absoluteUrl = new URL(href, baseUrl); + if (absoluteUrl.hostname === baseUrlObj.hostname) { + links.push(absoluteUrl.href); + } + } catch (e) { } + if (links.length > 50) break; // Limit extraction + } + + if (links.length > 0) { + return links[Math.floor(Math.random() * links.length)]; + } + } catch (e) { } + return null; +}