diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..457dd83 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,6 @@ +**/.git +**/node_modules +**/Dockerfile* +**/dist +/.angular/cache +**/compose.yml \ No newline at end of file diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d24a646 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,44 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +*.old \ No newline at end of file diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..1e4e282 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,8 @@ +angular.json +compose.yml +*.md +tsconfig*.json +*.html +.vscode +package*.json +ngsw-config.json diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..1867496 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,6 @@ +{ + "arrowParens": "avoid", + "semi": false, + "trailingComma": "es5", + "bracketSpacing": true +} diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json new file mode 100644 index 0000000..740e35a --- /dev/null +++ b/frontend/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "pwa-chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json new file mode 100644 index 0000000..a298b5b --- /dev/null +++ b/frontend/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/frontend/COPYING.md b/frontend/COPYING.md new file mode 100644 index 0000000..3b176db --- /dev/null +++ b/frontend/COPYING.md @@ -0,0 +1,676 @@ +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the +appropriate parts of the General Public License. Of course, your +program's commands might be different; for a GUI interface, you would +use an "about box". + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU GPL, see . + +The GNU General Public License does not permit incorporating your +program into proprietary programs. If your program is a subroutine +library, you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use the +GNU Lesser General Public License instead of this License. But first, +please read . + diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..82823f9 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22.12-alpine AS build +WORKDIR /build +ADD . . +RUN [ "npm", "ci" ] +RUN [ "npm", "run", "build" ] + +FROM httpd:alpine AS runtime +RUN apk add --no-cache certbot certbot-apache +COPY httpd.conf /usr/local/apache2/conf/httpd.conf +COPY cli.ini /etc/letsencrypt/cli.ini +COPY --from=build /build/dist/ipwa/browser /usr/local/apache2/htdocs/ipwa +COPY <> /usr/local/apache2/conf/httpd.conf < + ServerName $DOMAIN + +EOF + httpd -k start + certbot --apache -n --keep -d $DOMAIN -m $EMAIL + httpd -k stop + touch /usr/local/apache2/.configured + fi +fi +httpd-foreground \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..0411520 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,49 @@ +// @ts-check +const eslint = require("@eslint/js"); +const tseslint = require("typescript-eslint"); +const angular = require("angular-eslint"); + +module.exports = tseslint.config( + { + files: ["**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.stylistic, + ...angular.configs.tsRecommended, + ], + processor: angular.processInlineTemplates, + rules: { + "@angular-eslint/directive-selector": [ + "error", + { + type: "attribute", + prefix: "app", + style: "camelCase", + }, + ], + "@angular-eslint/component-selector": [ + "error", + { + type: "element", + prefix: "app", + style: "kebab-case", + }, + ], + "@angular-eslint/prefer-standalone": [ + "off" + ], + "no-unexpected-multiline": [ + "off" + ] + }, + }, + { + files: ["**/*.html"], + extends: [ + ...angular.configs.templateRecommended, + ...angular.configs.templateAccessibility, + ], + rules: {}, + } +); diff --git a/frontend/httpd.conf b/frontend/httpd.conf new file mode 100644 index 0000000..b556e8b --- /dev/null +++ b/frontend/httpd.conf @@ -0,0 +1,556 @@ +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so "logs/access_log" +# with ServerRoot set to "/usr/local/apache2" will be interpreted by the +# server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log" +# will be interpreted as '/logs/access_log'. + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to specify a local disk on the +# Mutex directive, if file-based mutexes are used. If you wish to share the +# same ServerRoot for multiple httpd daemons, you will need to change at +# least PidFile. +# +ServerRoot "/usr/local/apache2" + +# +# Mutex: Allows you to set the mutex mechanism and mutex file directory +# for individual mutexes, or change the global defaults +# +# Uncomment and change the directory if mutexes are file-based and the default +# mutex file directory is not on a local disk or is not appropriate for some +# other reason. +# +# Mutex default:logs + +# +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 +Listen 443 + +# +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +LoadModule mpm_event_module modules/mod_mpm_event.so +#LoadModule mpm_prefork_module modules/mod_mpm_prefork.so +#LoadModule mpm_worker_module modules/mod_mpm_worker.so +LoadModule authn_file_module modules/mod_authn_file.so +#LoadModule authn_dbm_module modules/mod_authn_dbm.so +#LoadModule authn_anon_module modules/mod_authn_anon.so +#LoadModule authn_dbd_module modules/mod_authn_dbd.so +#LoadModule authn_socache_module modules/mod_authn_socache.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_user_module modules/mod_authz_user.so +#LoadModule authz_dbm_module modules/mod_authz_dbm.so +#LoadModule authz_owner_module modules/mod_authz_owner.so +#LoadModule authz_dbd_module modules/mod_authz_dbd.so +LoadModule authz_core_module modules/mod_authz_core.so +#LoadModule authnz_ldap_module modules/mod_authnz_ldap.so +#LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule auth_basic_module modules/mod_auth_basic.so +#LoadModule auth_form_module modules/mod_auth_form.so +#LoadModule auth_digest_module modules/mod_auth_digest.so +#LoadModule allowmethods_module modules/mod_allowmethods.so +#LoadModule isapi_module modules/mod_isapi.so +#LoadModule file_cache_module modules/mod_file_cache.so +#LoadModule cache_module modules/mod_cache.so +#LoadModule cache_disk_module modules/mod_cache_disk.so +#LoadModule cache_socache_module modules/mod_cache_socache.so +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so +#LoadModule socache_dbm_module modules/mod_socache_dbm.so +#LoadModule socache_memcache_module modules/mod_socache_memcache.so +#LoadModule socache_redis_module modules/mod_socache_redis.so +#LoadModule watchdog_module modules/mod_watchdog.so +#LoadModule macro_module modules/mod_macro.so +#LoadModule dbd_module modules/mod_dbd.so +#LoadModule bucketeer_module modules/mod_bucketeer.so +#LoadModule dumpio_module modules/mod_dumpio.so +#LoadModule echo_module modules/mod_echo.so +#LoadModule example_hooks_module modules/mod_example_hooks.so +#LoadModule case_filter_module modules/mod_case_filter.so +#LoadModule case_filter_in_module modules/mod_case_filter_in.so +#LoadModule example_ipc_module modules/mod_example_ipc.so +#LoadModule buffer_module modules/mod_buffer.so +#LoadModule data_module modules/mod_data.so +#LoadModule ratelimit_module modules/mod_ratelimit.so +LoadModule reqtimeout_module modules/mod_reqtimeout.so +#LoadModule ext_filter_module modules/mod_ext_filter.so +#LoadModule request_module modules/mod_request.so +#LoadModule include_module modules/mod_include.so +LoadModule filter_module modules/mod_filter.so +#LoadModule reflector_module modules/mod_reflector.so +#LoadModule substitute_module modules/mod_substitute.so +#LoadModule sed_module modules/mod_sed.so +#LoadModule charset_lite_module modules/mod_charset_lite.so +#LoadModule deflate_module modules/mod_deflate.so +#LoadModule xml2enc_module modules/mod_xml2enc.so +#LoadModule proxy_html_module modules/mod_proxy_html.so +#LoadModule brotli_module modules/mod_brotli.so +LoadModule mime_module modules/mod_mime.so +#LoadModule ldap_module modules/mod_ldap.so +LoadModule log_config_module modules/mod_log_config.so +#LoadModule log_debug_module modules/mod_log_debug.so +#LoadModule log_forensic_module modules/mod_log_forensic.so +#LoadModule logio_module modules/mod_logio.so +#LoadModule lua_module modules/mod_lua.so +LoadModule env_module modules/mod_env.so +#LoadModule mime_magic_module modules/mod_mime_magic.so +#LoadModule cern_meta_module modules/mod_cern_meta.so +#LoadModule expires_module modules/mod_expires.so +LoadModule headers_module modules/mod_headers.so +#LoadModule ident_module modules/mod_ident.so +#LoadModule usertrack_module modules/mod_usertrack.so +#LoadModule unique_id_module modules/mod_unique_id.so +LoadModule setenvif_module modules/mod_setenvif.so +LoadModule version_module modules/mod_version.so +#LoadModule remoteip_module modules/mod_remoteip.so +LoadModule proxy_module modules/mod_proxy.so +#LoadModule proxy_connect_module modules/mod_proxy_connect.so +#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so +LoadModule proxy_http_module modules/mod_proxy_http.so +#LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so +#LoadModule proxy_scgi_module modules/mod_proxy_scgi.so +#LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so +#LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so +#LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so +#LoadModule proxy_ajp_module modules/mod_proxy_ajp.so +#LoadModule proxy_balancer_module modules/mod_proxy_balancer.so +#LoadModule proxy_express_module modules/mod_proxy_express.so +#LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so +#LoadModule session_module modules/mod_session.so +#LoadModule session_cookie_module modules/mod_session_cookie.so +#LoadModule session_crypto_module modules/mod_session_crypto.so +#LoadModule session_dbd_module modules/mod_session_dbd.so +#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so +#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so +LoadModule ssl_module modules/mod_ssl.so +#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so +#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so +#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so +#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so +#LoadModule dialup_module modules/mod_dialup.so +#LoadModule http2_module modules/mod_http2.so +#LoadModule proxy_http2_module modules/mod_proxy_http2.so +#LoadModule md_module modules/mod_md.so +#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so +#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so +#LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so +#LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so +LoadModule unixd_module modules/mod_unixd.so +#LoadModule heartbeat_module modules/mod_heartbeat.so +#LoadModule heartmonitor_module modules/mod_heartmonitor.so +#LoadModule dav_module modules/mod_dav.so +LoadModule status_module modules/mod_status.so +LoadModule autoindex_module modules/mod_autoindex.so +#LoadModule asis_module modules/mod_asis.so +#LoadModule info_module modules/mod_info.so +#LoadModule suexec_module modules/mod_suexec.so + + #LoadModule cgid_module modules/mod_cgid.so + + + #LoadModule cgi_module modules/mod_cgi.so + +#LoadModule dav_fs_module modules/mod_dav_fs.so +#LoadModule dav_lock_module modules/mod_dav_lock.so +#LoadModule vhost_alias_module modules/mod_vhost_alias.so +#LoadModule negotiation_module modules/mod_negotiation.so +LoadModule dir_module modules/mod_dir.so +#LoadModule imagemap_module modules/mod_imagemap.so +#LoadModule actions_module modules/mod_actions.so +#LoadModule speling_module modules/mod_speling.so +#LoadModule userdir_module modules/mod_userdir.so +LoadModule alias_module modules/mod_alias.so +LoadModule rewrite_module modules/mod_rewrite.so + + +# +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +# +User www-data +Group www-data + + + +# 'Main' server configuration +# +# The directives in this section set up the values used by the 'main' +# server, which responds to any requests that aren't handled by a +# definition. These values also provide defaults for +# any containers you may define later in the file. +# +# All of these directives may appear inside containers, +# in which case these default settings will be overridden for the +# virtual host being defined. +# + +# +# Deny access to the entirety of your server's filesystem. You must +# explicitly permit access to web content directories in other +# blocks below. +# + + AllowOverride none + Require all denied + + +# +# Note that from this point forward you must specifically allow +# particular features to be enabled - so if something's not working as +# you might expect, make sure that you have specifically enabled it +# below. +# + +# +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +DocumentRoot "/usr/local/apache2/htdocs" + + # + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + # + Options Indexes FollowSymLinks + + # + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # AllowOverride FileInfo AuthConfig Limit + # + AllowOverride All + + # + # Controls who can get stuff from this server. + # + Require all granted + + + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + +ProxyPass "/api" "http://api:8080" +ProxyPassReverse "/api" "http://api:8080" + +RedirectMatch ^/$ /ipwa + + + AliasPreservePath on + Alias "/var/www/certbot/.well-known/acme-challenge/" + + +# +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# + + DirectoryIndex index.html + + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + +# +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog /proc/self/fd/2 + +# +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +# +LogLevel warn + + + # + # The following directives define some format nicknames for use with + # a CustomLog directive (see below). + # + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + + # You need to enable mod_logio.c to use %I and %O + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + + # + # The location and format of the access logfile (Common Logfile Format). + # If you do not define any access logfiles within a + # container, they will be logged here. Contrariwise, if you *do* + # define per- access logfiles, transactions will be + # logged therein and *not* in this file. + # + CustomLog /proc/self/fd/1 common + + # + # If you prefer a logfile with access, agent, and referer information + # (Combined Logfile Format) you can use the following directive. + # + #CustomLog "logs/access_log" combined + + + + # + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + # + ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/" + + + + + # + # ScriptSock: On threaded servers, designate the path to the UNIX + # socket used to communicate with the CGI daemon of mod_cgid. + # + #Scriptsock cgisock + + +# +# "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. +# + + AllowOverride None + Options None + Require all granted + + + + # + # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied + # backend servers which have lingering "httpoxy" defects. + # 'Proxy' request header is undefined by the IETF, not listed by IANA + # + RequestHeader unset Proxy early + + + + # + # TypesConfig points to the file containing the list of mappings from + # filename extension to MIME-type. + # + TypesConfig conf/mime.types + + # + # AddType allows you to add to or override the MIME configuration + # file specified in TypesConfig for specific file types. + # + #AddType application/x-gzip .tgz + # + # AddEncoding allows you to have certain browsers uncompress + # information on the fly. Note: Not all browsers support this. + # + #AddEncoding x-compress .Z + #AddEncoding x-gzip .gz .tgz + # + # If the AddEncoding directives above are commented-out, then you + # probably should define those extensions to indicate media types: + # + AddType application/x-compress .Z + AddType application/x-gzip .gz .tgz + + # + # AddHandler allows you to map certain file extensions to "handlers": + # actions unrelated to filetype. These can be either built into the server + # or added with the Action directive (see below) + # + # To use CGI scripts outside of ScriptAliased directories: + # (You will also need to add "ExecCGI" to the "Options" directive.) + # + #AddHandler cgi-script .cgi + + # For type maps (negotiated resources): + #AddHandler type-map var + + # + # Filters allow you to process content before it is sent to the client. + # + # To parse .shtml files for server-side includes (SSI): + # (You will also need to add "Includes" to the "Options" directive.) + # + #AddType text/html .shtml + #AddOutputFilter INCLUDES .shtml + + +# +# The mod_mime_magic module allows the server to use various hints from the +# contents of the file itself to determine its type. The MIMEMagicFile +# directive tells the module where the hint definitions are located. +# +#MIMEMagicFile conf/magic + +# +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html +# + +# +# MaxRanges: Maximum number of Ranges in a request before +# returning the entire resource, or one of the special +# values 'default', 'none' or 'unlimited'. +# Default setting is to accept 200 Ranges. +#MaxRanges unlimited + +# +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall may be used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +# Defaults: EnableMMAP On, EnableSendfile Off +# +#EnableMMAP off +#EnableSendfile on + +# Supplemental configuration +# +# The configuration files in the conf/extra/ directory can be +# included to add extra features or to modify the default configuration of +# the server, or you may simply copy their contents here and change as +# necessary. + +# Server-pool management (MPM specific) +#Include conf/extra/httpd-mpm.conf + +# Multi-language error messages +#Include conf/extra/httpd-multilang-errordoc.conf + +# Fancy directory listings +#Include conf/extra/httpd-autoindex.conf + +# Language settings +#Include conf/extra/httpd-languages.conf + +# User home directories +#Include conf/extra/httpd-userdir.conf + +# Real-time info on requests and configuration +#Include conf/extra/httpd-info.conf + +# Virtual hosts +#Include conf/extra/httpd-vhosts.conf + +# Local access to the Apache HTTP Server Manual +#Include conf/extra/httpd-manual.conf + +# Distributed authoring and versioning (WebDAV) +#Include conf/extra/httpd-dav.conf + +# Various default settings +#Include conf/extra/httpd-default.conf + +# Configure mod_proxy_html to understand HTML4/XHTML1 + +Include conf/extra/proxy-html.conf + + +# Secure (SSL/TLS) connections +#Include conf/extra/httpd-ssl.conf +# +# Note: The following must must be present to support +# starting without SSL on platforms with no /dev/random equivalent +# but a statically compiled-in mod_ssl. +# + +SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_cache(512000)" +SSLSessionCacheTimeout 300 + + +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin + + diff --git a/frontend/ngsw-config.json b/frontend/ngsw-config.json new file mode 100644 index 0000000..f25973d --- /dev/null +++ b/frontend/ngsw-config.json @@ -0,0 +1,29 @@ +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/ipwa/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": [ + "/ipwa/favicon.ico", + "/ipwa/manifest.webmanifest", + "/ipwa/*.css", + "/ipwa/*.js" + ] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/ipwa/assets/**", + "/ipwa/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" + ] + } + } + ] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..29e70a8 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,11821 @@ +{ + "name": "ipwa", + "version": "2.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ipwa", + "version": "2.0.0", + "license": "GPL-3.0-or-later", + "dependencies": { + "@angular/animations": "^20.0.2", + "@angular/cdk": "^20.0.2", + "@angular/cli": "^20.3.18", + "@angular/common": "^20.3.14", + "@angular/compiler": "^20.3.16", + "@angular/core": "^20.3.17", + "@angular/forms": "^20.0.2", + "@angular/material": "^20.0.2", + "@angular/material-luxon-adapter": "^20.0.2", + "@angular/platform-browser": "^20.0.2", + "@angular/platform-browser-dynamic": "^20.0.2", + "@angular/router": "^20.0.2", + "@angular/service-worker": "^20.0.2", + "luxon": "^3.6.1", + "marked": "^12.0.1", + "rxjs": "~7.5.0", + "tslib": "^2.3.0", + "underscore": "^1.13.8", + "zone.js": "~0.15.1" + }, + "devDependencies": { + "@angular/build": "^20.0.1", + "@angular/compiler-cli": "^20.0.2", + "@angular/localize": "^20.0.2", + "@types/jasmine": "~4.3.0", + "@types/luxon": "^3.6.2", + "@types/underscore": "^1.13.0", + "angular-eslint": "20.0.0", + "eslint": "^9.28.0", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "prettier": "3.5.3", + "typescript": "~5.8.3", + "typescript-eslint": "8.33.1" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.1.0.tgz", + "integrity": "sha512-sEyWjw28a/9iluA37KLGu8vjxEIlb60uxznfTUmXImy7H5NvbpSO6yYgmgH5KiD7j+zTUUihiST0jEP12IoXow==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.35.0.tgz", + "integrity": "sha512-uUdHxbfHdoppDVflCHMxRlj49/IllPwwQ2cQ8DLC4LXr3kY96AHBpW0dMyi6ygkn2MtFCc6BxXCzr668ZRhLBQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.35.0.tgz", + "integrity": "sha512-SunAgwa9CamLcRCPnPHx1V2uxdQwJGqb1crYrRWktWUdld0+B2KyakNEeVn5lln4VyeNtW17Ia7V7qBWyM/Skw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.35.0.tgz", + "integrity": "sha512-ipE0IuvHu/bg7TjT2s+187kz/E3h5ssfTtjpg1LbWMgxlgiaZIgTTbyynM7NfpSJSKsgQvCQxWjGUO51WSCu7w==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.35.0.tgz", + "integrity": "sha512-UNbCXcBpqtzUucxExwTSfAe8gknAJ485NfPN6o1ziHm6nnxx97piIbcBQ3edw823Tej2Wxu1C0xBY06KgeZ7gA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.35.0.tgz", + "integrity": "sha512-/KWjttZ6UCStt4QnWoDAJ12cKlQ+fkpMtyPmBgSS2WThJQdSV/4UWcqCUqGH7YLbwlj3JjNirCu3Y7uRTClxvA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.35.0.tgz", + "integrity": "sha512-8oCuJCFf/71IYyvQQC+iu4kgViTODbXDk3m7yMctEncRSRV+u2RtDVlpGGfPlJQOrAY7OONwJlSHkmbbm2Kp/w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.35.0.tgz", + "integrity": "sha512-FfmdHTrXhIduWyyuko1YTcGLuicVbhUyRjO3HbXE4aP655yKZgdTIfMhZ/V5VY9bHuxv/fGEh3Od1Lvv2ODNTg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.35.0.tgz", + "integrity": "sha512-gPzACem9IL1Co8mM1LKMhzn1aSJmp+Vp434An4C0OBY4uEJRcqsLN3uLBlY+bYvFg8C8ImwM9YRiKczJXRk0XA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.35.0.tgz", + "integrity": "sha512-w9MGFLB6ashI8BGcQoVt7iLgDIJNCn4OIu0Q0giE3M2ItNrssvb8C0xuwJQyTy1OFZnemG0EB1OvXhIHOvQwWw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.35.0.tgz", + "integrity": "sha512-AhrVgaaXAb8Ue0u2nuRWwugt0dL5UmRgS9LXe0Hhz493a8KFeZVUE56RGIV3hAa6tHzmAV7eIoqcWTQvxzlJeQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.35.0.tgz", + "integrity": "sha512-diY415KLJZ6x1Kbwl9u96Jsz0OstE3asjXtJ9pmk1d+5gPuQ5jQyEsgC+WmEXzlec3iuVszm8AzNYYaqw6B+Zw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.35.0.tgz", + "integrity": "sha512-uydqnSmpAjrgo8bqhE9N1wgcB98psTRRQXcjc4izwMB7yRl9C8uuAQ/5YqRj04U0mMQ+fdu2fcNF6m9+Z1BzDQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.35.0.tgz", + "integrity": "sha512-RgLX78ojYOrThJHrIiPzT4HW3yfQa0D7K+MQ81rhxqaNyNBu4F1r+72LNHYH/Z+y9I1Mrjrd/c/Ue5zfDgAEjQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2000.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2000.1.tgz", + "integrity": "sha512-EcOGU1xEhARYpDF391VaeUg/+YRym9OxzJMcc0rSHl3YLK8/m+24ap2YAQY5N7n9+mmEqHVu/q31ldFpOoMCTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.0.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.1.tgz", + "integrity": "sha512-Ilafyj8JVwq3NZsaiGw5UDkP4EAkGKiEvZ4TC3WVidZbM4EpKt9/Jd7ZpsTRGDLG429U+fGhay+ZQeCFGqy5rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.2", + "source-map": "0.7.4" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/architect/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "20.3.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.18.tgz", + "integrity": "sha512-GRMEGl3YTL/qhQhaxYXLbSQxUTPTYMQ65IlxLQRq5+UKPomN9KVxxVdADXqs7Ss1uQcetr+jc+taVgxOqsAoxg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.3.18", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "8.2.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "20.3.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.18.tgz", + "integrity": "sha512-zGWMjMqE8qXYr8baYCs43k9HlKz9J4Gh3Yx+7XE0uS0Y1LXzzALevSoUw7GIPdSvOriQJAEgtWE6QKssqSGltQ==", + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-20.0.0.tgz", + "integrity": "sha512-9jS3VvY+K+EHw9pofsdwKxDirKuTuRBnjMZdaKoUfLoYy5eS1XGJBXoMdaQiM+mSlTv113+L0SK4U565xiBLHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": ">= 0.2000.0 < 0.2100.0", + "@angular-devkit/core": ">= 20.0.0 < 21.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/builder/node_modules/@angular-devkit/core": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.2.tgz", + "integrity": "sha512-qqTSpcIw+TqJ6u/tkQzqgpwVelHsHr8Jhws1Vlx6E0L6E+cRILBK48i9ttE+oYkQlcopQ3VZAmzcZodXJ1SQ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.2", + "source-map": "0.7.4" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/builder/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-eslint/builder/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-eslint/builder/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-20.0.0.tgz", + "integrity": "sha512-mDXMQd08s11q9fC6Ps3ffZmvXop9eLuAAXexofHhA7uuoQAoUWS2zoOSNTWtDR6oxMcqEeMnALCjjFeJVBSVmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-20.0.0.tgz", + "integrity": "sha512-xzaLj2yEn43DH0bE9Gw3GrmC+jivIS5/Hbh3bDj3ctw3mUUrD8hrS7kBo1neZ0gnoVLoo/mwIldG+xs5NDY66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "20.0.0", + "@angular-eslint/utils": "20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-20.0.0.tgz", + "integrity": "sha512-QoGgrawU5JFcaj0TjXHKC6fiZkxBeGVRj/TWJtTo/x+c5TVoV5k9pI7Uxdmo9kr4SkPXmt80ZklvExSA510gyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "20.0.0", + "@angular-eslint/utils": "20.0.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@angular-eslint/template-parser": "20.0.0", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-20.0.0.tgz", + "integrity": "sha512-VL3Sb6Df+iiUSPaQG8NxMPLx0dFRtRGSzsfe6CWYW7FUFP5dYEjpB63gKSAiIBLjPgnG6PMAzrRtfN4nDaTM+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 20.0.0 < 21.0.0", + "@angular-devkit/schematics": ">= 20.0.0 < 21.0.0", + "@angular-eslint/eslint-plugin": "20.0.0", + "@angular-eslint/eslint-plugin-template": "20.0.0", + "ignore": "7.0.5", + "semver": "7.7.2", + "strip-json-comments": "3.1.1" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/@angular-devkit/core": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.2.tgz", + "integrity": "sha512-qqTSpcIw+TqJ6u/tkQzqgpwVelHsHr8Jhws1Vlx6E0L6E+cRILBK48i9ttE+oYkQlcopQ3VZAmzcZodXJ1SQ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.2", + "source-map": "0.7.4" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/schematics/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-20.0.0.tgz", + "integrity": "sha512-5y9hxH/z+9rIOJp1FwRBSgJ6xt8/pgRfBF+eEIPyIHKl5mV0cVzlQiD7j1LMYTcxJZLHAoryomvSBDpmbtAlWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "20.0.0", + "eslint-scope": "^8.0.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-20.0.0.tgz", + "integrity": "sha512-3wsx0iX5f/IQgcTwXIzQq2VPHSjYXJasKNSfgMyKXn4MJGljaSNj+A0ao/5zjnwWVpL0vK5PQsk7EIuMcgAdrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.0.2.tgz", + "integrity": "sha512-p9TqZdVOFWMF75lfxk++5GZOBGO3K7qVdAXiQw89VLac8yqsu9iXFlcq34x256McHxONTjrrKBeP5oU1T8rxCw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.0.2", + "@angular/core": "20.0.2" + } + }, + "node_modules/@angular/build": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.0.1.tgz", + "integrity": "sha512-m/0jtXIeOaoU/WXtMLRuvq7UaGRxNHpoRKVVoJrifvZuNBYGM4e2lzxlIlo8kiQhPpZQc0zcAMoosbmzKKdkUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2000.1", + "@babel/core": "7.27.1", + "@babel/helper-annotate-as-pure": "7.27.1", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.10", + "@vitejs/plugin-basic-ssl": "2.0.0", + "beasties": "0.3.4", + "browserslist": "^4.23.0", + "esbuild": "0.25.5", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "8.3.3", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.1.0", + "picomatch": "4.0.2", + "piscina": "5.0.0", + "rollup": "4.40.2", + "sass": "1.88.0", + "semver": "7.7.2", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.13", + "vite": "6.3.5", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.3.0" + }, + "peerDependencies": { + "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/localize": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/service-worker": "^20.0.0", + "@angular/ssr": "^20.0.1", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^20.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.8 <5.9", + "vitest": "^3.1.1" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "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/@angular/build/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/build/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/build/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/build/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular/cdk": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.0.2.tgz", + "integrity": "sha512-gRQcpTNhnwBxXSmpnrljODUHQmB2Hnxc6L2Ad6mSMV+c3opd9KIFxL5eG2WOOPHGAaPrV4gNFw+t1i01U4grTg==", + "license": "MIT", + "dependencies": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^20.0.0 || ^21.0.0", + "@angular/core": "^20.0.0 || ^21.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "20.3.18", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.18.tgz", + "integrity": "sha512-I0kanxt3vzedZmLY4FLoxgo3yGG1mWoiGLlzwEslJdLJj5X1zd422WPtTygZgEHFHcGxR9qxdQ+PsPdMRwykQA==", + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2003.18", + "@angular-devkit/core": "20.3.18", + "@angular-devkit/schematics": "20.3.18", + "@inquirer/prompts": "7.8.2", + "@listr2/prompt-adapter-inquirer": "3.0.1", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "20.3.18", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.35.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.1", + "npm-package-arg": "13.0.0", + "pacote": "21.0.4", + "resolve": "1.22.10", + "semver": "7.7.2", + "yargs": "18.0.0", + "zod": "4.1.13" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.2003.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.18.tgz", + "integrity": "sha512-pPEDby3wQb40YSpH+UrjodJ78Z7q0Qvy3DTkS7mP2EIM4r0WVz8OlxLGS2uAc6tXSbIZe0bPp0B56P6uet3tUw==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.3.18", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "20.3.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.18.tgz", + "integrity": "sha512-zGWMjMqE8qXYr8baYCs43k9HlKz9J4Gh3Yx+7XE0uS0Y1LXzzALevSoUw7GIPdSvOriQJAEgtWE6QKssqSGltQ==", + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz", + "integrity": "sha512-3XFmGwm3u6ioREG+ynAQB7FoxfajgQnMhIu8wC5eo/Lsih4aKDg0VuIMGaOsYn7hJSJagSeaD4K8yfpkEoDEmA==", + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.1" + } + }, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular/cli/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@angular/cli/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/@angular/cli/node_modules/listr2": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", + "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@angular/cli/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/cli/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular/cli/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@angular/common": { + "version": "20.3.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.14.tgz", + "integrity": "sha512-OOUvjTtnpktJLsNupA+GFT2q5zNocPdpOENA8aSrXvAheNybLjgi+otO3U3sQsvB1VwaoEZ9GT5O3lZlstnA/A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "20.3.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "20.3.16", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.16.tgz", + "integrity": "sha512-Pt9Ms9GwTThgzdxWBwMfN8cH1JEtQ2DK5dc2yxYtPSaD+WKmG9AVL1PrzIYQEbaKcWk2jxASUHpEWSlNiwo8uw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.0.2.tgz", + "integrity": "sha512-kVKHS5ZRadTR+rRuBl3Dsccsv/jiHXdJJYlDQwQW87afd4RtAu75P3RsSd8jaUj+7P9O4Ve4vwCZVtgOh0yxbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.27.4", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "20.0.2", + "typescript": ">=5.8 <5.9" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/core": { + "version": "20.3.17", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.17.tgz", + "integrity": "sha512-YlQqxMeHI9XJw7I7oM3hYFQd4lQbK37IdlD9ztROIw5FjX6i6lmLU7+X1MQGSRi2r+X9l3IZtl33hRTNvkoUBw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "20.3.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.0.2.tgz", + "integrity": "sha512-RrQKwzFZsEDXsvesNXS4XxndEKZHC+VexIdRr1vlxx7isfvpl4htOxceW0D+Gvku1mnaS99eB/AWS50HxW3B3Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.0.2", + "@angular/core": "20.0.2", + "@angular/platform-browser": "20.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/localize": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.0.2.tgz", + "integrity": "sha512-k872tsni1WhrxZNEreCrPatcs35le1LZiLYXg6iGvDmwr8K125DiI2cC6l0dSoD6b3qwP2g+BtRT5lR8V6ROrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.27.4", + "@types/babel__core": "7.20.5", + "tinyglobby": "^0.2.12", + "yargs": "^18.0.0" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "20.0.2", + "@angular/compiler-cli": "20.0.2" + } + }, + "node_modules/@angular/material": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.0.2.tgz", + "integrity": "sha512-yIXvF+LjFdHjJWyvn1SxbWB9LdNxYnqEKbKzminW4WPXlPJMOAeyhEDFeQv9W92Zv+/ibS4tI3/SD759ejb45g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "20.0.2", + "@angular/common": "^20.0.0 || ^21.0.0", + "@angular/core": "^20.0.0 || ^21.0.0", + "@angular/forms": "^20.0.0 || ^21.0.0", + "@angular/platform-browser": "^20.0.0 || ^21.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material-luxon-adapter": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/material-luxon-adapter/-/material-luxon-adapter-20.0.2.tgz", + "integrity": "sha512-39poLZpr8tSygAcl5J6tDPzc1jh6+kdvojXR67iQl4fxW923lM8SyEBletHPrIQuA+Hds5g+gaFEBet7CWfWxw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^20.0.0 || ^21.0.0", + "@angular/material": "20.0.2", + "luxon": "^3.0.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.0.2.tgz", + "integrity": "sha512-4adMQSVlwxjY9z/LEk3Q5hr4/qbM9UD9FcqbyZOt3+BL+F2GwGdKzwg6Dj4Dv0Tv8/dudNSVgHc8lIdQ4C7K1w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "20.0.2", + "@angular/common": "20.0.2", + "@angular/core": "20.0.2" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.0.2.tgz", + "integrity": "sha512-8MDGsgcxUxSldcX6HRGB5dj+xOCQ8qmx8Vog9unEBNkuPH0vqvOepqn3prdV6dM31jYfJ9JAEKeEfNZFjuWSkA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.0.2", + "@angular/compiler": "20.0.2", + "@angular/core": "20.0.2", + "@angular/platform-browser": "20.0.2" + } + }, + "node_modules/@angular/router": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.0.2.tgz", + "integrity": "sha512-UyuTeoXkkZw1eFFNwrTfb1JXow6HKVdLNb3n9MhqDz+3ekdiqDH8EBaKhxYZxlcpNoa6cNbECZJYtaHy1lw38g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.0.2", + "@angular/core": "20.0.2", + "@angular/platform-browser": "20.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/service-worker": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-20.0.2.tgz", + "integrity": "sha512-3Kg4wFX0WjeTH7rEQurbNgcf1e4wTIie6au5/Q3ktqfYpKVa9p6d4Zqd8vJw5VqmTXnV09ZMB3czQQz2RvfBxA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "bin": { + "ngsw-config": "ngsw-config.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "20.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "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/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@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-annotate-as-pure": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "MIT", + "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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.10.tgz", + "integrity": "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.11", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.2.tgz", + "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.1", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.17", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts/node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.3.0.tgz", + "integrity": "sha512-LipbQobyEfQtu8WixasaFUZZ+JCGlho4OWwWIQ5ol0rB1RKkcZvypu7sS1CBvofBGVAa3vbOh8IOGQMrbmL5dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.3.0.tgz", + "integrity": "sha512-yA+9P+ZeA3vg76BLXWeUomIAjxfmSmR2eg8fueHXDg5Xe1Xmkl9JCKuHXUhtJ+mMVcH12d5k4kJBLbyXTadfGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.3.0.tgz", + "integrity": "sha512-EDYrW9kle+8wI19JCj/PhRnGoCN9bked5cdOPdo1wdgH/HzjgoLPFTn9DHlZccgTEVhp3O+bpWXdN/rWySVvjw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.3.0.tgz", + "integrity": "sha512-OeWvSgjXXZ/zmtLqqL78I3910F6UYpUubmsUU+iBHo6nTtjkpXms95rJtGrjkWQqwswKBD7xSMplbYC4LEsiPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.3.0.tgz", + "integrity": "sha512-wDd02mt5ScX4+xd6g78zKBr6ojpgCJCTrllCAabjgap5FzuETqOqaQfKhO+tJuGWv/J5q+GIds6uY7rNFueOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.3.0.tgz", + "integrity": "sha512-COotWhHJgzXULLiEjOgWQwqig6PoA+6ji6W+sDl6M1HhMXWIymEVHGs0edsVSNtsNSCAWMxJgR3asv6FNX/2EA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.3.0.tgz", + "integrity": "sha512-kqUgQH+l8HDbkAapx+aoko7Ez4X4DqkIraOqY/k0QY5EN/iialVlFpBUXh4wFXzirdmEVjbIUMrceUh0Kh8LeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "20.3.18", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.18.tgz", + "integrity": "sha512-JZdvBNrWODBTLrmtUF6+UD26z5cENpV0X9liR1jPDT1O7taQqwRePSuCQcjRo1qXCjlNfBW7pGGVxVCRKK8EXw==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.3.18", + "@angular-devkit/schematics": "20.3.18", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "20.3.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.18.tgz", + "integrity": "sha512-zGWMjMqE8qXYr8baYCs43k9HlKz9J4Gh3Yx+7XE0uS0Y1LXzzALevSoUw7GIPdSvOriQJAEgtWE6QKssqSGltQ==", + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@schematics/angular/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@schematics/angular/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", + "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", + "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", + "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tufjs/models/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/luxon": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "devOptional": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/underscore": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz", + "integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", + "integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/type-utils": "8.33.1", + "@typescript-eslint/utils": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.33.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", + "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz", + "integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/utils": "8.33.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.0.0.tgz", + "integrity": "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.35.0.tgz", + "integrity": "sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.1.0", + "@algolia/client-abtesting": "5.35.0", + "@algolia/client-analytics": "5.35.0", + "@algolia/client-common": "5.35.0", + "@algolia/client-insights": "5.35.0", + "@algolia/client-personalization": "5.35.0", + "@algolia/client-query-suggestions": "5.35.0", + "@algolia/client-search": "5.35.0", + "@algolia/ingestion": "1.35.0", + "@algolia/monitoring": "1.35.0", + "@algolia/recommend": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/angular-eslint": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-20.0.0.tgz", + "integrity": "sha512-9wCkzR+oxMKDXktFItI10dFaX4qCuz9SgClXdh/ZHmCANHK/RtPnXnD+gROPvhNN1M6BAJKialjIrs88orz97A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 20.0.0 < 21.0.0", + "@angular-devkit/schematics": ">= 20.0.0 < 21.0.0", + "@angular-eslint/builder": "20.0.0", + "@angular-eslint/eslint-plugin": "20.0.0", + "@angular-eslint/eslint-plugin-template": "20.0.0", + "@angular-eslint/schematics": "20.0.0", + "@angular-eslint/template-parser": "20.0.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" + } + }, + "node_modules/angular-eslint/node_modules/@angular-devkit/core": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.2.tgz", + "integrity": "sha512-qqTSpcIw+TqJ6u/tkQzqgpwVelHsHr8Jhws1Vlx6E0L6E+cRILBK48i9ttE+oYkQlcopQ3VZAmzcZodXJ1SQ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.2", + "source-map": "0.7.4" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/angular-eslint/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/angular-eslint/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/angular-eslint/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "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==", + "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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/beasties": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.4.tgz", + "integrity": "sha512-NmzN1zN1cvGccXFyZ73335+ASXwBlVWcUPssiUDIlFdfyatHPRRufjCd5w8oPaQPvVnf9ELklaCGb1gi9FBwIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "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, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "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": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "dev": true, + "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" + } + ] + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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==", + "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==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.165", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", + "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", + "dev": true + }, + "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==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "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/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "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/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "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/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/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, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "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==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "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.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.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==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.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, + "license": "MIT", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ignore-walk/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/immutable": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "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==" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "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==", + "dev": true, + "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==", + "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==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jasmine-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "dev": true + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "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, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "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, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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/karma/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.3.0.tgz", + "integrity": "sha512-MgJocUI6QEiSXQBFWLeyo1R7eQj8Rke5dlPxX0KFwli8/bsCxpM/KbXO5y0qmV/5llQ3wpneDWcTYxa+4vn8iQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.3.0", + "@lmdb/lmdb-darwin-x64": "3.3.0", + "@lmdb/lmdb-linux-arm": "3.3.0", + "@lmdb/lmdb-linux-arm64": "3.3.0", + "@lmdb/lmdb-linux-x64": "3.3.0", + "@lmdb/lmdb-win32-arm64": "3.3.0", + "@lmdb/lmdb-win32-x64": "3.3.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/marked": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz", + "integrity": "sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-fetch/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/msgpackr": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.4.tgz", + "integrity": "sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.0.tgz", + "integrity": "sha512-+t2etZAGcB7TbbLHfDwooV9ppB2LhhcT6A+L9cahsf9mEUAoQ6CktLEVvEnpD0N5CkX7zJqnPGaFtoQDy9EkHQ==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "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, + "license": "MIT", + "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/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", + "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pacote/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.1.0.tgz", + "integrity": "sha512-2ifK6Jb+ONoqOy5f+cYHsqvx1obHQdvIk13Jmt/5ezxP0U9p+fqd+R6O73KblGswyuzBYfetmsfK9ThMgnuPPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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==" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.0.0.tgz", + "integrity": "sha512-R+arufwL7sZvGjAhSMK3TfH55YdGOqhpKXkcwQJr432AAnJX/xxX19PA4QisrmJ+BTTfZVggaz6HexbkQq1l1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", + "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tuf-js/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/tuf-js/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz", + "integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.33.1", + "@typescript-eslint/parser": "8.33.1", + "@typescript-eslint/utils": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.36.tgz", + "integrity": "sha512-CPPLoCts2p7D8VbybttE3P2ylv0OBZEAy7a12DsulIEcAiMtWJy+PBgMXgWDI80D5UwqE8oQPHYnk13tm38M2Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true + }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", + "license": "ISC", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "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.2.0", + "picocolors": "^1.1.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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/zone.js": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..cd21a31 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,55 @@ +{ + "name": "ipwa", + "version": "2.1.0", + "license": "GPL-3.0-or-later", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build -c production", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "ci": "ng test --no-watch --no-progress --browsers=ChromiumHeadless", + "lint": "ng lint" + }, + "private": true, + "dependencies": { + "@angular/animations": "^20.0.2", + "@angular/cdk": "^20.0.2", + "@angular/cli": "^20.3.18", + "@angular/common": "^20.3.14", + "@angular/compiler": "^20.3.16", + "@angular/core": "^20.3.17", + "@angular/forms": "^20.0.2", + "@angular/material": "^20.0.2", + "@angular/material-luxon-adapter": "^20.0.2", + "@angular/platform-browser": "^20.0.2", + "@angular/platform-browser-dynamic": "^20.0.2", + "@angular/router": "^20.0.2", + "@angular/service-worker": "^20.0.2", + "luxon": "^3.6.1", + "marked": "^12.0.1", + "rxjs": "~7.5.0", + "tslib": "^2.3.0", + "underscore": "^1.13.8", + "zone.js": "~0.15.1" + }, + "devDependencies": { + "@angular/build": "^20.0.1", + "@angular/compiler-cli": "^20.0.2", + "@angular/localize": "^20.0.2", + "@types/jasmine": "~4.3.0", + "@types/luxon": "^3.6.2", + "@types/underscore": "^1.13.0", + "angular-eslint": "20.0.0", + "eslint": "^9.28.0", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "prettier": "3.5.3", + "typescript": "~5.8.3", + "typescript-eslint": "8.33.1" + } +} diff --git a/frontend/src/_theme-colors.scss b/frontend/src/_theme-colors.scss new file mode 100644 index 0000000..857fcc8 --- /dev/null +++ b/frontend/src/_theme-colors.scss @@ -0,0 +1,205 @@ +// This file was generated by running 'ng generate @angular/material:theme-color'. +// Proceed with caution if making changes to this file. + +@use 'sass:map'; +@use '@angular/material' as mat; + +// Note: Color palettes are generated from primary: #3850b0, secondary: #c5c330 +$_palettes: ( + primary: ( + 0: #000000, + 10: #001354, + 20: #002486, + 25: #143192, + 30: #243e9e, + 35: #324aaa, + 40: #3f57b7, + 50: #5970d2, + 60: #748aee, + 70: #93a6ff, + 80: #b8c4ff, + 90: #dde1ff, + 95: #efefff, + 98: #fbf8ff, + 99: #fefbff, + 100: #ffffff, + ), + secondary: ( + 0: #000000, + 10: #1d1d00, + 20: #333200, + 25: #3e3d00, + 30: #4a4900, + 35: #565500, + 40: #636100, + 50: #7c7b00, + 60: #979500, + 70: #b2b01a, + 80: #cecc39, + 90: #ebe954, + 95: #f9f761, + 98: #fffcc4, + 99: #fffbff, + 100: #ffffff, + ), + tertiary: ( + 0: #000000, + 10: #36003d, + 20: #560860, + 25: #63196c, + 30: #702678, + 35: #7e3385, + 40: #8b4092, + 50: #a759ad, + 60: #c473c9, + 70: #e28de5, + 80: #fdaaff, + 90: #ffd6fc, + 95: #ffebfa, + 98: #fff7fa, + 99: #fffbff, + 100: #ffffff, + ), + neutral: ( + 0: #000000, + 10: #1a1b21, + 20: #2f3037, + 25: #3a3b42, + 30: #46464d, + 35: #525259, + 40: #5e5e65, + 50: #76767e, + 60: #909098, + 70: #abaab3, + 80: #c7c5ce, + 90: #e3e1ea, + 95: #f1f0f9, + 98: #fbf8ff, + 99: #fefbff, + 100: #ffffff, + 4: #0d0e14, + 6: #121319, + 12: #1e1f25, + 17: #292a30, + 22: #34343b, + 24: #38393f, + 87: #dad9e2, + 92: #e9e7f0, + 94: #eeedf6, + 96: #f4f2fb, + ), + neutral-variant: ( + 0: #000000, + 10: #191b26, + 20: #2e303b, + 25: #393b47, + 30: #444652, + 35: #50525e, + 40: #5c5e6a, + 50: #757684, + 60: #8f909e, + 70: #a9aab9, + 80: #c5c5d4, + 90: #e1e1f1, + 95: #f0efff, + 98: #fbf8ff, + 99: #fefbff, + 100: #ffffff, + ), + error: ( + 0: #000000, + 10: #410002, + 20: #690005, + 25: #7e0007, + 30: #93000a, + 35: #a80710, + 40: #ba1a1a, + 50: #de3730, + 60: #ff5449, + 70: #ff897d, + 80: #ffb4ab, + 90: #ffdad6, + 95: #ffedea, + 98: #fff8f7, + 99: #fffbff, + 100: #ffffff, + ), +); + +$_rest: ( + secondary: map.get($_palettes, secondary), + neutral: map.get($_palettes, neutral), + neutral-variant: map.get($_palettes, neutral-variant), + error: map.get($_palettes, error), +); + +$primary-palette: map.merge(map.get($_palettes, primary), $_rest); +$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest); + +@function _high-contrast-value($light, $dark, $theme-type) { + @if ($theme-type == light) { + @return $light; + } + @if ($theme-type == dark) { + @return $dark; + } + @if ($theme-type == color-scheme) { + @return light-dark(#{$light}, #{$dark}); + } + + @error 'Unknown theme-type #{$theme-type}. Expected light, dark, or color-scheme'; +} + +@mixin high-contrast-overrides($theme-type) { + @include mat.theme-overrides(( + primary: _high-contrast-value(#00217c, #efefff, $theme-type), + on-primary: _high-contrast-value(#ffffff, #000000, $theme-type), + primary-container: _high-contrast-value(#2740a0, #b3bfff, $theme-type), + on-primary-container: _high-contrast-value(#ffffff, #00072e, $theme-type), + inverse-primary: _high-contrast-value(#b8c4ff, #253f9f, $theme-type), + primary-fixed: _high-contrast-value(#2740a0, #dde1ff, $theme-type), + primary-fixed-dim: _high-contrast-value(#032789, #b8c4ff, $theme-type), + on-primary-fixed: _high-contrast-value(#ffffff, #000000, $theme-type), + on-primary-fixed-variant: _high-contrast-value(#ffffff, #000b3c, $theme-type), + secondary: _high-contrast-value(#2e2e00, #f9f660, $theme-type), + on-secondary: _high-contrast-value(#ffffff, #000000, $theme-type), + secondary-container: _high-contrast-value(#4c4c00, #cac835, $theme-type), + on-secondary-container: _high-contrast-value(#ffffff, #0c0c00, $theme-type), + secondary-fixed: _high-contrast-value(#4c4c00, #ebe954, $theme-type), + secondary-fixed-dim: _high-contrast-value(#353400, #cecc39, $theme-type), + on-secondary-fixed: _high-contrast-value(#ffffff, #000000, $theme-type), + on-secondary-fixed-variant: _high-contrast-value(#ffffff, #121200, $theme-type), + tertiary: _high-contrast-value(#51015b, #ffeafa, $theme-type), + on-tertiary: _high-contrast-value(#ffffff, #000000, $theme-type), + tertiary-container: _high-contrast-value(#73297b, #fba4fe, $theme-type), + on-tertiary-container: _high-contrast-value(#ffffff, #1b0020, $theme-type), + tertiary-fixed: _high-contrast-value(#73297b, #ffd6fc, $theme-type), + tertiary-fixed-dim: _high-contrast-value(#590c62, #fdaaff, $theme-type), + on-tertiary-fixed: _high-contrast-value(#ffffff, #000000, $theme-type), + on-tertiary-fixed-variant: _high-contrast-value(#ffffff, #25002a, $theme-type), + background: _high-contrast-value(#fbf8ff, #121319, $theme-type), + on-background: _high-contrast-value(#1a1b21, #e3e1ea, $theme-type), + surface: _high-contrast-value(#fbf8ff, #121319, $theme-type), + surface-dim: _high-contrast-value(#b9b8c0, #121319, $theme-type), + surface-bright: _high-contrast-value(#fbf8ff, #4f4f57, $theme-type), + surface-container-low: _high-contrast-value(#f1f0f9, #1e1f25, $theme-type), + surface-container-lowest: _high-contrast-value(#ffffff, #000000, $theme-type), + surface-container: _high-contrast-value(#e3e1ea, #2f3037, $theme-type), + surface-container-high: _high-contrast-value(#d5d3dc, #3a3b42, $theme-type), + surface-container-highest: _high-contrast-value(#c7c5ce, #46464d, $theme-type), + on-surface: _high-contrast-value(#000000, #ffffff, $theme-type), + shadow: _high-contrast-value(#000000, #000000, $theme-type), + scrim: _high-contrast-value(#000000, #000000, $theme-type), + surface-tint: _high-contrast-value(#3f57b7, #b8c4ff, $theme-type), + inverse-surface: _high-contrast-value(#2f3037, #e3e1ea, $theme-type), + inverse-on-surface: _high-contrast-value(#ffffff, #000000, $theme-type), + outline: _high-contrast-value(#2a2b37, #efeffe, $theme-type), + outline-variant: _high-contrast-value(#474855, #c1c1d0, $theme-type), + error: _high-contrast-value(#600004, #ffece9, $theme-type), + on-error: _high-contrast-value(#ffffff, #000000, $theme-type), + error-container: _high-contrast-value(#98000a, #ffaea4, $theme-type), + on-error-container: _high-contrast-value(#ffffff, #220001, $theme-type), + surface-variant: _high-contrast-value(#e1e1f1, #444652, $theme-type), + on-surface-variant: _high-contrast-value(#000000, #ffffff, $theme-type), + )) + } diff --git a/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.html b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.html new file mode 100644 index 0000000..8a73adf --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.html @@ -0,0 +1,39 @@ +
+ + Wyszukaj + + + +
+
+ @if (ac.state() !== STATE.LOADED) { + + } + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
Imię{{element.fname}}Nazwisko{{element.surname}}Pokój{{element.room}}Nazwa użytkownika{{element.uname}}Karta użytkownika + +
+ +
diff --git a/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.scss b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.scss new file mode 100644 index 0000000..5475256 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.scss @@ -0,0 +1,30 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; +} + +.mainc { + position: relative; + height: 100%; + display: flex; + flex-direction: column; +} + +mat-paginator { + margin-top: auto; +} + +mat-form-field { + flex-grow: 1; +} + +#upper-bar { + display: flex; +} + +button[mat-icon-button] { + margin-left: 4pt; + margin-right: 4pt; + margin-top: 4pt; +} diff --git a/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.spec.ts b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.spec.ts new file mode 100644 index 0000000..81c782a --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AccountMgmtComponent } from './account-mgmt.component' +import { MatDialogModule } from '@angular/material/dialog' +import { MatSnackBarModule } from '@angular/material/snack-bar' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatIconModule } from '@angular/material/icon' +import { MatPaginatorModule } from '@angular/material/paginator' +import { MatTableModule } from '@angular/material/table' +import { MatInputModule } from '@angular/material/input' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { AccountMgmtService } from './account-mgmt.service' +import { LoadShadeComponent } from 'src/app/commonComponents/load-shade/load-shade.component' +import { of } from 'rxjs' +import { signal } from '@angular/core' +import { STATE } from 'src/app/types/state' + +describe('AccountMgmtComponent', () => { + let component: AccountMgmtComponent + let fixture: ComponentFixture + let acMock + + beforeEach(async () => { + acMock = { + accs: of([]), + state: signal(STATE.NOT_LOADED), + refresh: jasmine.createSpy('getAccs'), + error: signal(undefined) + } + await TestBed.configureTestingModule({ + declarations: [AccountMgmtComponent, LoadShadeComponent], + providers: [{ provide: AccountMgmtService, useValue: acMock }], + imports: [ + MatDialogModule, + MatSnackBarModule, + MatFormFieldModule, + MatIconModule, + MatPaginatorModule, + MatTableModule, + MatInputModule, + BrowserAnimationsModule, + MatProgressSpinnerModule, + ], + }).compileComponents() + fixture = TestBed.createComponent(AccountMgmtComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.ts b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.ts new file mode 100644 index 0000000..b024996 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account-mgmt.component.ts @@ -0,0 +1,79 @@ +import { AfterViewInit, Component, inject, ViewChild } from '@angular/core' +import { MatDialog } from '@angular/material/dialog' +import { MatTableDataSource } from '@angular/material/table' +import { MatPaginator } from '@angular/material/paginator' +import { UserEditComponent, UserEditComponentInputData, UserEditComponentReturnData } from './user-edit/user-edit.component' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { Group } from 'src/app/types/group' +import { User } from 'src/app/admin-view/account-mgmt/account.model' +import { AccountMgmtService } from './account-mgmt.service' +import { STATE } from 'src/app/types/state' + +@Component({ + selector: 'app-account-mgmt', + templateUrl: './account-mgmt.component.html', + styleUrls: ['./account-mgmt.component.scss'], + standalone: false, +}) +export class AccountMgmtComponent implements AfterViewInit { + protected groups: Group[] = [] + users: MatTableDataSource + @ViewChild(MatPaginator) paginator!: MatPaginator + + protected ac = inject(AccountMgmtService) + private dialog = inject(MatDialog) + protected ls = inject(LocalStorageService) + + constructor() { + this.users = new MatTableDataSource() + this.users.filterPredicate = ( + data: User, + filter: string + ): boolean => { + const dataStr = Object.keys(data) + .reduce((curr: string, key: string) => { + if (['_id', 'admin', 'groups', '__v', 'locked'].find(v => v == key)) { + return curr + '' + } + return curr + data[key as keyof User] + '⫂' + }, '') + .toLowerCase() + const filternew = filter.trim().toLowerCase() + return dataStr.indexOf(filternew) != -1 + } + this.ac.refresh() + this.ac.accs.subscribe(d => { + this.users.data = d + }) + } + + protected get STATE(): typeof STATE { + return STATE + } + + ngAfterViewInit() { + this.users.paginator = this.paginator + } + + filter(event: Event) { + const value = (event.target as HTMLInputElement).value + this.users.filter = value.toLowerCase().trim() + } + + openUserCard(id?: string) { + this.dialog + .open< + UserEditComponent, + UserEditComponentInputData, + UserEditComponentReturnData + >(UserEditComponent, { + data: { id: id, type: id ? 'edit' : 'new', groups: this.groups }, + }) + .afterClosed() + .subscribe(r => { + if (r) this.ac.refresh() + }) + } + + collumns = ['name', 'surname', 'uname', 'actions'] +} diff --git a/frontend/src/app/admin-view/account-mgmt/account-mgmt.service.spec.ts b/frontend/src/app/admin-view/account-mgmt/account-mgmt.service.spec.ts new file mode 100644 index 0000000..3be4308 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account-mgmt.service.spec.ts @@ -0,0 +1,96 @@ +import { TestBed } from '@angular/core/testing'; + +import { AccountMgmtService } from './account-mgmt.service'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { environment } from 'src/environments/environment'; +import { skip } from 'rxjs'; + +describe('AccountMgmtService', () => { + let service: AccountMgmtService; + let httpTesting: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + httpTesting = TestBed.inject(HttpTestingController); + service = TestBed.inject(AccountMgmtService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should get user accounts', () => { + service.refresh() + const req = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request to load all users") + + expect(req.request.method).toBe("GET") + + req.flush([]) + httpTesting.verify() + }) + + describe('create user', () => { + xit('should create a user account and refresh list', () => { + const test_user = { + uname: "test", + groups: [] + } + service.postAcc(test_user).subscribe(v => { + expect(v).toEqual(jasmine.objectContaining(test_user)) + }) + const req = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request new user") + + expect(req.request.method).toBe("POST") + + req.flush({ + ...test_user, + _id: "test_id" + }) + + const req2 = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request to load all users") + + expect(req2.request.method).toBe("GET") + + // service.accs.pipe(skip(1)).subscribe(v => { + // expect(v).toContain(createdUser) + // }) + + + req2.flush([ + { + ...test_user, + _id: "test_id" + } + ]) + httpTesting.verify() + }) + }) + + describe("delete user", () => { + it('should refresh accounts and not to contain deleted user', async () => { + service.deleteAcc("test").subscribe() + const req = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs/test", "Request delete user") + + expect(req.request.method).toBe("DELETE") + + req.flush({ status: 200 }) + + const req2 = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request to load all users") + + expect(req2.request.method).toBe("GET") + service.accs.pipe(skip(1)).subscribe(v => { + expect(v).not.toContain(jasmine.objectContaining({ _id: "test" })) + }) + + req2.flush([]) + + httpTesting.verify() + }) + }) +}); diff --git a/frontend/src/app/admin-view/account-mgmt/account-mgmt.service.ts b/frontend/src/app/admin-view/account-mgmt/account-mgmt.service.ts new file mode 100644 index 0000000..e323cf6 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account-mgmt.service.ts @@ -0,0 +1,109 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { BehaviorSubject, catchError, map, of, tap } from 'rxjs'; +import { STATE } from 'src/app/types/state'; +import { Status } from 'src/app/types/status'; +import { User, UserAPI } from 'src/app/admin-view/account-mgmt/account.model'; +import { environment } from 'src/environments/environment'; +import { DateTime } from 'luxon'; + +@Injectable({ + providedIn: 'root' +}) +export class AccountMgmtService { + private http = inject(HttpClient) + + private _accs = new BehaviorSubject([]) + public readonly accs = this._accs.asObservable() + private _state = signal(STATE.NOT_LOADED); + public readonly state = this._state.asReadonly(); + private _error = signal(undefined); + public readonly error = this._error.asReadonly(); + private _selAcc = new BehaviorSubject(null) + public readonly selAcc = this._selAcc.asObservable() + + public refresh() { + this.getAccs() + } + + private getAccs() { + this._state.set(STATE.PENDING) + this.http.get + + (environment.apiEndpoint + `/admin/accs`, { withCredentials: true }) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }), + map(v => { + return v.map(i => ({ + ...i, + regDate: DateTime.fromISO(i.regDate) + })) + }) + ).subscribe(v => { + this._error.set(undefined) + this._accs.next(v ?? []) + this._state.set(STATE.LOADED) + }) + } + + selectAccount(acc: User) { + this._selAcc.next(acc) + } + + //#region legacy + postAcc(item: Omit) { + return this.http.post( + environment.apiEndpoint + `/admin/accs`, + item, + { withCredentials: true } + ).pipe(tap(v => { + if (v instanceof Array) this.refresh() + })) + } + + putAcc(id: string, update: Partial) { + return this.http.put( + environment.apiEndpoint + `/admin/accs/${id}`, + update, + { withCredentials: true } + ) + } + + resetPass(id: string) { + return this.http.patch( + environment.apiEndpoint + `/admin/accs/${id}/reset`, + undefined, + { withCredentials: true } + ) + } + + deleteAcc(id: string) { + return this.http.delete( + environment.apiEndpoint + `/admin/accs/${id}`, + { withCredentials: true } + ) + .pipe(tap(v => { + if (v.status == 200) this.refresh() + })) + } + + getUser(id: string) { + return this.http.get< + Omit & { lockout: boolean; regDate: string } + >(environment.apiEndpoint + `/admin/accs/${id}`, { + withCredentials: true, + }) + } + + clearLockout(id: string) { + return this.http.delete( + environment.apiEndpoint + `/admin/accs/${id}/lockout`, + { withCredentials: true } + ) + } + //#endregion +} diff --git a/frontend/src/app/admin-view/account-mgmt/account.model.ts b/frontend/src/app/admin-view/account-mgmt/account.model.ts new file mode 100644 index 0000000..c65be43 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/account.model.ts @@ -0,0 +1,16 @@ +import { DateTime } from 'luxon' + +export interface User { + _id: string + uname: string + room?: string + admin?: string[] + locked?: boolean + fname?: string + surname?: string + groups: string[] + regDate: DateTime + defaultPage?: string +} + +export type UserAPI = Omit & {regDate: "string"} diff --git a/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.html b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.html new file mode 100644 index 0000000..81ea3e3 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.html @@ -0,0 +1,5 @@ +

Czy na pewno chcesz usunąć tego użytkownika

+ + + + diff --git a/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.scss b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.spec.ts b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.spec.ts new file mode 100644 index 0000000..de6317a --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { UserDeleteComponent } from './user-delete.component' +import { MatDialogModule } from '@angular/material/dialog' + +describe('UserDeleteComponent', () => { + let component: UserDeleteComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [UserDeleteComponent], + imports: [MatDialogModule], + }) + fixture = TestBed.createComponent(UserDeleteComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.ts b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.ts new file mode 100644 index 0000000..de9b1f4 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-delete/user-delete.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-user-delete', + templateUrl: './user-delete.component.html', + styleUrls: ['./user-delete.component.scss'], + standalone: false, +}) +export class UserDeleteComponent {} diff --git a/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.html b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.html new file mode 100644 index 0000000..3e45833 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.html @@ -0,0 +1,85 @@ +

Karta użytkownika

+ +
+
+ + Imię + + + + Nazwisko + + + + Pokój + + + + Grupy + + @for (item of adsyn.groups; track $index) { + {{item.name}} + } + + + @if (data.type === 'edit') { + Data rejestracji:
{{regDate?.toFormat('D')}}
+ } +
+
+ + Nazwa użytkownika + + + @if (data.type === "edit") { + + @if (locked) { + + } @else { + + } + @if (lockout) { + + } @else { + + } + @if (ls.permChecker("accs")) { + + Uprawnienia + + @if (ls.capCheck("news")) { + Wiadomości + } + @if (ls.capCheck("menu")) { + Jadłospis + } + @if (ls.capCheck("notif")) { + Powiadomienia + } + @if (ls.capCheck("groups")) { + Grupy + } + Konta + @if (ls.capCheck("key")) { + Klucze + } + @if (ls.capCheck("clean")) { + Czystość + } + + + } + } +
+
+
+ + @if (data.type === "edit") { + + } + + + @if (loading) { + + } + diff --git a/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.scss b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.scss new file mode 100644 index 0000000..eb8fb5e --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.scss @@ -0,0 +1,32 @@ +:host { + padding: 8pt; + display: block; +} + +form { + margin-top: 1ch !important; + display: flex; + grid-auto-flow: column; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; + column-gap: 3ch; + div { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: repeat(5, 1fr); + align-items: center; + button { + align-self: stretch; + justify-self: stretch; + height: auto; + margin-bottom: 1lh; + } + } +} + +mat-dialog-actions { + display: flex; + justify-content: flex-end; +} diff --git a/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.spec.ts b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.spec.ts new file mode 100644 index 0000000..1d43bb5 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { UserEditComponent } from './user-edit.component' +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog' +import { MatFormFieldModule } from '@angular/material/form-field' +import { ReactiveFormsModule } from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { MatSelectModule } from '@angular/material/select' +import { AccountMgmtService } from '../account-mgmt.service' + +xdescribe('UserEditComponent', () => { + let component: UserEditComponent + let fixture: ComponentFixture + let acMock + + beforeEach(async () => { + acMock = {} + await TestBed.configureTestingModule({ + declarations: [UserEditComponent], + imports: [ + MatDialogModule, + MatFormFieldModule, + ReactiveFormsModule, + MatInputModule, + NoopAnimationsModule, + MatSelectModule, + ], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: { groups: [] } }, + { provide: AccountMgmtService, useValue: acMock }, + ], + }).compileComponents() + fixture = TestBed.createComponent(UserEditComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.ts b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.ts new file mode 100644 index 0000000..3c036ce --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-edit/user-edit.component.ts @@ -0,0 +1,198 @@ +import { Component, inject } from '@angular/core' +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogRef, +} from '@angular/material/dialog' +import { FormControl, FormGroup } from '@angular/forms' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { Group } from 'src/app/types/group' +import { UserDeleteComponent } from '../user-delete/user-delete.component' +import { MatSnackBar } from '@angular/material/snack-bar' +import { UserResetComponent } from '../user-reset/user-reset.component' +import { catchError, throwError } from 'rxjs' +import { DateTime } from 'luxon' +import { AccountMgmtService } from '../account-mgmt.service' +import { AdminSyncService } from '../../admin-sync.service' + +export interface UserEditComponentInputData { type: 'new' | 'edit'; id?: string; groups: Group[] } +export type UserEditComponentReturnData = true | undefined + +@Component({ + selector: 'app-user-edit', + templateUrl: './user-edit.component.html', + styleUrls: ['./user-edit.component.scss'], + standalone: false, +}) +export class UserEditComponent { + lockout = false + locked = false + loading = false + form: FormGroup = new FormGroup({ + fname: new FormControl(''), + surname: new FormControl(''), + room: new FormControl(''), + uname: new FormControl(''), + groups: new FormControl([]), + flags: new FormControl([]), + }) + id?: string + regDate?: DateTime + + public dialogRef: MatDialogRef = inject(MatDialogRef) + public data: UserEditComponentInputData = inject(MAT_DIALOG_DATA) + protected ls = inject(LocalStorageService) + private acu = inject(AccountMgmtService) + private dialog = inject(MatDialog) + private sb = inject(MatSnackBar) + protected adsyn = inject(AdminSyncService) + + constructor() { + if (this.data.type == 'edit') { + this.id = this.data.id + this.acu.getUser(this.data.id!).subscribe(r => { + this.regDate = DateTime.fromISO(r.regDate) + this.locked = r.locked ? true : false + this.lockout = r.lockout + this.form.get('fname')?.setValue(r.fname) + this.form.get('surname')?.setValue(r.surname) + this.form.get('room')?.setValue(r.room) + this.form.get('uname')?.setValue(r.uname) + this.form.get('groups')?.setValue(r.groups) + this.form.get('flags')?.setValue(r.admin) + }) + } + } + + protected submit() { + this.loading = true + if (this.data.type == 'edit') { + this.acu + .putAcc(this.id!, this.getForm()) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + if (data.status == 200) { + this.sb.open('Użytkownik został zmodyfikowany.', undefined, { + duration: 2500, + }) + this.dialogRef.close(true) + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + this.loading = false + } + }) + } else { + this.acu + .postAcc(this.getForm()) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + if (data) { + this.sb.open('Użytkownik został utworzony.', undefined, { + duration: 2500, + }) + this.dialogRef.close(true) + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + this.loading = false + } + }) + } + } + + protected disableLockout() { + this.loading = true + this.acu + .clearLockout(this.id!) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(s => { + if (s.status == 200) { + this.loading = false + this.lockout = false + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + this.loading = false + } + }) + } + + protected getForm() { + return { + fname: this.form.get('fname')?.value, + surname: this.form.get('surname')?.value, + room: this.form.get('room')?.value, + uname: this.form.get('uname')?.value, + groups: this.form.get('groups')?.value, + admin: (() => { + const value = this.form.get('flags')?.value + if (this.ls.permChecker("super")) { + return value + } else { + return undefined + } + })(), + } + } + + protected delete() { + this.dialog + .open(UserDeleteComponent) + .afterClosed() + .subscribe(reply => { + if (reply) { + this.acu.deleteAcc(this.id!).subscribe(res => { + if (res.status == 200) { + this.sb.open('Użytkownik został usunięty.', undefined, { + duration: 2500, + }) + this.dialogRef.close() + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + console.error(res) + } + }) + } + }) + } + + protected resetPass() { + this.loading = true + this.dialog + .open(UserResetComponent) + .afterClosed() + .subscribe(res => { + if (res == true) { + this.acu.resetPass(this.id!).subscribe(patch => { + if (patch.status == 200) { + this.sb.open('Hasło zostało zresetowane', undefined, { + duration: 2500, + }) + this.loading = false + } + }) + } + }) + } + + protected toggleLock(state: boolean) { + this.acu.putAcc(this.id!, { locked: state }).subscribe(res => { + if (res.status == 200) { + this.locked = state + } + }) + } +} diff --git a/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.html b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.html new file mode 100644 index 0000000..08066e0 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.html @@ -0,0 +1,8 @@ +

Reset hasła

+ + Czy chcesz zresetować hasło temu użytkownikowi? + + + + + diff --git a/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.scss b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.spec.ts b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.spec.ts new file mode 100644 index 0000000..708c718 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { UserResetComponent } from './user-reset.component' +import { MatDialogModule } from '@angular/material/dialog' + +describe('UserResetComponent', () => { + let component: UserResetComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [UserResetComponent], + imports: [MatDialogModule], + }) + fixture = TestBed.createComponent(UserResetComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.ts b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.ts new file mode 100644 index 0000000..6626255 --- /dev/null +++ b/frontend/src/app/admin-view/account-mgmt/user-reset/user-reset.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-user-reset', + templateUrl: './user-reset.component.html', + styleUrls: ['./user-reset.component.scss'], + standalone: false, +}) +export class UserResetComponent {} diff --git a/frontend/src/app/admin-view/admin-sync.service.spec.ts b/frontend/src/app/admin-view/admin-sync.service.spec.ts new file mode 100644 index 0000000..9ee7784 --- /dev/null +++ b/frontend/src/app/admin-view/admin-sync.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { AdminSyncService } from './admin-sync.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('AdminSyncService', () => { + let service: AdminSyncService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(AdminSyncService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/admin-sync.service.ts b/frontend/src/app/admin-view/admin-sync.service.ts new file mode 100644 index 0000000..0013b54 --- /dev/null +++ b/frontend/src/app/admin-view/admin-sync.service.ts @@ -0,0 +1,27 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Group } from '../types/group'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class AdminSyncService { + private http = inject(HttpClient) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _data: any + + private sync() { + this.http.get(environment.apiEndpoint + `/admin/sync`, { withCredentials: true }).subscribe(v => { + this._data = v + }) + } + + public get groups(): Group[] { + const groups = this._data?.groups + if (!groups) this.sync() + return groups + } + +} diff --git a/frontend/src/app/admin-view/admin-view.component.html b/frontend/src/app/admin-view/admin-view.component.html new file mode 100644 index 0000000..a982dae --- /dev/null +++ b/frontend/src/app/admin-view/admin-view.component.html @@ -0,0 +1,24 @@ + + + + + @for (link of LINKS; track $index) { + + {{link.icon}} + {{link.title}} + + } + + developer_guide + Dokumentacja + + + close +

Zakończ edycję

+
+
+
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/admin-view/admin-view.component.scss b/frontend/src/app/admin-view/admin-view.component.scss new file mode 100644 index 0000000..57670bf --- /dev/null +++ b/frontend/src/app/admin-view/admin-view.component.scss @@ -0,0 +1,21 @@ +@use "@angular/material" as mat; +@use "../../theme-colors" as theme; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + @include mat.theme(( + color: theme.$tertiary-palette + )) +} + +mat-sidenav, +mat-toolbar { + padding: 8pt; +} + +mat-sidenav-container { + flex-grow: 1; +} diff --git a/frontend/src/app/admin-view/admin-view.component.spec.ts b/frontend/src/app/admin-view/admin-view.component.spec.ts new file mode 100644 index 0000000..afcd8bf --- /dev/null +++ b/frontend/src/app/admin-view/admin-view.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AdminViewComponent } from './admin-view.component' +import { MatToolbarModule } from '@angular/material/toolbar' +import { MatIconModule } from '@angular/material/icon' +import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { MatListModule } from '@angular/material/list' +import { RouterModule } from '@angular/router' +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'app-toolbar', + template: '', + standalone: false, +}) +class ToolbarMock { + @Input() drawer!: MatDrawer +} + +describe('AdminViewComponent', () => { + let component: AdminViewComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AdminViewComponent, ToolbarMock], + imports: [ + MatToolbarModule, + MatIconModule, + MatSidenavModule, + BrowserAnimationsModule, + MatListModule, + RouterModule.forRoot([]), + ], + }) + fixture = TestBed.createComponent(AdminViewComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/admin-view.component.ts b/frontend/src/app/admin-view/admin-view.component.ts new file mode 100644 index 0000000..aeb92ba --- /dev/null +++ b/frontend/src/app/admin-view/admin-view.component.ts @@ -0,0 +1,78 @@ +import { Component, inject } from '@angular/core' +import { Router } from '@angular/router' +import { LocalStorageService } from '../services/local-storage.service' +import { Link } from '../types/link' + +@Component({ + selector: 'app-admin-view', + templateUrl: './admin-view.component.html', + styleUrls: ['./admin-view.component.scss'], + standalone: false, +}) +export class AdminViewComponent { + readonly router = inject(Router) + readonly ls = inject(LocalStorageService) + + private readonly _LINKS: Link[] = [ + { + title: 'Wiadomości', + icon: 'newspaper', + href: 'news', + enabled: this.ls.permChecker("news") && this.ls.capCheck("news"), + }, + { + title: 'Jadłospis', + icon: 'restaurant_menu', + href: 'menu', + enabled: this.ls.permChecker("menu") && this.ls.capCheck("menu"), + }, + { + title: 'Wysyłanie powiadomień', + icon: 'notifications', + href: 'notifications', + enabled: this.ls.permChecker("notif") && this.ls.capCheck("notif"), + }, + { + title: 'Grupy', + icon: 'groups', + href: 'groups', + enabled: this.ls.permChecker("groups") && this.ls.capCheck("groups"), + }, + { + title: 'Zarządzanie kontami', + icon: 'manage_accounts', + href: 'accounts', + enabled: this.ls.permChecker("accs"), + }, + { + title: 'Klucze', + icon: 'key', + href: 'keys', + enabled: this.ls.permChecker("keys") && this.ls.capCheck("key"), + }, + { + title: 'Czystość', + icon: 'cleaning_services', + href: 'grades', + enabled: this.ls.permChecker("grades") && this.ls.capCheck("clean"), + }, + { + title: 'Frekwencja', + icon: 'checklist', + href: 'attendence', + enabled: false, + }, + { + title: 'Ustawienia', + icon: 'settings_applications', + href: 'settings', + enabled: this.ls.permChecker("super"), + }, + ] + public get LINKS(): Link[] { + return this._LINKS.filter(v => v.enabled) + } + goNormal() { + this.router.navigateByUrl('app') + } +} diff --git a/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.html b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.html new file mode 100644 index 0000000..36ba08f --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.html @@ -0,0 +1,29 @@ +
+

Uwaga: Obecność resetuje się o codziennie o 00:00

+
+ Legenda: + Wychowanek obecny + Wyjście w ciągu 30 min. + Wychowanek nieobecny +
+
+ +
+
+ + +
+
+ + +
+
+ + + + +
Pokój{{item.room}}Godziny@for (i of item.hours.sort().reverse(); track i; let isLast = $last) { + {{ isLast ? '' : ', '}} + }{{item.notes}}Usuń@if (!item.auto) { + + }
diff --git a/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.scss b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.scss new file mode 100644 index 0000000..02ef824 --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.scss @@ -0,0 +1,34 @@ +@use "sass:list"; + +#guide { + margin: 1em; +} + +#legend { + display: flex; + justify-self: center; + gap: 3ch; + * { + margin: 2px; + } +} + +.circle { + &::before { + border-radius: 7.5%; + width: 2.5ch; + height: 2.5ch; + display: inline-block; + content: ""; + vertical-align: middle; + margin-right: 3px; + } + + $list: (red, yellow, green); + + @for $n from 1 through 3 { + &:nth-of-type(#{$n})::before { + background-color: list.nth($list, $n); + } + } +} diff --git a/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.spec.ts b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.spec.ts new file mode 100644 index 0000000..a563eae --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AttendenceSummaryComponent } from './attendence-summary.component' +import { RouterModule } from '@angular/router' +import { MatTableModule } from '@angular/material/table' +import { GradesService } from '../grades.service' + +xdescribe('AttendenceSummaryComponent', () => { + let component: AttendenceSummaryComponent + let fixture: ComponentFixture + let acMock + + beforeEach(async () => { + acMock = {} + await TestBed.configureTestingModule({ + declarations: [AttendenceSummaryComponent], + imports: [RouterModule.forRoot([]), MatTableModule], + providers: [{ provide: GradesService, useValue: acMock }], + }).compileComponents() + + fixture = TestBed.createComponent(AttendenceSummaryComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.ts b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.ts new file mode 100644 index 0000000..923b84c --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/attendence-summary.component.ts @@ -0,0 +1,54 @@ +import { Component, inject, OnInit } from '@angular/core' +import { ToolbarService } from '../../toolbar/toolbar.service' +import { Router, ActivatedRoute } from '@angular/router' +import { MatTableDataSource } from '@angular/material/table' +import { GradesService } from '../grades.service' + +@Component({ + selector: 'app-attendence-summary', + templateUrl: './attendence-summary.component.html', + styleUrl: './attendence-summary.component.scss', + standalone: false, +}) +export class AttendenceSummaryComponent implements OnInit { + data: MatTableDataSource<{ + room: string + hours: string[] + notes: string + auto: boolean + }> = new MatTableDataSource<{ + room: string + hours: string[] + notes: string + auto: boolean + }>() + collumns = ['room', 'hours', 'actions'] + + private toolbar = inject(ToolbarService) + private router = inject(Router) + private route = inject(ActivatedRoute) + private ac = inject(GradesService) + + constructor() { + this.toolbar.comp = this + this.toolbar.menu = [ + { check: true, title: 'Ocenianie', fn: 'goBack', icon: 'arrow_back' }, + ] + } + + delete(room: string) { + this.ac.attendence.deleteRoom(room).subscribe(() => { + this.ngOnInit() + }) + } + + ngOnInit(): void { + this.ac.attendence.getSummary().subscribe(v => { + this.data.data = v + }) + } + + goBack() { + this.router.navigate(['../'], { relativeTo: this.route }) + } +} diff --git a/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.html b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.html new file mode 100644 index 0000000..656d4e0 --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.html @@ -0,0 +1 @@ +{{value}} \ No newline at end of file diff --git a/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.scss b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.scss new file mode 100644 index 0000000..ebbcfce --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.scss @@ -0,0 +1,8 @@ +:host { + display: inline; +} + +span { + padding: 2px; + border-radius: 7.5%; +} diff --git a/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.spec.ts b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.spec.ts new file mode 100644 index 0000000..fde16b8 --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { HourDisplayComponent } from './hour-display.component' + +describe('HourDisplayComponent', () => { + let component: HourDisplayComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HourDisplayComponent], + }).compileComponents() + + fixture = TestBed.createComponent(HourDisplayComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.ts b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.ts new file mode 100644 index 0000000..610407d --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence-summary/hour-display/hour-display.component.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core' +import { DateTime } from 'luxon' + +@Component({ + selector: 'app-hour-display', + templateUrl: './hour-display.component.html', + styleUrl: './hour-display.component.scss', + standalone: false, +}) +export class HourDisplayComponent { + @Input() value = '' + + style() { + if (/(0+[0-9]|1[0-9]|2[0-3]):(0+[0-9]|[1-5][0-9])/g.test(this.value)) { + const diff = DateTime.fromFormat(this.value, 'HH:mm').diffNow('minutes') + if (diff.as('minutes') > 30) { + return { 'background-color': 'red' } + } else if (diff.as('minutes') > 0) { + return { 'background-color': 'yellow', color: 'black' } + } else { + return { 'background-color': 'green' } + } + } else { + return { color: 'gray' } + } + } +} diff --git a/frontend/src/app/admin-view/grades/attendence/attendence.component.html b/frontend/src/app/admin-view/grades/attendence/attendence.component.html new file mode 100644 index 0000000..d86f8b1 --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence/attendence.component.html @@ -0,0 +1,22 @@ +Obecność w {{room}} + +
+
+ @for (item of users.controls; track i; let i = $index) { +
+ + : + + +
+ } +
+ + Notatki + + +
+
+ + + \ No newline at end of file diff --git a/frontend/src/app/admin-view/grades/attendence/attendence.component.scss b/frontend/src/app/admin-view/grades/attendence/attendence.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/grades/attendence/attendence.component.spec.ts b/frontend/src/app/admin-view/grades/attendence/attendence.component.spec.ts new file mode 100644 index 0000000..9201b81 --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence/attendence.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AttendenceComponent } from './attendence.component' +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog' +import { MatFormFieldModule } from '@angular/material/form-field' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { GradesService } from '../grades.service' + +xdescribe('AttendenceComponent', () => { + let component: AttendenceComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const acMock = {} + await TestBed.configureTestingModule({ + declarations: [AttendenceComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: {} }, + { provide: GradesService, useValue: acMock }, + ], + imports: [ + MatDialogModule, + MatFormFieldModule, + FormsModule, + ReactiveFormsModule, + MatInputModule, + NoopAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(AttendenceComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/grades/attendence/attendence.component.ts b/frontend/src/app/admin-view/grades/attendence/attendence.component.ts new file mode 100644 index 0000000..4703ead --- /dev/null +++ b/frontend/src/app/admin-view/grades/attendence/attendence.component.ts @@ -0,0 +1,55 @@ +import { Component, inject, OnInit } from '@angular/core' +import { FormArray, FormBuilder, FormGroup } from '@angular/forms' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { GradesService } from '../grades.service' + +@Component({ + selector: 'app-attendence', + templateUrl: './attendence.component.html', + styleUrl: './attendence.component.scss', + standalone: false, +}) +export class AttendenceComponent implements OnInit { + private fb = inject(FormBuilder) + public data: { room: string } = inject(MAT_DIALOG_DATA) + public dialogRef: MatDialogRef = inject(MatDialogRef) + private ac = inject(GradesService) + + ngOnInit(): void { + this.room = this.data.room + this.ac.attendence.getUsers(this.room).subscribe(query => { + query.users.forEach(v => { + const att = query.attendence + ? query.attendence.auto.find(z => z.id == v._id) + : false + this.users.push( + this.fb.group({ + id: v._id, + label: `${v.fname} ${v.surname}`, + att: this.fb.control(att), + hour: this.fb.control(att ? att.hour : ''), + }) + ) + }) + this.form.get('notes')?.setValue(query.attendence?.notes) + }) + } + + save() { + this.dialogRef.close({ + room: this.room, + ...this.form.value, + }) + } + + room = '' + + form: FormGroup = this.fb.group({ + users: this.fb.array([]), + notes: this.fb.control(''), + }) + + get users() { + return this.form.get('users') as FormArray + } +} diff --git a/frontend/src/app/admin-view/grades/grades.component.html b/frontend/src/app/admin-view/grades/grades.component.html new file mode 100644 index 0000000..043e34c --- /dev/null +++ b/frontend/src/app/admin-view/grades/grades.component.html @@ -0,0 +1,34 @@ + + +
+

Czystość pokoju {{room}} na {{date().toFormat("cccc, D")}}

+

Ocena: {{grade}}

+
+ + + + @if (id) { + + } +
+ @for (item of things.controls; track item; let i = $index) { +
+
+ + + @if (cb.checked) { + + + + + + } + +
+
+ } + + Dodatkowe uwagi + + +
diff --git a/frontend/src/app/admin-view/grades/grades.component.scss b/frontend/src/app/admin-view/grades/grades.component.scss new file mode 100644 index 0000000..86943de --- /dev/null +++ b/frontend/src/app/admin-view/grades/grades.component.scss @@ -0,0 +1,10 @@ +div#things { + display: flex; + flex-direction: column; +} + +div#buttons { + * { + margin: 0 2px 0 2px; + } +} diff --git a/frontend/src/app/admin-view/grades/grades.component.spec.ts b/frontend/src/app/admin-view/grades/grades.component.spec.ts new file mode 100644 index 0000000..ba0fe54 --- /dev/null +++ b/frontend/src/app/admin-view/grades/grades.component.spec.ts @@ -0,0 +1,64 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { GradesComponent } from './grades.component' +import { RouterModule } from '@angular/router' +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { MatIconModule } from '@angular/material/icon' +import { MatFormFieldModule } from '@angular/material/form-field' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { DateTime } from 'luxon' +import { GradesService } from './grades.service' + +@Component({ + selector: 'app-date-selector', + template: '', + standalone: false, +}) +class DateSelectorStub { + @Input() date: string = DateTime.now().toISODate() + @Output() dateChange = new EventEmitter() + @Input() filter: (date: DateTime | null) => boolean = () => true +} + +@Component({ + selector: 'app-room-chooser', + template: '', + standalone: false, +}) +class RoomSelectorStub { + @Input() rooms: string[] = [] + @Output() room: EventEmitter = new EventEmitter() +} + +xdescribe('GradesComponent', () => { + let component: GradesComponent + let fixture: ComponentFixture + let acMock + + beforeEach(async () => { + acMock = {} + await TestBed.configureTestingModule({ + declarations: [GradesComponent, DateSelectorStub, RoomSelectorStub], + providers: [{ provide: GradesService, useValue: acMock }], + imports: [ + RouterModule.forRoot([]), + MatIconModule, + MatFormFieldModule, + FormsModule, + ReactiveFormsModule, + MatInputModule, + NoopAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(GradesComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/grades/grades.component.ts b/frontend/src/app/admin-view/grades/grades.component.ts new file mode 100644 index 0000000..6b2b0d3 --- /dev/null +++ b/frontend/src/app/admin-view/grades/grades.component.ts @@ -0,0 +1,221 @@ +import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core' +import { FormArray, FormBuilder } from '@angular/forms' +import { filterLook, weekendFilter } from 'src/app/util' +import { MatSnackBar } from '@angular/material/snack-bar' +import { ToolbarService } from '../toolbar/toolbar.service' +import { ActivatedRoute, Router } from '@angular/router' +import { MatDialog } from '@angular/material/dialog' +import { AttendenceComponent } from './attendence/attendence.component' +import { DateTime } from 'luxon' +import { GradesService } from './grades.service' + +@Component({ + selector: 'app-grades', + templateUrl: './grades.component.html', + styleUrl: './grades.component.scss', + standalone: false, +}) +export class GradesComponent implements OnInit, OnDestroy { + private ac = inject(GradesService) + private fb = inject(FormBuilder) + private sb = inject(MatSnackBar) + private toolbar = inject(ToolbarService) + private router = inject(Router) + private route = inject(ActivatedRoute) + private dialog = inject(MatDialog) + + rooms!: string[] + room = '0' + grade = 6 + gradeDate?: DateTime + id?: string + filter = weekendFilter + date = signal(filterLook(this.filter, "behind", DateTime.now(), 7)!) + + get notes(): { label: string; weight: number }[] { + const th = this.things.value as { + cb: boolean + label: string + weight: number + }[] + return th + .filter(v => v.cb) + .map(v => { + return { ...v, cb: undefined } + }) + } + + set notes(value: { label: string; weight: number }[]) { + const things = this.things.controls + things.forEach(v => { + const thing = value.find(s => s.label == v.get('label')?.value) + if (thing) { + v.get('cb')?.setValue(true) + v.get('weight')?.setValue(thing.weight) + } else { + v.get('cb')?.setValue(false) + v.get('weight')?.setValue(1) + } + }) + } + + constructor() { + // if (!this.filter(this.date)) this.date.isoWeekday(8); + this.toolbar.comp = this + this.toolbar.menu = [ + { + title: 'Pokoje do sprawdzenia', + check: true, + fn: 'attendenceSummary', + icon: 'overview', + }, + { title: 'Podsumowanie', check: true, fn: 'summary', icon: 'analytics' }, + ] + this.form.valueChanges.subscribe(() => { + this.calculate() + }) + } + + form = this.fb.group({ + things: this.fb.array([]), + tips: this.fb.control(''), + }) + + get things() { + return this.form.get('things') as FormArray + } + + summary() { + this.router.navigate(['summary'], { relativeTo: this.route }) + } + + attendenceSummary() { + this.router.navigate(['attendenceSummary'], { relativeTo: this.route }) + } + + ngOnInit(): void { + this.ac.getConfig().subscribe(s => { + this.rooms = s.rooms + s.things.forEach(s => + this.things.push( + this.fb.group({ + cb: this.fb.control(false), + label: this.fb.control(s), + weight: this.fb.control(1), + }) + ) + ) + }) + } + + ngOnDestroy(): void { + this.toolbar.comp = undefined + this.toolbar.menu = undefined + } + + downloadData() { + this.ac.getClean(this.date(), this.room).subscribe(v => { + if (v) { + this.notes = v.notes + this.gradeDate = DateTime.fromISO(v.gradeDate) + this.grade = v.grade + this.id = v._id + this.form.get('tips')?.setValue(v.tips) + } else { + this.gradeDate = undefined + this.grade = 6 + this.notes = [] + this.id = undefined + this.form.get('tips')?.setValue('') + } + }) + } + + calculate() { + this.grade = 6 + this.things.controls.forEach(s => { + if (s.get('cb')?.value) this.grade -= 1 * s.get('weight')?.value + if (this.grade < 0) this.grade = 0 + }) + } + + group = { + add: (index: number) => { + const weight = this.things.at(index).get('weight')! + weight.setValue(weight.value + 1) + }, + sub: (index: number) => { + const weight = this.things.at(index).get('weight')! + if (weight.value < 1) { + weight.setValue(1) + } else { + if (weight.value - 1 < 1) { + weight.setValue(1) + } else { + weight.setValue(weight.value - 1) + } + } + }, + } + + save() { + this.calculate() + const obj = { + grade: this.grade, + date: this.date().toISODate(), + room: this.room, + notes: this.notes, + tips: this.form.get('tips')?.value, + } + this.ac.postClean(obj).subscribe(() => { + this.sb.open('Zapisano!', undefined, { duration: 1500 }) + this.downloadData() + }) + } + + remove() { + this.ac.delete(this.id!).subscribe(s => { + if (s.status == 200) { + this.downloadData() + } + }) + } + + roomNumber(value: string) { + this.room = value + this.downloadData() + } + + attendence() { + this.dialog + .open(AttendenceComponent, { data: { room: this.room } }) + .afterClosed() + .subscribe( + (v: { + room: string + users: { att: boolean; id: string; hour: string }[] + notes: string + }) => { + if (!v) return + const x: { room: string; users: { id: string; hour?: string }[] } = { + room: v.room, + users: [], + } + v.users.forEach(i => { + if (i.att && i.hour) { + x.users.push({ id: i.id, hour: i.hour }) + } + }) + this.ac.attendence + .postAttendence(x.room, { auto: x.users, notes: v.notes }) + .subscribe(s => { + if (s.status == 200) { + this.sb.open('Zapisano obecność!', undefined, { + duration: 1500, + }) + } + }) + } + ) + } +} diff --git a/frontend/src/app/admin-view/grades/grades.service.spec.ts b/frontend/src/app/admin-view/grades/grades.service.spec.ts new file mode 100644 index 0000000..fa7e6b2 --- /dev/null +++ b/frontend/src/app/admin-view/grades/grades.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { GradesService } from './grades.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('GradesService', () => { + let service: GradesService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(GradesService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/grades/grades.service.ts b/frontend/src/app/admin-view/grades/grades.service.ts new file mode 100644 index 0000000..d10ea42 --- /dev/null +++ b/frontend/src/app/admin-view/grades/grades.service.ts @@ -0,0 +1,92 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { DateTime } from 'luxon'; +import { Status } from 'src/app/types/status'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class GradesService { + private http = inject(HttpClient) + + getConfig() { + return this.http.get<{ rooms: string[]; things: string[] }>( + environment.apiEndpoint + `/admin/clean/config`, + { withCredentials: true } + ) + } + + getClean(date: DateTime, room: string) { + return this.http.get<{ + _id: string + date: string + grade: number + gradeDate: string + notes: { label: string; weight: number }[] + room: string + tips: string + } | null>(environment.apiEndpoint + `/admin/clean/${date.toISODate()}/${room}`, { + withCredentials: true, + }) + } + + postClean(obj: object) { + return this.http.post( + environment.apiEndpoint + `/admin/clean/`, + obj, + { withCredentials: true } + ) + } + + delete(id: string) { + return this.http.delete( + environment.apiEndpoint + `/admin/clean/${id}`, + { withCredentials: true } + ) + } + + summary = { + getSummary: (start: DateTime, end: DateTime) => { + return this.http.get<{ room: string; avg: number }[]>( + environment.apiEndpoint + + `/admin/clean/summary/${start.toISO()}/${end.toISO()}`, + { withCredentials: true } + ) + }, + } + + attendence = { + getUsers: (room: string) => { + return this.http.get<{ + users: { fname: string; surname: string; _id: string }[] + attendence?: { auto: { id: string; hour?: string }[]; notes: string } + }>(environment.apiEndpoint + `/admin/clean/attendence/${room}`, { + withCredentials: true, + }) + }, + postAttendence: ( + room: string, + attendence: { auto: { id: string; hour?: string }[]; notes: string } + ) => { + return this.http.post( + environment.apiEndpoint + `/admin/clean/attendence/${room}`, + attendence, + { withCredentials: true } + ) + }, + getSummary: () => { + return this.http.get< + { room: string; hours: string[]; notes: string; auto: boolean }[] + >(environment.apiEndpoint + `/admin/clean/attendenceSummary`, { + withCredentials: true, + }) + }, + deleteRoom: (room: string) => { + return this.http.delete( + environment.apiEndpoint + `/admin/clean/attendence/${room}`, + { withCredentials: true } + ) + }, + } +} diff --git a/frontend/src/app/admin-view/grades/summary/summary.component.html b/frontend/src/app/admin-view/grades/summary/summary.component.html new file mode 100644 index 0000000..d46592a --- /dev/null +++ b/frontend/src/app/admin-view/grades/summary/summary.component.html @@ -0,0 +1,25 @@ +
+ + Wybierz tydzień + + + + + + + + +
+ +
+
+ + +
+
+ + + + + +
Pokój{{item.room}}Średnia{{item.avg}}
\ No newline at end of file diff --git a/frontend/src/app/admin-view/grades/summary/summary.component.scss b/frontend/src/app/admin-view/grades/summary/summary.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/grades/summary/summary.component.spec.ts b/frontend/src/app/admin-view/grades/summary/summary.component.spec.ts new file mode 100644 index 0000000..79f099f --- /dev/null +++ b/frontend/src/app/admin-view/grades/summary/summary.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { SummaryComponent } from './summary.component' +import { RouterModule } from '@angular/router' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatDatepickerModule } from '@angular/material/datepicker' +import { MatIconModule } from '@angular/material/icon' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatTableModule } from '@angular/material/table' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter' +import { GradesService } from '../grades.service' + +xdescribe('SummaryComponent', () => { + let component: SummaryComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const acMock = {} + await TestBed.configureTestingModule({ + declarations: [SummaryComponent], + providers: [ + { provide: GradesService, useValue: acMock }, + provideLuxonDateAdapter(), + ], + imports: [ + RouterModule.forRoot([]), + MatFormFieldModule, + MatDatepickerModule, + MatIconModule, + FormsModule, + ReactiveFormsModule, + MatTableModule, + NoopAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(SummaryComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/grades/summary/summary.component.ts b/frontend/src/app/admin-view/grades/summary/summary.component.ts new file mode 100644 index 0000000..ae1c9d7 --- /dev/null +++ b/frontend/src/app/admin-view/grades/summary/summary.component.ts @@ -0,0 +1,68 @@ +import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { ToolbarService } from '../../toolbar/toolbar.service' +import { ActivatedRoute, Router } from '@angular/router' +import { MatTableDataSource } from '@angular/material/table' +import { FormBuilder } from '@angular/forms' +import { MatSort } from '@angular/material/sort' +import { DateTime } from 'luxon' +import { GradesService } from '../grades.service' + +@Component({ + selector: 'app-summary', + templateUrl: './summary.component.html', + styleUrl: './summary.component.scss', + standalone: false, +}) +export class SummaryComponent implements OnInit, OnDestroy { + private toolbar = inject(ToolbarService) + private router = inject(Router) + private route = inject(ActivatedRoute) + private ac = inject(GradesService) + private fb = inject(FormBuilder) + + data: MatTableDataSource<{ room: string; avg: number }> = + new MatTableDataSource<{ room: string; avg: number }>() + collumns = ['room', 'avg'] + + dateSelector = this.fb.group({ + start: this.fb.control(DateTime.utc().startOf('day')), + end: this.fb.control(DateTime.utc().endOf('day')), + }) + + @ViewChild(MatSort, { static: false }) set content(sort: MatSort) { + this.data.sort = sort + } + + constructor() { + this.toolbar.comp = this + this.toolbar.menu = [ + { check: true, title: 'Ocenianie', fn: 'goBack', icon: 'arrow_back' }, + ] + this.dateSelector.valueChanges.subscribe(() => { + this.download() + }) + } + ngOnInit(): void { + this.download() + } + + download() { + this.ac.summary + .getSummary( + this.dateSelector.get('start')!.value!.startOf('day')!, + this.dateSelector.get('end')!.value!.endOf('day')! + ) + .subscribe(v => { + this.data.data = v + }) + } + + goBack() { + this.router.navigate(['../'], { relativeTo: this.route }) + } + + ngOnDestroy(): void { + this.toolbar.comp = undefined + this.toolbar.menu = undefined + } +} diff --git a/frontend/src/app/admin-view/groups/groups.component.html b/frontend/src/app/admin-view/groups/groups.component.html new file mode 100644 index 0000000..6e71b9a --- /dev/null +++ b/frontend/src/app/admin-view/groups/groups.component.html @@ -0,0 +1,11 @@ + +@for (item of groups; track item) { + + + {{item.name}} + + + + + +} diff --git a/frontend/src/app/admin-view/groups/groups.component.scss b/frontend/src/app/admin-view/groups/groups.component.scss new file mode 100644 index 0000000..fc953ba --- /dev/null +++ b/frontend/src/app/admin-view/groups/groups.component.scss @@ -0,0 +1,12 @@ +:host { + display: flex; +} + +mat-card { + margin: 15px; + padding: 1ch; +} + +mat-card-title { + font-size: 1.5rem; +} diff --git a/frontend/src/app/admin-view/groups/groups.component.spec.ts b/frontend/src/app/admin-view/groups/groups.component.spec.ts new file mode 100644 index 0000000..1cfee1a --- /dev/null +++ b/frontend/src/app/admin-view/groups/groups.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { GroupsComponent } from './groups.component' +import { GroupsService } from './groups.service' + +xdescribe('GroupsComponent', () => { + let component: GroupsComponent + let fixture: ComponentFixture + + beforeEach(() => { + const acMock = {} + TestBed.configureTestingModule({ + declarations: [GroupsComponent], + providers: [{ provide: GroupsService, useValue: acMock }], + }) + fixture = TestBed.createComponent(GroupsComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/groups/groups.component.ts b/frontend/src/app/admin-view/groups/groups.component.ts new file mode 100644 index 0000000..2d7f96b --- /dev/null +++ b/frontend/src/app/admin-view/groups/groups.component.ts @@ -0,0 +1,69 @@ +import { Component, inject, OnInit } from '@angular/core' +import { Group } from 'src/app/types/group' +import { Status } from 'src/app/types/status' +import { MatDialog } from '@angular/material/dialog' +import { RemoveConfirmComponent } from './remove-confirm/remove-confirm.component' +import { GroupsService } from './groups.service' + +@Component({ + selector: 'app-groups', + templateUrl: './groups.component.html', + styleUrls: ['./groups.component.scss'], + standalone: false, +}) +export class GroupsComponent implements OnInit { + protected acs = inject(GroupsService) + private dialog = inject(MatDialog) + + groups?: Group[] + + ngOnInit(): void { + this.acs.getGroups().subscribe(v => { + this.groups = v + }) + } + + private refreshIfGood(s: Status) { + if (s.status.toString().match(/2\d\d/)) { + this.ngOnInit() + } + } + + get groupOptions(): { id: string; text: string }[] { + return this.groups!.map(v => { + return { id: v._id as string, text: v.name as string } + }) + } + + protected getId(g: Group[] | undefined) { + if (!g) return undefined + return g.map(v => v._id) + } + + groupNames(groups: Group[]) { + return groups.flatMap(g => g.name) + } + + protected nameEdit(id: string, name: string | string[]) { + name = name as string + this.acs.editName(id, name).subscribe(s => this.refreshIfGood(s)) + } + + protected newGroup() { + const name = prompt('Nazwa grupy') + if (name) { + this.acs.newGroup(name).subscribe(s => this.refreshIfGood(s)) + } + } + + protected remove(id: string) { + this.dialog + .open(RemoveConfirmComponent) + .afterClosed() + .subscribe(v => { + if (v) { + this.acs.remove(id).subscribe(s => this.refreshIfGood(s)) + } + }) + } +} diff --git a/frontend/src/app/admin-view/groups/groups.service.spec.ts b/frontend/src/app/admin-view/groups/groups.service.spec.ts new file mode 100644 index 0000000..df4c400 --- /dev/null +++ b/frontend/src/app/admin-view/groups/groups.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { GroupsService } from './groups.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('GroupsService', () => { + let service: GroupsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(GroupsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/groups/groups.service.ts b/frontend/src/app/admin-view/groups/groups.service.ts new file mode 100644 index 0000000..12d5aec --- /dev/null +++ b/frontend/src/app/admin-view/groups/groups.service.ts @@ -0,0 +1,45 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Group } from 'src/app/types/group'; +import { Status } from 'src/app/types/status'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class GroupsService { + private http = inject(HttpClient) + + getGroups() { + return this.http.get(environment.apiEndpoint + `/admin/groups`, { + withCredentials: true, + }) + } + + newGroup(name: string) { + return this.http.post( + environment.apiEndpoint + `/admin/groups`, + { name: name }, + { withCredentials: true } + ) + } + + editName(id: string, name: string) { + return this.putGroups(id, { name: name.trim() }) + } + + remove(id: string) { + return this.http.delete( + environment.apiEndpoint + `/admin/groups/${id}`, + { withCredentials: true } + ) + } + + private putGroups(id: string, update: Partial) { + return this.http.put( + environment.apiEndpoint + `/admin/groups/${id}`, + update, + { withCredentials: true } + ) + } +} diff --git a/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.html b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.html new file mode 100644 index 0000000..10a24af --- /dev/null +++ b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.html @@ -0,0 +1,8 @@ +

Czy chcesz usunąć tą grupę?

+ + Ta akcja jest nieodwracalna! + + + + + diff --git a/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.scss b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.spec.ts b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.spec.ts new file mode 100644 index 0000000..ed4e6b0 --- /dev/null +++ b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { RemoveConfirmComponent } from './remove-confirm.component' +import { MatDialogModule } from '@angular/material/dialog' + +describe('RemoveConfirmComponent', () => { + let component: RemoveConfirmComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [RemoveConfirmComponent], + imports: [MatDialogModule], + }) + fixture = TestBed.createComponent(RemoveConfirmComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.ts b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.ts new file mode 100644 index 0000000..e43ea51 --- /dev/null +++ b/frontend/src/app/admin-view/groups/remove-confirm/remove-confirm.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-remove-confirm', + templateUrl: './remove-confirm.component.html', + styleUrls: ['./remove-confirm.component.scss'], + standalone: false, +}) +export class RemoveConfirmComponent {} diff --git a/frontend/src/app/admin-view/key/key.component.html b/frontend/src/app/admin-view/key/key.component.html new file mode 100644 index 0000000..8559d7c --- /dev/null +++ b/frontend/src/app/admin-view/key/key.component.html @@ -0,0 +1,46 @@ +
+ + Wyszukaj + + + + Pokaż wszystko + + +
+@if (loading) { + +} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
Sala{{element.room}}WypożyczającyData wypożyczenia{{element.borrow.toFormat("HH:mm, ccc d.LL.")}}Data zwrotu + @if (element.tb) { + {{element.tb.toFormat("HH:mm, ccc d.LL.")}} + } + Akcje + @if (!element.tb) { + + } +
+ diff --git a/frontend/src/app/admin-view/key/key.component.scss b/frontend/src/app/admin-view/key/key.component.scss new file mode 100644 index 0000000..27a1492 --- /dev/null +++ b/frontend/src/app/admin-view/key/key.component.scss @@ -0,0 +1,5 @@ +#upper-bar { + display: flex; + align-items: baseline; + gap: 4pt; +} diff --git a/frontend/src/app/admin-view/key/key.component.spec.ts b/frontend/src/app/admin-view/key/key.component.spec.ts new file mode 100644 index 0000000..6d331bf --- /dev/null +++ b/frontend/src/app/admin-view/key/key.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AdminKeyComponent } from './key.component' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatChipsModule } from '@angular/material/chips' +import { MatIconModule } from '@angular/material/icon' +import { MatPaginatorModule } from '@angular/material/paginator' +import { FormsModule } from '@angular/forms' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { MatTableModule } from '@angular/material/table' +import { MatInputModule } from '@angular/material/input' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { KeyService } from './key.service' + +xdescribe('AdminKeyComponent', () => { + let component: AdminKeyComponent + let fixture: ComponentFixture + let acMock + + beforeEach(async () => { + acMock = {} + await TestBed.configureTestingModule({ + declarations: [AdminKeyComponent], + providers: [{ provide: KeyService, useValue: acMock }], + imports: [ + MatFormFieldModule, + MatChipsModule, + MatIconModule, + MatPaginatorModule, + FormsModule, + MatProgressSpinnerModule, + MatTableModule, + MatInputModule, + NoopAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(AdminKeyComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/key/key.component.ts b/frontend/src/app/admin-view/key/key.component.ts new file mode 100644 index 0000000..cf07d06 --- /dev/null +++ b/frontend/src/app/admin-view/key/key.component.ts @@ -0,0 +1,111 @@ +import { AfterViewInit, Component, inject, OnInit, ViewChild } from '@angular/core' +import { MatPaginator } from '@angular/material/paginator' +import { MatTableDataSource } from '@angular/material/table' +import { AKey } from 'src/app/types/key' +import { MatDialog } from '@angular/material/dialog' +import { NewKeyComponent } from './new-key/new-key.component' +import { catchError, throwError } from 'rxjs' +import { MatSnackBar } from '@angular/material/snack-bar' +import { KeyService } from './key.service' + +@Component({ + selector: 'app-admin-key', + templateUrl: './key.component.html', + styleUrl: './key.component.scss', + standalone: false, +}) +export class AdminKeyComponent implements AfterViewInit, OnInit { + private ac = inject(KeyService) + private dialog = inject(MatDialog) + private sb = inject(MatSnackBar) + + keys: MatTableDataSource = new MatTableDataSource() + pureData: AKey[] = [] + private _filters: string[] = [] + public get filters(): string[] { + return this._filters + } + public set filters(value: string[]) { + if (value.includes('showAll')) { + this.collumns = ['room', 'whom', 'borrow', 'tb', 'actions'] + } else { + this.collumns = ['room', 'whom', 'borrow', 'actions'] + } + this._filters = value + this.transformData() + } + + collumns = ['room', 'whom', 'borrow', 'tb', 'actions'] + + loading = true + @ViewChild(MatPaginator) paginator!: MatPaginator + + constructor( + ) { + this.filters = [] + } + + fetchData() { + this.loading = true + this.ac.getKeys().subscribe(r => { + this.loading = false + this.pureData = r + this.transformData() + }) + } + + transformData() { + let finalData: AKey[] = this.pureData + if (!this.filters.includes('showAll')) + finalData = finalData.filter(v => v.tb == undefined) + this.keys.data = finalData + } + + filter(event: Event) { + const value = (event.target as HTMLInputElement).value + this.keys.filter = value.toLowerCase().trim() + } + + ngAfterViewInit(): void { + this.keys.paginator = this.paginator + } + + ngOnInit(): void { + this.fetchData() + } + + new() { + this.dialog + .open(NewKeyComponent) + .afterClosed() + .subscribe(v => { + if (v) { + this.ac + .postKey(v.room, v.user) + .pipe( + catchError((err) => { + if (err.status == 404) { + this.sb.open('Nie znaleziono użytkownika', undefined, { + duration: 2500, + }) + } + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(s => { + if (s.status == 201) { + this.fetchData() + } + }) + } + }) + } + + tb(id: string) { + this.ac.returnKey(id).subscribe(r => { + if (r.status == 200) { + this.fetchData() + } + }) + } +} diff --git a/frontend/src/app/admin-view/key/key.service.spec.ts b/frontend/src/app/admin-view/key/key.service.spec.ts new file mode 100644 index 0000000..d1abc3f --- /dev/null +++ b/frontend/src/app/admin-view/key/key.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { KeyService } from './key.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('KeyService', () => { + let service: KeyService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(KeyService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/key/key.service.ts b/frontend/src/app/admin-view/key/key.service.ts new file mode 100644 index 0000000..cd8e38c --- /dev/null +++ b/frontend/src/app/admin-view/key/key.service.ts @@ -0,0 +1,55 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { DateTime } from 'luxon'; +import { map } from 'rxjs'; +import { AKey, AKeyAPI } from 'src/app/types/key'; +import { Status } from 'src/app/types/status'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class KeyService { + + private http = inject(HttpClient) + + getKeys() { + return this.http + .get(environment.apiEndpoint + `/admin/keys`, { withCredentials: true }) + .pipe( + map(v => v.map(r => ({ + ...r, + borrow: DateTime.fromISO(r.borrow!), + tb: r.tb ? DateTime.fromISO(r.tb!) : undefined + }))) + ) + } + + avalKeys() { + return this.http.get( + environment.apiEndpoint + `/admin/keys/available`, + { withCredentials: true } + ) + } + + postKey(room: string, uname: string) { + return this.http.post( + environment.apiEndpoint + `/admin/keys/`, + { room: room, whom: uname }, + { withCredentials: true } + ) + } + + returnKey(id: string) { + return this.putKeys(id, { tb: DateTime.now() }) + } + + private putKeys(id: string, update: Partial) { + return this.http.put( + environment.apiEndpoint + `/admin/keys/${id}`, + update, + { withCredentials: true } + ) + } + +} diff --git a/frontend/src/app/admin-view/key/new-key/new-key.component.html b/frontend/src/app/admin-view/key/new-key/new-key.component.html new file mode 100644 index 0000000..95b4569 --- /dev/null +++ b/frontend/src/app/admin-view/key/new-key/new-key.component.html @@ -0,0 +1,23 @@ + +
+ + Sala + + @for (item of rooms; track $index) { + {{item}} + } + + @if (form.controls['room'].hasError('required')) { + Wymagane + } + + + Wypożyczający + + @if (form.controls['user'].hasError('required')) { + Wymagane + } + + +
+
diff --git a/frontend/src/app/admin-view/key/new-key/new-key.component.scss b/frontend/src/app/admin-view/key/new-key/new-key.component.scss new file mode 100644 index 0000000..6807926 --- /dev/null +++ b/frontend/src/app/admin-view/key/new-key/new-key.component.scss @@ -0,0 +1,4 @@ +form { + display: flex; + flex-direction: column; +} diff --git a/frontend/src/app/admin-view/key/new-key/new-key.component.spec.ts b/frontend/src/app/admin-view/key/new-key/new-key.component.spec.ts new file mode 100644 index 0000000..ccb6761 --- /dev/null +++ b/frontend/src/app/admin-view/key/new-key/new-key.component.spec.ts @@ -0,0 +1,102 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { NewKeyComponent } from './new-key.component' +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { + MatFormFieldControl, + MatFormFieldModule, +} from '@angular/material/form-field' +import { MatSelectModule } from '@angular/material/select' +import { Component, forwardRef } from '@angular/core' +import { Observable, of } from 'rxjs' +import { + AbstractControlDirective, + ControlValueAccessor, + FormsModule, + NG_VALUE_ACCESSOR, + NgControl, + ReactiveFormsModule, +} from '@angular/forms' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { KeyService } from '../key.service' + +@Component({ + selector: 'app-user-search', + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => UserSearchStub), + multi: true, + }, + { + provide: MatFormFieldControl, + useExisting: UserSearchStub, + }, + ], + standalone: false, +}) +class UserSearchStub + implements ControlValueAccessor, MatFormFieldControl +{ + value = null + stateChanges: Observable = of() + id = '' + placeholder = '' + ngControl: NgControl | AbstractControlDirective | null = null + focused = false + empty = true + shouldLabelFloat = true + required = false + disabled = false + errorState = false + controlType?: string | undefined + autofilled?: boolean | undefined + userAriaDescribedBy?: string | undefined + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + setDescribedByIds(ids: string[]): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + onContainerClick(event: MouseEvent): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + writeValue(obj: unknown): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + registerOnChange(fn: unknown): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + registerOnTouched(fn: unknown): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + setDisabledState?(isDisabled: boolean): void {} +} + +xdescribe('NewKeyComponent', () => { + let component: NewKeyComponent + let fixture: ComponentFixture + let acMock + + beforeEach(async () => { + acMock = { + } + await TestBed.configureTestingModule({ + declarations: [NewKeyComponent, UserSearchStub], + providers: [ + { provide: KeyService, useValue: acMock }, + { provide: MatDialogRef, useValue: {} }, + ], + imports: [ + MatDialogModule, + MatFormFieldModule, + MatSelectModule, + FormsModule, + ReactiveFormsModule, + NoopAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(NewKeyComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/key/new-key/new-key.component.ts b/frontend/src/app/admin-view/key/new-key/new-key.component.ts new file mode 100644 index 0000000..117b00d --- /dev/null +++ b/frontend/src/app/admin-view/key/new-key/new-key.component.ts @@ -0,0 +1,32 @@ +import { Component, inject, OnInit } from '@angular/core' +import { MatDialogRef } from '@angular/material/dialog' +import { FormControl, FormGroup } from '@angular/forms' +import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component' +import { KeyService } from '../key.service' + +@Component({ + selector: 'app-new-key', + templateUrl: './new-key.component.html', + styleUrl: './new-key.component.scss', + standalone: false, +}) +export class NewKeyComponent implements OnInit { + private ac = inject(KeyService) + public dialogRef: MatDialogRef = inject(MatDialogRef) + + rooms: string[] = [] + form = new FormGroup({ + room: new FormControl(''), + user: new FormControl(null), + }) + + ngOnInit(): void { + this.ac.avalKeys().subscribe(v => { + this.rooms = v + }) + } + + send() { + if (this.form.valid) this.dialogRef.close(this.form.value) + } +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.html b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.html new file mode 100644 index 0000000..074c72f --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.html @@ -0,0 +1,33 @@ +

Tworzenie wpisów do jadłospisu

+ + + Dzień + Tydzień + Plik + +
+ @switch (type) { + @case ("day") { + + } + @case ("week") { + + Wybierz tydzień + + + + + + + + } + @case ("file") { + + } + } +
+
+ + + + diff --git a/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.scss b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.spec.ts new file mode 100644 index 0000000..0de82b8 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { MenuAddComponent } from './menu-add.component' +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog' +import { MatRadioModule } from '@angular/material/radio' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' + +describe('MenuAddComponent', () => { + let component: MenuAddComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MenuAddComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: {} }, + ], + imports: [ + MatDialogModule, + MatRadioModule, + ReactiveFormsModule, + FormsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(MenuAddComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.ts b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.ts new file mode 100644 index 0000000..4aa97b9 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-add/menu-add.component.ts @@ -0,0 +1,59 @@ +import { Component, inject, signal } from '@angular/core' +import { MenuUploadComponent } from '../menu-upload/menu-upload.component' +import { MatDialog, MatDialogRef } from '@angular/material/dialog' +import { FDSelection } from 'src/app/fd.da' +import { FormControl, FormGroup } from '@angular/forms' +import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker' +import { DateTime } from 'luxon' +import { weekendFilter } from 'src/app/util' + +@Component({ + selector: 'app-menu-add', + templateUrl: './menu-add.component.html', + styleUrl: './menu-add.component.scss', + providers: [ + { provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection }, + ], + standalone: false, +}) +export class MenuAddComponent { + public dialogRef: MatDialogRef = inject(MatDialogRef) + private dialog = inject(MatDialog) + + type: string | undefined + filter = weekendFilter + + day = signal(DateTime.now()) + + range = new FormGroup({ + start: new FormControl(null), + end: new FormControl(null), + }) + + submit() { + switch (this.type) { + case 'day': + this.dialogRef.close({ type: 'day', value: this.day() }) + break + case 'week': + this.dialogRef.close({ + type: 'week', + value: { start: this.range.value.start, count: 5 }, + }) + break + default: + break + } + } + + activateUpload() { + this.dialog + .open(MenuUploadComponent) + .afterClosed() + .subscribe(data => { + if (data) { + this.dialogRef.close({ type: 'file', ...data }) + } + }) + } +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.component.html b/frontend/src/app/admin-view/menu-edit/menu-edit.component.html new file mode 100644 index 0000000..2e9949a --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.component.html @@ -0,0 +1,147 @@ +
+ + Wybierz tydzień + + + + + + + + + + +
+ +
+ @switch (ac.state()) { + @case (0) { +

Wybierz zakres dat powyżej i kliknij szukaj

+ } + @case (1) { +
+ +
+ } + @case (2) { + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
Dzień + {{element.day.toFormat('dd.LL.yyyy')}}r. +

{{element.day.toFormat('cccc') | titlecase }}

+

+ +
Śniadanie +
    + @for (i of ls.defaultItems.sn; track i) { +
  • {{i}}
  • + } +

+
+
    +
  • +
+
Obiad +
    +
  • +
  • +
  • +

+
+
    +
  • +

+ +
Kolacja +
+ @switch (element.day.weekday) { + @default { +
+
    + @for (i of ls.defaultItems.kol; track i) { +
  • {{i}}
  • + } +

+
    +
  • +
+
+ } + @case (5) { +
+

Kolacja w domu!

+

(Nie edytowalne)

+
+ } + } +
+
+ } + } +
+ +@if (options) { + + @for (i of options.sn.fancy; track i) { + + } + + + @for (i of options.sn.second; track i) { + + } + + + @for (i of options.ob.soup; track i) { + + } + + + @for (i of options.ob.vege; track i) { + + } + + + @for (i of options.ob.meal; track i) { + + } + + + @for (i of options.ob.condiments; track i) { + + } + + + @for (i of options.ob.drink; track i) { + + } + + + @for (i of options.ob.other; track i) { + + } + + + @for (i of options.kol; track i) { + + } + +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.component.scss b/frontend/src/app/admin-view/menu-edit/menu-edit.component.scss new file mode 100644 index 0000000..a4b6743 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.component.scss @@ -0,0 +1,34 @@ +#upper-bar { + display: flex; +} + +mat-form-field { + flex-grow: 1; +} + +button[mat-icon-button] { + margin-left: 4pt; + margin-right: 4pt; + margin-top: 4pt; +} + +.non-editable { + color: gray; + font-style: italic; +} + +.mainc { + height: 100%; + position: relative; +} + +.spinner { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.component.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-edit.component.spec.ts new file mode 100644 index 0000000..e9f4100 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { MenuEditComponent } from './menu-edit.component' +import { MatTableModule } from '@angular/material/table' +import { MatInputModule } from '@angular/material/input' +import { + MAT_DATE_RANGE_SELECTION_STRATEGY, + MatDatepickerModule, +} from '@angular/material/datepicker' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { FDSelection } from 'src/app/fd.da' +import { ReactiveFormsModule } from '@angular/forms' +import { MatDialogModule } from '@angular/material/dialog' +import { MatIconModule } from '@angular/material/icon' +import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter' +import { MenuEditService } from './menu-edit.service' + +xdescribe('MenuEditComponent', () => { + let component: MenuEditComponent + let fixture: ComponentFixture + + beforeEach(() => { + const acMock = jasmine.createSpyObj('MenuEditService', {}) + TestBed.configureTestingModule({ + declarations: [MenuEditComponent], + imports: [ + MatTableModule, + MatInputModule, + MatDatepickerModule, + BrowserAnimationsModule, + ReactiveFormsModule, + MatDialogModule, + MatIconModule, + ], + providers: [ + provideLuxonDateAdapter(), + { provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection }, + { provide: MenuEditService, useValue: acMock }, + ], + }) + fixture = TestBed.createComponent(MenuEditComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.component.ts b/frontend/src/app/admin-view/menu-edit/menu-edit.component.ts new file mode 100644 index 0000000..1bfe07c --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.component.ts @@ -0,0 +1,160 @@ +import { Component, inject, OnDestroy } from '@angular/core' +import { FormControl, FormGroup } from '@angular/forms' +import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker' +import { FDSelection } from 'src/app/fd.da' +import { Menu } from 'src/app/types/menu' +import { MatTableDataSource } from '@angular/material/table' +import { MatDialog } from '@angular/material/dialog' +import { MenuUploadComponent } from './menu-upload/menu-upload.component' +import { Status } from 'src/app/types/status' +import { MatSnackBar } from '@angular/material/snack-bar' +import { MenuAddComponent } from './menu-add/menu-add.component' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { DateTime } from 'luxon' +import { MenuEditService } from './menu-edit.service' +import { MenuOptions } from './menu-edit.model' +import { ToolbarService } from '../toolbar/toolbar.service' +import { ActivatedRoute, Router } from '@angular/router' + +@Component({ + selector: 'app-menu-edit', + templateUrl: './menu-edit.component.html', + styleUrls: ['./menu-edit.component.scss'], + providers: [ + { provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection }, + ], + standalone: false, +}) +export class MenuEditComponent implements OnDestroy { + protected ac = inject(MenuEditService) + private dialog = inject(MatDialog) + private sb = inject(MatSnackBar) + private tb = inject(ToolbarService) + private router = inject(Router) + private route = inject(ActivatedRoute) + readonly ls = inject(LocalStorageService) + + dcols: string[] = ['day', 'sn', 'ob', 'kol'] + dataSource: MatTableDataSource = new MatTableDataSource() + range = new FormGroup({ + start: new FormControl(null), + end: new FormControl(null), + }) + public options?: MenuOptions + + constructor() { + this.range.setValue(this.ac.seDates()) + this.range.valueChanges.subscribe(v => { + this.ac.seDates.set({start: v.start!, end: v.end!}) + }) + this.ac.menuItems.subscribe(v => { + this.dataSource.data = v + }) + this.refresh() + this.tb.comp = this + this.tb.menu = [ + { title: "Badanie opinii", icon: "analytics", fn: "statistics"} + ] + } + + ngOnDestroy(): void { + this.tb.comp = undefined + this.tb.menu = undefined + } + + statistics() { + this.router.navigate(['stats'], { relativeTo: this.route }) + } + + print() { + this.ac + .print(this.range.value.start, this.range.value.end) + ?.subscribe(r => { + if (r && r.length > 0) { + const mywindow = window.open( + undefined, + 'Drukowanie', + 'height=400,width=400' + ) + mywindow?.document.write(r) + mywindow?.print() + mywindow?.close() + } + }) + } + + addDate() { + this.dialog + .open(MenuAddComponent) + .afterClosed() + .subscribe(data => { + if (data) { + switch (data.type) { + case 'day': + this.ac.new + .single(data.value) + .subscribe(s => this.refreshIfGood(s)) + break + case 'week': + this.ac.new + .range(data.value.start, data.value.count) + .subscribe(s => this.refreshIfGood(s)) + break + case 'file': + this.refresh() + break + default: + break + } + } + }) + } + + refresh() { + this.ac.refresh() + this.ac.getOpts().subscribe(o => { + this.options = o + }) + } + + private refreshIfGood(s: Status) { + if (s.status.toString().match(/2\d\d/)) this.refresh() + } + + activateUpload() { + this.dialog + .open(MenuUploadComponent) + .afterClosed() + .subscribe(data => { + if (data) this.refresh() + }) + } + + editSn(id: string) { + this.ac + .editSn(id, this.dataSource.data.find(v => v._id == id)!.sn) + .subscribe(s => this.refreshIfGood(s)) + } + + editOb(id: string) { + this.ac + .editOb(id, this.dataSource.data.find(v => v._id == id)!.ob) + .subscribe(s => this.refreshIfGood(s)) + } + + editKol(id: string) { + this.ac + .editKol(id, this.dataSource.data.find(v => v._id == id)?.kol) + .subscribe(s => this.refreshIfGood(s)) + } + + editTitle(id: string) { + this.ac + .editTitle(id, this.dataSource.data.find(v => v._id == id)?.dayTitle) + .subscribe(s => this.refreshIfGood(s)) + } + + remove(id: string) { + this.ac.rm(id).subscribe(s => this.refreshIfGood(s)) + } +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.model.ts b/frontend/src/app/admin-view/menu-edit/menu-edit.model.ts new file mode 100644 index 0000000..01c9472 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.model.ts @@ -0,0 +1,15 @@ +export interface MenuOptions { + sn: { + fancy: string[] + second: string[] + } + ob: { + soup: string[] + vege: string[] + meal: string[] + condiments: string[] + drink: string[] + other: string[] + } + kol: string[] +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.service.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-edit.service.spec.ts new file mode 100644 index 0000000..ee92a1f --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { MenuEditService } from './menu-edit.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('MenuEditService', () => { + let service: MenuEditService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(MenuEditService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/menu-edit/menu-edit.service.ts b/frontend/src/app/admin-view/menu-edit/menu-edit.service.ts new file mode 100644 index 0000000..feb72df --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-edit.service.ts @@ -0,0 +1,145 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { DateTime } from 'luxon'; +import { BehaviorSubject, catchError, map, of } from 'rxjs'; +import { Menu, MenuAPI } from 'src/app/types/menu'; +import { STATE } from 'src/app/types/state'; +import { Status } from 'src/app/types/status'; +import { environment } from 'src/environments/environment'; +import { MenuOptions } from './menu-edit.model'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuEditService { + private http = inject(HttpClient) + + private _menuItems = new BehaviorSubject([]) + public readonly menuItems = this._menuItems.asObservable() + private _state = signal(STATE.NOT_LOADED); + public readonly state = this._state.asReadonly(); + private _error = signal(undefined); + public readonly error = this._error.asReadonly(); + + seDates = signal<{ + start: DateTime | null, + end: DateTime | null + }>({ + start: null, + end: null + }) + + public refresh() { + this.getMenu() + } + + private getMenu() { + if (!(this.seDates().start && this.seDates().end)) return + this._state.set(STATE.PENDING) + const body = { start: this.seDates().start!.toString(), end: this.seDates().end!.toString() } + this.http.get + + (environment.apiEndpoint + `/admin/menu`, { withCredentials: true, params: body }) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }), + map(v => + v.map(i => ({ + ...i, + day: DateTime.fromISO(i.day) + }))) + ).subscribe(v => { + this._error.set(undefined) + this._menuItems.next(v ?? []) + this._state.set(STATE.LOADED) + }) + } + + getOpts() { + return this.http.get(environment.apiEndpoint + `/admin/menu/opts`, { + withCredentials: true, + }) + } + + postMenu(file: File) { + if (file) { + const formData = new FormData() + formData.append('menu', file) + return this.http.post( + environment.apiEndpoint + '/admin/menu/upload', + formData, + { withCredentials: true } + ) + } + return + } + + editSn(id: string, content: Menu['sn']) { + return this.putMenu(id, { sn: content }) + } + + editOb(id: string, content: Menu['ob']) { + return this.putMenu(id, { ob: content }) + } + + editKol(id: string, content: Menu['kol']) { + return this.putMenu(id, { kol: content }) + } + + editTitle(id: string, content: Menu['dayTitle']) { + return this.putMenu(id, { dayTitle: content }) + } + + print(start?: DateTime | null, end?: DateTime | null) { + if (start && end) { + const body = { start: start.toString(), end: end.toString() } + return this.http.get(environment.apiEndpoint + '/admin/menu/print', { + withCredentials: true, + params: body, + responseType: 'text', + }) + } + return + } + + stat(day: DateTime, m: 'ob' | 'kol') { + return this.http.get<{ y: number; n: number }>( + environment.apiEndpoint + `/admin/menu/${day.toISO()}/votes/${m}`, + { withCredentials: true } + ) + } + + new = { + single: (day: DateTime) => { + return this.http.post( + environment.apiEndpoint + `/admin/menu/${day.toISO()}`, + null, + { withCredentials: true } + ) + }, + range: (start: DateTime, count: number) => { + return this.http.post( + environment.apiEndpoint + `/admin/menu/${start.toISO()}/${count}/`, + null, + { withCredentials: true } + ) + }, + } + rm(id: string) { + return this.http.delete( + environment.apiEndpoint + `/admin/menu/${id}`, + { withCredentials: true } + ) + } + + private putMenu(id: string, update: Partial) { + return this.http.put( + environment.apiEndpoint + `/admin/menu/${id}`, + update, + { withCredentials: true } + ) + } +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.html b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.html new file mode 100644 index 0000000..9b53376 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.html @@ -0,0 +1,45 @@ +

+ {{ data.date.toFormat('dd.LL.yyyy') }}
+ {{ data.item }} +

+ + + @switch (msi.state()) { + @case (1) { +
+ +
+ } + @case (2) { +

Ilość opinii: {{ count }}

+

Ocena:

+ +

Komentarze:

+ @for (item of comments; track $index) { + + + {{item}} + + + } + @empty { +

Brak

+ } +

Tagi:

+ + @for (tag of tags | keyvalue; track tag.key) { + + {{tag.key}} — {{tag.value}} + + } + + } + @case (3) { +

Wystąpił błąd podczas ładowania statystyk: {{msi.error()}}

+ } + } +
+ + + + diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.scss b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.scss new file mode 100644 index 0000000..868df33 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.scss @@ -0,0 +1,12 @@ +h2 { + text-align: center; +} + +.gray { + color: gray; + font-style: italic; +} + +mat-card { + margin: 5px; +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.spec.ts new file mode 100644 index 0000000..5888231 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MenuItemStatsDialogComponent } from './menu-item-stats-dialog.component'; + +describe('MenuItemStatsDialogComponent', () => { + let component: MenuItemStatsDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MenuItemStatsDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MenuItemStatsDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.ts b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.ts new file mode 100644 index 0000000..bf203c2 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component.ts @@ -0,0 +1,34 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { DateTime } from 'luxon'; +import { MenuItemStatsService } from './menu-item-stats.service'; + +@Component({ + selector: 'app-menu-item-stats-dialog', + standalone: false, + templateUrl: './menu-item-stats-dialog.component.html', + styleUrl: './menu-item-stats-dialog.component.scss' +}) +export class MenuItemStatsDialogComponent implements OnInit { + public data: { date: DateTime; item: string } = inject(MAT_DIALOG_DATA) + protected msi = inject(MenuItemStatsService) + + protected rating = 0; + protected comments: string[] = []; + protected tags: Record = {} + protected count = 0; + + ngOnInit(): void { + this.msi.date.set(this.data.date) + this.msi.item.set(this.data.item) + this.msi.refresh() + + this.msi.menuItems.subscribe(v => { + this.rating = v.value.rating ?? 0 + this.comments = v.value.comments ?? [] + this.tags = v.value.tags ?? {} + this.count = v.value.count ?? 0 + }) + } + +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats.service.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats.service.spec.ts new file mode 100644 index 0000000..f37561a --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MenuItemStatsService } from './menu-item-stats.service'; + +describe('MenuItemStatsService', () => { + let service: MenuItemStatsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MenuItemStatsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats.service.ts b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats.service.ts new file mode 100644 index 0000000..8476017 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats.service.ts @@ -0,0 +1,57 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { DateTime } from 'luxon'; +import { BehaviorSubject, catchError, of } from 'rxjs'; +import { MenuStats } from 'src/app/types/menu-stats'; +import { STATE } from 'src/app/types/state'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuItemStatsService { + protected http = inject(HttpClient) + + private _menuItems = new BehaviorSubject({ + name: '', + type: 'other', + value: { + count: 0, + comments: [], + rating: 0, + tags: {} + } + }); + public readonly menuItems = this._menuItems.asObservable() + private _state = signal(STATE.NOT_LOADED); + public readonly state = this._state.asReadonly(); + private _error = signal(undefined); + public readonly error = this._error.asReadonly(); + + public date = signal(null) + public item = signal('') + + public refresh() { + this.getStats() + } + + private getStats() { + if (!(this.date() && this.item())) return + this._state.set(STATE.PENDING) + const body = { date: this.date()!.toString(), item: this.item() } + this.http.get + + (environment.apiEndpoint + `/admin/menu/editor/stats`, { withCredentials: true, params: body }) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }) + ).subscribe(v => { + this._error.set(undefined) + this._menuItems.next(v) + this._state.set(STATE.LOADED) + }) + } +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.html b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.html new file mode 100644 index 0000000..3e1acb8 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.html @@ -0,0 +1,121 @@ +
+ + Wybierz tydzień + + + + + + + + +
+ +
+ @switch (ss.state()) { + @case (0) { +

Wybierz zakres dat powyżej i kliknij szukaj

+ } + @case (1) { +
+ +
+ } + @case (2) { + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
Dzień + {{element.day.toFormat('dd.LL.yyyy')}}r. +

{{element.day.toFormat('cccc') | titlecase }}

+

+ {{element.dayTitle}} +

+
Śniadanie +
    + @for (i of ls.defaultItems.sn; track i) { +
  • {{i}}
  • + } +
+
    + @for (i of element.sn.fancy; track $index) { +
  • + } +
+ @if (element.sn.second) { +
    +
  • {{element.sn.second}}
  • +
+ } +
Obiad +
    + @if (element.ob.soup) { +
  • + } + @if (element.ob.vege) { +
  • + } + @if (element.ob.meal) { +
  • + } +
+
    + @for (i of element.ob.condiments; track $index) { +
  • + } +
+
    + @if (element.ob.drink) { +
  • + } +
+
    + @for (i of element.ob.other; track $index) { +
  • + } +
+
Kolacja +
+ @switch (element.day.weekday) { + @default { +
+
    + @for (i of ls.defaultItems.kol; track i) { +
  • {{i}}
  • + } +
+
    + @if (element.kol) { +
  • + } +
+
+ } + @case (5) { +
+

Kolacja w domu!

+
+ } + } +
+
+ } + } +
diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.scss b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.scss new file mode 100644 index 0000000..0349c5d --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.scss @@ -0,0 +1,23 @@ +#upper-bar { + display: flex; +} + +mat-form-field { + flex-grow: 1; +} + +button[mat-icon-button] { + margin-left: 4pt; + margin-right: 4pt; + margin-top: 4pt; +} + +.mainc { + height: 100%; + position: relative; +} + +.non-editable { + color: gray; + font-style: italic; +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.spec.ts new file mode 100644 index 0000000..3f3f223 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MenuStatsComponent } from './menu-stats.component'; + +describe('MenuStatsComponent', () => { + let component: MenuStatsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MenuStatsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MenuStatsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.ts b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.ts new file mode 100644 index 0000000..ff6cce8 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-stats/menu-stats.component.ts @@ -0,0 +1,71 @@ +import { Component, inject, OnDestroy } from '@angular/core'; +import { MenuEditService } from '../menu-edit.service'; +import { FormControl, FormGroup } from '@angular/forms'; +import { DateTime } from 'luxon'; +import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker'; +import { FDSelection } from 'src/app/fd.da'; +import { MatTableDataSource } from '@angular/material/table'; +import { Menu } from 'src/app/types/menu'; +import { LocalStorageService } from 'src/app/services/local-storage.service'; +import { MatDialog } from '@angular/material/dialog'; +import { MenuItemStatsDialogComponent } from './menu-item-stats-dialog/menu-item-stats-dialog.component'; +import { ToolbarService } from '../../toolbar/toolbar.service'; +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + selector: 'app-menu-stats', + standalone: false, + templateUrl: './menu-stats.component.html', + styleUrl: './menu-stats.component.scss', + providers: [ + { provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection }, + ], +}) +export class MenuStatsComponent implements OnDestroy { + protected ss = inject(MenuEditService) + protected ls = inject(LocalStorageService) + protected tb = inject(ToolbarService) + protected dialog = inject(MatDialog) + protected router = inject(Router) + protected route = inject(ActivatedRoute) + + dcols: string[] = ['day', 'sn', 'ob', 'kol'] + + range = new FormGroup({ + start: new FormControl(null), + end: new FormControl(null), + }) + dataSource: MatTableDataSource = new MatTableDataSource() + + constructor() { + this.range.setValue(this.ss.seDates()) + this.range.valueChanges.subscribe(v => { + this.ss.seDates.set({ start: v.start!, end: v.end! }) + }) + this.ss.menuItems.subscribe(v => { + this.dataSource.data = v + }) + this.tb.comp = this + this.tb.menu = [ + { + fn: "goBack", + title: "Edytowanie jadłospisu", + icon: "arrow_back" + } + ] + } + + ngOnDestroy(): void { + this.tb.comp = undefined + this.tb.menu = undefined + } + + openDialog(date: DateTime, item: string) { + this.dialog.open(MenuItemStatsDialogComponent, { + data: { date, item } + }) + } + goBack() { + this.router.navigate(['../'], { relativeTo: this.route }) + } +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.html b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.html new file mode 100644 index 0000000..a75b627 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.html @@ -0,0 +1,20 @@ +

Import z pliku

+ + + +
+

UWAGA!

+

Przed wysłaniem upewnij się że

+
    +
  • Daty w pliku są prawidłowe i poprawnie sformatowane (DD.MM.RRRR)
  • +
  • Wszystkie pozycje w menu są w osobnych linijkach
  • +
  • Załączony dokument to arkusz w formacie XLSX lub ODS
  • +
+

Nie spełnienie któregokolwiek z tych wymagań może skutkować szkodami w programie!

+

Późniejsza modyfikacja danych jest niemożliwa w tej wersji programu.

+
+
+ + + + diff --git a/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.scss b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.scss new file mode 100644 index 0000000..e6f808d --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.scss @@ -0,0 +1,4 @@ +:host { + margin: 8pt; + display: block; +} diff --git a/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.spec.ts b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.spec.ts new file mode 100644 index 0000000..679501f --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { MenuUploadComponent } from './menu-upload.component' +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MenuEditService } from '../menu-edit.service' + +xdescribe('MenuUploadComponent', () => { + let component: MenuUploadComponent + let fixture: ComponentFixture + + beforeEach(() => { + const acMock = jasmine.createSpyObj('AdminCommService') + TestBed.configureTestingModule({ + declarations: [MenuUploadComponent], + providers: [ + { provide: MenuEditService, useValue: acMock }, + { provide: MatDialogRef, useValue: {} }, + ], + imports: [MatDialogModule], + }) + fixture = TestBed.createComponent(MenuUploadComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.ts b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.ts new file mode 100644 index 0000000..64216c1 --- /dev/null +++ b/frontend/src/app/admin-view/menu-edit/menu-upload/menu-upload.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core' +import { MatDialogRef } from '@angular/material/dialog' +import { MenuEditService } from '../menu-edit.service' + +@Component({ + selector: 'app-upload-edit', + templateUrl: './menu-upload.component.html', + styleUrls: ['./menu-upload.component.scss'], + standalone: false, +}) +export class MenuUploadComponent { + private ac = inject(MenuEditService) + public dialogRef: MatDialogRef = inject(MatDialogRef) + + protected file: File | undefined + onFileChange(event: Event) { + const file: File = (event.target as HTMLInputElement).files![0] + if (file) { + this.file = file + } else { + this.file = undefined + } + } + + submit() { + this.ac.postMenu(this.file!)?.subscribe(value => { + this.dialogRef.close(value) + }) + } +} diff --git a/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.html b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.html new file mode 100644 index 0000000..c6a90c5 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.html @@ -0,0 +1,11 @@ +
+ + Tytuł + + + + Treść + + + +
\ No newline at end of file diff --git a/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.scss b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.scss new file mode 100644 index 0000000..4631ede --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.scss @@ -0,0 +1,10 @@ +:host { + padding: 8pt; + display: block; +} + +form { + display: flex; + flex-direction: column; + align-items: stretch; +} diff --git a/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.spec.ts b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.spec.ts new file mode 100644 index 0000000..a775c5e --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { NewPostComponent } from './edit-post.component' +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import { ReactiveFormsModule } from '@angular/forms' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' + +describe('NewPostComponent', () => { + let component: NewPostComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [NewPostComponent], + imports: [ + MatDialogModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + BrowserAnimationsModule, + ], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }) + fixture = TestBed.createComponent(NewPostComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.ts b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.ts new file mode 100644 index 0000000..87cd206 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/new-post/edit-post.component.ts @@ -0,0 +1,35 @@ +import { Component, inject } from '@angular/core' +import { FormControl, FormGroup } from '@angular/forms' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { News } from 'src/app/types/news.model' + +@Component({ + selector: 'app-edit-post', + templateUrl: './edit-post.component.html', + styleUrls: ['./edit-post.component.scss'], + standalone: false, +}) +export class NewPostComponent { + public dialogRef: MatDialogRef = inject(MatDialogRef) + public data: Partial = inject(MAT_DIALOG_DATA) + form: FormGroup + constructor() { + if (this.data == null) { + this.data = { + title: '', + content: '', + } + } + this.form = new FormGroup({ + title: new FormControl(this.data.title), + content: new FormControl(this.data.content), + }) + } + + protected makePost() { + this.dialogRef.close({ + title: this.form.get('title')?.value, + content: this.form.get('content')?.value, + }) + } +} diff --git a/frontend/src/app/admin-view/news-edit/news-edit.component.html b/frontend/src/app/admin-view/news-edit/news-edit.component.html new file mode 100644 index 0000000..2d8c758 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.component.html @@ -0,0 +1,43 @@ + +
+ @if (ac.state() !== 2) { + + } + @for (item of ac.news | async; track item) { + + + {{item.title}} + @if (item.pinned) { + push_pin + } + + + + + + @switch (item.visible) { + @case (true) { + + } + @default { + + } + } + + + +

{{fullName(item)}} {{item.date | date:'d-LL-yyyy HH:mm'}}

+
+
+ } @empty { + +

+ Brak wiadomości. +

+
+ } +
diff --git a/frontend/src/app/admin-view/news-edit/news-edit.component.scss b/frontend/src/app/admin-view/news-edit/news-edit.component.scss new file mode 100644 index 0000000..706ab5e --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.component.scss @@ -0,0 +1,48 @@ +mat-card { + margin: 15px; + padding: 1ch; +} + +mat-card-title { + font-size: 1.5rem; +} + +mat-card-footer p { + font-size: 0.8rem; + color: #4a4a4a; + margin-bottom: 0; + text-align: end; + @media (prefers-color-scheme: dark) { + color: #999999; + } +} + +mat-card-content p { + white-space: pre-line; +} + +mat-card p { + margin: 15px; +} + +button { + margin-right: 4pt; +} + +:host { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + width: 100%; +} + +.mainc { + position: relative; + height: 100%; + width: 100%; +} + +.newPost { + margin: 1ch; +} diff --git a/frontend/src/app/admin-view/news-edit/news-edit.component.spec.ts b/frontend/src/app/admin-view/news-edit/news-edit.component.spec.ts new file mode 100644 index 0000000..faf3836 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { NewsEditComponent } from './news-edit.component' +import { MatDialogModule } from '@angular/material/dialog' +import { MatSnackBarModule } from '@angular/material/snack-bar' +import { MatCardModule } from '@angular/material/card' +import { NewsEditService } from './news-edit.service' + +xdescribe('NewsEditComponent', () => { + let component: NewsEditComponent + let fixture: ComponentFixture + let acMock + + beforeEach(() => { + acMock = {} + TestBed.configureTestingModule({ + declarations: [NewsEditComponent], + providers: [{ provide: NewsEditService, useValue: acMock }], + imports: [MatDialogModule, MatSnackBarModule, MatCardModule], + }) + fixture = TestBed.createComponent(NewsEditComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/news-edit/news-edit.component.ts b/frontend/src/app/admin-view/news-edit/news-edit.component.ts new file mode 100644 index 0000000..aed5275 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.component.ts @@ -0,0 +1,123 @@ +import { Component, inject, OnInit } from '@angular/core' +import { MatDialog } from '@angular/material/dialog' +import { NewPostComponent } from './new-post/edit-post.component' +import { catchError, throwError } from 'rxjs' +import { MatSnackBar } from '@angular/material/snack-bar' +import { News } from 'src/app/types/news.model' +import { NewsEditService } from './news-edit.service' + +@Component({ + selector: 'app-news-edit', + templateUrl: './news-edit.component.html', + styleUrls: ['./news-edit.component.scss'], + standalone: false, +}) +export class NewsEditComponent implements OnInit { + protected ac = inject(NewsEditService) + private dialog = inject(MatDialog) + private sb = inject(MatSnackBar) + + ngOnInit() { + this.ac.refresh() + } + + newPost() { + this.dialog + .open(NewPostComponent, { width: '90vw' }) + .afterClosed() + .subscribe(result => { + if (result == undefined) return + this.ac + .postNews(result.title, result.content) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + if (data.status == 201) { + this.ngOnInit() + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + } + }) + }) + } + + editPost(item: News) { + this.dialog + .open(NewPostComponent, { data: item, width: '90vh' }) + .afterClosed() + .subscribe(result => { + if (result == undefined) return + this.ac + .updateNews(item._id, result.title, result.content) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + if (data.status == 200) { + this.ngOnInit() + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + } + }) + }) + } + + delete(id: string) { + this.ac.deleteNews(id).subscribe(data => { + if (data.status == 200) { + this.ngOnInit() + } + }) + } + + visibleToggle(item: News) { + this.ac + .toggleNews(item._id, !!item.visible) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + if (data.status == 200) { + this.ngOnInit() + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + } + }) + } + + pinToggle(item: News) { + this.ac + .togglePin(item._id, !!item.pinned) + .pipe( + catchError(err => { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + if (data.status == 200) { + this.ngOnInit() + } else { + this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.') + } + }) + } + + fullName(n: News): string { + const { author: { fname, surname, uname } } = n; + if (fname || surname) { + return [fname, surname].filter(Boolean).join(' '); + } + return uname; + } +} diff --git a/frontend/src/app/admin-view/news-edit/news-edit.model.ts b/frontend/src/app/admin-view/news-edit/news-edit.model.ts new file mode 100644 index 0000000..cf5ed14 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.model.ts @@ -0,0 +1,5 @@ +import { News } from "src/app/types/news.model"; + +export interface NewsFormatted extends News { + formatted: string +} diff --git a/frontend/src/app/admin-view/news-edit/news-edit.service.spec.ts b/frontend/src/app/admin-view/news-edit/news-edit.service.spec.ts new file mode 100644 index 0000000..06921bd --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { NewsEditService } from './news-edit.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('NewsEditService', () => { + let service: NewsEditService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(NewsEditService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/news-edit/news-edit.service.ts b/frontend/src/app/admin-view/news-edit/news-edit.service.ts new file mode 100644 index 0000000..f0cc917 --- /dev/null +++ b/frontend/src/app/admin-view/news-edit/news-edit.service.ts @@ -0,0 +1,91 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { marked } from 'marked'; +import { BehaviorSubject, catchError, map, of } from 'rxjs'; +import { News } from 'src/app/types/news.model'; +import { STATE } from 'src/app/types/state'; +import { Status } from 'src/app/types/status'; +import { environment } from 'src/environments/environment'; +import { NewsFormatted } from './news-edit.model'; + +@Injectable({ + providedIn: 'root' +}) +export class NewsEditService { + private http = inject(HttpClient) + + private _news = new BehaviorSubject([]) + public readonly news = this._news.asObservable() + private _state = signal(STATE.NOT_LOADED); + public readonly state = this._state.asReadonly(); + private _error = signal(undefined); + public readonly error = this._error.asReadonly(); + + public refresh() { + this.getNews() + } + + private getNews() { + this._state.set(STATE.PENDING) + this.http.get + + (environment.apiEndpoint + `/admin/news`, { withCredentials: true, }) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }), + map(i => { + return i.map(v => ({ + ...v, + formatted: marked.parse(v.content, { breaks: true }).toString() + })) + }) + ).subscribe(v => { + this._error.set(undefined) + this._news.next(v ?? []) + this._state.set(STATE.LOADED) + }) + } + + postNews(title: string, content: string) { + return this.http.post( + environment.apiEndpoint + `/admin/news`, + { title: title, content: content }, + { withCredentials: true } + ) + } + + deleteNews(id: string) { + return this.http.delete( + environment.apiEndpoint + `/admin/news/${id}`, + { withCredentials: true } + ) + } + + toggleNews(id: string, inverter: boolean) { + return this.putNews(id, { visible: !inverter }) + } + + togglePin(id: string, inverter: boolean) { + return this.putNews(id, { pinned: !inverter }) + } + + updateNews(id: string, title: string, content: string) { + return this.putNews(id, { + title: title, + content: content, + date: Date.now, + }) + } + + private putNews(id: string, update: object) { + return this.http.put( + environment.apiEndpoint + `/admin/news/${id}`, + update, + { withCredentials: true } + ) + } + +} diff --git a/frontend/src/app/admin-view/notifications/notifications.component.html b/frontend/src/app/admin-view/notifications/notifications.component.html new file mode 100644 index 0000000..67c247a --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.component.html @@ -0,0 +1,48 @@ +
+
+ + + + Użytkownik + + + + + + Pokój + + + + @if (ls.capCheck("notif")) { + + + Grupa + + @for (item of groups; track item) { + {{item.name}} + } + + + + } + +
+ + Tytuł + + +
+ + Zawartość wiadomości + + +
+ +
+ + @if (success) { +

Udało się wysłać {{success.sent}} z {{success.possible}} = {{success.sent/success.possible | percent}}

+ } \ No newline at end of file diff --git a/frontend/src/app/admin-view/notifications/notifications.component.scss b/frontend/src/app/admin-view/notifications/notifications.component.scss new file mode 100644 index 0000000..9a88bea --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.component.scss @@ -0,0 +1,11 @@ +:host { + display: flex; + justify-content: center; + align-items: center; + height: 80%; + flex-direction: column; +} + +mat-radio-button { + display: block; +} diff --git a/frontend/src/app/admin-view/notifications/notifications.component.spec.ts b/frontend/src/app/admin-view/notifications/notifications.component.spec.ts new file mode 100644 index 0000000..a7a27db --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.component.spec.ts @@ -0,0 +1,100 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { NotificationsComponent } from './notifications.component' +import { RouterModule } from '@angular/router' +import { MatRadioModule } from '@angular/material/radio' +import { + MatFormFieldControl, + MatFormFieldModule, +} from '@angular/material/form-field' +import { Component, forwardRef } from '@angular/core' +import { MatIconModule } from '@angular/material/icon' +import { Observable, of } from 'rxjs' +import { + AbstractControlDirective, + ControlValueAccessor, + FormsModule, + NG_VALUE_ACCESSOR, + NgControl, + ReactiveFormsModule, +} from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { NotificationsService } from './notifications.service' + +@Component({ + selector: 'app-user-search', + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => UserSearchStub), + multi: true, + }, + { + provide: MatFormFieldControl, + useExisting: UserSearchStub, + }, + ], + standalone: false, +}) +class UserSearchStub + implements ControlValueAccessor, MatFormFieldControl +{ + value = null + stateChanges: Observable = of() + id = '' + placeholder = '' + ngControl: NgControl | AbstractControlDirective | null = null + focused = false + empty = true + shouldLabelFloat = true + required = false + disabled = false + errorState = false + controlType?: string | undefined + autofilled?: boolean | undefined + userAriaDescribedBy?: string | undefined + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + setDescribedByIds(ids: string[]): void {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + onContainerClick(event: MouseEvent): void {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + writeValue(obj: unknown): void {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + registerOnChange(fn: unknown): void {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + registerOnTouched(fn: unknown): void {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + setDisabledState?(isDisabled: boolean): void {} +} + +xdescribe('NotificationsComponent', () => { + let component: NotificationsComponent + let fixture: ComponentFixture + + beforeEach(() => { + const acMock = {} + TestBed.configureTestingModule({ + declarations: [NotificationsComponent, UserSearchStub], + providers: [{ provide: NotificationsService, useValue: acMock }], + imports: [ + RouterModule.forRoot([]), + MatRadioModule, + MatFormFieldModule, + MatIconModule, + FormsModule, + ReactiveFormsModule, + MatInputModule, + NoopAnimationsModule, + ], + }) + fixture = TestBed.createComponent(NotificationsComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/notifications/notifications.component.ts b/frontend/src/app/admin-view/notifications/notifications.component.ts new file mode 100644 index 0000000..8400c07 --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.component.ts @@ -0,0 +1,85 @@ +import { Component, inject, OnDestroy, OnInit } from '@angular/core' +import { FormBuilder } from '@angular/forms' +import { Notification } from 'src/app/types/notification' +import { Group } from 'src/app/types/group' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { ToolbarService } from '../toolbar/toolbar.service' +import { ActivatedRoute, Router } from '@angular/router' +import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component' +import { NotificationsService, SendResult } from './notifications.service' +import { AdminSyncService } from '../admin-sync.service' +import { catchError, throwError } from 'rxjs' +import { MatSnackBar } from '@angular/material/snack-bar' + +@Component({ + selector: 'app-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.scss'], + standalone: false, +}) +export class NotificationsComponent implements OnInit, OnDestroy { + private ns = inject(NotificationsService) + private as = inject(AdminSyncService) + readonly ls = inject(LocalStorageService) + private toolbar = inject(ToolbarService) + private router = inject(Router) + private route = inject(ActivatedRoute) + private fb = inject(FormBuilder) + private sb = inject(MatSnackBar) + + groups!: Group[] + form = this.fb.group({ + recp: this.fb.group({ + uid: this.fb.control(null), + room: this.fb.control(null), + group: this.fb.control(''), + type: this.fb.control<'room' | 'uid' | 'group'>('uid', { + nonNullable: true, + }), + }), + title: this.fb.control('', { nonNullable: true }), + body: this.fb.control('', { nonNullable: true }), + }) + + constructor( + ) { + this.toolbar.comp = this + this.toolbar.menu = [{ title: 'Wysłane', fn: 'outbox', icon: 'outbox' }] + } + + outbox() { + this.router.navigate(['outbox'], { relativeTo: this.route }) + } + + ngOnInit(): void { + this.groups = this.as.groups + } + + ngOnDestroy(): void { + this.toolbar.comp = undefined + this.toolbar.menu = undefined + } + + success?: { sent: number; possible: number } + + submit() { + this.ns + .send({ + ...this.form.value, + recp: { + ...this.form.get('recp')?.value, + uid: this.form.controls['recp'].controls['uid'].value?._id, + }, + } as Notification) + .pipe( + catchError((err) => { + if (err.status === 404) { + this.sb.open("Brak odbiorców") + } + return throwError(() => err) + }) + ).subscribe(data => { + this.success = data as SendResult + }) + } +} diff --git a/frontend/src/app/admin-view/notifications/notifications.model.ts b/frontend/src/app/admin-view/notifications/notifications.model.ts new file mode 100644 index 0000000..4dd9e6b --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.model.ts @@ -0,0 +1,19 @@ +import { DateTime } from "luxon"; + +export type MessageAPI = Omit & { sentDate: string } + +export interface Message { + _id: string; + sentDate: DateTime; + title: string; + message?: string; + rcpts?: MessageRecipients[] +} + +export interface MessageRecipients { + _id: string + uname: string + room?: string + fname?: string + surname?: string +} diff --git a/frontend/src/app/admin-view/notifications/notifications.service.spec.ts b/frontend/src/app/admin-view/notifications/notifications.service.spec.ts new file mode 100644 index 0000000..3b46b9c --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { NotificationsService } from './notifications.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('NotificationsService', () => { + let service: NotificationsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(NotificationsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/notifications/notifications.service.ts b/frontend/src/app/admin-view/notifications/notifications.service.ts new file mode 100644 index 0000000..849f20e --- /dev/null +++ b/frontend/src/app/admin-view/notifications/notifications.service.ts @@ -0,0 +1,120 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { DateTime } from 'luxon'; +import { BehaviorSubject, catchError, map, of } from 'rxjs'; +import { Notification } from 'src/app/types/notification'; +import { STATE } from 'src/app/types/state'; +import { environment } from 'src/environments/environment'; +import { Message, MessageAPI, MessageRecipients } from './notifications.model'; +import { Status } from 'src/app/types/status'; + +export interface SendResult { + sent: number; + possible: number +} + +@Injectable({ + providedIn: 'root' +}) +export class NotificationsService { + private http = inject(HttpClient) + + private _msgs = new BehaviorSubject([]) + public readonly msgs = this._msgs.asObservable() + private _state = signal(STATE.NOT_LOADED); + public readonly state = this._state.asReadonly(); + private _error = signal(undefined); + public readonly error = this._error.asReadonly(); + + send(n: Notification) { + return this.http.post( + environment.apiEndpoint + '/admin/notif/send', + n, + { withCredentials: true } + ) + } + + public refreshOutbox() { + this.getOutbox() + } + + private getOutbox() { + this._state.set(STATE.PENDING) + this.http.get + + (environment.apiEndpoint + '/admin/notif/outbox', { withCredentials: true }) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }), + map(v => + v.map(i => ({ + ...i, + sentDate: DateTime.fromISO(i.sentDate), + })) + ) + ).subscribe(v => { + this._error.set(undefined) + this._msgs.next(v ?? []) + this._state.set(STATE.LOADED) + }) + } + + getMessageBody(id: string) { + this._state.set(STATE.PENDING) + this.http.get( + environment.apiEndpoint + `/admin/notif/outbox/${id}/message`, + { withCredentials: true, responseType: 'text' } + ) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }) + ) + .subscribe(msg => { + this._error.set(undefined) + const msgTemp = this._msgs.value.map(v => { + if (v._id === id) { + return { + ...v, + message: msg + } + } + return v + }) + this._msgs.next(msgTemp) + this._state.set(STATE.LOADED) + }) + } + + getMessageRcpts(id: string) { + this.http.get + + (environment.apiEndpoint + `/admin/notif/outbox/${id}/rcpts`, { withCredentials: true, }) + .pipe( + catchError((err: Error) => { + this._state.set(STATE.ERROR) + this._error.set(err.message) + return of() + }) + ) + .subscribe(rcpts => { + this._error.set(undefined) + const msgTemp = this._msgs.value.map(v => { + if (v._id === id) { + return { + ...v, + rcpts: rcpts + } + } + return v + }) + this._msgs.next(msgTemp) + this._state.set(STATE.LOADED) + }) + } +} diff --git a/frontend/src/app/admin-view/notifications/outbox/message/message.component.html b/frontend/src/app/admin-view/notifications/outbox/message/message.component.html new file mode 100644 index 0000000..a36b6d7 --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/message/message.component.html @@ -0,0 +1,41 @@ + + + + + {{item.title}} + + Wysłano {{item.sentDate.toFormat("cccc dd LLLL yyyyr. 'o' HH:mm")}} + + + + @if (item.message) { +

+ {{item.message}} +

+ } +
+ @if (item.rcpts) { + Odbiorcy: +
    + @for (user of item.rcpts; track $index) { +
  • + @if (user.room) { + {{user.room}}: + }{{user.fname}} {{user.surname}} ({{user.uname}}) +
  • + } +
+ } +
+ + + @if (!item.message) { + + } + @if (!item.rcpts) { + + } + + +
diff --git a/frontend/src/app/admin-view/notifications/outbox/message/message.component.scss b/frontend/src/app/admin-view/notifications/outbox/message/message.component.scss new file mode 100644 index 0000000..0835c9f --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/message/message.component.scss @@ -0,0 +1,3 @@ +mat-card-title { + font-size: 24pt; +} diff --git a/frontend/src/app/admin-view/notifications/outbox/message/message.component.spec.ts b/frontend/src/app/admin-view/notifications/outbox/message/message.component.spec.ts new file mode 100644 index 0000000..f38615e --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/message/message.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MessageComponent } from './message.component' +import { MatCardModule } from '@angular/material/card' +import { DateTime } from 'luxon' +import { NotificationsService } from '../../notifications.service' + +xdescribe('MessageComponent', () => { + let component: MessageComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const acMock = {} + await TestBed.configureTestingModule({ + declarations: [MessageComponent], + providers: [{ provide: NotificationsService, useValue: acMock }], + imports: [MatCardModule], + }).compileComponents() + + fixture = TestBed.createComponent(MessageComponent) + component = fixture.componentInstance + component.item = { _id: 'test', sentDate: DateTime.now(), title: 'Test' } + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/notifications/outbox/message/message.component.ts b/frontend/src/app/admin-view/notifications/outbox/message/message.component.ts new file mode 100644 index 0000000..636bc4a --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/message/message.component.ts @@ -0,0 +1,23 @@ +import { Component, inject, Input } from '@angular/core' +import { NotificationsService } from '../../notifications.service'; +import { Message } from '../../notifications.model'; + +@Component({ + selector: 'app-message', + templateUrl: './message.component.html', + styleUrl: './message.component.scss', + standalone: false, +}) +export class MessageComponent { + protected acu = inject(NotificationsService) + + @Input() item!: Message + + getMessage() { + this.acu.getMessageBody(this.item._id) + } + + getRcpts() { + this.acu.getMessageRcpts(this.item._id) + } +} diff --git a/frontend/src/app/admin-view/notifications/outbox/outbox.component.html b/frontend/src/app/admin-view/notifications/outbox/outbox.component.html new file mode 100644 index 0000000..f5d73f6 --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/outbox.component.html @@ -0,0 +1,9 @@ +

Wysłane wiadomości:

+
+ @if (ns.state() !== 2) { + + } + @for (item of messages; track $index) { + + } +
diff --git a/frontend/src/app/admin-view/notifications/outbox/outbox.component.scss b/frontend/src/app/admin-view/notifications/outbox/outbox.component.scss new file mode 100644 index 0000000..8dc662f --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/outbox.component.scss @@ -0,0 +1,6 @@ +.cardContainer { + display: flex; + flex-wrap: wrap; + gap: 1ch; + margin: 1ch; +} diff --git a/frontend/src/app/admin-view/notifications/outbox/outbox.component.spec.ts b/frontend/src/app/admin-view/notifications/outbox/outbox.component.spec.ts new file mode 100644 index 0000000..585d29b --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/outbox.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { OutboxComponent } from './outbox.component' +import { RouterModule } from '@angular/router' +import { NotificationsService } from '../notifications.service' + +xdescribe('OutboxComponent', () => { + let component: OutboxComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const acMock = {} + await TestBed.configureTestingModule({ + declarations: [OutboxComponent], + providers: [{ provide: NotificationsService, useValue: acMock }], + imports: [RouterModule.forRoot([])], + }).compileComponents() + + fixture = TestBed.createComponent(OutboxComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/notifications/outbox/outbox.component.ts b/frontend/src/app/admin-view/notifications/outbox/outbox.component.ts new file mode 100644 index 0000000..e25d1b2 --- /dev/null +++ b/frontend/src/app/admin-view/notifications/outbox/outbox.component.ts @@ -0,0 +1,43 @@ +import { Component, inject, OnDestroy, OnInit } from '@angular/core' +import { Router, ActivatedRoute } from '@angular/router' +import { ToolbarService } from '../../toolbar/toolbar.service' +import { NotificationsService } from '../notifications.service' +import { Message } from '../notifications.model' + +@Component({ + selector: 'app-outbox', + templateUrl: './outbox.component.html', + styleUrl: './outbox.component.scss', + standalone: false, +}) +export class OutboxComponent implements OnInit, OnDestroy { + protected ns = inject(NotificationsService) + private toolbar = inject(ToolbarService) + private router = inject(Router) + private route = inject(ActivatedRoute) + + messages!: Message[] + + constructor() { + this.toolbar.comp = this + this.toolbar.menu = [ + { title: 'Powiadomienia', fn: 'goBack', icon: 'arrow_back' }, + ] + } + + ngOnDestroy(): void { + this.toolbar.comp = undefined + this.toolbar.menu = undefined + } + + goBack() { + this.router.navigate(['../'], { relativeTo: this.route }) + } + + ngOnInit(): void { + this.ns.refreshOutbox() + this.ns.msgs.subscribe(v => { + this.messages = v + }) + } +} diff --git a/frontend/src/app/admin-view/settings/settings.component.html b/frontend/src/app/admin-view/settings/settings.component.html new file mode 100644 index 0000000..0c2fd54 --- /dev/null +++ b/frontend/src/app/admin-view/settings/settings.component.html @@ -0,0 +1,169 @@ + + + + + Pokoje + Numery wszystkich pokoi + +

Kliknij listę aby edytować

+ +
+ + + + + Powody nieczystości + Za co są przyznawane oceny za czystość + +

Kliknij listę aby edytować

+ +
+ + + + + Sale z kluczami + + + + + + + + + Ustawienia jadłospisu + + + + + + + + + + + + + + + + + + +
Domyślne wpisy w jadłospisie dla danych pozycji
ŚniadanieKolacja
+ + + +
+
+ +

Tagi do szybkiego wybierania przez użytkowników w trakcie oddawania opinii (kliknij aby edytować)

+
+ + Danie + + Śniadanie + Obiad + Kolacja + + +
+ @switch (selectedMenuItem) { + @case ('sn') { +

Śniadanie:

+ + } + @case ('ob') { +
+

Obiad ma 4 różne listy tagów, ponieważ jest oceniany w 4 kategoriach: zupa, danie główne, dodatki i napój +

+

Zupa:

+ +

Danie główne (także wege):

+ +

Dodatki:

+ +

Napój:

+ +
+ } + @case ('kol') { +

Kolacja:

+ + } + } +
+
+
+ + + + + Bezpieczeństwo + + + +

+ Domyślne hasło użytkownika po wygenerowaniu konto to pierwszelogowanie
+ Reset hasła powoduje zmianę na reset +

+
+

Ograniczenia logowania

+ + Dozwolone próby logowania + +
+ + Okres liczenia prób + + Podaj w minutach +
+ + Czas blokady konta + + Podaj w minutach +
+ +
+
+
+
+ + + + + Moduły + Włączanie/Wyłączanie poszczególnych funkcji programu + +
+ Wiadomości + Jadłospis + Powiadomienia + Grupy + Czystość + Klucze + +
+
+ + + + + Sterowanie programem + Zachowaj szczególną ostrożność przy tych ustawieniach! + + + + + + +
diff --git a/frontend/src/app/admin-view/settings/settings.component.scss b/frontend/src/app/admin-view/settings/settings.component.scss new file mode 100644 index 0000000..c9e41d0 --- /dev/null +++ b/frontend/src/app/admin-view/settings/settings.component.scss @@ -0,0 +1,11 @@ +:host { + margin: 15px; + display: block; +} + +.slide-form { + display: flex; + flex-direction: column; + gap: 1em; + width: fit-content; +} \ No newline at end of file diff --git a/frontend/src/app/admin-view/settings/settings.component.spec.ts b/frontend/src/app/admin-view/settings/settings.component.spec.ts new file mode 100644 index 0000000..fea0611 --- /dev/null +++ b/frontend/src/app/admin-view/settings/settings.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { SettingsComponent } from './settings.component' +import { MatExpansionModule } from '@angular/material/expansion' +import { Component, Input } from '@angular/core' +import { MatTabsModule } from '@angular/material/tabs' +import { MatFormFieldModule } from '@angular/material/form-field' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatIconModule } from '@angular/material/icon' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { MatInputModule } from '@angular/material/input' +import { SettingsService } from './settings.service' + +@Component({ + selector: 'app-list-editor', + template: '', + standalone: false, +}) +class ListEditorStub { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + @Input() converter?: any[] + @Input() list?: string[] +} + +xdescribe('SettingsComponent', () => { + let component: SettingsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const acMock = {} + await TestBed.configureTestingModule({ + declarations: [SettingsComponent, ListEditorStub], + providers: [{ provide: SettingsService, useValue: acMock }], + imports: [ + MatExpansionModule, + MatTabsModule, + MatFormFieldModule, + FormsModule, + ReactiveFormsModule, + MatIconModule, + NoopAnimationsModule, + MatInputModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(SettingsComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/settings/settings.component.ts b/frontend/src/app/admin-view/settings/settings.component.ts new file mode 100644 index 0000000..63b7829 --- /dev/null +++ b/frontend/src/app/admin-view/settings/settings.component.ts @@ -0,0 +1,176 @@ +import { Component, inject, OnInit } from '@angular/core' +import { MatSnackBar } from '@angular/material/snack-bar' +import { FormBuilder } from '@angular/forms' +import { SettingsService } from './settings.service' +import { Capabilities } from 'src/app/types/capability' +import { MenuTags } from 'src/app/types/menu' + +@Component({ + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrl: './settings.component.scss', + standalone: false, +}) +export class SettingsComponent implements OnInit { + private acu = inject(SettingsService) + private sb = inject(MatSnackBar) + private fb = inject(FormBuilder) + + usettings: IUSettings = { + cleanThings: [], + keyrooms: [], + menu: { + defaultItems: { kol: [], sn: [] }, + tags: { + sn: [], + ob: [], + kol: [], + soup: [], + cd: [], + dr: [], + other: [], + } + }, + rooms: [], + security: { loginTimeout: { attempts: 0, lockout: 0, time: 0 } }, + modules: { + news: true, + menu: true, + notif: true, + groups: true, + clean: true, + key: true + } + } + reloadTimeout = false + + selectedMenuItem: 'sn' | 'ob' | 'kol' | null = null + + accSec = this.fb.nonNullable.group({ + attempts: this.fb.nonNullable.control(1), + time: this.fb.nonNullable.control(1), + lockout: this.fb.nonNullable.control(1), + }) + + modules = this.fb.nonNullable.group({ + news: this.fb.nonNullable.control(true), + menu: this.fb.nonNullable.control(true), + notif: this.fb.nonNullable.control(true), + groups: this.fb.nonNullable.control(true), + clean: this.fb.nonNullable.control(true), + key: this.fb.nonNullable.control(true) + }) + + ngOnInit(): void { + this.acu.getAll().subscribe(r => { + this.usettings = r + this.accSecTimeouts = r.security.loginTimeout + this.modulesConfig = r.modules + }) + } + + saveRoom(event: string[]) { + this.usettings.rooms = event + this.send() + } + + saveCleanThings(event: string[]) { + this.usettings.cleanThings = event + this.send() + } + + saveKeyrooms(event: string[]) { + this.usettings.keyrooms = event + this.send() + } + + saveSn(event: string[]) { + this.usettings.menu.defaultItems.sn = event + this.send() + } + + saveKol(event: string[]) { + this.usettings.menu.defaultItems.kol = event + this.send() + } + + saveAccSecTimeouts() { + this.usettings.security.loginTimeout = this.accSecTimeouts + this.send() + } + + set accSecTimeouts(value: IUSettings['security']['loginTimeout']) { + this.accSec.setValue({ + attempts: value.attempts, + lockout: value.lockout / 60, + time: value.time / 60, + }) + } + get accSecTimeouts(): IUSettings['security']['loginTimeout'] { + return { + attempts: this.accSec.controls['attempts'].value, + lockout: this.accSec.controls['lockout'].value * 60, + time: this.accSec.controls['time'].value * 60, + } + } + + set modulesConfig(value: Capabilities) { + this.modules.setValue(value) + } + get modulesConfig(): Capabilities { + return this.modules.value as Capabilities + } + + saveModules() { + this.usettings.modules = this.modulesConfig + this.send() + } + + send() { + this.acu.post(this.usettings).subscribe(s => { + if (s.status == 200) { + this.sb.open('Zapisano!', undefined, { duration: 1000 }) + } else { + console.error(s) + } + }) + } + + reloadSettings() { + if (this.reloadTimeout) { + return + } + this.reloadTimeout = true + setTimeout(() => { + this.reloadTimeout = false + }, 5000) + this.acu.reload().subscribe(s => { + if (s.status == 200) { + this.sb.open('Przeładowano ustawienia!', undefined, { duration: 3000 }) + } else { + console.error(s) + } + }) + } +} + +export interface IUSettings { + keyrooms: string[] + rooms: string[] + cleanThings: string[] + menu: { + defaultItems: { + sn: string[] + kol: string[] + }, + tags: MenuTags + } + security: { + loginTimeout: { + attempts: number + time: number + lockout: number + } + }, + modules: Capabilities +} diff --git a/frontend/src/app/admin-view/settings/settings.service.spec.ts b/frontend/src/app/admin-view/settings/settings.service.spec.ts new file mode 100644 index 0000000..ef34aa7 --- /dev/null +++ b/frontend/src/app/admin-view/settings/settings.service.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; + +import { SettingsService } from './settings.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('SettingsService', () => { + let service: SettingsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(SettingsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin-view/settings/settings.service.ts b/frontend/src/app/admin-view/settings/settings.service.ts new file mode 100644 index 0000000..6522ece --- /dev/null +++ b/frontend/src/app/admin-view/settings/settings.service.ts @@ -0,0 +1,34 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { IUSettings } from './settings.component'; +import { environment } from 'src/environments/environment'; +import { Status } from 'src/app/types/status'; + +@Injectable({ + providedIn: 'root' +}) +export class SettingsService { + private http = inject(HttpClient) + + getAll() { + return this.http.get( + environment.apiEndpoint + `/admin/settings/`, + { withCredentials: true } + ) + } + + post(settings: IUSettings) { + return this.http.post( + environment.apiEndpoint + `/admin/settings/`, + settings, + { withCredentials: true } + ) + } + + reload() { + return this.http.get( + environment.apiEndpoint + `/admin/settings/reload/`, + { withCredentials: true } + ) + } +} diff --git a/frontend/src/app/admin-view/start/start.component.html b/frontend/src/app/admin-view/start/start.component.html new file mode 100644 index 0000000..8c6c0fe --- /dev/null +++ b/frontend/src/app/admin-view/start/start.component.html @@ -0,0 +1 @@ +arrow_upwardWybierz zakładkę w menu \ No newline at end of file diff --git a/frontend/src/app/admin-view/start/start.component.scss b/frontend/src/app/admin-view/start/start.component.scss new file mode 100644 index 0000000..c7ff7e9 --- /dev/null +++ b/frontend/src/app/admin-view/start/start.component.scss @@ -0,0 +1,13 @@ +.main { + margin-top: 8px; + margin-left: 16px; + display: flex; + align-items: center; + gap: 1ch; +} + +.icon { + width: fit-content; + height: fit-content; + font-size: 32pt; +} diff --git a/frontend/src/app/admin-view/start/start.component.spec.ts b/frontend/src/app/admin-view/start/start.component.spec.ts new file mode 100644 index 0000000..8336d78 --- /dev/null +++ b/frontend/src/app/admin-view/start/start.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { StartAdminComponent } from './start.component' +import { MatIconModule } from '@angular/material/icon' + +describe('StartAdminComponent', () => { + let component: StartAdminComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StartAdminComponent], + imports: [MatIconModule], + }).compileComponents() + + fixture = TestBed.createComponent(StartAdminComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/start/start.component.ts b/frontend/src/app/admin-view/start/start.component.ts new file mode 100644 index 0000000..b05a4ef --- /dev/null +++ b/frontend/src/app/admin-view/start/start.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-start', + templateUrl: './start.component.html', + styleUrl: './start.component.scss', + standalone: false, +}) +export class StartAdminComponent {} diff --git a/frontend/src/app/admin-view/toolbar/toolbar.component.html b/frontend/src/app/admin-view/toolbar/toolbar.component.html new file mode 100644 index 0000000..285ac0e --- /dev/null +++ b/frontend/src/app/admin-view/toolbar/toolbar.component.html @@ -0,0 +1,20 @@ + + + {{title.getTitle()}} + + @if (toolbar.menu) { + + } + + + @for (item of _menu; track $index) { + @if (item.check ?? true) { + + } + } + diff --git a/frontend/src/app/admin-view/toolbar/toolbar.component.scss b/frontend/src/app/admin-view/toolbar/toolbar.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/admin-view/toolbar/toolbar.component.spec.ts b/frontend/src/app/admin-view/toolbar/toolbar.component.spec.ts new file mode 100644 index 0000000..3d615a0 --- /dev/null +++ b/frontend/src/app/admin-view/toolbar/toolbar.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { ToolbarComponent } from './toolbar.component' +import { MatToolbarModule } from '@angular/material/toolbar' +import { MatIconModule } from '@angular/material/icon' +import { MatMenuModule } from '@angular/material/menu' + +describe('ToolbarComponent', () => { + let component: ToolbarComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToolbarComponent], + imports: [MatToolbarModule, MatIconModule, MatMenuModule], + }).compileComponents() + + fixture = TestBed.createComponent(ToolbarComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/toolbar/toolbar.component.ts b/frontend/src/app/admin-view/toolbar/toolbar.component.ts new file mode 100644 index 0000000..0d67232 --- /dev/null +++ b/frontend/src/app/admin-view/toolbar/toolbar.component.ts @@ -0,0 +1,26 @@ +import { Component, inject, Input, ViewChild } from '@angular/core' +import { MatDrawer } from '@angular/material/sidenav' +import { Title } from '@angular/platform-browser' +import { ToolbarService } from './toolbar.service' +import { MatMenuTrigger } from '@angular/material/menu' + +@Component({ + selector: 'app-toolbar', + templateUrl: './toolbar.component.html', + styleUrl: './toolbar.component.scss', + standalone: false, +}) +export class ToolbarComponent { + readonly title = inject(Title) + protected toolbar = inject(ToolbarService) + + @Input() drawer!: MatDrawer + @ViewChild(MatMenuTrigger) trigger!: MatMenuTrigger + + protected _menu?: typeof this.toolbar.menu + + openMenu() { + this._menu = this.toolbar.menu + this.trigger.openMenu() + } +} diff --git a/frontend/src/app/admin-view/toolbar/toolbar.service.spec.ts b/frontend/src/app/admin-view/toolbar/toolbar.service.spec.ts new file mode 100644 index 0000000..d56edac --- /dev/null +++ b/frontend/src/app/admin-view/toolbar/toolbar.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing' + +import { ToolbarService } from './toolbar.service' + +describe('ToolbarService', () => { + let service: ToolbarService + + beforeEach(() => { + TestBed.configureTestingModule({}) + service = TestBed.inject(ToolbarService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin-view/toolbar/toolbar.service.ts b/frontend/src/app/admin-view/toolbar/toolbar.service.ts new file mode 100644 index 0000000..517aeaf --- /dev/null +++ b/frontend/src/app/admin-view/toolbar/toolbar.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core' + +@Injectable({ + providedIn: 'root', +}) +export class ToolbarService { + /** + * In order to use this variable properly: + * + * In component's constructor set the ToolbarService's comp property to the component instance like following: + * ```ts + * ToolbarService#comp = this + * ``` + * + * In component's onDestroy: + * ```ts + * ToolbarService#comp = undefined + * ``` + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public comp?: any + + /** + * @param check - whether to show the option + * @param fn - set the param to the name of the function that should be called when user clicks the link + * + * In similar fashion to comp, insert in component's onDestroy: + * ```ts + * ToolbarService#menu = undefined + * ``` + */ + public menu?: { title: string; check?: boolean; icon?: string; fn: string }[] +} diff --git a/frontend/src/app/admin.guard.spec.ts b/frontend/src/app/admin.guard.spec.ts new file mode 100644 index 0000000..2541c15 --- /dev/null +++ b/frontend/src/app/admin.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing' +import { CanActivateChildFn } from '@angular/router' + +import { adminGuard } from './admin.guard' + +describe('adminGuard', () => { + const executeGuard: CanActivateChildFn = (...guardParameters) => + TestBed.runInInjectionContext(() => adminGuard(...guardParameters)) + + beforeEach(() => { + TestBed.configureTestingModule({}) + }) + + it('should be created', () => { + expect(executeGuard).toBeTruthy() + }) +}) diff --git a/frontend/src/app/admin.guard.ts b/frontend/src/app/admin.guard.ts new file mode 100644 index 0000000..9952311 --- /dev/null +++ b/frontend/src/app/admin.guard.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { inject } from '@angular/core' +import { CanActivateChildFn, RedirectCommand, Router } from '@angular/router' +import { LocalStorageService } from './services/local-storage.service' + +export const adminGuard: CanActivateChildFn = (childRoute, state) => { + const router = inject(Router) + if (inject(LocalStorageService).admin == undefined) + return new RedirectCommand(router.parseUrl('/')) + return true +} diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..cd42a2e --- /dev/null +++ b/frontend/src/app/app-routing.module.ts @@ -0,0 +1,118 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { NewsComponent } from './app-view/news/news.component' +import { MenuComponent } from './app-view/menu/menu.component' +import { AppViewComponent } from './app-view/app-view.component' +import { LoginComponent } from './login/login.component' +import { authGuard } from './auth.guard' +import { PersonalComponent } from './app-view/personal/personal.component' +import { AdminViewComponent } from './admin-view/admin-view.component' +import { NewsEditComponent } from './admin-view/news-edit/news-edit.component' +import { AccountMgmtComponent } from './admin-view/account-mgmt/account-mgmt.component' +import { MenuEditComponent } from './admin-view/menu-edit/menu-edit.component' +import { adminGuard } from './admin.guard' +import { GroupsComponent } from './admin-view/groups/groups.component' +import { StartComponent } from './app-view/start/start.component' +import { AdminKeyComponent } from './admin-view/key/key.component' +import { GradesComponent } from './admin-view/grades/grades.component' +import { SummaryComponent } from './admin-view/grades/summary/summary.component' +import { SettingsComponent } from './admin-view/settings/settings.component' +import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component' +import { NotificationsComponent } from './admin-view/notifications/notifications.component' +import { OutboxComponent } from './admin-view/notifications/outbox/outbox.component' +import { StartAdminComponent } from './admin-view/start/start.component' +import { MenuStatsComponent } from './admin-view/menu-edit/menu-stats/menu-stats.component' + +const routes: Routes = [ + { path: '', redirectTo: 'login', pathMatch: 'full' }, + { path: 'login', component: LoginComponent }, + { + path: 'app', + component: AppViewComponent, + title: 'Internat', + canActivateChild: [authGuard], + children: [ + { path: '', component: StartComponent, pathMatch: 'full' }, + { path: 'news', component: NewsComponent, title: 'Wiadomości' }, + { path: 'menu', component: MenuComponent, title: 'Jadłospis' }, + { path: 'grades', component: PersonalComponent, title: 'Konto' }, + ], + }, + { + path: 'admin', + component: AdminViewComponent, + title: 'Panel administracyjny', + canActivateChild: [authGuard, adminGuard], + children: [ + { path: '', pathMatch: 'full', component: StartAdminComponent }, + { + path: 'news', + title: 'Edytowanie wiadomości', + component: NewsEditComponent, + }, + { + path: 'menu', + children: [ + { + path: '', + pathMatch: 'full', + title: 'Edytowanie jadłospisu', + component: MenuEditComponent, + }, + { + path: 'stats', + title: 'Badanie opinii', + component: MenuStatsComponent + } + ] + }, + { + path: 'accounts', + title: 'Użytkownicy', + component: AccountMgmtComponent, + }, + { + path: 'notifications', + children: [ + { + path: '', + pathMatch: 'full', + title: 'Powiadomienia', + component: NotificationsComponent, + }, + { path: 'outbox', title: 'Wysłane', component: OutboxComponent }, + ], + }, + { path: 'groups', title: 'Grupy', component: GroupsComponent }, + { path: 'keys', title: 'Klucze', component: AdminKeyComponent }, + { + path: 'grades', + children: [ + { + path: '', + pathMatch: 'full', + title: 'Oceny', + component: GradesComponent, + }, + { + path: 'summary', + title: 'Podsumowanie ocen', + component: SummaryComponent, + }, + { + path: 'attendenceSummary', + title: 'Obecność', + component: AttendenceSummaryComponent, + }, + ], + }, + { path: 'settings', title: 'Ustawienia', component: SettingsComponent }, + ], + }, +] + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule { } diff --git a/frontend/src/app/app-view/app-view.component.html b/frontend/src/app/app-view/app-view.component.html new file mode 100644 index 0000000..186095e --- /dev/null +++ b/frontend/src/app/app-view/app-view.component.html @@ -0,0 +1,15 @@ + +
+ +
+
+ diff --git a/frontend/src/app/app-view/app-view.component.scss b/frontend/src/app/app-view/app-view.component.scss new file mode 100644 index 0000000..b4b55a1 --- /dev/null +++ b/frontend/src/app/app-view/app-view.component.scss @@ -0,0 +1,29 @@ +#bot-navigation { + width: 100%; +} + +#outlet { + width: 100%; + flex: 1 1 auto; + overflow-y: auto; +} + +#scrollable { + overflow: auto; + height: 100%; + position: relative; +} + +:host { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + @supports (-webkit-touch-callout: none) { + height: 95vh; + } +} + +a { + text-decoration-line: none; +} diff --git a/frontend/src/app/app-view/app-view.component.spec.ts b/frontend/src/app/app-view/app-view.component.spec.ts new file mode 100644 index 0000000..364a75b --- /dev/null +++ b/frontend/src/app/app-view/app-view.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AppViewComponent } from './app-view.component' +import { AuthClient } from '../services/auth.client' +import { SwPush } from '@angular/service-worker' +import { UpdatesService } from '../services/updates.service' +import { MatTabsModule } from '@angular/material/tabs' +import { RouterModule } from '@angular/router' +import { MatIconModule } from '@angular/material/icon' +import { of } from 'rxjs' + +describe('AppViewComponent', () => { + let component: AppViewComponent + let fixture: ComponentFixture + + beforeEach(() => { + const authSpy = jasmine.createSpyObj('AuthClient', ['check']) + const pushSpy = jasmine.createSpyObj('SwPush', ['requestSubscription']) + const updatesSpy = jasmine.createSpyObj('UpdatesService', { + newsCheck: of(), + }) + TestBed.configureTestingModule({ + declarations: [AppViewComponent], + providers: [ + { provide: AuthClient, useValue: authSpy }, + { provide: SwPush, useValue: pushSpy }, + { provide: UpdatesService, useValue: updatesSpy }, + ], + imports: [MatTabsModule, RouterModule.forRoot([]), MatIconModule], + }) + fixture = TestBed.createComponent(AppViewComponent) + component = fixture.componentInstance + + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/app-view.component.ts b/frontend/src/app/app-view/app-view.component.ts new file mode 100644 index 0000000..bf61557 --- /dev/null +++ b/frontend/src/app/app-view/app-view.component.ts @@ -0,0 +1,79 @@ +import { Component, inject, OnInit } from '@angular/core' +import { AuthClient } from '../services/auth.client' +import { SwPush } from '@angular/service-worker' +import { UpdatesService } from '../services/updates.service' +import { Link } from '../types/link' +import { LocalStorageService } from '../services/local-storage.service' +import { SyncService } from '../services/sync.service' + +@Component({ + selector: 'app-app-view', + templateUrl: './app-view.component.html', + styleUrls: ['./app-view.component.scss'], + standalone: false, +}) +export class AppViewComponent implements OnInit { + private ac = inject(AuthClient) + private swPush = inject(SwPush) + private us = inject(UpdatesService) + private ls = inject(LocalStorageService) + private sync = inject(SyncService) + + + private readonly _LINKS: Link[] = [ + { + title: 'Jadłospis', + href: 'menu', + icon: 'restaurant_menu', + enabled: this.ls.capCheck("menu"), + }, + { + title: 'Wiadomości', + href: 'news', + icon: 'newspaper', + badge: this.ls.newsFlag, + enabled: this.ls.capCheck("news"), + }, + { + title: 'Konto', + href: 'grades', + icon: 'account_circle', + enabled: true + } + ] + + public get LINKS() { + return this._LINKS.filter(v => { + return v.enabled + }) + } + + subscribeToNotif() { + if (this.swPush.isEnabled && this.ls.capCheck("notif")) { + this.swPush + .requestSubscription({ + serverPublicKey: this.ls.vapid, + }) + .then(sub => { + this.us.postNotif(sub) + }) + } + } + + ngOnInit() { + this.subscribeToNotif() + this.sync.notifCheck() + this.ac.check() + this.us.getNews() + this.us.news.subscribe(i => { + const newsWithDates = i.map(v => ({ ...v, date: new Date(v.date) })) + const newestDate = newsWithDates.sort((a, b) => b.date.getTime() - a.date.getTime())[0]?.date ?? new Date(0) + if (this.ls.newsDate.getTime() < newestDate.getTime()) { + this.ls.newsFlag.set(newsWithDates.filter(v => { + return v.date.getTime() > this.ls.newsDate.getTime() + }).length) + this.ls.newsDate = newestDate.toISOString() + } + }) + } +} diff --git a/frontend/src/app/app-view/menu/allergens/allergens.component.html b/frontend/src/app/app-view/menu/allergens/allergens.component.html new file mode 100644 index 0000000..a8fe972 --- /dev/null +++ b/frontend/src/app/app-view/menu/allergens/allergens.component.html @@ -0,0 +1,10 @@ +

Alergeny

+
    + +
  1. Nabiał
  2. +
  3. Gluten
  4. +
  5. Seler
  6. +
  7. Jaja
  8. +
  9. Ryby
  10. +
  11. Sezam / Orzechy
  12. +
diff --git a/frontend/src/app/app-view/menu/allergens/allergens.component.scss b/frontend/src/app/app-view/menu/allergens/allergens.component.scss new file mode 100644 index 0000000..5aa03f7 --- /dev/null +++ b/frontend/src/app/app-view/menu/allergens/allergens.component.scss @@ -0,0 +1,3 @@ +ol > li::marker { + font-weight: bold; +} diff --git a/frontend/src/app/app-view/menu/allergens/allergens.component.spec.ts b/frontend/src/app/app-view/menu/allergens/allergens.component.spec.ts new file mode 100644 index 0000000..d270616 --- /dev/null +++ b/frontend/src/app/app-view/menu/allergens/allergens.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AllergensComponent } from './allergens.component' + +describe('AllergensComponent', () => { + let component: AllergensComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AllergensComponent], + }) + fixture = TestBed.createComponent(AllergensComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/menu/allergens/allergens.component.ts b/frontend/src/app/app-view/menu/allergens/allergens.component.ts new file mode 100644 index 0000000..035cc87 --- /dev/null +++ b/frontend/src/app/app-view/menu/allergens/allergens.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-allergens', + templateUrl: './allergens.component.html', + styleUrls: ['./allergens.component.scss'], + standalone: false, +}) +export class AllergensComponent {} diff --git a/frontend/src/app/app-view/menu/menu.component.html b/frontend/src/app/app-view/menu/menu.component.html new file mode 100644 index 0000000..763ba4d --- /dev/null +++ b/frontend/src/app/app-view/menu/menu.component.html @@ -0,0 +1,90 @@ +
+ @if (loading) { + + } + @if (gettitle) { + + + {{gettitle}} + + + + } + @if (getsn) { + + + Śniadanie + + +
    + @for (i of ls.defaultItems.sn; track i) { +
  • {{i}}
  • + } + @for (i of getsn.fancy; track i) { +
  • {{i | capitalize}}
  • + } + @if (getsn.second) { +
  • {{getsn.second | capitalize}}
  • + } +
+
+ + + +
+ } + @if (getob) { + + + Obiad + + +
    + @if (getob.soup) { +
  • Z: {{getob.soup}}
  • + } + @if (getob.vege) { +
  • V: {{getob.vege}}
  • + } + @if (getob.meal) { +
  • {{getob.meal}}
  • + } + @for (i of getob.condiments; track i) { +
  • {{i}}
  • + } + @if (getob.drink) { +
  • {{getob.drink}}
  • + } + @for (i of getob.other; track i) { +
  • {{i}}
  • + } +
+
+ + + +
+ } + @if (getkol) { + + + Kolacja + + +
    +
    + + + +
    + } + @if (!(getkol || getob || getsn || loading || gettitle)) { + + + Brak danych, wybierz inny dzień. + + + } + +
    + diff --git a/frontend/src/app/app-view/menu/menu.component.scss b/frontend/src/app/app-view/menu/menu.component.scss new file mode 100644 index 0000000..56fd1d0 --- /dev/null +++ b/frontend/src/app/app-view/menu/menu.component.scss @@ -0,0 +1,41 @@ +:host { + display: flex; + flex-direction: column; + overflow: none; + height: 100%; +} + +#cards { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: auto; +} + +#alrg { + align-self: center; +} + +mat-spinner { + align-self: center; +} + +mat-card { + margin: 15px; + padding: 1ch; +} + +#no-data { + color: #777; + @media (prefers-color-scheme: dark) { + color: #aaa; + } +} + +mat-card-title { + font-size: 1.5rem; +} + +app-date-selector { + width: 100%; +} diff --git a/frontend/src/app/app-view/menu/menu.component.spec.ts b/frontend/src/app/app-view/menu/menu.component.spec.ts new file mode 100644 index 0000000..3805406 --- /dev/null +++ b/frontend/src/app/app-view/menu/menu.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { MenuComponent } from './menu.component' +import { UpdatesService } from 'src/app/services/updates.service' +import { DateSelectorComponent } from '../../commonComponents/date-selector/date-selector.component' +import { MatIconModule } from '@angular/material/icon' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatDatepickerModule } from '@angular/material/datepicker' +import { MatCardModule } from '@angular/material/card' +import { ReactiveFormsModule } from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { MatBottomSheetModule } from '@angular/material/bottom-sheet' +import { of } from 'rxjs' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter' + +describe('MenuComponent', () => { + let component: MenuComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const updatesSpy = jasmine.createSpyObj('UpdatesService', { + getMenu: of(), + }) + await TestBed.configureTestingModule({ + declarations: [MenuComponent, DateSelectorComponent], + providers: [ + { provide: UpdatesService, useValue: updatesSpy }, + provideLuxonDateAdapter(), + ], + imports: [ + MatIconModule, + MatFormFieldModule, + MatDatepickerModule, + MatCardModule, + ReactiveFormsModule, + MatInputModule, + BrowserAnimationsModule, + MatBottomSheetModule, + MatProgressSpinnerModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(MenuComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/menu/menu.component.ts b/frontend/src/app/app-view/menu/menu.component.ts new file mode 100644 index 0000000..76a9fa2 --- /dev/null +++ b/frontend/src/app/app-view/menu/menu.component.ts @@ -0,0 +1,100 @@ +import { Component, inject, signal } from '@angular/core' +import { UpdatesService } from '../../services/updates.service' +import { Menu } from '../../types/menu' +import { MatBottomSheet } from '@angular/material/bottom-sheet' +import { AllergensComponent } from './allergens/allergens.component' +import { filterLook, weekendFilter } from '../../util' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { DateTime } from 'luxon' +import { toObservable } from '@angular/core/rxjs-interop' +import { PollDialogComponent } from './poll-dialog/poll-dialog.component' + +@Component({ + selector: 'app-menu', + templateUrl: './menu.component.html', + styleUrls: ['./menu.component.scss'], + standalone: false, +}) +export class MenuComponent { + private uc = inject(UpdatesService) + private bs = inject(MatBottomSheet) + protected ls = inject(LocalStorageService) + + constructor() { + toObservable(this.day).subscribe(() => { + this.updateMenu() + }) + } + loading = true + + public filter = weekendFilter + + day = signal(filterLook(this.filter, "ahead", DateTime.now(), 7)!) + + menu?: Menu + get getsn() { + return this.menu && this.checkIfAnyProperty(this.menu.sn) + ? this.menu.sn + : null + } + get getob() { + return this.menu && this.checkIfAnyProperty(this.menu.ob) + ? this.menu.ob + : null + } + get getkol() { + return this.menu && this.menu.kol ? this.menu.kol : null + } + get gettitle() { + return this.menu && this.menu.dayTitle && this.menu.dayTitle != '' + ? this.menu.dayTitle + : null + } + + private checkIfAnyProperty(obj: Record) { + for (const i in obj) { + if (Array.isArray(obj[i])) { + if (obj[i].length > 0) return true + } else { + if (obj[i]) return true + } + } + return false + } + + updateMenu(silent?: boolean) { + this.loading = !silent + if (!silent) this.menu = undefined + this.uc.getMenu(this.day()).subscribe(m => { + this.loading = false + this.menu = m + }) + } + + alrg() { + this.bs.open(AllergensComponent) + } + + protected vegeColor(text: string) { + if (text.startsWith('V: ')) { + return '#43A047' + } + return 'inherit' + } + + openPoll(type: "sn" | "ob" | "kol") { + this.bs.open(PollDialogComponent, { + data: { + menu: this.menu, + type, + date: this.day() + } + }).afterDismissed().subscribe((v) => { + if (v) console.log(v); else return + this.uc.postVote(this.day(), v).subscribe(s => { + console.log(s); + + }) + }) + } +} diff --git a/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.html b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.html new file mode 100644 index 0000000..a4dd1e0 --- /dev/null +++ b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.html @@ -0,0 +1,41 @@ +
    +
    +

    Karta oceny

    + @if (loading) { + + } @else { +
    + } +
    +@for (item of items; track $index) { + + + + {{item.name | capitalize}} + @if (item.controls.dirty) { + + } + + +
    + + + @for (opt of TAGS[item.type]; track $index) { + {{opt}} + } + + + Uwagi + + +
    +
    +
    +} + +
    + + + + +
    diff --git a/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.scss b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.scss new file mode 100644 index 0000000..72be95f --- /dev/null +++ b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.scss @@ -0,0 +1,31 @@ +mat-form-field, textarea { + width: 100%; + height: fit-content; +} + +div.actions { + display: flex; + align-items: center; + margin-top: 1em; + span.align-end { + margin-left: auto; + } +} + +div.header { + display: flex; + align-items: center; + + .spacer { + flex: 1; + } + + h1 { + flex: 2; + } +} + +mat-card-header { + display: flex; + align-items: center; +} diff --git a/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.spec.ts b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.spec.ts new file mode 100644 index 0000000..333a59c --- /dev/null +++ b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PollDialogComponent } from './poll-dialog.component'; + +describe('PollDialogComponent', () => { + let component: PollDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PollDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PollDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.ts b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.ts new file mode 100644 index 0000000..11c1a27 --- /dev/null +++ b/frontend/src/app/app-view/menu/poll-dialog/poll-dialog.component.ts @@ -0,0 +1,127 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef } from '@angular/material/bottom-sheet'; +import { DateTime } from 'luxon'; +import { filter, Observable, Subject } from 'rxjs'; +import { LocalStorageService } from 'src/app/services/local-storage.service'; +import { UpdatesService } from 'src/app/services/updates.service'; +import { Menu, MenuTags } from 'src/app/types/menu'; + +class PollItem { + get value() { + if (this.controls.dirty) { + return { + name: this.name, + type: this.type, + value: this.controls.value + } + } else { + return { + name: this.name, + type: this.type, + value: null + } + } + } + + setValue(value: Observable<{ rating: number | null; tags: string[]; comment: string | null; }>) { + value.subscribe((v) => { + if (v) { + this.controls.patchValue(v) + } + }) + return this + } + + name: string; + type: keyof MenuTags; + controls: FormGroup<{ + rating: FormControl, + tags: FormControl, + comment: FormControl + }> + + constructor(name: string, type: keyof MenuTags, value?: typeof this.controls.value) { + this.name = name + this.type = type + this.controls = new FormGroup({ + rating: new FormControl(value?.rating ?? null), + tags: new FormControl(value?.tags ?? [], { nonNullable: true }), + comment: new FormControl(value?.comment ?? '') + }) + } +} + +@Component({ + selector: 'app-poll-dialog', + standalone: false, + templateUrl: './poll-dialog.component.html', + styleUrl: './poll-dialog.component.scss' +}) +export class PollDialogComponent implements OnInit { + data = inject(MAT_BOTTOM_SHEET_DATA) as { menu: Menu, type: 'sn' | 'ob' | 'kol', date: DateTime } + ref = inject(MatBottomSheetRef) + protected ls = inject(LocalStorageService) + protected update = inject(UpdatesService) + protected TAGS = this.ls.menuTags + + protected loading = false; + + items = [] as PollItem[] + + private generateItem(name: string | undefined, type: keyof MenuTags, value: Observable<{ name: string, rating: number | null; tags: string[]; comment: string | null; }>) { + if (name) { + this.items.push(new PollItem(name, type).setValue(value.pipe(filter(v=>v.name === name)))) + } + } + + private getData() { + const subject = new Subject<{ name: string, rating: number | null; tags: string[]; comment: string | null; }>() + this.loading = true + this.update.getVote(this.data.date).subscribe((v) => { + v.forEach((element: any) => { + subject.next({name: element.name, ...element.vote}) + }); + this.loading = false + }) + return subject.asObservable() + } + + async ngOnInit() { + const dataObs = this.getData() + + switch (this.data.type) { + case 'sn': + this.data.menu.sn.fancy.forEach((item) => { + this.generateItem(item, 'sn', dataObs) + }) + break + case 'ob': { + const wd = this.data.menu.ob + this.generateItem(wd.soup, 'soup', dataObs) + this.generateItem(wd.vege, 'ob', dataObs) + this.generateItem(wd.meal, 'ob', dataObs) + wd.condiments.forEach(v => this.generateItem(v, 'cd', dataObs)) + this.generateItem(wd.drink, 'dr', dataObs) + wd.other.forEach(v => this.generateItem(v, 'other', dataObs)) + break + } + case 'kol': + this.generateItem(this.data.menu.kol, 'kol', dataObs) + break + } + } + + save() { + this.ref.dismiss(this.items.map(v => v.value).filter(Boolean)) + } + + close() { + this.ref.dismiss(); + } + + resetGroup(item: PollItem) { + item.controls.reset() + item.controls.markAsPristine() + } +} diff --git a/frontend/src/app/app-view/news/news.component.html b/frontend/src/app/app-view/news/news.component.html new file mode 100644 index 0000000..93382b3 --- /dev/null +++ b/frontend/src/app/app-view/news/news.component.html @@ -0,0 +1,25 @@ +@if (newsapi.newsState() !== 2) { + +} +@for (item of news; track item) { + + + @if (item.pinned) { + push_pin + } + {{item.title}} + + + + +

    {{fullName(item)}} {{item.date | date:'d-LL-yyyy HH:mm'}}

    +
    +
    +} +@empty { + +

    + Brak wiadomości. +

    +
    +} diff --git a/frontend/src/app/app-view/news/news.component.scss b/frontend/src/app/app-view/news/news.component.scss new file mode 100644 index 0000000..759f54f --- /dev/null +++ b/frontend/src/app/app-view/news/news.component.scss @@ -0,0 +1,35 @@ +:host { + display: flex; + flex-direction: column; +} + +mat-card { + margin: 15px; + padding: 1ch; +} + +mat-spinner { + align-self: center; +} + +mat-card-title { + font-size: 1.5rem; +} + +mat-card-footer p { + font-size: 0.8rem; + color: #4a4a4a; + margin-bottom: 0; + text-align: end; + @media (prefers-color-scheme: dark) { + color: #999999; + } +} + +mat-card-content p { + white-space: pre-line; +} + +mat-card p { + margin: 15px; +} diff --git a/frontend/src/app/app-view/news/news.component.spec.ts b/frontend/src/app/app-view/news/news.component.spec.ts new file mode 100644 index 0000000..6823807 --- /dev/null +++ b/frontend/src/app/app-view/news/news.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { NewsComponent } from './news.component' +import { UpdatesService } from 'src/app/services/updates.service' +import { of } from 'rxjs' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { MatCardModule } from '@angular/material/card' + +describe('NewsComponent', () => { + let component: NewsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const updatesMock = jasmine.createSpyObj('UpdatesService', { + getNews: of(), + }) + const lsMock = { + news: [], + } + await TestBed.configureTestingModule({ + declarations: [NewsComponent], + providers: [ + { provide: UpdatesService, useValue: updatesMock }, + { provide: LocalStorageService, useValue: lsMock }, + ], + imports: [MatProgressSpinnerModule, NoopAnimationsModule, MatCardModule], + }).compileComponents() + + fixture = TestBed.createComponent(NewsComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/news/news.component.ts b/frontend/src/app/app-view/news/news.component.ts new file mode 100644 index 0000000..b3b4505 --- /dev/null +++ b/frontend/src/app/app-view/news/news.component.ts @@ -0,0 +1,39 @@ +import { Component, inject, OnInit } from '@angular/core' +import { UpdatesService } from '../../services/updates.service' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { News } from 'src/app/types/news.model' +import { marked } from 'marked' + +@Component({ + selector: 'app-news', + templateUrl: './news.component.html', + styleUrls: ['./news.component.scss'], + standalone: false, +}) +export class NewsComponent implements OnInit { + protected newsapi = inject(UpdatesService) + private ls = inject(LocalStorageService) + news: News[] = [] + + async ngOnInit() { + this.newsapi.news.subscribe(i => { + this.news = i.map(v => { + return { ...v, content: marked.parse(v.content, { breaks: true }).toString() } + }) + // var date = this.news.map(v => ({ ...v, date: new Date(v.date) })).sort((a, b) => b.date.getTime() - a.date.getTime())[0].date + // if (date > this.ls.newsDate) { + // this.ls.newsDate = date.toISOString() + // } + this.ls.newsFlag.set(null) + }) + + } + + fullName(n: News): string { + const { author: { fname, surname, uname } } = n; + if (fname || surname) { + return [fname, surname].filter(Boolean).join(' '); + } + return uname; + } +} diff --git a/frontend/src/app/app-view/notif-dialog/notif-dialog.component.html b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.html new file mode 100644 index 0000000..9d42eae --- /dev/null +++ b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.html @@ -0,0 +1,10 @@ +

    {{data.message.title}}

    + +

    + {{data.message.body}} +

    +
    Wysłano {{date.toFormat("cccc dd LLLL yyyyr. 'o' HH:mm")}}
    +
    + + + diff --git a/frontend/src/app/app-view/notif-dialog/notif-dialog.component.scss b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app-view/notif-dialog/notif-dialog.component.spec.ts b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.spec.ts new file mode 100644 index 0000000..590eaa5 --- /dev/null +++ b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { NotifDialogComponent } from './notif-dialog.component' +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog' +import { UpdatesService } from 'src/app/services/updates.service' +import { of } from 'rxjs' + +describe('NotifDialogComponent', () => { + let component: NotifDialogComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const uMock = jasmine.createSpyObj('UpdatesService', { + postInfoAck: of(), + }) + await TestBed.configureTestingModule({ + declarations: [NotifDialogComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: { message: 'Test' } }, + { provide: MatDialogRef, useValue: {} }, + { provide: UpdatesService, useValue: uMock }, + ], + imports: [MatDialogModule], + }).compileComponents() + + fixture = TestBed.createComponent(NotifDialogComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/notif-dialog/notif-dialog.component.ts b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.ts new file mode 100644 index 0000000..3fe2064 --- /dev/null +++ b/frontend/src/app/app-view/notif-dialog/notif-dialog.component.ts @@ -0,0 +1,32 @@ +import { Component, inject } from '@angular/core' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { DateTime } from 'luxon' +import { UpdatesService } from 'src/app/services/updates.service' + +@Component({ + selector: 'app-notif-dialog', + templateUrl: './notif-dialog.component.html', + styleUrl: './notif-dialog.component.scss', + standalone: false, +}) +export class NotifDialogComponent { + public data: { + _id: string + message: { title: string; body: string } + sentDate: string + } = inject(MAT_DIALOG_DATA) + public dialogRef: MatDialogRef = inject(MatDialogRef) + private uc = inject(UpdatesService) + + date: DateTime + + constructor() { + this.date = DateTime.fromISO(this.data.sentDate) + } + + ack() { + this.uc.postInfoAck(this.data._id).subscribe(() => { + this.dialogRef.close() + }) + } +} diff --git a/frontend/src/app/app-view/personal/about/about.component.html b/frontend/src/app/app-view/personal/about/about.component.html new file mode 100644 index 0000000..99bfa63 --- /dev/null +++ b/frontend/src/app/app-view/personal/about/about.component.html @@ -0,0 +1,17 @@ +

    O programie

    + +

    Projekt IPWA

    +

    Stworzone z myślą o potrzebach internatu.

    + + @for (link of LINKS; track $index) { + + {{link.icon}} + {{link.title}} + {{link.info}} + + } + +
    + + + \ No newline at end of file diff --git a/frontend/src/app/app-view/personal/about/about.component.scss b/frontend/src/app/app-view/personal/about/about.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app-view/personal/about/about.component.spec.ts b/frontend/src/app/app-view/personal/about/about.component.spec.ts new file mode 100644 index 0000000..8194a99 --- /dev/null +++ b/frontend/src/app/app-view/personal/about/about.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AboutComponent } from './about.component' +import { MatDialogModule } from '@angular/material/dialog' +import { MatListModule } from '@angular/material/list' + +describe('AboutComponent', () => { + let component: AboutComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AboutComponent], + imports: [MatDialogModule, MatListModule], + }).compileComponents() + + fixture = TestBed.createComponent(AboutComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/about/about.component.ts b/frontend/src/app/app-view/personal/about/about.component.ts new file mode 100644 index 0000000..1781d61 --- /dev/null +++ b/frontend/src/app/app-view/personal/about/about.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-about', + templateUrl: './about.component.html', + styleUrl: './about.component.scss', + standalone: false, +}) +export class AboutComponent { + LINKS: { title: string; info: string; icon: string; link: string }[] = [ + { + title: 'Autor', + info: 'Jan Szumotalski', + icon: 'person', + link: 'https://github.com/Slasherss1/', + }, + { + title: 'Źrodło', + info: 'Aplikacja jest darmowa i może ją uruchomić każdy!', + icon: 'code', + link: 'https://github.com/Slasherss1/ipwa-selfhosted', + }, + { + title: 'Licencja', + info: 'GPL-3.0', + icon: 'license', + link: 'https://www.gnu.org/licenses/gpl-3.0-standalone.html', + }, + ] +} diff --git a/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.html b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.html new file mode 100644 index 0000000..c854b11 --- /dev/null +++ b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.html @@ -0,0 +1,36 @@ +

    Zmiana hasła

    + +
    + + Aktualne hasło + + @if (form.hasError('wrongPass', "oldPass")) { + Złe hasło + } + @if (form.hasError('required', 'oldPass')) { + Wymagane + } +
    + + Nowe hasło + + @if (form.hasError('required', 'newPass')) { + Wymagane + } +
    + + Powtórz nowe hasło + + @if (form.hasError('noMatch')) { + Hasła muszą się zgadzać + } + @if (form.hasError('required', 'newPassRepeat')) { + Wymagane + } +
    + @if (error) { +

    {{error}}

    + } +
    +
    +
    diff --git a/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.scss b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.scss new file mode 100644 index 0000000..10c5d73 --- /dev/null +++ b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.scss @@ -0,0 +1,11 @@ +mat-error { + font-size: 10pt; +} + +form { + padding: 1rem; + display: flex; + align-items: center; + flex-direction: column; + // width: fit-content; +} diff --git a/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.spec.ts b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.spec.ts new file mode 100644 index 0000000..57949e0 --- /dev/null +++ b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { ChangePasswordDialogComponent } from './change-password-dialog.component' +import { AuthClient } from 'src/app/services/auth.client' +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatFormFieldModule } from '@angular/material/form-field' +import { ReactiveFormsModule } from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { HarnessLoader } from '@angular/cdk/testing' +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' +import { MatFormFieldHarness } from "@angular/material/form-field/testing"; +import { MatInputHarness } from "@angular/material/input/testing" +import { of } from 'rxjs' + +describe('ChangePasswordDialogComponent', () => { + let component: ChangePasswordDialogComponent + let fixture: ComponentFixture + let loader: HarnessLoader + let authClientSpy: jasmine.SpyObj + + beforeEach(() => { + const authMock = jasmine.createSpyObj('AuthClient', { + chpass: of(undefined) + }) + const drMock = jasmine.createSpyObj("DialogRef", ["close"]) + TestBed.configureTestingModule({ + declarations: [ChangePasswordDialogComponent], + providers: [ + { provide: AuthClient, useValue: authMock }, + { provide: MatDialogRef, useValue: drMock }, + ], + imports: [ + MatDialogModule, + MatFormFieldModule, + ReactiveFormsModule, + MatInputModule, + BrowserAnimationsModule, + ], + }) + fixture = TestBed.createComponent(ChangePasswordDialogComponent) + component = fixture.componentInstance + fixture.detectChanges() + loader = TestbedHarnessEnvironment.loader(fixture) + authClientSpy = TestBed.inject(AuthClient) as jasmine.SpyObj + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should be able to get a form fields', async () => { + const formFields = await loader.getAllHarnesses(MatFormFieldHarness) + expect(formFields.length).withContext("form fields").toBe(3) + }) + + xdescribe("form submit events", () => { + it('should submit when there are no form errors', async () => { + const formFields = await loader.getAllHarnesses(MatFormFieldHarness); + formFields.forEach(async element => { + const control = await element.getControl() as MatInputHarness + await control.setValue("test") + }); + component.changePass() + expect(authClientSpy.chpass).toHaveBeenCalled() + }) + + it('should not submit if there are form errors', async () => { + // const formFields = await loader.getAllHarnesses(MatFormFieldHarness); + // formFields.forEach(async element => { + // (await element.getControl() as MatInputHarness).setValue("test") + // }); + // component.changePass() + await fixture.whenStable() + expect(component.changePass).toThrowError("Form invalid") + }) + }) + +}) diff --git a/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.ts b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.ts new file mode 100644 index 0000000..fad88bb --- /dev/null +++ b/frontend/src/app/app-view/personal/change-password-dialog/change-password-dialog.component.ts @@ -0,0 +1,66 @@ +import { Component, inject } from '@angular/core' +import { AuthClient } from '../../../services/auth.client' +import { + FormControl, + FormGroup, + Validators, +} from '@angular/forms' +import { catchError, throwError } from 'rxjs' +import { MatDialogRef } from '@angular/material/dialog' +import { Router } from '@angular/router' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { PasswordValidators } from './pwd.validators' + +@Component({ + selector: 'app-change-password-dialog', + templateUrl: './change-password-dialog.component.html', + styleUrls: ['./change-password-dialog.component.scss'], + standalone: false, +}) +export class ChangePasswordDialogComponent { + private ac = inject(AuthClient) + public dr: MatDialogRef = inject(MatDialogRef) + private router = inject(Router) + private ls = inject(LocalStorageService) + + error: string | null = null + form = new FormGroup( + { + oldPass: new FormControl("", { validators: Validators.required, nonNullable: true }), + newPass: new FormControl("", { validators: Validators.required, nonNullable: true }), + newPassRepeat: new FormControl("", { validators: Validators.required, nonNullable: true }), + }, + { validators: [PasswordValidators.matchPass, PasswordValidators.childrenRequired] } + ) + + changePass() { + this.form.markAllAsTouched() + + if (this.form.errors) { + throw new Error("Form invalid") + } else { + this.error = null + } + + this.ac + .chpass(this.form.controls['oldPass'].value, this.form.controls['newPass'].value) + .pipe( + catchError(err => { + if (err.status == 401) { + this.form.controls['oldPass'].setErrors({ + wrongPass: true + }) + return throwError(() => new Error(err.message)) + } + this.error = 'Nieznany błąd' + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(() => { + if (this.error == null) { + this.dr.close() + this.ac.logout() + } + }) + } +} diff --git a/frontend/src/app/app-view/personal/change-password-dialog/pwd.validators.ts b/frontend/src/app/app-view/personal/change-password-dialog/pwd.validators.ts new file mode 100644 index 0000000..f4f499b --- /dev/null +++ b/frontend/src/app/app-view/personal/change-password-dialog/pwd.validators.ts @@ -0,0 +1,33 @@ +import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms" + +export class PasswordValidators { + static get matchPass(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const newpass = control.get('newPass') + const newpassrepeat = control.get('newPassRepeat') + if (newpass?.value !== newpassrepeat?.value) { + const err = { noMatch: true } + newpassrepeat?.setErrors(err) + return err + } + // newpassrepeat?.setErrors(null) + return null + } + } + + static get childrenRequired(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control instanceof FormGroup) { + const hasError = Object.keys(control.controls).find(key => { + return control.get(key)?.hasError("required") + }) + if (hasError) { + return {required: true} + } + return null + } else { + throw new Error("childrenRequired is only applicable on a FormGroup") + } + } + } +} diff --git a/frontend/src/app/app-view/personal/clean/clean.component.html b/frontend/src/app/app-view/personal/clean/clean.component.html new file mode 100644 index 0000000..1416934 --- /dev/null +++ b/frontend/src/app/app-view/personal/clean/clean.component.html @@ -0,0 +1,24 @@ +

    Czystość

    + + @if (grade) { +

    Twoja ocena: {{grade}}

    + } + @else { +

    Nie oceniono

    + } + @if (notes.length > 0) { +

    Uwagi:

    + } +
      + @for (i of notes; track i) { +
    • {{i.label}}@if (i.weight > 1) { + ({{i.weight}}) + }
    • + } +
    +

    {{tips}}

    + +
    + + + diff --git a/frontend/src/app/app-view/personal/clean/clean.component.scss b/frontend/src/app/app-view/personal/clean/clean.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app-view/personal/clean/clean.component.spec.ts b/frontend/src/app/app-view/personal/clean/clean.component.spec.ts new file mode 100644 index 0000000..9cdd867 --- /dev/null +++ b/frontend/src/app/app-view/personal/clean/clean.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { CleanComponent } from './clean.component' +import { UpdatesService } from 'src/app/services/updates.service' +import { MatDialogModule } from '@angular/material/dialog' +import { MatIconModule } from '@angular/material/icon' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatDatepicker } from '@angular/material/datepicker' +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { DateTime } from 'luxon' +import { CleanService } from './clean.service' + +@Component({ + selector: 'app-date-selector', + template: '', + standalone: false, +}) +class DateSelectorStub { + @Input() date: string = DateTime.now().toISODate() + @Output() dateChange = new EventEmitter() + @Input() filter: (date: DateTime | null) => boolean = () => true +} + +describe('CleanComponent', () => { + let component: CleanComponent + let fixture: ComponentFixture + let updates: jasmine.SpyObj + + beforeEach(async () => { + updates = jasmine.createSpyObj('CleanService', { + getClean: new Promise(() => { + return + }), + }) + await TestBed.configureTestingModule({ + declarations: [CleanComponent, DateSelectorStub], + providers: [{ provide: UpdatesService, useValue: updates }], + imports: [ + MatDialogModule, + MatIconModule, + MatFormFieldModule, + MatDatepicker, + ], + }).compileComponents() + + fixture = TestBed.createComponent(CleanComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/clean/clean.component.ts b/frontend/src/app/app-view/personal/clean/clean.component.ts new file mode 100644 index 0000000..2cc604f --- /dev/null +++ b/frontend/src/app/app-view/personal/clean/clean.component.ts @@ -0,0 +1,39 @@ +import { Component, inject, OnInit, signal } from '@angular/core' +import { toObservable } from "@angular/core/rxjs-interop"; +import { DateTime } from 'luxon' +import { filterLook, weekendFilter } from 'src/app/util' +import { CleanNote } from 'src/app/types/clean-note' +import { CleanService } from './clean.service'; + +@Component({ + selector: 'app-clean', + templateUrl: './clean.component.html', + styleUrl: './clean.component.scss', + standalone: false, +}) +export class CleanComponent implements OnInit { + private updates = inject(CleanService) + + grade: number | null = null + notes: CleanNote[] = [] + tips = '' + filter = weekendFilter + protected day = signal(filterLook(this.filter, "behind", DateTime.now(), 7)!) + + constructor() { + toObservable(this.day).subscribe(() => { + this.update() + }) + } + + ngOnInit(): void { + this.update() + } + + async update() { + const upd = await this.updates.getClean(this.day()) + this.grade = upd?.grade || null + this.notes = upd?.notes || [] + this.tips = upd?.tips || '' + } +} diff --git a/frontend/src/app/app-view/personal/clean/clean.service.spec.ts b/frontend/src/app/app-view/personal/clean/clean.service.spec.ts new file mode 100644 index 0000000..649d66e --- /dev/null +++ b/frontend/src/app/app-view/personal/clean/clean.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CleanService } from './clean.service'; + +xdescribe('CleanService', () => { + let service: CleanService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CleanService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/app-view/personal/clean/clean.service.ts b/frontend/src/app/app-view/personal/clean/clean.service.ts new file mode 100644 index 0000000..6f1fd5c --- /dev/null +++ b/frontend/src/app/app-view/personal/clean/clean.service.ts @@ -0,0 +1,31 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { DateTime } from 'luxon'; +import { firstValueFrom } from 'rxjs'; +import { CleanNote } from 'src/app/types/clean-note'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class CleanService { + private http = inject(HttpClient) + + private _dailyClean = signal(undefined) + readonly dailyClean = this._dailyClean.asReadonly() + + constructor() { + this.getClean(DateTime.now()).then(v => { + this._dailyClean.set(v?.grade) + }) + } + + async getClean(date: DateTime) { + return await firstValueFrom( + this.http.get<{ grade: number; notes: CleanNote[]; tips: string } | undefined>( + environment.apiEndpoint + `/app/clean/${date.toISODate()}`, + { withCredentials: true } + ) + ) + } +} diff --git a/frontend/src/app/app-view/personal/extra/extra.component.html b/frontend/src/app/app-view/personal/extra/extra.component.html new file mode 100644 index 0000000..dd85f94 --- /dev/null +++ b/frontend/src/app/app-view/personal/extra/extra.component.html @@ -0,0 +1,16 @@ +

    Dodatkowe ustawienia

    + + + @for (link of LINKS; track link) { + + } + + + + + \ No newline at end of file diff --git a/frontend/src/app/app-view/personal/extra/extra.component.scss b/frontend/src/app/app-view/personal/extra/extra.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app-view/personal/extra/extra.component.spec.ts b/frontend/src/app/app-view/personal/extra/extra.component.spec.ts new file mode 100644 index 0000000..6c6318b --- /dev/null +++ b/frontend/src/app/app-view/personal/extra/extra.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { ExtraComponent } from './extra.component' +import { MatDialogModule } from '@angular/material/dialog' +import { MatListModule } from '@angular/material/list' + +describe('ExtraComponent', () => { + let component: ExtraComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ExtraComponent], + imports: [MatDialogModule, MatListModule], + }).compileComponents() + + fixture = TestBed.createComponent(ExtraComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/extra/extra.component.ts b/frontend/src/app/app-view/personal/extra/extra.component.ts new file mode 100644 index 0000000..447c52a --- /dev/null +++ b/frontend/src/app/app-view/personal/extra/extra.component.ts @@ -0,0 +1,38 @@ +import { ComponentType } from '@angular/cdk/portal' +import { Component, inject } from '@angular/core' +import { Link } from 'src/app/types/link' +import { RedirectComponent } from './redirect/redirect.component' +import { MatDialog } from '@angular/material/dialog' + +@Component({ + selector: 'app-extra', + templateUrl: './extra.component.html', + styleUrl: './extra.component.scss', + standalone: false, +}) +export class ExtraComponent { + private dialog = inject(MatDialog) + + private readonly _LINKS: (Omit & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: ComponentType + })[] = [ + { + title: 'Domyślna strona po logowaniu', + component: RedirectComponent, + enabled: true, + icon: 'home', + }, + ] + + public get LINKS() { + return this._LINKS.filter(v => { + return v.enabled + }) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + open(component: ComponentType) { + this.dialog.open(component) + } +} diff --git a/frontend/src/app/app-view/personal/extra/redirect/redirect.component.html b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.html new file mode 100644 index 0000000..caed671 --- /dev/null +++ b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.html @@ -0,0 +1,14 @@ +

    Domyślna strona po logowaniu

    + +

    Wpisz link względem /ipwa/ w poniższym polu.

    +

    Przykład: /app/menu

    +

    Jeśli nie wiesz co tu wpisać, najlepiej nie zmieniaj tego ustawienia

    + + + Link + +
    + + + + \ No newline at end of file diff --git a/frontend/src/app/app-view/personal/extra/redirect/redirect.component.scss b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app-view/personal/extra/redirect/redirect.component.spec.ts b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.spec.ts new file mode 100644 index 0000000..319cc2e --- /dev/null +++ b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { RedirectComponent } from './redirect.component' +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { AuthClient } from 'src/app/services/auth.client' +import { MatFormFieldModule } from '@angular/material/form-field' +import { FormsModule } from '@angular/forms' +import { MatInputModule } from '@angular/material/input' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' + +describe('RedirectComponent', () => { + let component: RedirectComponent + let fixture: ComponentFixture + let authMock + + beforeEach(async () => { + authMock = jasmine.createSpyObj( + 'AuthClient', + {}, + { redirect: '' } + ) + await TestBed.configureTestingModule({ + declarations: [RedirectComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: AuthClient, useValue: authMock }, + ], + imports: [ + MatDialogModule, + MatFormFieldModule, + MatInputModule, + FormsModule, + NoopAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(RedirectComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/extra/redirect/redirect.component.ts b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.ts new file mode 100644 index 0000000..02a7012 --- /dev/null +++ b/frontend/src/app/app-view/personal/extra/redirect/redirect.component.ts @@ -0,0 +1,24 @@ +import { Component, inject } from '@angular/core' +import { MatDialogRef } from '@angular/material/dialog' +import { AuthClient } from 'src/app/services/auth.client' + +@Component({ + selector: 'app-redirect', + templateUrl: './redirect.component.html', + styleUrl: './redirect.component.scss', + standalone: false, +}) +export class RedirectComponent { + public dialogRef: MatDialogRef = inject(MatDialogRef) + private ac = inject(AuthClient) + + protected redirect = '' + constructor() { + this.redirect = this.ac.redirect + } + + protected save() { + this.ac.redirect = this.redirect + this.dialogRef.close() + } +} diff --git a/frontend/src/app/app-view/personal/key/key.component.html b/frontend/src/app/app-view/personal/key/key.component.html new file mode 100644 index 0000000..f3f7233 --- /dev/null +++ b/frontend/src/app/app-view/personal/key/key.component.html @@ -0,0 +1,14 @@ +

    Klucze

    + + @for (item of keys; track $index) { + @if (item.taken) { +
    {{item.room}}: Zajęte
    + } + @else { +
    {{item.room}}: Wolne
    + } + } +
    + + + \ No newline at end of file diff --git a/frontend/src/app/app-view/personal/key/key.component.scss b/frontend/src/app/app-view/personal/key/key.component.scss new file mode 100644 index 0000000..c0b2648 --- /dev/null +++ b/frontend/src/app/app-view/personal/key/key.component.scss @@ -0,0 +1,6 @@ +.free { + color: black; + @media (prefers-color-scheme: dark) { + color: white; + } +} diff --git a/frontend/src/app/app-view/personal/key/key.component.spec.ts b/frontend/src/app/app-view/personal/key/key.component.spec.ts new file mode 100644 index 0000000..ee08b82 --- /dev/null +++ b/frontend/src/app/app-view/personal/key/key.component.spec.ts @@ -0,0 +1,32 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { KeyComponent } from './key.component' +import { UpdatesService } from 'src/app/services/updates.service' +import { of } from 'rxjs' +import { MatDialogModule } from '@angular/material/dialog' +import { MatIconModule } from '@angular/material/icon' + +describe('KeyComponent', () => { + let component: KeyComponent + let fixture: ComponentFixture + let uMock: jasmine.SpyObj + + beforeEach(async () => { + uMock = jasmine.createSpyObj('UpdatesService', { + getKeys: of(), + }) + await TestBed.configureTestingModule({ + declarations: [KeyComponent], + providers: [{ provide: UpdatesService, useValue: uMock }], + imports: [MatDialogModule, MatIconModule], + }).compileComponents() + + fixture = TestBed.createComponent(KeyComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/key/key.component.ts b/frontend/src/app/app-view/personal/key/key.component.ts new file mode 100644 index 0000000..509af6f --- /dev/null +++ b/frontend/src/app/app-view/personal/key/key.component.ts @@ -0,0 +1,21 @@ +import { Component, inject, OnInit } from '@angular/core' +import { UpdatesService } from 'src/app/services/updates.service' +import { UKey } from 'src/app/types/key' + +@Component({ + selector: 'app-key', + templateUrl: './key.component.html', + styleUrl: './key.component.scss', + standalone: false, +}) +export class KeyComponent implements OnInit { + private us = inject(UpdatesService) + + keys!: UKey[] + + ngOnInit(): void { + this.us.getKeys().subscribe(v => { + this.keys = v + }) + } +} diff --git a/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.html b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.html new file mode 100644 index 0000000..a12f2ea --- /dev/null +++ b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.html @@ -0,0 +1,7 @@ + + Czy na pewno chcesz się wylogować? + + + + + \ No newline at end of file diff --git a/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.scss b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.spec.ts b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.spec.ts new file mode 100644 index 0000000..1b32ff7 --- /dev/null +++ b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { LogoutConfirmationComponent } from './logout-confirmation.component' +import { MatDialogModule } from '@angular/material/dialog' + +describe('LogoutConfirmationComponent', () => { + let component: LogoutConfirmationComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [LogoutConfirmationComponent], + imports: [MatDialogModule], + }) + fixture = TestBed.createComponent(LogoutConfirmationComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.ts b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.ts new file mode 100644 index 0000000..a09a27a --- /dev/null +++ b/frontend/src/app/app-view/personal/logout-confirmation/logout-confirmation.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'app-logout-confirmation', + templateUrl: './logout-confirmation.component.html', + styleUrls: ['./logout-confirmation.component.scss'], + standalone: false, +}) +export class LogoutConfirmationComponent {} diff --git a/frontend/src/app/app-view/personal/personal.component.html b/frontend/src/app/app-view/personal/personal.component.html new file mode 100644 index 0000000..cb1f61c --- /dev/null +++ b/frontend/src/app/app-view/personal/personal.component.html @@ -0,0 +1,69 @@ +
    + @if (ls.user?.fname) { +

    {{ls.user?.fname}} {{ls.user?.surname}}

    + } @else { +

    Użytkownik: {{ls.user?.uname}}

    + } + @if (ls.user?.room) { +

    Pokój: {{ls.user?.room}}

    + } + @if (ls.capCheck("clean") && ls.hasRoom()) { + + } +
    + + @if (ls.admin) { + + } + @if (ls.capCheck("key")) { + + } + + + + + + diff --git a/frontend/src/app/app-view/personal/personal.component.scss b/frontend/src/app/app-view/personal/personal.component.scss new file mode 100644 index 0000000..e762c2f --- /dev/null +++ b/frontend/src/app/app-view/personal/personal.component.scss @@ -0,0 +1,9 @@ +:host { + display: flex; + flex-wrap: wrap; + column-gap: 5rem; +} + +.info { + margin: 1rem; +} diff --git a/frontend/src/app/app-view/personal/personal.component.spec.ts b/frontend/src/app/app-view/personal/personal.component.spec.ts new file mode 100644 index 0000000..1952f2c --- /dev/null +++ b/frontend/src/app/app-view/personal/personal.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { PersonalComponent } from './personal.component' +import { AuthClient } from 'src/app/services/auth.client' +import { MatDialogModule } from '@angular/material/dialog' +import { MatSnackBarModule } from '@angular/material/snack-bar' +import { MatListModule } from '@angular/material/list' +import { + NoopAnimationsModule, +} from '@angular/platform-browser/animations' +import { AppUpdateService } from 'src/app/services/app-update.service' +import { of } from 'rxjs' +import { MatIconModule } from '@angular/material/icon' + +describe('PersonalComponent', () => { + let component: PersonalComponent + let fixture: ComponentFixture + let auMock: jasmine.SpyObj + + beforeEach(() => { + auMock = jasmine.createSpyObj('aumock', { + checkForUpdate: of(), + }) + const authMock = jasmine.createSpyObj('AuthClient', ['s']) + TestBed.configureTestingModule({ + declarations: [PersonalComponent], + providers: [ + { provide: AuthClient, useValue: authMock }, + { provide: AppUpdateService, useValue: auMock }, + ], + imports: [ + MatDialogModule, + MatSnackBarModule, + MatListModule, + NoopAnimationsModule, + MatIconModule, + ], + }) + fixture = TestBed.createComponent(PersonalComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/personal/personal.component.ts b/frontend/src/app/app-view/personal/personal.component.ts new file mode 100644 index 0000000..e9dc9ef --- /dev/null +++ b/frontend/src/app/app-view/personal/personal.component.ts @@ -0,0 +1,78 @@ +import { Component, inject } from '@angular/core' +import { AuthClient } from '../../services/auth.client' +import { Router } from '@angular/router' +import { MatDialog } from '@angular/material/dialog' +import { ChangePasswordDialogComponent } from './change-password-dialog/change-password-dialog.component' +import { environment } from 'src/environments/environment' +import { LogoutConfirmationComponent } from './logout-confirmation/logout-confirmation.component' +import { AppUpdateService } from 'src/app/services/app-update.service' +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { KeyComponent } from './key/key.component' +import { CleanComponent } from './clean/clean.component' +import { AboutComponent } from './about/about.component' +import { ExtraComponent } from './extra/extra.component' +import { CleanService } from './clean/clean.service' + +@Component({ + selector: 'app-personal', + templateUrl: './personal.component.html', + styleUrls: ['./personal.component.scss'], + standalone: false, +}) +export class PersonalComponent { + private ac = inject(AuthClient) + private router = inject(Router) + private dialog = inject(MatDialog) + private update = inject(AppUpdateService) + protected clean = inject(CleanService) + protected ls = inject(LocalStorageService) + + updateaval: boolean | unknown = false + checking: boolean | 'err' | 'aval' = false + public version = environment.version + protected logout() { + this.dialog.open(LogoutConfirmationComponent).afterClosed().subscribe(async (v) => { + if (v === true) { + await this.ac.logout() + } + }) + } + + protected openPassChange() { + this.dialog.open(ChangePasswordDialogComponent) + } + + protected openKey() { + this.dialog.open(KeyComponent) + } + + protected openClean() { + this.dialog.open(CleanComponent) + } + + protected goToAdmin() { + this.router.navigateByUrl('admin') + } + + protected async checkUpdate() { + this.checking = true + this.update.checkForUpdate().subscribe({ + next: v => { + this.checking = false + if (v) { + this.checking = 'aval' + } + }, + error: () => (this.checking = 'err'), + }) + this.ac.check() + } + + protected openExtra() { + this.dialog.open(ExtraComponent) + } + + protected openAbout() { + this.dialog.open(AboutComponent) + } +} diff --git a/frontend/src/app/app-view/start/start.component.html b/frontend/src/app/app-view/start/start.component.html new file mode 100644 index 0000000..fb3d192 --- /dev/null +++ b/frontend/src/app/app-view/start/start.component.html @@ -0,0 +1,12 @@ +
    +

    Witamy w aplikacji!

    +

    Dostępne funkcje:

    + + @for (link of LINKS; track $index) { + + {{link.title}} + {{link.icon}} + + } + +
    \ No newline at end of file diff --git a/frontend/src/app/app-view/start/start.component.scss b/frontend/src/app/app-view/start/start.component.scss new file mode 100644 index 0000000..73a940f --- /dev/null +++ b/frontend/src/app/app-view/start/start.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + height: 100%; + justify-content: center; + align-items: center; +} diff --git a/frontend/src/app/app-view/start/start.component.spec.ts b/frontend/src/app/app-view/start/start.component.spec.ts new file mode 100644 index 0000000..92de6d1 --- /dev/null +++ b/frontend/src/app/app-view/start/start.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { StartComponent } from './start.component' +import { RouterModule } from '@angular/router' +import { MatListModule } from '@angular/material/list' + +describe('StartComponent', () => { + let component: StartComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StartComponent], + imports: [RouterModule.forRoot([]), MatListModule], + }).compileComponents() + + fixture = TestBed.createComponent(StartComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/app-view/start/start.component.ts b/frontend/src/app/app-view/start/start.component.ts new file mode 100644 index 0000000..a313f56 --- /dev/null +++ b/frontend/src/app/app-view/start/start.component.ts @@ -0,0 +1,64 @@ +import { Component, inject } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { ɵ$localize as $localize } from "@angular/localize"; +import { LocalStorageService } from 'src/app/services/local-storage.service' +import { Link } from 'src/app/types/link' + +@Component({ + selector: 'app-start', + templateUrl: './start.component.html', + styleUrl: './start.component.scss', + standalone: false, +}) +export class StartComponent { + private r = inject(Router) + private route = inject(ActivatedRoute) + private ls = inject(LocalStorageService) + + private readonly _LINKS: Link[] = [ + { + title: $localize`Jadłospis (z funkcją zbierania opinii)`, + href: 'menu', + icon: 'restaurant_menu', + enabled: this.ls.capCheck("menu"), + }, + { + title: $localize`Wiadomości`, + href: 'news', + icon: 'newspaper', + enabled: this.ls.capCheck("news"), + }, + { + title: $localize`Ustawienia konta`, + href: 'grades', + icon: 'settings_account_box', + enabled: true, + }, + { + title: $localize`Klucze do sal`, + href: 'grades', + icon: 'key', + enabled: this.ls.capCheck("key"), + }, + { + title: $localize`Oceny za czystość`, + href: 'grades', + icon: 'cleaning_services', + enabled: this.ls.capCheck("clean"), + }, + { + title: $localize`Administracja`, + href: 'grades', + icon: 'admin_panel_settings', + enabled: this.ls.isAdmin, + }, + ] + public get LINKS(): Link[] { + return this._LINKS.filter(v => v.enabled) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected redirect(link: any) { + this.r.navigate([link], { relativeTo: this.route }) + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 0000000..90c6b64 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..5000d15 --- /dev/null +++ b/frontend/src/app/app.component.spec.ts @@ -0,0 +1,28 @@ +import { TestBed } from '@angular/core/testing' +import { AppComponent } from './app.component' +import { AppUpdateService } from './services/app-update.service' +import { RouterModule } from '@angular/router' + +describe('AppComponent', () => { + let auMock + beforeEach(async () => { + auMock = {} + await TestBed.configureTestingModule({ + declarations: [AppComponent], + providers: [{ provide: AppUpdateService, useValue: auMock }], + imports: [RouterModule], + }).compileComponents() + }) + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent) + const app = fixture.componentInstance + expect(app).toBeTruthy() + }) + + it(`should have as title 'Internat'`, () => { + const fixture = TestBed.createComponent(AppComponent) + const app = fixture.componentInstance + expect(app.title).toEqual('Internat') + }) +}) diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 0000000..885d9f2 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,39 @@ +import { Component, inject, LOCALE_ID, OnInit } from '@angular/core' +import { AppUpdateService } from './services/app-update.service' +import { MatIconRegistry } from '@angular/material/icon' +import { Settings } from 'luxon' +import { LocalStorageService } from './services/local-storage.service' +import { SyncService } from './services/sync.service' +import { MatSnackBar } from '@angular/material/snack-bar' + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + standalone: false, +}) +export class AppComponent implements OnInit { + readonly updates = inject(AppUpdateService) + mir = inject(MatIconRegistry) + lang = inject(LOCALE_ID) + private ls = inject(LocalStorageService) + private sync = inject(SyncService) + private sb = inject(MatSnackBar) + + + constructor() { + this.mir.setDefaultFontSetClass('material-symbols-rounded') + Settings.defaultLocale = this.lang + } + + ngOnInit(): void { + if (this.ls.loggedIn) { + this.sync.subscribe() + this.sync.notifEvents.subscribe(v => { + this.sync.notifCheck() + }) + } + } + + title = 'Internat' +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts new file mode 100644 index 0000000..c1fffe1 --- /dev/null +++ b/frontend/src/app/app.module.ts @@ -0,0 +1,211 @@ +import { NgModule } from '@angular/core' +import { BrowserModule } from '@angular/platform-browser' + +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MatCardModule } from '@angular/material/card' +import { MatDatepickerModule } from '@angular/material/datepicker' +import { MatDialogModule } from '@angular/material/dialog' +import { MatIconModule } from '@angular/material/icon' +import { MatInputModule } from '@angular/material/input' +import { MatListModule } from '@angular/material/list' +import { MatTabsModule } from '@angular/material/tabs' +import { MatToolbarModule } from '@angular/material/toolbar' +import { MatSidenavModule } from '@angular/material/sidenav' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { ServiceWorkerModule } from '@angular/service-worker' +import { AdminViewComponent } from './admin-view/admin-view.component' +import { AppRoutingModule } from './app-routing.module' +import { AppViewComponent } from './app-view/app-view.component' +import { DateSelectorComponent } from './commonComponents/date-selector/date-selector.component' +import { MenuComponent } from './app-view/menu/menu.component' +import { NewsComponent } from './app-view/news/news.component' +import { ChangePasswordDialogComponent } from './app-view/personal/change-password-dialog/change-password-dialog.component' +import { LogoutConfirmationComponent } from './app-view/personal/logout-confirmation/logout-confirmation.component' +import { PersonalComponent } from './app-view/personal/personal.component' +import { AppComponent } from './app.component' +import { LoginComponent } from './login/login.component' +import { MenuUploadComponent } from './admin-view/menu-edit/menu-upload/menu-upload.component' +import { NewsEditComponent } from './admin-view/news-edit/news-edit.component' +import { MatSnackBarModule } from '@angular/material/snack-bar' +import { NewPostComponent } from './admin-view/news-edit/new-post/edit-post.component' +import { AccountMgmtComponent } from './admin-view/account-mgmt/account-mgmt.component' +import { MatTableModule } from '@angular/material/table' +import { UserEditComponent } from './admin-view/account-mgmt/user-edit/user-edit.component' +import { MatPaginatorModule } from '@angular/material/paginator' +import { UserDeleteComponent } from './admin-view/account-mgmt/user-delete/user-delete.component' +import { MatSelectModule } from '@angular/material/select' +import { MenuEditComponent } from './admin-view/menu-edit/menu-edit.component' +import { FDSelection } from './fd.da' +import { CeDirective } from './ce.directive' +import { AllergensComponent } from './app-view/menu/allergens/allergens.component' +import { MatBottomSheetModule } from '@angular/material/bottom-sheet' +import { UserResetComponent } from './admin-view/account-mgmt/user-reset/user-reset.component' +import { MatSlideToggleModule } from '@angular/material/slide-toggle' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { MatRadioModule } from '@angular/material/radio' +import { NotificationsComponent } from './admin-view/notifications/notifications.component' +import { GroupsComponent } from './admin-view/groups/groups.component' +import { ListEditorComponent } from './commonComponents/list-editor/list-editor.component' +import { RemoveConfirmComponent } from './admin-view/groups/remove-confirm/remove-confirm.component' +import { StartComponent } from './app-view/start/start.component' +import { KeyComponent } from './app-view/personal/key/key.component' +import { AdminKeyComponent } from './admin-view/key/key.component' +import { MatChipsModule } from '@angular/material/chips' +import { NewKeyComponent } from './admin-view/key/new-key/new-key.component' +import { GradesComponent } from './admin-view/grades/grades.component' +import { RoomChooserComponent } from './commonComponents/room-chooser/room-chooser.component' +import { CleanComponent } from './app-view/personal/clean/clean.component' +import { MatCheckboxModule } from '@angular/material/checkbox' +import { LabelDirective } from './label.directive' +import { MatMenuModule } from '@angular/material/menu' +import { SummaryComponent } from './admin-view/grades/summary/summary.component' +import { MatSortModule } from '@angular/material/sort' +import { MatButtonToggleModule } from '@angular/material/button-toggle' +import { SettingsComponent } from './admin-view/settings/settings.component' +import { MatExpansionModule } from '@angular/material/expansion' +import { DragDropModule } from '@angular/cdk/drag-drop' +import { MatBadgeModule } from '@angular/material/badge' +import { MenuAddComponent } from './admin-view/menu-edit/menu-add/menu-add.component' +import { FieldEditorComponent } from './commonComponents/field-editor/field-editor.component' +import { A11yModule } from '@angular/cdk/a11y' +import { PortalModule } from '@angular/cdk/portal' +import { MatAutocompleteModule } from '@angular/material/autocomplete' +import { AttendenceComponent } from './admin-view/grades/attendence/attendence.component' +import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component' +import { HourDisplayComponent } from './admin-view/grades/attendence-summary/hour-display/hour-display.component' +import { AboutComponent } from './app-view/personal/about/about.component' +import { environment } from 'src/environments/environment' +import { ExtraComponent } from './app-view/personal/extra/extra.component' +import { RedirectComponent } from './app-view/personal/extra/redirect/redirect.component' +import { OutboxComponent } from './admin-view/notifications/outbox/outbox.component' +import { ToolbarComponent } from './admin-view/toolbar/toolbar.component' +import { MessageComponent } from './admin-view/notifications/outbox/message/message.component' +import { NotifDialogComponent } from './app-view/notif-dialog/notif-dialog.component' +import { UserSearchComponent } from './commonComponents/user-search/user-search.component' +import { StartAdminComponent } from './admin-view/start/start.component' +import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter'; +import { LoadShadeComponent } from './commonComponents/load-shade/load-shade.component' +import { GradeColorDirective } from './grade-color.directive'; +import { UserDisplayComponent } from './commonComponents/user-display/user-display.component'; +import { PollDialogComponent } from './app-view/menu/poll-dialog/poll-dialog.component'; +import { StarControlComponent } from './commonComponents/star-control/star-control.component'; +import { StarComponent } from './commonComponents/star-control/star/star.component' +import { CapitalizePipe } from "./capitalize.pipe"; +import { MenuStatsComponent } from './admin-view/menu-edit/menu-stats/menu-stats.component'; +import { MenuItemStatsDialogComponent } from './admin-view/menu-edit/menu-stats/menu-item-stats-dialog/menu-item-stats-dialog.component'; +import { MatTooltipModule } from "@angular/material/tooltip"; + +@NgModule({ + declarations: [ + AppComponent, + NewsComponent, + MenuComponent, + LoginComponent, + AppViewComponent, + PersonalComponent, + ChangePasswordDialogComponent, + LogoutConfirmationComponent, + DateSelectorComponent, + AdminViewComponent, + MenuUploadComponent, + NewsEditComponent, + NewPostComponent, + AccountMgmtComponent, + UserEditComponent, + UserDeleteComponent, + MenuEditComponent, + CeDirective, + LabelDirective, + AllergensComponent, + UserResetComponent, + NotificationsComponent, + GroupsComponent, + ListEditorComponent, + RemoveConfirmComponent, + StartComponent, + KeyComponent, + AdminKeyComponent, + NewKeyComponent, + GradesComponent, + RoomChooserComponent, + CleanComponent, + SummaryComponent, + SettingsComponent, + MenuAddComponent, + FieldEditorComponent, + AttendenceComponent, + AttendenceSummaryComponent, + HourDisplayComponent, + AboutComponent, + ExtraComponent, + RedirectComponent, + OutboxComponent, + ToolbarComponent, + MessageComponent, + NotifDialogComponent, + UserSearchComponent, + StartAdminComponent, + LoadShadeComponent, + GradeColorDirective, + UserDisplayComponent, + PollDialogComponent, + StarControlComponent, + StarComponent, + MenuStatsComponent, + MenuItemStatsDialogComponent, + ], + bootstrap: [AppComponent], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatTabsModule, + MatCardModule, + MatButtonModule, + MatIconModule, + MatDatepickerModule, + MatInputModule, + ReactiveFormsModule, + FormsModule, + MatDialogModule, + MatListModule, + MatToolbarModule, + MatSidenavModule, + MatSnackBarModule, + MatTableModule, + MatPaginatorModule, + MatSelectModule, + MatBottomSheetModule, + MatSlideToggleModule, + MatProgressSpinnerModule, + MatRadioModule, + MatChipsModule, + MatCheckboxModule, + MatMenuModule, + MatSortModule, + MatButtonToggleModule, + MatExpansionModule, + DragDropModule, + MatBadgeModule, + PortalModule, + A11yModule, + MatAutocompleteModule, + MatBadgeModule, + MatTooltipModule, + ServiceWorkerModule.register('ngsw-worker.js', { + enabled: environment.production, + // Register the ServiceWorker as soon as the application is stable + // or after 30 seconds (whichever comes first). + registrationStrategy: 'registerWhenStable:30000', + }), + CapitalizePipe + ], + providers: [ + provideLuxonDateAdapter(), + FDSelection, + provideHttpClient(withInterceptorsFromDi()), + ], +}) +export class AppModule { } diff --git a/frontend/src/app/auth.guard.spec.ts b/frontend/src/app/auth.guard.spec.ts new file mode 100644 index 0000000..8bd7996 --- /dev/null +++ b/frontend/src/app/auth.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing' +import { CanActivateFn } from '@angular/router' + +import { authGuard } from './auth.guard' + +describe('authGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => authGuard(...guardParameters)) + + beforeEach(() => { + TestBed.configureTestingModule({}) + }) + + it('should be created', () => { + expect(executeGuard).toBeTruthy() + }) +}) diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts new file mode 100644 index 0000000..c6def0a --- /dev/null +++ b/frontend/src/app/auth.guard.ts @@ -0,0 +1,11 @@ +import { inject } from '@angular/core' +import { CanActivateChildFn, RedirectCommand, Router } from '@angular/router' +import { LocalStorageService } from './services/local-storage.service' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const authGuard: CanActivateChildFn = (childRoute, state) => { + const router = inject(Router) + if (!inject(LocalStorageService).loggedIn) + return new RedirectCommand(router.parseUrl('/login')) + return true +} diff --git a/frontend/src/app/capitalize.pipe.spec.ts b/frontend/src/app/capitalize.pipe.spec.ts new file mode 100644 index 0000000..848c962 --- /dev/null +++ b/frontend/src/app/capitalize.pipe.spec.ts @@ -0,0 +1,8 @@ +import { CapitalizePipe } from './capitalize.pipe'; + +describe('CapitalizePipe', () => { + it('create an instance', () => { + const pipe = new CapitalizePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/capitalize.pipe.ts b/frontend/src/app/capitalize.pipe.ts new file mode 100644 index 0000000..d4c5f67 --- /dev/null +++ b/frontend/src/app/capitalize.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'capitalize' +}) +export class CapitalizePipe implements PipeTransform { + + transform(value: string): string { + return value.charAt(0).toUpperCase() + value.substring(1); + } + +} diff --git a/frontend/src/app/ce.directive.spec.ts b/frontend/src/app/ce.directive.spec.ts new file mode 100644 index 0000000..f0e490c --- /dev/null +++ b/frontend/src/app/ce.directive.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { CeDirective } from './ce.directive' +import { Component, DebugElement } from '@angular/core' +import { By } from '@angular/platform-browser' + +@Component({standalone: false, template: "

    Yoy

    "}) +class TestComponent {} + +describe('CeDirective', () => { + let fixture: ComponentFixture + let el: DebugElement + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + + }).createComponent(TestComponent) + + fixture.detectChanges() + + el = fixture.debugElement.query(By.directive(CeDirective)) + }) + + it('should be on a component', () => { + expect(el).toBeDefined() + }) +}) diff --git a/frontend/src/app/ce.directive.ts b/frontend/src/app/ce.directive.ts new file mode 100644 index 0000000..6e8a5ed --- /dev/null +++ b/frontend/src/app/ce.directive.ts @@ -0,0 +1,44 @@ +import { + Directive, + ElementRef, + EventEmitter, + HostListener, + inject, + Input, + Output, +} from '@angular/core' + +@Directive({ + selector: '[appCe] [contenteditable]', + standalone: false, +}) +export class CeDirective { + readonly el: ElementRef = inject(ElementRef) + + @Input() multiline = false + @Output() edit = new EventEmitter() + private originalValue: string | ChildNode[] + + constructor() { + this.originalValue = this.el.nativeElement.innerText + } + + @HostListener('focusin') focusin() { + this.originalValue = this.el.nativeElement.innerText + } + + @HostListener('focusout') focusout() { + const newText = this.el.nativeElement.innerText + if (newText != this.originalValue) { + this.edit.emit(newText) + } + } + + @HostListener('keydown', ['$event']) keyup(e: KeyboardEvent) { + if (this.multiline) return + if (e.key == 'Enter') { + e.preventDefault() + this.el.nativeElement.blur() + } + } +} diff --git a/frontend/src/app/commonComponents/date-selector/date-selector.component.html b/frontend/src/app/commonComponents/date-selector/date-selector.component.html new file mode 100644 index 0000000..1169355 --- /dev/null +++ b/frontend/src/app/commonComponents/date-selector/date-selector.component.html @@ -0,0 +1,7 @@ + +

    {{date()!.toFormat('cccc, d.LL') | titlecase }}

    + + + + + diff --git a/frontend/src/app/commonComponents/date-selector/date-selector.component.scss b/frontend/src/app/commonComponents/date-selector/date-selector.component.scss new file mode 100644 index 0000000..7abdb68 --- /dev/null +++ b/frontend/src/app/commonComponents/date-selector/date-selector.component.scss @@ -0,0 +1,11 @@ +:host { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; +} +p { + font-size: 15pt; + user-select: none; +} diff --git a/frontend/src/app/commonComponents/date-selector/date-selector.component.spec.ts b/frontend/src/app/commonComponents/date-selector/date-selector.component.spec.ts new file mode 100644 index 0000000..3b9f919 --- /dev/null +++ b/frontend/src/app/commonComponents/date-selector/date-selector.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { DateSelectorComponent } from './date-selector.component' +import { MatIconModule } from '@angular/material/icon' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatDatepickerModule } from '@angular/material/datepicker' +import { MatInputModule } from '@angular/material/input' +import { ReactiveFormsModule } from '@angular/forms' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter' +import { DateTime } from 'luxon' + +describe('DateSelectorComponent', () => { + let component: DateSelectorComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DateSelectorComponent], + imports: [ + MatIconModule, + MatFormFieldModule, + MatDatepickerModule, + ReactiveFormsModule, + MatInputModule, + BrowserAnimationsModule, + ], + providers: [provideLuxonDateAdapter()], + }) + fixture = TestBed.createComponent(DateSelectorComponent) + component = fixture.componentInstance + }) + + it('should create', async () => { + fixture.componentRef.setInput("date", DateTime.now()) + fixture.detectChanges() + await fixture.whenStable() + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/commonComponents/date-selector/date-selector.component.ts b/frontend/src/app/commonComponents/date-selector/date-selector.component.ts new file mode 100644 index 0000000..668c5b6 --- /dev/null +++ b/frontend/src/app/commonComponents/date-selector/date-selector.component.ts @@ -0,0 +1,58 @@ +import { + Component, + Input, + model, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core' +import { FormControl } from '@angular/forms' +import { DateFilterFn } from '@angular/material/datepicker' +import { DateTime } from 'luxon' + +@Component({ + selector: 'app-date-selector', + templateUrl: './date-selector.component.html', + styleUrls: ['./date-selector.component.scss'], + standalone: false, +}) +export class DateSelectorComponent implements OnChanges, OnInit { + date = model.required() + @Input() filter: DateFilterFn = () => true + protected dateInput: FormControl + + constructor() { + this.dateInput = new FormControl() + this.dateInput.valueChanges.subscribe(v => { + this.date.set(v) + }) + } + + ngOnInit(): void { + this.dateInput.setValue(this.date()) + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['date']) { + this.dateInput.setValue(this.date()!, { emitEvent: false }) + } + } + + prevDay(): void { + const yesterday = this.date()!.minus({ day: 1 }) + if (this.filter(yesterday)) { + this.dateInput.setValue(yesterday) + } else { + this.dateInput.setValue(this.date()!.set({ weekday: 5 }).minus({ week: 1 })) + } + } + + nextDay(): void { + const tomorrow = this.date()!.plus({ day: 1 }) + if (this.filter(tomorrow)) { + this.dateInput.setValue(tomorrow) + } else { + this.dateInput.setValue(this.date()!.set({ weekday: 1 }).plus({ week: 1 })) + } + } +} diff --git a/frontend/src/app/commonComponents/field-editor/field-editor.component.html b/frontend/src/app/commonComponents/field-editor/field-editor.component.html new file mode 100644 index 0000000..592f59e --- /dev/null +++ b/frontend/src/app/commonComponents/field-editor/field-editor.component.html @@ -0,0 +1,15 @@ +@if (!focused) { + @if (word !== "") { + {{word}} + } @else { + {{category}} + } + +} @else { + + +
    + + +
    +} \ No newline at end of file diff --git a/frontend/src/app/commonComponents/field-editor/field-editor.component.scss b/frontend/src/app/commonComponents/field-editor/field-editor.component.scss new file mode 100644 index 0000000..89d0dbc --- /dev/null +++ b/frontend/src/app/commonComponents/field-editor/field-editor.component.scss @@ -0,0 +1,4 @@ +.empty { + color: lightgray; + font-style: italic; +} diff --git a/frontend/src/app/commonComponents/field-editor/field-editor.component.spec.ts b/frontend/src/app/commonComponents/field-editor/field-editor.component.spec.ts new file mode 100644 index 0000000..02c56b5 --- /dev/null +++ b/frontend/src/app/commonComponents/field-editor/field-editor.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { FieldEditorComponent } from './field-editor.component' + +describe('FieldEditorComponent', () => { + let component: FieldEditorComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FieldEditorComponent], + }).compileComponents() + + fixture = TestBed.createComponent(FieldEditorComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/commonComponents/field-editor/field-editor.component.ts b/frontend/src/app/commonComponents/field-editor/field-editor.component.ts new file mode 100644 index 0000000..888039b --- /dev/null +++ b/frontend/src/app/commonComponents/field-editor/field-editor.component.ts @@ -0,0 +1,52 @@ +import { + Component, + ElementRef, + EventEmitter, + HostBinding, + HostListener, + inject, + Input, + Output, +} from '@angular/core' + +@Component({ + selector: 'app-field-editor', + templateUrl: './field-editor.component.html', + styleUrl: './field-editor.component.scss', + standalone: false, +}) +export class FieldEditorComponent { + private elRef = inject(ElementRef) + + @HostBinding('tabindex') tabindex = 0 + @Input() word!: string + @Output() wordChange = new EventEmitter() + @Input() category!: string + @Input() list?: string + + focused = false + + @HostListener('click') focusin() { + if (!this.focused) { + console.log('in') + this.focused = true + } + } + + @HostListener('focusout') focusout() { + console.log('out') + } + + save(event: Event, v: string) { + this.wordChange.emit(v) + this.elRef.nativeElement.blur() + this.focused = false + event.stopPropagation() + } + + cancel(event: Event) { + this.elRef.nativeElement.blur() + this.focused = false + event.stopPropagation() + } +} diff --git a/frontend/src/app/commonComponents/list-editor/list-editor.component.html b/frontend/src/app/commonComponents/list-editor/list-editor.component.html new file mode 100644 index 0000000..c0bf701 --- /dev/null +++ b/frontend/src/app/commonComponents/list-editor/list-editor.component.html @@ -0,0 +1,44 @@ +@if (!focused) { +
      + @for (item of _list; track item) { +
    • + @if (options) { + {{idToOption(item)}} + } @else { + {{item}} + } +
    • + } + @empty { +
    • [Kliknij aby edytować.]
    • + } +
    +} +@if (focused) { +
    + @if (workList.length === 0) { + + } +
    + @for (item of workList; track $index) { + + + @if (dropdown) { + + } + + + + } +
    + + + + +
    +} diff --git a/frontend/src/app/commonComponents/list-editor/list-editor.component.scss b/frontend/src/app/commonComponents/list-editor/list-editor.component.scss new file mode 100644 index 0000000..18cec65 --- /dev/null +++ b/frontend/src/app/commonComponents/list-editor/list-editor.component.scss @@ -0,0 +1,9 @@ +ul { + border: 0.1em gray solid; + padding: 0.5rem 40px; +} + +div.vertical { + display: flex; + flex-direction: column; +} diff --git a/frontend/src/app/commonComponents/list-editor/list-editor.component.spec.ts b/frontend/src/app/commonComponents/list-editor/list-editor.component.spec.ts new file mode 100644 index 0000000..cea54bb --- /dev/null +++ b/frontend/src/app/commonComponents/list-editor/list-editor.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { ListEditorComponent } from './list-editor.component' + +describe('ListEditorComponent', () => { + let component: ListEditorComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ListEditorComponent], + }) + fixture = TestBed.createComponent(ListEditorComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/commonComponents/list-editor/list-editor.component.ts b/frontend/src/app/commonComponents/list-editor/list-editor.component.ts new file mode 100644 index 0000000..70b709a --- /dev/null +++ b/frontend/src/app/commonComponents/list-editor/list-editor.component.ts @@ -0,0 +1,118 @@ +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop' +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostBinding, + HostListener, + inject, + Input, + OnChanges, + Output, + QueryList, + SimpleChanges, + ViewChildren, +} from '@angular/core' + +@Component({ + selector: 'app-list-editor[list], app-list-editor[converter]', + templateUrl: './list-editor.component.html', + styleUrls: ['./list-editor.component.scss'], + standalone: false, +}) +export class ListEditorComponent implements OnChanges { + private cdRef = inject(ChangeDetectorRef) + + @HostBinding('tabindex') tabindex = 0 + @Input() list?: string[] + @Output() listChange = new EventEmitter() + @Input() converter?: unknown[] + @Input() options?: { id: string; text: string }[] + @Input() dropdown?: boolean + @Input() dataList?: string + @Output() edit = new EventEmitter() + + @ViewChildren('input') inputList!: QueryList + + protected _list: string[] = [] + workList: string[] = [] + focused = false + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ngOnChanges(changes: SimpleChanges): void { + if (this.list) { + this._list = [...this.list] + } else if (this.converter) { + this._list = [...this.converter.map(String)] + } + } + + @HostListener('focusin') focusin() { + if (!this.focused) { + if (this.list) { + this.workList = [...this.list] + } else if (this.converter) { + this.workList = [...this.converter.map(String)] + } + } + this.focused = true + } + + save() { + this.listChange.emit(this.workList) + this.edit.emit(this.workList) + this.focused = false + } + + cancel() { + this.focused = false + } + + remPos(index: number) { + this.workList.splice(index, 1) + } + + autoRemPos(event: Event, index: number) { + if (index == 0) return; + if (this.workList[index].length != 0) return; + event.preventDefault() + this.remPos(index) + this.cdRef.detectChanges() + this.inputList.get(index - 1)?.nativeElement.focus() + } + + prev(index: number) { + if (index == 0) return; + this.inputList.get(index - 1)?.nativeElement.focus() + } + + next(index: number) { + if (this.workList[index+1] === undefined) { + this.addPos(index) + } else { + this._next(index) + } + } + + addPos(index: number) { + this.workList.splice(index + 1, 0, '') + this._next(index) + } + + private _next(index: number) { + this.inputList.notifyOnChanges() + this.cdRef.detectChanges() + this.inputList.get(index + 1)?.nativeElement.focus() + } + + drop(event: CdkDragDrop) { + moveItemInArray(this.workList, event.previousIndex, event.currentIndex) + } + + protected idToOption(item: string) { + return this.options?.find(v => { + return v.id == item + })?.text + } +} diff --git a/frontend/src/app/commonComponents/load-shade/load-shade.component.html b/frontend/src/app/commonComponents/load-shade/load-shade.component.html new file mode 100644 index 0000000..d577800 --- /dev/null +++ b/frontend/src/app/commonComponents/load-shade/load-shade.component.html @@ -0,0 +1,22 @@ +@switch (state()) { + @case (STATE.PENDING) { + + } + @case (STATE.ERROR) { + + + Błąd wczytywania + Podczas wczytywania treści wystąpił błąd. + + +

    {{ error() }}

    +
    + + + +
    + } + @default { +

    {{state()}}

    + } +} diff --git a/frontend/src/app/commonComponents/load-shade/load-shade.component.scss b/frontend/src/app/commonComponents/load-shade/load-shade.component.scss new file mode 100644 index 0000000..ec00705 --- /dev/null +++ b/frontend/src/app/commonComponents/load-shade/load-shade.component.scss @@ -0,0 +1,12 @@ +:host { + position: absolute; + background-color: hsla(0, 0%, 0%, 0.5); + top: 0; + bottom: 0; + right: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} diff --git a/frontend/src/app/commonComponents/load-shade/load-shade.component.spec.ts b/frontend/src/app/commonComponents/load-shade/load-shade.component.spec.ts new file mode 100644 index 0000000..bcc5c08 --- /dev/null +++ b/frontend/src/app/commonComponents/load-shade/load-shade.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoadShadeComponent } from './load-shade.component'; + +describe('LoadShadeComponent', () => { + let component: LoadShadeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LoadShadeComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoadShadeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/commonComponents/load-shade/load-shade.component.ts b/frontend/src/app/commonComponents/load-shade/load-shade.component.ts new file mode 100644 index 0000000..c40847c --- /dev/null +++ b/frontend/src/app/commonComponents/load-shade/load-shade.component.ts @@ -0,0 +1,18 @@ +import { Component, input, output } from '@angular/core'; +import { STATE } from 'src/app/types/state'; + +@Component({ + selector: 'app-load-shade', + standalone: false, + templateUrl: './load-shade.component.html', + styleUrl: './load-shade.component.scss', +}) +export class LoadShadeComponent { + state = input(STATE.NOT_LOADED) + error = input(undefined) + refresh = output() + + public get STATE(): typeof STATE { + return STATE + } +} diff --git a/frontend/src/app/commonComponents/room-chooser/room-chooser.component.html b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.html new file mode 100644 index 0000000..481afae --- /dev/null +++ b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.html @@ -0,0 +1,3 @@ + +

    {{room_1}}

    + \ No newline at end of file diff --git a/frontend/src/app/commonComponents/room-chooser/room-chooser.component.scss b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.scss new file mode 100644 index 0000000..a28221b --- /dev/null +++ b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.scss @@ -0,0 +1,12 @@ +:host { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; +} + +p { + font-size: 15pt; + user-select: none; +} diff --git a/frontend/src/app/commonComponents/room-chooser/room-chooser.component.spec.ts b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.spec.ts new file mode 100644 index 0000000..e682686 --- /dev/null +++ b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { RoomChooserComponent } from './room-chooser.component' +import { MatIconModule } from '@angular/material/icon' + +describe('RoomChooserComponent', () => { + let component: RoomChooserComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RoomChooserComponent], + imports: [MatIconModule], + }).compileComponents() + + fixture = TestBed.createComponent(RoomChooserComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/commonComponents/room-chooser/room-chooser.component.ts b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.ts new file mode 100644 index 0000000..a44779f --- /dev/null +++ b/frontend/src/app/commonComponents/room-chooser/room-chooser.component.ts @@ -0,0 +1,56 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, +} from '@angular/core' + +@Component({ + selector: 'app-room-chooser', + templateUrl: './room-chooser.component.html', + styleUrl: './room-chooser.component.scss', + standalone: false, +}) +export class RoomChooserComponent implements OnChanges { + @Input() rooms: string[] = [] + private _roomIndex = 0 + public get roomIndex(): number { + return this._roomIndex + } + public set roomIndex(value: number) { + if (value < 0 || value + 1 > this.rooms.length) { + return + } + this._roomIndex = value + } + private _room: string = this.rooms[this.roomIndex] + protected get room_1() { + return this._room + } + protected set room_1(value) { + this.room.emit(value) + this._room = value + } + @Output() room: EventEmitter = new EventEmitter() + + ngOnChanges(changes: SimpleChanges): void { + if (changes['rooms'] && this.rooms) { + if (this.rooms.length > 0) { + this._roomIndex = 0 + this.room_1 = this.rooms[this.roomIndex] + } + } + } + + prevRoom() { + this.roomIndex -= 1 + this.room_1 = this.rooms[this.roomIndex] + } + + nextRoom() { + this.roomIndex += 1 + this.room_1 = this.rooms[this.roomIndex] + } +} diff --git a/frontend/src/app/commonComponents/star-control/star-control.component.html b/frontend/src/app/commonComponents/star-control/star-control.component.html new file mode 100644 index 0000000..c863b57 --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star-control.component.html @@ -0,0 +1,6 @@ +
    + + @for (i of [1,2,3,4,5]; track $index) { + + } +
    diff --git a/frontend/src/app/commonComponents/star-control/star-control.component.scss b/frontend/src/app/commonComponents/star-control/star-control.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/commonComponents/star-control/star-control.component.spec.ts b/frontend/src/app/commonComponents/star-control/star-control.component.spec.ts new file mode 100644 index 0000000..3b177ca --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star-control.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StarControlComponent } from './star-control.component'; + +describe('StarControlComponent', () => { + let component: StarControlComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StarControlComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StarControlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/commonComponents/star-control/star-control.component.ts b/frontend/src/app/commonComponents/star-control/star-control.component.ts new file mode 100644 index 0000000..63d5c98 --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star-control.component.ts @@ -0,0 +1,113 @@ +import { BooleanInput, coerceBooleanProperty, coerceNumberProperty, NumberInput } from '@angular/cdk/coercion'; +import { Component, ElementRef, HostBinding, inject, input, Input } from '@angular/core'; +import { ControlValueAccessor, FormControl, NgControl } from '@angular/forms'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { Subject } from 'rxjs'; + +@Component({ + selector: 'app-star-control', + standalone: false, + templateUrl: './star-control.component.html', + styleUrl: './star-control.component.scss', + providers: [{ provide: MatFormFieldControl, useExisting: StarControlComponent }] +}) +export class StarControlComponent implements MatFormFieldControl, ControlValueAccessor { + private _elementRef = inject(ElementRef) + control: FormControl = new FormControl(null) + + touched = false; + + writeValue(obj: NumberInput): void { + this.value = coerceNumberProperty(obj); + this.stateChanges.next(); + } + registerOnChange(fn: (value: number | null) => void): void { + this.control.valueChanges.subscribe(fn); + } + registerOnTouched(fn: () => void): void { + this.control.registerOnChange(() => { + this.touched = true; + fn(); + }) + } + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + @Input() + public get value(): number | null { + return this.control.value; + } + public set value(value: NumberInput | null) { + this.control.setValue(coerceNumberProperty(value)); + this.stateChanges.next(); + } + stateChanges = new Subject(); + static nextId = 0; + @HostBinding() id = `star-control-${StarControlComponent.nextId++}`; + @Input() + get placeholder() { + return this._placeholder; + } + set placeholder(plh) { + this._placeholder = plh; + this.stateChanges.next(); + } + private _placeholder = ""; + public ngControl = inject(NgControl, { optional: true, self: true }); + focused = false; + get empty() { + return this.value === null || this.value === undefined; + } + @HostBinding('class.floating') + get shouldLabelFloat() { + return this.focused || !this.empty; + } + @Input() + get required(): boolean { + return this._required; + } + set required(req: BooleanInput) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + private _required = false; + @Input() + get disabled(): boolean { return this._disabled; } + set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value); + if (this._disabled) { + this.control.disable(); + } else { + this.control.enable(); + } + this.stateChanges.next(); + } + private _disabled = false; + get errorState(): boolean { + return this.control.invalid && this.touched; + } + controlType?: string | undefined; + autofilled?: boolean | undefined; + userAriaDescribedBy?: string | undefined; + disableAutomaticLabeling?: boolean | undefined; + setDescribedByIds(ids: string[]): void { + const controlElement = this._elementRef.nativeElement.querySelector( + '.app-star-control-container' + )! + controlElement.setAttribute('aria-describedby', ids.join(' ')) + } + onContainerClick(event: MouseEvent): void { + if ((event.target as Element).tagName.toLowerCase() != 'input') { + this._elementRef.nativeElement.querySelector('input').focus() + } + } + + display = input(false, { transform: coerceBooleanProperty }); + + constructor() { + if (this.ngControl != null) { + this.ngControl.valueAccessor = this; + } + } + +} diff --git a/frontend/src/app/commonComponents/star-control/star/star.component.html b/frontend/src/app/commonComponents/star-control/star/star.component.html new file mode 100644 index 0000000..ebc8578 --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star/star.component.html @@ -0,0 +1,19 @@ +@if (display()) { + + @if ((value() ?? 0) >= i()) { + star + } @else if ((value() ?? 0) > i() - 1) { + star_half + } @else { + star + } + +} @else { + +} diff --git a/frontend/src/app/commonComponents/star-control/star/star.component.scss b/frontend/src/app/commonComponents/star-control/star/star.component.scss new file mode 100644 index 0000000..cb6a65e --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star/star.component.scss @@ -0,0 +1,11 @@ +.empty { + font-variation-settings: 'FILL' 0; +} + +.filled { + font-variation-settings: 'FILL' 1; +} + +.display { + padding: 8px; +} diff --git a/frontend/src/app/commonComponents/star-control/star/star.component.spec.ts b/frontend/src/app/commonComponents/star-control/star/star.component.spec.ts new file mode 100644 index 0000000..19b4c38 --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star/star.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StarComponent } from './star.component'; + +describe('StarComponent', () => { + let component: StarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/commonComponents/star-control/star/star.component.ts b/frontend/src/app/commonComponents/star-control/star/star.component.ts new file mode 100644 index 0000000..e924c81 --- /dev/null +++ b/frontend/src/app/commonComponents/star-control/star/star.component.ts @@ -0,0 +1,15 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Component, input, model } from '@angular/core'; + +@Component({ + selector: 'app-star', + standalone: false, + templateUrl: './star.component.html', + styleUrl: './star.component.scss' +}) +export class StarComponent { + value = model(); + i = input(0); + disabled = input(false); + display = input(false, {transform: coerceBooleanProperty}); +} diff --git a/frontend/src/app/commonComponents/user-display/user-display.component.html b/frontend/src/app/commonComponents/user-display/user-display.component.html new file mode 100644 index 0000000..b6d7895 --- /dev/null +++ b/frontend/src/app/commonComponents/user-display/user-display.component.html @@ -0,0 +1,8 @@ +@if (fname()) { + {{fname()}} {{surname()}} + @if (room()) { + ({{room()}}) + } +} @else { + {{uname()}} +} diff --git a/frontend/src/app/commonComponents/user-display/user-display.component.scss b/frontend/src/app/commonComponents/user-display/user-display.component.scss new file mode 100644 index 0000000..1740ec9 --- /dev/null +++ b/frontend/src/app/commonComponents/user-display/user-display.component.scss @@ -0,0 +1,3 @@ +.room { + color: gray; +} diff --git a/frontend/src/app/commonComponents/user-display/user-display.component.spec.ts b/frontend/src/app/commonComponents/user-display/user-display.component.spec.ts new file mode 100644 index 0000000..767e3bd --- /dev/null +++ b/frontend/src/app/commonComponents/user-display/user-display.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDisplayComponent } from './user-display.component'; + +describe('UserDisplayComponent', () => { + let component: UserDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserDisplayComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/commonComponents/user-display/user-display.component.ts b/frontend/src/app/commonComponents/user-display/user-display.component.ts new file mode 100644 index 0000000..bdef5bc --- /dev/null +++ b/frontend/src/app/commonComponents/user-display/user-display.component.ts @@ -0,0 +1,16 @@ +import { Component, computed, input } from '@angular/core'; +import { User } from 'src/app/types/user'; + +@Component({ + selector: 'app-user-display', + standalone: false, + templateUrl: './user-display.component.html', + styleUrl: './user-display.component.scss' +}) +export class UserDisplayComponent { + item = input.required() + uname = computed(() => this.item().uname) + fname = computed(() => this.item().fname) + surname = computed(() => this.item().surname) + room = computed(() => this.item().room) +} diff --git a/frontend/src/app/commonComponents/user-search/user-search.component.html b/frontend/src/app/commonComponents/user-search/user-search.component.html new file mode 100644 index 0000000..26129f0 --- /dev/null +++ b/frontend/src/app/commonComponents/user-search/user-search.component.html @@ -0,0 +1,13 @@ +
    + + @if (loading) { + + } + + @for (item of list; track $index) { + + + + } + +
    diff --git a/frontend/src/app/commonComponents/user-search/user-search.component.scss b/frontend/src/app/commonComponents/user-search/user-search.component.scss new file mode 100644 index 0000000..5c6ddf5 --- /dev/null +++ b/frontend/src/app/commonComponents/user-search/user-search.component.scss @@ -0,0 +1,14 @@ +:host { + display: flex; + align-items: center; + gap: 1ch; +} + +input { + border: none; + background: none; + padding: 0; + outline: 0; + font: inherit; + color: currentColor; +} diff --git a/frontend/src/app/commonComponents/user-search/user-search.component.spec.ts b/frontend/src/app/commonComponents/user-search/user-search.component.spec.ts new file mode 100644 index 0000000..2cf5756 --- /dev/null +++ b/frontend/src/app/commonComponents/user-search/user-search.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { UserSearchComponent } from './user-search.component' +import { MatAutocompleteModule } from '@angular/material/autocomplete' +import { MatInputModule } from '@angular/material/input' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' + +xdescribe('UserSearchComponent', () => { + let component: UserSearchComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserSearchComponent], + imports: [ + MatAutocompleteModule, + MatInputModule, + FormsModule, + ReactiveFormsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(UserSearchComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/commonComponents/user-search/user-search.component.ts b/frontend/src/app/commonComponents/user-search/user-search.component.ts new file mode 100644 index 0000000..6b9ed41 --- /dev/null +++ b/frontend/src/app/commonComponents/user-search/user-search.component.ts @@ -0,0 +1,218 @@ +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion' +import { HttpClient } from '@angular/common/http' +import { + Component, + DoCheck, + ElementRef, + HostBinding, + inject, + Input, + OnDestroy, +} from '@angular/core' +import { + ControlValueAccessor, + FormControl, + FormGroupDirective, + NgControl, + NgForm, +} from '@angular/forms' +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete' +import { MatFormFieldControl } from '@angular/material/form-field' +import { debounceTime, filter, Subject } from 'rxjs' +import { environment } from 'src/environments/environment' + +export interface UserSearchResult { + _id: string + fname: string + surname: string + uname: string + room: string +} + +@Component({ + selector: 'app-user-search', + templateUrl: './user-search.component.html', + styleUrl: './user-search.component.scss', + providers: [ + { + provide: MatFormFieldControl, + useExisting: UserSearchComponent, + }, + ], + host: { + '(blur)': '_onTouched()', + }, + standalone: false, +}) +export class UserSearchComponent + implements + ControlValueAccessor, + MatFormFieldControl, + OnDestroy, + DoCheck { + public ngControl = inject(NgControl, { optional: true, self: true }) + private _parentForm = inject(NgForm, { optional: true }) + private _parentFormGroup = inject(FormGroupDirective, { optional: true }) + private _elementRef = inject(ElementRef) + private http = inject(HttpClient) + protected loading = false + control: FormControl = new FormControl("") + protected list: UserSearchResult[] = [] + private _onChange!: (_: UserSearchResult) => void + private _onTouched!: () => void + + static nextId = 0 + + @Input() + public get value(): UserSearchResult | null { + return this.control.value + } + + public set value(value: UserSearchResult | null) { + this.control.setValue(value) + this.stateChanges.next() + } + + touched = false + + stateChanges = new Subject() + + @HostBinding() id = `app-user-search-${UserSearchComponent.nextId++}` + + private _placeholder = '' + @Input() + public get placeholder(): string { + return this._placeholder + } + public set placeholder(value: string) { + this._placeholder = value + this.stateChanges.next() + } + + focused = false + onFocusIn() { + if (!this.focused) { + this.focused = true + this.stateChanges.next() + } + } + + onFocusOut(event: FocusEvent) { + if ( + !this._elementRef.nativeElement.contains(event.relatedTarget as Element) + ) { + this.touched = true + this.focused = false + this._onTouched() + this.stateChanges.next() + } + } + get empty(): boolean { + return !this.control.value + } + @HostBinding('class.floating') + get shouldLabelFloat(): boolean { + return this.focused || !this.empty + } + private _required = false + @Input() + public get required(): boolean { + return this._required + } + + public set required(value: BooleanInput) { + this._required = coerceBooleanProperty(value) + this.stateChanges.next() + } + + private _disabled = false + @Input() + public get disabled(): boolean { + return this._disabled + } + public set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this._disabled ? this.control.disable() : this.control.enable() + this.stateChanges.next() + } + errorState = false + controlType?: string | undefined = 'app-user-search' + autofilled?: boolean | undefined + @Input() 'aria-describedby'?: string + setDescribedByIds(ids: string[]): void { + const controlElement = this._elementRef.nativeElement.querySelector( + '.app-user-search-container' + )! + controlElement.setAttribute('aria-describedby', ids.join(' ')) + } + onContainerClick(event: MouseEvent): void { + if ((event.target as Element).tagName.toLowerCase() != 'input') { + this._elementRef.nativeElement.querySelector('input').focus() + } + } + + constructor() { + if (this.ngControl != null) { + ; (this.ngControl as NgControl).valueAccessor = this + } + this.control.valueChanges.pipe(debounceTime(500), filter(v => typeof v == "string")).subscribe(v => { + this.loading = true + this.http.get(environment.apiEndpoint + `/admin/usearch`, { + params: { q: v }, + withCredentials: true, + }).subscribe(v => { + this.list = v + this.loading = false + }) + }) + } + ngDoCheck(): void { + if (this.ngControl) { + this.updateErrorState() + } + } + private updateErrorState() { + const parent = this._parentFormGroup || this._parentForm + + const oldState = this.errorState + const newState = + (this.ngControl?.invalid || this.control.invalid) && + (this.touched || parent?.submitted) + + if (oldState !== newState) { + this.errorState = !!newState + this.stateChanges.next() + } + } + ngOnDestroy(): void { + this.stateChanges.complete() + } + + writeValue(obj: UserSearchResult): void { + this.value = obj + } + + registerOnChange(fn: (_: UserSearchResult) => void): void { + this._onChange = fn + } + + registerOnTouched(fn: () => void): void { + this._onTouched = fn + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled + } + + protected displayFn(u: UserSearchResult): string { + if (!u) return '' + return u.fname ? `${u.fname} ${u.surname}` : u.uname + } + + protected saveValue(e: MatAutocompleteSelectedEvent) { + this.autofilled = true + this.value = e.option.value + this._onChange(this.value!) + } +} diff --git a/frontend/src/app/fd.da.ts b/frontend/src/app/fd.da.ts new file mode 100644 index 0000000..2aa09a4 --- /dev/null +++ b/frontend/src/app/fd.da.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core' +import { + DateRange, + MatDateRangeSelectionStrategy, +} from '@angular/material/datepicker' +import { DateTime } from 'luxon' + +@Injectable() +export class FDSelection implements MatDateRangeSelectionStrategy { + selectionFinished(date: DateTime | null): DateRange { + return this._cr(date) + } + createPreview(activeDate: DateTime | null): DateRange { + return this._cr(activeDate) + } + + private _cr(date: DateTime | null) { + if (date) { + const start = date.toUTC(undefined, {keepLocalTime: true}).startOf('week') + const end = date.toUTC(undefined, {keepLocalTime: true}).set({ weekday: 5 }).endOf('day') + return new DateRange(start, end) + } + return new DateRange(null, null) + } +} diff --git a/frontend/src/app/grade-color.directive.spec.ts b/frontend/src/app/grade-color.directive.spec.ts new file mode 100644 index 0000000..d039cf9 --- /dev/null +++ b/frontend/src/app/grade-color.directive.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; +import { GradeColorDirective } from './grade-color.directive'; + +describe('GradeColorDirective', () => { + it('should create an instance', () => { + let directive + TestBed.runInInjectionContext(() => { + directive = new GradeColorDirective(); + }) + expect(directive).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/grade-color.directive.ts b/frontend/src/app/grade-color.directive.ts new file mode 100644 index 0000000..6b872b6 --- /dev/null +++ b/frontend/src/app/grade-color.directive.ts @@ -0,0 +1,31 @@ +import { Directive, HostBinding, input, numberAttribute } from '@angular/core'; + +@Directive({ + selector: 'span[appGradeColor]', + standalone: false +}) +export class GradeColorDirective { + + appGradeColor = input.required({transform: numberAttribute}) + + @HostBinding("style") + get gc() { + switch (this.appGradeColor()) { + case 1: + return { color: 'red' } + case 2: + return { color: 'darkorange' } + case 3: + return { color: 'orange' } + case 4: + return { color: 'olive' } + case 5: + return { color: 'green' } + case 6: + return { color: 'springgreen' } + default: + return { color: 'gray' } + } + } + +} diff --git a/frontend/src/app/label.directive.spec.ts b/frontend/src/app/label.directive.spec.ts new file mode 100644 index 0000000..2d555a6 --- /dev/null +++ b/frontend/src/app/label.directive.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing' +import { LabelDirective } from './label.directive' + +describe('LabelDirective', () => { + it('should create an instance', () => { + let directive + TestBed.runInInjectionContext(() => { + directive = new LabelDirective() + }) + expect(directive).toBeTruthy() + }) +}) diff --git a/frontend/src/app/label.directive.ts b/frontend/src/app/label.directive.ts new file mode 100644 index 0000000..b235980 --- /dev/null +++ b/frontend/src/app/label.directive.ts @@ -0,0 +1,18 @@ +import { Directive, HostBinding, inject, Input } from '@angular/core' +import { ControlContainer } from '@angular/forms' + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'span[control]', + standalone: false, +}) +export class LabelDirective { + private parent? = inject(ControlContainer, {optional: true}) + + @Input() control = '' + + @HostBinding('textContent') + get controlValue() { + return this.parent ? this.parent.control?.get(this.control)?.value : '' + } +} diff --git a/frontend/src/app/login/login.component.html b/frontend/src/app/login/login.component.html new file mode 100644 index 0000000..584736a --- /dev/null +++ b/frontend/src/app/login/login.component.html @@ -0,0 +1,18 @@ + + +
    + + Login + + + + Hasło + + + + @if (error) { +

    {{error}}

    + } +
    +
    +
    diff --git a/frontend/src/app/login/login.component.scss b/frontend/src/app/login/login.component.scss new file mode 100644 index 0000000..c25c2a2 --- /dev/null +++ b/frontend/src/app/login/login.component.scss @@ -0,0 +1,15 @@ +form { + display: flex; + flex-direction: column; +} +mat-card { + margin: 15px; + width: fit-content; +} +:host { + width: 100%; + height: 100%; + display: grid; + align-items: center; + justify-items: center; +} diff --git a/frontend/src/app/login/login.component.spec.ts b/frontend/src/app/login/login.component.spec.ts new file mode 100644 index 0000000..a42d129 --- /dev/null +++ b/frontend/src/app/login/login.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { LoginComponent } from './login.component' +import { AuthClient } from '../services/auth.client' +import { of } from 'rxjs' +import { MatCardModule } from '@angular/material/card' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import { ReactiveFormsModule } from '@angular/forms' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' + +describe('LoginComponent', () => { + let component: LoginComponent + let fixture: ComponentFixture + let authMock: AuthClient + + beforeEach(async () => { + authMock = jasmine.createSpyObj('AuthClient', { + login: of(), + }) + await TestBed.configureTestingModule({ + declarations: [LoginComponent], + providers: [{ provide: AuthClient, useValue: authMock }], + imports: [ + MatCardModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + BrowserAnimationsModule, + ], + }).compileComponents() + + fixture = TestBed.createComponent(LoginComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts new file mode 100644 index 0000000..bd9fd69 --- /dev/null +++ b/frontend/src/app/login/login.component.ts @@ -0,0 +1,54 @@ +import { Component, inject, OnInit } from '@angular/core' +import { FormBuilder, FormGroup } from '@angular/forms' +import { AuthClient } from '../services/auth.client' +import { Router } from '@angular/router' +import { catchError, throwError } from 'rxjs' +import { LocalStorageService } from '../services/local-storage.service' + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], + standalone: false, +}) +export class LoginComponent implements OnInit { + protected error: string | null = null + form: FormGroup + + private ac = inject(AuthClient) + private fb = inject(FormBuilder) + private router = inject(Router) + private ls = inject(LocalStorageService) + + constructor() { + this.form = this.fb.group({ + uname: [''], + pass: [''], + }) + } + + ngOnInit() { + if (this.ls.loggedIn) { + this.router.navigateByUrl(this.ac.redirect || 'app') + } + } + + submit() { + const val = this.form.value + this.ac + .login(val.uname, val.pass) + .pipe( + catchError((err) => { + this.error = err.error.message + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + this.ls.loggedIn = true + this.router.navigateByUrl(this.ac.redirect || 'app') + if (data.admin) { + this.ls.admin = data.admin + } + }) + } +} diff --git a/frontend/src/app/services/app-update.service.spec.ts b/frontend/src/app/services/app-update.service.spec.ts new file mode 100644 index 0000000..d388120 --- /dev/null +++ b/frontend/src/app/services/app-update.service.spec.ts @@ -0,0 +1,65 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AppUpdateService } from './app-update.service' +import { SwUpdate } from '@angular/service-worker' +import { of } from 'rxjs' +import { MATERIAL_ANIMATIONS } from '@angular/material/core' +import { HarnessLoader } from '@angular/cdk/testing' +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' +import { MatSnackBarHarness } from "@angular/material/snack-bar/testing"; +import { Component } from '@angular/core' + +@Component({ + standalone: false, +}) +class BlankPage { } + +describe('AppUpdateService', () => { + let fixture: ComponentFixture + let loader: HarnessLoader + let service: AppUpdateService + let swuMock: jasmine.SpyObj + + beforeEach(() => { + swuMock = jasmine.createSpyObj("SwUpdate", { + checkForUpdate: new Promise((resolve) => resolve(false)), + }, { + versionUpdates: of() + }) + TestBed.configureTestingModule({ + declarations: [BlankPage], + providers: [ + { provide: SwUpdate, useValue: swuMock }, + { provide: MATERIAL_ANIMATIONS, useValue: { animationsDisabled: true } } + ] + }).compileComponents() + fixture = TestBed.createComponent(BlankPage) + fixture.detectChanges() + service = TestBed.inject(AppUpdateService) + loader = TestbedHarnessEnvironment.documentRootLoader(fixture) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) + + it('should load harness for snackbar', async () => { + service.promptUser() + const snackBars = await loader.getAllHarnesses(MatSnackBarHarness); + + expect(snackBars.length).toBe(1) + }) + + it('should be able to get snackbar content', async () => { + service.promptUser() + const snackBar = await loader.getHarness(MatSnackBarHarness) + expect(await snackBar.getMessage()).toBeDefined() + }) + + it('should be able to get snackbar action', async () => { + service.promptUser() + const snackBar = await loader.getHarness(MatSnackBarHarness) + expect(await snackBar.getActionDescription()).toBeDefined() + }) + +}) diff --git a/frontend/src/app/services/app-update.service.ts b/frontend/src/app/services/app-update.service.ts new file mode 100644 index 0000000..ed4be1e --- /dev/null +++ b/frontend/src/app/services/app-update.service.ts @@ -0,0 +1,71 @@ +import { ApplicationRef, inject, Injectable } from '@angular/core' +import { MatSnackBar } from '@angular/material/snack-bar' +import { SwUpdate } from '@angular/service-worker' +import { + catchError, + concat, + first, + from, + interval, + tap, + throwError, +} from 'rxjs' + +@Injectable({ + providedIn: 'root', +}) +export class AppUpdateService { + readonly appRef = inject(ApplicationRef) + update = inject(SwUpdate) + readonly sb = inject(MatSnackBar) + + constructor() { + this.update.versionUpdates.subscribe(evt => { + switch (evt.type) { + case 'VERSION_DETECTED': + console.log(`Downloading ${evt.version.hash}`) + break + case 'VERSION_READY': + console.log( + `Current: ${evt.currentVersion.hash}, new: ${evt.latestVersion.hash}` + ) + break + case 'VERSION_INSTALLATION_FAILED': + console.error(`Failed to install ${evt.version.hash}: ${evt.error}`) + break + } + }) + const appIsStable = this.appRef.isStable.pipe( + first(isStable => isStable === true) + ) + const everyday = interval(1000 * 60 * 60 * 24) + const stableDay = concat(appIsStable, everyday) + + stableDay.subscribe(() => this.checkForUpdate()) + } + + checkForUpdate() { + const ov = from(this.update.checkForUpdate()) + return ov + .pipe( + catchError(err => { + console.error('Failed to check for updates:', err) + return throwError(() => err) + }) + ) + .pipe( + tap(val => { + if (val) this.promptUser() + }) + ) + } + + promptUser() { + this.sb + .open('Dostępna jest aktualizacja', 'Restart') + .onAction() + .subscribe(() => { + document.location.reload() + }) + } +} diff --git a/frontend/src/app/services/auth.client.spec.ts b/frontend/src/app/services/auth.client.spec.ts new file mode 100644 index 0000000..6d36786 --- /dev/null +++ b/frontend/src/app/services/auth.client.spec.ts @@ -0,0 +1,33 @@ +import { TestBed } from '@angular/core/testing' + +import { AuthClient } from './auth.client' +import { + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' + +describe('AuthService', () => { + let service: AuthClient + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let httpTestingController: HttpTestingController + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }) + service = TestBed.inject(AuthClient) + httpTestingController = TestBed.inject(HttpTestingController) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/frontend/src/app/services/auth.client.ts b/frontend/src/app/services/auth.client.ts new file mode 100644 index 0000000..8f97492 --- /dev/null +++ b/frontend/src/app/services/auth.client.ts @@ -0,0 +1,117 @@ +import { HttpClient } from '@angular/common/http' +import { inject, Injectable } from '@angular/core' +import { Router } from '@angular/router' +import { catchError, EMPTY, firstValueFrom, tap, throwError } from 'rxjs' +import { environment } from 'src/environments/environment' +import { LocalStorageService } from './local-storage.service' +import { Status } from '../types/status' +import { User } from '../types/user' +import { Capabilities } from '../types/capability' +import _ from "underscore" +import { MenuTags } from '../types/menu' + +@Injectable({ + providedIn: 'root', +}) +export class AuthClient { + private http = inject(HttpClient) + private router = inject(Router) + private ls = inject(LocalStorageService) + + private _redirect = '' + public get redirect(): string { + return this._redirect + } + public set redirect(value: string) { + this._redirect = value + this.putRedirect(value).subscribe() + } + + public login(uname: string, pass: string) { + return this.http + .post( + environment.apiEndpoint + '/auth/login', + { + username: uname, + password: pass, + }, + { withCredentials: true } + ) + .pipe( + tap(v => { + if (v.redirect) this._redirect = v.redirect + }) + ) + } + + public async logout() { + const res = await firstValueFrom( + this.http.delete(environment.apiEndpoint + '/auth/logout', { + withCredentials: true, + }) + ) + + if (res.status === 200) { + this.router.navigateByUrl('/login') + this.ls.logOut() + } + } + + public check() { + this.http + .get<{ + admin?: string[] + user: User, + features: Capabilities + menu: { + defaultItems: { + sn: string[] + kol: string[] + }, + tags: MenuTags + } + vapid: string + }>(environment.apiEndpoint + '/auth/check', { withCredentials: true }) + .pipe( + catchError(err => { + if (err.status == 401 && this.ls.loggedIn) { + this.ls.logOut() + this.router.navigateByUrl('/login') + return EMPTY + } + return throwError(() => new Error(err.message)) + }) + ) + .subscribe(data => { + this.ls.admin = data.admin + if (!_.isEqual(this.ls.capFlag, data.features)) { + this.ls.capFlag = data.features + window.location.reload() + } + this.ls.user = data.user + this.ls.vapid = data.vapid + if (data.menu.defaultItems) { + this.ls.defaultItems = data.menu.defaultItems + } + if (data.menu.tags) { + this.ls.menuTags = data.menu.tags + } + }) + } + + public chpass(oldpass: string, newpass: string) { + return this.http.post( + environment.apiEndpoint + '/auth/chpass', + { oldPass: oldpass, newPass: newpass }, + { withCredentials: true, responseType: 'text' } + ) + } + + private putRedirect(redirect: string) { + return this.http.put( + environment.apiEndpoint + '/auth/redirect', + { redirect: redirect }, + { withCredentials: true } + ) + } +} diff --git a/frontend/src/app/services/local-storage.service.spec.ts b/frontend/src/app/services/local-storage.service.spec.ts new file mode 100644 index 0000000..2517e92 --- /dev/null +++ b/frontend/src/app/services/local-storage.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing' + +import { LocalStorageService } from './local-storage.service' + +describe('LocalStorageService', () => { + let service: LocalStorageService + + beforeEach(() => { + TestBed.configureTestingModule({}) + service = TestBed.inject(LocalStorageService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) +}) diff --git a/frontend/src/app/services/local-storage.service.ts b/frontend/src/app/services/local-storage.service.ts new file mode 100644 index 0000000..88ebc71 --- /dev/null +++ b/frontend/src/app/services/local-storage.service.ts @@ -0,0 +1,160 @@ +import { effect, Injectable, signal } from '@angular/core' +import { User } from '../types/user' +import { Capabilities } from '../types/capability' +import { MenuTags } from '../types/menu' + +@Injectable({ + providedIn: 'root', +}) +export class LocalStorageService { + get user() { + return JSON.parse(localStorage.getItem('user')!) ?? undefined + } + + set user(value: User | undefined) { + if (!value) { + localStorage.removeItem('user') + } else { + localStorage.setItem('user', JSON.stringify(value)) + } + } + + permChecker(perm: string) { + return this.admin?.includes(perm) ?? false + } + + logOut() { + localStorage.clear() + } + + public hasRoom() { + if (this.room) { + return true + } else { + return false + } + } + + get room(): string | undefined { + return this.user?.room + } + + get loggedIn() { + if (localStorage.getItem('loggedIn')) { + return true + } + return + } + + set loggedIn(is: true | undefined) { + if (is) { + localStorage.setItem('loggedIn', 'true') + } else { + localStorage.removeItem('loggedIn') + } + } + + set admin(perms: string[] | undefined) { + if (perms && perms.length != 0) { + localStorage.setItem('admin', JSON.stringify(perms)) + } else { + localStorage.removeItem('admin') + } + } + + get admin() { + const lsa = localStorage.getItem('admin') + return lsa ? JSON.parse(lsa) : undefined + } + + get isAdmin(): boolean { + return this.admin ? true : false + } + + set capFlag(n: Capabilities) { + if (n) { + localStorage.setItem('cap', JSON.stringify(n)) + } else { + localStorage.removeItem('cap') + } + } + + get capFlag() { + const capItem = localStorage.getItem('cap') + if (capItem) { + const cap = JSON.parse(capItem) + if (typeof cap == "object") { + return cap + } else { + localStorage.removeItem('cap') + } + } + return { + clean: false, + groups: false, + key: false, + menu: false, + news: false, + notif: false + } + } + + public capCheck(perm: keyof Capabilities) { + return perm in this.capFlag ? this.capFlag[perm] : false + } + + public get defaultItems(): { sn: string[]; kol: string[] } { + const di = localStorage.getItem('defaultItems') + if (di) { + return JSON.parse(di) + } else { + return { sn: [], kol: [] } + } + } + + public set defaultItems(value: { sn: string[]; kol: string[] }) { + localStorage.setItem('defaultItems', JSON.stringify(value)) + } + + public get newsDate(): Date { + return new Date(localStorage.getItem("newsDate")!) + } + + public set newsDate(value: string) { + localStorage.setItem("newsDate", value) + } + + public newsFlag = signal(this._getNewsFlag()) + private syncNewsFlag = effect(() => this._setNewsFlag) + + private _getNewsFlag(): number | null { + const item = localStorage.getItem("newsFlag") + if (item) { + return Number(item) + } else { + return null + } + } + + private _setNewsFlag(value: number | null) { + if (value) { + localStorage.setItem("newsFlag", value.toString()) + } else { + localStorage.removeItem("newsFlag") + } + } + + public get vapid(): string { + return localStorage.getItem('vapid')! + } + public set vapid(value: string) { + localStorage.setItem('vapid', value) + } + + get menuTags() : MenuTags { + return JSON.parse(localStorage.getItem('menuTags') || '{}') + } + set menuTags(v: MenuTags) { + localStorage.setItem('menuTags', JSON.stringify(v)) + } +} diff --git a/frontend/src/app/services/sync.service.spec.ts b/frontend/src/app/services/sync.service.spec.ts new file mode 100644 index 0000000..935ae1b --- /dev/null +++ b/frontend/src/app/services/sync.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SyncService } from './sync.service'; + +describe('SyncService', () => { + let service: SyncService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SyncService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/sync.service.ts b/frontend/src/app/services/sync.service.ts new file mode 100644 index 0000000..3d6b4f1 --- /dev/null +++ b/frontend/src/app/services/sync.service.ts @@ -0,0 +1,59 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, inject, Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { repeat, Subject } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { LocalStorageService } from './local-storage.service'; +import { UpdatesService } from './updates.service'; +import { NotifDialogComponent } from '../app-view/notif-dialog/notif-dialog.component'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatIconModule } from '@angular/material/icon'; + +@Component({ + standalone: true, + template: "cloud_off Tryb offline", + styles: ":host {display: flex; gap: 1ch}", + imports: [MatIconModule] +}) +class OfflineMessageBar {} + +@Injectable({ + providedIn: 'root' +}) +export class SyncService { + private http = inject(HttpClient) + private dialog = inject(MatDialog) + private ls = inject(LocalStorageService) + private us = inject(UpdatesService) + private sb = inject(MatSnackBar) + + subscribe() { + this.http.get(environment.apiEndpoint + "/sync", { withCredentials: true }).pipe(repeat()).subscribe({ + next: v => { + switch (v.type) { + case "notif": + this.notifEvents.next() + break; + default: + break; + } + }, + error: v => { + this.sb.openFromComponent(OfflineMessageBar, {duration: 2000}) + } + }) + } + + public notifEvents = new Subject() + + notifCheck() { + if (this.ls.capCheck("notif")) { + this.us.getNotifCheck().subscribe(s => { + s.forEach(v => { + this.dialog.open(NotifDialogComponent, { data: v }) + }) + }) + } + } + +} diff --git a/frontend/src/app/services/updates.service.spec.ts b/frontend/src/app/services/updates.service.spec.ts new file mode 100644 index 0000000..e7a5377 --- /dev/null +++ b/frontend/src/app/services/updates.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing' + +import { UpdatesService } from './updates.service' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' +import { + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http' +import { environment } from 'src/environments/environment' +import { firstValueFrom } from 'rxjs' + +describe('UpdatesService', () => { + let service: UpdatesService + let httpTestingController: HttpTestingController + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }) + httpTestingController = TestBed.inject(HttpTestingController) + service = TestBed.inject(UpdatesService) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) + + it('should grab news', () => { + const res = firstValueFrom(service.getNews()) + const req = httpTestingController.expectOne(environment.apiEndpoint+`/app/news`) + + expect(res).withContext("empty news array").toBeDefined() + + req.flush([]) + httpTestingController.verify() + }) +}) diff --git a/frontend/src/app/services/updates.service.ts b/frontend/src/app/services/updates.service.ts new file mode 100644 index 0000000..0bdc6d8 --- /dev/null +++ b/frontend/src/app/services/updates.service.ts @@ -0,0 +1,104 @@ +import { inject, Injectable, signal } from '@angular/core' +import { HttpClient } from '@angular/common/http' +import { Menu } from '../types/menu' +import { environment } from 'src/environments/environment' +import { News } from '../types/news.model' +import { UKey } from '../types/key' +import { Status } from '../types/status' +import { DateTime } from 'luxon' +import { catchError, of, ReplaySubject } from 'rxjs' +import { STATE } from '../types/state' + +@Injectable({ + providedIn: 'root', +}) +export class UpdatesService { + private http = inject(HttpClient) + + public news = new ReplaySubject(1) + public newsState = signal(STATE.NOT_LOADED) + + getNews() { + this.newsState.set(STATE.PENDING) + this.http.get(environment.apiEndpoint + '/app/news', { withCredentials: true }) + .pipe( + catchError((err: Error) => { + this.newsState.set(STATE.ERROR) + return of() + })) + .subscribe(v => { + this.newsState.set(STATE.LOADED) + this.news.next(v) + }) + } + + newsCheck() { + return this.http.get<{ hash: string; count: number }>( + environment.apiEndpoint + `/app/news/check`, + { withCredentials: true } + ) + } + + getMenu(dom: DateTime) { + const headers = { + 'Content-Type': 'application/json', + } + return this.http.get(environment.apiEndpoint + `/app/menu/${dom.toISODate()}`, { + headers: headers, + withCredentials: true, + }) + } + + getVote(date: DateTime) { + return this.http.get(environment.apiEndpoint + `/app/menu/${date.toISODate()}/stat`, {withCredentials: true}) + } + + postVote(date: DateTime, vote: any[]) { + const all = vote.map(element => { + return { + ...element + } + }); + return this.http.post( + environment.apiEndpoint + `/app/menu/${date.toUTC(undefined, {keepLocalTime: true}).toISODate()}`, + all, + { withCredentials: true } + ) + } + + postNotif(nd: object) { + const headers = { + 'Content-Type': 'application/json', + } + this.http + .post(environment.apiEndpoint + `/notif`, nd, { + headers: headers, + withCredentials: true, + }) + .subscribe() + } + + getKeys() { + return this.http.get(environment.apiEndpoint + `/app/keys`, { + withCredentials: true, + }) + } + + getNotifCheck() { + return this.http.get< + { + _id: string + message: { title: string; body: string } + sentDate: string + }[] + >(environment.apiEndpoint + `/app/notif/check`, { withCredentials: true }) + } + + postInfoAck(id: string) { + return this.http.post( + environment.apiEndpoint + `/app/notif/${id}/ack`, + undefined, + { withCredentials: true } + ) + } +} diff --git a/frontend/src/app/types/capability.ts b/frontend/src/app/types/capability.ts new file mode 100644 index 0000000..2053fc9 --- /dev/null +++ b/frontend/src/app/types/capability.ts @@ -0,0 +1,8 @@ +export interface Capabilities { + news: boolean, + menu: boolean, + notif: boolean, + groups: boolean, + clean: boolean, + key: boolean + } \ No newline at end of file diff --git a/frontend/src/app/types/clean-note.ts b/frontend/src/app/types/clean-note.ts new file mode 100644 index 0000000..bfadb70 --- /dev/null +++ b/frontend/src/app/types/clean-note.ts @@ -0,0 +1,4 @@ +export interface CleanNote { + label: string + weight: number +} diff --git a/frontend/src/app/types/group.ts b/frontend/src/app/types/group.ts new file mode 100644 index 0000000..a52c79c --- /dev/null +++ b/frontend/src/app/types/group.ts @@ -0,0 +1,4 @@ +export interface Group { + _id: string + name: string +} diff --git a/frontend/src/app/types/key.ts b/frontend/src/app/types/key.ts new file mode 100644 index 0000000..19b2e74 --- /dev/null +++ b/frontend/src/app/types/key.ts @@ -0,0 +1,16 @@ +import { DateTime } from 'luxon' +import { User } from './user' + +export interface UKey { + room: string + taken: boolean +} + +export type AKeyAPI = Omit & {borrow: string, tb?: string} + +export interface AKey { + room: string + whom?: User + borrow: DateTime + tb?: DateTime +} diff --git a/frontend/src/app/types/link.ts b/frontend/src/app/types/link.ts new file mode 100644 index 0000000..3448884 --- /dev/null +++ b/frontend/src/app/types/link.ts @@ -0,0 +1,10 @@ +import { Signal } from "@angular/core" +import { MatBadge } from "@angular/material/badge" + +export interface Link { + title: string + href: string + icon?: string + enabled: boolean + badge?: Signal +} diff --git a/frontend/src/app/types/menu-stats.ts b/frontend/src/app/types/menu-stats.ts new file mode 100644 index 0000000..eb6fd79 --- /dev/null +++ b/frontend/src/app/types/menu-stats.ts @@ -0,0 +1,10 @@ +export interface MenuStats { + name: string; + type: "soup" | "other" | "kol" | "sn" | "ob" | "cd" | "dr"; + value: { + count: number; + rating: number; + tags: Record; + comments: string[]; + }; +} diff --git a/frontend/src/app/types/menu.ts b/frontend/src/app/types/menu.ts new file mode 100644 index 0000000..4668032 --- /dev/null +++ b/frontend/src/app/types/menu.ts @@ -0,0 +1,33 @@ +import { DateTime } from 'luxon' + +export type MenuAPI = Omit & { day: string } + +export interface Menu { + _id: string + day: DateTime + sn: { + fancy: string[] + second: string + } + ob: { + soup: string + vege: string + meal: string + condiments: string[] + drink: string + other: string[] + } + kol?: string + stat?: { kol?: string; ob?: string } + dayTitle?: string +} + +export interface MenuTags { + sn: string[], + ob: string[], + kol: string[], + soup: string[], + cd: string[], + dr: string[], + other: void[] +} diff --git a/frontend/src/app/types/news.model.ts b/frontend/src/app/types/news.model.ts new file mode 100644 index 0000000..6f833c2 --- /dev/null +++ b/frontend/src/app/types/news.model.ts @@ -0,0 +1,13 @@ +export interface News { + _id: string + title: string + content: string + date: string + visible?: boolean + pinned?: boolean, + author: { + fname?: string, + surname?: string, + uname: string + } +} diff --git a/frontend/src/app/types/notification.ts b/frontend/src/app/types/notification.ts new file mode 100644 index 0000000..e2405e5 --- /dev/null +++ b/frontend/src/app/types/notification.ts @@ -0,0 +1,9 @@ +export interface Notification { + body: string + title: string + recp: { + uid: string | null + room: string | null + type: 'all' | 'room' | 'uname' + } +} diff --git a/frontend/src/app/types/state.ts b/frontend/src/app/types/state.ts new file mode 100644 index 0000000..986a3c4 --- /dev/null +++ b/frontend/src/app/types/state.ts @@ -0,0 +1,6 @@ +export enum STATE { + NOT_LOADED, + PENDING, + LOADED, + ERROR +} diff --git a/frontend/src/app/types/status.ts b/frontend/src/app/types/status.ts new file mode 100644 index 0000000..8a563fa --- /dev/null +++ b/frontend/src/app/types/status.ts @@ -0,0 +1,4 @@ +export interface Status { + status: number + message?: string +} diff --git a/frontend/src/app/types/user.ts b/frontend/src/app/types/user.ts new file mode 100644 index 0000000..1b51ace --- /dev/null +++ b/frontend/src/app/types/user.ts @@ -0,0 +1,7 @@ +export interface User { + _id: string + fname: string + surname: string + uname: string + room: string +} diff --git a/frontend/src/app/util.ts b/frontend/src/app/util.ts new file mode 100644 index 0000000..a902b43 --- /dev/null +++ b/frontend/src/app/util.ts @@ -0,0 +1,29 @@ +import { DateFilterFn } from "@angular/material/datepicker" +import { DateTime } from "luxon" + +export const weekendFilter: DateFilterFn = ( + date: DateTime | null +): boolean => { + const day = date?.weekday + return day !== 6 && day !== 7 +} + +export function filterLook(filter: DateFilterFn, ab: "ahead" | "behind" = "ahead", date: DateTime = DateTime.now(), maxSearchDays = 365): DateTime | null { + let currentDate = date + switch (ab) { + case "ahead": + for (let i = 0; i < maxSearchDays; i++) { + if (filter(currentDate)) return currentDate + currentDate = currentDate.plus({day: 1}) + } + return null; + case "behind": + for (let i = 0; i < maxSearchDays; i++) { + if (filter(currentDate)) return currentDate + currentDate = currentDate.minus({day: 1}) + } + return null; + default: + return null; + } +} diff --git a/frontend/src/assets/.gitkeep b/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/icons/icon-144x144.png b/frontend/src/assets/icons/icon-144x144.png new file mode 100644 index 0000000..098999b Binary files /dev/null and b/frontend/src/assets/icons/icon-144x144.png differ diff --git a/frontend/src/assets/icons/icon-192x192.png b/frontend/src/assets/icons/icon-192x192.png new file mode 100644 index 0000000..66d7f10 Binary files /dev/null and b/frontend/src/assets/icons/icon-192x192.png differ diff --git a/frontend/src/assets/icons/icon-48x48.png b/frontend/src/assets/icons/icon-48x48.png new file mode 100644 index 0000000..690f09c Binary files /dev/null and b/frontend/src/assets/icons/icon-48x48.png differ diff --git a/frontend/src/assets/icons/icon-512x512.png b/frontend/src/assets/icons/icon-512x512.png new file mode 100644 index 0000000..7ad8f07 Binary files /dev/null and b/frontend/src/assets/icons/icon-512x512.png differ diff --git a/frontend/src/assets/icons/icon-72x72.png b/frontend/src/assets/icons/icon-72x72.png new file mode 100644 index 0000000..e86c6b8 Binary files /dev/null and b/frontend/src/assets/icons/icon-72x72.png differ diff --git a/frontend/src/assets/icons/icon-96x96.png b/frontend/src/assets/icons/icon-96x96.png new file mode 100644 index 0000000..51e9721 Binary files /dev/null and b/frontend/src/assets/icons/icon-96x96.png differ diff --git a/frontend/src/environments/environment.development.ts b/frontend/src/environments/environment.development.ts new file mode 100644 index 0000000..9b3577f --- /dev/null +++ b/frontend/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + apiEndpoint: `http://${window.location.hostname}:8080`, + version: 'testing', + production: false, +} diff --git a/frontend/src/environments/environment.swdev.ts b/frontend/src/environments/environment.swdev.ts new file mode 100644 index 0000000..0c9d072 --- /dev/null +++ b/frontend/src/environments/environment.swdev.ts @@ -0,0 +1,5 @@ +export const environment = { + apiEndpoint: `http://localhost:12230`, + version: 'testing (swDev)', + production: true, +} diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 0000000..c8ddf0d --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + apiEndpoint: `${window.location.origin}/api`, + version: 'v2.1.0', + production: true, +} diff --git a/frontend/src/favicon.ico b/frontend/src/favicon.ico new file mode 100644 index 0000000..b6737ad Binary files /dev/null and b/frontend/src/favicon.ico differ diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..7d25f95 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,18 @@ + + + + + Internat + + + + + + + + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..56c38f2 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,13 @@ +import { platformBrowser } from '@angular/platform-browser' + +import { AppModule } from './app/app.module' +import { environment } from './environments/environment' + +platformBrowser() + .bootstrapModule(AppModule) + .then(() => { + if ('serviceWorker' in navigator && environment.production) { + navigator.serviceWorker.register('./ngsw-worker.js') + } + }) + .catch(err => console.error(err)) diff --git a/frontend/src/manifest.webmanifest b/frontend/src/manifest.webmanifest new file mode 100644 index 0000000..12a6e05 --- /dev/null +++ b/frontend/src/manifest.webmanifest @@ -0,0 +1,47 @@ +{ + "name": "Internat: Jadłospis i ogłoszenia", + "short_name": "Internat", + "theme_color": "#2e46a4", + "background_color": "#ffffff", + "display": "standalone", + "scope": "./", + "start_url": "./", + "icons": [ + { + "src": "assets/icons/icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "assets/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "assets/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "assets/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "assets/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "assets/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable any" + } + ] +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss new file mode 100644 index 0000000..75c42dc --- /dev/null +++ b/frontend/src/styles.scss @@ -0,0 +1,64 @@ +// Custom Theming for Angular Material +// For more information: https://material.angular.io/guide/theming +@use "@angular/material" as mat; +@use "theme-colors" as theme; + +html { + color-scheme: light dark; + height: 100%; + overscroll-behavior-y: contain; + @include mat.theme( + ( + color: theme.$primary-palette, + typography: Roboto, + density: 0, + ) + ); +} + +body { + background: var(--mat-sys-surface); + color: var(--mat-sys-on-surface); + margin: 0; + height: 100%; + font-family: Roboto, "Helvetica Neue", sans-serif; +} + +.error-color { + @include mat.expansion-overrides( + ( + container-background-color: var(--mat-sys-error-container), + container-text-color: var(--mat-sys-on-error-container), + ) + ); + @include mat.button-overrides( + ( + filled-container-color: var(--mat-sys-error), + outlined-outline-color: var(--mat-sys-error), + filled-label-text-color: var(--mat-sys-on-error), + outlined-label-text-color: var(--mat-sys-error), + text-label-text-color: var(--mat-sys-error) + ) + ); + @include mat.fab-overrides( + ( + container-color: var(--mat-sys-error), + small-container-color: var(--mat-sys-error), + foreground-color: var(--mat-sys-on-error), + small-foreground-color: var(--mat-sys-on-error), + ) + ); +} + +mat-icon +{ + font-variation-settings: 'FILL' 1; + + &.error-color { + @include mat.icon-overrides( + ( + color: var(--mat-sys-error), + ) + ); + } +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..ec26f70 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,16 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "@angular/localize" + ] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..f2112f4 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +}