ZSCGR commited on
Commit
30c9413
·
verified ·
1 Parent(s): a065f80

Upload folder using huggingface_hub

Browse files
.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"[![Deploy with HFSpaceDeploy](https://img.shields.io/badge/Deploy_with-HFSpaceDeploy-green?style=social&logo=rocket)](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