Upload folder using huggingface_hub
Browse files- .env.template +2 -0
- .gitignore +9 -0
- Dockerfile +27 -0
- LICENSE +674 -0
- app/auth.py +15 -0
- app/config.py +21 -0
- app/deploy.py +111 -0
- app/exceptions.py +18 -0
- app/models.py +19 -0
- app/routes.py +104 -0
- app/static/i18n.js +307 -0
- app/static/style.css +737 -0
- app/store.py +43 -0
- app/templates/base.html +216 -0
- app/templates/deploy_status.html +430 -0
- app/templates/index.html +817 -0
- app/utils.py +6 -0
- images/img0.png +0 -0
- images/img1.png +0 -0
- images/img2.png +0 -0
- main.py +17 -0
- requirements.txt +11 -0
.env.template
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
REDIS_URL=redis://localhost:6379
|
2 |
+
API_KEY=123456
|
.gitignore
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.DS_Store
|
2 |
+
__pycache__
|
3 |
+
test/
|
4 |
+
venv/
|
5 |
+
.github/
|
6 |
+
.idea
|
7 |
+
.env
|
8 |
+
*.log
|
9 |
+
*.md
|
Dockerfile
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# 使用Python 3.11作为基础镜像
|
3 |
+
FROM python:3.11-slim
|
4 |
+
|
5 |
+
# 设置工作目录
|
6 |
+
WORKDIR /app
|
7 |
+
|
8 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
9 |
+
ENV PYTHONUNBUFFERED=1
|
10 |
+
|
11 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
12 |
+
git \
|
13 |
+
&& apt-get clean \
|
14 |
+
&& rm -rf /var/lib/apt/lists/*
|
15 |
+
|
16 |
+
COPY requirements.txt .
|
17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
18 |
+
|
19 |
+
COPY . .
|
20 |
+
|
21 |
+
RUN adduser --disabled-password --gecos "" appuser
|
22 |
+
USER appuser
|
23 |
+
|
24 |
+
EXPOSE 7860
|
25 |
+
|
26 |
+
# 启动应用
|
27 |
+
CMD ["python", "main.py"]
|
LICENSE
ADDED
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
GNU GENERAL PUBLIC LICENSE
|
2 |
+
Version 3, 29 June 2007
|
3 |
+
|
4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
6 |
+
of this license document, but changing it is not allowed.
|
7 |
+
|
8 |
+
Preamble
|
9 |
+
|
10 |
+
The GNU General Public License is a free, copyleft license for
|
11 |
+
software and other kinds of works.
|
12 |
+
|
13 |
+
The licenses for most software and other practical works are designed
|
14 |
+
to take away your freedom to share and change the works. By contrast,
|
15 |
+
the GNU General Public License is intended to guarantee your freedom to
|
16 |
+
share and change all versions of a program--to make sure it remains free
|
17 |
+
software for all its users. We, the Free Software Foundation, use the
|
18 |
+
GNU General Public License for most of our software; it applies also to
|
19 |
+
any other work released this way by its authors. You can apply it to
|
20 |
+
your programs, too.
|
21 |
+
|
22 |
+
When we speak of free software, we are referring to freedom, not
|
23 |
+
price. Our General Public Licenses are designed to make sure that you
|
24 |
+
have the freedom to distribute copies of free software (and charge for
|
25 |
+
them if you wish), that you receive source code or can get it if you
|
26 |
+
want it, that you can change the software or use pieces of it in new
|
27 |
+
free programs, and that you know you can do these things.
|
28 |
+
|
29 |
+
To protect your rights, we need to prevent others from denying you
|
30 |
+
these rights or asking you to surrender the rights. Therefore, you have
|
31 |
+
certain responsibilities if you distribute copies of the software, or if
|
32 |
+
you modify it: responsibilities to respect the freedom of others.
|
33 |
+
|
34 |
+
For example, if you distribute copies of such a program, whether
|
35 |
+
gratis or for a fee, you must pass on to the recipients the same
|
36 |
+
freedoms that you received. You must make sure that they, too, receive
|
37 |
+
or can get the source code. And you must show them these terms so they
|
38 |
+
know their rights.
|
39 |
+
|
40 |
+
Developers that use the GNU GPL protect your rights with two steps:
|
41 |
+
(1) assert copyright on the software, and (2) offer you this License
|
42 |
+
giving you legal permission to copy, distribute and/or modify it.
|
43 |
+
|
44 |
+
For the developers' and authors' protection, the GPL clearly explains
|
45 |
+
that there is no warranty for this free software. For both users' and
|
46 |
+
authors' sake, the GPL requires that modified versions be marked as
|
47 |
+
changed, so that their problems will not be attributed erroneously to
|
48 |
+
authors of previous versions.
|
49 |
+
|
50 |
+
Some devices are designed to deny users access to install or run
|
51 |
+
modified versions of the software inside them, although the manufacturer
|
52 |
+
can do so. This is fundamentally incompatible with the aim of
|
53 |
+
protecting users' freedom to change the software. The systematic
|
54 |
+
pattern of such abuse occurs in the area of products for individuals to
|
55 |
+
use, which is precisely where it is most unacceptable. Therefore, we
|
56 |
+
have designed this version of the GPL to prohibit the practice for those
|
57 |
+
products. If such problems arise substantially in other domains, we
|
58 |
+
stand ready to extend this provision to those domains in future versions
|
59 |
+
of the GPL, as needed to protect the freedom of users.
|
60 |
+
|
61 |
+
Finally, every program is threatened constantly by software patents.
|
62 |
+
States should not allow patents to restrict development and use of
|
63 |
+
software on general-purpose computers, but in those that do, we wish to
|
64 |
+
avoid the special danger that patents applied to a free program could
|
65 |
+
make it effectively proprietary. To prevent this, the GPL assures that
|
66 |
+
patents cannot be used to render the program non-free.
|
67 |
+
|
68 |
+
The precise terms and conditions for copying, distribution and
|
69 |
+
modification follow.
|
70 |
+
|
71 |
+
TERMS AND CONDITIONS
|
72 |
+
|
73 |
+
0. Definitions.
|
74 |
+
|
75 |
+
"This License" refers to version 3 of the GNU General Public License.
|
76 |
+
|
77 |
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
78 |
+
works, such as semiconductor masks.
|
79 |
+
|
80 |
+
"The Program" refers to any copyrightable work licensed under this
|
81 |
+
License. Each licensee is addressed as "you". "Licensees" and
|
82 |
+
"recipients" may be individuals or organizations.
|
83 |
+
|
84 |
+
To "modify" a work means to copy from or adapt all or part of the work
|
85 |
+
in a fashion requiring copyright permission, other than the making of an
|
86 |
+
exact copy. The resulting work is called a "modified version" of the
|
87 |
+
earlier work or a work "based on" the earlier work.
|
88 |
+
|
89 |
+
A "covered work" means either the unmodified Program or a work based
|
90 |
+
on the Program.
|
91 |
+
|
92 |
+
To "propagate" a work means to do anything with it that, without
|
93 |
+
permission, would make you directly or secondarily liable for
|
94 |
+
infringement under applicable copyright law, except executing it on a
|
95 |
+
computer or modifying a private copy. Propagation includes copying,
|
96 |
+
distribution (with or without modification), making available to the
|
97 |
+
public, and in some countries other activities as well.
|
98 |
+
|
99 |
+
To "convey" a work means any kind of propagation that enables other
|
100 |
+
parties to make or receive copies. Mere interaction with a user through
|
101 |
+
a computer network, with no transfer of a copy, is not conveying.
|
102 |
+
|
103 |
+
An interactive user interface displays "Appropriate Legal Notices"
|
104 |
+
to the extent that it includes a convenient and prominently visible
|
105 |
+
feature that (1) displays an appropriate copyright notice, and (2)
|
106 |
+
tells the user that there is no warranty for the work (except to the
|
107 |
+
extent that warranties are provided), that licensees may convey the
|
108 |
+
work under this License, and how to view a copy of this License. If
|
109 |
+
the interface presents a list of user commands or options, such as a
|
110 |
+
menu, a prominent item in the list meets this criterion.
|
111 |
+
|
112 |
+
1. Source Code.
|
113 |
+
|
114 |
+
The "source code" for a work means the preferred form of the work
|
115 |
+
for making modifications to it. "Object code" means any non-source
|
116 |
+
form of a work.
|
117 |
+
|
118 |
+
A "Standard Interface" means an interface that either is an official
|
119 |
+
standard defined by a recognized standards body, or, in the case of
|
120 |
+
interfaces specified for a particular programming language, one that
|
121 |
+
is widely used among developers working in that language.
|
122 |
+
|
123 |
+
The "System Libraries" of an executable work include anything, other
|
124 |
+
than the work as a whole, that (a) is included in the normal form of
|
125 |
+
packaging a Major Component, but which is not part of that Major
|
126 |
+
Component, and (b) serves only to enable use of the work with that
|
127 |
+
Major Component, or to implement a Standard Interface for which an
|
128 |
+
implementation is available to the public in source code form. A
|
129 |
+
"Major Component", in this context, means a major essential component
|
130 |
+
(kernel, window system, and so on) of the specific operating system
|
131 |
+
(if any) on which the executable work runs, or a compiler used to
|
132 |
+
produce the work, or an object code interpreter used to run it.
|
133 |
+
|
134 |
+
The "Corresponding Source" for a work in object code form means all
|
135 |
+
the source code needed to generate, install, and (for an executable
|
136 |
+
work) run the object code and to modify the work, including scripts to
|
137 |
+
control those activities. However, it does not include the work's
|
138 |
+
System Libraries, or general-purpose tools or generally available free
|
139 |
+
programs which are used unmodified in performing those activities but
|
140 |
+
which are not part of the work. For example, Corresponding Source
|
141 |
+
includes interface definition files associated with source files for
|
142 |
+
the work, and the source code for shared libraries and dynamically
|
143 |
+
linked subprograms that the work is specifically designed to require,
|
144 |
+
such as by intimate data communication or control flow between those
|
145 |
+
subprograms and other parts of the work.
|
146 |
+
|
147 |
+
The Corresponding Source need not include anything that users
|
148 |
+
can regenerate automatically from other parts of the Corresponding
|
149 |
+
Source.
|
150 |
+
|
151 |
+
The Corresponding Source for a work in source code form is that
|
152 |
+
same work.
|
153 |
+
|
154 |
+
2. Basic Permissions.
|
155 |
+
|
156 |
+
All rights granted under this License are granted for the term of
|
157 |
+
copyright on the Program, and are irrevocable provided the stated
|
158 |
+
conditions are met. This License explicitly affirms your unlimited
|
159 |
+
permission to run the unmodified Program. The output from running a
|
160 |
+
covered work is covered by this License only if the output, given its
|
161 |
+
content, constitutes a covered work. This License acknowledges your
|
162 |
+
rights of fair use or other equivalent, as provided by copyright law.
|
163 |
+
|
164 |
+
You may make, run and propagate covered works that you do not
|
165 |
+
convey, without conditions so long as your license otherwise remains
|
166 |
+
in force. You may convey covered works to others for the sole purpose
|
167 |
+
of having them make modifications exclusively for you, or provide you
|
168 |
+
with facilities for running those works, provided that you comply with
|
169 |
+
the terms of this License in conveying all material for which you do
|
170 |
+
not control copyright. Those thus making or running the covered works
|
171 |
+
for you must do so exclusively on your behalf, under your direction
|
172 |
+
and control, on terms that prohibit them from making any copies of
|
173 |
+
your copyrighted material outside their relationship with you.
|
174 |
+
|
175 |
+
Conveying under any other circumstances is permitted solely under
|
176 |
+
the conditions stated below. Sublicensing is not allowed; section 10
|
177 |
+
makes it unnecessary.
|
178 |
+
|
179 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
180 |
+
|
181 |
+
No covered work shall be deemed part of an effective technological
|
182 |
+
measure under any applicable law fulfilling obligations under article
|
183 |
+
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
184 |
+
similar laws prohibiting or restricting circumvention of such
|
185 |
+
measures.
|
186 |
+
|
187 |
+
When you convey a covered work, you waive any legal power to forbid
|
188 |
+
circumvention of technological measures to the extent such circumvention
|
189 |
+
is effected by exercising rights under this License with respect to
|
190 |
+
the covered work, and you disclaim any intention to limit operation or
|
191 |
+
modification of the work as a means of enforcing, against the work's
|
192 |
+
users, your or third parties' legal rights to forbid circumvention of
|
193 |
+
technological measures.
|
194 |
+
|
195 |
+
4. Conveying Verbatim Copies.
|
196 |
+
|
197 |
+
You may convey verbatim copies of the Program's source code as you
|
198 |
+
receive it, in any medium, provided that you conspicuously and
|
199 |
+
appropriately publish on each copy an appropriate copyright notice;
|
200 |
+
keep intact all notices stating that this License and any
|
201 |
+
non-permissive terms added in accord with section 7 apply to the code;
|
202 |
+
keep intact all notices of the absence of any warranty; and give all
|
203 |
+
recipients a copy of this License along with the Program.
|
204 |
+
|
205 |
+
You may charge any price or no price for each copy that you convey,
|
206 |
+
and you may offer support or warranty protection for a fee.
|
207 |
+
|
208 |
+
5. Conveying Modified Source Versions.
|
209 |
+
|
210 |
+
You may convey a work based on the Program, or the modifications to
|
211 |
+
produce it from the Program, in the form of source code under the
|
212 |
+
terms of section 4, provided that you also meet all of these conditions:
|
213 |
+
|
214 |
+
a) The work must carry prominent notices stating that you modified
|
215 |
+
it, and giving a relevant date.
|
216 |
+
|
217 |
+
b) The work must carry prominent notices stating that it is
|
218 |
+
released under this License and any conditions added under section
|
219 |
+
7. This requirement modifies the requirement in section 4 to
|
220 |
+
"keep intact all notices".
|
221 |
+
|
222 |
+
c) You must license the entire work, as a whole, under this
|
223 |
+
License to anyone who comes into possession of a copy. This
|
224 |
+
License will therefore apply, along with any applicable section 7
|
225 |
+
additional terms, to the whole of the work, and all its parts,
|
226 |
+
regardless of how they are packaged. This License gives no
|
227 |
+
permission to license the work in any other way, but it does not
|
228 |
+
invalidate such permission if you have separately received it.
|
229 |
+
|
230 |
+
d) If the work has interactive user interfaces, each must display
|
231 |
+
Appropriate Legal Notices; however, if the Program has interactive
|
232 |
+
interfaces that do not display Appropriate Legal Notices, your
|
233 |
+
work need not make them do so.
|
234 |
+
|
235 |
+
A compilation of a covered work with other separate and independent
|
236 |
+
works, which are not by their nature extensions of the covered work,
|
237 |
+
and which are not combined with it such as to form a larger program,
|
238 |
+
in or on a volume of a storage or distribution medium, is called an
|
239 |
+
"aggregate" if the compilation and its resulting copyright are not
|
240 |
+
used to limit the access or legal rights of the compilation's users
|
241 |
+
beyond what the individual works permit. Inclusion of a covered work
|
242 |
+
in an aggregate does not cause this License to apply to the other
|
243 |
+
parts of the aggregate.
|
244 |
+
|
245 |
+
6. Conveying Non-Source Forms.
|
246 |
+
|
247 |
+
You may convey a covered work in object code form under the terms
|
248 |
+
of sections 4 and 5, provided that you also convey the
|
249 |
+
machine-readable Corresponding Source under the terms of this License,
|
250 |
+
in one of these ways:
|
251 |
+
|
252 |
+
a) Convey the object code in, or embodied in, a physical product
|
253 |
+
(including a physical distribution medium), accompanied by the
|
254 |
+
Corresponding Source fixed on a durable physical medium
|
255 |
+
customarily used for software interchange.
|
256 |
+
|
257 |
+
b) Convey the object code in, or embodied in, a physical product
|
258 |
+
(including a physical distribution medium), accompanied by a
|
259 |
+
written offer, valid for at least three years and valid for as
|
260 |
+
long as you offer spare parts or customer support for that product
|
261 |
+
model, to give anyone who possesses the object code either (1) a
|
262 |
+
copy of the Corresponding Source for all the software in the
|
263 |
+
product that is covered by this License, on a durable physical
|
264 |
+
medium customarily used for software interchange, for a price no
|
265 |
+
more than your reasonable cost of physically performing this
|
266 |
+
conveying of source, or (2) access to copy the
|
267 |
+
Corresponding Source from a network server at no charge.
|
268 |
+
|
269 |
+
c) Convey individual copies of the object code with a copy of the
|
270 |
+
written offer to provide the Corresponding Source. This
|
271 |
+
alternative is allowed only occasionally and noncommercially, and
|
272 |
+
only if you received the object code with such an offer, in accord
|
273 |
+
with subsection 6b.
|
274 |
+
|
275 |
+
d) Convey the object code by offering access from a designated
|
276 |
+
place (gratis or for a charge), and offer equivalent access to the
|
277 |
+
Corresponding Source in the same way through the same place at no
|
278 |
+
further charge. You need not require recipients to copy the
|
279 |
+
Corresponding Source along with the object code. If the place to
|
280 |
+
copy the object code is a network server, the Corresponding Source
|
281 |
+
may be on a different server (operated by you or a third party)
|
282 |
+
that supports equivalent copying facilities, provided you maintain
|
283 |
+
clear directions next to the object code saying where to find the
|
284 |
+
Corresponding Source. Regardless of what server hosts the
|
285 |
+
Corresponding Source, you remain obligated to ensure that it is
|
286 |
+
available for as long as needed to satisfy these requirements.
|
287 |
+
|
288 |
+
e) Convey the object code using peer-to-peer transmission, provided
|
289 |
+
you inform other peers where the object code and Corresponding
|
290 |
+
Source of the work are being offered to the general public at no
|
291 |
+
charge under subsection 6d.
|
292 |
+
|
293 |
+
A separable portion of the object code, whose source code is excluded
|
294 |
+
from the Corresponding Source as a System Library, need not be
|
295 |
+
included in conveying the object code work.
|
296 |
+
|
297 |
+
A "User Product" is either (1) a "consumer product", which means any
|
298 |
+
tangible personal property which is normally used for personal, family,
|
299 |
+
or household purposes, or (2) anything designed or sold for incorporation
|
300 |
+
into a dwelling. In determining whether a product is a consumer product,
|
301 |
+
doubtful cases shall be resolved in favor of coverage. For a particular
|
302 |
+
product received by a particular user, "normally used" refers to a
|
303 |
+
typical or common use of that class of product, regardless of the status
|
304 |
+
of the particular user or of the way in which the particular user
|
305 |
+
actually uses, or expects or is expected to use, the product. A product
|
306 |
+
is a consumer product regardless of whether the product has substantial
|
307 |
+
commercial, industrial or non-consumer uses, unless such uses represent
|
308 |
+
the only significant mode of use of the product.
|
309 |
+
|
310 |
+
"Installation Information" for a User Product means any methods,
|
311 |
+
procedures, authorization keys, or other information required to install
|
312 |
+
and execute modified versions of a covered work in that User Product from
|
313 |
+
a modified version of its Corresponding Source. The information must
|
314 |
+
suffice to ensure that the continued functioning of the modified object
|
315 |
+
code is in no case prevented or interfered with solely because
|
316 |
+
modification has been made.
|
317 |
+
|
318 |
+
If you convey an object code work under this section in, or with, or
|
319 |
+
specifically for use in, a User Product, and the conveying occurs as
|
320 |
+
part of a transaction in which the right of possession and use of the
|
321 |
+
User Product is transferred to the recipient in perpetuity or for a
|
322 |
+
fixed term (regardless of how the transaction is characterized), the
|
323 |
+
Corresponding Source conveyed under this section must be accompanied
|
324 |
+
by the Installation Information. But this requirement does not apply
|
325 |
+
if neither you nor any third party retains the ability to install
|
326 |
+
modified object code on the User Product (for example, the work has
|
327 |
+
been installed in ROM).
|
328 |
+
|
329 |
+
The requirement to provide Installation Information does not include a
|
330 |
+
requirement to continue to provide support service, warranty, or updates
|
331 |
+
for a work that has been modified or installed by the recipient, or for
|
332 |
+
the User Product in which it has been modified or installed. Access to a
|
333 |
+
network may be denied when the modification itself materially and
|
334 |
+
adversely affects the operation of the network or violates the rules and
|
335 |
+
protocols for communication across the network.
|
336 |
+
|
337 |
+
Corresponding Source conveyed, and Installation Information provided,
|
338 |
+
in accord with this section must be in a format that is publicly
|
339 |
+
documented (and with an implementation available to the public in
|
340 |
+
source code form), and must require no special password or key for
|
341 |
+
unpacking, reading or copying.
|
342 |
+
|
343 |
+
7. Additional Terms.
|
344 |
+
|
345 |
+
"Additional permissions" are terms that supplement the terms of this
|
346 |
+
License by making exceptions from one or more of its conditions.
|
347 |
+
Additional permissions that are applicable to the entire Program shall
|
348 |
+
be treated as though they were included in this License, to the extent
|
349 |
+
that they are valid under applicable law. If additional permissions
|
350 |
+
apply only to part of the Program, that part may be used separately
|
351 |
+
under those permissions, but the entire Program remains governed by
|
352 |
+
this License without regard to the additional permissions.
|
353 |
+
|
354 |
+
When you convey a copy of a covered work, you may at your option
|
355 |
+
remove any additional permissions from that copy, or from any part of
|
356 |
+
it. (Additional permissions may be written to require their own
|
357 |
+
removal in certain cases when you modify the work.) You may place
|
358 |
+
additional permissions on material, added by you to a covered work,
|
359 |
+
for which you have or can give appropriate copyright permission.
|
360 |
+
|
361 |
+
Notwithstanding any other provision of this License, for material you
|
362 |
+
add to a covered work, you may (if authorized by the copyright holders of
|
363 |
+
that material) supplement the terms of this License with terms:
|
364 |
+
|
365 |
+
a) Disclaiming warranty or limiting liability differently from the
|
366 |
+
terms of sections 15 and 16 of this License; or
|
367 |
+
|
368 |
+
b) Requiring preservation of specified reasonable legal notices or
|
369 |
+
author attributions in that material or in the Appropriate Legal
|
370 |
+
Notices displayed by works containing it; or
|
371 |
+
|
372 |
+
c) Prohibiting misrepresentation of the origin of that material, or
|
373 |
+
requiring that modified versions of such material be marked in
|
374 |
+
reasonable ways as different from the original version; or
|
375 |
+
|
376 |
+
d) Limiting the use for publicity purposes of names of licensors or
|
377 |
+
authors of the material; or
|
378 |
+
|
379 |
+
e) Declining to grant rights under trademark law for use of some
|
380 |
+
trade names, trademarks, or service marks; or
|
381 |
+
|
382 |
+
f) Requiring indemnification of licensors and authors of that
|
383 |
+
material by anyone who conveys the material (or modified versions of
|
384 |
+
it) with contractual assumptions of liability to the recipient, for
|
385 |
+
any liability that these contractual assumptions directly impose on
|
386 |
+
those licensors and authors.
|
387 |
+
|
388 |
+
All other non-permissive additional terms are considered "further
|
389 |
+
restrictions" within the meaning of section 10. If the Program as you
|
390 |
+
received it, or any part of it, contains a notice stating that it is
|
391 |
+
governed by this License along with a term that is a further
|
392 |
+
restriction, you may remove that term. If a license document contains
|
393 |
+
a further restriction but permits relicensing or conveying under this
|
394 |
+
License, you may add to a covered work material governed by the terms
|
395 |
+
of that license document, provided that the further restriction does
|
396 |
+
not survive such relicensing or conveying.
|
397 |
+
|
398 |
+
If you add terms to a covered work in accord with this section, you
|
399 |
+
must place, in the relevant source files, a statement of the
|
400 |
+
additional terms that apply to those files, or a notice indicating
|
401 |
+
where to find the applicable terms.
|
402 |
+
|
403 |
+
Additional terms, permissive or non-permissive, may be stated in the
|
404 |
+
form of a separately written license, or stated as exceptions;
|
405 |
+
the above requirements apply either way.
|
406 |
+
|
407 |
+
8. Termination.
|
408 |
+
|
409 |
+
You may not propagate or modify a covered work except as expressly
|
410 |
+
provided under this License. Any attempt otherwise to propagate or
|
411 |
+
modify it is void, and will automatically terminate your rights under
|
412 |
+
this License (including any patent licenses granted under the third
|
413 |
+
paragraph of section 11).
|
414 |
+
|
415 |
+
However, if you cease all violation of this License, then your
|
416 |
+
license from a particular copyright holder is reinstated (a)
|
417 |
+
provisionally, unless and until the copyright holder explicitly and
|
418 |
+
finally terminates your license, and (b) permanently, if the copyright
|
419 |
+
holder fails to notify you of the violation by some reasonable means
|
420 |
+
prior to 60 days after the cessation.
|
421 |
+
|
422 |
+
Moreover, your license from a particular copyright holder is
|
423 |
+
reinstated permanently if the copyright holder notifies you of the
|
424 |
+
violation by some reasonable means, this is the first time you have
|
425 |
+
received notice of violation of this License (for any work) from that
|
426 |
+
copyright holder, and you cure the violation prior to 30 days after
|
427 |
+
your receipt of the notice.
|
428 |
+
|
429 |
+
Termination of your rights under this section does not terminate the
|
430 |
+
licenses of parties who have received copies or rights from you under
|
431 |
+
this License. If your rights have been terminated and not permanently
|
432 |
+
reinstated, you do not qualify to receive new licenses for the same
|
433 |
+
material under section 10.
|
434 |
+
|
435 |
+
9. Acceptance Not Required for Having Copies.
|
436 |
+
|
437 |
+
You are not required to accept this License in order to receive or
|
438 |
+
run a copy of the Program. Ancillary propagation of a covered work
|
439 |
+
occurring solely as a consequence of using peer-to-peer transmission
|
440 |
+
to receive a copy likewise does not require acceptance. However,
|
441 |
+
nothing other than this License grants you permission to propagate or
|
442 |
+
modify any covered work. These actions infringe copyright if you do
|
443 |
+
not accept this License. Therefore, by modifying or propagating a
|
444 |
+
covered work, you indicate your acceptance of this License to do so.
|
445 |
+
|
446 |
+
10. Automatic Licensing of Downstream Recipients.
|
447 |
+
|
448 |
+
Each time you convey a covered work, the recipient automatically
|
449 |
+
receives a license from the original licensors, to run, modify and
|
450 |
+
propagate that work, subject to this License. You are not responsible
|
451 |
+
for enforcing compliance by third parties with this License.
|
452 |
+
|
453 |
+
An "entity transaction" is a transaction transferring control of an
|
454 |
+
organization, or substantially all assets of one, or subdividing an
|
455 |
+
organization, or merging organizations. If propagation of a covered
|
456 |
+
work results from an entity transaction, each party to that
|
457 |
+
transaction who receives a copy of the work also receives whatever
|
458 |
+
licenses to the work the party's predecessor in interest had or could
|
459 |
+
give under the previous paragraph, plus a right to possession of the
|
460 |
+
Corresponding Source of the work from the predecessor in interest, if
|
461 |
+
the predecessor has it or can get it with reasonable efforts.
|
462 |
+
|
463 |
+
You may not impose any further restrictions on the exercise of the
|
464 |
+
rights granted or affirmed under this License. For example, you may
|
465 |
+
not impose a license fee, royalty, or other charge for exercise of
|
466 |
+
rights granted under this License, and you may not initiate litigation
|
467 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
468 |
+
any patent claim is infringed by making, using, selling, offering for
|
469 |
+
sale, or importing the Program or any portion of it.
|
470 |
+
|
471 |
+
11. Patents.
|
472 |
+
|
473 |
+
A "contributor" is a copyright holder who authorizes use under this
|
474 |
+
License of the Program or a work on which the Program is based. The
|
475 |
+
work thus licensed is called the contributor's "contributor version".
|
476 |
+
|
477 |
+
A contributor's "essential patent claims" are all patent claims
|
478 |
+
owned or controlled by the contributor, whether already acquired or
|
479 |
+
hereafter acquired, that would be infringed by some manner, permitted
|
480 |
+
by this License, of making, using, or selling its contributor version,
|
481 |
+
but do not include claims that would be infringed only as a
|
482 |
+
consequence of further modification of the contributor version. For
|
483 |
+
purposes of this definition, "control" includes the right to grant
|
484 |
+
patent sublicenses in a manner consistent with the requirements of
|
485 |
+
this License.
|
486 |
+
|
487 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
488 |
+
patent license under the contributor's essential patent claims, to
|
489 |
+
make, use, sell, offer for sale, import and otherwise run, modify and
|
490 |
+
propagate the contents of its contributor version.
|
491 |
+
|
492 |
+
In the following three paragraphs, a "patent license" is any express
|
493 |
+
agreement or commitment, however denominated, not to enforce a patent
|
494 |
+
(such as an express permission to practice a patent or covenant not to
|
495 |
+
sue for patent infringement). To "grant" such a patent license to a
|
496 |
+
party means to make such an agreement or commitment not to enforce a
|
497 |
+
patent against the party.
|
498 |
+
|
499 |
+
If you convey a covered work, knowingly relying on a patent license,
|
500 |
+
and the Corresponding Source of the work is not available for anyone
|
501 |
+
to copy, free of charge and under the terms of this License, through a
|
502 |
+
publicly available network server or other readily accessible means,
|
503 |
+
then you must either (1) cause the Corresponding Source to be so
|
504 |
+
available, or (2) arrange to deprive yourself of the benefit of the
|
505 |
+
patent license for this particular work, or (3) arrange, in a manner
|
506 |
+
consistent with the requirements of this License, to extend the patent
|
507 |
+
license to downstream recipients. "Knowingly relying" means you have
|
508 |
+
actual knowledge that, but for the patent license, your conveying the
|
509 |
+
covered work in a country, or your recipient's use of the covered work
|
510 |
+
in a country, would infringe one or more identifiable patents in that
|
511 |
+
country that you have reason to believe are valid.
|
512 |
+
|
513 |
+
If, pursuant to or in connection with a single transaction or
|
514 |
+
arrangement, you convey, or propagate by procuring conveyance of, a
|
515 |
+
covered work, and grant a patent license to some of the parties
|
516 |
+
receiving the covered work authorizing them to use, propagate, modify
|
517 |
+
or convey a specific copy of the covered work, then the patent license
|
518 |
+
you grant is automatically extended to all recipients of the covered
|
519 |
+
work and works based on it.
|
520 |
+
|
521 |
+
A patent license is "discriminatory" if it does not include within
|
522 |
+
the scope of its coverage, prohibits the exercise of, or is
|
523 |
+
conditioned on the non-exercise of one or more of the rights that are
|
524 |
+
specifically granted under this License. You may not convey a covered
|
525 |
+
work if you are a party to an arrangement with a third party that is
|
526 |
+
in the business of distributing software, under which you make payment
|
527 |
+
to the third party based on the extent of your activity of conveying
|
528 |
+
the work, and under which the third party grants, to any of the
|
529 |
+
parties who would receive the covered work from you, a discriminatory
|
530 |
+
patent license (a) in connection with copies of the covered work
|
531 |
+
conveyed by you (or copies made from those copies), or (b) primarily
|
532 |
+
for and in connection with specific products or compilations that
|
533 |
+
contain the covered work, unless you entered into that arrangement,
|
534 |
+
or that patent license was granted, prior to 28 March 2007.
|
535 |
+
|
536 |
+
Nothing in this License shall be construed as excluding or limiting
|
537 |
+
any implied license or other defenses to infringement that may
|
538 |
+
otherwise be available to you under applicable patent law.
|
539 |
+
|
540 |
+
12. No Surrender of Others' Freedom.
|
541 |
+
|
542 |
+
If conditions are imposed on you (whether by court order, agreement or
|
543 |
+
otherwise) that contradict the conditions of this License, they do not
|
544 |
+
excuse you from the conditions of this License. If you cannot convey a
|
545 |
+
covered work so as to satisfy simultaneously your obligations under this
|
546 |
+
License and any other pertinent obligations, then as a consequence you may
|
547 |
+
not convey it at all. For example, if you agree to terms that obligate you
|
548 |
+
to collect a royalty for further conveying from those to whom you convey
|
549 |
+
the Program, the only way you could satisfy both those terms and this
|
550 |
+
License would be to refrain entirely from conveying the Program.
|
551 |
+
|
552 |
+
13. Use with the GNU Affero General Public License.
|
553 |
+
|
554 |
+
Notwithstanding any other provision of this License, you have
|
555 |
+
permission to link or combine any covered work with a work licensed
|
556 |
+
under version 3 of the GNU Affero General Public License into a single
|
557 |
+
combined work, and to convey the resulting work. The terms of this
|
558 |
+
License will continue to apply to the part which is the covered work,
|
559 |
+
but the special requirements of the GNU Affero General Public License,
|
560 |
+
section 13, concerning interaction through a network will apply to the
|
561 |
+
combination as such.
|
562 |
+
|
563 |
+
14. Revised Versions of this License.
|
564 |
+
|
565 |
+
The Free Software Foundation may publish revised and/or new versions of
|
566 |
+
the GNU General Public License from time to time. Such new versions will
|
567 |
+
be similar in spirit to the present version, but may differ in detail to
|
568 |
+
address new problems or concerns.
|
569 |
+
|
570 |
+
Each version is given a distinguishing version number. If the
|
571 |
+
Program specifies that a certain numbered version of the GNU General
|
572 |
+
Public License "or any later version" applies to it, you have the
|
573 |
+
option of following the terms and conditions either of that numbered
|
574 |
+
version or of any later version published by the Free Software
|
575 |
+
Foundation. If the Program does not specify a version number of the
|
576 |
+
GNU General Public License, you may choose any version ever published
|
577 |
+
by the Free Software Foundation.
|
578 |
+
|
579 |
+
If the Program specifies that a proxy can decide which future
|
580 |
+
versions of the GNU General Public License can be used, that proxy's
|
581 |
+
public statement of acceptance of a version permanently authorizes you
|
582 |
+
to choose that version for the Program.
|
583 |
+
|
584 |
+
Later license versions may give you additional or different
|
585 |
+
permissions. However, no additional obligations are imposed on any
|
586 |
+
author or copyright holder as a result of your choosing to follow a
|
587 |
+
later version.
|
588 |
+
|
589 |
+
15. Disclaimer of Warranty.
|
590 |
+
|
591 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
592 |
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
593 |
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
594 |
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
595 |
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
596 |
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
597 |
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
598 |
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
599 |
+
|
600 |
+
16. Limitation of Liability.
|
601 |
+
|
602 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
603 |
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
604 |
+
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
605 |
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
606 |
+
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
607 |
+
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
608 |
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
609 |
+
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
610 |
+
SUCH DAMAGES.
|
611 |
+
|
612 |
+
17. Interpretation of Sections 15 and 16.
|
613 |
+
|
614 |
+
If the disclaimer of warranty and limitation of liability provided
|
615 |
+
above cannot be given local legal effect according to their terms,
|
616 |
+
reviewing courts shall apply local law that most closely approximates
|
617 |
+
an absolute waiver of all civil liability in connection with the
|
618 |
+
Program, unless a warranty or assumption of liability accompanies a
|
619 |
+
copy of the Program in return for a fee.
|
620 |
+
|
621 |
+
END OF TERMS AND CONDITIONS
|
622 |
+
|
623 |
+
How to Apply These Terms to Your New Programs
|
624 |
+
|
625 |
+
If you develop a new program, and you want it to be of the greatest
|
626 |
+
possible use to the public, the best way to achieve this is to make it
|
627 |
+
free software which everyone can redistribute and change under these terms.
|
628 |
+
|
629 |
+
To do so, attach the following notices to the program. It is safest
|
630 |
+
to attach them to the start of each source file to most effectively
|
631 |
+
state the exclusion of warranty; and each file should have at least
|
632 |
+
the "copyright" line and a pointer to where the full notice is found.
|
633 |
+
|
634 |
+
<one line to give the program's name and a brief idea of what it does.>
|
635 |
+
Copyright (C) <year> <name of author>
|
636 |
+
|
637 |
+
This program is free software: you can redistribute it and/or modify
|
638 |
+
it under the terms of the GNU General Public License as published by
|
639 |
+
the Free Software Foundation, either version 3 of the License, or
|
640 |
+
(at your option) any later version.
|
641 |
+
|
642 |
+
This program is distributed in the hope that it will be useful,
|
643 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
644 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
645 |
+
GNU General Public License for more details.
|
646 |
+
|
647 |
+
You should have received a copy of the GNU General Public License
|
648 |
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
649 |
+
|
650 |
+
Also add information on how to contact you by electronic and paper mail.
|
651 |
+
|
652 |
+
If the program does terminal interaction, make it output a short
|
653 |
+
notice like this when it starts in an interactive mode:
|
654 |
+
|
655 |
+
<program> Copyright (C) <year> <name of author>
|
656 |
+
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
657 |
+
This is free software, and you are welcome to redistribute it
|
658 |
+
under certain conditions; type `show c' for details.
|
659 |
+
|
660 |
+
The hypothetical commands `show w' and `show c' should show the appropriate
|
661 |
+
parts of the General Public License. Of course, your program's commands
|
662 |
+
might be different; for a GUI interface, you would use an "about box".
|
663 |
+
|
664 |
+
You should also get your employer (if you work as a programmer) or school,
|
665 |
+
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
666 |
+
For more information on this, and how to apply and follow the GNU GPL, see
|
667 |
+
<https://www.gnu.org/licenses/>.
|
668 |
+
|
669 |
+
The GNU General Public License does not permit incorporating your program
|
670 |
+
into proprietary programs. If your program is a subroutine library, you
|
671 |
+
may consider it more useful to permit linking proprietary applications with
|
672 |
+
the library. If this is what you want to do, use the GNU Lesser General
|
673 |
+
Public License instead of this License. But first, please read
|
674 |
+
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
app/auth.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import Depends, HTTPException, Security
|
2 |
+
from fastapi.security.api_key import APIKeyHeader
|
3 |
+
from starlette.status import HTTP_403_FORBIDDEN
|
4 |
+
from .config import get_settings
|
5 |
+
|
6 |
+
api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False)
|
7 |
+
|
8 |
+
def require_api_key(api_key: str | None = Security(api_key_header)) -> str:
|
9 |
+
settings = get_settings()
|
10 |
+
if api_key is None or api_key not in settings.api_key:
|
11 |
+
raise HTTPException(
|
12 |
+
status_code=HTTP_403_FORBIDDEN,
|
13 |
+
detail="Invalid or missing API key",
|
14 |
+
)
|
15 |
+
return api_key # returned value can be injected downstream if needed
|
app/config.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import lru_cache
|
2 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
3 |
+
from pydantic import Field
|
4 |
+
from pathlib import Path
|
5 |
+
|
6 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
7 |
+
|
8 |
+
|
9 |
+
class Settings(BaseSettings):
|
10 |
+
"""Global configuration loaded from environment variables."""
|
11 |
+
redis_url: str = Field("redis://localhost:6379")
|
12 |
+
api_key: str = Field("123456")
|
13 |
+
|
14 |
+
model_config = SettingsConfigDict(
|
15 |
+
env_file = BASE_DIR / ".env",
|
16 |
+
env_file_encoding = "utf-8",
|
17 |
+
)
|
18 |
+
|
19 |
+
@lru_cache
|
20 |
+
def get_settings() -> Settings:
|
21 |
+
return Settings()
|
app/deploy.py
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from __future__ import annotations
|
2 |
+
import shutil, tempfile, textwrap, time
|
3 |
+
from pathlib import Path
|
4 |
+
from typing import Dict
|
5 |
+
import git
|
6 |
+
from huggingface_hub import HfApi
|
7 |
+
from huggingface_hub.errors import HfHubHTTPError
|
8 |
+
from .exceptions import (
|
9 |
+
DockerfileMissingError,
|
10 |
+
RepoCloneError,
|
11 |
+
SpaceBuildTimeoutError,
|
12 |
+
SpaceCreationError,
|
13 |
+
SpaceDeployError,
|
14 |
+
)
|
15 |
+
|
16 |
+
__all__ = [
|
17 |
+
"deploy_space",
|
18 |
+
"SpaceDeployError",
|
19 |
+
]
|
20 |
+
|
21 |
+
from .utils import _chmod_and_retry
|
22 |
+
|
23 |
+
|
24 |
+
def deploy_space(*, hf_token: str, git_repo_url: str, deploy_path: str, space_name: str, space_port: int, description: str, env_vars: Dict[str, str], private: bool) -> str:
|
25 |
+
deploy_path = deploy_path.strip(".").strip("/")
|
26 |
+
api = HfApi(token=hf_token)
|
27 |
+
try:
|
28 |
+
username = api.whoami().get("name", None)
|
29 |
+
repo_id = f"{username}/{space_name}"
|
30 |
+
api.create_repo(
|
31 |
+
repo_id=repo_id,
|
32 |
+
repo_type="space",
|
33 |
+
space_sdk="docker",
|
34 |
+
private=private,
|
35 |
+
# space_variables=[{}],
|
36 |
+
space_secrets=[{"key": k, "value": v} for k, v in env_vars.items()],
|
37 |
+
exist_ok=False,
|
38 |
+
)
|
39 |
+
except HfHubHTTPError as exc:
|
40 |
+
raise SpaceCreationError(exc) from exc
|
41 |
+
|
42 |
+
tmp = tempfile.mkdtemp(prefix="hf_space_")
|
43 |
+
try:
|
44 |
+
try:
|
45 |
+
if not deploy_path:
|
46 |
+
git.Repo.clone_from(
|
47 |
+
git_repo_url,
|
48 |
+
tmp,
|
49 |
+
depth=1,
|
50 |
+
multi_options=["--single-branch"]
|
51 |
+
)
|
52 |
+
else:
|
53 |
+
repo = git.Repo.clone_from(
|
54 |
+
git_repo_url, tmp,
|
55 |
+
depth=1,
|
56 |
+
no_checkout=True,
|
57 |
+
filter=["tree:0"],
|
58 |
+
sparse=True,
|
59 |
+
multi_options=["--single-branch"],
|
60 |
+
)
|
61 |
+
repo.git.sparse_checkout("init", "--no-cone")
|
62 |
+
repo.git.sparse_checkout("set", "--no-cone", f"/{deploy_path}/**")
|
63 |
+
repo.git.checkout()
|
64 |
+
|
65 |
+
# 平移文件到根目录
|
66 |
+
src = Path(tmp, deploy_path)
|
67 |
+
for item in src.iterdir():
|
68 |
+
shutil.move(item, tmp) # 根目录目前是空的
|
69 |
+
shutil.rmtree(src)
|
70 |
+
except git.GitCommandError as exc:
|
71 |
+
raise RepoCloneError(str(exc)) from exc
|
72 |
+
|
73 |
+
git_dir = Path(tmp, ".git")
|
74 |
+
if git_dir.exists() and git_dir.is_dir():
|
75 |
+
shutil.rmtree(git_dir, onerror=_chmod_and_retry)
|
76 |
+
|
77 |
+
if not Path(tmp, "Dockerfile").exists():
|
78 |
+
raise DockerfileMissingError()
|
79 |
+
|
80 |
+
readme = Path(tmp, "README.md")
|
81 |
+
header = (
|
82 |
+
f"---\n"
|
83 |
+
f"title: \"{space_name}\"\n"
|
84 |
+
f"emoji: \"🚀\"\n"
|
85 |
+
f"colorFrom: blue\n"
|
86 |
+
f"colorTo: green\n"
|
87 |
+
f"sdk: docker\n"
|
88 |
+
f"app_port: {space_port}\n"
|
89 |
+
f"---\n"
|
90 |
+
)
|
91 |
+
badge = (
|
92 |
+
f"### 🚀 一键部署\n"
|
93 |
+
f"[](https://github.com/kfcx/HFSpaceDeploy)\n\n"
|
94 |
+
f"本项目由[HFSpaceDeploy](https://github.com/kfcx/HFSpaceDeploy)一键部署\n"
|
95 |
+
)
|
96 |
+
readme.write_text(f"{header}\n{badge}\n{description}\n", encoding="utf-8")
|
97 |
+
api.upload_folder(folder_path=tmp, repo_id=repo_id, repo_type="space", ignore_patterns=[".git"])
|
98 |
+
finally:
|
99 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
100 |
+
|
101 |
+
deadline = time.time() + 15 * 60
|
102 |
+
while time.time() < deadline:
|
103 |
+
stage = api.get_space_runtime(repo_id).stage
|
104 |
+
if stage == "RUNNING":
|
105 |
+
return f"https://huggingface.co/spaces/{repo_id}"
|
106 |
+
if stage in ("BUILD_ERROR", "CONFIG_ERROR", "RUNTIME_ERROR",):
|
107 |
+
raise SpaceDeployError("Space failed during config/build/runtime")
|
108 |
+
if stage in ("NO_APP_FILE", "STOPPED", "PAUSED", "DELETING", ):
|
109 |
+
raise SpaceDeployError("Space is currently unavailable")
|
110 |
+
time.sleep(5)
|
111 |
+
raise SpaceBuildTimeoutError()
|
app/exceptions.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
class SpaceDeployError(Exception):
|
3 |
+
"""Base for deployment‑related faults."""
|
4 |
+
|
5 |
+
class RepoCloneError(SpaceDeployError):
|
6 |
+
pass
|
7 |
+
|
8 |
+
class DockerfileMissingError(SpaceDeployError):
|
9 |
+
pass
|
10 |
+
|
11 |
+
class SpaceCreationError(SpaceDeployError):
|
12 |
+
pass
|
13 |
+
|
14 |
+
class SpaceBuildTimeoutError(SpaceDeployError):
|
15 |
+
pass
|
16 |
+
|
17 |
+
class SpaceAlreadyExistsError(SpaceDeployError):
|
18 |
+
pass
|
app/models.py
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Any, Dict, Optional
|
2 |
+
# from huggingface_hub import SpaceStage
|
3 |
+
from pydantic import BaseModel, Field
|
4 |
+
|
5 |
+
class DeployRequest(BaseModel):
|
6 |
+
hf_token: str = Field(..., description="HF token with write permission")
|
7 |
+
git_repo_url: str = Field(...)
|
8 |
+
deploy_path: str = Field("/")
|
9 |
+
space_name: str = Field(...)
|
10 |
+
space_port: int = Field(7860)
|
11 |
+
description: str = Field("")
|
12 |
+
env_vars: Dict[str, str] = Field(default_factory=dict)
|
13 |
+
private: bool = Field(False)
|
14 |
+
|
15 |
+
class DeployStatus(BaseModel):
|
16 |
+
task_id: str
|
17 |
+
status: str # PENDING | IN_PROGRESS | SUCCESS | FAILED
|
18 |
+
detail: Optional[Any] = None
|
19 |
+
|
app/routes.py
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import BackgroundTasks, FastAPI, HTTPException, Depends, Request, Form
|
2 |
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
3 |
+
from fastapi.staticfiles import StaticFiles
|
4 |
+
from fastapi.templating import Jinja2Templates
|
5 |
+
from fastapi.middleware.gzip import GZipMiddleware
|
6 |
+
import uuid
|
7 |
+
from app.models import DeployRequest, DeployStatus
|
8 |
+
from app.store import TaskStore
|
9 |
+
from app.deploy import deploy_space, SpaceDeployError
|
10 |
+
from app.auth import require_api_key
|
11 |
+
|
12 |
+
app = FastAPI(title="HF Space Deployer", version="1.0.0")
|
13 |
+
|
14 |
+
# Static files and templates
|
15 |
+
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
16 |
+
app.add_middleware(GZipMiddleware)
|
17 |
+
templates = Jinja2Templates(directory="app/templates")
|
18 |
+
|
19 |
+
|
20 |
+
# Web Interface Routes
|
21 |
+
@app.get("/", response_class=HTMLResponse)
|
22 |
+
async def index(request: Request):
|
23 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
24 |
+
|
25 |
+
|
26 |
+
@app.post("/web/deploy")
|
27 |
+
async def web_deploy(
|
28 |
+
request: Request,
|
29 |
+
hf_token: str = Form(...),
|
30 |
+
git_repo_url: str = Form(...),
|
31 |
+
space_name: str = Form(...),
|
32 |
+
description: str = Form(""),
|
33 |
+
space_port: int = Form(7860),
|
34 |
+
private: bool = Form(False),
|
35 |
+
env_vars_text: str = Form(""),
|
36 |
+
deploy_path: str = Form("/"),
|
37 |
+
bg: BackgroundTasks = BackgroundTasks()
|
38 |
+
):
|
39 |
+
# Parse environment variables from textarea
|
40 |
+
env_vars = {}
|
41 |
+
if env_vars_text.strip():
|
42 |
+
for line in env_vars_text.strip().split('\n'):
|
43 |
+
if '=' in line:
|
44 |
+
key, value = line.split('=', 1)
|
45 |
+
env_vars[key.strip()] = value.strip()
|
46 |
+
|
47 |
+
deploy_req = DeployRequest(
|
48 |
+
hf_token=hf_token,
|
49 |
+
git_repo_url=git_repo_url,
|
50 |
+
space_name=space_name,
|
51 |
+
description=description,
|
52 |
+
space_port=space_port,
|
53 |
+
private=private,
|
54 |
+
env_vars=env_vars,
|
55 |
+
deploy_path=deploy_path
|
56 |
+
)
|
57 |
+
|
58 |
+
task_id = str(uuid.uuid4())
|
59 |
+
TaskStore.save(DeployStatus(task_id=task_id, status="PENDING"))
|
60 |
+
bg.add_task(_run_task, task_id, deploy_req)
|
61 |
+
|
62 |
+
# Redirect to status page instead of returning HTML
|
63 |
+
return RedirectResponse(url=f"/deploy/{task_id}", status_code=303)
|
64 |
+
|
65 |
+
|
66 |
+
@app.get("/deploy/{task_id}", response_class=HTMLResponse)
|
67 |
+
async def deploy_status_page(request: Request, task_id: str):
|
68 |
+
status = TaskStore.load(task_id)
|
69 |
+
if not status:
|
70 |
+
raise HTTPException(status_code=404, detail="Task not found")
|
71 |
+
|
72 |
+
return templates.TemplateResponse("deploy_status.html", {
|
73 |
+
"request": request,
|
74 |
+
"task_id": task_id,
|
75 |
+
"status": status.status
|
76 |
+
})
|
77 |
+
|
78 |
+
|
79 |
+
# API Routes
|
80 |
+
@app.post("/deploy", response_model=DeployStatus, status_code=202)
|
81 |
+
async def deploy(req: DeployRequest, bg: BackgroundTasks):
|
82 |
+
task_id = str(uuid.uuid4())
|
83 |
+
TaskStore.save(DeployStatus(task_id=task_id, status="PENDING"))
|
84 |
+
bg.add_task(_run_task, task_id, req)
|
85 |
+
return TaskStore.load(task_id)
|
86 |
+
|
87 |
+
|
88 |
+
@app.get("/deploy/status/{task_id}", response_model=DeployStatus)
|
89 |
+
async def status(task_id: str):
|
90 |
+
status = TaskStore.load(task_id)
|
91 |
+
if not status:
|
92 |
+
raise HTTPException(status_code=404, detail="Task not found")
|
93 |
+
return status
|
94 |
+
|
95 |
+
|
96 |
+
# ------------------------------------------------------------------ #
|
97 |
+
|
98 |
+
def _run_task(task_id: str, req: DeployRequest):
|
99 |
+
TaskStore.save(DeployStatus(task_id=task_id, status="IN_PROGRESS"))
|
100 |
+
try:
|
101 |
+
url = deploy_space(**req.dict())
|
102 |
+
TaskStore.save(DeployStatus(task_id=task_id, status="SUCCESS", detail={"space_url": url}))
|
103 |
+
except SpaceDeployError as exc:
|
104 |
+
TaskStore.save(DeployStatus(task_id=task_id, status="FAILED", detail={"error": str(exc)}))
|
app/static/i18n.js
ADDED
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Internationalization (i18n) support
|
2 |
+
const translations = {
|
3 |
+
en: {
|
4 |
+
// Navigation
|
5 |
+
'nav.title': 'HF Space Deployer',
|
6 |
+
'nav.theme': 'Toggle Theme',
|
7 |
+
'nav.language': 'Language',
|
8 |
+
|
9 |
+
// Hero Section
|
10 |
+
'hero.title': 'One-Click Deploy',
|
11 |
+
'hero.subtitle': 'Deploy to HuggingFace Spaces instantly',
|
12 |
+
'hero.deployTime': 'Deploy Time',
|
13 |
+
'hero.freeHosting': 'Free Hosting',
|
14 |
+
'hero.minutes': '2-5min',
|
15 |
+
'hero.percentage': '100%',
|
16 |
+
|
17 |
+
// Features
|
18 |
+
'feature.fast.title': 'Lightning Fast',
|
19 |
+
'feature.fast.desc': 'Deploy from Git in minutes',
|
20 |
+
'feature.secure.title': 'Secure',
|
21 |
+
'feature.secure.desc': 'Full control with your token',
|
22 |
+
'feature.monitor.title': 'Real-time',
|
23 |
+
'feature.monitor.desc': 'Live deployment status',
|
24 |
+
|
25 |
+
// Form
|
26 |
+
'form.title': 'Configuration',
|
27 |
+
'form.token': 'HuggingFace Token',
|
28 |
+
'form.token.placeholder': 'hf_...',
|
29 |
+
'form.token.get': 'Get Token',
|
30 |
+
'form.token.hint': 'Write permission required',
|
31 |
+
'form.token.toggle': 'Toggle visibility',
|
32 |
+
'form.token.clear': 'Clear cached token',
|
33 |
+
'form.token.cleared': 'Token cache cleared',
|
34 |
+
'form.repo': 'Git Repository',
|
35 |
+
'form.repo.placeholder': 'https://github.com/user/repo.git',
|
36 |
+
'form.repo.hint': 'GitHub, GitLab, etc.',
|
37 |
+
'form.space': 'Space Name',
|
38 |
+
'form.space.placeholder': 'my-app',
|
39 |
+
'form.space.hint': 'Letters, numbers, hyphens only',
|
40 |
+
'form.desc': 'Description',
|
41 |
+
'form.desc.placeholder': 'Brief description...',
|
42 |
+
'form.advanced': 'Advanced',
|
43 |
+
'form.deployPath': 'Deploy Path',
|
44 |
+
'form.deployPath.placeholder': '/',
|
45 |
+
'form.deployPath.hint': 'Subdirectory to deploy (default: root)',
|
46 |
+
'form.port': 'Port',
|
47 |
+
'form.private': 'Private Space',
|
48 |
+
'form.env': 'Environment Variables',
|
49 |
+
'form.env.placeholder': 'KEY1=value\nKEY2=value',
|
50 |
+
'form.env.hint': 'One per line',
|
51 |
+
'form.submit': 'Deploy',
|
52 |
+
|
53 |
+
// Requirements
|
54 |
+
'req.title': 'Requirements',
|
55 |
+
'req.dockerfile': 'Repository must contain Dockerfile',
|
56 |
+
'req.token': 'Token needs write permissions',
|
57 |
+
'req.time': 'Deployment takes 2-5 minutes',
|
58 |
+
'req.docker': 'Supports Dockerized apps',
|
59 |
+
|
60 |
+
// Pro Tips
|
61 |
+
'tips.title': 'Tips',
|
62 |
+
'tips.test': 'Test Dockerfile locally',
|
63 |
+
'tips.env': 'Use env vars for secrets',
|
64 |
+
'tips.size': 'Keep image size small',
|
65 |
+
'tips.limits': 'Check HF resource limits',
|
66 |
+
|
67 |
+
// Status Page
|
68 |
+
'status.title': 'Deployment Status',
|
69 |
+
'status.taskId': 'Task ID',
|
70 |
+
'status.initializing': 'Initializing...',
|
71 |
+
'status.preparing': 'Preparing your Space...',
|
72 |
+
'status.queued': 'Queued',
|
73 |
+
'status.queued.desc': 'Request received',
|
74 |
+
'status.progress': 'In Progress',
|
75 |
+
'status.progress.desc': 'Building and deploying...',
|
76 |
+
'status.success': 'Success!',
|
77 |
+
'status.success.desc': 'Your Space is live',
|
78 |
+
'status.failed': 'Failed',
|
79 |
+
'status.failed.desc': 'Deployment error',
|
80 |
+
'status.url': 'Space URL',
|
81 |
+
'status.visit': 'Visit Space',
|
82 |
+
'status.error': 'Error Details',
|
83 |
+
'status.troubleshoot': 'Troubleshooting',
|
84 |
+
'status.newDeploy': 'New Deploy',
|
85 |
+
'status.refresh': 'Refresh',
|
86 |
+
'status.copy': 'Copy',
|
87 |
+
'status.autoRefresh': 'Auto-refresh every 2s',
|
88 |
+
|
89 |
+
// Loading
|
90 |
+
'loading.title': 'Deploying...',
|
91 |
+
'loading.desc': 'Please wait...',
|
92 |
+
|
93 |
+
// Configuration Import/Export
|
94 |
+
'config.import': 'Import',
|
95 |
+
'config.export': 'Export',
|
96 |
+
'config.import.title': 'Import Configuration',
|
97 |
+
'config.import.info': 'Paste your configuration JSON or share configuration URL',
|
98 |
+
'config.import.label': 'Configuration JSON',
|
99 |
+
'config.import.placeholder': '{"space_name": "my-app", "git_repo_url": "https://github.com/..."}',
|
100 |
+
'config.import.apply': 'Apply Configuration',
|
101 |
+
'config.import.success': 'Configuration imported successfully',
|
102 |
+
'config.import.error': 'Failed to import configuration',
|
103 |
+
'config.import.empty': 'Please enter configuration JSON',
|
104 |
+
'config.import.invalid': 'Invalid configuration format',
|
105 |
+
'config.export.title': 'Export Configuration',
|
106 |
+
'config.export.info': 'Configuration exported successfully',
|
107 |
+
'config.export.label': 'Configuration JSON',
|
108 |
+
'config.export.url': 'Share URL',
|
109 |
+
'config.export.save': 'Save as File',
|
110 |
+
'config.export.saved': 'Configuration saved to file',
|
111 |
+
'config.copy': 'Copy',
|
112 |
+
'config.copied': 'Configuration copied!',
|
113 |
+
'config.url.copied': 'Share URL copied!',
|
114 |
+
'config.cancel': 'Cancel',
|
115 |
+
'config.close': 'Close',
|
116 |
+
|
117 |
+
// Footer
|
118 |
+
'footer.title': 'HF Space Deployer',
|
119 |
+
'footer.desc': 'Quick deployment tool',
|
120 |
+
|
121 |
+
// Copied toast
|
122 |
+
'toast.copied': 'Copied!',
|
123 |
+
'toast.copyFailed': 'Copy failed',
|
124 |
+
'toast.deploySuccess': 'Deployment successful!',
|
125 |
+
'toast.deployFailed': 'Deployment failed',
|
126 |
+
'toast.requestFailed': 'Request failed'
|
127 |
+
},
|
128 |
+
|
129 |
+
zh: {
|
130 |
+
// Navigation
|
131 |
+
'nav.title': 'HF Space 部署器',
|
132 |
+
'nav.theme': '切换主题',
|
133 |
+
'nav.language': '语言',
|
134 |
+
|
135 |
+
// Hero Section
|
136 |
+
'hero.title': '一键部署',
|
137 |
+
'hero.subtitle': '快速部署到 HuggingFace Spaces',
|
138 |
+
'hero.deployTime': '部署时间',
|
139 |
+
'hero.freeHosting': '免费托管',
|
140 |
+
'hero.minutes': '2-5分钟',
|
141 |
+
'hero.percentage': '100%',
|
142 |
+
|
143 |
+
// Features
|
144 |
+
'feature.fast.title': '极速部署',
|
145 |
+
'feature.fast.desc': '分钟级 Git 部署',
|
146 |
+
'feature.secure.title': '安全可靠',
|
147 |
+
'feature.secure.desc': 'Token 完全掌控',
|
148 |
+
'feature.monitor.title': '实时监控',
|
149 |
+
'feature.monitor.desc': '部署状态实时更新',
|
150 |
+
|
151 |
+
// Form
|
152 |
+
'form.title': '配置设置',
|
153 |
+
'form.token': 'HuggingFace 令牌',
|
154 |
+
'form.token.placeholder': 'hf_...',
|
155 |
+
'form.token.get': '获取令牌',
|
156 |
+
'form.token.hint': '需要写入权限',
|
157 |
+
'form.token.toggle': '切换可见性',
|
158 |
+
'form.token.clear': '清除缓存令牌',
|
159 |
+
'form.token.cleared': '令牌缓存已清除',
|
160 |
+
'form.repo': 'Git 仓库',
|
161 |
+
'form.repo.placeholder': 'https://github.com/用户名/仓库名.git',
|
162 |
+
'form.repo.hint': '支持 GitHub、GitLab 等',
|
163 |
+
'form.space': '空间名称',
|
164 |
+
'form.space.placeholder': 'my-app',
|
165 |
+
'form.space.hint': '仅限字母、数字、连字符',
|
166 |
+
'form.desc': '描述',
|
167 |
+
'form.desc.placeholder': '简短描述...',
|
168 |
+
'form.advanced': '高级设置',
|
169 |
+
'form.deployPath': '部署路径',
|
170 |
+
'form.deployPath.placeholder': '/',
|
171 |
+
'form.deployPath.hint': '要部署的子目录(默认:根目录)',
|
172 |
+
'form.port': '端口',
|
173 |
+
'form.private': '私有空间',
|
174 |
+
'form.env': '环境变量',
|
175 |
+
'form.env.placeholder': 'KEY1=value\nKEY2=value',
|
176 |
+
'form.env.hint': '每行一个',
|
177 |
+
'form.submit': '部署',
|
178 |
+
|
179 |
+
// Requirements
|
180 |
+
'req.title': '要求',
|
181 |
+
'req.dockerfile': '仓库需包含 Dockerfile',
|
182 |
+
'req.token': '令牌需要写入权限',
|
183 |
+
'req.time': '部署需要 2-5 分钟',
|
184 |
+
'req.docker': '支持 Docker 应用',
|
185 |
+
|
186 |
+
// Pro Tips
|
187 |
+
'tips.title': '提示',
|
188 |
+
'tips.test': '先本地测试 Dockerfile',
|
189 |
+
'tips.env': '敏感数据用环境变量',
|
190 |
+
'tips.size': '保持镜像体积小',
|
191 |
+
'tips.limits': '检查 HF 资源限制',
|
192 |
+
|
193 |
+
// Status Page
|
194 |
+
'status.title': '部署状态',
|
195 |
+
'status.taskId': '任务 ID',
|
196 |
+
'status.initializing': '初始化中...',
|
197 |
+
'status.preparing': '准备 Space 中...',
|
198 |
+
'status.queued': '排队中',
|
199 |
+
'status.queued.desc': '已接收请求',
|
200 |
+
'status.progress': '进行中',
|
201 |
+
'status.progress.desc': '构建部署中...',
|
202 |
+
'status.success': '成功!',
|
203 |
+
'status.success.desc': 'Space 已上线',
|
204 |
+
'status.failed': '失败',
|
205 |
+
'status.failed.desc': '部署出错',
|
206 |
+
'status.url': 'Space 地址',
|
207 |
+
'status.visit': '访问 Space',
|
208 |
+
'status.error': '错误详情',
|
209 |
+
'status.troubleshoot': '故障排除',
|
210 |
+
'status.newDeploy': '新建部署',
|
211 |
+
'status.refresh': '刷新',
|
212 |
+
'status.copy': '复制',
|
213 |
+
'status.autoRefresh': '每2秒自动刷新',
|
214 |
+
|
215 |
+
// Loading
|
216 |
+
'loading.title': '部署中...',
|
217 |
+
'loading.desc': '请稍候...',
|
218 |
+
|
219 |
+
// Configuration Import/Export
|
220 |
+
'config.import': '导入',
|
221 |
+
'config.export': '导出',
|
222 |
+
'config.import.title': '导入配置',
|
223 |
+
'config.import.info': '粘贴配置 JSON 或分享配置链接',
|
224 |
+
'config.import.label': '配置 JSON',
|
225 |
+
'config.import.placeholder': '{"space_name": "my-app", "git_repo_url": "https://github.com/..."}',
|
226 |
+
'config.import.apply': '应用配置',
|
227 |
+
'config.import.success': '配置导入成功',
|
228 |
+
'config.import.error': '配置导入失败',
|
229 |
+
'config.import.empty': '请输入配置 JSON',
|
230 |
+
'config.import.invalid': '配置格式无效',
|
231 |
+
'config.export.title': '导出配置',
|
232 |
+
'config.export.info': '配置导出成功',
|
233 |
+
'config.export.label': '配置 JSON',
|
234 |
+
'config.export.url': '分享链接',
|
235 |
+
'config.export.save': '保存为文��',
|
236 |
+
'config.export.saved': '配置已保存到文件',
|
237 |
+
'config.copy': '复制',
|
238 |
+
'config.copied': '配置已复制!',
|
239 |
+
'config.url.copied': '分享链接已复制!',
|
240 |
+
'config.cancel': '取消',
|
241 |
+
'config.close': '关闭',
|
242 |
+
|
243 |
+
// Footer
|
244 |
+
'footer.title': 'HF Space 部署器',
|
245 |
+
'footer.desc': '快速部署工具',
|
246 |
+
|
247 |
+
// Copied toast
|
248 |
+
'toast.copied': '已复制!',
|
249 |
+
'toast.copyFailed': '复制失败',
|
250 |
+
'toast.deploySuccess': '部署成功!',
|
251 |
+
'toast.deployFailed': '部署失败',
|
252 |
+
'toast.requestFailed': '请求失败'
|
253 |
+
}
|
254 |
+
};
|
255 |
+
|
256 |
+
// Current language
|
257 |
+
let currentLang = localStorage.getItem('language') || 'en';
|
258 |
+
|
259 |
+
// Translate function
|
260 |
+
function t(key) {
|
261 |
+
return translations[currentLang][key] || translations['en'][key] || key;
|
262 |
+
}
|
263 |
+
|
264 |
+
// Set language
|
265 |
+
function setLanguage(lang) {
|
266 |
+
currentLang = lang;
|
267 |
+
localStorage.setItem('language', lang);
|
268 |
+
updatePageTranslations();
|
269 |
+
}
|
270 |
+
|
271 |
+
// Toggle language
|
272 |
+
function toggleLanguage() {
|
273 |
+
const newLang = currentLang === 'en' ? 'zh' : 'en';
|
274 |
+
setLanguage(newLang);
|
275 |
+
}
|
276 |
+
|
277 |
+
// Update all translations on page
|
278 |
+
function updatePageTranslations() {
|
279 |
+
// Update all elements with data-i18n attribute
|
280 |
+
document.querySelectorAll('[data-i18n]').forEach(element => {
|
281 |
+
const key = element.getAttribute('data-i18n');
|
282 |
+
element.textContent = t(key);
|
283 |
+
});
|
284 |
+
|
285 |
+
// Update all elements with data-i18n-placeholder attribute
|
286 |
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
|
287 |
+
const key = element.getAttribute('data-i18n-placeholder');
|
288 |
+
element.placeholder = t(key);
|
289 |
+
});
|
290 |
+
|
291 |
+
// Update all elements with data-i18n-title attribute
|
292 |
+
document.querySelectorAll('[data-i18n-title]').forEach(element => {
|
293 |
+
const key = element.getAttribute('data-i18n-title');
|
294 |
+
element.title = t(key);
|
295 |
+
});
|
296 |
+
|
297 |
+
// Update document title
|
298 |
+
document.title = t('nav.title');
|
299 |
+
|
300 |
+
// Dispatch custom event
|
301 |
+
document.dispatchEvent(new Event('languageChanged'));
|
302 |
+
}
|
303 |
+
|
304 |
+
// Initialize on page load
|
305 |
+
document.addEventListener('DOMContentLoaded', () => {
|
306 |
+
updatePageTranslations();
|
307 |
+
});
|
app/static/style.css
ADDED
@@ -0,0 +1,737 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Custom styles for HuggingFace Space Deployer */
|
2 |
+
|
3 |
+
/* Light Theme Configuration - CMYK Theme */
|
4 |
+
[data-theme="light"] {
|
5 |
+
/* Color scheme */
|
6 |
+
color-scheme: light;
|
7 |
+
|
8 |
+
/* Base colors */
|
9 |
+
--b1: 100% 0 0; /* base-100 - Pure white */
|
10 |
+
--b2: 95% 0 0; /* base-200 */
|
11 |
+
--b3: 90% 0 0; /* base-300 */
|
12 |
+
--bc: 20% 0 0; /* base-content - Dark gray */
|
13 |
+
|
14 |
+
/* Brand colors */
|
15 |
+
--p: 71.772% 0.133 239.443; /* primary - Blue */
|
16 |
+
--pc: 14.354% 0.026 239.443; /* primary-content */
|
17 |
+
--s: 64.476% 0.202 359.339; /* secondary - Red/Pink */
|
18 |
+
--sc: 12.895% 0.04 359.339; /* secondary-content */
|
19 |
+
--a: 94.228% 0.189 105.306; /* accent - Yellow-green */
|
20 |
+
--ac: 18.845% 0.037 105.306; /* accent-content */
|
21 |
+
--n: 21.778% 0 0; /* neutral - Dark gray */
|
22 |
+
--nc: 84.355% 0 0; /* neutral-content */
|
23 |
+
|
24 |
+
/* State colors */
|
25 |
+
--in: 68.475% 0.094 217.284; /* info - Light blue */
|
26 |
+
--inc: 13.695% 0.018 217.284; /* info-content */
|
27 |
+
--su: 46.949% 0.162 321.406; /* success - Magenta */
|
28 |
+
--suc: 89.389% 0.032 321.406; /* success-content */
|
29 |
+
--wa: 71.236% 0.159 52.023; /* warning - Orange */
|
30 |
+
--wac: 14.247% 0.031 52.023; /* warning-content */
|
31 |
+
--er: 62.013% 0.208 28.717; /* error - Red-orange */
|
32 |
+
--erc: 12.402% 0.041 28.717; /* error-content */
|
33 |
+
|
34 |
+
/* Design tokens */
|
35 |
+
--rounded-box: 1rem; /* Larger border radius */
|
36 |
+
--rounded-btn: 1rem; /* Rounded buttons */
|
37 |
+
--rounded-badge: 0.5rem; /* Medium rounded badges */
|
38 |
+
--border-btn: 1px; /* Border width */
|
39 |
+
|
40 |
+
/* Effects */
|
41 |
+
--animation-btn: 0.25s;
|
42 |
+
--animation-input: 0.2s;
|
43 |
+
--btn-focus-scale: 0.98;
|
44 |
+
--tab-radius: 1rem;
|
45 |
+
}
|
46 |
+
|
47 |
+
/* Dark Theme Configuration - Aqua Theme */
|
48 |
+
[data-theme="dark"] {
|
49 |
+
/* Color scheme */
|
50 |
+
color-scheme: dark;
|
51 |
+
|
52 |
+
/* Base colors */
|
53 |
+
--b1: 37% 0.146 265.522; /* base-100 - Deep blue-purple */
|
54 |
+
--b2: 28% 0.091 267.935; /* base-200 */
|
55 |
+
--b3: 22% 0.091 267.935; /* base-300 */
|
56 |
+
--bc: 90% 0.058 230.902; /* base-content - Light cyan */
|
57 |
+
|
58 |
+
/* Brand colors */
|
59 |
+
--p: 85.661% 0.144 198.645; /* primary - Cyan */
|
60 |
+
--pc: 40.124% 0.068 197.603; /* primary-content */
|
61 |
+
--s: 60.682% 0.108 309.782; /* secondary - Purple */
|
62 |
+
--sc: 96% 0.016 293.756; /* secondary-content */
|
63 |
+
--a: 93.426% 0.102 94.555; /* accent - Yellow */
|
64 |
+
--ac: 18.685% 0.02 94.555; /* accent-content */
|
65 |
+
--n: 27% 0.146 265.522; /* neutral - Dark blue */
|
66 |
+
--nc: 80% 0.146 265.522; /* neutral-content */
|
67 |
+
|
68 |
+
/* State colors */
|
69 |
+
--in: 54.615% 0.215 262.88; /* info - Blue */
|
70 |
+
--inc: 90.923% 0.043 262.88; /* info-content */
|
71 |
+
--su: 62.705% 0.169 149.213; /* success - Green */
|
72 |
+
--suc: 12.541% 0.033 149.213; /* success-content */
|
73 |
+
--wa: 66.584% 0.157 58.318; /* warning - Orange */
|
74 |
+
--wac: 27% 0.077 45.635; /* warning-content */
|
75 |
+
--er: 73.95% 0.19 27.33; /* error - Red */
|
76 |
+
--erc: 14.79% 0.038 27.33; /* error-content */
|
77 |
+
|
78 |
+
/* Design tokens */
|
79 |
+
--rounded-box: 1rem; /* Larger border radius */
|
80 |
+
--rounded-btn: 1rem; /* Rounded buttons */
|
81 |
+
--rounded-badge: 0.5rem; /* Medium rounded badges */
|
82 |
+
--border-btn: 1px; /* Border width */
|
83 |
+
|
84 |
+
/* Effects */
|
85 |
+
--animation-btn: 0.25s;
|
86 |
+
--animation-input: 0.2s;
|
87 |
+
--btn-focus-scale: 0.98;
|
88 |
+
--tab-radius: 1rem;
|
89 |
+
}
|
90 |
+
|
91 |
+
/* Custom theme variables */
|
92 |
+
:root {
|
93 |
+
--hover-opacity: 0.08;
|
94 |
+
--focus-opacity: 0.12;
|
95 |
+
--transition-speed: 0.2s;
|
96 |
+
}
|
97 |
+
|
98 |
+
/* Animations */
|
99 |
+
@keyframes fadeIn {
|
100 |
+
from {
|
101 |
+
opacity: 0;
|
102 |
+
transform: translateY(20px);
|
103 |
+
}
|
104 |
+
to {
|
105 |
+
opacity: 1;
|
106 |
+
transform: translateY(0);
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
@keyframes slideIn {
|
111 |
+
from {
|
112 |
+
opacity: 0;
|
113 |
+
transform: translateX(-20px);
|
114 |
+
}
|
115 |
+
to {
|
116 |
+
opacity: 1;
|
117 |
+
transform: translateX(0);
|
118 |
+
}
|
119 |
+
}
|
120 |
+
|
121 |
+
@keyframes pulse {
|
122 |
+
0%, 100% {
|
123 |
+
opacity: 1;
|
124 |
+
}
|
125 |
+
50% {
|
126 |
+
opacity: 0.5;
|
127 |
+
}
|
128 |
+
}
|
129 |
+
|
130 |
+
@keyframes shimmer {
|
131 |
+
0% {
|
132 |
+
background-position: -1000px 0;
|
133 |
+
}
|
134 |
+
100% {
|
135 |
+
background-position: 1000px 0;
|
136 |
+
}
|
137 |
+
}
|
138 |
+
|
139 |
+
/* Animation Classes */
|
140 |
+
.fade-in {
|
141 |
+
animation: fadeIn 0.5s ease-out;
|
142 |
+
}
|
143 |
+
|
144 |
+
.slide-in {
|
145 |
+
animation: slideIn 0.3s ease-out;
|
146 |
+
}
|
147 |
+
|
148 |
+
.animate-pulse {
|
149 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
150 |
+
}
|
151 |
+
|
152 |
+
/* Loading States */
|
153 |
+
.loading-spinner {
|
154 |
+
animation: spin 2s linear infinite;
|
155 |
+
}
|
156 |
+
|
157 |
+
@keyframes spin {
|
158 |
+
from {
|
159 |
+
transform: rotate(0deg);
|
160 |
+
}
|
161 |
+
to {
|
162 |
+
transform: rotate(360deg);
|
163 |
+
}
|
164 |
+
}
|
165 |
+
|
166 |
+
/* Gradient backgrounds */
|
167 |
+
.gradient-primary {
|
168 |
+
background: linear-gradient(135deg, oklch(var(--p)) 0%, oklch(var(--s)) 100%);
|
169 |
+
}
|
170 |
+
|
171 |
+
.gradient-success {
|
172 |
+
background: linear-gradient(135deg, oklch(var(--su)) 0%, oklch(var(--a)) 100%);
|
173 |
+
}
|
174 |
+
|
175 |
+
.gradient-warning {
|
176 |
+
background: linear-gradient(135deg, oklch(var(--wa)) 0%, oklch(var(--wa) / 0.8) 100%);
|
177 |
+
}
|
178 |
+
|
179 |
+
.gradient-error {
|
180 |
+
background: linear-gradient(135deg, oklch(var(--er)) 0%, oklch(var(--er) / 0.8) 100%);
|
181 |
+
}
|
182 |
+
|
183 |
+
/* Card hover effects */
|
184 |
+
.card {
|
185 |
+
transition: all 0.3s ease;
|
186 |
+
position: relative;
|
187 |
+
z-index: 1;
|
188 |
+
border-radius: var(--rounded-box);
|
189 |
+
}
|
190 |
+
|
191 |
+
.card:hover {
|
192 |
+
transform: translateY(-2px);
|
193 |
+
}
|
194 |
+
|
195 |
+
/* Form enhancements */
|
196 |
+
.form-control input:focus,
|
197 |
+
.form-control textarea:focus {
|
198 |
+
outline: none;
|
199 |
+
box-shadow: 0 0 0 3px oklch(var(--p) / 0.1);
|
200 |
+
}
|
201 |
+
|
202 |
+
/* Status indicators */
|
203 |
+
.status-pending {
|
204 |
+
background: linear-gradient(90deg, oklch(var(--wa)), oklch(var(--wa) / 0.8));
|
205 |
+
background-size: 200% 200%;
|
206 |
+
animation: gradient-shift 2s ease-in-out infinite;
|
207 |
+
}
|
208 |
+
|
209 |
+
.status-in-progress {
|
210 |
+
background: linear-gradient(90deg, oklch(var(--in)), oklch(var(--in) / 0.8));
|
211 |
+
background-size: 200% 200%;
|
212 |
+
animation: gradient-shift 2s ease-in-out infinite;
|
213 |
+
}
|
214 |
+
|
215 |
+
@keyframes gradient-shift {
|
216 |
+
0% { background-position: 0% 50%; }
|
217 |
+
50% { background-position: 100% 50%; }
|
218 |
+
100% { background-position: 0% 50%; }
|
219 |
+
}
|
220 |
+
|
221 |
+
/* Steps animation */
|
222 |
+
.step.step-primary {
|
223 |
+
animation: step-complete 0.5s ease-in-out;
|
224 |
+
}
|
225 |
+
|
226 |
+
@keyframes step-complete {
|
227 |
+
0% { transform: scale(0.8); opacity: 0; }
|
228 |
+
100% { transform: scale(1); opacity: 1; }
|
229 |
+
}
|
230 |
+
|
231 |
+
/* Copy button animation */
|
232 |
+
.copy-success {
|
233 |
+
animation: copy-feedback 0.3s ease-in-out;
|
234 |
+
}
|
235 |
+
|
236 |
+
@keyframes copy-feedback {
|
237 |
+
0% { transform: scale(1); }
|
238 |
+
50% { transform: scale(1.1); }
|
239 |
+
100% { transform: scale(1); }
|
240 |
+
}
|
241 |
+
|
242 |
+
/* Mobile responsiveness */
|
243 |
+
@media (max-width: 768px) {
|
244 |
+
.steps-horizontal {
|
245 |
+
display: block !important;
|
246 |
+
}
|
247 |
+
|
248 |
+
.hero-content {
|
249 |
+
padding: 2rem 1rem !important;
|
250 |
+
}
|
251 |
+
|
252 |
+
.card-body {
|
253 |
+
padding: 1.5rem !important;
|
254 |
+
}
|
255 |
+
}
|
256 |
+
|
257 |
+
/* Remove old dark mode color overrides since we now have proper theme variables */
|
258 |
+
[data-theme="dark"] .gradient-primary {
|
259 |
+
/* Let it use the theme colors defined above */
|
260 |
+
opacity: 1;
|
261 |
+
}
|
262 |
+
|
263 |
+
/* Animations for theme transitions */
|
264 |
+
* {
|
265 |
+
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
266 |
+
}
|
267 |
+
|
268 |
+
/* Button Enhancements */
|
269 |
+
.btn {
|
270 |
+
transition: all 0.2s ease;
|
271 |
+
position: relative;
|
272 |
+
overflow: hidden;
|
273 |
+
z-index: 2;
|
274 |
+
border-radius: var(--rounded-btn);
|
275 |
+
}
|
276 |
+
|
277 |
+
.btn::before {
|
278 |
+
content: '';
|
279 |
+
position: absolute;
|
280 |
+
top: 50%;
|
281 |
+
left: 50%;
|
282 |
+
width: 0;
|
283 |
+
height: 0;
|
284 |
+
background: rgba(255, 255, 255, 0.2);
|
285 |
+
border-radius: 50%;
|
286 |
+
transform: translate(-50%, -50%);
|
287 |
+
transition: width 0.3s, height 0.3s;
|
288 |
+
}
|
289 |
+
|
290 |
+
.btn:active::before {
|
291 |
+
width: 300px;
|
292 |
+
height: 300px;
|
293 |
+
}
|
294 |
+
|
295 |
+
/* Status Badge Animations */
|
296 |
+
.badge {
|
297 |
+
transition: all 0.2s ease;
|
298 |
+
}
|
299 |
+
|
300 |
+
/* Copy Button Enhancement */
|
301 |
+
.copy-btn:active {
|
302 |
+
transform: scale(0.95);
|
303 |
+
}
|
304 |
+
|
305 |
+
/* Toast Animations */
|
306 |
+
.toast-enter {
|
307 |
+
animation: slideIn 0.3s ease-out;
|
308 |
+
}
|
309 |
+
|
310 |
+
.toast-exit {
|
311 |
+
animation: fadeOut 0.3s ease-out;
|
312 |
+
}
|
313 |
+
|
314 |
+
@keyframes fadeOut {
|
315 |
+
from {
|
316 |
+
opacity: 1;
|
317 |
+
transform: translateY(0);
|
318 |
+
}
|
319 |
+
to {
|
320 |
+
opacity: 0;
|
321 |
+
transform: translateY(-10px);
|
322 |
+
}
|
323 |
+
}
|
324 |
+
|
325 |
+
/* Responsive Design Enhancements */
|
326 |
+
@media (max-width: 768px) {
|
327 |
+
.hero h1 {
|
328 |
+
font-size: 2.5rem;
|
329 |
+
}
|
330 |
+
|
331 |
+
.stats {
|
332 |
+
flex-direction: column;
|
333 |
+
}
|
334 |
+
}
|
335 |
+
|
336 |
+
/* Shimmer Effect for Loading */
|
337 |
+
.shimmer {
|
338 |
+
background: linear-gradient(
|
339 |
+
90deg,
|
340 |
+
rgba(255, 255, 255, 0) 0%,
|
341 |
+
rgba(255, 255, 255, 0.2) 50%,
|
342 |
+
rgba(255, 255, 255, 0) 100%
|
343 |
+
);
|
344 |
+
background-size: 1000px 100%;
|
345 |
+
animation: shimmer 2s infinite;
|
346 |
+
}
|
347 |
+
|
348 |
+
/* Custom Scrollbar */
|
349 |
+
::-webkit-scrollbar {
|
350 |
+
width: 8px;
|
351 |
+
height: 8px;
|
352 |
+
}
|
353 |
+
|
354 |
+
::-webkit-scrollbar-track {
|
355 |
+
background: oklch(var(--b2));
|
356 |
+
}
|
357 |
+
|
358 |
+
::-webkit-scrollbar-thumb {
|
359 |
+
background: oklch(var(--b3));
|
360 |
+
border-radius: 4px;
|
361 |
+
}
|
362 |
+
|
363 |
+
::-webkit-scrollbar-thumb:hover {
|
364 |
+
background: oklch(var(--bc) / 0.3);
|
365 |
+
}
|
366 |
+
|
367 |
+
/* Code Block Styling */
|
368 |
+
code {
|
369 |
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
370 |
+
font-size: 0.875rem;
|
371 |
+
padding: 0.125rem 0.25rem;
|
372 |
+
border-radius: var(--rounded-badge);
|
373 |
+
}
|
374 |
+
|
375 |
+
pre {
|
376 |
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
377 |
+
line-height: 1.5;
|
378 |
+
}
|
379 |
+
|
380 |
+
/* Status Update Transitions */
|
381 |
+
#status-container > * {
|
382 |
+
animation: fadeIn 0.5s ease-out;
|
383 |
+
}
|
384 |
+
|
385 |
+
/* Fixed Overlay Blur */
|
386 |
+
.backdrop-blur-sm {
|
387 |
+
backdrop-filter: blur(4px);
|
388 |
+
}
|
389 |
+
|
390 |
+
/* Icon Hover Effects */
|
391 |
+
[data-lucide]:hover {
|
392 |
+
opacity: 0.8;
|
393 |
+
transition: opacity 0.2s ease;
|
394 |
+
}
|
395 |
+
|
396 |
+
/* Fix z-index and overlay issues */
|
397 |
+
.card-body {
|
398 |
+
position: relative;
|
399 |
+
z-index: 1;
|
400 |
+
}
|
401 |
+
|
402 |
+
/* Ensure form controls are interactive */
|
403 |
+
.form-control {
|
404 |
+
position: relative;
|
405 |
+
z-index: 1;
|
406 |
+
}
|
407 |
+
|
408 |
+
.form-control input,
|
409 |
+
.form-control textarea,
|
410 |
+
.form-control select {
|
411 |
+
position: relative;
|
412 |
+
z-index: 2;
|
413 |
+
border-radius: var(--rounded-badge);
|
414 |
+
}
|
415 |
+
|
416 |
+
/* Ensure the loading overlay is visible when shown */
|
417 |
+
#loading-overlay .card {
|
418 |
+
pointer-events: auto;
|
419 |
+
}
|
420 |
+
|
421 |
+
/* Fix collapse toggle */
|
422 |
+
.collapse input[type="checkbox"] {
|
423 |
+
position: relative;
|
424 |
+
z-index: 3;
|
425 |
+
}
|
426 |
+
|
427 |
+
/* Ensure toast notifications are on top */
|
428 |
+
#toast-container {
|
429 |
+
z-index: 9999;
|
430 |
+
}
|
431 |
+
|
432 |
+
/* Fix potential Alpine.js element issues */
|
433 |
+
[x-data], [x-show], [x-ref] {
|
434 |
+
position: relative;
|
435 |
+
}
|
436 |
+
|
437 |
+
/* Ensure eye button for password toggle is clickable */
|
438 |
+
.btn-ghost.btn-xs {
|
439 |
+
z-index: 3;
|
440 |
+
}
|
441 |
+
|
442 |
+
/* Theme toggle button animation */
|
443 |
+
.theme-icon {
|
444 |
+
transition: transform 0.3s ease;
|
445 |
+
}
|
446 |
+
|
447 |
+
.btn-circle:hover .theme-icon {
|
448 |
+
transform: rotate(180deg);
|
449 |
+
}
|
450 |
+
|
451 |
+
/* Smooth theme transition */
|
452 |
+
html {
|
453 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
454 |
+
}
|
455 |
+
|
456 |
+
/* Add enhanced theme transition effects */
|
457 |
+
html[data-theme="light"] {
|
458 |
+
background-color: oklch(var(--b1));
|
459 |
+
color: oklch(var(--bc));
|
460 |
+
}
|
461 |
+
|
462 |
+
html[data-theme="dark"] {
|
463 |
+
background-color: oklch(var(--b1));
|
464 |
+
color: oklch(var(--bc));
|
465 |
+
}
|
466 |
+
|
467 |
+
/* Smooth transitions for theme changes */
|
468 |
+
html, body {
|
469 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
470 |
+
}
|
471 |
+
|
472 |
+
/* Enhanced button hover states */
|
473 |
+
.btn-primary:hover {
|
474 |
+
background-color: oklch(var(--p) / 0.9);
|
475 |
+
border-color: oklch(var(--p) / 0.9);
|
476 |
+
}
|
477 |
+
|
478 |
+
.btn-secondary:hover {
|
479 |
+
background-color: oklch(var(--s) / 0.9);
|
480 |
+
border-color: oklch(var(--s) / 0.9);
|
481 |
+
}
|
482 |
+
|
483 |
+
.btn-accent:hover {
|
484 |
+
background-color: oklch(var(--a) / 0.9);
|
485 |
+
border-color: oklch(var(--a) / 0.9);
|
486 |
+
}
|
487 |
+
|
488 |
+
/* Card enhancements with theme colors */
|
489 |
+
.card {
|
490 |
+
background-color: oklch(var(--b1));
|
491 |
+
border: 1px solid oklch(var(--b3) / 0.2);
|
492 |
+
}
|
493 |
+
|
494 |
+
.card:hover {
|
495 |
+
border-color: oklch(var(--p) / 0.3);
|
496 |
+
box-shadow: 0 4px 12px oklch(var(--bc) / 0.1);
|
497 |
+
}
|
498 |
+
|
499 |
+
/* Input field enhancements */
|
500 |
+
.input, .textarea, .select {
|
501 |
+
background-color: oklch(var(--b1));
|
502 |
+
border-color: oklch(var(--bc) / 0.2);
|
503 |
+
border-radius: var(--rounded-badge);
|
504 |
+
}
|
505 |
+
|
506 |
+
.input:focus, .textarea:focus, .select:focus {
|
507 |
+
border-color: oklch(var(--p));
|
508 |
+
background-color: oklch(var(--b1));
|
509 |
+
}
|
510 |
+
|
511 |
+
/* Badge color enhancements */
|
512 |
+
.badge-primary {
|
513 |
+
background-color: oklch(var(--p));
|
514 |
+
color: oklch(var(--pc));
|
515 |
+
}
|
516 |
+
|
517 |
+
.badge-secondary {
|
518 |
+
background-color: oklch(var(--s));
|
519 |
+
color: oklch(var(--sc));
|
520 |
+
}
|
521 |
+
|
522 |
+
.badge-accent {
|
523 |
+
background-color: oklch(var(--a));
|
524 |
+
color: oklch(var(--ac));
|
525 |
+
}
|
526 |
+
|
527 |
+
.badge-success {
|
528 |
+
background-color: oklch(var(--su));
|
529 |
+
color: oklch(var(--suc));
|
530 |
+
}
|
531 |
+
|
532 |
+
.badge-warning {
|
533 |
+
background-color: oklch(var(--wa));
|
534 |
+
color: oklch(var(--wac));
|
535 |
+
}
|
536 |
+
|
537 |
+
.badge-error {
|
538 |
+
background-color: oklch(var(--er));
|
539 |
+
color: oklch(var(--erc));
|
540 |
+
}
|
541 |
+
|
542 |
+
.badge-info {
|
543 |
+
background-color: oklch(var(--in));
|
544 |
+
color: oklch(var(--inc));
|
545 |
+
}
|
546 |
+
|
547 |
+
/* Alert color enhancements */
|
548 |
+
.alert-success {
|
549 |
+
background-color: oklch(var(--su) / 0.2);
|
550 |
+
border-color: oklch(var(--su));
|
551 |
+
color: oklch(var(--suc));
|
552 |
+
}
|
553 |
+
|
554 |
+
.alert-warning {
|
555 |
+
background-color: oklch(var(--wa) / 0.2);
|
556 |
+
border-color: oklch(var(--wa));
|
557 |
+
color: oklch(var(--wac));
|
558 |
+
}
|
559 |
+
|
560 |
+
.alert-error {
|
561 |
+
background-color: oklch(var(--er) / 0.2);
|
562 |
+
border-color: oklch(var(--er));
|
563 |
+
color: oklch(var(--erc));
|
564 |
+
}
|
565 |
+
|
566 |
+
.alert-info {
|
567 |
+
background-color: oklch(var(--in) / 0.2);
|
568 |
+
border-color: oklch(var(--in));
|
569 |
+
color: oklch(var(--inc));
|
570 |
+
}
|
571 |
+
|
572 |
+
/* Light theme alert text color fixes for better readability */
|
573 |
+
[data-theme="light"] .alert-success {
|
574 |
+
background-color: oklch(var(--su) / 0.15);
|
575 |
+
border-color: oklch(var(--su) / 0.5);
|
576 |
+
color: oklch(25% 0.162 321.406); /* Darker text for better contrast */
|
577 |
+
}
|
578 |
+
|
579 |
+
[data-theme="light"] .alert-warning {
|
580 |
+
background-color: oklch(var(--wa) / 0.15);
|
581 |
+
border-color: oklch(var(--wa) / 0.5);
|
582 |
+
color: oklch(25% 0.159 52.023); /* Darker text for better contrast */
|
583 |
+
}
|
584 |
+
|
585 |
+
[data-theme="light"] .alert-error {
|
586 |
+
background-color: oklch(var(--er) / 0.15);
|
587 |
+
border-color: oklch(var(--er) / 0.5);
|
588 |
+
color: oklch(25% 0.208 28.717); /* Darker text for better contrast */
|
589 |
+
}
|
590 |
+
|
591 |
+
[data-theme="light"] .alert-info {
|
592 |
+
background-color: oklch(var(--in) / 0.15);
|
593 |
+
border-color: oklch(var(--in) / 0.5);
|
594 |
+
color: oklch(25% 0.094 217.284); /* Darker text for better contrast */
|
595 |
+
}
|
596 |
+
|
597 |
+
/* Alert icon styling */
|
598 |
+
.alert > :first-child {
|
599 |
+
flex-shrink: 0;
|
600 |
+
}
|
601 |
+
|
602 |
+
/* Ensure alert text is readable */
|
603 |
+
.alert {
|
604 |
+
font-weight: 500;
|
605 |
+
}
|
606 |
+
|
607 |
+
/* Navbar enhancement */
|
608 |
+
.navbar {
|
609 |
+
background-color: oklch(var(--p));
|
610 |
+
color: oklch(var(--pc));
|
611 |
+
}
|
612 |
+
|
613 |
+
/* Footer enhancement */
|
614 |
+
.footer {
|
615 |
+
background-color: oklch(var(--b2));
|
616 |
+
color: oklch(var(--bc));
|
617 |
+
}
|
618 |
+
|
619 |
+
/* Link colors */
|
620 |
+
.link {
|
621 |
+
color: oklch(var(--p));
|
622 |
+
}
|
623 |
+
|
624 |
+
.link:hover {
|
625 |
+
color: oklch(var(--p) / 0.8);
|
626 |
+
}
|
627 |
+
|
628 |
+
/* Code block colors */
|
629 |
+
code {
|
630 |
+
background-color: oklch(var(--b2));
|
631 |
+
color: oklch(var(--bc));
|
632 |
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
633 |
+
font-size: 0.875rem;
|
634 |
+
padding: 0.125rem 0.25rem;
|
635 |
+
border-radius: var(--rounded-badge);
|
636 |
+
}
|
637 |
+
|
638 |
+
pre {
|
639 |
+
background-color: oklch(var(--b2));
|
640 |
+
color: oklch(var(--bc));
|
641 |
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
642 |
+
line-height: 1.5;
|
643 |
+
}
|
644 |
+
|
645 |
+
/* Enhanced visual effects for new themes */
|
646 |
+
/* CMYK theme light mode special effects */
|
647 |
+
[data-theme="light"] .hero {
|
648 |
+
background: linear-gradient(135deg,
|
649 |
+
oklch(var(--p) / 0.9) 0%,
|
650 |
+
oklch(var(--s) / 0.9) 50%,
|
651 |
+
oklch(var(--a) / 0.9) 100%
|
652 |
+
);
|
653 |
+
}
|
654 |
+
|
655 |
+
/* Aqua theme dark mode special effects */
|
656 |
+
[data-theme="dark"] .hero {
|
657 |
+
background: linear-gradient(135deg,
|
658 |
+
oklch(var(--p) / 0.8) 0%,
|
659 |
+
oklch(var(--s) / 0.8) 50%,
|
660 |
+
oklch(var(--a) / 0.8) 100%
|
661 |
+
);
|
662 |
+
}
|
663 |
+
|
664 |
+
/* Enhanced card styling with new border radius */
|
665 |
+
.card {
|
666 |
+
overflow: hidden;
|
667 |
+
}
|
668 |
+
|
669 |
+
/* Modal and dialog styling */
|
670 |
+
.modal-box {
|
671 |
+
border-radius: var(--rounded-box);
|
672 |
+
}
|
673 |
+
|
674 |
+
/* Toast notifications with new radius */
|
675 |
+
.alert {
|
676 |
+
border-radius: var(--rounded-btn);
|
677 |
+
}
|
678 |
+
|
679 |
+
/* Enhanced button styles for CMYK/Aqua themes */
|
680 |
+
[data-theme="light"] .btn-primary {
|
681 |
+
box-shadow: 0 2px 8px oklch(var(--p) / 0.3);
|
682 |
+
}
|
683 |
+
|
684 |
+
[data-theme="light"] .btn-primary:hover {
|
685 |
+
box-shadow: 0 4px 12px oklch(var(--p) / 0.4);
|
686 |
+
}
|
687 |
+
|
688 |
+
[data-theme="dark"] .btn-primary {
|
689 |
+
box-shadow: 0 2px 8px oklch(var(--p) / 0.3);
|
690 |
+
}
|
691 |
+
|
692 |
+
[data-theme="dark"] .btn-primary:hover {
|
693 |
+
box-shadow: 0 4px 12px oklch(var(--p) / 0.4);
|
694 |
+
}
|
695 |
+
|
696 |
+
/* Special glow effects for dark theme */
|
697 |
+
[data-theme="dark"] .card:hover {
|
698 |
+
box-shadow: 0 0 20px oklch(var(--p) / 0.2),
|
699 |
+
0 4px 12px oklch(var(--bc) / 0.1);
|
700 |
+
}
|
701 |
+
|
702 |
+
[data-theme="dark"] .input:focus,
|
703 |
+
[data-theme="dark"] .textarea:focus {
|
704 |
+
box-shadow: 0 0 0 3px oklch(var(--p) / 0.2),
|
705 |
+
0 0 20px oklch(var(--p) / 0.1);
|
706 |
+
}
|
707 |
+
|
708 |
+
/* Enhance badge styling with new themes */
|
709 |
+
[data-theme="light"] .badge {
|
710 |
+
border-radius: var(--rounded-badge);
|
711 |
+
font-weight: 600;
|
712 |
+
}
|
713 |
+
|
714 |
+
[data-theme="dark"] .badge {
|
715 |
+
border-radius: var(--rounded-badge);
|
716 |
+
font-weight: 500;
|
717 |
+
}
|
718 |
+
|
719 |
+
/* Update divider styling for new themes */
|
720 |
+
[data-theme="light"] .divider {
|
721 |
+
color: oklch(var(--bc) / 0.3);
|
722 |
+
}
|
723 |
+
|
724 |
+
[data-theme="dark"] .divider {
|
725 |
+
color: oklch(var(--bc) / 0.3);
|
726 |
+
}
|
727 |
+
|
728 |
+
/* Stat component enhancement */
|
729 |
+
[data-theme="light"] .stats {
|
730 |
+
border-radius: var(--rounded-box);
|
731 |
+
}
|
732 |
+
|
733 |
+
[data-theme="dark"] .stats {
|
734 |
+
border-radius: var(--rounded-box);
|
735 |
+
background: oklch(var(--b1) / 0.5);
|
736 |
+
backdrop-filter: blur(10px);
|
737 |
+
}
|
app/store.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from typing import Optional
|
3 |
+
from redis import StrictRedis
|
4 |
+
from .config import get_settings
|
5 |
+
from .models import DeployStatus
|
6 |
+
|
7 |
+
_settings = get_settings()
|
8 |
+
redis_client = StrictRedis.from_url(
|
9 |
+
_settings.redis_url,
|
10 |
+
decode_responses=True,
|
11 |
+
# 连接池配置
|
12 |
+
max_connections=50, # 最大连接数
|
13 |
+
retry_on_timeout=True, # 超时重试
|
14 |
+
socket_timeout=15, # socket 超时
|
15 |
+
socket_connect_timeout=15, # 连接超时
|
16 |
+
health_check_interval=300, # 健康检查间隔
|
17 |
+
)
|
18 |
+
|
19 |
+
class TaskStore:
|
20 |
+
prefix = "task:"
|
21 |
+
|
22 |
+
@classmethod
|
23 |
+
def _key(cls, task_id: str) -> str:
|
24 |
+
return f"{cls.prefix}{task_id}"
|
25 |
+
|
26 |
+
@classmethod
|
27 |
+
def save(cls, status: DeployStatus) -> None:
|
28 |
+
redis_client.hset(
|
29 |
+
cls._key(status.task_id),
|
30 |
+
mapping={
|
31 |
+
"status": status.status,
|
32 |
+
"detail": json.dumps(status.detail) if status.detail else "",
|
33 |
+
},
|
34 |
+
)
|
35 |
+
redis_client.expire(cls._key(status.task_id), 24 * 3600)
|
36 |
+
|
37 |
+
@classmethod
|
38 |
+
def load(cls, task_id: str) -> Optional[DeployStatus]:
|
39 |
+
data = redis_client.hgetall(cls._key(task_id))
|
40 |
+
if not data:
|
41 |
+
return None
|
42 |
+
detail = json.loads(data.get("detail", "null")) if data.get("detail") else None
|
43 |
+
return DeployStatus(task_id=task_id, status=data.get("status", "UNKNOWN"), detail=detail)
|
app/templates/base.html
ADDED
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en" data-theme="light">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title data-i18n="nav.title">HF Space Deployer</title>
|
7 |
+
|
8 |
+
<!-- Tailwind CSS + DaisyUI -->
|
9 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css" />
|
10 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
11 |
+
|
12 |
+
<!-- Custom CSS -->
|
13 |
+
<link href="/static/style.css" rel="stylesheet" type="text/css" />
|
14 |
+
|
15 |
+
<!-- HTMX -->
|
16 |
+
<script src="https://unpkg.com/[email protected]"></script>
|
17 |
+
|
18 |
+
<!-- Icons -->
|
19 |
+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
20 |
+
|
21 |
+
<!-- Alpine.js for reactive components -->
|
22 |
+
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
|
23 |
+
|
24 |
+
<!-- i18n -->
|
25 |
+
<script src="/static/i18n.js"></script>
|
26 |
+
|
27 |
+
<style>
|
28 |
+
.loading-spinner {
|
29 |
+
animation: spin 2s linear infinite;
|
30 |
+
}
|
31 |
+
@keyframes spin {
|
32 |
+
from { transform: rotate(0deg); }
|
33 |
+
to { transform: rotate(360deg); }
|
34 |
+
}
|
35 |
+
.fade-in {
|
36 |
+
animation: fadeIn 0.5s ease-in-out;
|
37 |
+
}
|
38 |
+
@keyframes fadeIn {
|
39 |
+
from { opacity: 0; transform: translateY(10px); }
|
40 |
+
to { opacity: 1; transform: translateY(0); }
|
41 |
+
}
|
42 |
+
</style>
|
43 |
+
</head>
|
44 |
+
<body class="min-h-screen bg-base-100">
|
45 |
+
<!-- Navigation -->
|
46 |
+
<div class="navbar bg-primary text-primary-content shadow-lg sticky top-0 z-50">
|
47 |
+
<div class="container mx-auto">
|
48 |
+
<div class="flex-1">
|
49 |
+
<a href="/" class="btn btn-ghost text-xl font-bold">
|
50 |
+
<i data-lucide="rocket" class="w-6 h-6 mr-2"></i>
|
51 |
+
<span data-i18n="nav.title">HF Space Deployer</span>
|
52 |
+
</a>
|
53 |
+
</div>
|
54 |
+
<div class="flex-none space-x-2">
|
55 |
+
<button
|
56 |
+
class="btn btn-ghost btn-circle"
|
57 |
+
onclick="toggleLanguage()"
|
58 |
+
data-i18n-title="nav.language"
|
59 |
+
title="Language"
|
60 |
+
>
|
61 |
+
<span class="text-lg font-bold lang-icon">EN</span>
|
62 |
+
</button>
|
63 |
+
<button
|
64 |
+
class="btn btn-ghost btn-circle"
|
65 |
+
onclick="toggleTheme()"
|
66 |
+
data-i18n-title="nav.theme"
|
67 |
+
title="Toggle Theme"
|
68 |
+
>
|
69 |
+
<i data-lucide="sun" class="w-5 h-5 theme-icon"></i>
|
70 |
+
</button>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
|
75 |
+
<!-- Toast Container -->
|
76 |
+
<div id="toast-container" class="fixed top-20 right-4 z-50 space-y-2"></div>
|
77 |
+
|
78 |
+
<!-- Main Content -->
|
79 |
+
<main class="container mx-auto px-4 py-8 max-w-6xl">
|
80 |
+
{% block content %}{% endblock %}
|
81 |
+
</main>
|
82 |
+
|
83 |
+
<!-- Footer -->
|
84 |
+
<footer class="footer footer-center p-8 bg-base-200 text-base-content mt-16">
|
85 |
+
<div>
|
86 |
+
<p class="font-bold text-lg" data-i18n="footer.title">HF Space Deployer</p>
|
87 |
+
<p class="text-sm opacity-70" data-i18n="footer.desc">Quick deployment tool</p>
|
88 |
+
</div>
|
89 |
+
<div class="flex items-center space-x-4 text-sm">
|
90 |
+
<a href="https://github.com/kfcx/HFSpaceDeploy.git" target="_blank" class="link link-hover">GitHub</a>
|
91 |
+
<span>•</span>
|
92 |
+
<a href="https://huggingface.co" target="_blank" class="link link-hover">HuggingFace</a>
|
93 |
+
</div>
|
94 |
+
</footer>
|
95 |
+
|
96 |
+
<script>
|
97 |
+
// Language icon update
|
98 |
+
function updateLanguageIcon() {
|
99 |
+
const langIcon = document.querySelector('.lang-icon');
|
100 |
+
langIcon.textContent = currentLang === 'en' ? 'EN' : '中';
|
101 |
+
}
|
102 |
+
|
103 |
+
// Override toggleLanguage to update icon
|
104 |
+
if (typeof window.originalToggleLanguage === 'undefined') {
|
105 |
+
window.originalToggleLanguage = toggleLanguage;
|
106 |
+
window.toggleLanguage = function() {
|
107 |
+
window.originalToggleLanguage();
|
108 |
+
updateLanguageIcon();
|
109 |
+
};
|
110 |
+
}
|
111 |
+
|
112 |
+
// Theme switcher - simplified for light/dark only
|
113 |
+
function toggleTheme() {
|
114 |
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
115 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
116 |
+
setTheme(newTheme);
|
117 |
+
}
|
118 |
+
|
119 |
+
function setTheme(theme) {
|
120 |
+
document.documentElement.setAttribute('data-theme', theme);
|
121 |
+
localStorage.setItem('theme', theme);
|
122 |
+
|
123 |
+
// Update theme icon
|
124 |
+
const themeIcon = document.querySelector('.theme-icon');
|
125 |
+
if (theme === 'dark') {
|
126 |
+
themeIcon.setAttribute('data-lucide', 'moon');
|
127 |
+
} else {
|
128 |
+
themeIcon.setAttribute('data-lucide', 'sun');
|
129 |
+
}
|
130 |
+
lucide.createIcons();
|
131 |
+
}
|
132 |
+
|
133 |
+
// Load saved theme (default to light)
|
134 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
135 |
+
setTheme(savedTheme);
|
136 |
+
|
137 |
+
// Initialize Lucide icons
|
138 |
+
lucide.createIcons();
|
139 |
+
|
140 |
+
// HTMX event listeners
|
141 |
+
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
142 |
+
// Reinitialize icons after HTMX requests
|
143 |
+
lucide.createIcons();
|
144 |
+
// Update translations for new content
|
145 |
+
updatePageTranslations();
|
146 |
+
});
|
147 |
+
|
148 |
+
// Copy to clipboard function
|
149 |
+
function copyToClipboard(text) {
|
150 |
+
navigator.clipboard.writeText(text).then(function() {
|
151 |
+
showToast(t('toast.copied'), 'success');
|
152 |
+
}).catch(function(err) {
|
153 |
+
showToast(t('toast.copyFailed'), 'error');
|
154 |
+
});
|
155 |
+
}
|
156 |
+
|
157 |
+
// Toast notification system
|
158 |
+
function showToast(message, type = 'info') {
|
159 |
+
const toastContainer = document.getElementById('toast-container');
|
160 |
+
const toast = document.createElement('div');
|
161 |
+
toast.className = `alert alert-${type} shadow-lg fade-in min-w-[300px]`;
|
162 |
+
|
163 |
+
const icons = {
|
164 |
+
'success': 'check-circle',
|
165 |
+
'error': 'x-circle',
|
166 |
+
'warning': 'alert-triangle',
|
167 |
+
'info': 'info'
|
168 |
+
};
|
169 |
+
|
170 |
+
toast.innerHTML = `
|
171 |
+
<i data-lucide="${icons[type]}" class="w-5 h-5"></i>
|
172 |
+
<span>${message}</span>
|
173 |
+
`;
|
174 |
+
|
175 |
+
toastContainer.appendChild(toast);
|
176 |
+
lucide.createIcons();
|
177 |
+
|
178 |
+
// Remove toast after 3 seconds
|
179 |
+
setTimeout(() => {
|
180 |
+
toast.style.opacity = '0';
|
181 |
+
toast.style.transform = 'translateY(-10px)';
|
182 |
+
setTimeout(() => toast.remove(), 300);
|
183 |
+
}, 3000);
|
184 |
+
}
|
185 |
+
|
186 |
+
// HTMX extensions
|
187 |
+
document.body.addEventListener('htmx:responseError', function(evt) {
|
188 |
+
showToast(t('toast.requestFailed'), 'error');
|
189 |
+
});
|
190 |
+
|
191 |
+
// Global event listener for custom events
|
192 |
+
document.addEventListener('show-toast', function(event) {
|
193 |
+
const { message, type } = event.detail;
|
194 |
+
showToast(message, type);
|
195 |
+
});
|
196 |
+
|
197 |
+
// Initialize page functionality
|
198 |
+
document.addEventListener('DOMContentLoaded', function() {
|
199 |
+
// Initialize all icons
|
200 |
+
if (typeof lucide !== 'undefined') {
|
201 |
+
lucide.createIcons();
|
202 |
+
}
|
203 |
+
|
204 |
+
// Update language icon
|
205 |
+
updateLanguageIcon();
|
206 |
+
});
|
207 |
+
|
208 |
+
// Ensure Alpine.js is loaded before dependent components
|
209 |
+
if (typeof Alpine !== 'undefined') {
|
210 |
+
document.addEventListener('alpine:init', () => {
|
211 |
+
console.log('Alpine.js initialized');
|
212 |
+
});
|
213 |
+
}
|
214 |
+
</script>
|
215 |
+
</body>
|
216 |
+
</html>
|
app/templates/deploy_status.html
ADDED
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}部署状态 - HuggingFace Space 部署器{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="max-w-2xl mx-auto">
|
7 |
+
<!-- Header -->
|
8 |
+
<div class="text-center mb-8">
|
9 |
+
<h1 class="text-3xl font-bold mb-4">
|
10 |
+
<i data-lucide="activity" class="w-8 h-8 inline mr-2"></i>
|
11 |
+
部署状态监控
|
12 |
+
</h1>
|
13 |
+
<p class="text-base-content/70">任务 ID: <code class="bg-base-300 px-2 py-1 rounded">{{ task_id }}</code></p>
|
14 |
+
</div>
|
15 |
+
|
16 |
+
<!-- Deployment Status Card -->
|
17 |
+
<div class="card bg-base-100 shadow-2xl border border-base-300 fade-in max-w-4xl mx-auto">
|
18 |
+
<div class="card-body">
|
19 |
+
<h2 class="card-title text-2xl mb-6 flex items-center">
|
20 |
+
<i data-lucide="activity" class="w-6 h-6 mr-2"></i>
|
21 |
+
<span data-i18n="status.title">Deployment Status</span>
|
22 |
+
</h2>
|
23 |
+
|
24 |
+
<!-- Task Info -->
|
25 |
+
<div class="bg-base-200 rounded-lg p-4 mb-6">
|
26 |
+
<div class="flex items-center justify-between">
|
27 |
+
<span class="text-sm font-semibold" data-i18n="status.taskId">Task ID</span>
|
28 |
+
<div class="flex items-center gap-2">
|
29 |
+
<code class="text-xs bg-base-300 px-2 py-1 rounded" id="task-id">{{ task_id }}</code>
|
30 |
+
<button
|
31 |
+
onclick="copyToClipboard('{{ task_id }}')"
|
32 |
+
class="btn btn-ghost btn-xs"
|
33 |
+
data-i18n-title="status.copy"
|
34 |
+
title="Copy"
|
35 |
+
>
|
36 |
+
<i data-lucide="copy" class="w-3 h-3"></i>
|
37 |
+
</button>
|
38 |
+
</div>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<!-- Status Container with fixed min-height to prevent jumping -->
|
43 |
+
<div id="status-container" class="min-h-[400px] relative transition-all duration-300">
|
44 |
+
<!-- Initial loading state -->
|
45 |
+
<div class="status-content absolute inset-0 flex items-center justify-center">
|
46 |
+
<div class="flex flex-col items-center justify-center py-8">
|
47 |
+
<div class="loading loading-spinner loading-lg text-primary mb-4"></div>
|
48 |
+
<h3 class="text-xl font-semibold mb-2" data-i18n="status.initializing">Initializing...</h3>
|
49 |
+
<p class="text-base-content/70" data-i18n="status.preparing">Preparing your Space...</p>
|
50 |
+
</div>
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
|
54 |
+
<!-- Auto-refresh indicator -->
|
55 |
+
<div id="refresh-indicator" class="mt-6 text-center transition-opacity duration-300">
|
56 |
+
<p class="text-xs text-base-content/50">
|
57 |
+
<i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1 animate-spin"></i>
|
58 |
+
<span data-i18n="status.autoRefresh">Auto-refresh every 2s</span>
|
59 |
+
</p>
|
60 |
+
</div>
|
61 |
+
|
62 |
+
<!-- Action Buttons -->
|
63 |
+
<div class="divider mt-8"></div>
|
64 |
+
|
65 |
+
<div class="flex gap-4 justify-center">
|
66 |
+
<a href="/" class="btn btn-ghost">
|
67 |
+
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
68 |
+
<span data-i18n="status.newDeploy">New Deploy</span>
|
69 |
+
</a>
|
70 |
+
<button
|
71 |
+
onclick="window.location.reload()"
|
72 |
+
class="btn btn-ghost"
|
73 |
+
>
|
74 |
+
<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i>
|
75 |
+
<span data-i18n="status.refresh">Refresh</span>
|
76 |
+
</button>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
</div>
|
80 |
+
|
81 |
+
<!-- Help -->
|
82 |
+
<div class="alert alert-info mt-8">
|
83 |
+
<i data-lucide="help-circle" class="w-5 h-5"></i>
|
84 |
+
<div>
|
85 |
+
<h3 class="font-bold">关于部署过程</h3>
|
86 |
+
<div class="text-sm mt-2">
|
87 |
+
<p>• <strong>PENDING:</strong> 任务已创建,等待开始处理</p>
|
88 |
+
<p>• <strong>IN_PROGRESS:</strong> 正在克隆代码并部署到 HuggingFace Spaces</p>
|
89 |
+
<p>• <strong>SUCCESS:</strong> 部署成功,您的应用已上线</p>
|
90 |
+
<p>• <strong>FAILED:</strong> 部署失败,请检查错误信息</p>
|
91 |
+
</div>
|
92 |
+
</div>
|
93 |
+
</div>
|
94 |
+
</div>
|
95 |
+
|
96 |
+
<style>
|
97 |
+
/* Smooth transitions for status updates */
|
98 |
+
.status-content {
|
99 |
+
transition: opacity 0.3s ease-in-out;
|
100 |
+
}
|
101 |
+
|
102 |
+
.status-content.fade-out {
|
103 |
+
opacity: 0;
|
104 |
+
}
|
105 |
+
|
106 |
+
.status-content.fade-in {
|
107 |
+
opacity: 1;
|
108 |
+
}
|
109 |
+
|
110 |
+
/* Prevent layout shifts */
|
111 |
+
#status-container {
|
112 |
+
display: flex;
|
113 |
+
align-items: center;
|
114 |
+
justify-content: center;
|
115 |
+
}
|
116 |
+
|
117 |
+
/* Progress steps animation */
|
118 |
+
.steps .step {
|
119 |
+
transition: all 0.3s ease;
|
120 |
+
}
|
121 |
+
</style>
|
122 |
+
|
123 |
+
<script>
|
124 |
+
// Deployment status monitoring
|
125 |
+
const taskId = '{{ task_id }}';
|
126 |
+
let statusInterval = null;
|
127 |
+
let retryCount = 0;
|
128 |
+
const maxRetries = 3;
|
129 |
+
let currentStatus = null;
|
130 |
+
let lastUpdateTime = Date.now();
|
131 |
+
|
132 |
+
// Status templates
|
133 |
+
const statusTemplates = {
|
134 |
+
PENDING: () => `
|
135 |
+
<div class="flex flex-col items-center justify-center py-8">
|
136 |
+
<div class="loading loading-dots loading-lg text-info mb-4"></div>
|
137 |
+
<h3 class="text-xl font-semibold mb-2">${t('status.queued')}</h3>
|
138 |
+
<p class="text-base-content/70">${t('status.queued.desc')}</p>
|
139 |
+
|
140 |
+
<div class="mt-6">
|
141 |
+
<span class="badge badge-info badge-lg">
|
142 |
+
<i data-lucide="clock" class="w-4 h-4 mr-2"></i>
|
143 |
+
PENDING
|
144 |
+
</span>
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
`,
|
148 |
+
|
149 |
+
IN_PROGRESS: () => `
|
150 |
+
<div class="flex flex-col items-center justify-center py-8">
|
151 |
+
<div class="loading loading-spinner loading-lg text-primary mb-4"></div>
|
152 |
+
<h3 class="text-xl font-semibold mb-2">${t('status.progress')}</h3>
|
153 |
+
<p class="text-base-content/70">${t('status.progress.desc')}</p>
|
154 |
+
|
155 |
+
<div class="mt-6">
|
156 |
+
<span class="badge badge-primary badge-lg">
|
157 |
+
<i data-lucide="loader-2" class="w-4 h-4 mr-2 loading-spinner"></i>
|
158 |
+
IN PROGRESS
|
159 |
+
</span>
|
160 |
+
</div>
|
161 |
+
|
162 |
+
<div class="mt-8 w-full max-w-md">
|
163 |
+
<ul class="steps steps-vertical lg:steps-horizontal w-full">
|
164 |
+
<li class="step step-primary" data-step="1">Initialize</li>
|
165 |
+
<li class="step step-primary" data-step="2">Clone</li>
|
166 |
+
<li class="step step-primary" data-step="3">Build</li>
|
167 |
+
<li class="step" data-step="4">Deploy</li>
|
168 |
+
</ul>
|
169 |
+
</div>
|
170 |
+
</div>
|
171 |
+
`,
|
172 |
+
|
173 |
+
SUCCESS: (detail) => `
|
174 |
+
<div class="flex flex-col items-center justify-center py-8">
|
175 |
+
<div class="mb-4">
|
176 |
+
<div class="w-20 h-20 bg-success/20 rounded-full flex items-center justify-center">
|
177 |
+
<i data-lucide="check-circle" class="w-12 h-12 text-success"></i>
|
178 |
+
</div>
|
179 |
+
</div>
|
180 |
+
<h3 class="text-2xl font-bold mb-2 text-success">${t('status.success')}</h3>
|
181 |
+
<p class="text-base-content/70 mb-6">${t('status.success.desc')}</p>
|
182 |
+
|
183 |
+
<div class="mt-4">
|
184 |
+
<span class="badge badge-success badge-lg">
|
185 |
+
<i data-lucide="check" class="w-4 h-4 mr-2"></i>
|
186 |
+
SUCCESS
|
187 |
+
</span>
|
188 |
+
</div>
|
189 |
+
|
190 |
+
${detail && detail.space_url ? `
|
191 |
+
<div class="mt-8 p-6 bg-success/10 border border-success/20 rounded-lg w-full max-w-lg">
|
192 |
+
<div class="text-center">
|
193 |
+
<p class="text-sm text-base-content/70 mb-2">${t('status.url')}</p>
|
194 |
+
<div class="flex items-center justify-center gap-2 bg-base-100 p-3 rounded-lg">
|
195 |
+
<a
|
196 |
+
href="${detail.space_url}"
|
197 |
+
target="_blank"
|
198 |
+
class="link link-primary text-lg font-mono truncate max-w-sm"
|
199 |
+
>
|
200 |
+
${detail.space_url}
|
201 |
+
</a>
|
202 |
+
<button
|
203 |
+
onclick="copyToClipboard('${detail.space_url}')"
|
204 |
+
class="btn btn-ghost btn-sm"
|
205 |
+
title="${t('status.copy')}"
|
206 |
+
>
|
207 |
+
<i data-lucide="copy" class="w-4 h-4"></i>
|
208 |
+
</button>
|
209 |
+
</div>
|
210 |
+
</div>
|
211 |
+
|
212 |
+
<div class="mt-6 flex justify-center">
|
213 |
+
<a
|
214 |
+
href="${detail.space_url}"
|
215 |
+
target="_blank"
|
216 |
+
class="btn btn-success"
|
217 |
+
>
|
218 |
+
<i data-lucide="external-link" class="w-4 h-4 mr-2"></i>
|
219 |
+
<span>${t('status.visit')}</span>
|
220 |
+
</a>
|
221 |
+
</div>
|
222 |
+
</div>
|
223 |
+
` : ''}
|
224 |
+
</div>
|
225 |
+
`,
|
226 |
+
|
227 |
+
FAILED: (detail) => `
|
228 |
+
<div class="flex flex-col items-center justify-center py-8">
|
229 |
+
<div class="mb-4">
|
230 |
+
<div class="w-20 h-20 bg-error/20 rounded-full flex items-center justify-center">
|
231 |
+
<i data-lucide="x-circle" class="w-12 h-12 text-error"></i>
|
232 |
+
</div>
|
233 |
+
</div>
|
234 |
+
<h3 class="text-2xl font-bold mb-2 text-error">${t('status.failed')}</h3>
|
235 |
+
<p class="text-base-content/70 mb-6">${t('status.failed.desc')}</p>
|
236 |
+
|
237 |
+
<div class="mt-4">
|
238 |
+
<span class="badge badge-error badge-lg">
|
239 |
+
<i data-lucide="x" class="w-4 h-4 mr-2"></i>
|
240 |
+
FAILED
|
241 |
+
</span>
|
242 |
+
</div>
|
243 |
+
|
244 |
+
${detail && detail.error ? `
|
245 |
+
<div class="mt-8 p-6 bg-error/10 border border-error/20 rounded-lg w-full max-w-lg">
|
246 |
+
<div class="flex items-start gap-3">
|
247 |
+
<i data-lucide="alert-triangle" class="w-5 h-5 text-error mt-0.5"></i>
|
248 |
+
<div class="flex-1">
|
249 |
+
<p class="font-semibold text-error mb-2">${t('status.error')}</p>
|
250 |
+
<pre class="text-sm bg-base-100 p-3 rounded overflow-x-auto whitespace-pre-wrap">${detail.error}</pre>
|
251 |
+
</div>
|
252 |
+
</div>
|
253 |
+
|
254 |
+
<div class="mt-6">
|
255 |
+
<h4 class="font-semibold mb-2">${t('status.troubleshoot')}</h4>
|
256 |
+
<ul class="text-sm space-y-1 text-base-content/70">
|
257 |
+
<li>• ${t('req.dockerfile')}</li>
|
258 |
+
<li>• ${t('req.token')}</li>
|
259 |
+
<li>• Verify repository URL is accessible</li>
|
260 |
+
<li>• Check Space name is unique</li>
|
261 |
+
</ul>
|
262 |
+
</div>
|
263 |
+
</div>
|
264 |
+
` : ''}
|
265 |
+
</div>
|
266 |
+
`
|
267 |
+
};
|
268 |
+
|
269 |
+
// Smooth update function
|
270 |
+
function smoothUpdate(container, newContent) {
|
271 |
+
const currentContent = container.querySelector('.status-content');
|
272 |
+
if (!currentContent) {
|
273 |
+
container.innerHTML = `<div class="status-content">${newContent}</div>`;
|
274 |
+
return;
|
275 |
+
}
|
276 |
+
|
277 |
+
// Create new content element
|
278 |
+
const newElement = document.createElement('div');
|
279 |
+
newElement.className = 'status-content absolute inset-0 flex items-center justify-center fade-out';
|
280 |
+
newElement.innerHTML = newContent;
|
281 |
+
|
282 |
+
// Add new content
|
283 |
+
container.appendChild(newElement);
|
284 |
+
|
285 |
+
// Fade out old content and fade in new content
|
286 |
+
currentContent.classList.add('fade-out');
|
287 |
+
|
288 |
+
setTimeout(() => {
|
289 |
+
newElement.classList.remove('fade-out');
|
290 |
+
newElement.classList.add('fade-in');
|
291 |
+
|
292 |
+
setTimeout(() => {
|
293 |
+
currentContent.remove();
|
294 |
+
newElement.classList.remove('absolute');
|
295 |
+
}, 300);
|
296 |
+
}, 50);
|
297 |
+
}
|
298 |
+
|
299 |
+
// Fetch status from API
|
300 |
+
async function fetchStatus() {
|
301 |
+
try {
|
302 |
+
const response = await fetch(`/deploy/status/${taskId}`);
|
303 |
+
if (!response.ok) {
|
304 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
305 |
+
}
|
306 |
+
|
307 |
+
const data = await response.json();
|
308 |
+
|
309 |
+
// Only update if status changed or enough time has passed
|
310 |
+
if (data.status !== currentStatus || Date.now() - lastUpdateTime > 10000) {
|
311 |
+
updateStatus(data);
|
312 |
+
currentStatus = data.status;
|
313 |
+
lastUpdateTime = Date.now();
|
314 |
+
}
|
315 |
+
|
316 |
+
retryCount = 0; // Reset retry count on successful fetch
|
317 |
+
|
318 |
+
} catch (error) {
|
319 |
+
console.error('Failed to fetch status:', error);
|
320 |
+
retryCount++;
|
321 |
+
|
322 |
+
if (retryCount >= maxRetries) {
|
323 |
+
stopStatusPolling();
|
324 |
+
showError('Failed to fetch deployment status. Please refresh the page.');
|
325 |
+
}
|
326 |
+
}
|
327 |
+
}
|
328 |
+
|
329 |
+
// Update status display
|
330 |
+
function updateStatus(data) {
|
331 |
+
const container = document.getElementById('status-container');
|
332 |
+
const template = statusTemplates[data.status];
|
333 |
+
|
334 |
+
if (template) {
|
335 |
+
smoothUpdate(container, template(data.detail));
|
336 |
+
|
337 |
+
// Re-initialize icons after transition
|
338 |
+
setTimeout(() => {
|
339 |
+
if (typeof lucide !== 'undefined') {
|
340 |
+
lucide.createIcons();
|
341 |
+
}
|
342 |
+
|
343 |
+
// Update translations
|
344 |
+
if (typeof updatePageTranslations !== 'undefined') {
|
345 |
+
updatePageTranslations();
|
346 |
+
}
|
347 |
+
}, 350);
|
348 |
+
|
349 |
+
// Stop polling if final state
|
350 |
+
if (data.status === 'SUCCESS' || data.status === 'FAILED') {
|
351 |
+
stopStatusPolling();
|
352 |
+
|
353 |
+
// Show toast notification
|
354 |
+
if (data.status === 'SUCCESS') {
|
355 |
+
showToast(t('toast.deploySuccess'), 'success');
|
356 |
+
} else {
|
357 |
+
showToast(t('toast.deployFailed'), 'error');
|
358 |
+
}
|
359 |
+
}
|
360 |
+
}
|
361 |
+
}
|
362 |
+
|
363 |
+
// Show error message
|
364 |
+
function showError(message) {
|
365 |
+
const container = document.getElementById('status-container');
|
366 |
+
const errorContent = `
|
367 |
+
<div class="flex flex-col items-center justify-center py-8">
|
368 |
+
<div class="mb-4">
|
369 |
+
<div class="w-20 h-20 bg-error/20 rounded-full flex items-center justify-center">
|
370 |
+
<i data-lucide="alert-triangle" class="w-12 h-12 text-error"></i>
|
371 |
+
</div>
|
372 |
+
</div>
|
373 |
+
<h3 class="text-xl font-bold mb-2 text-error">Connection Error</h3>
|
374 |
+
<p class="text-base-content/70">${message}</p>
|
375 |
+
</div>
|
376 |
+
`;
|
377 |
+
|
378 |
+
smoothUpdate(container, errorContent);
|
379 |
+
|
380 |
+
setTimeout(() => {
|
381 |
+
if (typeof lucide !== 'undefined') {
|
382 |
+
lucide.createIcons();
|
383 |
+
}
|
384 |
+
}, 350);
|
385 |
+
}
|
386 |
+
|
387 |
+
// Start status polling
|
388 |
+
function startStatusPolling() {
|
389 |
+
// Initial fetch
|
390 |
+
fetchStatus();
|
391 |
+
|
392 |
+
// Set up interval
|
393 |
+
statusInterval = setInterval(fetchStatus, 2000);
|
394 |
+
}
|
395 |
+
|
396 |
+
// Stop status polling
|
397 |
+
function stopStatusPolling() {
|
398 |
+
if (statusInterval) {
|
399 |
+
clearInterval(statusInterval);
|
400 |
+
statusInterval = null;
|
401 |
+
}
|
402 |
+
|
403 |
+
// Fade out refresh indicator
|
404 |
+
const indicator = document.getElementById('refresh-indicator');
|
405 |
+
if (indicator) {
|
406 |
+
indicator.style.opacity = '0';
|
407 |
+
setTimeout(() => {
|
408 |
+
indicator.style.display = 'none';
|
409 |
+
}, 300);
|
410 |
+
}
|
411 |
+
}
|
412 |
+
|
413 |
+
// Initialize on page load
|
414 |
+
document.addEventListener('DOMContentLoaded', function() {
|
415 |
+
startStatusPolling();
|
416 |
+
|
417 |
+
// Re-initialize icons
|
418 |
+
setTimeout(() => {
|
419 |
+
if (typeof lucide !== 'undefined') {
|
420 |
+
lucide.createIcons();
|
421 |
+
}
|
422 |
+
}, 100);
|
423 |
+
});
|
424 |
+
|
425 |
+
// Clean up on page unload
|
426 |
+
window.addEventListener('beforeunload', function() {
|
427 |
+
stopStatusPolling();
|
428 |
+
});
|
429 |
+
</script>
|
430 |
+
{% endblock %}
|
app/templates/index.html
ADDED
@@ -0,0 +1,817 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Deploy to HuggingFace Spaces{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<!-- Hero Section -->
|
7 |
+
<div class="hero bg-gradient-to-br from-primary via-secondary to-accent text-primary-content rounded-2xl mb-10 shadow-2xl">
|
8 |
+
<div class="hero-content text-center py-20">
|
9 |
+
<div class="max-w-2xl">
|
10 |
+
<h1 class="text-6xl font-bold mb-6 flex items-center justify-center">
|
11 |
+
<i data-lucide="rocket" class="w-16 h-16 mr-4 animate-pulse"></i>
|
12 |
+
<span data-i18n="hero.title">One-Click Deploy</span>
|
13 |
+
</h1>
|
14 |
+
<p class="text-xl opacity-90 mb-8" data-i18n="hero.subtitle">
|
15 |
+
Deploy to HuggingFace Spaces instantly
|
16 |
+
</p>
|
17 |
+
<div class="stats shadow-lg bg-base-100/20 backdrop-blur">
|
18 |
+
<div class="stat">
|
19 |
+
<div class="stat-figure text-secondary">
|
20 |
+
<i data-lucide="zap" class="w-8 h-8"></i>
|
21 |
+
</div>
|
22 |
+
<div class="stat-title text-primary-content/80" data-i18n="hero.deployTime">Deploy Time</div>
|
23 |
+
<div class="stat-value" data-i18n="hero.minutes">2-5min</div>
|
24 |
+
</div>
|
25 |
+
<div class="stat">
|
26 |
+
<div class="stat-figure text-secondary">
|
27 |
+
<i data-lucide="server" class="w-8 h-8"></i>
|
28 |
+
</div>
|
29 |
+
<div class="stat-title text-primary-content/80" data-i18n="hero.freeHosting">Free Hosting</div>
|
30 |
+
<div class="stat-value" data-i18n="hero.percentage">100%</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
|
37 |
+
<!-- Features Grid -->
|
38 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
39 |
+
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
40 |
+
<div class="card-body items-center text-center">
|
41 |
+
<div class="w-16 h-16 bg-warning/20 rounded-full flex items-center justify-center mb-4">
|
42 |
+
<i data-lucide="zap" class="w-8 h-8 text-warning"></i>
|
43 |
+
</div>
|
44 |
+
<h2 class="card-title" data-i18n="feature.fast.title">Lightning Fast</h2>
|
45 |
+
<p data-i18n="feature.fast.desc">Deploy from Git in minutes</p>
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
49 |
+
<div class="card-body items-center text-center">
|
50 |
+
<div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center mb-4">
|
51 |
+
<i data-lucide="shield-check" class="w-8 h-8 text-success"></i>
|
52 |
+
</div>
|
53 |
+
<h2 class="card-title" data-i18n="feature.secure.title">Secure</h2>
|
54 |
+
<p data-i18n="feature.secure.desc">Full control with your token</p>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
58 |
+
<div class="card-body items-center text-center">
|
59 |
+
<div class="w-16 h-16 bg-info/20 rounded-full flex items-center justify-center mb-4">
|
60 |
+
<i data-lucide="activity" class="w-8 h-8 text-info"></i>
|
61 |
+
</div>
|
62 |
+
<h2 class="card-title" data-i18n="feature.monitor.title">Real-time</h2>
|
63 |
+
<p data-i18n="feature.monitor.desc">Live deployment status</p>
|
64 |
+
</div>
|
65 |
+
</div>
|
66 |
+
</div>
|
67 |
+
|
68 |
+
<!-- Deploy Form -->
|
69 |
+
<div class="card bg-base-100 shadow-2xl border border-base-300">
|
70 |
+
<div class="card-body">
|
71 |
+
<div class="flex items-center justify-between mb-8">
|
72 |
+
<h2 class="card-title text-3xl flex items-center">
|
73 |
+
<i data-lucide="settings" class="w-8 h-8 mr-3"></i>
|
74 |
+
<span data-i18n="form.title">Configuration</span>
|
75 |
+
</h2>
|
76 |
+
<div class="flex gap-2">
|
77 |
+
<button
|
78 |
+
type="button"
|
79 |
+
onclick="showImportModal()"
|
80 |
+
class="btn btn-ghost btn-sm"
|
81 |
+
data-i18n-title="config.import"
|
82 |
+
title="Import Configuration"
|
83 |
+
>
|
84 |
+
<i data-lucide="upload" class="w-4 h-4 mr-2"></i>
|
85 |
+
<span data-i18n="config.import">Import</span>
|
86 |
+
</button>
|
87 |
+
<button
|
88 |
+
type="button"
|
89 |
+
onclick="showExportModal()"
|
90 |
+
class="btn btn-ghost btn-sm"
|
91 |
+
data-i18n-title="config.export"
|
92 |
+
title="Export Configuration"
|
93 |
+
>
|
94 |
+
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
95 |
+
<span data-i18n="config.export">Export</span>
|
96 |
+
</button>
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
+
|
100 |
+
<form
|
101 |
+
id="deploy-form"
|
102 |
+
action="/web/deploy"
|
103 |
+
method="POST"
|
104 |
+
class="space-y-6"
|
105 |
+
x-data="{ showAdvanced: false }"
|
106 |
+
>
|
107 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
108 |
+
<!-- Left Column -->
|
109 |
+
<div class="space-y-6">
|
110 |
+
<!-- HuggingFace Token -->
|
111 |
+
<div class="form-control">
|
112 |
+
<label class="label">
|
113 |
+
<span class="label-text font-semibold text-lg">
|
114 |
+
<i data-lucide="key" class="w-4 h-4 inline mr-2"></i>
|
115 |
+
<span data-i18n="form.token">HF Token</span>
|
116 |
+
</span>
|
117 |
+
<span class="label-text-alt">
|
118 |
+
<a href="https://huggingface.co/settings/tokens" target="_blank" class="link link-primary" data-i18n="form.token.get">
|
119 |
+
Get Token
|
120 |
+
</a>
|
121 |
+
</span>
|
122 |
+
</label>
|
123 |
+
<div class="relative">
|
124 |
+
<input
|
125 |
+
type="password"
|
126 |
+
name="hf_token"
|
127 |
+
data-i18n-placeholder="form.token.placeholder"
|
128 |
+
placeholder="hf_..."
|
129 |
+
class="input input-bordered input-primary w-full pr-24"
|
130 |
+
required
|
131 |
+
x-ref="tokenInput"
|
132 |
+
>
|
133 |
+
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1" style="z-index: 10;">
|
134 |
+
<button
|
135 |
+
type="button"
|
136 |
+
class="btn btn-ghost btn-xs p-1"
|
137 |
+
@click="$refs.tokenInput.type = $refs.tokenInput.type === 'password' ? 'text' : 'password'"
|
138 |
+
data-i18n-title="form.token.toggle"
|
139 |
+
title="Toggle visibility"
|
140 |
+
>
|
141 |
+
<i data-lucide="eye" class="w-4 h-4"></i>
|
142 |
+
</button>
|
143 |
+
<button
|
144 |
+
type="button"
|
145 |
+
class="btn btn-ghost btn-xs p-1"
|
146 |
+
onclick="clearCachedToken()"
|
147 |
+
data-i18n-title="form.token.clear"
|
148 |
+
title="Clear cached token"
|
149 |
+
id="clear-token-btn"
|
150 |
+
style="display: none;"
|
151 |
+
>
|
152 |
+
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
153 |
+
</button>
|
154 |
+
</div>
|
155 |
+
</div>
|
156 |
+
<label class="label">
|
157 |
+
<span class="label-text-alt text-warning">
|
158 |
+
<i data-lucide="alert-circle" class="w-3 h-3 inline"></i>
|
159 |
+
<span data-i18n="form.token.hint">Write permission required</span>
|
160 |
+
</span>
|
161 |
+
</label>
|
162 |
+
</div>
|
163 |
+
|
164 |
+
<!-- Git Repository URL -->
|
165 |
+
<div class="form-control">
|
166 |
+
<label class="label">
|
167 |
+
<span class="label-text font-semibold text-lg">
|
168 |
+
<i data-lucide="git-branch" class="w-4 h-4 inline mr-2"></i>
|
169 |
+
<span data-i18n="form.repo">Git Repository</span>
|
170 |
+
</span>
|
171 |
+
</label>
|
172 |
+
<input
|
173 |
+
type="url"
|
174 |
+
name="git_repo_url"
|
175 |
+
data-i18n-placeholder="form.repo.placeholder"
|
176 |
+
placeholder="https://github.com/user/repo.git"
|
177 |
+
class="input input-bordered input-primary w-full"
|
178 |
+
required
|
179 |
+
>
|
180 |
+
<label class="label">
|
181 |
+
<span class="label-text-alt" data-i18n="form.repo.hint">GitHub, GitLab, etc.</span>
|
182 |
+
</label>
|
183 |
+
</div>
|
184 |
+
</div>
|
185 |
+
|
186 |
+
<!-- Right Column -->
|
187 |
+
<div class="space-y-6">
|
188 |
+
<!-- Space Name -->
|
189 |
+
<div class="form-control">
|
190 |
+
<label class="label">
|
191 |
+
<span class="label-text font-semibold text-lg">
|
192 |
+
<i data-lucide="tag" class="w-4 h-4 inline mr-2"></i>
|
193 |
+
<span data-i18n="form.space">Space Name</span>
|
194 |
+
</span>
|
195 |
+
</label>
|
196 |
+
<input
|
197 |
+
type="text"
|
198 |
+
name="space_name"
|
199 |
+
data-i18n-placeholder="form.space.placeholder"
|
200 |
+
placeholder="my-app"
|
201 |
+
class="input input-bordered input-primary w-full"
|
202 |
+
pattern="[a-zA-Z0-9_\-]+"
|
203 |
+
required
|
204 |
+
>
|
205 |
+
<label class="label">
|
206 |
+
<span class="label-text-alt" data-i18n="form.space.hint">Letters, numbers, hyphens only</span>
|
207 |
+
</label>
|
208 |
+
</div>
|
209 |
+
|
210 |
+
<!-- Description -->
|
211 |
+
<div class="form-control">
|
212 |
+
<label class="label">
|
213 |
+
<span class="label-text font-semibold text-lg">
|
214 |
+
<i data-lucide="file-text" class="w-4 h-4 inline mr-2"></i>
|
215 |
+
<span data-i18n="form.desc">Description</span>
|
216 |
+
</span>
|
217 |
+
</label>
|
218 |
+
<textarea
|
219 |
+
name="description"
|
220 |
+
data-i18n-placeholder="form.desc.placeholder"
|
221 |
+
placeholder="Brief description..."
|
222 |
+
class="textarea textarea-bordered textarea-primary h-24"
|
223 |
+
></textarea>
|
224 |
+
</div>
|
225 |
+
</div>
|
226 |
+
</div>
|
227 |
+
|
228 |
+
<!-- Advanced Settings -->
|
229 |
+
<div class="divider"></div>
|
230 |
+
|
231 |
+
<div class="collapse collapse-plus bg-base-200 rounded-lg">
|
232 |
+
<input type="checkbox" x-model="showAdvanced" />
|
233 |
+
<div class="collapse-title text-lg font-medium flex items-center">
|
234 |
+
<i data-lucide="sliders" class="w-5 h-5 mr-2"></i>
|
235 |
+
<span data-i18n="form.advanced">Advanced</span>
|
236 |
+
</div>
|
237 |
+
<div class="collapse-content">
|
238 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-4">
|
239 |
+
<!-- Deploy Path -->
|
240 |
+
<div class="form-control">
|
241 |
+
<label class="label">
|
242 |
+
<span class="label-text font-semibold">
|
243 |
+
<i data-lucide="folder-open" class="w-4 h-4 inline mr-2"></i>
|
244 |
+
<span data-i18n="form.deployPath">Deploy Path</span>
|
245 |
+
</span>
|
246 |
+
</label>
|
247 |
+
<input
|
248 |
+
type="text"
|
249 |
+
name="deploy_path"
|
250 |
+
value="/"
|
251 |
+
data-i18n-placeholder="form.deployPath.placeholder"
|
252 |
+
placeholder="/"
|
253 |
+
class="input input-bordered w-full"
|
254 |
+
pattern="^\.?\/?([\w\-\.\/]*)?$"
|
255 |
+
>
|
256 |
+
<label class="label">
|
257 |
+
<span class="label-text-alt" data-i18n="form.deployPath.hint">Subdirectory to deploy (default: root)</span>
|
258 |
+
</label>
|
259 |
+
</div>
|
260 |
+
|
261 |
+
<!-- Port -->
|
262 |
+
<div class="form-control">
|
263 |
+
<label class="label">
|
264 |
+
<span class="label-text font-semibold">
|
265 |
+
<i data-lucide="wifi" class="w-4 h-4 inline mr-2"></i>
|
266 |
+
<span data-i18n="form.port">Port</span>
|
267 |
+
</span>
|
268 |
+
</label>
|
269 |
+
<input
|
270 |
+
type="number"
|
271 |
+
name="space_port"
|
272 |
+
value="7860"
|
273 |
+
min="1"
|
274 |
+
max="65535"
|
275 |
+
class="input input-bordered w-full"
|
276 |
+
>
|
277 |
+
</div>
|
278 |
+
|
279 |
+
<!-- Privacy -->
|
280 |
+
<div class="form-control">
|
281 |
+
<label class="label cursor-pointer">
|
282 |
+
<span class="label-text font-semibold">
|
283 |
+
<i data-lucide="lock" class="w-4 h-4 inline mr-2"></i>
|
284 |
+
<span data-i18n="form.private">Private Space</span>
|
285 |
+
</span>
|
286 |
+
<input type="checkbox" name="private" class="toggle toggle-primary" />
|
287 |
+
</label>
|
288 |
+
</div>
|
289 |
+
|
290 |
+
<!-- Environment Variables -->
|
291 |
+
<div class="form-control lg:col-span-2">
|
292 |
+
<label class="label">
|
293 |
+
<span class="label-text font-semibold">
|
294 |
+
<i data-lucide="terminal" class="w-4 h-4 inline mr-2"></i>
|
295 |
+
<span data-i18n="form.env">Environment Variables</span>
|
296 |
+
</span>
|
297 |
+
</label>
|
298 |
+
<textarea
|
299 |
+
name="env_vars_text"
|
300 |
+
data-i18n-placeholder="form.env.placeholder"
|
301 |
+
placeholder="KEY=value"
|
302 |
+
class="textarea textarea-bordered h-32 font-mono text-sm"
|
303 |
+
></textarea>
|
304 |
+
<label class="label">
|
305 |
+
<span class="label-text-alt" data-i18n="form.env.hint">One per line</span>
|
306 |
+
</label>
|
307 |
+
</div>
|
308 |
+
</div>
|
309 |
+
</div>
|
310 |
+
</div>
|
311 |
+
|
312 |
+
<!-- Submit Button -->
|
313 |
+
<div class="form-control mt-8">
|
314 |
+
<button type="submit" class="btn btn-primary btn-lg w-full gap-3">
|
315 |
+
<i data-lucide="rocket" class="w-6 h-6"></i>
|
316 |
+
<span data-i18n="form.submit">Deploy</span>
|
317 |
+
</button>
|
318 |
+
</div>
|
319 |
+
</form>
|
320 |
+
</div>
|
321 |
+
</div>
|
322 |
+
|
323 |
+
<!-- Loading Overlay -->
|
324 |
+
<div id="loading-overlay" class="fixed inset-0 bg-base-100/80 backdrop-blur-sm z-50 flex items-center justify-center" style="display: none;">
|
325 |
+
<div class="card bg-base-100 shadow-2xl">
|
326 |
+
<div class="card-body">
|
327 |
+
<div class="flex flex-col items-center space-y-4">
|
328 |
+
<div class="loading loading-spinner loading-lg text-primary"></div>
|
329 |
+
<h3 class="text-xl font-semibold" data-i18n="loading.title">Deploying...</h3>
|
330 |
+
<p class="text-base-content/70" data-i18n="loading.desc">Please wait...</p>
|
331 |
+
</div>
|
332 |
+
</div>
|
333 |
+
</div>
|
334 |
+
</div>
|
335 |
+
|
336 |
+
<!-- Instructions -->
|
337 |
+
<div class="mt-12 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
338 |
+
<div class="card bg-info/10 border border-info/20">
|
339 |
+
<div class="card-body">
|
340 |
+
<h3 class="card-title text-info">
|
341 |
+
<i data-lucide="info" class="w-5 h-5"></i>
|
342 |
+
<span data-i18n="req.title">Requirements</span>
|
343 |
+
</h3>
|
344 |
+
<ul class="space-y-2 text-sm">
|
345 |
+
<li class="flex items-start">
|
346 |
+
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
347 |
+
<span data-i18n="req.dockerfile">Repository must contain Dockerfile</span>
|
348 |
+
</li>
|
349 |
+
<li class="flex items-start">
|
350 |
+
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
351 |
+
<span data-i18n="req.token">Token needs write permissions</span>
|
352 |
+
</li>
|
353 |
+
<li class="flex items-start">
|
354 |
+
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
355 |
+
<span data-i18n="req.time">Deployment takes 2-5 minutes</span>
|
356 |
+
</li>
|
357 |
+
<li class="flex items-start">
|
358 |
+
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
359 |
+
<span data-i18n="req.docker">Supports Dockerized apps</span>
|
360 |
+
</li>
|
361 |
+
</ul>
|
362 |
+
</div>
|
363 |
+
</div>
|
364 |
+
|
365 |
+
<div class="card bg-warning/10 border border-warning/20">
|
366 |
+
<div class="card-body">
|
367 |
+
<h3 class="card-title text-warning">
|
368 |
+
<i data-lucide="lightbulb" class="w-5 h-5"></i>
|
369 |
+
<span data-i18n="tips.title">Tips</span>
|
370 |
+
</h3>
|
371 |
+
<ul class="space-y-2 text-sm">
|
372 |
+
<li class="flex items-start">
|
373 |
+
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
374 |
+
<span data-i18n="tips.test">Test Dockerfile locally</span>
|
375 |
+
</li>
|
376 |
+
<li class="flex items-start">
|
377 |
+
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
378 |
+
<span data-i18n="tips.env">Use env vars for secrets</span>
|
379 |
+
</li>
|
380 |
+
<li class="flex items-start">
|
381 |
+
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
382 |
+
<span data-i18n="tips.size">Keep image size small</span>
|
383 |
+
</li>
|
384 |
+
<li class="flex items-start">
|
385 |
+
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
386 |
+
<span data-i18n="tips.limits">Check HF resource limits</span>
|
387 |
+
</li>
|
388 |
+
</ul>
|
389 |
+
</div>
|
390 |
+
</div>
|
391 |
+
</div>
|
392 |
+
|
393 |
+
<!-- Import Configuration Modal -->
|
394 |
+
<dialog id="import-modal" class="modal">
|
395 |
+
<div class="modal-box max-w-2xl">
|
396 |
+
<h3 class="font-bold text-lg mb-4">
|
397 |
+
<i data-lucide="upload" class="w-5 h-5 inline mr-2"></i>
|
398 |
+
<span data-i18n="config.import.title">Import Configuration</span>
|
399 |
+
</h3>
|
400 |
+
|
401 |
+
<div class="space-y-4">
|
402 |
+
<div class="alert alert-info">
|
403 |
+
<i data-lucide="info" class="w-4 h-4"></i>
|
404 |
+
<span data-i18n="config.import.info">Paste your configuration JSON or share configuration URL</span>
|
405 |
+
</div>
|
406 |
+
|
407 |
+
<div class="form-control">
|
408 |
+
<label class="label">
|
409 |
+
<span class="label-text" data-i18n="config.import.label">Configuration JSON</span>
|
410 |
+
</label>
|
411 |
+
<textarea
|
412 |
+
id="import-config-text"
|
413 |
+
class="textarea textarea-bordered h-48 font-mono text-sm"
|
414 |
+
data-i18n-placeholder="config.import.placeholder"
|
415 |
+
placeholder='{"space_name": "my-app", "git_repo_url": "https://github.com/..."}'
|
416 |
+
></textarea>
|
417 |
+
</div>
|
418 |
+
|
419 |
+
<div class="modal-action">
|
420 |
+
<button type="button" onclick="importConfig()" class="btn btn-primary">
|
421 |
+
<i data-lucide="check" class="w-4 h-4 mr-2"></i>
|
422 |
+
<span data-i18n="config.import.apply">Apply Configuration</span>
|
423 |
+
</button>
|
424 |
+
<form method="dialog">
|
425 |
+
<button class="btn btn-ghost" data-i18n="config.cancel">Cancel</button>
|
426 |
+
</form>
|
427 |
+
</div>
|
428 |
+
</div>
|
429 |
+
</div>
|
430 |
+
<form method="dialog" class="modal-backdrop">
|
431 |
+
<button>close</button>
|
432 |
+
</form>
|
433 |
+
</dialog>
|
434 |
+
|
435 |
+
<!-- Export Configuration Modal -->
|
436 |
+
<dialog id="export-modal" class="modal">
|
437 |
+
<div class="modal-box max-w-2xl">
|
438 |
+
<h3 class="font-bold text-lg mb-4">
|
439 |
+
<i data-lucide="download" class="w-5 h-5 inline mr-2"></i>
|
440 |
+
<span data-i18n="config.export.title">Export Configuration</span>
|
441 |
+
</h3>
|
442 |
+
|
443 |
+
<div class="space-y-4">
|
444 |
+
<div class="alert alert-success">
|
445 |
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
446 |
+
<span data-i18n="config.export.info">Configuration exported successfully</span>
|
447 |
+
</div>
|
448 |
+
|
449 |
+
<div class="form-control">
|
450 |
+
<label class="label">
|
451 |
+
<span class="label-text" data-i18n="config.export.label">Configuration JSON</span>
|
452 |
+
<button
|
453 |
+
type="button"
|
454 |
+
onclick="copyExportConfig()"
|
455 |
+
class="btn btn-ghost btn-xs"
|
456 |
+
>
|
457 |
+
<i data-lucide="copy" class="w-4 h-4 mr-1"></i>
|
458 |
+
<span data-i18n="config.copy">Copy</span>
|
459 |
+
</button>
|
460 |
+
</label>
|
461 |
+
<textarea
|
462 |
+
id="export-config-text"
|
463 |
+
class="textarea textarea-bordered h-48 font-mono text-sm"
|
464 |
+
readonly
|
465 |
+
></textarea>
|
466 |
+
</div>
|
467 |
+
|
468 |
+
<div class="form-control">
|
469 |
+
<label class="label">
|
470 |
+
<span class="label-text" data-i18n="config.export.url">Share URL</span>
|
471 |
+
<button
|
472 |
+
type="button"
|
473 |
+
onclick="copyShareUrl()"
|
474 |
+
class="btn btn-ghost btn-xs"
|
475 |
+
>
|
476 |
+
<i data-lucide="copy" class="w-4 h-4 mr-1"></i>
|
477 |
+
<span data-i18n="config.copy">Copy</span>
|
478 |
+
</button>
|
479 |
+
</label>
|
480 |
+
<input
|
481 |
+
id="share-url"
|
482 |
+
type="text"
|
483 |
+
class="input input-bordered font-mono text-sm"
|
484 |
+
readonly
|
485 |
+
>
|
486 |
+
</div>
|
487 |
+
|
488 |
+
<div class="modal-action">
|
489 |
+
<button type="button" onclick="saveConfigAsFile()" class="btn btn-primary">
|
490 |
+
<i data-lucide="save" class="w-4 h-4 mr-2"></i>
|
491 |
+
<span data-i18n="config.export.save">Save as File</span>
|
492 |
+
</button>
|
493 |
+
<form method="dialog">
|
494 |
+
<button class="btn btn-ghost" data-i18n="config.close">Close</button>
|
495 |
+
</form>
|
496 |
+
</div>
|
497 |
+
</div>
|
498 |
+
</div>
|
499 |
+
<form method="dialog" class="modal-backdrop">
|
500 |
+
<button>close</button>
|
501 |
+
</form>
|
502 |
+
</dialog>
|
503 |
+
|
504 |
+
<script>
|
505 |
+
// Token cache management
|
506 |
+
const TOKEN_CACHE_KEY = 'hf_deploy_token';
|
507 |
+
|
508 |
+
// Load cached token on page load
|
509 |
+
function loadCachedToken() {
|
510 |
+
try {
|
511 |
+
const cachedToken = localStorage.getItem(TOKEN_CACHE_KEY);
|
512 |
+
if (cachedToken) {
|
513 |
+
const tokenInput = document.querySelector('input[name="hf_token"]');
|
514 |
+
if (tokenInput) {
|
515 |
+
tokenInput.value = cachedToken;
|
516 |
+
// Show clear button
|
517 |
+
const clearBtn = document.getElementById('clear-token-btn');
|
518 |
+
if (clearBtn) {
|
519 |
+
clearBtn.style.display = 'block';
|
520 |
+
}
|
521 |
+
}
|
522 |
+
}
|
523 |
+
} catch (error) {
|
524 |
+
console.error('Error loading cached token:', error);
|
525 |
+
}
|
526 |
+
}
|
527 |
+
|
528 |
+
// Save token to cache
|
529 |
+
function saveTokenToCache(token) {
|
530 |
+
try {
|
531 |
+
if (token && token.startsWith('hf_')) {
|
532 |
+
localStorage.setItem(TOKEN_CACHE_KEY, token);
|
533 |
+
}
|
534 |
+
} catch (error) {
|
535 |
+
console.error('Error saving token to cache:', error);
|
536 |
+
}
|
537 |
+
}
|
538 |
+
|
539 |
+
// Clear cached token
|
540 |
+
function clearCachedToken() {
|
541 |
+
try {
|
542 |
+
localStorage.removeItem(TOKEN_CACHE_KEY);
|
543 |
+
const tokenInput = document.querySelector('input[name="hf_token"]');
|
544 |
+
if (tokenInput) {
|
545 |
+
tokenInput.value = '';
|
546 |
+
tokenInput.focus();
|
547 |
+
}
|
548 |
+
// Hide clear button
|
549 |
+
const clearBtn = document.getElementById('clear-token-btn');
|
550 |
+
if (clearBtn) {
|
551 |
+
clearBtn.style.display = 'none';
|
552 |
+
}
|
553 |
+
showToast(t('form.token.cleared') || 'Token cache cleared', 'success');
|
554 |
+
} catch (error) {
|
555 |
+
console.error('Error clearing cached token:', error);
|
556 |
+
}
|
557 |
+
}
|
558 |
+
|
559 |
+
// Monitor token input changes
|
560 |
+
function initTokenCacheHandler() {
|
561 |
+
const tokenInput = document.querySelector('input[name="hf_token"]');
|
562 |
+
if (tokenInput) {
|
563 |
+
let saveTimeout;
|
564 |
+
|
565 |
+
// Show/hide clear button and auto-save token
|
566 |
+
tokenInput.addEventListener('input', function() {
|
567 |
+
const clearBtn = document.getElementById('clear-token-btn');
|
568 |
+
if (clearBtn) {
|
569 |
+
clearBtn.style.display = this.value ? 'block' : 'none';
|
570 |
+
}
|
571 |
+
|
572 |
+
// Clear previous timeout
|
573 |
+
if (saveTimeout) {
|
574 |
+
clearTimeout(saveTimeout);
|
575 |
+
}
|
576 |
+
|
577 |
+
// Auto-save valid token with debounce (500ms delay)
|
578 |
+
const token = this.value.trim();
|
579 |
+
if (token && token.startsWith('hf_')) {
|
580 |
+
saveTimeout = setTimeout(() => {
|
581 |
+
saveTokenToCache(token);
|
582 |
+
// Optional: show a subtle indicator that token was saved
|
583 |
+
const originalBorder = this.style.borderColor;
|
584 |
+
this.style.borderColor = 'var(--success)';
|
585 |
+
setTimeout(() => {
|
586 |
+
this.style.borderColor = originalBorder;
|
587 |
+
}, 1000);
|
588 |
+
}, 500);
|
589 |
+
}
|
590 |
+
});
|
591 |
+
|
592 |
+
// Also save on blur (when user leaves the field)
|
593 |
+
tokenInput.addEventListener('blur', function() {
|
594 |
+
const token = this.value.trim();
|
595 |
+
if (token && token.startsWith('hf_')) {
|
596 |
+
saveTokenToCache(token);
|
597 |
+
}
|
598 |
+
});
|
599 |
+
}
|
600 |
+
}
|
601 |
+
|
602 |
+
// Configuration management
|
603 |
+
const CONFIG_FIELDS = {
|
604 |
+
git_repo_url: 'input[name="git_repo_url"]',
|
605 |
+
space_name: 'input[name="space_name"]',
|
606 |
+
description: 'textarea[name="description"]',
|
607 |
+
deploy_path: 'input[name="deploy_path"]',
|
608 |
+
space_port: 'input[name="space_port"]',
|
609 |
+
private: 'input[name="private"]',
|
610 |
+
env_vars_text: 'textarea[name="env_vars_text"]'
|
611 |
+
};
|
612 |
+
|
613 |
+
// Check for config in URL on page load
|
614 |
+
document.addEventListener('DOMContentLoaded', function() {
|
615 |
+
checkUrlConfig();
|
616 |
+
|
617 |
+
// Load cached token
|
618 |
+
loadCachedToken();
|
619 |
+
initTokenCacheHandler();
|
620 |
+
|
621 |
+
// Re-initialize icons after DOM updates
|
622 |
+
setTimeout(() => {
|
623 |
+
if (typeof lucide !== 'undefined') {
|
624 |
+
lucide.createIcons();
|
625 |
+
}
|
626 |
+
}, 100);
|
627 |
+
});
|
628 |
+
|
629 |
+
// Listen for hash changes (e.g., when user navigates to a URL with config)
|
630 |
+
window.addEventListener('hashchange', function() {
|
631 |
+
checkUrlConfig();
|
632 |
+
});
|
633 |
+
|
634 |
+
// Check URL for config parameter
|
635 |
+
function checkUrlConfig() {
|
636 |
+
// Check fragment instead of query parameters
|
637 |
+
const hash = window.location.hash;
|
638 |
+
|
639 |
+
if (hash && hash.startsWith('#config=')) {
|
640 |
+
try {
|
641 |
+
// Extract config from fragment
|
642 |
+
const configParam = hash.substring(8); // Remove '#config='
|
643 |
+
|
644 |
+
// Decode base64 and parse JSON
|
645 |
+
const configJson = atob(configParam);
|
646 |
+
const config = JSON.parse(configJson);
|
647 |
+
applyConfiguration(config);
|
648 |
+
|
649 |
+
// Show success toast
|
650 |
+
showToast(t('config.import.success'), 'success');
|
651 |
+
|
652 |
+
// Remove config from URL to clean up
|
653 |
+
window.history.replaceState({}, document.title, window.location.pathname);
|
654 |
+
} catch (error) {
|
655 |
+
console.error('Failed to load config from URL:', error);
|
656 |
+
showToast(t('config.import.error'), 'error');
|
657 |
+
}
|
658 |
+
}
|
659 |
+
}
|
660 |
+
|
661 |
+
// Show import modal
|
662 |
+
function showImportModal() {
|
663 |
+
document.getElementById('import-modal').showModal();
|
664 |
+
// Re-initialize icons in modal
|
665 |
+
setTimeout(() => lucide.createIcons(), 100);
|
666 |
+
}
|
667 |
+
|
668 |
+
// Show export modal
|
669 |
+
function showExportModal() {
|
670 |
+
const config = extractConfiguration();
|
671 |
+
const configJson = JSON.stringify(config, null, 2);
|
672 |
+
|
673 |
+
// Set export text
|
674 |
+
document.getElementById('export-config-text').value = configJson;
|
675 |
+
|
676 |
+
// Generate share URL using fragment instead of query parameter
|
677 |
+
const encoded = btoa(configJson);
|
678 |
+
const shareUrl = `${window.location.origin}${window.location.pathname}#config=${encoded}`;
|
679 |
+
document.getElementById('share-url').value = shareUrl;
|
680 |
+
|
681 |
+
document.getElementById('export-modal').showModal();
|
682 |
+
// Re-initialize icons in modal
|
683 |
+
setTimeout(() => lucide.createIcons(), 100);
|
684 |
+
}
|
685 |
+
|
686 |
+
// Extract current form configuration
|
687 |
+
function extractConfiguration() {
|
688 |
+
const config = {};
|
689 |
+
|
690 |
+
for (const [field, selector] of Object.entries(CONFIG_FIELDS)) {
|
691 |
+
const element = document.querySelector(selector);
|
692 |
+
if (element) {
|
693 |
+
if (element.type === 'checkbox') {
|
694 |
+
config[field] = element.checked;
|
695 |
+
} else {
|
696 |
+
config[field] = element.value;
|
697 |
+
}
|
698 |
+
}
|
699 |
+
}
|
700 |
+
|
701 |
+
return config;
|
702 |
+
}
|
703 |
+
|
704 |
+
// Apply configuration to form
|
705 |
+
function applyConfiguration(config) {
|
706 |
+
for (const [field, selector] of Object.entries(CONFIG_FIELDS)) {
|
707 |
+
const element = document.querySelector(selector);
|
708 |
+
if (element && config.hasOwnProperty(field)) {
|
709 |
+
if (element.type === 'checkbox') {
|
710 |
+
element.checked = config[field];
|
711 |
+
} else {
|
712 |
+
element.value = config[field];
|
713 |
+
}
|
714 |
+
|
715 |
+
// Trigger change event for any listeners
|
716 |
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
717 |
+
}
|
718 |
+
}
|
719 |
+
|
720 |
+
// Expand advanced settings if any advanced field is set
|
721 |
+
if (config.deploy_path || config.space_port !== 7860 || config.private || config.env_vars_text) {
|
722 |
+
const advancedToggle = document.querySelector('.collapse input[type="checkbox"]');
|
723 |
+
if (advancedToggle) {
|
724 |
+
advancedToggle.checked = true;
|
725 |
+
}
|
726 |
+
}
|
727 |
+
}
|
728 |
+
|
729 |
+
// Import configuration from modal
|
730 |
+
function importConfig() {
|
731 |
+
const configText = document.getElementById('import-config-text').value.trim();
|
732 |
+
|
733 |
+
if (!configText) {
|
734 |
+
showToast(t('config.import.empty'), 'warning');
|
735 |
+
return;
|
736 |
+
}
|
737 |
+
|
738 |
+
try {
|
739 |
+
const config = JSON.parse(configText);
|
740 |
+
applyConfiguration(config);
|
741 |
+
|
742 |
+
// Close modal
|
743 |
+
document.getElementById('import-modal').close();
|
744 |
+
|
745 |
+
// Show success toast
|
746 |
+
showToast(t('config.import.success'), 'success');
|
747 |
+
} catch (error) {
|
748 |
+
console.error('Invalid configuration JSON:', error);
|
749 |
+
showToast(t('config.import.invalid'), 'error');
|
750 |
+
}
|
751 |
+
}
|
752 |
+
|
753 |
+
// Copy export configuration
|
754 |
+
function copyExportConfig() {
|
755 |
+
const configText = document.getElementById('export-config-text');
|
756 |
+
configText.select();
|
757 |
+
navigator.clipboard.writeText(configText.value).then(() => {
|
758 |
+
showToast(t('config.copied'), 'success');
|
759 |
+
});
|
760 |
+
}
|
761 |
+
|
762 |
+
// Copy share URL
|
763 |
+
function copyShareUrl() {
|
764 |
+
const shareUrl = document.getElementById('share-url');
|
765 |
+
shareUrl.select();
|
766 |
+
navigator.clipboard.writeText(shareUrl.value).then(() => {
|
767 |
+
showToast(t('config.url.copied'), 'success');
|
768 |
+
});
|
769 |
+
}
|
770 |
+
|
771 |
+
// Save configuration as file
|
772 |
+
function saveConfigAsFile() {
|
773 |
+
const config = extractConfiguration();
|
774 |
+
const configJson = JSON.stringify(config, null, 2);
|
775 |
+
|
776 |
+
// Create blob and download
|
777 |
+
const blob = new Blob([configJson], { type: 'application/json' });
|
778 |
+
const url = URL.createObjectURL(blob);
|
779 |
+
const a = document.createElement('a');
|
780 |
+
a.href = url;
|
781 |
+
a.download = `hf-deploy-config-${config.space_name || 'config'}.json`;
|
782 |
+
document.body.appendChild(a);
|
783 |
+
a.click();
|
784 |
+
document.body.removeChild(a);
|
785 |
+
URL.revokeObjectURL(url);
|
786 |
+
|
787 |
+
showToast(t('config.export.saved'), 'success');
|
788 |
+
}
|
789 |
+
|
790 |
+
// Form validation and enhancement
|
791 |
+
document.getElementById('deploy-form').addEventListener('submit', function(e) {
|
792 |
+
const submitBtn = e.target.querySelector('button[type="submit"]');
|
793 |
+
submitBtn.disabled = true;
|
794 |
+
|
795 |
+
// Show loading overlay
|
796 |
+
document.getElementById('loading-overlay').style.display = 'flex';
|
797 |
+
});
|
798 |
+
|
799 |
+
// Initialize all interactive elements after page load
|
800 |
+
document.addEventListener('DOMContentLoaded', function() {
|
801 |
+
// Re-initialize Lucide icons
|
802 |
+
if (typeof lucide !== 'undefined') {
|
803 |
+
lucide.createIcons();
|
804 |
+
}
|
805 |
+
|
806 |
+
// Make sure HTMX is initialized
|
807 |
+
if (typeof htmx !== 'undefined') {
|
808 |
+
htmx.process(document.body);
|
809 |
+
}
|
810 |
+
|
811 |
+
// Initialize eye toggle button for password visibility
|
812 |
+
setTimeout(() => {
|
813 |
+
lucide.createIcons();
|
814 |
+
}, 100);
|
815 |
+
});
|
816 |
+
</script>
|
817 |
+
{% endblock %}
|
app/utils.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os, stat
|
2 |
+
|
3 |
+
|
4 |
+
def _chmod_and_retry(func, path, exc):
|
5 |
+
os.chmod(path, stat.S_IWRITE)
|
6 |
+
func(path)
|
images/img0.png
ADDED
![]() |
images/img1.png
ADDED
![]() |
images/img2.png
ADDED
![]() |
main.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
HuggingFace Space Deployer
|
4 |
+
A web application for deploying code to HuggingFace Spaces with one click.
|
5 |
+
"""
|
6 |
+
|
7 |
+
import uvicorn
|
8 |
+
|
9 |
+
|
10 |
+
if __name__ == "__main__":
|
11 |
+
uvicorn.run(
|
12 |
+
"app.routes:app",
|
13 |
+
host="0.0.0.0",
|
14 |
+
port=7860,
|
15 |
+
reload=False,
|
16 |
+
log_level="info"
|
17 |
+
)
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi>=0.104.0
|
2 |
+
uvicorn[standard]>=0.24.0
|
3 |
+
jinja2>=3.1.0
|
4 |
+
python-multipart>=0.0.6
|
5 |
+
python-dotenv>=1.0.0
|
6 |
+
huggingface-hub>=0.19.0
|
7 |
+
aiofiles>=23.0.0
|
8 |
+
redis
|
9 |
+
pydantic
|
10 |
+
pydantic_settings
|
11 |
+
gitpython>=3.1.30
|