From 13c8093a75d0ea83d5eeba1560b9dd12cc2e013c Mon Sep 17 00:00:00 2001 From: Andus Date: Thu, 10 Apr 2025 23:08:57 +0200 Subject: [PATCH] Initial commit v0.1 --- .gitignore | 130 +++++++ LICENSE.md | 675 +++++++++++++++++++++++++++++++++++ README.md | 1 + app.py | 6 + app/__init__.py | 20 ++ app/api.py | 270 ++++++++++++++ app/auth.py | 16 + app/docker_utils.py | 164 +++++++++ app/port_utils.py | 57 +++ app/routes.py | 39 ++ app/static/sounds/hover.wav | Bin 0 -> 45052 bytes app/static/style.css | 535 +++++++++++++++++++++++++++ app/templates/dashboard.html | 501 ++++++++++++++++++++++++++ app/templates/home.html | 73 ++++ app/templates/layout.html | 114 ++++++ app/templates/setup.html | 66 ++++ 16 files changed, 2667 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/api.py create mode 100644 app/auth.py create mode 100644 app/docker_utils.py create mode 100644 app/port_utils.py create mode 100644 app/routes.py create mode 100644 app/static/sounds/hover.wav create mode 100644 app/static/style.css create mode 100644 app/templates/dashboard.html create mode 100644 app/templates/home.html create mode 100644 app/templates/layout.html create mode 100644 app/templates/setup.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6324c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.idea/ + +client_secrets.json + +ports.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5dc6b42 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,675 @@ + + 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: + + {project} Copyright (C) {year} {fullname} + 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/README.md b/README.md new file mode 100644 index 0000000..6d34898 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Hosting diff --git a/app.py b/app.py new file mode 100644 index 0000000..bce0a8c --- /dev/null +++ b/app.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0') diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..7d79303 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,20 @@ +import os + +from flask import Flask +from .routes import main +from .api import api +from .auth import init_auth +from dotenv import load_dotenv + +load_dotenv() + + +def create_app(): + app = Flask(__name__) + app.secret_key = os.getenv("SECRET_KEY") + app.register_blueprint(main) + app.register_blueprint(api, url_prefix='/api') + + init_auth(app) + + return app diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..5bb3314 --- /dev/null +++ b/app/api.py @@ -0,0 +1,270 @@ +import json +import os +import shutil +import subprocess + +import docker +from flask import Blueprint, jsonify, request, send_from_directory, abort + +from .auth import oidc +from .docker_utils import start_server, stop_server, restart_server, get_logs, delete_server, save_server_info, \ + DEFAULT_CONFIG + +api = Blueprint('api', __name__) + +client = docker.from_env() + + +# Server Deployment +@api.route('/setup', methods=['POST']) +def setup_server(): + data = request.get_json() + username = oidc.user_getfield('preferred_username') + type_ = data['type'] + version = data['version'] + + path = f"./servers/mc-{username}" + os.makedirs(path, exist_ok=True) + save_server_info(username, type_, version) + + return jsonify({'success': True}) + + +@api.route('/delete', methods=['POST']) +def delete(): + data = request.get_json() + username = data.get("username") + + if not username: + return jsonify({"error": "Brak nazwy użytkownika"}), 400 + + result = delete_server(username) + return jsonify({"message": result}) + + +# Server Controls +@api.route('/start', methods=['POST']) +def start(): + username = request.json['username'] + setup_file_path = f"./servers/mc-{username}/server_info.json" + + if not os.path.exists(setup_file_path): + return jsonify({"error": "Server setup file not found."}), 400 + + with open(setup_file_path, 'r') as file: + server_info = json.load(file) + + server_type = server_info.get('type') + server_version = server_info.get('version') + + if not server_type or not server_version: + return jsonify({"error": "Invalid server info."}), 400 + + start_server(username) + + return jsonify({"status": "started"}) + + +@api.route('/stop', methods=['POST']) +def stop(): + username = request.json['username'] + stop_server(username) + return jsonify({"status": "stopped"}) + + +@api.route('/restart', methods=['POST']) +def restart(): + username = request.json['username'] + restart_server(username) + return jsonify({"status": "restarted"}) + + +@api.route('/logs', methods=['GET']) +def logs(): + username = request.args.get('username') + return jsonify({"logs": get_logs(username)}) + + +@api.route('/command', methods=['POST']) +@oidc.require_login +def send_command(): + data = request.get_json() + username = data['username'] + command = data['command'] + + container_name = f"mc-{username}" + + try: + subprocess.run( + ["docker", "exec", container_name, "rcon-cli", command], + check=True, + capture_output=True + ) + return jsonify(success=True) + except subprocess.CalledProcessError as e: + return jsonify(success=False, error=str(e)), 500 + + +# Files APIs (Upload, download, delete) +@api.route('/files', methods=['GET']) +def list_files(): + username = request.args.get('username') + path = request.args.get('path', '') + + base_path = os.path.abspath(f'./servers/mc-{username}') + requested_path = os.path.abspath(os.path.join(base_path, path)) + + if not requested_path.startswith(base_path): + return abort(403) # Prevent directory traversal + + if not os.path.exists(requested_path): + return abort(404) + + entries = [] + for item in os.listdir(requested_path): + if item == "server_info.json": # Hiding panel-specific files + continue + full_path = os.path.join(requested_path, item) + entries.append({ + 'name': item, + 'is_dir': os.path.isdir(full_path) + }) + + return jsonify(entries) + + +@api.route('/files/download', methods=['GET']) +def download_file(): + username = request.args.get('username') + path = request.args.get('path') + + base_path = os.path.abspath(f'./servers/mc-{username}') + file_path = os.path.abspath(os.path.join(base_path, path)) + + if not file_path.startswith(base_path) or not os.path.isfile(file_path): + return abort(403) + + directory = os.path.dirname(file_path) + filename = os.path.basename(file_path) + return send_from_directory(directory, filename, as_attachment=True) + + +@api.route('/files/upload', methods=['POST']) +def upload_file(): + username = request.form.get('username') + path = request.form.get('path', '') + file = request.files['files'] + base_path = os.path.abspath(f'./servers/mc-{username}') + upload_path = os.path.abspath(os.path.join(base_path, path)) + + if not upload_path.startswith(base_path): + return abort(403) + + os.makedirs(upload_path, exist_ok=True) + file.save(os.path.join(upload_path, file.filename)) + + return jsonify({'success': True}) + + +@api.route('/files/delete', methods=['POST']) +def delete_file_or_folder(): + data = request.get_json() + username = data.get('username') + path = data.get('path') + base_path = os.path.abspath(f'./servers/mc-{username}') + target_path = os.path.abspath(os.path.join(base_path, path)) + if not target_path.startswith(base_path): + return abort(403) + if not os.path.exists(target_path): + return jsonify({"error": "Nie znaleziono pliku lub folderu"}), 404 + try: + if os.path.isdir(target_path): + shutil.rmtree(target_path) + else: + os.remove(target_path) + return jsonify({"success": True}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +# Server config +@api.route('/config') +def get_config(): + username = request.args.get('username') + server_info_path = f'./servers/mc-{username}/server_info.json' + if not os.path.exists(server_info_path): + return jsonify({"success": False, "message": "Server config not found"}) + with open(server_info_path, 'r') as f: + server_info = json.load(f) + + return jsonify({"success": True, "config": server_info["config"], "version": server_info["version"], "type": server_info["type"]}) + + +@api.route('/config', methods=['POST']) +def update_config(): + data = request.json + username = data.get('username') + incoming_config = data.get('config', {}) + server_info_path = f'./servers/mc-{username}/server_info.json' + if os.path.exists(server_info_path): + with open(server_info_path, 'r') as f: + server_info = json.load(f) + else: + server_info = DEFAULT_CONFIG.copy() + for key in ["type", "version"]: + if key in incoming_config: + server_info[key] = incoming_config.pop(key) + formatted_config = {key.replace('_', '-'): value for key, value in incoming_config.items()} + server_info["config"] = formatted_config + with open(server_info_path, 'w') as f: + json.dump(server_info, f, indent=4) + + return jsonify({"success": True}) + + +# Server stats +@api.route("/status") +def status(): + username = request.args.get("username") + container_name = f"mc-{username}" + try: + container = client.containers.get(container_name) + return jsonify(running=container.status == "running") + except docker.errors.NotFound: + return jsonify(running=False) + + +@api.route('/stats', methods=['GET']) +def stats(): + username = request.args.get("username") + container_name = f"mc-{username}" + + try: + container = client.containers.get(container_name) + stats = container.stats(stream=False) + + # RAM (MB) + memory_usage = stats['memory_stats']['usage'] / (1024 * 1024) + + # CPU % + cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage'] + system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage'] + percpu = stats['cpu_stats']['cpu_usage'].get('percpu_usage') + cpu_count = len(percpu) if percpu else 1 + cpu_usage = (cpu_delta / system_delta) * cpu_count * 100 if system_delta > 0 else 0 + + # Disk Usage (GB) + server_path = f"./servers/{container_name}" + total_size = sum( + os.path.getsize(os.path.join(dp, f)) for dp, dn, filenames in os.walk(server_path) for f in filenames + ) / (1024 * 1024 * 1024) + disk_usage = min(total_size, 15) + + return jsonify({ + "cpu": round(cpu_usage, 2), + "ram": round(memory_usage, 2), + "disk": round(disk_usage, 2), + "disk_max": 15 + }) + except docker.errors.NotFound: + return jsonify({"error": "Container not found"}), 404 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..d25e4af --- /dev/null +++ b/app/auth.py @@ -0,0 +1,16 @@ +import os +from dotenv import load_dotenv + +from flask_oidc import OpenIDConnect + +oidc = OpenIDConnect() +load_dotenv() + +def init_auth(app): + app.config.update({ + 'SECRET_KEY': os.getenv('SECRET_KEY'), + 'OIDC_CLIENT_SECRETS': 'client_secrets.json', + 'OIDC_SCOPES': ['openid', 'email', 'profile'], + 'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post', + }) + oidc.init_app(app) diff --git a/app/docker_utils.py b/app/docker_utils.py new file mode 100644 index 0000000..9f57e1b --- /dev/null +++ b/app/docker_utils.py @@ -0,0 +1,164 @@ +import json +import shutil + +import docker +import os + +from docker.errors import NotFound + +from .port_utils import assign_ports + +client = docker.from_env() + +DEFAULT_CONFIG = { + "max-players": 20, + "pvp": True, + "difficulty": "easy", + "online-mode": True, + "spawn-monsters": True, + "spawn-animals": True, + "allow-nether": True, + "max-build-height": 256, + "view-distance": 10 +} + + +def get_server_config(username): + server_info_path = f"./servers/mc-{username}/server_info.json" + + if not os.path.exists(server_info_path): + server_info = { + "type": "paper", + "version": "latest", + "config": DEFAULT_CONFIG.copy() + } + os.makedirs(os.path.dirname(server_info_path), exist_ok=True) + with open(server_info_path, "w") as f: + json.dump(server_info, f, indent=4) + return DEFAULT_CONFIG.copy(), "paper", "latest" + + with open(server_info_path, "r") as f: + server_info = json.load(f) + + server_type = server_info.get("type", "paper") + server_version = server_info.get("version", "latest") + server_config = server_info.get("config", DEFAULT_CONFIG.copy()) + + updated = False + + for key, default_value in DEFAULT_CONFIG.items(): + if key not in server_config: + server_config[key] = default_value + updated = True + + if updated: + server_info["config"] = server_config + with open(server_info_path, "w") as f: + json.dump(server_info, f, indent=4) + + print(f"Loaded config: {server_config}") + return server_config, server_type, server_version + + +def save_server_info(username, server_type, server_version): + server_info_path = f"./servers/mc-{username}/server_info.json" + server_info = { + "type": server_type, + "version": server_version, + "config": DEFAULT_CONFIG.copy() + } + os.makedirs(os.path.dirname(server_info_path), exist_ok=True) + with open(server_info_path, "w") as f: + json.dump(server_info, f) + + +def get_server_info(username): + server_info_path = f"./servers/mc-{username}/server_info.json" + if os.path.exists(server_info_path): + with open(server_info_path, "r") as f: + return json.load(f) + return None + + +def start_server(username): + name = f"mc-{username}" + ports = assign_ports(username) + path = f"./servers/{name}" + os.makedirs(path, exist_ok=True) + server_config, server_type, server_version = get_server_config(username) + + environment = { + "EULA": "TRUE", + "SERVER_PORT": ports[0], + "MOTD": f"Serwer użytkownika §9{username}", + "TYPE": server_type, + "VERSION": server_version, + "MAX_PLAYERS": server_config["max-players"], + "PVP": "TRUE" if server_config["pvp"] else "FALSE", + "DIFFICULTY": server_config["difficulty"], + "SPAWN_MONSTERS": "TRUE" if server_config["spawn-monsters"] else "FALSE", + "SPAWN_ANIMALS": "TRUE" if server_config["spawn-animals"] else "FALSE", + "ALLOW_NETHER": "TRUE" if server_config["allow-nether"] else "FALSE", + "MAX_BUILD_HEIGHT": server_config["max-build-height"], + "VIEW_DISTANCE": server_config["view-distance"], + "ONLINE_MODE": "TRUE" if server_config["online-mode"] else "FALSE", + "INIT_MEMORY": "1G", + "MAX_MEMORY": "4G" + } + + client.containers.run( + "itzg/minecraft-server", + detach=True, + name=f"mc-{username}", + ports={ + f"{ports[0]}/tcp": ports[0], + f"{ports[1]}/tcp": ports[1], + f"{ports[2]}/tcp": ports[2], + }, + volumes={ + os.path.abspath(path): {'bind': '/data', 'mode': 'rw'} + }, + environment=environment, + restart_policy={"Name": "unless-stopped"} + ) + + +def stop_server(username): + name = f"mc-{username}" + container = client.containers.get(name) + container.stop() + container.remove() + + +def restart_server(username): + name = f"mc-{username}" + container = client.containers.get(name) + container.stop() + container.remove() + start_server(username) + + +def delete_server(username): + container_name = f"mc-{username}" + server_path = f"./servers/{container_name}" + + try: + container = client.containers.get(container_name) + container.stop() + container.remove() + except NotFound: + pass + + if os.path.exists(server_path): + shutil.rmtree(server_path) + + from .port_utils import free_ports_for_user + free_ports_for_user(username) + + return f"Serwer {container_name} został usunięty." + + +def get_logs(username): + name = f"mc-{username}" + container = client.containers.get(name) + return container.logs(tail=100).decode() diff --git a/app/port_utils.py b/app/port_utils.py new file mode 100644 index 0000000..28725da --- /dev/null +++ b/app/port_utils.py @@ -0,0 +1,57 @@ +import json +import os + +PORTS_FILE = 'ports.json' +BASE_PORT = 25500 +PORTS_PER_USER = 3 +MAX_PORTS = 100 + + +def load_ports(): + if not os.path.exists(PORTS_FILE): + with open(PORTS_FILE, 'w') as f: + json.dump({}, f) + with open(PORTS_FILE, 'r') as f: + return json.load(f) + + +def save_ports(ports): + with open(PORTS_FILE, 'w') as f: + json.dump(ports, f, indent=2) + + +def assign_ports(username): + ports = load_ports() + if username in ports: + return ports[username] + + used = {p for group in ports.values() for p in group} + for i in range(0, MAX_PORTS * PORTS_PER_USER, PORTS_PER_USER): + trial = [BASE_PORT + i + j for j in range(PORTS_PER_USER)] + if not any(p in used for p in trial): + ports[username] = trial + save_ports(ports) + return trial + + raise Exception("Brak dostępnych portów") + + +def free_ports_for_user(username): + if not os.path.exists(PORTS_FILE): + return + + with open(PORTS_FILE, "r") as f: + data = json.load(f) + + container_name = f"mc-{username}" + if container_name in data: + del data[container_name] + with open(PORTS_FILE, "w") as f: + json.dump(data, f, indent=2) + +def get_user_ports(username): + ports = load_ports() + if username in ports: + return ports[username] + + return None \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..e5d2be7 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,39 @@ +import os + +from flask import Blueprint, render_template +from .auth import oidc +from .port_utils import get_user_ports +from dotenv import load_dotenv + +main = Blueprint('main', __name__) + +load_dotenv() + + +@main.route("/") +@oidc.require_login +def home(): + username = oidc.user_getfield("preferred_username") + server_path = f"./servers/mc-{username}" + has_server = os.path.exists(server_path) + return render_template("home.html", has_server=has_server) + + +@main.route('/setup') +@oidc.require_login +def setup(): + return render_template('setup.html') + + +@main.route('/dashboard') +@oidc.require_login +def dashboard(): + username = oidc.user_getfield('preferred_username') + server_path = f"./servers/mc-{username}" + has_server = os.path.exists(server_path) + ip = os.getenv("SERVER_IP") + ports = get_user_ports(username) + if (has_server): + return render_template('dashboard.html', username=username, ip=ip, ports=ports) + else: + return render_template('setup.html', username=username, ip=ip, ports=ports) diff --git a/app/static/sounds/hover.wav b/app/static/sounds/hover.wav new file mode 100644 index 0000000000000000000000000000000000000000..b7a19e6628fae27ee8f1bec7e8e976c3d02745e4 GIT binary patch literal 45052 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 g^@LLoW#s=yfK!h?l3ylkBOKOzIoRsaA1 literal 0 HcmV?d00001 diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..ebb9b51 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,535 @@ +/* Reset & Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; +} + +body { + font-family: 'Inter', sans-serif; + background: linear-gradient(145deg, #0a0a0f, #000000); + color: #f0f0f0; + min-height: 100vh; + padding: 32px 20px; + display: flex; + flex-direction: column; + align-items: center; + overflow-x: hidden; +} + +/* Glowing cursor */ +*, +*::before, +*::after { + cursor: none !important; +} + +body::after { + content: ''; + position: fixed; + top: var(--cursor-y, 0); + left: var(--cursor-x, 0); + width: 60px; + height: 60px; + background: radial-gradient(circle, rgba(109, 125, 251, 0.35) 0%, transparent 80%); + border-radius: 50%; + pointer-events: none; + transform: translate(-50%, -50%); + z-index: 9999; + transition: top 0.08s ease-out, left 0.08s ease-out; +} + +.cursor-glow { + position: fixed; + top: 0; + left: 0; + width: 24px; + height: 24px; + border-radius: 50%; + pointer-events: none; + mix-blend-mode: screen; + z-index: 999999; + transition: transform 0.05s linear; +} + +.cursor-glow:nth-child(1) { background: radial-gradient(circle, #b9c5ff 0%, transparent 80%); } +.cursor-glow:nth-child(2) { background: radial-gradient(circle, rgba(179, 192, 253, 0.53) 0%, transparent 80%); width: 28px; height: 28px; } +.cursor-glow:nth-child(3) { background: radial-gradient(circle, rgba(175, 187, 248, 0.33) 0%, transparent 80%); width: 32px; height: 32px; } +.cursor-glow:nth-child(4) { background: radial-gradient(circle, rgba(160, 170, 243, 0.2) 0%, transparent 80%); width: 36px; height: 36px; } +.cursor-glow:nth-child(5) { background: radial-gradient(circle, rgba(130, 140, 230, 0.1) 0%, transparent 80%); width: 40px; height: 40px; } +.cursor-glow:nth-child(6) { background: radial-gradient(circle, rgba(100, 110, 210, 0.05) 0%, transparent 80%); width: 44px; height: 44px; } + +/* Headings */ +h2 { + font-size: clamp(1.8rem, 2.5vw, 2.3rem); + color: #ffffff; + margin-bottom: 24px; + text-align: center; + letter-spacing: 1px; +} + +/* Card Container */ +.dashboard-card { + background: linear-gradient(to bottom right, #0e0e1a, #10101c); + position:relative; + backdrop-filter: blur(14px); + border: 1px solid #1a1a2a; + box-shadow: 0 0 25px rgba(74, 90, 239, 0.2); + border-radius: 20px; + padding: 28px; + width: 100%; + max-width: 80%; + transition: all 0.3s ease; +} + +/* Tabs */ +.tabs { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 14px; + margin-bottom: 26px; +} + +.tab-button { + padding: 14px; + border: none; + background-color: #151520; + border-radius: 12px; + font-size: 0.95rem; + cursor: pointer; + color: #ccc; + transition: background-color 0.25s, transform 0.2s; + box-shadow: 0 0 8px transparent; +} + +.tab-button:hover { + background-color: #1e1e2f; + transform: scale(1.03); +} + +.tab-button.active { + background: linear-gradient(145deg, #4a5aef, #6c7bff); + color: white; + box-shadow: 0 0 14px #4a5aef99; +} + +.square-button { + aspect-ratio: 1 / 1; + width: 100px; + background-color: #1a1a2e; + border: 2px solid #2c2c3c; + border-radius: 16px; + margin: 10px; + padding: 12px; + cursor: pointer; + transition: 0.3s ease; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #ccc; + font-size: 14px; + font-weight: 500; +} + +.square-button:hover { + background-color: #23233a; + border-color: #4a4a6a; + color: #fff; + transform: translateY(-2px); +} + +.square-button img { + width: 40px; + height: auto; + margin-bottom: 8px; + filter: brightness(0.9); +} + +.square-button.selected { + background-color: #3b3b5c; + border-color: #6a6af0; + color: #fff; + box-shadow: 0 0 10px rgba(106, 106, 240, 0.5); +} + +/* Panels */ +.tab-content { + margin-top: 12px; + width: 100%; +} + +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; +} + +/* Buttons */ +.btn { + padding: 14px 24px; + border: none; + border-radius: 10px; + font-weight: 600; + font-size: 0.95rem; + margin: 6px 4px; + cursor: pointer; + transition: 0.2s ease; + text-align: center; + min-width: 110px; + box-shadow: 0 0 8px transparent; +} + +.btn:hover { + opacity: 0.95; + transform: translateY(-2px); +} + +.btn-primary { background: #4a5aef; color: white; box-shadow: 0 0 10px #4a5aef99; } +.btn-success { background: #2ecc71; color: white; box-shadow: 0 0 10px #2ecc7199; } +.btn-danger { background: #e74c3c; color: white; box-shadow: 0 0 10px #e74c3c99; } +.btn-warning { background: #f39c12; color: black; box-shadow: 0 0 10px #f39c1288; } +.btn-info { background: #3498db; color: white; box-shadow: 0 0 10px #3498db99; } + +/* Console Output */ +.console-output { + background: #0d0d13; + color: #e0e0e0; + padding: 18px; + border-radius: 10px; + height: 240px; + overflow-y: auto; + font-family: 'Fira Code', monospace; + font-size: 0.9rem; + border: 1px solid #29293a; + white-space: pre-wrap; + margin-bottom: 14px; +} + +/* Console Input */ +.console-input { + margin-top: 10px; + display: flex; + gap: 10px; +} + +.console-input input { + flex: 1; + padding: 6px 10px; + background: #1e1e1e; + border: 1px solid #333; + color: #fff; + border-radius: 4px; +} + +.console-input button { + padding: 6px 12px; + background-color: #444; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.console-input button:hover { + background-color: #666; +} + +/* Chart Container */ +.chart-container { + background: #12121c; + padding: 20px; + border-radius: 14px; + margin-top: 30px; + border: 1px solid #232334; + box-shadow: 0 0 12px #1a1a2a; + overflow-x: auto; +} + +body > *:not(script):not(style):not(.cursor-glow) { + animation: growIn 0.6s ease-out; +} + +/* Page load transition */ +@keyframes growIn { + 0% { + opacity: 0; + transform: scale(0.95); + filter: blur(4px); + } + 100% { + opacity: 1; + transform: scale(1); + filter: blur(0); + } +} + +.grow-in { + animation: growIn 0.6s ease-out forwards; +} + +/* Files */ +#drop-zone { + border: 2px dashed #aaa; + border-radius: 10px; + padding: 40px; + text-align: center; + cursor: pointer; + margin: 20px; + transition: background-color 0.3s ease; +} + +#drop-zone.dragover { + background-color: #f3f3f3; +} + +#upload-progress { + margin-top: 15px; + width: 100%; + height: 20px; + background: #ddd; + border-radius: 10px; + overflow: hidden; +} + +#progress-bar { + height: 100%; + width: 0%; + background: #4caf50; + transition: width 0.2s ease; +} + +.hidden { + display: none; +} + +/* General Input and Checkbox */ +input[type="text"], input[type="number"], input[type="checkbox"], select { + background-color: #1e1e1e; + border: 1px solid #333; + color: #fff; + font-size: 1rem; + border-radius: 4px; + padding: 8px 12px; + margin: 8px 0; + width: auto; + transition: background-color 0.3s, border-color 0.3s; +} + +input[type="checkbox"] { + width: auto; + margin-right: 8px; +} + +input[type="text"]:focus, input[type="number"]:focus, select:focus { + border-color: #4a5aef; + background-color: #151520; + outline: none; +} + +input[type="checkbox"]:checked { + background-color: #4a5aef; + border-color: #4a5aef; +} + +label { + font-size: 1rem; + color: #ccc; + display: block; + margin-bottom: 6px; +} + +input[type="checkbox"] { + accent-color: #4a5aef; +} + +/* Number Inputs */ +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Dropdown */ +select { + background-color: #1e1e1e; + color: #fff; + border: 1px solid #333; + padding: 8px 12px; + font-size: 1rem; + border-radius: 4px; + width: auto; + transition: background-color 0.3s, border-color 0.3s; +} + +select:focus { + border-color: #4a5aef; + background-color: #151520; + outline: none; +} + +form { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 600px; + margin: 0 auto; +} + +.checkbox-group { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.checkbox-group label { + font-size: 1rem; + color: #ccc; + margin-right: 10px; +} + +/* Background */ +#particle-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; /* Keep the particles behind other content */ + pointer-events: none; + overflow: hidden; +} + +.particle { + position: absolute; + width: 4px; + height: 4px; + background-color: rgba(255, 255, 255, 0.2); /* More transparent */ + border-radius: 50%; /* Keep particles circular but will make them fade out quickly */ + animation: particle-move 4s infinite; +} + +@keyframes particle-move { + 0% { + transform: scale(1) translate(0, 0); + opacity: 1; + } + 100% { + transform: scale(0.5) translate(var(--x-move), var(--y-move)); /* Move in random directions */ + opacity: 0; + } +} + +/* Navigation */ +.top-nav { + width: 100%; + max-width: 1280px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + margin-bottom: 24px; + background: #0e0e1a; + border-radius: 14px; + border: 1px solid #1a1a2a; + box-shadow: 0 0 14px rgba(74, 90, 239, 0.15); +} + +.nav-logo { + font-size: 1.3rem; + font-weight: bold; + color: #b9c5ff; +} + +.nav-links a { + margin-left: 20px; + color: #ccc; + text-decoration: none; + font-size: 1rem; +} + +.nav-links a:hover { + color: #fff; + text-shadow: 0 0 6px #4a5aef; +} + +/* Links */ +a { + color: #4a5aef; + text-decoration: none; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +a:hover { + color: #999cdc; + text-shadow: 0 0 6px #8085d8; +} + +a:active { + color: #bbbeee; +} + +a.non-link { + color: inherit; + text-decoration: none; + cursor: default; +} + +a.non-link:hover { + color: inherit; + text-shadow: none; +} + +/* Charts */ +.charts-row { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; +} + +.chart-container { + background: #12121c; + padding: 20px; + border-radius: 14px; + border: 1px solid #232334; + box-shadow: 0 0 12px #1a1a2a; + width: 360px; + height: 240px; + max-width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +/* Mobile Tweaks */ +@media (max-width: 600px) { + body { + padding: 24px 12px; + } + + .dashboard-card { + padding: 20px; + } + + .btn { + width: 100%; + margin: 8px 0; + } + + .console-output { + height: 180px; + } +} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..6cd5a50 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,501 @@ +{% extends "layout.html" %} +{% block content %} + +

Twój serwer Minecraft

+ +
+ + + + + +
+ +
+
+
+

IP: {{ ip }}

+

Port (Serwer MC): {{ ports[0] }}

+

Dodatkowe porty: {{ ports[1:] | join(", ") }}

+
+

Status serwera: Sprawdzanie...


+ + +
+ +
+ +
+
Ładowanie logów...
+
+ + +
+
+ +
+

/

+
+

Przeciągnij pliki tutaj lub kliknij, aby przesłać

+ + +
+ +
+

Lista plików:

+
    +
    + +
    +
    +

    Konfiguracja Serwera:

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    W tym trybie mogą dołączyć tylko gracze z kupionym kontem Minecraft. Jest owiele bezpieczniejszy. +
    Jeżeli chcesz używać serwera z wyłączonym trybem online, zainstaluj AuthMe lub podobny plugin i włącz whitelistę
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + + +{% endblock %} diff --git a/app/templates/home.html b/app/templates/home.html new file mode 100644 index 0000000..17c2244 --- /dev/null +++ b/app/templates/home.html @@ -0,0 +1,73 @@ +{% extends "layout.html" %} +{% block content %} +
    +

    MCPanel

    +

    Jesteśmy 1.5x lepsi od Aternosa!

    +
    + +
    + {% if has_server %} + Idź do panelu + {% else %} + Ustaw własny serwer + {% endif %} +
    + + + + +{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html new file mode 100644 index 0000000..e434270 --- /dev/null +++ b/app/templates/layout.html @@ -0,0 +1,114 @@ + + + + + MCPanel + + + + + +
    +
    +
    +
    +
    +
    + + +
    + + + +
    + {% block content %}{% endblock %} +
    +

    Wersja: v0.1

    + + + diff --git a/app/templates/setup.html b/app/templates/setup.html new file mode 100644 index 0000000..7aae0d3 --- /dev/null +++ b/app/templates/setup.html @@ -0,0 +1,66 @@ +{% extends "layout.html" %} +{% block content %} +

    Wybierz typ serwera

    + +
    + + +
    + + + + +{% endblock %}