commit f2966d0f5c2fa612a1da8491a25a31a35a32bdc1 Author: Harry-zklcdc Date: Sun Dec 29 01:02:31 2024 +0800 [Init] 🎉 MEGREZ Community diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..950075e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +data + +config.yml +docker-compose.yml + +megrez \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c0a6e5a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..53786f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.formatOnSave": true + }, + "[vue]": { + "editor.defaultFormatter": "Vue.volar", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bae94e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. \ No newline at end of file diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..9a4fb78 --- /dev/null +++ b/config.example.yml @@ -0,0 +1,12 @@ +http: + host: 0.0.0.0 + port: 34567 +database: + host: gm-postgres + port: 5432 + username: GpuManager + password: GpuManager + database: GpuManager +redis: + host: ms-redis + port: 6379 diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..68cd832 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,41 @@ +version: "3" + +services: + megrez: + image: zklcdc/ubuntu-with-tzdata:latest + container_name: megrez + restart: always + environment: + - TZ=Asia/Shanghai + volumes: + - $PWD/megrez:/home/megrez + - $PWD/config.yaml:/home/config.yaml + depends_on: + - "megrez-redis" + - "megrez-postgres" + command: sh -c "cd /home/ && /home/megrez -config /home/config.yml" + ports: + - 34567:34567 + + megrez-postgres: + image: postgres:16-alpine + container_name: megrez-postgres + restart: always + volumes: + - $PWD/data/postgres:/var/lib/postgresql/data + environment: + - TZ=Asia/Shanghai + - POSTGRES_DB=GpuManager + - POSTGRES_USER=GpuManager + - POSTGRES_PASSWORD=GpuManager + + megrez-redis: + image: redis:7-alpine + container_name: megrez-redis + restart: always + volumes: + - $PWD/data/redis:/data + +networks: + default: + name: megrez-network \ No newline at end of file diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..b585a76 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..ec96cff --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,23 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, + env: { + node: true + }, + extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier'], + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + 'vue/multi-word-component-names': 'off', + 'vue/no-reserved-component-names': 'off', + 'vue/component-tags-order': [ + 'error', + { + order: ['script', 'template', 'style'] + } + ] + } +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d45df1c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,14 @@ +node_modules +coverage +*.log* +.nuxt +.nitro +.cache +.output +.env +dist +.DS_Store +.idea +.eslintcache +api-generator/typedoc.json +**/.DS_Store diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..6e1c17b --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "useTabs": false, + "tabWidth": 2, + "trailingComma": "none", + "semi": true, + "singleQuote": true, + "vueIndentScriptAndStyle": false, + "printWidth": 250, + "bracketSameLine": false +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7d82c64 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + MEGREZ 天权算能聚联计算平台 + + + +
+ + + + \ No newline at end of file diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..aafb595 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..2ca5ab0 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5113 @@ +{ + "name": "megrez-ui", + "version": "4.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "megrez-ui", + "version": "4.1.0", + "dependencies": { + "@microsoft/clarity": "^1.0.0", + "@primevue/themes": "^4.0.0", + "axios": "^1.7.7", + "chart.js": "3.3.2", + "pinia": "^2.2.4", + "pinia-plugin-persistedstate": "^4.1.1", + "primeicons": "^6.0.1", + "primevue": "^4.0.0", + "vue": "^3.4.34", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@primevue/auto-import-resolver": "^4.0.1", + "@rushstack/eslint-patch": "^1.8.0", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-prettier": "^9.0.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", + "postcss": "^8.4.40", + "prettier": "^3.2.5", + "sass": "^1.55.0", + "tailwindcss": "^3.4.6", + "tailwindcss-primeui": "^0.3.2", + "unplugin-vue-components": "^0.27.3", + "vite": "^5.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/standalone": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/standalone/-/standalone-7.25.7.tgz", + "integrity": "sha512-7H+mK18Ew4C/pIIiZwF1eiVjUEh2Ju/BpwRZwcPeXltF/rIjHjFL0gol7PtGrHocmIq6P6ubJrylmmWQ3lGJPA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/clarity": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@microsoft/clarity/-/clarity-1.0.0.tgz", + "integrity": "sha512-2QY6SmXnqRj6dWhNY8NYCN3e53j4zCFebH4wGnNhdGV1mqAsQwql2fT0w8TISxCvwwfVp8idsWLIdrRHOms1PQ==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt/kit": { + "version": "3.13.2", + "resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-3.13.2.tgz", + "integrity": "sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==", + "license": "MIT", + "dependencies": { + "@nuxt/schema": "3.13.2", + "c12": "^1.11.2", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "globby": "^14.0.2", + "hash-sum": "^2.0.0", + "ignore": "^5.3.2", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "semver": "^7.6.3", + "ufo": "^1.5.4", + "unctx": "^2.3.1", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/schema": { + "version": "3.13.2", + "resolved": "https://registry.npmmirror.com/@nuxt/schema/-/schema-3.13.2.tgz", + "integrity": "sha512-CCZgpm+MkqtOMDEgF9SWgGPBXlQ01hV/6+2reDEpJuqFPGzV8HYKPBcIFvn7/z5ahtgutHLzjP71Na+hYcqSpw==", + "license": "MIT", + "dependencies": { + "compatx": "^0.1.8", + "consola": "^3.2.3", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@primeuix/styled": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz", + "integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==", + "dependencies": { + "@primeuix/utils": "^0.0.5" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz", + "integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/auto-import-resolver": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/auto-import-resolver/-/auto-import-resolver-4.0.3.tgz", + "integrity": "sha512-zwWEeIwgIJcDbW1lz6YIn8du6BY9Q3mSnBRZUYLNWVw3fqlO78vKbOLD3brY8PNVvZMRxT7zWH0c+WtGK8bUtA==", + "dev": true, + "dependencies": { + "@primevue/metadata": "4.0.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.3.tgz", + "integrity": "sha512-pKdXsYHRMtJfw1tjPJ2u3bC+QLUDIftdHgR7DRfZ4ZV0/9qOGpFZIBYTv3Px+NGQnV+qAyp3Yeh/pS9UO83wHQ==", + "dependencies": { + "@primeuix/styled": "^0.0.5", + "@primeuix/utils": "^0.0.5" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.3.tgz", + "integrity": "sha512-eamWkgXhAt/3nC0HJLYmkpXf6DP9QDQ/ecaqT9onNlHXHXL+QDd25OG/wKHPKB94jh3AO/9rD1aMW+YtXtkKbQ==", + "dependencies": { + "@primeuix/utils": "^0.0.5", + "@primevue/core": "4.0.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/metadata": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/metadata/-/metadata-4.0.3.tgz", + "integrity": "sha512-xL3BlD9Nx5gCCQAHyK2BMujUm34FmBlODuFyevWzEX/lkhzWs7jLVQ5lUpoaiXm+jcdgzq7K0QDA8OxCVITucA==", + "dev": true, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/themes": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.3.tgz", + "integrity": "sha512-5Mcb8UES2wg9WtvuK/lKW61xZpkbmRGdxKBh3LfEsxx8pwRmFtRD8XCXUPzaxUv9lwVEW3F/AQ0oAyBbnXptoQ==", + "dependencies": { + "@primeuix/styled": "^0.0.5" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", + "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", + "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", + "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", + "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", + "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", + "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", + "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", + "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", + "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", + "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", + "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz", + "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz", + "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", + "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", + "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", + "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.1.tgz", + "integrity": "sha512-sDckXxlHpMsjRQbAH9WanangrfrblsOd3pNifePs+FOHjJg1jfWq5L/P0PsBRndEt3nmdUnmvieP8ULDeX5AvA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.34.tgz", + "integrity": "sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.34", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.34.tgz", + "integrity": "sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==", + "dependencies": { + "@vue/compiler-core": "3.4.34", + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.34.tgz", + "integrity": "sha512-x6lm0UrM03jjDXTPZgD9Ad8bIVD1ifWNit2EaWQIZB5CULr46+FbLQ5RpK7AXtDHGjx9rmvC7QRCTjsiGkAwRw==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.34", + "@vue/compiler-dom": "3.4.34", + "@vue/compiler-ssr": "3.4.34", + "@vue/shared": "3.4.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.39", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.34.tgz", + "integrity": "sha512-8TDBcLaTrFm5rnF+Qm4BlliaopJgqJ28Nsrc80qazynm5aJO+Emu7y0RWw34L8dNnTRdcVBpWzJxhGYzsoVu4g==", + "dependencies": { + "@vue/compiler-dom": "3.4.34", + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "dev": true, + "dependencies": { + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.34.tgz", + "integrity": "sha512-ua+Lo+wBRlBEX9TtgPOShE2JwIO7p6BTZ7t1KZVPoaBRfqbC7N3c8Mpzicx173fXxx5VXeU6ykiHo7WgLzJQDA==", + "dependencies": { + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.34.tgz", + "integrity": "sha512-PXhkiRPwcPGJ1BnyBZFI96GfInCVskd0HPNIAZn7i3YOmLbtbTZpB7/kDTwC1W7IqdGPkTVC63IS7J2nZs4Ebg==", + "dependencies": { + "@vue/reactivity": "3.4.34", + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.34.tgz", + "integrity": "sha512-dXqIe+RqFAK2Euak4UsvbIupalrhc67OuQKpD7HJ3W2fv8jlqvI7szfBCsAEcE8o/wyNpkloxB6J8viuF/E3gw==", + "dependencies": { + "@vue/reactivity": "3.4.34", + "@vue/runtime-core": "3.4.34", + "@vue/shared": "3.4.34", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.34.tgz", + "integrity": "sha512-GeyEUfMVRZMD/mZcNONEqg7MiU10QQ1DB3O/Qr6+8uXpbwdlmVgQ5Qs1/ZUAFX1X2UUtqMoGrDRbxdWfOJFT7Q==", + "dependencies": { + "@vue/compiler-ssr": "3.4.34", + "@vue/shared": "3.4.34" + }, + "peerDependencies": { + "vue": "3.4.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.34.tgz", + "integrity": "sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==" + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "1.11.2", + "resolved": "https://registry.npmmirror.com/c12/-/c12-1.11.2.tgz", + "integrity": "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001667", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chart.js": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz", + "integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA==" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compatx": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/compatx/-/compatx-0.1.8.tgz", + "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deep-pick-omit": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz", + "integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==", + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.34", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.34.tgz", + "integrity": "sha512-/TZAiChbAflBNjCg+VvstbcwAtIL/VdMFO3NgRFIzBjpvPzWOTIbbO8kNb6RwU4bt9TP7K+3KqBKw/lOU+Y+GA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", + "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmmirror.com/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.1.0.tgz", + "integrity": "sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.3.12", + "resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.2.4.tgz", + "integrity": "sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persistedstate": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.1.1.tgz", + "integrity": "sha512-fUiUsbfBetGUZzX28z+ImAZw7FDXzwRrk+fN+ljF5OhQMhsSYfYeUzI9FLLtpjekYbfFHWvJiECkLI60RIuiPA==", + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.13.2", + "deep-pick-omit": "^1.2.1", + "defu": "^6.1.4", + "destr": "^2.0.3" + }, + "peerDependencies": { + "@pinia/nuxt": ">=0.5.0", + "pinia": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@pinia/nuxt": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/primeicons": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/primeicons/-/primeicons-6.0.1.tgz", + "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==", + "license": "MIT" + }, + "node_modules/primevue": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.3.tgz", + "integrity": "sha512-f/41JMb7pMP++zKuiymXYmDMk/4oFRlJKXQzgrAy5Q3esU29hS59MBaP2CsHdPoJasQSzVAgOT+36S4/WXLajg==", + "dependencies": { + "@primeuix/styled": "^0.0.5", + "@primeuix/utils": "^0.0.5", + "@primevue/core": "4.0.3", + "@primevue/icons": "4.0.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz", + "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==", + "devOptional": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.19.1", + "@rollup/rollup-android-arm64": "4.19.1", + "@rollup/rollup-darwin-arm64": "4.19.1", + "@rollup/rollup-darwin-x64": "4.19.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.1", + "@rollup/rollup-linux-arm-musleabihf": "4.19.1", + "@rollup/rollup-linux-arm64-gnu": "4.19.1", + "@rollup/rollup-linux-arm64-musl": "4.19.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1", + "@rollup/rollup-linux-riscv64-gnu": "4.19.1", + "@rollup/rollup-linux-s390x-gnu": "4.19.1", + "@rollup/rollup-linux-x64-gnu": "4.19.1", + "@rollup/rollup-linux-x64-musl": "4.19.1", + "@rollup/rollup-win32-arm64-msvc": "4.19.1", + "@rollup/rollup-win32-ia32-msvc": "4.19.1", + "@rollup/rollup-win32-x64-msvc": "4.19.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.77.8", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", + "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", + "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-primeui": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/tailwindcss-primeui/-/tailwindcss-primeui-0.3.4.tgz", + "integrity": "sha512-5+Qfoe5Kpq2Iwrd6umBUb3rQH6b7+pL4jxJUId0Su5agUM6TwCyH5Pyl9R0y3QQB3IRuTxBNmeS11B41f+30zw==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=3.1.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/unctx": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.3.1.tgz", + "integrity": "sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==", + "license": "MIT", + "dependencies": { + "acorn": "^8.8.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.0", + "unplugin": "^1.3.1" + } + }, + "node_modules/unctx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport": { + "version": "3.13.1", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.13.1.tgz", + "integrity": "sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.2", + "acorn": "^8.12.1", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.0", + "unplugin": "^1.14.1" + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.14.1.tgz", + "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "webpack-sources": "^3" + }, + "peerDependenciesMeta": { + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.3.tgz", + "integrity": "sha512-5wg7lbdg5ZcrAQNzyYK+6gcg/DG8K6rO+f5YeuvqGHs/PhpapBvpA4O/0ex/pFthE5WgRk43iWuRZEMLVsdz4Q==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "chokidar": "^3.6.0", + "debug": "^4.3.5", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.10", + "minimatch": "^9.0.5", + "mlly": "^1.7.1", + "unplugin": "^1.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unplugin-vue-components/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/untyped": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/untyped/-/untyped-1.5.1.tgz", + "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.7", + "@babel/standalone": "^7.25.7", + "@babel/types": "^7.25.7", + "defu": "^6.1.4", + "jiti": "^2.3.1", + "mri": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/untyped/node_modules/jiti": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.34.tgz", + "integrity": "sha512-VZze05HWlA3ItreQ/ka7Sx7PoD0/3St8FEiSlSTVgb6l4hL+RjtP2/8g5WQBzZgyf8WG2f+g1bXzC7zggLhAJA==", + "dependencies": { + "@vue/compiler-dom": "3.4.34", + "@vue/compiler-sfc": "3.4.34", + "@vue/runtime-dom": "3.4.34", + "@vue/server-renderer": "3.4.34", + "@vue/shared": "3.4.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", + "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", + "dependencies": { + "@vue/devtools-api": "^6.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d9b1d43 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "megrez-ui", + "version": "4.1.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint --fix . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" + }, + "dependencies": { + "@microsoft/clarity": "^1.0.0", + "@primevue/themes": "^4.0.0", + "axios": "^1.7.7", + "chart.js": "3.3.2", + "pinia": "^2.2.4", + "pinia-plugin-persistedstate": "^4.1.1", + "primeicons": "^6.0.1", + "primevue": "^4.0.0", + "vue": "^3.4.34", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@primevue/auto-import-resolver": "^4.0.1", + "@rushstack/eslint-patch": "^1.8.0", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-prettier": "^9.0.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", + "postcss": "^8.4.40", + "prettier": "^3.2.5", + "sass": "^1.55.0", + "tailwindcss": "^3.4.6", + "tailwindcss-primeui": "^0.3.2", + "unplugin-vue-components": "^0.27.3", + "vite": "^5.3.1" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..fe66dd6 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..1991588 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..d81a9e5 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,8 @@ + + + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..a9a4d41 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,126 @@ +import axios from "axios"; + +const ajax = (url, method, { params = {}, data = {} }) => { + axios.defaults.withCredentials = true + axios.defaults.crossDomain = true + axios.defaults.baseURL = '/api/v1' + return new Promise((resolve, reject) => { + axios({ + url, + method, + params, + data + }).then(res => { + if (res.data.code != 200) { + reject(res) + } else { + resolve(res) + } + }, res => { + reject(res) + }) + }) +} + +export default { + GetUserProfile() { + return ajax('user/profile', 'get', {}) + }, + UserLogin(data) { + return ajax('user/login', 'post', { data }) + }, + UserLogout() { + return ajax('user/logout', 'get', {}) + }, + UserRegister(data) { + return ajax('user/register', 'post', { data }) + }, + + UserInstancesList(params) { + return ajax('user/instances', 'get', { params }) + }, + UserInstancesDetail(id) { + return ajax(`user/instances/${id}`, 'get', {}) + }, + UserInstancesModify(id, data) { + return ajax(`user/instances/${id}`, 'post', { data }) + }, + UserInstancesModifyLabel(id, data) { + return ajax(`user/instances/${id}/label`, 'post', { data }) + }, + UserInstancesAction(id, data) { + return ajax(`user/instances/${id}/`, 'put', { data }) + }, + UserInstancesCreate(data) { + return ajax('user/instances', 'post', { data }) + }, + UserInstancesDelete(id) { + return ajax(`user/instances/${id}`, 'delete', {}) + }, + + UserServerList(params) { + return ajax('user/servers', 'get', { params }) + }, + UserServerDetail(id) { + return ajax(`user/servers/${id}`, 'get', {}) + }, + + UserImages() { + return ajax('user/images', 'get', {}) + }, + + AdminInstancesList(params) { + return ajax('admin/instances', 'get', { params }) + }, + AdminInstancesDetail(id) { + return ajax(`admin/instances/${id}`, 'get', {}) + }, + AdminInstancesModify(id, data) { + return ajax(`admin/instances/${id}`, 'post', { data }) + }, + AdminInstancesModifyLabel(id, data) { + return ajax(`admin/instances/${id}/label`, 'post', { data }) + }, + AdminInstancesAction(id, data) { + return ajax(`admin/instances/${id}/`, 'put', { data }) + }, + AdminInstancesCreate(data) { + return ajax('admin/instances', 'post', { data }) + }, + AdminInstancesDelete(id) { + return ajax(`admin/instances/${id}`, 'delete', {}) + }, + + AdminUserList(params) { + return ajax('admin/users', 'get', { params }) + }, + AdminUserModify(id, data) { + return ajax(`admin/users/${id}`, 'post', { data }) + }, + AdminUserDelete(id) { + return ajax(`admin/users/${id}`, 'delete', {}) + }, + + AdminServersList(params) { + return ajax('admin/servers', 'get', { params }) + }, + AdminServersDetail(id) { + return ajax(`admin/servers/${id}`, 'get', {}) + }, + AdminServersAdd(data) { + return ajax('admin/servers', 'post', { data }) + }, + AdminServersModify(id, data) { + return ajax(`admin/servers/${id}`, 'post', { data }) + }, + AdminServersDelete(id) { + return ajax(`admin/servers/${id}`, 'delete', {}) + }, + + AdminImagesList() { + return ajax('admin/images', 'get', {}) + }, + AdminImagesModify(data) { + return ajax('admin/images', 'post', { data }) + }, +} \ No newline at end of file diff --git a/frontend/src/assets/layout/_core.scss b/frontend/src/assets/layout/_core.scss new file mode 100644 index 0000000..920f351 --- /dev/null +++ b/frontend/src/assets/layout/_core.scss @@ -0,0 +1,23 @@ +html { + height: 100%; + font-size: 14px; +} + +body { + font-family: 'Lato', sans-serif; + color: var(--text-color); + // background-color: var(--surface-ground); + margin: 0; + padding: 0; + min-height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + text-decoration: none; +} + +.layout-wrapper { + min-height: 100vh; +} diff --git a/frontend/src/assets/layout/_footer.scss b/frontend/src/assets/layout/_footer.scss new file mode 100644 index 0000000..27bcbf0 --- /dev/null +++ b/frontend/src/assets/layout/_footer.scss @@ -0,0 +1,8 @@ +.layout-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 0 1rem 0; + gap: 0.5rem; + border-top: 1px solid var(--surface-border); +} diff --git a/frontend/src/assets/layout/_main.scss b/frontend/src/assets/layout/_main.scss new file mode 100644 index 0000000..943dbc7 --- /dev/null +++ b/frontend/src/assets/layout/_main.scss @@ -0,0 +1,16 @@ +.layout-main-container { + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; + padding: 2rem 2rem 0 2rem; + margin-top: 4rem; + transition: margin-left var(--layout-section-transition-duration); + border-radius: 1rem; + background-color: var(--surface-ground); +} + +.layout-main { + flex: 1 1 auto; + padding-bottom: 2rem; +} diff --git a/frontend/src/assets/layout/_menu.scss b/frontend/src/assets/layout/_menu.scss new file mode 100644 index 0000000..05a6141 --- /dev/null +++ b/frontend/src/assets/layout/_menu.scss @@ -0,0 +1,160 @@ +.layout-sidebar { + position: fixed; + width: 20rem; + // height: calc(100vh - 8rem); + height: calc(100vh - 4rem); + z-index: 999; + overflow-y: auto; + user-select: none; + // top: 6rem; + top: 4rem; + // left: 2rem; + transition: + transform var(--layout-section-transition-duration), + left var(--layout-section-transition-duration); + background-color: var(--surface-overlay); + // border-radius: var(--content-border-radius); + padding: 0.5rem 1.5rem; +} + +.layout-menu { + margin: 0; + padding: 0; + list-style-type: none; + + .layout-root-menuitem { + > .layout-menuitem-root-text { + font-size: 0.857rem; + text-transform: uppercase; + font-weight: 700; + color: var(--text-color); + margin: 0.75rem 0; + } + + > a { + display: none; + } + } + + a { + user-select: none; + + &.active-menuitem { + > .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + li.active-menuitem { + > a { + .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + ul { + margin: 0; + padding: 0; + list-style-type: none; + + a { + display: flex; + align-items: center; + position: relative; + outline: 0 none; + color: var(--text-color); + cursor: pointer; + padding: 0.75rem 1rem; + border-radius: var(--content-border-radius); + transition: + background-color var(--element-transition-duration), + box-shadow var(--element-transition-duration); + + .layout-menuitem-icon { + margin-right: 0.5rem; + } + + .layout-submenu-toggler { + font-size: 75%; + margin-left: auto; + transition: transform var(--element-transition-duration); + } + + &.active-route { + font-weight: 700; + color: var(--primary-color); + } + + &:hover { + background-color: var(--surface-hover); + } + + &:focus { + @include focused-inset(); + } + } + + ul { + overflow: hidden; + border-radius: var(--content-border-radius); + + li { + a { + margin-left: 1rem; + } + + li { + a { + margin-left: 2rem; + } + + li { + a { + margin-left: 2.5rem; + } + + li { + a { + margin-left: 3rem; + } + + li { + a { + margin-left: 3.5rem; + } + + li { + a { + margin-left: 4rem; + } + } + } + } + } + } + } + } + } +} + +.layout-submenu-enter-from, +.layout-submenu-leave-to { + max-height: 0; +} + +.layout-submenu-enter-to, +.layout-submenu-leave-from { + max-height: 1000px; +} + +.layout-submenu-leave-active { + overflow: hidden; + transition: max-height 0.45s cubic-bezier(0, 1, 0, 1); +} + +.layout-submenu-enter-active { + overflow: hidden; + transition: max-height 1s ease-in-out; +} diff --git a/frontend/src/assets/layout/_mixins.scss b/frontend/src/assets/layout/_mixins.scss new file mode 100644 index 0000000..ad330b1 --- /dev/null +++ b/frontend/src/assets/layout/_mixins.scss @@ -0,0 +1,15 @@ +@mixin focused() { + outline-width: var(--focus-ring-width); + outline-style: var(--focus-ring-style); + outline-color: var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + box-shadow: var(--focus-ring-shadow); + transition: + box-shadow var(--transition-duration), + outline-color var(--transition-duration); +} + +@mixin focused-inset() { + outline-offset: -1px; + box-shadow: inset var(--focus-ring-shadow); +} diff --git a/frontend/src/assets/layout/_preloading.scss b/frontend/src/assets/layout/_preloading.scss new file mode 100644 index 0000000..a814104 --- /dev/null +++ b/frontend/src/assets/layout/_preloading.scss @@ -0,0 +1,47 @@ +.preloader { + position: fixed; + z-index: 999999; + background: #edf1f5; + width: 100%; + height: 100%; +} +.preloader-content { + border: 0 solid transparent; + border-radius: 50%; + width: 150px; + height: 150px; + position: absolute; + top: calc(50vh - 75px); + left: calc(50vw - 75px); +} + +.preloader-content:before, .preloader-content:after{ + content: ''; + border: 1em solid var(--primary-color); + border-radius: 50%; + width: inherit; + height: inherit; + position: absolute; + top: 0; + left: 0; + animation: loader 2s linear infinite; + opacity: 0; +} + +.preloader-content:before{ + animation-delay: 0.5s; +} + +@keyframes loader{ + 0%{ + transform: scale(0); + opacity: 0; + } + 50%{ + opacity: 1; + } + 100%{ + transform: scale(1); + opacity: 0; + } +} diff --git a/frontend/src/assets/layout/_responsive.scss b/frontend/src/assets/layout/_responsive.scss new file mode 100644 index 0000000..9f5d418 --- /dev/null +++ b/frontend/src/assets/layout/_responsive.scss @@ -0,0 +1,110 @@ +@media screen and (min-width: 1960px) { + .layout-main, + .landing-wrapper { + width: 1504px; + margin-left: auto !important; + margin-right: auto !important; + } +} + +@media (min-width: 992px) { + .layout-wrapper { + &.layout-overlay { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-right: 1px solid var(--surface-border); + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + } + + &.layout-overlay-active { + .layout-sidebar { + transform: translateX(0); + } + } + } + + &.layout-static { + .layout-main-container { + margin-left: 20rem; + } + + &.layout-static-inactive { + .layout-sidebar { + transform: translateX(-100%); + left: 0; + } + + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + } + } + + .layout-mask { + display: none; + } + } +} + +@media (max-width: 991px) { + .blocked-scroll { + overflow: hidden; + } + + .layout-wrapper { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + } + + .layout-mask { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 998; + width: 100%; + height: 100%; + background-color: var(--maskbg); + } + + &.layout-mobile-active { + .layout-sidebar { + transform: translateX(0); + } + + .layout-mask { + display: block; + } + } + } +} diff --git a/frontend/src/assets/layout/_topbar.scss b/frontend/src/assets/layout/_topbar.scss new file mode 100644 index 0000000..3338f25 --- /dev/null +++ b/frontend/src/assets/layout/_topbar.scss @@ -0,0 +1,204 @@ +.layout-topbar { + position: fixed; + height: 4rem; + z-index: 997; + left: 0; + top: 0; + width: 100%; + padding: 0 2rem; + background-color: var(--surface-card); + transition: left var(--layout-section-transition-duration); + display: flex; + align-items: center; + + .layout-topbar-logo-container { + width: 28rem; + display: flex; + align-items: center; + } + + .layout-topbar-logo { + display: inline-flex; + align-items: center; + font-size: 1.5rem; + border-radius: var(--content-border-radius); + color: var(--text-color); + font-weight: 500; + gap: 0.8rem; + + svg { + width: 3rem; + } + + img { + width: 2rem; + } + + &:focus-visible { + @include focused(); + } + } + + .layout-topbar-action { + display: inline-flex; + justify-content: center; + align-items: center; + color: var(--text-color-secondary); + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + color: var(--text-color); + transition: background-color var(--element-transition-duration); + cursor: pointer; + + &:hover { + background-color: var(--surface-hover); + } + + &:focus-visible { + @include focused(); + } + + i { + font-size: 1.25rem; + } + + span { + font-size: 1rem; + display: none; + } + + &.layout-topbar-action-highlight { + background-color: var(--primary-color); + color: var(--primary-contrast-color); + } + } + + .layout-menu-button { + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: none; + } + + .layout-topbar-actions { + margin-left: auto; + display: flex; + gap: 1rem; + } + + .layout-topbar-menu-content { + display: flex; + gap: 1rem; + } + + .layout-config-menu { + display: flex; + gap: 1rem; + } +} + +@media (max-width: 991px) { + .layout-topbar { + padding: 0 2rem; + + .layout-topbar-logo-container { + width: auto; + } + + .layout-menu-button { + margin-left: 0; + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: inline-flex; + } + + .layout-topbar-menu { + position: absolute; + background-color: var(--surface-overlay); + transform-origin: top; + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + border-radius: var(--content-border-radius); + padding: 1rem; + right: 2rem; + top: 4rem; + min-width: 15rem; + border: 1px solid var(--surface-border); + + .layout-topbar-menu-content { + gap: 0.5rem; + } + + .layout-topbar-action { + display: flex; + width: 100%; + height: auto; + justify-content: flex-start; + border-radius: var(--content-border-radius); + padding: 0.5rem 1rem; + + i { + font-size: 1rem; + margin-right: 0.5rem; + } + + span { + font-weight: medium; + display: block; + } + } + } + + .layout-topbar-menu-content { + flex-direction: column; + } + } +} + +.config-panel { + .config-panel-label { + font-size: 0.875rem; + color: var(--text-secondary-color); + font-weight: 600; + line-height: 1; + } + + .config-panel-colors { + > div { + padding-top: 0.5rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: space-between; + + button { + border: none; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + padding: 0; + cursor: pointer; + outline-color: transparent; + outline-width: 2px; + outline-style: solid; + outline-offset: 1px; + + &.active-color { + outline-color: var(--primary-color); + } + } + } + } + + .config-panel-settings { + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/frontend/src/assets/layout/_typography.scss b/frontend/src/assets/layout/_typography.scss new file mode 100644 index 0000000..b17bbc2 --- /dev/null +++ b/frontend/src/assets/layout/_typography.scss @@ -0,0 +1,68 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.5rem 0 1rem 0; + font-family: inherit; + font-weight: 700; + line-height: 1.5; + color: var(--text-color); + + &:first-child { + margin-top: 0; + } +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +mark { + background: #fff8e1; + padding: 0.25rem 0.4rem; + border-radius: var(--content-border-radius); + font-family: monospace; +} + +blockquote { + margin: 1rem 0; + padding: 0 2rem; + border-left: 4px solid #90a4ae; +} + +hr { + border-top: solid var(--surface-border); + border-width: 1px 0 0 0; + margin: 1rem 0; +} + +p { + margin: 0 0 1rem 0; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/frontend/src/assets/layout/_utils.scss b/frontend/src/assets/layout/_utils.scss new file mode 100644 index 0000000..6ccec88 --- /dev/null +++ b/frontend/src/assets/layout/_utils.scss @@ -0,0 +1,25 @@ +/* Utils */ +.clearfix:after { + content: ' '; + display: block; + clear: both; +} + +.card { + background: var(--surface-card); + padding: 2rem; + margin-bottom: 2rem; + border-radius: var(--content-border-radius); + + &:last-child { + margin-bottom: 0; + } +} + +.p-toast { + &.p-toast-top-right, + &.p-toast-top-left, + &.p-toast-top-center { + top: 100px; + } +} diff --git a/frontend/src/assets/layout/layout.scss b/frontend/src/assets/layout/layout.scss new file mode 100644 index 0000000..8971f62 --- /dev/null +++ b/frontend/src/assets/layout/layout.scss @@ -0,0 +1,13 @@ +@import './variables/_common'; +@import './variables/_light'; +@import './variables/_dark'; +@import './_mixins'; +@import './_preloading'; +@import './_core'; +@import './_main'; +@import './_topbar'; +@import './_menu'; +@import './_footer'; +@import './_responsive'; +@import './_utils'; +@import './_typography'; diff --git a/frontend/src/assets/layout/variables/_common.scss b/frontend/src/assets/layout/variables/_common.scss new file mode 100644 index 0000000..2a040c2 --- /dev/null +++ b/frontend/src/assets/layout/variables/_common.scss @@ -0,0 +1,20 @@ +:root { + --primary-color: var(--p-primary-color); + --primary-contrast-color: var(--p-primary-contrast-color); + --text-color: var(--p-text-color); + --text-color-secondary: var(--p-text-muted-color); + --surface-border: var(--p-content-border-color); + --surface-card: var(--p-content-background); + --surface-hover: var(--p-content-hover-background); + --surface-overlay: var(--p-overlay-popover-background); + --transition-duration: var(--p-transition-duration); + --maskbg: var(--p-mask-background); + --content-border-radius: var(--p-content-border-radius); + --layout-section-transition-duration: 0.2s; + --element-transition-duration: var(--p-transition-duration); + --focus-ring-width: var(--p-focus-ring-width); + --focus-ring-style: var(--p-focus-ring-style); + --focus-ring-color: var(--p-focus-ring-color); + --focus-ring-offset: var(--p-focus-ring-offset); + --focus-ring-shadow: var(--p-focus-ring-shadow); +} diff --git a/frontend/src/assets/layout/variables/_dark.scss b/frontend/src/assets/layout/variables/_dark.scss new file mode 100644 index 0000000..bb91605 --- /dev/null +++ b/frontend/src/assets/layout/variables/_dark.scss @@ -0,0 +1,5 @@ +:root[class*='app-dark'] { + --surface-ground: var(--p-surface-950); + --code-background: var(--p-surface-800); + --code-color: var(--p-surface-100); +} diff --git a/frontend/src/assets/layout/variables/_light.scss b/frontend/src/assets/layout/variables/_light.scss new file mode 100644 index 0000000..aa3403c --- /dev/null +++ b/frontend/src/assets/layout/variables/_light.scss @@ -0,0 +1,5 @@ +:root { + --surface-ground: var(--p-surface-100); + --code-background: var(--p-surface-900); + --code-color: var(--p-surface-200); +} diff --git a/frontend/src/assets/logo-text.webp b/frontend/src/assets/logo-text.webp new file mode 100644 index 0000000..7909385 Binary files /dev/null and b/frontend/src/assets/logo-text.webp differ diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100755 index 0000000..3db3f0b --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1,18 @@ + + + Layer 1 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/styles.scss b/frontend/src/assets/styles.scss new file mode 100644 index 0000000..5a22be2 --- /dev/null +++ b/frontend/src/assets/styles.scss @@ -0,0 +1,4 @@ +/* You can add global styles to this file, and also import other style files */ +@import '@/assets/layout/layout.scss'; +@import 'primeicons/primeicons.css'; + diff --git a/frontend/src/assets/tailwind.css b/frontend/src/assets/tailwind.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/frontend/src/assets/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/components/CopyIcon.vue b/frontend/src/components/CopyIcon.vue new file mode 100644 index 0000000..6b49189 --- /dev/null +++ b/frontend/src/components/CopyIcon.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/EditLabel.vue b/frontend/src/components/EditLabel.vue new file mode 100644 index 0000000..29a12e7 --- /dev/null +++ b/frontend/src/components/EditLabel.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/FloatingConfigurator.vue b/frontend/src/components/FloatingConfigurator.vue new file mode 100644 index 0000000..bf8dd58 --- /dev/null +++ b/frontend/src/components/FloatingConfigurator.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/components/SectionBanner.vue b/frontend/src/components/SectionBanner.vue new file mode 100644 index 0000000..3c6d574 --- /dev/null +++ b/frontend/src/components/SectionBanner.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/frontend/src/layout/AppFooter.vue b/frontend/src/layout/AppFooter.vue new file mode 100644 index 0000000..6917419 --- /dev/null +++ b/frontend/src/layout/AppFooter.vue @@ -0,0 +1,10 @@ + + + diff --git a/frontend/src/layout/AppLayout.vue b/frontend/src/layout/AppLayout.vue new file mode 100644 index 0000000..52046a2 --- /dev/null +++ b/frontend/src/layout/AppLayout.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/layout/AppMenu.vue b/frontend/src/layout/AppMenu.vue new file mode 100644 index 0000000..ddb9f7f --- /dev/null +++ b/frontend/src/layout/AppMenu.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/frontend/src/layout/AppMenuItem.vue b/frontend/src/layout/AppMenuItem.vue new file mode 100644 index 0000000..9cff3cd --- /dev/null +++ b/frontend/src/layout/AppMenuItem.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/frontend/src/layout/AppSidebar.vue b/frontend/src/layout/AppSidebar.vue new file mode 100644 index 0000000..c6ea0db --- /dev/null +++ b/frontend/src/layout/AppSidebar.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/layout/AppTopbar.vue b/frontend/src/layout/AppTopbar.vue new file mode 100644 index 0000000..62b0c50 --- /dev/null +++ b/frontend/src/layout/AppTopbar.vue @@ -0,0 +1,110 @@ + + + + \ No newline at end of file diff --git a/frontend/src/layout/composables/layout.js b/frontend/src/layout/composables/layout.js new file mode 100644 index 0000000..d8a4cef --- /dev/null +++ b/frontend/src/layout/composables/layout.js @@ -0,0 +1,84 @@ +import { computed, reactive, readonly } from 'vue'; + +const layoutConfig = reactive({ + preset: 'Aura', + primary: 'blue', + surface: null, + darkTheme: false, + menuMode: 'static' +}); + +const layoutState = reactive({ + staticMenuDesktopInactive: false, + overlayMenuActive: false, + profileSidebarVisible: false, + configSidebarVisible: false, + staticMenuMobileActive: false, + menuHoverActive: false, + activeMenuItem: null +}); + +export function useLayout() { + const setPrimary = (value) => { + layoutConfig.primary = value; + }; + + const setSurface = (value) => { + layoutConfig.surface = value; + }; + + const setPreset = (value) => { + layoutConfig.preset = value; + }; + + const setActiveMenuItem = (item) => { + layoutState.activeMenuItem = item.value || item; + }; + + const setMenuMode = (mode) => { + layoutConfig.menuMode = mode; + }; + + const toggleDarkMode = () => { + if (!document.startViewTransition) { + executeDarkModeToggle(); + + return; + } + + document.startViewTransition(() => executeDarkModeToggle(event)); + }; + + const executeDarkModeToggle = () => { + layoutConfig.darkTheme = !layoutConfig.darkTheme; + document.documentElement.classList.toggle('app-dark'); + }; + + const onMenuToggle = () => { + if (layoutConfig.menuMode === 'overlay') { + layoutState.overlayMenuActive = !layoutState.overlayMenuActive; + } + + if (window.innerWidth > 991) { + layoutState.staticMenuDesktopInactive = !layoutState.staticMenuDesktopInactive; + } else { + layoutState.staticMenuMobileActive = !layoutState.staticMenuMobileActive; + } + }; + + const resetMenu = () => { + layoutState.overlayMenuActive = false; + layoutState.staticMenuMobileActive = false; + layoutState.menuHoverActive = false; + }; + + const isSidebarActive = computed(() => layoutState.overlayMenuActive || layoutState.staticMenuMobileActive); + + const isDarkTheme = computed(() => layoutConfig.darkTheme); + + const getPrimary = computed(() => layoutConfig.primary); + + const getSurface = computed(() => layoutConfig.surface); + + return { layoutConfig: readonly(layoutConfig), layoutState: readonly(layoutState), onMenuToggle, isSidebarActive, isDarkTheme, getPrimary, getSurface, setActiveMenuItem, toggleDarkMode, setPrimary, setSurface, setPreset, resetMenu, setMenuMode }; +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..e7a7a61 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,83 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router'; + +import { createPinia } from 'pinia'; +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; + +import { updatePreset } from '@primevue/themes'; +import Aura from '@primevue/themes/aura'; +import PrimeVue from 'primevue/config'; +import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; + +import '@/assets/styles.scss'; +import '@/assets/tailwind.css'; + +const app = createApp(App); + +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) + +app.use(pinia); +app.use(router); +app.use(PrimeVue, { + theme: { + preset: Aura, + options: { + darkModeSelector: '.app-dark' + } + }, + pt: { + card: { + root: { + class: 'rounded-2xl' + } + }, + message: { + root: { + class: 'rounded-2xl' + } + } + } +}); +app.use(ToastService); +app.use(ConfirmationService); + +updatePreset({ + semantic: { + primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' }, + colorScheme: { + light: { + primary: { + color: '{primary.500}', + contrastColor: '#ffffff', + hoverColor: '{primary.600}', + activeColor: '{primary.700}' + }, + highlight: { + background: '{primary.50}', + focusBackground: '{primary.100}', + color: '{primary.700}', + focusColor: '{primary.800}' + } + }, + dark: { + primary: { + color: '{primary.400}', + contrastColor: '{surface.900}', + hoverColor: '{primary.300}', + activeColor: '{primary.200}' + }, + highlight: { + background: 'color-mix(in srgb, {primary.400}, transparent 84%)', + focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)', + color: 'rgba(255,255,255,.87)', + focusColor: 'rgba(255,255,255,.87)' + } + } + } + } +}); + +app.mount('#app'); diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..7e7b625 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,85 @@ +import AppLayout from '@/layout/AppLayout.vue'; +import { createRouter, createWebHistory } from 'vue-router'; + +import Login from '@/views/Login.vue'; +import Register from '@/views/Register.vue'; + +import InstanceCreate from '@/views/users/InstanceCreate.vue'; +import InstanceList from '@/views/users/InstanceList.vue'; +import Settings from '@/views/users/Settings.vue'; + +import Images from '@/views/admin/Images.vue'; +import Instances from '@/views/admin/Instances.vue'; +import Servers from '@/views/admin/Servers.vue'; +import Users from '@/views/admin/Users.vue'; + +import NotFound from '@/views/NotFound.vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + redirect: '/instances' + }, + { + path: '/login', + name: 'login', + component: Login + }, + { + path: '/register', + name: 'register', + component: Register + }, + { + path: '/', + name: 'dashboard', + component: AppLayout, + children: [ + { + path: 'instances', + name: 'instances', + component: InstanceList + }, + { + path: 'instances/create', + name: 'instances-create', + component: InstanceCreate + }, + { + path: 'settings', + name: 'settings', + component: Settings + }, + { + path: 'admin/images', + name: 'admin-images', + component: Images + }, + { + path: 'admin/instances', + name: 'admin-instances', + component: Instances + }, + { + path: 'admin/servers', + name: 'admin-servers', + component: Servers + }, + { + path: 'admin/users', + name: 'admin-users', + component: Users + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: NotFound + } + ] +}); + +export default router; diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js new file mode 100644 index 0000000..de6d3f3 --- /dev/null +++ b/frontend/src/stores/profile.js @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia'; + +export const useProfileStore = defineStore('profile', { + state: () => ({ + userProfile: { + username: '', + email: '', + role: 0 + } + }), + actions: { + setUserProfile(profile) { + this.userProfile = profile; + }, + clearUserProfile() { + this.userProfile = { + username: '', + email: '', + role: 0 + }; + } + }, + getters: { + isAuthenticated: (state) => !!state.userProfile.email, + isAdmin: (state) => state.userProfile.role == 2, + isSuperAdmin: (state) => state.userProfile.role === 3, + username: (state) => state.userProfile.username, + }, + persist: true // 开启持久化 +}); \ No newline at end of file diff --git a/frontend/src/utils/time.js b/frontend/src/utils/time.js new file mode 100644 index 0000000..3124aca --- /dev/null +++ b/frontend/src/utils/time.js @@ -0,0 +1,17 @@ +function formatDate(dateString) { + const date = new Date(dateString); + + // 自定义格式化输出 (YYYY-MM-DD HH:mm:ss) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从 0 开始 + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` +} + +export { + formatDate +}; diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..3d24506 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..4b89a4c --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,52 @@ + + + diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..51bfad9 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,83 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Images.vue b/frontend/src/views/admin/Images.vue new file mode 100644 index 0000000..c6cedf0 --- /dev/null +++ b/frontend/src/views/admin/Images.vue @@ -0,0 +1,118 @@ + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Instances.vue b/frontend/src/views/admin/Instances.vue new file mode 100644 index 0000000..30065c6 --- /dev/null +++ b/frontend/src/views/admin/Instances.vue @@ -0,0 +1,609 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Servers.vue b/frontend/src/views/admin/Servers.vue new file mode 100644 index 0000000..56db1fa --- /dev/null +++ b/frontend/src/views/admin/Servers.vue @@ -0,0 +1,404 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Users.vue b/frontend/src/views/admin/Users.vue new file mode 100644 index 0000000..a575c23 --- /dev/null +++ b/frontend/src/views/admin/Users.vue @@ -0,0 +1,186 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/users/InstanceCreate.vue b/frontend/src/views/users/InstanceCreate.vue new file mode 100644 index 0000000..4478dab --- /dev/null +++ b/frontend/src/views/users/InstanceCreate.vue @@ -0,0 +1,282 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/users/InstanceList.vue b/frontend/src/views/users/InstanceList.vue new file mode 100644 index 0000000..9487764 --- /dev/null +++ b/frontend/src/views/users/InstanceList.vue @@ -0,0 +1,611 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/users/Settings.vue b/frontend/src/views/users/Settings.vue new file mode 100644 index 0000000..2ac9230 --- /dev/null +++ b/frontend/src/views/users/Settings.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..3519d0b --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ['selector', '[class*="app-dark"]'], + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + plugins: [require('tailwindcss-primeui')], + theme: { + screens: { + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + '2xl': '1920px' + } + } +}; diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs new file mode 100644 index 0000000..7f34388 --- /dev/null +++ b/frontend/vite.config.mjs @@ -0,0 +1,36 @@ +import { fileURLToPath, URL } from 'node:url'; + +import { PrimeVueResolver } from '@primevue/auto-import-resolver'; +import vue from '@vitejs/plugin-vue'; +import Components from 'unplugin-vue-components/vite'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + outDir: '../routers/index/web', + }, + optimizeDeps: { + noDiscovery: true + }, + plugins: [ + vue(), + Components({ + resolvers: [PrimeVueResolver()] + }) + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '/api': { + target: 'http://127.0.0.1:34567/api', + // changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + } +}); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..211c87c --- /dev/null +++ b/go.mod @@ -0,0 +1,65 @@ +module megrez + +go 1.23.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/kataras/iris/v12 v12.2.11 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect + github.com/CloudyKit/jet/v6 v6.2.0 // indirect + github.com/Joker/jade v1.1.3 // indirect + github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/flosch/pongo2/v4 v4.0.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/iris-contrib/schema v0.0.6 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kataras/blocks v0.0.8 // indirect + github.com/kataras/golog v0.1.11 // indirect + github.com/kataras/pio v0.0.13 // indirect + github.com/kataras/sitemap v0.0.6 // indirect + github.com/kataras/tunnel v0.0.4 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/mailgun/raymond/v2 v2.0.48 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/redis/go-redis/v9 v9.5.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/schollz/closestmatch v2.1.0+incompatible // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/tdewolff/minify/v2 v2.20.19 // indirect + github.com/tdewolff/parse/v2 v2.7.12 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/yosssi/ace v0.0.5 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..60f71b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,211 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= +github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= +github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= +github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= +github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= +github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= +github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= +github.com/kataras/golog v0.1.11 h1:dGkcCVsIpqiAMWTlebn/ZULHxFvfG4K43LF1cNWSh20= +github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= +github.com/kataras/iris/v12 v12.2.11 h1:sGgo43rMPfzDft8rjVhPs6L3qDJy3TbBrMD/zGL1pzk= +github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= +github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= +github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= +github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= +github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= +github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= +github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= +github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= +github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= +github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= +github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= +github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/libs/crypto/base64.go b/libs/crypto/base64.go new file mode 100644 index 0000000..5efb658 --- /dev/null +++ b/libs/crypto/base64.go @@ -0,0 +1,11 @@ +package crypto + +import "encoding/base64" + +func Base64Encode(src []byte) string { + return base64.StdEncoding.EncodeToString(src) +} + +func Base64Decode(src string) ([]byte, error) { + return base64.StdEncoding.DecodeString(src) +} diff --git a/libs/crypto/hex.go b/libs/crypto/hex.go new file mode 100644 index 0000000..90fcfd3 --- /dev/null +++ b/libs/crypto/hex.go @@ -0,0 +1,45 @@ +package crypto + +import ( + "math/rand" + "strings" + "time" +) + +const letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +var src = rand.NewSource(time.Now().UnixNano()) + +const ( + // 6 bits to represent a letter index + letterIdBits = 6 + // All 1-bits as many as letterIdBits + letterIdMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdMax + } + if idx := int(cache & letterIdMask); idx < len(letters) { + sb.WriteByte(letters[idx]) + i-- + } + cache >>= letterIdBits + remain-- + } + return sb.String() +} + +func HexLowercase(n int) string { + return strings.ToLower(Hex(n)) +} + +func HexUpper(n int) string { + return strings.ToUpper(Hex(n)) +} diff --git a/libs/crypto/sha256.go b/libs/crypto/sha256.go new file mode 100644 index 0000000..26c33bd --- /dev/null +++ b/libs/crypto/sha256.go @@ -0,0 +1,13 @@ +package crypto + +import ( + "crypto/sha256" + "encoding/hex" +) + +func Sha256(str string) string { + hash := sha256.New() + hash.Write([]byte(str)) + bytes := hash.Sum(nil) + return hex.EncodeToString(bytes) +} diff --git a/libs/crypto/uuid.go b/libs/crypto/uuid.go new file mode 100644 index 0000000..c9abe26 --- /dev/null +++ b/libs/crypto/uuid.go @@ -0,0 +1,24 @@ +package crypto + +import ( + "strings" + + "github.com/google/uuid" +) + +// GenerateUUID 生成一个新的 UUID +func GenerateUUID() (string, error) { + newUUID, err := uuid.NewUUID() + if err != nil { + return "", err + } + return newUUID.String(), nil +} + +func GenerateUUIDWithoutHyphen() (string, error) { + newUUID, err := uuid.NewUUID() + if err != nil { + return "", err + } + return strings.ReplaceAll(newUUID.String(), "-", ""), nil +} diff --git a/libs/logger/logger.go b/libs/logger/logger.go new file mode 100644 index 0000000..5ada9dd --- /dev/null +++ b/libs/logger/logger.go @@ -0,0 +1,225 @@ +package logger + +import ( + "container/list" + "context" + "errors" + "io" + "log" + "os" + "strings" + "sync" + "time" + + "gorm.io/gorm/logger" +) + +const ( + DEBUG = "DEBUG" + INFO = "INFO" + WARN = "WARN" + ERROR = "ERROR" +) + +const ( + Reset = "\033[0m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + White = "\033[37m" + BlueBold = "\033[34;1m" + MagentaBold = "\033[35;1m" + RedBold = "\033[31;1m" + YellowBold = "\033[33;1m" +) + +var levelArry = [4]string{DEBUG, INFO, WARN, ERROR} +var colorArry = [4]string{MagentaBold, BlueBold, YellowBold, RedBold} + +var levelMap = map[string]int{ + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} + +type LoggerStruct struct { + level string + // logger *log.Logger + model string + function string +} + +type Interface interface { + LogMode(logger.LogLevel) Interface + Info(context.Context, string, ...interface{}) + Warn(context.Context, string, ...interface{}) + Error(context.Context, string, ...interface{}) + Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) +} + +type logetContent struct { + format string + v []interface{} +} + +type logQueueStruct struct { + *list.List + Mu sync.Mutex + RWLock sync.RWMutex +} + +var logQueue logQueueStruct +var logLogger *log.Logger + +func init() { + logQueue = logQueueStruct{ + List: list.New(), + } +} + +func logOutput() { + if !logQueue.Mu.TryLock() { + return + } + for logQueue.Len() > 0 { + logQueue.RWLock.RLock() + e := logQueue.Front() + logQueue.RWLock.RUnlock() + switch e.Value.(type) { + case logetContent: + content := e.Value.(logetContent) + logLogger.Printf(content.format, content.v...) + default: + } + logQueue.RWLock.Lock() + logQueue.Remove(e) + logQueue.RWLock.Unlock() + } + logQueue.Mu.Unlock() +} + +func NewLogger(level string, args ...any) (*LoggerStruct, error) { + level = strings.ToUpper(level) + if _, ok := levelMap[level]; !ok { + return nil, errors.New("level not found") + } + var err error + filename := "data/logs/backend.log" + if len(args) > 0 { + if args[0] == "stdout" { + logLogger = log.New(io.MultiWriter(os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds) + return &LoggerStruct{ + level: level, + // logger: log.New(io.MultiWriter(os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds), + }, nil + } + filename = args[0].(string) + } + logFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, os.FileMode(0775)) + + if os.IsNotExist(err) { + err = os.MkdirAll("data/logs", os.FileMode(0775)) + if err != nil { + return nil, err + } + logFile, _ = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, os.FileMode(0775)) + } + + logLogger = log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds) + return &LoggerStruct{ + level: level, + // logger: log.New(io.MultiWriter(logPipe, os.Stdout), "", log.Ldate|log.Ltime), + // logger: log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds), + }, nil +} + +func (l *LoggerStruct) Close() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + for logQueue.Len() > 0 { + select { + case <-ctx.Done(): + cancel() + return + default: + continue + } + } +} + +func (l *LoggerStruct) Clone() *LoggerStruct { + return &LoggerStruct{ + level: l.level, + // logger: log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds), + model: l.model, + function: l.function, + } +} + +func (l *LoggerStruct) SetModel(model string) { + l.model = model +} + +func (l *LoggerStruct) SetFunction(function string) { + l.function = function +} + +func (l *LoggerStruct) SetLevel(level string) { + l.level = strings.ToUpper(level) +} + +func (l *LoggerStruct) Fatal(v ...interface{}) { + l.Error("%v", v...) + l.Close() + os.Exit(1) +} + +func (l *LoggerStruct) Print(level, format string, v ...interface{}) { + if levelMap[level] >= levelMap[l.level] { + logQueue.RWLock.Lock() + if l.model != "" && l.function != "" { + // l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+Blue+"["+l.model+"."+l.function+"]"+Reset+" - "+format, v...) + logQueue.PushBack(logetContent{ + format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + Blue + "[" + l.model + "." + l.function + "]" + Reset + " - " + format, + v: v, + }) + } else if l.model != "" { + // l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+Blue+"["+l.model+"]"+Reset+" - "+format, v...) + logQueue.PushBack(logetContent{ + format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + Blue + "[" + l.model + "]" + Reset + " - " + format, + v: v, + }) + } else { + // l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+format, v...) + logQueue.PushBack(logetContent{ + format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + format, + v: v, + }) + } + logQueue.RWLock.Unlock() + go logOutput() + } +} + +func (l *LoggerStruct) Println(level string, v ...interface{}) { + l.Print(level, "%v", v...) +} + +func (l *LoggerStruct) Debug(format string, v ...interface{}) { + l.Print(DEBUG, format, v...) +} + +func (l *LoggerStruct) Info(format string, v ...interface{}) { + l.Print(INFO, format, v...) +} + +func (l *LoggerStruct) Warn(format string, v ...interface{}) { + l.Print(WARN, format, v...) +} + +func (l *LoggerStruct) Error(format string, v ...interface{}) { + l.Print(ERROR, format, v...) +} diff --git a/libs/logger/logger_test.go b/libs/logger/logger_test.go new file mode 100644 index 0000000..4a99e43 --- /dev/null +++ b/libs/logger/logger_test.go @@ -0,0 +1,48 @@ +package logger_test + +import ( + "testing" + + "megrez/libs/logger" +) + +func TestLogger(t *testing.T) { + t.Log("TestLogger") + l, err := logger.NewLogger(logger.DEBUG, "stdout") + if err != nil { + t.Fatal(err) + } + l.Info("TestInfo") + l.Warn("TestWarn") + l.Error("TestError") + l.Debug("TestDebug") + // l.Fatal("Fatal") + + t.Log(("SetModel")) + l.SetModel("Logger") + l.Info("SetModel TestInfo") + l.Warn("SetModel TestWarn") + l.Error("SetModel TestError") + l.Debug("SetModel TestDebug") + + t.Log(("SetFunction")) + l.SetFunction("TestLogger") + l.Info("SetFunction TestInfo") + l.Warn("SetFunction TestWarn") + l.Error("SetFunction TestError") + l.Debug("SetFunction TestDebug") + + l.Close() +} + +func BenchmarkLogger(b *testing.B) { + b.Log("BenchmarkLogger") + l, err := logger.NewLogger(logger.DEBUG, "stdout") + if err != nil { + b.Fatal(err) + } + for i := 0; i < b.N; i++ { + go l.Info("BenchmarkLogger: %d", i) + } + l.Close() +} diff --git a/libs/request/request.go b/libs/request/request.go new file mode 100644 index 0000000..3eaa702 --- /dev/null +++ b/libs/request/request.go @@ -0,0 +1,281 @@ +package request + +import ( + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// Client ==> 客户端实例 +type Client struct { + Request *Request + Cookies []*http.Cookie + Result Result +} + +// Request ==> 请求体 +type Request struct { + Url string + Method string + Data io.Reader + ContentType string + Authorization string + UserAgent string + Header map[string]string + Timeout time.Duration + // The proxy type is determined by the URL scheme. "http", + // "https", and "socks5" are supported. If the scheme is empty, + // + // If Proxy is nil or nil *URL, no proxy is used. + ProxyUrl url.URL + Redirect bool + MaxRedirects int +} + +// Result ==> 结果集 +type Result struct { + Header http.Header + Location *url.URL + Body []byte + Status int +} + +// NewRequest ==> 新建请求 +func NewRequest() *Client { + return &Client{ + Request: &Request{ + Method: "GET", + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", + Header: make(map[string]string), + Redirect: false, + MaxRedirects: 10, + }, + Result: Result{}, + } +} + +// Do ==> 执行请求 +func (c *Client) Do() *Client { + //HTTP请求构造 + request, _ := http.NewRequest(c.Request.Method, c.Request.Url, c.Request.Data) + request.Header.Set("Content-Type", c.Request.ContentType) + if c.Request.Authorization != "" { + request.Header.Set("Authorization", c.Request.Authorization) + } + if c.Request.UserAgent != "" { + request.Header.Set("User-Agent", c.Request.UserAgent) + } + if len(c.Cookies) != 0 { + for _, cookie := range c.Cookies { + request.AddCookie(cookie) + } + } + // 支持自定义Header + for k, v := range c.Request.Header { + request.Header.Set(k, v) + } + + var client *http.Client + if c.Request.ProxyUrl == (url.URL{}) { + client = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if !c.Request.Redirect { + return http.ErrUseLastResponse + } + if len(via) >= c.Request.MaxRedirects { + return http.ErrUseLastResponse + } + return nil + }, + } + } else { + client = &http.Client{ + Transport: &http.Transport{Proxy: http.ProxyURL(&c.Request.ProxyUrl)}, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if !c.Request.Redirect { + return http.ErrUseLastResponse + } + if len(via) >= c.Request.MaxRedirects { + return http.ErrUseLastResponse + } + return nil + }, + } + } + if c.Request.Timeout != 0 { + client.Timeout = c.Request.Timeout + } + res, err := client.Do(request) + if err != nil { + // fmt.Println(err) + return c + } + if len(res.Cookies()) > 1 { + c.Cookies = append(c.Cookies, res.Cookies()...) + } + defer res.Body.Close() + c.Result.Status = res.StatusCode + c.Result.Body, _ = io.ReadAll(res.Body) + c.Result.Header = res.Header + return c +} + +// Get ==> 定义请求方式 +func (c *Client) Get() *Client { + c.Request.Method = "GET" + return c +} + +// Post ==> 定义请求方式 +func (c *Client) Post() *Client { + c.Request.Method = "POST" + return c +} + +// Put ==> 定义请求方式 +func (c *Client) Put() *Client { + c.Request.Method = "PUT" + return c +} + +// Patch ==> 定义请求方式 +func (c *Client) Patch() *Client { + c.Request.Method = "PATCH" + return c +} + +// Delete ==> 定义请求方式 +func (c *Client) Delete() *Client { + c.Request.Method = "DELETE" + return c +} + +// SetUrl ==> 定义请求目标 +func (c *Client) SetUrl(url ...any) *Client { + c.Request.Url = fmt.Sprintf(url[0].(string), url[1:]...) + return c +} + +// SetMethod ==> 定义请求方法 +func (c *Client) SetMethod(method string) *Client { + c.Request.Method = method + return c +} + +// SetContentType ==> 定义内容类型 +func (c *Client) SetContentType(contentType string) *Client { + c.Request.ContentType = contentType + return c +} + +// SetUserAgent ==> 定义用户代理 +func (c *Client) SetUserAgent(userAgent string) *Client { + c.Request.UserAgent = userAgent + return c +} + +// SetBody ==> 定义请求内容 +func (c *Client) SetBody(body io.Reader) *Client { + c.Request.Data = body + return c +} + +// SerHeaders ==> 定义请求头列表 +func (c *Client) SetHeaders(headers map[string]string) *Client { + c.Request.Header = headers + return c +} + +// SetHeader ==> 定义请求头 +func (c *Client) SetHeader(key, value string) *Client { + c.Request.Header[key] = value + return c +} + +// SetAuthorization ==> 定义身份验证 +func (c *Client) SetAuthorization(credentials string) *Client { + c.Request.Authorization = credentials + return c +} + +// SetTimeOut ==> 设置会话超时上限 +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.Request.Timeout = timeout + return c +} + +// SetCookie ==> 设置Cookie +func (c *Client) SetCookie(cookie *http.Cookie) *Client { + c.Cookies = append(c.Cookies, cookie) + return c +} + +// SetCookies ==> 设置Cookies +func (c *Client) SetCookies(cookies string) *Client { + cookielist := strings.Split(cookies, "; ") + for _, cookie := range cookielist { + cookiekv := strings.Split(cookie, "=") + c.SetCookie(&http.Cookie{ + Name: cookiekv[0], + Value: strings.Join(cookiekv[1:], "="), + }) + } + return c +} + +// SetProxy ==> 设置代理 +func (c *Client) SetProxy(proxyUrl url.URL) *Client { + c.Request.ProxyUrl = proxyUrl + return c +} + +// SetRedirect ==> 设置重定向 +func (c *Client) SetRedirect(redirect bool) *Client { + c.Request.Redirect = redirect + return c +} + +// SetMaxRedirects ==> 设置最大重定向次数 +func (c *Client) SetMaxRedirects(maxRedirects int) *Client { + c.Request.MaxRedirects = maxRedirects + return c +} + +// GetStatusCode ==> 获取请求状态码 +func (c *Client) GetStatusCode() int { + return c.Result.Status +} + +// GetBody ==> 获取返回内容 +func (c *Client) GetBody() []byte { + return c.Result.Body +} + +// GetBody ==> 获取返回内容 +func (c *Client) GetBodyString() string { + return string(c.Result.Body) +} + +// GetHeaders ==> 获取返回头字典 +func (c *Client) GetHeaders() http.Header { + return c.Result.Header +} + +// GetHeader ==> 获取返回头 +func (c *Client) GetHeader(key string) string { + return c.Result.Header.Get(key) +} + +// SaveToFile ==> 写出结果到文件 +func (c *Client) SaveToFile(filepath string) (err error) { + // Write the body to file + err = os.WriteFile(filepath, c.GetBody(), 0777) + if err != nil { + return err + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..62287d2 --- /dev/null +++ b/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "flag" + "megrez/services/config" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/http" + "megrez/services/instanceController" + "megrez/services/logger" + "megrez/services/redis" + "megrez/services/system" + "runtime" +) + +var ( + configFilePath = "config.yml" + l = logger.Logger.Clone() +) + +func main() { + flag.StringVar(&configFilePath, "c", "config.yml", "config file path") + flag.Parse() + + config.InitConfig(configFilePath) + + defer func() { + redis.Close() + + l.Close() + }() + defer func() { + defer func() { + if err := recover(); err != nil { + l.Error("Panic: %v", err) + buf := make([]byte, 1024) + n := runtime.Stack(buf, false) + l.Error("Stack trace: \n%s", buf[:n]) + } + }() + }() + + logger.InitLogger(config.GetLogLevel(), config.GetLogFile()) + + database.Connect() + redis.Connect() + instanceController.InitInstanceController() + dispatcher.Init() + + go system.Check() + + http.Start() +} diff --git a/models/instance.go b/models/instance.go new file mode 100644 index 0000000..db5cb9c --- /dev/null +++ b/models/instance.go @@ -0,0 +1,61 @@ +package models + +import ( + "slices" + "time" + + "gorm.io/gorm" +) + +type Status int + +const ( + InstanceFail Status = -1 + InstanceRunning Status = 0 + InstancePaused Status = 1 + InstanceStopped Status = 2 + + InstanceReady Status = 3 + InstanceStarting Status = 4 + InstanceStopping Status = 5 + InstancePausing Status = 6 + InstanceRestarting Status = 7 + InstanceModifying Status = 8 + InstanceDeleting Status = 9 +) + +var instanceIngStatus = []Status{InstanceReady, InstanceStarting, InstanceStopping, InstancePausing, InstanceRestarting, InstanceModifying, InstanceDeleting} + +type Instances struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + UserID uint `json:"user_id,omitempty" gorm:"not null"` + ServerID uint `json:"server_id" gorm:"not null"` + + ImageName string `json:"image_name" gorm:"type:varchar(255);not null"` + ContainerName string `json:"container_name,omitempty" gorm:"type:varchar(255);not null"` + CpuOnly bool `json:"cpu_only" gorm:"not null"` + GpuCount int `json:"gpu_count" gorm:"not null"` + VolumeName string `json:"volume_name,omitempty" gorm:"type:varchar(255);not null"` + VolumeSize int `json:"volume_size" gorm:"not null"` + + SshAddress string `json:"ssh_address" gorm:"type:varchar(255);not null"` + SshPasswd string `json:"ssh_passwd" gorm:"type:varchar(255);not null"` + JupyterAddress string `json:"jupyter_address" gorm:"type:varchar(255);not null"` + TensorBoardAddress string `json:"tensor_board_address" gorm:"type:varchar(255);not null"` + GrafanaAddress string `json:"grafana_address" gorm:"type:varchar(255);not null"` + Status Status `json:"status" gorm:"not null"` // -1: fail, 0: running, 1: paused, 2: stopped, 3: readying + + Label string `json:"label" gorm:"type:varchar(255)"` + + CreatedAt time.Time `json:"create_at"` + UpdatedAt time.Time `json:"update_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func InstanceIngStatusCheck(status Status) bool { + if slices.Index(instanceIngStatus, status) != -1 { + return true + } + return false +} diff --git a/models/orders.go b/models/orders.go new file mode 100644 index 0000000..d7c1aa6 --- /dev/null +++ b/models/orders.go @@ -0,0 +1,27 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Orders struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + UserID uint `json:"user_id" gorm:"not null"` + ServerID uint `json:"server_id" gorm:"not null"` + StartTime time.Time `json:"start_time" gorm:"not null"` + EndTime time.Time `json:"end_time" gorm:"not null"` + + GpuNum int `json:"gpu_num" gorm:"not null"` + DiskUsed float64 `json:"disk_used" gorm:"not null"` + + PricePerHour float64 `json:"price_per_gpu_per_hour" gorm:"not null"` + PriceDiskPerHour float64 `json:"price_disk_per_hour" gorm:"not null"` + + Price float64 `json:"price" gorm:"not null"` + + CreatedAt time.Time `json:"create_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} diff --git a/models/servers.go b/models/servers.go new file mode 100644 index 0000000..341a5f2 --- /dev/null +++ b/models/servers.go @@ -0,0 +1,32 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Servers struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + Name string `json:"name" gorm:"type:varchar(255);not null"` + IP string `json:"ip,omitempty" gorm:"type:varchar(255);not null"` + Port int `json:"port,omitempty" gorm:"not null"` + Apikey string `json:"apikey,omitempty" gorm:"type:varchar(255);not null"` + GpuType string `json:"gpu_type" gorm:"type:varchar(255);not null"` + GpuNum int `json:"gpu_num" gorm:"not null"` + GpuDriverVersion string `json:"gpu_driver_version" gorm:"type:varchar(255);not null"` + GpuCudaVersion string `json:"gpu_cuda_version" gorm:"type:varchar(255);not null"` + + CpuCountPerGpu int `json:"cpu_count_per_gpu" gorm:"not null"` + MemoryPerGpu int `json:"memory_per_gpu" gorm:"not null"` // Unit `GB` + VolumeTotal int `json:"volume_total" gorm:"not null"` // Unit `GB` + Price float64 `json:"price" gorm:"not null"` // 1 GPU Per Hour + PriceVolume float64 `json:"price_volume" gorm:"not null"` // 1GB Per Hour + + GpuUsed int `json:"gpu_used" gorm:"not null,default:0"` + VolumeUsed int `json:"volume_used" gorm:"not null,default:0"` + + CreatedAt time.Time `json:"create_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} diff --git a/models/system.go b/models/system.go new file mode 100644 index 0000000..a3ac65b --- /dev/null +++ b/models/system.go @@ -0,0 +1,6 @@ +package models + +type System struct { + Key string `json:"key" gorm:"primary_key;unique;index"` + Value string `json:"value"` +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..ded9dcc --- /dev/null +++ b/models/users.go @@ -0,0 +1,32 @@ +package models + +import ( + "megrez/libs/crypto" + "megrez/services/config" + "time" + + "gorm.io/gorm" +) + +type Users struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + Username string `json:"username" gorm:"type:varchar(255);uniqueIndex;unique;not null"` + Password string `json:"password,omitempty" gorm:"type:varchar(255);not null"` + Role int `json:"role" gorm:"not null,default:0"` + + Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"` + + Balance float64 `json:"balance" gorm:"not null"` + + CreatedAt time.Time `json:"create_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (u *Users) PasswordHash(password string) string { + return crypto.Sha256(password + u.Email + config.GetSystemSalt()) +} + +func (u *Users) CheckPassword(password string) bool { + return u.Password == u.PasswordHash(password) +} diff --git a/routers/.gitignore b/routers/.gitignore new file mode 100644 index 0000000..6f3439a --- /dev/null +++ b/routers/.gitignore @@ -0,0 +1 @@ +web \ No newline at end of file diff --git a/routers/api/v1/admin/images/list.go b/routers/api/v1/admin/images/list.go new file mode 100644 index 0000000..011df4f --- /dev/null +++ b/routers/api/v1/admin/images/list.go @@ -0,0 +1,35 @@ +package images + +import ( + "encoding/json" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + + res := make(map[string]string) + + system := models.System{ + Key: imagesKey, + } + result := database.DB.FirstOrCreate(&system) + if result.Error != nil { + l.Error("get system error: %v", result.Error) + middleware.Result(ctx, res) + return + } + + err := json.Unmarshal([]byte(system.Value), &res) + if err != nil { + l.Error("unmarshal system error: %v", err) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/admin/images/modify.go b/routers/api/v1/admin/images/modify.go new file mode 100644 index 0000000..d1d0461 --- /dev/null +++ b/routers/api/v1/admin/images/modify.go @@ -0,0 +1,48 @@ +package images + +import ( + "encoding/json" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + req := make(map[string]string) + err := ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + system := models.System{ + Key: imagesKey, + } + result := database.DB.FirstOrCreate(&system) + if result.Error != nil { + l.Error("get system config error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + valueBytes, err := json.Marshal(req) + if err != nil { + l.Error("marshal system error: %v", err) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + system.Value = string(valueBytes) + result = database.DB.Save(&system) + if result.Error != nil { + l.Error("save system error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/images/routers.go b/routers/api/v1/admin/images/routers.go new file mode 100644 index 0000000..c3c90d7 --- /dev/null +++ b/routers/api/v1/admin/images/routers.go @@ -0,0 +1,22 @@ +package images + +import ( + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" + + _logger "megrez/libs/logger" +) + +const imagesKey = "images" + +var l *_logger.LoggerStruct + +func InitImages(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Images") + + party.Get("/", listHandler) + party.Post("/", middleware.SuperAdminCheck, modifyHandler) +} diff --git a/routers/api/v1/admin/instance/add.go b/routers/api/v1/admin/instance/add.go new file mode 100644 index 0000000..ff508d4 --- /dev/null +++ b/routers/api/v1/admin/instance/add.go @@ -0,0 +1,104 @@ +package instances + +import ( + "megrez/libs/crypto" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type addReqStruct struct { + UserID uint `json:"user_id"` + ServerID uint `json:"server_id"` + + ImageName string `json:"image_name"` + GpuCount int `json:"gpu_count"` + VolumeSize int `json:"volume_size"` +} + +func addHandler(ctx iris.Context) { + l.SetFunction("addHandler") + + var req addReqStruct + err := ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.UserID == 0 || req.ServerID == 0 || req.ImageName == "" || req.GpuCount <= 0 || req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + } + + user := models.Users{ + ID: req.UserID, + } + result := database.DB.First(&user) + if result.Error != nil { + l.Error("query user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserQueryError, iris.StatusInternalServerError) + return + } + + server := models.Servers{ + ID: req.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(-req.VolumeSize-30)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 || remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(req.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(req.VolumeSize+30)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + UserID: req.UserID, + ServerID: req.ServerID, + ImageName: req.ImageName, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + + SshPasswd: crypto.Hex(16), + + Status: models.InstanceReady, + } + result = database.DB.Create(&instance) + if result.Error != nil { + l.Error("create instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInternalCreateError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Add, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/control.go b/routers/api/v1/admin/instance/control.go new file mode 100644 index 0000000..af4b24a --- /dev/null +++ b/routers/api/v1/admin/instance/control.go @@ -0,0 +1,135 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type controlStruct struct { + Action instanceController.Action `json:"action"` // 1: start, 2: pause , 3: stop, 4: restart +} + +func controlHandler(ctx iris.Context) { + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req controlStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + if models.InstanceIngStatusCheck(instance.Status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStop && instance.Status != models.InstanceRunning && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionPause && instance.Status != models.InstanceRunning { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStart && instance.Status != models.InstanceStopped && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if status == models.InstanceStopped && (req.Action == instanceController.ActionStart || req.Action == instanceController.ActionRestart) { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-instance.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + switch req.Action { + case instanceController.ActionStart: + result = database.DB.Model(&instance).Update("status", models.InstanceStarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStartError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionPause: + result = database.DB.Model(&instance).Update("status", models.InstancePausing) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstancePauseError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionStop: + result = database.DB.Model(&instance).Update("status", models.InstanceStopping) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStopError, iris.StatusInternalServerError) + return + } + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + + case instanceController.ActionRestart: + result = database.DB.Model(&instance).Update("status", models.InstanceRestarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceRestartError, iris.StatusInternalServerError) + return + } + + default: + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Control, + InstanceID: instance.ID, + Status: status, + Action: req.Action, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/delete.go b/routers/api/v1/admin/instance/delete.go new file mode 100644 index 0000000..bd38912 --- /dev/null +++ b/routers/api/v1/admin/instance/delete.go @@ -0,0 +1,61 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("get instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDeleteError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if models.InstanceIngStatusCheck(status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + result = database.DB.Model(&instance).Update("status", models.InstanceDeleting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + if status == models.InstanceRunning || status == models.InstancePaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.VolumeSize+30)) + + // TODO: Price calculation + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Delete, + Status: status, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/detail.go b/routers/api/v1/admin/instance/detail.go new file mode 100644 index 0000000..70dfbeb --- /dev/null +++ b/routers/api/v1/admin/instance/detail.go @@ -0,0 +1,53 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Select("id", "user_id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "tensor_board_address", "grafana_address", "status", "image_name", "created_at").First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.Select("name", "gpu_type", "gpu_num").First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res := instanceStruct{ + Instances: instance, + GpuType: server.GpuType, + GpuNum: server.GpuNum, + GpuUsed: server.GpuUsed, + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/admin/instance/label.go b/routers/api/v1/admin/instance/label.go new file mode 100644 index 0000000..09ff22b --- /dev/null +++ b/routers/api/v1/admin/instance/label.go @@ -0,0 +1,49 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type labelReqStruct struct { + Label string `json:"label"` +} + +func labelHandler(ctx iris.Context) { + l.SetFunction("labelHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req labelReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where(&instance).First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + result = database.DB.Model(&instance).Update("label", req.Label) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceSaveError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/list.go b/routers/api/v1/admin/instance/list.go new file mode 100644 index 0000000..0d0b226 --- /dev/null +++ b/routers/api/v1/admin/instance/list.go @@ -0,0 +1,88 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var instances []models.Instances + totalResult := database.DB.Model(&models.Instances{}).Count(&total) + if totalResult.Error != nil { + l.Error("list instances error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "user_id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "jupyter_address", "tensor_board_address", "grafana_address", "status", "image_name", "label", "created_at").Order("id").Find(&instances) + if result.Error != nil { + l.Error("list instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + res := make([]instanceStruct, len(instances)) + for i, instance := range instances { + res[i] = instanceStruct{ + Instances: instance, + } + user := models.Users{ + ID: instance.UserID, + } + result := database.DB.Select("username").First(&user) + if result.Error == nil { + res[i].Username = user.Username + } else { + l.Error("query user %d error: %v", instance.UserID, result.Error) + } + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server) + if result.Error == nil { + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res[i].ServerName = server.Name + res[i].GpuType = server.GpuType + res[i].GpuNum = server.GpuNum + res[i].GpuUsed = server.GpuUsed + res[i].CpuCountPerGpu = server.CpuCountPerGpu + res[i].MemoryPerGpu = server.MemoryPerGpu + } else { + l.Error("query server %d error: %v", instance.ServerID, result.Error) + } + } + + middleware.ResultWithTotal(ctx, res, total) +} diff --git a/routers/api/v1/admin/instance/modify.go b/routers/api/v1/admin/instance/modify.go new file mode 100644 index 0000000..f9fee12 --- /dev/null +++ b/routers/api/v1/admin/instance/modify.go @@ -0,0 +1,128 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type modifyReqStruct struct { + CpuOnly bool `json:"cpu_only"` + GpuCount *int `json:"gpu_count"` + VolumeSize *int `json:"volume_size"` +} + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req modifyReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.GpuCount != nil { + if *req.GpuCount < 0 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + if req.VolumeSize != nil { + if *req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + if instance.Status != models.InstanceStopped { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.CpuOnly == instance.CpuOnly && req.CpuOnly { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if req.GpuCount != nil { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-*req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(*req.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + if req.VolumeSize != nil { + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(instance.VolumeSize-*req.VolumeSize)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + if remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(*req.VolumeSize-instance.VolumeSize)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + status := instance.Status + result = database.DB.Model(&instance).Update("status", models.InstanceModifying) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Modify, + InstanceID: instance.ID, + Status: status, + + CpuOnly: req.CpuOnly, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/routers.go b/routers/api/v1/admin/instance/routers.go new file mode 100644 index 0000000..5c4320b --- /dev/null +++ b/routers/api/v1/admin/instance/routers.go @@ -0,0 +1,37 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + _logger "megrez/libs/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +type instanceStruct struct { + models.Instances + Username string `json:"username"` + ServerName string `json:"server_name"` + GpuType string `json:"gpu_type"` + GpuNum int `json:"gpu_num"` + GpuUsed int `json:"gpu_used"` + CpuCountPerGpu int `json:"cpu_count_per_gpu"` + MemoryPerGpu int `json:"memory_per_gpu"` +} + +func InitInstances(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Instance") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + party.Put("/{id:uint}", middleware.SuperAdminCheck, controlHandler) + party.Post("/", middleware.SuperAdminCheck, addHandler) + party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler) + party.Post("/{id:uint}/label", middleware.SuperAdminCheck, labelHandler) + party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler) +} diff --git a/routers/api/v1/admin/routers.go b/routers/api/v1/admin/routers.go new file mode 100644 index 0000000..6ba9387 --- /dev/null +++ b/routers/api/v1/admin/routers.go @@ -0,0 +1,20 @@ +package admin + +import ( + "megrez/routers/api/v1/admin/images" + instances "megrez/routers/api/v1/admin/instance" + "megrez/routers/api/v1/admin/servers" + "megrez/routers/api/v1/admin/users" + "megrez/routers/api/v1/middleware" + + "github.com/kataras/iris/v12/core/router" +) + +func InitAdmin(party router.Party) { + party.Use(middleware.Auth, middleware.AuthCheck, middleware.AdminCheck) + + users.InitUser(party.Party("/users")) + servers.InitServer(party.Party("/servers")) + instances.InitInstances(party.Party("/instances")) + images.InitImages(party.Party("/images")) +} diff --git a/routers/api/v1/admin/servers/add.go b/routers/api/v1/admin/servers/add.go new file mode 100644 index 0000000..262a4d6 --- /dev/null +++ b/routers/api/v1/admin/servers/add.go @@ -0,0 +1,56 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func addHandler(ctx iris.Context) { + l.SetFunction("addHandler") + + var s serverStruct + if err := ctx.ReadJSON(&s); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if s.Name == "" || s.IP == "" || s.Port == 0 || s.Apikey == "" || s.GpuType == "" || s.GpuNum == 0 || s.GpuDirverVersion == "" || s.GpuCudaVersion == "" || s.CpuCpuntPerGpu == 0 || s.MemoryPerGpu == 0 || s.VolumeTotal == 0 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + l.Debug("add server: %+v", s) + + server := models.Servers{ + Name: s.Name, + IP: s.IP, + Port: s.Port, + Apikey: s.Apikey, + GpuType: s.GpuType, + GpuNum: s.GpuNum, + GpuDriverVersion: s.GpuDirverVersion, + GpuCudaVersion: s.GpuCudaVersion, + + CpuCountPerGpu: s.CpuCpuntPerGpu, + MemoryPerGpu: s.MemoryPerGpu, + VolumeTotal: s.VolumeTotal, + Price: s.Price, + PriceVolume: s.PriceVolume, + } + result := database.DB.Create(&server) + if result.Error != nil { + l.Error("add server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError) + return + } + + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(server.GpuNum)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(server.VolumeTotal)) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/servers/delete.go b/routers/api/v1/admin/servers/delete.go new file mode 100644 index 0000000..23993ec --- /dev/null +++ b/routers/api/v1/admin/servers/delete.go @@ -0,0 +1,43 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var instances []models.Instances + result := database.DB.Where("server_id = ?", id).Find(&instances) + if result.Error != nil { + l.Error("get instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDeleteError, iris.StatusInternalServerError) + return + } + if len(instances) > 0 { + middleware.Error(ctx, middleware.CodeAdminServerInstanceError, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result = database.DB.Delete(&server) + if result.Error != nil { + l.Error("delete server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDeleteError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/servers/detail.go b/routers/api/v1/admin/servers/detail.go new file mode 100644 index 0000000..f5a0025 --- /dev/null +++ b/routers/api/v1/admin/servers/detail.go @@ -0,0 +1,39 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(id))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(id))).Scan(&server.VolumeUsed) + server.VolumeUsed = server.VolumeTotal - server.VolumeUsed + + middleware.Result(ctx, server) +} diff --git a/routers/api/v1/admin/servers/list.go b/routers/api/v1/admin/servers/list.go new file mode 100644 index 0000000..35727fd --- /dev/null +++ b/routers/api/v1/admin/servers/list.go @@ -0,0 +1,62 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var servers []models.Servers + totalResult := database.DB.Model(&models.Servers{}).Count(&total) + if totalResult.Error != nil { + l.Error("list servers error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "name", "ip", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").Order("id").Find(&servers) + if result.Error != nil { + l.Error("list servers error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + for i, server := range servers { + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].GpuUsed) + servers[i].GpuUsed = server.GpuNum - servers[i].GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].VolumeUsed) + servers[i].VolumeUsed = server.VolumeTotal - servers[i].VolumeUsed + } + + middleware.ResultWithTotal(ctx, servers, total) +} diff --git a/routers/api/v1/admin/servers/modify.go b/routers/api/v1/admin/servers/modify.go new file mode 100644 index 0000000..7d0ce56 --- /dev/null +++ b/routers/api/v1/admin/servers/modify.go @@ -0,0 +1,84 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req serverStruct + if err := ctx.ReadJSON(&req); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError) + return + } + + if req.Name != "" && req.Name != server.Name { + server.Name = req.Name + } + if req.IP != "" && req.IP != server.IP { + server.IP = req.IP + } + if req.Port != 0 && req.Port != server.Port { + server.Port = req.Port + } + if req.Apikey != "" && req.Apikey != server.Apikey { + server.Apikey = req.Apikey + } + if req.GpuType != "" && req.GpuType != server.GpuType { + server.GpuType = req.GpuType + } + if req.GpuNum != 0 && req.GpuNum != server.GpuNum { + server.GpuNum = req.GpuNum + } + if req.GpuDirverVersion != "" && req.GpuDirverVersion != server.GpuDriverVersion { + server.GpuDriverVersion = req.GpuDirverVersion + } + if req.GpuCudaVersion != "" && req.GpuCudaVersion != server.GpuCudaVersion { + server.GpuCudaVersion = req.GpuCudaVersion + } + if req.CpuCpuntPerGpu != 0 && req.CpuCpuntPerGpu != server.CpuCountPerGpu { + server.CpuCountPerGpu = req.CpuCpuntPerGpu + } + if req.MemoryPerGpu != 0 && req.MemoryPerGpu != server.MemoryPerGpu { + server.MemoryPerGpu = req.MemoryPerGpu + } + if req.VolumeTotal != 0 && req.VolumeTotal != server.VolumeTotal { + server.VolumeTotal = req.VolumeTotal + } + if req.Price != 0 && req.Price != server.Price { + server.Price = req.Price + } + if req.PriceVolume != 0 && req.PriceVolume != server.PriceVolume { + server.PriceVolume = req.PriceVolume + } + + result = database.DB.Save(&server) + if result.Error != nil { + l.Error("save server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/servers/routers.go b/routers/api/v1/admin/servers/routers.go new file mode 100644 index 0000000..2f3f8f3 --- /dev/null +++ b/routers/api/v1/admin/servers/routers.go @@ -0,0 +1,40 @@ +package servers + +import ( + "github.com/kataras/iris/v12/core/router" + + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + _logger "megrez/libs/logger" +) + +var l *_logger.LoggerStruct + +type serverStruct struct { + Name string `json:"name,omitempty"` + IP string `json:"ip,omitempty"` + Port int `json:"port,omitempty"` + Apikey string `json:"apikey,omitempty"` + GpuType string `json:"gpu_type,omitempty"` + GpuNum int `json:"gpu_num,omitempty"` + GpuDirverVersion string `json:"gpu_driver_version,omitempty"` + GpuCudaVersion string `json:"gpu_cuda_version,omitempty"` + + CpuCpuntPerGpu int `json:"cpu_count_per_gpu,omitempty"` + MemoryPerGpu int `json:"memory_per_gpu,omitempty"` // Unit `GB` + VolumeTotal int `json:"volume_total,omitempty"` // Unit `GB` + Price float64 `json:"price,omitempty"` // 1 GPU Per Hour + PriceVolume float64 `json:"price_volume,omitempty"` // 1GB Per Hour +} + +func InitServer(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Servers") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + party.Post("/", middleware.SuperAdminCheck, addHandler) + party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler) + party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler) +} diff --git a/routers/api/v1/admin/users/add.go b/routers/api/v1/admin/users/add.go new file mode 100644 index 0000000..82abcb9 --- /dev/null +++ b/routers/api/v1/admin/users/add.go @@ -0,0 +1 @@ +package users diff --git a/routers/api/v1/admin/users/delete.go b/routers/api/v1/admin/users/delete.go new file mode 100644 index 0000000..505bce1 --- /dev/null +++ b/routers/api/v1/admin/users/delete.go @@ -0,0 +1,49 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if id == 1 { + middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusBadRequest) + return + } + + var instances []models.Instances + result := database.DB.Where("user_id = ?", id).Find(&instances) + if result.Error != nil { + l.Error("get user instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusInternalServerError) + return + } + + if len(instances) > 0 { + middleware.Error(ctx, middleware.CodeAdminUserInstanceNoEmpty, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: id, + } + result = database.DB.Delete(&user) + if result.Error != nil { + l.Error("delete user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/users/detail.go b/routers/api/v1/admin/users/detail.go new file mode 100644 index 0000000..adc1392 --- /dev/null +++ b/routers/api/v1/admin/users/detail.go @@ -0,0 +1,31 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: id, + } + result := database.DB.Select("id", "username", "email", "role", "balance", "created_at").First(&user) + if result.Error != nil { + l.Error("detail user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserDetailError, iris.StatusInternalServerError) + return + } + + middleware.Result(ctx, user) +} diff --git a/routers/api/v1/admin/users/list.go b/routers/api/v1/admin/users/list.go new file mode 100644 index 0000000..e8a9ea2 --- /dev/null +++ b/routers/api/v1/admin/users/list.go @@ -0,0 +1,53 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var users []models.Users + totalResult := database.DB.Model(&models.Users{}).Count(&total) + if totalResult.Error != nil { + l.Error("list users error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "username", "email", "role", "balance", "created_at").Order("id").Find(&users) + if result.Error != nil { + l.Error("list users error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError) + return + } + + middleware.ResultWithTotal(ctx, users, total) +} diff --git a/routers/api/v1/admin/users/modify.go b/routers/api/v1/admin/users/modify.go new file mode 100644 index 0000000..32aa034 --- /dev/null +++ b/routers/api/v1/admin/users/modify.go @@ -0,0 +1,65 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type modifyReqStruct struct { + Password *string `json:"password"` + Role *int `json:"role"` +} + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + userId, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req modifyReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if userId == 1 { + if req.Role != nil { + middleware.Error(ctx, middleware.CodeAdminUserModifyError, iris.StatusBadRequest) + return + } + } + + user := models.Users{ + ID: userId, + } + result := database.DB.Where(&user).First(&user) + if result.Error != nil { + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + if req.Password != nil { + if *req.Password != "" { + user.Password = user.PasswordHash(*req.Password) + } + } + + if req.Role != nil { + user.Role = *req.Role + } + + result = database.DB.Save(&user) + if result.Error != nil { + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/users/routers.go b/routers/api/v1/admin/users/routers.go new file mode 100644 index 0000000..c24d690 --- /dev/null +++ b/routers/api/v1/admin/users/routers.go @@ -0,0 +1,22 @@ +package users + +import ( + _logger "megrez/libs/logger" + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +func InitUser(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Users") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + // party.Post("/", addHandler) + party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler) + party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler) +} diff --git a/routers/api/v1/middleware/code.go b/routers/api/v1/middleware/code.go new file mode 100644 index 0000000..730496d --- /dev/null +++ b/routers/api/v1/middleware/code.go @@ -0,0 +1,87 @@ +package middleware + +type ResCode int + +const ( + CodeSuccess ResCode = 200 + CodeServeBusy ResCode = 500 + CodeBadRequest ResCode = 400 + CodeUnauthorized ResCode = 401 + CodeForbidden ResCode = 403 + + CodePasswordError ResCode = 1000 + CodeLoginError ResCode = 1001 + CodeUserNotExist ResCode = 1002 + CodeRegisterRequestError ResCode = 1003 + CodeRegisterError ResCode = 1004 + CodeInternalCreateError ResCode = 1010 + CodeInstanceDeleteError ResCode = 1011 + CodeInstanceQueryError ResCode = 1012 + CodeInternalPatchError ResCode = 1013 + CodeInstanceListError ResCode = 1014 + CodeInstanceDetailError ResCode = 1015 + CodeInstanceStartError ResCode = 1016 + CodeInstancePauseError ResCode = 1017 + CodeInstanceStopError ResCode = 1018 + CodeInstanceRestartError ResCode = 1019 + CodeInstanceStatusError ResCode = 1020 + CodeInstanceSaveError ResCode = 1021 + + CodeServerQueryError ResCode = 1031 + CodeServerSaveError ResCode = 1032 + CodeResourceInsufficient ResCode = 1040 + + CodeAdminServerAddEditError ResCode = 2001 + CodeAdminServerListError ResCode = 2002 + CodeAdminServerDetailError ResCode = 2003 + CodeAdminServerDeleteError ResCode = 2004 + CodeAdminServerInstanceError ResCode = 2005 + CodeAdminUserQueryError ResCode = 2010 + CodeAdminUserListError ResCode = 2011 + CodeAdminUserDetailError ResCode = 2012 + CodeAdminUserDeleteError ResCode = 2013 + CodeAdminUserInstanceNoEmpty ResCode = 2014 + CodeAdminUserModifyError ResCode = 2015 +) + +var codeMsgMap = map[ResCode]string{ + CodeSuccess: "success", + CodeServeBusy: "server busy", + CodeUnauthorized: "unauthorized", + CodeBadRequest: "bad request", + CodeForbidden: "forbidden", + + CodePasswordError: "password error", + CodeLoginError: "login error", + CodeUserNotExist: "user not exist", + CodeRegisterRequestError: "register request error", + CodeRegisterError: "username or email exist", + CodeInternalCreateError: "create error", + CodeInstanceDeleteError: "delete instance error", + CodeInstanceStatusError: "instance status error", + CodeInstanceQueryError: "query instance error", + CodeInstanceListError: "list instance error", + CodeInstanceDetailError: "detail instance error", + CodeInstanceStartError: "start instance error", + CodeInstancePauseError: "pause instance error", + CodeInstanceStopError: "stop instance error", + CodeInstanceRestartError: "restart instance error", + CodeResourceInsufficient: "resource insufficient", + CodeInternalPatchError: "patch error", + CodeInstanceSaveError: "save instance error", + + CodeServerQueryError: "query server error", + CodeServerSaveError: "save server error", + + CodeAdminServerAddEditError: "add server error", + CodeAdminServerListError: "list server error", + CodeAdminServerDetailError: "detail server error", + CodeAdminServerDeleteError: "delete server error", + CodeAdminServerInstanceError: "server instances not empty", + CodeAdminUserQueryError: "query user error", + CodeAdminUserListError: "list user error", + CodeAdminUserDetailError: "detail user error", + CodeAdminUserDeleteError: "delete user error", + CodeAdminUserModifyError: "modify user error", + CodeAdminUserInstanceNoEmpty: "user instances not empty", +} diff --git a/routers/api/v1/middleware/middleware.go b/routers/api/v1/middleware/middleware.go new file mode 100644 index 0000000..7dd99fb --- /dev/null +++ b/routers/api/v1/middleware/middleware.go @@ -0,0 +1,71 @@ +package middleware + +import ( + "megrez/routers/api/v1/sessions" + "megrez/services/redis" + + "github.com/kataras/iris/v12" +) + +func Auth(ctx iris.Context) { + sess := sessions.Session() + sess.UseDatabase(redis.DB) + session := sess.Start(ctx) + + auth, _ := session.GetBoolean("authenticated") + if !auth { + ctx.Values().Set("authenticated", false) + ctx.Next() + return + } else { + ctx.Values().Set("authenticated", true) + } + + userId, _ := session.GetInt("userId") + ctx.Values().Set("userId", userId) + ctx.Values().Set("email", session.GetString("email")) + + role, _ := session.GetInt("role") + ctx.Values().Set("role", role) + + ctx.Next() +} + +func AuthCheck(ctx iris.Context) { + if ctx.Values().Get("authenticated") == nil || !ctx.Values().Get("authenticated").(bool) { + Error(ctx, CodeUnauthorized, iris.StatusUnauthorized) + return + } + + ctx.Next() +} + +func UserCheck(ctx iris.Context) { + usertype, _ := ctx.Values().GetInt("role") + if usertype < 1 { + Error(ctx, CodeForbidden, iris.StatusForbidden) + return + } + + ctx.Next() +} + +func AdminCheck(ctx iris.Context) { + usertype, _ := ctx.Values().GetInt("role") + if usertype < 2 { + Error(ctx, CodeForbidden, iris.StatusForbidden) + return + } + + ctx.Next() +} + +func SuperAdminCheck(ctx iris.Context) { + usertype, _ := ctx.Values().GetInt("role") + if usertype < 3 { + Error(ctx, CodeForbidden, iris.StatusForbidden) + return + } + + ctx.Next() +} diff --git a/routers/api/v1/middleware/response.go b/routers/api/v1/middleware/response.go new file mode 100644 index 0000000..556a550 --- /dev/null +++ b/routers/api/v1/middleware/response.go @@ -0,0 +1,41 @@ +package middleware + +import "github.com/kataras/iris/v12" + +func Success(ctx iris.Context) { + ctx.JSON(Response{ + Code: CodeSuccess, + Msg: codeMsgMap[CodeSuccess], + Data: nil, + }) +} + +func Result(ctx iris.Context, data any) { + ctx.JSON(Response{ + Code: CodeSuccess, + Msg: codeMsgMap[CodeSuccess], + Data: &Data{ + Result: data, + }, + }) +} + +func ResultWithTotal(ctx iris.Context, data any, total int64) { + ctx.JSON(Response{ + Code: CodeSuccess, + Msg: codeMsgMap[CodeSuccess], + Data: &Data{ + Result: data, + Total: total, + }, + }) +} + +func Error(ctx iris.Context, code ResCode, statusCode int) { + ctx.StatusCode(statusCode) + ctx.JSON(Response{ + Code: code, + Msg: codeMsgMap[code], + Data: nil, + }) +} diff --git a/routers/api/v1/middleware/struct.go b/routers/api/v1/middleware/struct.go new file mode 100644 index 0000000..c0466c6 --- /dev/null +++ b/routers/api/v1/middleware/struct.go @@ -0,0 +1,12 @@ +package middleware + +type Response struct { + Code ResCode `json:"code"` + Msg string `json:"msg"` + Data *Data `json:"data"` +} + +type Data struct { + Result any `json:"result"` + Total int64 `json:"total,omitempty"` +} diff --git a/routers/api/v1/routers.go b/routers/api/v1/routers.go new file mode 100644 index 0000000..f8b54ae --- /dev/null +++ b/routers/api/v1/routers.go @@ -0,0 +1,13 @@ +package v1 + +import ( + "megrez/routers/api/v1/admin" + "megrez/routers/api/v1/user" + + "github.com/kataras/iris/v12/core/router" +) + +func InitApiV1(party router.Party) { + admin.InitAdmin(party.Party("/admin")) + user.InitUser(party.Party("/user")) +} diff --git a/routers/api/v1/sessions/session.go b/routers/api/v1/sessions/session.go new file mode 100644 index 0000000..172901c --- /dev/null +++ b/routers/api/v1/sessions/session.go @@ -0,0 +1,14 @@ +package sessions + +import ( + "time" + + "github.com/kataras/iris/v12/sessions" +) + +func Session() *sessions.Sessions { + return sessions.New(sessions.Config{ + Cookie: "session_id", + Expires: 7 * 24 * time.Hour, // 7 Days + }) +} diff --git a/routers/api/v1/user/images/list.go b/routers/api/v1/user/images/list.go new file mode 100644 index 0000000..bd89919 --- /dev/null +++ b/routers/api/v1/user/images/list.go @@ -0,0 +1,35 @@ +package images + +import ( + "encoding/json" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + + res := make(map[string]string) + + system := models.System{ + Key: imagesKey, + } + result := database.DB.First(&system) + if result.Error != nil { + l.Error("get system error: %v", result.Error) + middleware.Result(ctx, res) + return + } + + err := json.Unmarshal([]byte(system.Value), &res) + if err != nil { + l.Error("unmarshal system error: %v", err) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/user/images/routers.go b/routers/api/v1/user/images/routers.go new file mode 100644 index 0000000..0574297 --- /dev/null +++ b/routers/api/v1/user/images/routers.go @@ -0,0 +1,20 @@ +package images + +import ( + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" + + _logger "megrez/libs/logger" +) + +const imagesKey = "images" + +var l *_logger.LoggerStruct + +func InitImages(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User.Images") + + party.Get("/", listHandler) +} diff --git a/routers/api/v1/user/instances/add.go b/routers/api/v1/user/instances/add.go new file mode 100644 index 0000000..0ddf6c5 --- /dev/null +++ b/routers/api/v1/user/instances/add.go @@ -0,0 +1,99 @@ +package instances + +import ( + "megrez/libs/crypto" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type addReqStruct struct { + ServerID uint `json:"server_id"` + + ImageName string `json:"image_name"` + GpuCount int `json:"gpu_count"` + VolumeSize int `json:"volume_size"` +} + +func addHandler(ctx iris.Context) { + l.SetFunction("addHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req addReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.ServerID == 0 || req.ImageName == "" || req.GpuCount <= 0 || req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + } + + server := models.Servers{ + ID: req.ServerID, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(-req.VolumeSize-30)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 || remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(req.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(req.VolumeSize+30)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + UserID: uint(userId), + ServerID: req.ServerID, + ImageName: req.ImageName, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + + SshPasswd: crypto.Hex(16), + + Status: models.InstanceReady, + } + result = database.DB.Create(&instance) + if result.Error != nil { + l.Error("create instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInternalCreateError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Add, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/control.go b/routers/api/v1/user/instances/control.go new file mode 100644 index 0000000..cd7d1ac --- /dev/null +++ b/routers/api/v1/user/instances/control.go @@ -0,0 +1,141 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type controlStruct struct { + Action instanceController.Action `json:"action"` // 1: start, 2: pause , 3: stop, 4: restart +} + +func controlHandler(ctx iris.Context) { + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req controlStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where("id = ?", id).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + if models.InstanceIngStatusCheck(instance.Status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStop && instance.Status != models.InstanceRunning && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionPause && instance.Status != models.InstanceRunning { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStart && instance.Status != models.InstanceStopped && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if status == models.InstanceStopped && (req.Action == instanceController.ActionStart || req.Action == instanceController.ActionRestart) { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-instance.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + switch req.Action { + case instanceController.ActionStart: + result = database.DB.Model(&instance).Update("status", models.InstanceStarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStartError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionPause: + result = database.DB.Model(&instance).Update("status", models.InstancePausing) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstancePauseError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionStop: + result = database.DB.Model(&instance).Update("status", models.InstanceStopping) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStopError, iris.StatusInternalServerError) + return + } + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + + case instanceController.ActionRestart: + result = database.DB.Model(&instance).Update("status", models.InstanceRestarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceRestartError, iris.StatusInternalServerError) + return + } + + default: + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Control, + InstanceID: instance.ID, + Status: status, + Action: req.Action, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/delete.go b/routers/api/v1/user/instances/delete.go new file mode 100644 index 0000000..1867ff5 --- /dev/null +++ b/routers/api/v1/user/instances/delete.go @@ -0,0 +1,65 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var instance models.Instances + result := database.DB.Where("id = ?", id).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("get instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDeleteError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if models.InstanceIngStatusCheck(status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + result = database.DB.Model(&instance).Update("status", models.InstanceDeleting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + if status == models.InstanceRunning || status == models.InstancePaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.VolumeSize+30)) + + // TODO: Price calculation + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Delete, + Status: status, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/detail.go b/routers/api/v1/user/instances/detail.go new file mode 100644 index 0000000..efda447 --- /dev/null +++ b/routers/api/v1/user/instances/detail.go @@ -0,0 +1,61 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where("id = ?", id).Where("user_id = ?", userId).Select("id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "tensor_board_address", "grafana_address", "status", "image_name", "created_at").First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res := instanceStruct{ + Instances: instance, + GpuType: server.GpuType, + GpuNum: server.GpuNum, + GpuUsed: server.GpuUsed, + CpuCountPerGpu: server.CpuCountPerGpu, + MemoryPerGpu: server.MemoryPerGpu, + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/user/instances/label.go b/routers/api/v1/user/instances/label.go new file mode 100644 index 0000000..61f3bf9 --- /dev/null +++ b/routers/api/v1/user/instances/label.go @@ -0,0 +1,55 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type labelReqStruct struct { + Label string `json:"label"` +} + +func labelHandler(ctx iris.Context) { + l.SetFunction("labelHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req labelReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where(&instance).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + result = database.DB.Model(&instance).Update("label", req.Label) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceSaveError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/list.go b/routers/api/v1/user/instances/list.go new file mode 100644 index 0000000..736ac11 --- /dev/null +++ b/routers/api/v1/user/instances/list.go @@ -0,0 +1,85 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var instances []models.Instances + totalResult := database.DB.Model(&models.Instances{}).Where("user_id = ?", userId).Count(&total) + if totalResult.Error != nil { + l.Error("list instances error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Where("user_id = ?", userId).Limit(limit).Offset(offset).Select("id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "jupyter_address", "tensor_board_address", "grafana_address", "status", "image_name", "label", "created_at").Order("id").Find(&instances) + if result.Error != nil { + l.Error("list instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + res := make([]instanceStruct, len(instances)) + for i, instance := range instances { + res[i] = instanceStruct{ + Instances: instance, + } + server := models.Servers{ + ID: instance.ServerID, + } + result := database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server) + if result.Error != nil { + l.Error("query server %d error: %v", instance.ServerID, result.Error) + continue + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res[i].ServerName = server.Name + res[i].GpuType = server.GpuType + res[i].GpuNum = server.GpuNum + res[i].GpuUsed = server.GpuUsed + res[i].CpuCountPerGpu = server.CpuCountPerGpu + res[i].MemoryPerGpu = server.MemoryPerGpu + } + + middleware.ResultWithTotal(ctx, res, total) +} diff --git a/routers/api/v1/user/instances/modify.go b/routers/api/v1/user/instances/modify.go new file mode 100644 index 0000000..dbe32e8 --- /dev/null +++ b/routers/api/v1/user/instances/modify.go @@ -0,0 +1,134 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type modifyReqStruct struct { + CpuOnly bool `json:"cpu_only"` + GpuCount *int `json:"gpu_count"` + VolumeSize *int `json:"volume_size"` +} + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req modifyReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.GpuCount != nil { + if *req.GpuCount < 0 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + if req.VolumeSize != nil { + if *req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where(&instance).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + if instance.Status != models.InstanceStopped { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.CpuOnly == instance.CpuOnly && req.CpuOnly { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if req.GpuCount != nil { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-*req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(*req.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + if req.VolumeSize != nil { + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(instance.VolumeSize-*req.VolumeSize)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + if remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(*req.VolumeSize-instance.VolumeSize)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + status := instance.Status + result = database.DB.Model(&instance).Update("status", models.InstanceModifying) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Modify, + InstanceID: instance.ID, + Status: status, + + CpuOnly: req.CpuOnly, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/routers.go b/routers/api/v1/user/instances/routers.go new file mode 100644 index 0000000..b2b2e78 --- /dev/null +++ b/routers/api/v1/user/instances/routers.go @@ -0,0 +1,38 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + _logger "megrez/libs/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +type instanceStruct struct { + models.Instances + ServerName string `json:"server_name"` + GpuType string `json:"gpu_type"` + GpuNum int `json:"gpu_num"` + GpuUsed int `json:"gpu_used"` + CpuCountPerGpu int `json:"cpu_count_per_gpu"` + MemoryPerGpu int `json:"memory_per_gpu"` +} + +func InitInstances(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User.Instance") + + party.Use(middleware.AuthCheck, middleware.UserCheck) + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + party.Put("/{id:uint}", controlHandler) + party.Post("/", addHandler) + party.Post("/{id:uint}", modifyHandler) + party.Post("/{id:uint}/label", labelHandler) + party.Delete("/{id:uint}", deleteHandler) +} diff --git a/routers/api/v1/user/login.go b/routers/api/v1/user/login.go new file mode 100644 index 0000000..4d5852e --- /dev/null +++ b/routers/api/v1/user/login.go @@ -0,0 +1,71 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/routers/api/v1/sessions" + "megrez/services/database" + "megrez/services/redis" + "time" + + "github.com/kataras/iris/v12" +) + +type userLoginStruct struct { + Account string `json:"account"` + Password string `json:"password"` + // HCaptchaToken string + // CaptchaType string + // Key string +} + +func loginHandler(ctx iris.Context) { + sess := sessions.Session() + session := sess.Start(ctx) + sess.UseDatabase(redis.DB) + + var userReq userLoginStruct + if err := ctx.ReadJSON(&userReq); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if userReq.Account == "" || userReq.Password == "" { + middleware.Error(ctx, middleware.CodeLoginError, iris.StatusBadRequest) + return + } + + user := models.Users{} + result := database.DB.Where("username = ?", userReq.Account).Or("email = ?", userReq.Account).First(&user) + if result.Error != nil { + middleware.Error(ctx, middleware.CodeLoginError, iris.StatusBadRequest) + return + } + + if !user.CheckPassword(userReq.Password) { + middleware.Error(ctx, middleware.CodeLoginError, iris.StatusBadRequest) + return + } + + if user.Role < 1 { + middleware.Error(ctx, middleware.CodeForbidden, iris.StatusForbidden) + return + } + + session.Set("authenticated", true) + session.Set("userId", user.ID) + session.Set("username", user.Username) + session.Set("email", user.Email) + session.Set("role", user.Role) + session.Set("loginTime", time.Now().Unix()) + + profile := profile{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: user.Role, + Balance: user.Balance, + } + + middleware.Result(ctx, profile) +} diff --git a/routers/api/v1/user/logout.go b/routers/api/v1/user/logout.go new file mode 100644 index 0000000..e586987 --- /dev/null +++ b/routers/api/v1/user/logout.go @@ -0,0 +1,26 @@ +package user + +import ( + "megrez/routers/api/v1/middleware" + "megrez/routers/api/v1/sessions" + "megrez/services/redis" + "net/http" + "time" + + "github.com/kataras/iris/v12" +) + +func logoutHandler(ctx iris.Context) { + sess := sessions.Session() + session := sess.Start(ctx) + sess.UseDatabase(redis.DB) + sessionId := session.ID() + + session.Destroy() + ctx.SetCookie(&http.Cookie{ + Name: "session_id", + Value: sessionId, + Expires: time.Now(), + }) + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/orders.go b/routers/api/v1/user/orders.go new file mode 100644 index 0000000..a00006b --- /dev/null +++ b/routers/api/v1/user/orders.go @@ -0,0 +1 @@ +package user diff --git a/routers/api/v1/user/profile.go b/routers/api/v1/user/profile.go new file mode 100644 index 0000000..934ee64 --- /dev/null +++ b/routers/api/v1/user/profile.go @@ -0,0 +1,46 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type profile struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Role int `json:"role"` + Balance float64 `json:"balance"` +} + +func profileHandler(ctx iris.Context) { + l.SetFunction("profileHandler") + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: uint(userId), + } + result := database.DB.First(&user) + if result.Error != nil { + l.Error("get user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError) + return + } + + profile := profile{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: user.Role, + Balance: user.Balance, + } + + middleware.Result(ctx, profile) +} diff --git a/routers/api/v1/user/register.go b/routers/api/v1/user/register.go new file mode 100644 index 0000000..9235ed7 --- /dev/null +++ b/routers/api/v1/user/register.go @@ -0,0 +1,52 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/config" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type registerStruct struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +func registerHandler(ctx iris.Context) { + l.SetFunction("registerHandler") + + var userReq registerStruct + if err := ctx.ReadJSON(&userReq); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if userReq.Username == "" || userReq.Email == "" || userReq.Password == "" { + middleware.Error(ctx, middleware.CodeRegisterRequestError, iris.StatusBadRequest) + return + } + + user := models.Users{ + Username: userReq.Username, + Email: userReq.Email, + Role: 0, + Balance: 0, + } + user.Password = user.PasswordHash(userReq.Password) + + if !config.GetSystemVerify() { + user.Role = 1 + } + + result := database.DB.Create(&user) + if result.Error != nil { + l.Error("create user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeRegisterError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/resetPassword.go b/routers/api/v1/user/resetPassword.go new file mode 100644 index 0000000..2c7ecbc --- /dev/null +++ b/routers/api/v1/user/resetPassword.go @@ -0,0 +1,55 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type resetPasswordStruct struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +func resetPasswordHandler(ctx iris.Context) { + l.SetFunction("resetPasswordHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var resetPassword resetPasswordStruct + if err := ctx.ReadJSON(&resetPassword); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: uint(userId), + } + result := database.DB.First(&user) + if result.Error != nil { + l.Error("get user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError) + return + } + + if !user.CheckPassword(resetPassword.OldPassword) { + middleware.Error(ctx, middleware.CodePasswordError, iris.StatusBadRequest) + return + } + + user.Password = user.PasswordHash(resetPassword.NewPassword) + result = database.DB.Save(&user) + if result.Error != nil { + l.Error("save user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInternalPatchError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/routers.go b/routers/api/v1/user/routers.go new file mode 100644 index 0000000..2668309 --- /dev/null +++ b/routers/api/v1/user/routers.go @@ -0,0 +1,32 @@ +package user + +import ( + "megrez/routers/api/v1/middleware" + "megrez/routers/api/v1/user/images" + "megrez/routers/api/v1/user/instances" + "megrez/routers/api/v1/user/servers" + "megrez/services/logger" + + _logger "megrez/libs/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +func InitUser(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User") + + party.Use(middleware.Auth) + + party.Post("/login", loginHandler) + party.Get("/logout", middleware.AuthCheck, logoutHandler) + party.Post("/register", registerHandler) + party.Get("/profile", middleware.AuthCheck, profileHandler) + party.Post("/resetPassword", middleware.AuthCheck, resetPasswordHandler) + + servers.InitServers(party.Party("/servers")) + instances.InitInstances(party.Party("/instances")) + images.InitImages(party.Party("/images")) +} diff --git a/routers/api/v1/user/servers/detail.go b/routers/api/v1/user/servers/detail.go new file mode 100644 index 0000000..c8059fe --- /dev/null +++ b/routers/api/v1/user/servers/detail.go @@ -0,0 +1,39 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result := database.DB.Select("id", "name", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(id))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(id))).Scan(&server.VolumeUsed) + server.VolumeUsed = server.VolumeTotal - server.VolumeUsed + + middleware.Result(ctx, server) +} diff --git a/routers/api/v1/user/servers/list.go b/routers/api/v1/user/servers/list.go new file mode 100644 index 0000000..2957009 --- /dev/null +++ b/routers/api/v1/user/servers/list.go @@ -0,0 +1,62 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("serversHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var servers []models.Servers + totalResult := database.DB.Model(&models.Servers{}).Count(&total) + if totalResult.Error != nil { + l.Error("list servers error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "name", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").Order("id").Find(&servers) + if result.Error != nil { + l.Error("list servers error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + for i, server := range servers { + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].GpuUsed) + servers[i].GpuUsed = server.GpuNum - servers[i].GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].VolumeUsed) + servers[i].VolumeUsed = server.VolumeTotal - servers[i].VolumeUsed + } + + middleware.ResultWithTotal(ctx, servers, total) +} diff --git a/routers/api/v1/user/servers/routers.go b/routers/api/v1/user/servers/routers.go new file mode 100644 index 0000000..8c91b06 --- /dev/null +++ b/routers/api/v1/user/servers/routers.go @@ -0,0 +1,19 @@ +package servers + +import ( + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" + + _logger "megrez/libs/logger" +) + +var l *_logger.LoggerStruct + +func InitServers(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User.Servers") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) +} diff --git a/routers/index/routers.go b/routers/index/routers.go new file mode 100644 index 0000000..0dfa714 --- /dev/null +++ b/routers/index/routers.go @@ -0,0 +1,13 @@ +package index + +import ( + "github.com/kataras/iris/v12" +) + +func InitIndex(app *iris.Application) { + app.HandleDir("/", GetWebFS(), iris.DirOptions{ + IndexName: "/index.html", + Compress: true, + SPA: true, + }) +} diff --git a/routers/index/web.go b/routers/index/web.go new file mode 100644 index 0000000..48fc94b --- /dev/null +++ b/routers/index/web.go @@ -0,0 +1,14 @@ +package index + +import ( + "embed" + "io/fs" +) + +//go:embed web +var assets embed.FS + +func GetWebFS() fs.FS { + serverRoot, _ := fs.Sub(assets, "web") + return serverRoot +} diff --git a/routers/index/web/.gitkeep b/routers/index/web/.gitkeep new file mode 100644 index 0000000..291b675 --- /dev/null +++ b/routers/index/web/.gitkeep @@ -0,0 +1 @@ +Put FrontEnd Dist File Here \ No newline at end of file diff --git a/routers/middleware.go b/routers/middleware.go new file mode 100644 index 0000000..8b7d1ab --- /dev/null +++ b/routers/middleware.go @@ -0,0 +1,11 @@ +package routers + +import ( + "github.com/kataras/iris/v12" +) + +func middleware(ctx iris.Context) { + ctx.Next() + + l.Info("IP: %s - %s %d %s %d - User-Agent: %s", ctx.RemoteAddr(), ctx.Request().Method, ctx.GetStatusCode(), ctx.Request().RequestURI, ctx.Request().ContentLength, ctx.Request().UserAgent()) +} diff --git a/routers/routers.go b/routers/routers.go new file mode 100644 index 0000000..9cd40de --- /dev/null +++ b/routers/routers.go @@ -0,0 +1,21 @@ +package routers + +import ( + _logger "megrez/libs/logger" + v1 "megrez/routers/api/v1" + "megrez/routers/index" + "megrez/services/logger" + + "github.com/kataras/iris/v12" +) + +var l *_logger.LoggerStruct + +func InitRouter(app *iris.Application) { + l = logger.Logger.Clone() + l.SetModel("HTTP") + + app.Use(middleware) + v1.InitApiV1(app.Party("/api/v1")) + index.InitIndex(app) +} diff --git a/services/config/config.go b/services/config/config.go new file mode 100644 index 0000000..eb63ce5 --- /dev/null +++ b/services/config/config.go @@ -0,0 +1,74 @@ +package config + +import "megrez/services/logger" + +var l = logger.Logger.Clone() +var configPath string + +type configStruct struct { + Http httpStruct `yaml:"http,omitempty"` + Database databaseStruct `yaml:"database,omitempty"` + Redis redisStruct `yaml:"redis,omitempty"` + Log logStruct `yaml:"log,omitempty"` + System systemStruct `yaml:"system,omitempty"` +} + +type httpStruct struct { + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` +} + +type databaseStruct struct { + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Database string `yaml:"database,omitempty"` + SSLMode bool `yaml:"ssl,omitempty"` +} + +type redisStruct struct { + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Password string `yaml:"password,omitempty"` + Database int `yaml:"database,omitempty"` + SentinelPassword string `yaml:"sentinel_password,omitempty"` +} + +type logStruct struct { + Level string `yaml:"level,omitempty"` + File string `yaml:"file,omitempty"` +} + +type systemStruct struct { + Salt string `yaml:"salt,omitempty"` + Verify bool `yaml:"verify,omitempty"` +} + +var config = configStruct{ + Http: httpStruct{ + Host: "0.0.0.0", + Port: 8080, + }, + Database: databaseStruct{ + Host: "localhost", + Port: 5432, + Username: "GpuManager", + Password: "GpuManager", + Database: "GpuManager", + }, + Redis: redisStruct{ + Host: "localhost", + Port: 6379, + Password: "GpuManager", + Database: 0, + }, + Log: logStruct{ + Level: "DEBUG", + File: "data/logs/backend.log", + }, + System: systemStruct{ + Salt: "", + Verify: false, + }, +} diff --git a/services/config/exporter.go b/services/config/exporter.go new file mode 100644 index 0000000..f25337f --- /dev/null +++ b/services/config/exporter.go @@ -0,0 +1,58 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +func InitConfig(path string) { + l.SetFunction("InitConfig") + + configPath = path + c, err := os.ReadFile(path) + if err != nil { + l.Fatal("Failed to read config file", err) + } + err = yaml.Unmarshal(c, &config) + if err != nil { + l.Fatal("Failed to unmarshal config file", err) + } + l.SetLevel(config.GetLogLevel()) +} + +func GetDatabase() databaseStruct { + return config.GetDatabase() +} + +func GetHttpAddress() string { + return config.GetHttpAddress() +} + +func GetRedis() redisStruct { + return config.GetRedis() +} + +func GetLogLevel() string { + return config.GetLogLevel() +} + +func GetLogFile() string { + return config.GetLogFile() +} + +func GetSystemSalt() string { + return config.GetSystemSalt() +} + +func SetSystemSalt(salt string) { + config.SetSystemSalt(salt) +} + +func GetSystemVerify() bool { + return config.GetSystemVerify() +} + +func Save() error { + return config.Save() +} diff --git a/services/config/internal.go b/services/config/internal.go new file mode 100644 index 0000000..29fa092 --- /dev/null +++ b/services/config/internal.go @@ -0,0 +1,65 @@ +package config + +import ( + "errors" + "os" + "strconv" + + "gopkg.in/yaml.v3" +) + +func (c *configStruct) GetDatabase() databaseStruct { + return c.Database +} + +func (c *configStruct) GetHttpAddress() string { + return c.Http.Host + ":" + strconv.Itoa(c.Http.Port) +} + +func (c *configStruct) GetRedis() redisStruct { + return c.Redis +} + +func (c *configStruct) GetLogLevel() string { + return c.Log.Level +} + +func (c *configStruct) GetLogFile() string { + return c.Log.File +} + +func (c *configStruct) GetSystemSalt() string { + return c.System.Salt +} + +func (c *configStruct) SetSystemSalt(salt string) { + c.System.Salt = salt +} + +func (c *configStruct) GetSystemVerify() bool { + return c.System.Verify +} + +func (c *configStruct) Save() error { + return c.save() +} + +func (c *configStruct) save() error { + // Save config to file + bytes, err := yaml.Marshal(c) + if err != nil { + return errors.New("yaml marshal Error: " + err.Error()) + } + + f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + return errors.New("open file Error: " + err.Error()) + } + + _, err = f.Write(bytes) + if err != nil { + return errors.New("write file Error: " + err.Error()) + } + + return nil +} diff --git a/services/database/database.go b/services/database/database.go new file mode 100644 index 0000000..01d7483 --- /dev/null +++ b/services/database/database.go @@ -0,0 +1,39 @@ +package database + +import ( + "fmt" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + + _logger "megrez/libs/logger" + "megrez/services/config" + "megrez/services/logger" + + __logger "gorm.io/gorm/logger" +) + +var l *_logger.LoggerStruct + +var DB *gorm.DB + +func Connect() (err error) { + l = logger.Logger.Clone() + l.SetModel("database") + l.SetFunction("Connect") + + c := config.GetDatabase() + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", c.Host, c.Username, c.Password, c.Database, c.Port) + DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ + PrepareStmt: true, + Logger: __logger.Default.LogMode(__logger.Silent), + }) + if err != nil { + l.Fatal("Failed to connect to database", err) + } else { + l.Info("Database Connect Success") + } + + autoMigrate() + return +} diff --git a/services/database/migrate.go b/services/database/migrate.go new file mode 100644 index 0000000..337dedf --- /dev/null +++ b/services/database/migrate.go @@ -0,0 +1,34 @@ +package database + +import "megrez/models" + +func autoMigrate() (err error) { + l.SetFunction("autoMigration") + + err = DB.AutoMigrate(&models.Instances{}) + if err != nil { + l.Fatal("Failed to migrate containers", err) + } + + err = DB.AutoMigrate(&models.Orders{}) + if err != nil { + l.Fatal("Failed to migrate orders", err) + } + + err = DB.AutoMigrate(&models.Servers{}) + if err != nil { + l.Fatal("Failed to migrate servers", err) + } + + err = DB.AutoMigrate(&models.System{}) + if err != nil { + l.Fatal("Failed to migrate system", err) + } + + err = DB.AutoMigrate(&models.Users{}) + if err != nil { + l.Fatal("Failed to migrate users", err) + } + + return nil +} diff --git a/services/dispatcher/add.go b/services/dispatcher/add.go new file mode 100644 index 0000000..db69767 --- /dev/null +++ b/services/dispatcher/add.go @@ -0,0 +1,57 @@ +package dispatcher + +import ( + "context" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +func add(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("add") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + containerName, volumeName, err := instanceController.Create(&instance) + if err != nil { + lc.Error("create instance error: %v", err) + ctx := context.Background() + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(instance.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(instance.VolumeSize+30)) + return + } + + server.GpuUsed += instance.GpuCount + server.VolumeUsed += instance.VolumeSize + 30 + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + if volumeName != "" { + lc.Info("create volume success: %v", volumeName) + } + lc.Info("create instance success: %v", containerName) + + return +} diff --git a/services/dispatcher/control.go b/services/dispatcher/control.go new file mode 100644 index 0000000..57daad1 --- /dev/null +++ b/services/dispatcher/control.go @@ -0,0 +1,128 @@ +package dispatcher + +import ( + "context" + "errors" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +func control(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("control") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + if models.InstanceIngStatusCheck(data.Status) { + lc.Error("instance status error") + return errors.New("instance status error") + } + + if data.Action == instanceController.ActionPause && data.Status != models.InstanceRunning { + lc.Error("instance status error") + return errors.New("instance status error") + } + + if data.Action == instanceController.ActionStart && data.Status != models.InstanceStopped && data.Status != models.InstancePaused { + lc.Error("instance status error") + return errors.New("instance status error") + } + + switch data.Action { + case instanceController.ActionStart: + if data.Status == models.InstancePaused { + err = instanceController.Continue(&instance) + if err != nil { + lc.Error("instance continue error: %v", err) + return + } + } else if data.Status == models.InstanceStopped { + err = instanceController.Restart(&instance) + if err != nil { + lc.Error("instance restart error: %v", err) + ctx := context.Background() + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(instance.GpuCount)) + return + } + } + + if data.Status == models.InstanceStopped && instance.Status == models.InstanceRunning { + server.GpuUsed += instance.GpuCount + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + } + + lc.Info("start instance success: %v", instance.ID) + + case instanceController.ActionPause: + err = instanceController.Pause(&instance) + if err != nil { + lc.Error("instance pause error: %v", err) + return + } + + lc.Info("pause instance success: %v", instance.ID) + + case instanceController.ActionStop: + err = instanceController.Stop(&instance) + if err != nil { + lc.Error("instance stop error: %v", err) + return + } + + if (data.Status == models.InstanceRunning || data.Status == models.InstancePaused) && instance.Status == models.InstanceStopped { + server.GpuUsed -= instance.GpuCount + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + } + + lc.Info("stop instance success: %v", instance.ID) + + case instanceController.ActionRestart: + err = instanceController.Restart(&instance) + if err != nil { + lc.Error("instance restart error: %v", err) + return + } + + if data.Status == models.InstanceStopped && instance.Status == models.InstanceRunning { + server.GpuUsed += instance.GpuCount + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + } + + lc.Info("restart instance success: %v", instance.ID) + + default: + } + + return +} diff --git a/services/dispatcher/delete.go b/services/dispatcher/delete.go new file mode 100644 index 0000000..edeafd9 --- /dev/null +++ b/services/dispatcher/delete.go @@ -0,0 +1,58 @@ +package dispatcher + +import ( + "context" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +func delete(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("delete") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + err = instanceController.Delete(&instance) + if err != nil { + lc.Error("delete instance error: %v", err) + ctx := context.Background() + if data.Status == models.InstanceRunning || data.Status == models.InstancePaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(-instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(-instance.VolumeSize-30)) + return + } + + server.VolumeUsed -= instance.VolumeSize + 30 + if data.Status == models.InstanceRunning || data.Status == models.InstancePaused { + server.GpuUsed -= instance.GpuCount + } + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + lc.Info("delete instance success: %v", instance.ID) + + return +} diff --git a/services/dispatcher/init.go b/services/dispatcher/init.go new file mode 100644 index 0000000..5598f70 --- /dev/null +++ b/services/dispatcher/init.go @@ -0,0 +1,45 @@ +package dispatcher + +import ( + "context" + "megrez/models" + "megrez/services/database" + "megrez/services/logger" + "megrez/services/redis" + "strconv" +) + +func Init() { + l = logger.Logger.Clone() + l.SetModel("dispatcher") + l.SetFunction("Init") + + status = make(map[int]bool) + // run() + + var servers []models.Servers + result := database.DB.Select("id", "gpu_num", "gpu_used", "volume_total", "volume_used").Find(&servers) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return + } + + ctx := context.Background() + + for _, server := range servers { + status[int(server.ID)] = false + go run(server.ID, false) + + _, err := redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Result() + if err != nil { + l.Info("init server %d remain gpu: %d", server.ID, server.GpuNum-server.GpuUsed) + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(server.GpuNum-server.GpuUsed)) + } + + _, err = redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Result() + if err != nil { + l.Info("init server %d remain volume: %d", server.ID, server.VolumeTotal-server.VolumeUsed) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(server.VolumeTotal-server.VolumeUsed)) + } + } +} diff --git a/services/dispatcher/interface.go b/services/dispatcher/interface.go new file mode 100644 index 0000000..61805a1 --- /dev/null +++ b/services/dispatcher/interface.go @@ -0,0 +1,33 @@ +package dispatcher + +import ( + _logger "megrez/libs/logger" + "megrez/models" + "megrez/services/instanceController" + "sync" +) + +var l *_logger.LoggerStruct +var status map[int]bool +var mx sync.Mutex + +type Data struct { + Type Type `json:"type"` // 1:Add 2:Contorl 3:Modify 4:Delete + InstanceID uint `json:"instance_id"` + Status models.Status `json:"status"` + + CpuOnly bool `json:"cpu_only,omitempty"` + GpuCount *int `json:"gpu_count,omitempty"` + VolumeSize *int `json:"volume_size,omitempty"` + + Action instanceController.Action `json:"action,omitempty"` +} + +type Type int + +const ( + Add Type = iota + 1 + Control + Modify + Delete +) diff --git a/services/dispatcher/modify.go b/services/dispatcher/modify.go new file mode 100644 index 0000000..67061eb --- /dev/null +++ b/services/dispatcher/modify.go @@ -0,0 +1,84 @@ +package dispatcher + +import ( + "context" + "errors" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +type modifyDataStruct struct { + CpuOnly bool `json:"cpu_only"` + GpuCount *int `json:"gpu_count"` + VolumeSize *int `json:"volume_size"` +} + +func modify(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("modify") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + if data.Status != models.InstanceStopped { + lc.Error("instance status error") + return errors.New("instance status error") + } + + if data.CpuOnly == instance.CpuOnly && data.CpuOnly { + lc.Error("instance already cpu_only mode") + return errors.New("instance already cpu_only mode") + } + + oldVolumeSize := instance.VolumeSize + gpuCount := instance.GpuCount + volumeSize := instance.VolumeSize + if data.GpuCount != nil { + gpuCount = *data.GpuCount + } + if data.VolumeSize != nil { + volumeSize = *data.VolumeSize + } + + err = instanceController.Patch(&instance, gpuCount, volumeSize, data.CpuOnly) + if err != nil { + lc.Error("patch instance error: %v", err) + ctx := context.Background() + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(gpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(volumeSize-oldVolumeSize)) + return + } + + if data.Status == models.InstanceStopped { + server.GpuUsed += instance.GpuCount + } + server.VolumeUsed += instance.VolumeSize - oldVolumeSize + + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + lc.Info("modify instance success: %d", instance.ID) + + return +} diff --git a/services/dispatcher/push.go b/services/dispatcher/push.go new file mode 100644 index 0000000..4831cf5 --- /dev/null +++ b/services/dispatcher/push.go @@ -0,0 +1,33 @@ +package dispatcher + +import ( + "context" + "encoding/json" + "megrez/services/redis" + "strconv" +) + +func Push(serverID uint, data Data) (err error) { + l.SetFunction("Push") + + ctx := context.Background() + + dataBytes, err := json.Marshal(data) + if err != nil { + l.Error("Failed to marshal data, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + return + } + + _, err = redis.RawDB.RPush(ctx, "dispatcher:server:"+strconv.Itoa(int(serverID)), string(dataBytes)).Result() + + if err != nil { + l.Error("Failed to push data to redis, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + return + } + + l.Info("Push data to redis success, Server ID: %d, Data: %+v", serverID, data) + + go run(serverID, false) + + return +} diff --git a/services/dispatcher/run.go b/services/dispatcher/run.go new file mode 100644 index 0000000..ed1c2e4 --- /dev/null +++ b/services/dispatcher/run.go @@ -0,0 +1,91 @@ +package dispatcher + +import ( + "context" + "encoding/json" + "megrez/services/redis" + "strconv" +) + +func run(serverID uint, recursion bool) { + lc := l.Clone() + lc.SetFunction("run:" + strconv.Itoa(int(serverID))) + + if !recursion && status[int(serverID)] { + return + } + + mx.Lock() + status[int(serverID)] = true + mx.Unlock() + + ctx := context.Background() + + llen, err := redis.RawDB.LLen(ctx, "dispatcher:server:"+strconv.Itoa(int(serverID))).Result() + if err != nil { + lc.Error("Failed to get length of list, Server ID: %d, Error: %v", serverID, err) + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + return + } + + if llen == 0 { + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + l.Debug("No data in redis, Server ID: %d", serverID) + return + } + + dataStr, err := redis.RawDB.LPop(ctx, "dispatcher:server:"+strconv.Itoa(int(serverID))).Result() + if err != nil { + lc.Error("Failed to pop data from redis, Server ID: %d, Error: %v", serverID, err) + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + + go run(serverID, true) + return + } + + var data Data + err = json.Unmarshal([]byte(dataStr), &data) + if err != nil { + lc.Error("Failed to unmarshal data, Server ID: %d, Data: %s, Error: %v", serverID, dataStr, err) + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + + run(serverID, true) + return + } + + switch data.Type { + case Add: + err = add(serverID, data) + if err != nil { + lc.Error("Failed to add instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + case Control: + err = control(serverID, data) + if err != nil { + lc.Error("Failed to control instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + case Modify: + err = modify(serverID, data) + if err != nil { + lc.Error("Failed to modify instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + case Delete: + err = delete(serverID, data) + if err != nil { + lc.Error("Failed to delete instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + default: + } + + go run(serverID, true) + + return +} diff --git a/services/http/http.go b/services/http/http.go new file mode 100644 index 0000000..62004de --- /dev/null +++ b/services/http/http.go @@ -0,0 +1,62 @@ +package http + +import ( + "context" + "fmt" + _logger "megrez/libs/logger" + "megrez/routers" + "megrez/services/config" + "megrez/services/logger" + "time" + + "github.com/kataras/iris/v12" +) + +var l *_logger.LoggerStruct + +func Start() { + l = logger.Logger.Clone() + l.SetModel("HTTP") + l.SetFunction("Start") + + hc := config.GetHttpAddress() + + app := iris.New() + app.Use(iris.Compression) + + idleConnsClosed := make(chan struct{}) + iris.RegisterOnInterrupt(func() { + fmt.Println() + l.SetFunction("CloseHttp") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := app.Shutdown(ctx) + if err != nil { + l.Error("Http Shutdown err, Error: %v", err.Error()) + } + l.Info("Http Closed") + + close(idleConnsClosed) + }) + + routers.InitRouter(app) + + l.Info("Http Server Listening on http://%s", hc) + l.Info("Press CTRL+C to shut down.") + err := app.Listen( + hc, + iris.WithConfiguration(iris.Configuration{ + DisableStartupLog: true, + LogLevel: "disable", + Charset: "UTF-8", + EnableOptimizations: true, + }), + iris.WithoutServerError(iris.ErrServerClosed), + ) + if err != nil { + l.Error("Http Server Listen err, Error: %v", err.Error()) + return + } + <-idleConnsClosed +} diff --git a/services/instanceController/chatTable.go b/services/instanceController/chatTable.go new file mode 100644 index 0000000..9f98d1d --- /dev/null +++ b/services/instanceController/chatTable.go @@ -0,0 +1,32 @@ +package instanceController + +const ( + apiPrefix = "/api/v1" +) + +const ( + instancePrefix = "/replicaSet" + instanceRestart = "/restart" + instanceStart = "/continue" + instanceStop = "/stop" + instancePause = "/pause" + instanceHistory = "/history" + instanceRollback = "/rollback" + instanceImage = "/commit" + instanceExecute = "/execute" +) + +const ( + volumePrefix = "/volumes" + volumeSize = "/size" + volumeHistory = "/history" +) + +type Action int + +const ( + ActionStart Action = 1 + ActionPause Action = 2 + ActionStop Action = 3 + ActionRestart Action = 4 +) diff --git a/services/instanceController/continue.go b/services/instanceController/continue.go new file mode 100644 index 0000000..ca812ab --- /dev/null +++ b/services/instanceController/continue.go @@ -0,0 +1,74 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Continue(instance *models.Instances) (err error) { + l.SetFunction("Start") + + instance.Status = models.InstanceStarting + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = continueInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("start instance error: %v", err) + return err + } + + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func continueInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("startInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceStart). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("start instance error: %v", c.GetBody()) + return errors.New("start instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("start instance error: %v", res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/create.go b/services/instanceController/create.go new file mode 100644 index 0000000..131c9eb --- /dev/null +++ b/services/instanceController/create.go @@ -0,0 +1,69 @@ +package instanceController + +import ( + "megrez/models" + "megrez/services/database" +) + +func Create(instance *models.Instances) (containerName, volumeName string, err error) { + l.SetFunction("Create") + + server := models.Servers{ + ID: instance.ServerID, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return "", "", result.Error + } + + if instance.VolumeSize != 0 { + volumeName, err = createVolume(server.IP, server.Port, server.Apikey, instance.VolumeSize) + if err != nil { + l.Error("create volume error: %v", err) + return "", "", err + } + instance.VolumeName = volumeName + } + + containerName, err = createInstance(server.IP, server.Port, server.Apikey, instance.GpuCount, server.CpuCountPerGpu*instance.GpuCount, server.MemoryPerGpu*instance.GpuCount, instance.VolumeName, instance.ImageName) + if err != nil { + l.Error("create instance error: %v", err) + deleteInstance(server.IP, server.Port, server.Apikey, containerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + return "", "", err + } + instance.ContainerName = containerName + + err = SetRootPassword(server.IP, server.Port, server.Apikey, containerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, containerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return "", "", err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, containerName) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, containerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("get port forward error: %v", err) + return "", "", err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.Status = 0 + result = database.DB.Save(&instance) + + return containerName, volumeName, nil +} diff --git a/services/instanceController/createInstance.go b/services/instanceController/createInstance.go new file mode 100644 index 0000000..b993745 --- /dev/null +++ b/services/instanceController/createInstance.go @@ -0,0 +1,93 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/crypto" + "megrez/libs/request" + "strconv" +) + +type createInstanceReqStruct struct { + ImageName string `json:"imageName"` + ReplicaSetName string `json:"replicaSetName"` + CpuCount int `json:"cpuCount"` + GpuCount int `json:"gpuCount"` + Memory string `json:"memory"` + Binds []bindStruct `json:"binds"` + Env []string `json:"env"` + ContainerPorts []string `json:"containerPorts"` +} + +func createInstance(ip string, port int, apikey string, + gpuCount, cpuCount, memorySize int, volumeName string, imageName string, +) (containerName string, err error) { + l.SetFunction("createInstance") + + containerName, err = crypto.GenerateUUIDWithoutHyphen() + if err != nil { + l.Error("generate instance name error: %v", err) + return "", err + } + + data := createInstanceReqStruct{ + ImageName: imageName, + ReplicaSetName: containerName, + CpuCount: cpuCount, + GpuCount: gpuCount, + Memory: strconv.Itoa(memorySize) + "GB", + ContainerPorts: []string{ + "22", // SSH + "6007", // TensorBoard + "8888", // Jupyter Notebook + "3000", // Grafana + "34567", // Custom Port + }, + } + + if volumeName != "" { + data.Binds = []bindStruct{ + { + Src: volumeName, + Dest: "/root/megrez-tmp", + }, + { + Src: "/data/pub", + Dest: "/root/megrez-pub", + }, + } + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return "", err + } + + c := request.NewRequest().Post(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("create instance error: %d", c.GetStatusCode()) + return "", errors.New("create instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return "", err + } + + if res.Code != 200 { + l.Error("create instance code: %d, error: %s", res.Code, res.Msg) + return "", errors.New(res.Msg) + } + + return +} diff --git a/services/instanceController/createVolume.go b/services/instanceController/createVolume.go new file mode 100644 index 0000000..578ddee --- /dev/null +++ b/services/instanceController/createVolume.go @@ -0,0 +1,66 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/crypto" + "megrez/libs/request" + "strconv" +) + +type createVolumeReqStruct struct { + Name string `json:"name"` + Size string `json:"size"` +} + +func createVolume(ip string, port int, apikey string, + volumeSize int, +) (volumeName string, err error) { + l.SetFunction("createVolume") + + volumeName, err = crypto.GenerateUUIDWithoutHyphen() + if err != nil { + l.Error("generate volume name error: %v", err) + return "", err + } + + data := createVolumeReqStruct{ + Name: volumeName, + Size: strconv.Itoa(volumeSize) + "GB", + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return "", err + } + + c := request.NewRequest().Post(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("create volume error code: %d", c.GetStatusCode()) + return "", errors.New("create volume request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return "", err + } + + if res.Code != 200 { + l.Error("create volume code: %d, error: %s", res.Code, res.Msg) + return "", errors.New(res.Msg) + } + + volumeName = volumeName + "-1" + + return +} diff --git a/services/instanceController/delete.go b/services/instanceController/delete.go new file mode 100644 index 0000000..0ba00e1 --- /dev/null +++ b/services/instanceController/delete.go @@ -0,0 +1,47 @@ +package instanceController + +import ( + "megrez/models" + "megrez/services/database" +) + +func Delete(instance *models.Instances) (err error) { + l.SetFunction("Delete") + + instance.Status = models.InstanceDeleting + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("delete instance error: %v", err) + return err + } + + if instance.VolumeName != "" { + err = deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + if err != nil { + l.Error("delete volume error: %v", err) + } + } + + result = database.DB.Delete(&instance) + if result.Error != nil { + l.Error("delete instance error: %v", result.Error) + return result.Error + } + + return nil +} diff --git a/services/instanceController/deleteInstance.go b/services/instanceController/deleteInstance.go new file mode 100644 index 0000000..ca56f99 --- /dev/null +++ b/services/instanceController/deleteInstance.go @@ -0,0 +1,39 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +func deleteInstance(ip string, port int, apikey string, + instanceName string, +) (err error) { + l.SetFunction("deleteInstance") + + c := request.NewRequest().Delete(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + instanceName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("delete instance error code: %d", c.GetStatusCode()) + return errors.New("delete instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return + } + + if res.Code != 200 { + l.Error("delete instance code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return +} diff --git a/services/instanceController/deleteVolume.go b/services/instanceController/deleteVolume.go new file mode 100644 index 0000000..ae0a343 --- /dev/null +++ b/services/instanceController/deleteVolume.go @@ -0,0 +1,45 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" + "strings" +) + +func deleteVolume(ip string, port int, apikey string, + volumeName string, noAll bool, +) (err error) { + l.SetFunction("deleteVolume") + + url := "http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix + "/" + strings.Split(volumeName, "-")[0] + if noAll { + url = "http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix + "/" + volumeName + "?noall=true" + } + + c := request.NewRequest().Delete(). + SetUrl(url). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("delete volume error code: %d", c.GetStatusCode()) + return errors.New("delete volume request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return + } + + if res.Code != 200 { + l.Error("delete volume code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/execute.go b/services/instanceController/execute.go new file mode 100644 index 0000000..20a7dd9 --- /dev/null +++ b/services/instanceController/execute.go @@ -0,0 +1,58 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +type executeReq struct { + Cmd []string `json:"cmd"` +} + +func SetRootPassword(ip string, port int, apikey string, + containerName, password string) (err error) { + l.SetFunction("SetRootPassword") + + data := executeReq{ + Cmd: []string{ + "sh", + "-c", + "echo root:" + password + " | chpasswd", + }, + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return err + } + + c := request.NewRequest().Post(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceExecute). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("set root password error: %d", c.GetStatusCode()) + return errors.New("set root password request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("set root password code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/getPortForward.go b/services/instanceController/getPortForward.go new file mode 100644 index 0000000..7d9df4f --- /dev/null +++ b/services/instanceController/getPortForward.go @@ -0,0 +1,59 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" + "strings" +) + +type infoResStruct struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Info struct { + HostConfig struct { + PortBindings map[string][]struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` + } + } `json:"HostConfig"` + } `json:"info"` + } `json:"data"` +} + +func GetPortForward(ip string, port int, apikey string, containerName string) (portBindings map[string]string, err error) { + l.SetFunction("GetPortForward") + + portBindings = make(map[string]string) + + c := request.NewRequest().Get(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("get port forward error: %d", c.GetStatusCode()) + return nil, errors.New("get port forward request error") + } + + var res infoResStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return + } + + if res.Code != 200 { + l.Error("get port forward code: %d, error: %s", res.Code, res.Msg) + return nil, errors.New(res.Msg) + } + + for k, v := range res.Data.Info.HostConfig.PortBindings { + portBindings[strings.Split(k, "/")[0]] = v[0].HostPort + } + + return portBindings, nil +} diff --git a/services/instanceController/history.go b/services/instanceController/history.go new file mode 100644 index 0000000..ee28e49 --- /dev/null +++ b/services/instanceController/history.go @@ -0,0 +1 @@ +package instanceController diff --git a/services/instanceController/init.go b/services/instanceController/init.go new file mode 100644 index 0000000..90ccbf2 --- /dev/null +++ b/services/instanceController/init.go @@ -0,0 +1,25 @@ +package instanceController + +import ( + "megrez/services/logger" + + _logger "megrez/libs/logger" +) + +var l *_logger.LoggerStruct + +func InitInstanceController() { + l = logger.Logger.Clone() + l.SetModel("instanceController") +} + +type bindStruct struct { + Src string `json:"src"` + Dest string `json:"dest"` +} + +type resStruct struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data map[string]any `json:"data"` +} diff --git a/services/instanceController/patch.go b/services/instanceController/patch.go new file mode 100644 index 0000000..b1df6ae --- /dev/null +++ b/services/instanceController/patch.go @@ -0,0 +1,162 @@ +package instanceController + +import ( + "errors" + "megrez/models" + "megrez/services/database" + "strings" +) + +type patchReqStruct struct { + CpuPatch *cpuPatchStruct `json:"cpuPatch"` + GpuPatch *gpuPatchStruct `json:"gpuPatch"` + MemoryPatch *MemoryPatchStruct `json:"memoryPatch"` + VolumePatch *volumePatchStruct `json:"volumePatch"` +} + +type cpuPatchStruct struct { + CpuCount int `json:"cpuCount"` +} + +type gpuPatchStruct struct { + GpuCount int `json:"gpuCount"` +} + +type MemoryPatchStruct struct { + Memory string `json:"memory"` +} + +type volumePatchStruct struct { + OldBind bindStruct `json:"oldBind"` + NewBind bindStruct `json:"newBind"` +} + +func Patch(instance *models.Instances, gpuCount, volumeSize int, cpuOnly bool) (err error) { + l.SetFunction("Patch") + + instance.Status = models.InstanceModifying + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + if gpuCount == instance.GpuCount && volumeSize == instance.VolumeSize && cpuOnly == instance.CpuOnly { + instance.Status = models.InstanceStopped + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + return errors.New("no change") + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + oldVolumeName := instance.VolumeName + if volumeSize != instance.VolumeSize { + newVolumeName, err := patchVolume(server.IP, server.Port, server.Apikey, strings.Split(instance.VolumeName, "-")[0], volumeSize) + if err != nil { + l.Error("patch volume error: %v", err) + return err + } + instance.VolumeName = newVolumeName + instance.VolumeSize = volumeSize + + defer func(server models.Servers, instance *models.Instances, volumeName string) { + err := deleteVolume(server.IP, server.Port, server.Apikey, volumeName, true) + if err != nil { + l.Error("delete volume error: %v", err) + } + }(server, instance, oldVolumeName) + } + + if cpuOnly { + err = patchCpuOnly(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.VolumeName, oldVolumeName) + if err != nil { + l.Error("patch cpu only error: %v", err) + return err + } + + err = SetRootPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("get port forward error: %v", err) + return err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.CpuOnly = true + instance.GpuCount = 0 + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + return nil + } + + err = patchGpu(server.IP, server.Port, server.Apikey, + instance.ContainerName, + server.CpuCountPerGpu, server.MemoryPerGpu, + instance.VolumeName, oldVolumeName, + gpuCount, instance.GpuCount, + ) + if err != nil { + l.Error("patch gpu error: %v", err) + return err + } + + err = SetRootPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("get port forward error: %v", err) + return err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.CpuOnly = false + instance.GpuCount = gpuCount + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} diff --git a/services/instanceController/patchCpuOnly.go b/services/instanceController/patchCpuOnly.go new file mode 100644 index 0000000..1ca7b44 --- /dev/null +++ b/services/instanceController/patchCpuOnly.go @@ -0,0 +1,73 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +func patchCpuOnly(ip string, port int, apikey string, + instanceName string, + volumeName string, oldVolumeName string, +) (err error) { + l.SetFunction("patchCpuOnly") + + data := patchReqStruct{ + CpuPatch: &cpuPatchStruct{ + CpuCount: 1, + }, + GpuPatch: &gpuPatchStruct{ + GpuCount: 0, + }, + MemoryPatch: &MemoryPatchStruct{ + Memory: "2GB", + }, + } + + if oldVolumeName != volumeName { + data.VolumePatch = &volumePatchStruct{ + OldBind: bindStruct{ + Src: oldVolumeName, + Dest: "/root/megrez-tmp", + }, + NewBind: bindStruct{ + Src: volumeName, + Dest: "/root/megrez-tmp", + }, + } + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return err + } + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + instanceName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("patch cpu only error code: %d", c.GetStatusCode()) + return errors.New("patch cpu only request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("patch cpu only code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/patchGpu.go b/services/instanceController/patchGpu.go new file mode 100644 index 0000000..ab71abf --- /dev/null +++ b/services/instanceController/patchGpu.go @@ -0,0 +1,81 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +func patchGpu(ip string, port int, apikey string, + instanceName string, + cpuCountPerGpu int, memoryPerGpu int, + volumeName string, oldVolumeName string, + gpuCount int, oldGpuCount int, +) (err error) { + l.SetFunction("patchGpu") + + var data patchReqStruct + + if gpuCount == oldGpuCount && volumeName == oldVolumeName { + return nil + } + + if gpuCount != oldGpuCount { + data.GpuPatch = &gpuPatchStruct{ + GpuCount: gpuCount, + } + data.CpuPatch = &cpuPatchStruct{ + CpuCount: cpuCountPerGpu * gpuCount, + } + data.MemoryPatch = &MemoryPatchStruct{ + Memory: strconv.Itoa(memoryPerGpu*gpuCount) + "GB", + } + } + + if volumeName != oldVolumeName { + data.VolumePatch = &volumePatchStruct{ + OldBind: bindStruct{ + Src: oldVolumeName, + Dest: "/root/megrez-tmp", + }, + NewBind: bindStruct{ + Src: volumeName, + Dest: "/root/megrez-tmp", + }, + } + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return err + } + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + instanceName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("patch gpu error code: %d", c.GetStatusCode()) + return errors.New("patch gpu request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("patch gpu code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/patchVolume.go b/services/instanceController/patchVolume.go new file mode 100644 index 0000000..213479c --- /dev/null +++ b/services/instanceController/patchVolume.go @@ -0,0 +1,64 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +type patchVolumeReqStruct struct { + Size string `json:"size"` +} + +type patchVolumeResStruct struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Name string `json:"name"` + Size string `json:"size"` + } `json:"data"` +} + +func patchVolume(ip string, port int, apikey string, + volumeName string, newVolumeSize int, +) (newVolumeName string, err error) { + l.SetFunction("patchVolume") + + data := patchVolumeReqStruct{ + Size: strconv.Itoa(newVolumeSize) + "GB", + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return "", err + } + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix + "/" + volumeName + volumeSize). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("patch volume error code: %d", c.GetStatusCode()) + return "", errors.New("patch volume request error") + } + + var res patchVolumeResStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return "", err + } + + if res.Code != 200 { + l.Error("patch volume code: %d, error: %s", res.Code, res.Msg) + return "", errors.New(res.Msg) + } + + return res.Data.Name, nil +} diff --git a/services/instanceController/pause.go b/services/instanceController/pause.go new file mode 100644 index 0000000..5bcdeb5 --- /dev/null +++ b/services/instanceController/pause.go @@ -0,0 +1,74 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Pause(instance *models.Instances) (err error) { + l.SetFunction("Pause") + + instance.Status = models.InstancePausing + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = pauseInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("pause instance error: %v", err) + return err + } + + instance.Status = models.InstancePaused + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func pauseInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("pauseInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instancePause). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("pause instance error: %v", c.GetBody()) + return errors.New("pause instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("pause instance error: %v", res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/restart.go b/services/instanceController/restart.go new file mode 100644 index 0000000..f6b29c1 --- /dev/null +++ b/services/instanceController/restart.go @@ -0,0 +1,99 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Restart(instance *models.Instances) (err error) { + l.SetFunction("Restart") + + if instance.Status == models.InstanceStopped || instance.Status == models.InstancePaused { + instance.Status = models.InstanceStarting + } else { + instance.Status = models.InstanceRestarting + } + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = restartInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("restart instance error: %v", err) + return err + } + + err = SetRootPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("get port forward error: %v", err) + return err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func restartInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("restartInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceRestart). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("restart instance error: %v", c.GetBody()) + return errors.New("restart instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("restart instance error: %v", res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/rollback.go b/services/instanceController/rollback.go new file mode 100644 index 0000000..ee28e49 --- /dev/null +++ b/services/instanceController/rollback.go @@ -0,0 +1 @@ +package instanceController diff --git a/services/instanceController/stop.go b/services/instanceController/stop.go new file mode 100644 index 0000000..be965cf --- /dev/null +++ b/services/instanceController/stop.go @@ -0,0 +1,74 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Stop(instance *models.Instances) (err error) { + l.SetFunction("Stop") + + instance.Status = models.InstanceStopping + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = stopInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("stop instance error: %v", err) + return err + } + + instance.Status = models.InstanceStopped + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func stopInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("stopInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceStop). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("stop instance error: %v", c.GetBody()) + return errors.New("stop instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("stop instance code: %d, error: %v", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/logger/logger.go b/services/logger/logger.go new file mode 100644 index 0000000..4cf7ec7 --- /dev/null +++ b/services/logger/logger.go @@ -0,0 +1,13 @@ +package logger + +import "megrez/libs/logger" + +var Logger *logger.LoggerStruct + +func init() { + Logger, _ = logger.NewLogger(logger.DEBUG, "stdout") +} + +func InitLogger(level, path string) { + Logger, _ = logger.NewLogger(level, path) +} diff --git a/services/redis/redis.go b/services/redis/redis.go new file mode 100644 index 0000000..8f7d816 --- /dev/null +++ b/services/redis/redis.go @@ -0,0 +1,61 @@ +package redis + +import ( + "strconv" + "time" + + "github.com/kataras/iris/v12/sessions/sessiondb/redis" + goRedis "github.com/redis/go-redis/v9" + + _logger "megrez/libs/logger" + "megrez/services/config" + "megrez/services/logger" +) + +var l *_logger.LoggerStruct +var DB *redis.Database +var RawDB *goRedis.Client + +func Connect() (err error) { + l = logger.Logger.Clone() + l.SetModel("redis") + l.SetFunction("Connect") + + rd := config.GetRedis() + DB = redis.New(redis.Config{ + Network: "tcp", + Addr: rd.Host + ":" + strconv.Itoa(rd.Port), + Timeout: time.Duration(30) * time.Second, + MaxActive: 10, + Password: rd.Password, + Database: strconv.Itoa(rd.Database), + Prefix: "", + }) + + RawDB = goRedis.NewClient(&goRedis.Options{ + Addr: rd.Host + ":" + strconv.Itoa(rd.Port), + Password: rd.Password, + DB: rd.Database, + }) + + l.Info("Redis connected") + + return nil +} + +func Close() (err error) { + l.SetFunction("Close") + + if err = DB.Close(); err != nil { + l.Error("Failed to close Redis Client, Error: %v", err) + return + } + + if err = RawDB.Close(); err != nil { + l.Error("Failed to close Redis Client, Error: %v", err) + return + } + + l.Info("Redis closed") + return +} diff --git a/services/system/check.go b/services/system/check.go new file mode 100644 index 0000000..889b7e0 --- /dev/null +++ b/services/system/check.go @@ -0,0 +1,25 @@ +package system + +import ( + _logger "megrez/libs/logger" + "megrez/services/config" + "megrez/services/logger" +) + +var l *_logger.LoggerStruct + +func Check() (err error) { + l = logger.Logger.Clone() + l.SetModel("system") + l.SetFunction("Check") + + salt := config.GetSystemSalt() + if salt == "" { + l.Info("System not init, initializing") + systemInit() + } else { + l.Debug("System already init") + } + + return nil +} diff --git a/services/system/init.go b/services/system/init.go new file mode 100644 index 0000000..9f8dde8 --- /dev/null +++ b/services/system/init.go @@ -0,0 +1,36 @@ +package system + +import ( + "megrez/libs/crypto" + "megrez/models" + "megrez/services/config" + "megrez/services/database" +) + +func systemInit() (err error) { + l.SetFunction("systemInit") + + salt := crypto.Hex(32) + config.SetSystemSalt(salt) + err = config.Save() + if err != nil { + l.Error("Save config failed, Error: %v", err) + return + } + + user := models.Users{ + Username: "admin", + Email: "admin@gpuManager.com", + Role: 3, + } + user.Password = user.PasswordHash("admin123456") + result := database.DB.Create(&user) + if result.Error != nil { + l.Error("Create admin user failed, Error: %v", result.Error) + return + } + + l.Info("System init success") + + return +}