Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -22,7 +22,6 @@ app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login_synkris'
|
|
| 22 |
DATA_FILE = 'data.json'
|
| 23 |
DATA_FILE_TEMP = 'data.json.tmp'
|
| 24 |
PROMPTS_FILE = 'prompts.json'
|
| 25 |
-
PROMPTS_FILE_TEMP = 'prompts.json.tmp'
|
| 26 |
|
| 27 |
SYNC_FILES = [DATA_FILE, PROMPTS_FILE]
|
| 28 |
|
|
@@ -82,7 +81,7 @@ Render the product with hyperrealistic lighting and shadows that accentuate its
|
|
| 82 |
"street": "Dynamic street style shot in a bustling metropolis (e.g., Tokyo, New York). Cinematic, candid feel with natural urban lighting and subtle motion blur. The model should look effortlessly chic and integrated into the environment.",
|
| 83 |
"lookbook": "Minimalist lookbook aesthetic. Clean, textured background (e.g., concrete, colored paper). Soft, diffused light creating a sophisticated and modern mood. Focus is entirely on the garment's form and drape.",
|
| 84 |
"minimalism": "Extreme architectural minimalism. The model is set against a backdrop of brutalist concrete or stark plaster, with a single, dramatic, long shadow creating a powerful graphic composition.",
|
| 85 |
-
"selfie": "
|
| 86 |
"creative": "Avant-garde, conceptual photoshoot. Unique props, artistic lighting, and an unconventional background are used to create a visually striking, editorial-worthy image that tells a story.",
|
| 87 |
"new_year": "Festive New Year's atmosphere. Soft bokeh from fairy lights, dynamic sparkler trails, set against a beautifully decorated tree or a magical snowy landscape. Evokes warmth and celebration.",
|
| 88 |
"retro": "Authentic 35mm film photograph emulation. Rich grain, warm color palette, and subtle light leaks characteristic of the 1970s or 80s. Poses and environment reflect the era.",
|
|
@@ -131,16 +130,6 @@ def load_prompts():
|
|
| 131 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 132 |
return {}
|
| 133 |
|
| 134 |
-
def save_prompts(data):
|
| 135 |
-
try:
|
| 136 |
-
with open(PROMPTS_FILE_TEMP, 'w', encoding='utf-8') as file:
|
| 137 |
-
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 138 |
-
os.replace(PROMPTS_FILE_TEMP, PROMPTS_FILE)
|
| 139 |
-
upload_db_to_hf(specific_file=PROMPTS_FILE)
|
| 140 |
-
except Exception:
|
| 141 |
-
if os.path.exists(PROMPTS_FILE_TEMP):
|
| 142 |
-
os.remove(PROMPTS_FILE_TEMP)
|
| 143 |
-
|
| 144 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 145 |
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 146 |
return False
|
|
@@ -307,16 +296,18 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 307 |
h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
|
| 308 |
h1 { margin-bottom: 25px; font-size: 1.5rem; }
|
| 309 |
h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
.section { margin-bottom: 25px; }
|
| 312 |
|
| 313 |
.add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
|
| 314 |
|
| 315 |
-
input[type="text"]
|
| 316 |
width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
|
| 317 |
font-family: inherit; background: #fff; -webkit-appearance: none;
|
| 318 |
}
|
| 319 |
-
textarea { resize: vertical; }
|
| 320 |
|
| 321 |
.controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
|
| 322 |
.radio-group { display: flex; gap: 15px; }
|
|
@@ -334,7 +325,6 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 334 |
.button.warning { background-color: var(--warning); color: #333; }
|
| 335 |
.button.info { background-color: var(--info); }
|
| 336 |
.button.success { background-color: var(--success); }
|
| 337 |
-
.button.secondary { background-color: #6c757d; }
|
| 338 |
|
| 339 |
.env-list { list-style: none; padding: 0; margin: 0; }
|
| 340 |
.env-item {
|
|
@@ -361,8 +351,7 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 361 |
.message.error { background-color: #f8d7da; color: #721c24; }
|
| 362 |
|
| 363 |
.modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
|
| 364 |
-
.modal-content { background-color: #fff; margin:
|
| 365 |
-
.modal-body { overflow-y: auto; }
|
| 366 |
.close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
|
| 367 |
.stats-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85rem; }
|
| 368 |
.stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
|
@@ -371,11 +360,6 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 371 |
|
| 372 |
.empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
|
| 373 |
.no-margin { margin-bottom: 0; }
|
| 374 |
-
|
| 375 |
-
.style-item { border: 1px solid #ddd; border-radius: 8px; padding: 10px; margin-bottom: 10px; }
|
| 376 |
-
.style-item p { margin: 0; font-family: monospace; font-size: 0.8rem; background: #f0f0f0; padding: 8px; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }
|
| 377 |
-
.style-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
| 378 |
-
.style-item-header span { font-weight: bold; }
|
| 379 |
|
| 380 |
@media (max-width: 768px) {
|
| 381 |
.env-item { grid-template-columns: 1fr; gap: 12px; }
|
|
@@ -419,16 +403,78 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 419 |
</div>
|
| 420 |
</form>
|
| 421 |
</div>
|
| 422 |
-
|
| 423 |
-
<div class="section"
|
| 424 |
-
<input type="
|
| 425 |
-
<button class="button secondary" onclick="openStylesModal()"><i class="fas fa-paint-brush"></i> Стили</button>
|
| 426 |
</div>
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
<div class="section">
|
| 429 |
-
{% if
|
| 430 |
<ul class="env-list">
|
| 431 |
-
{% for env in
|
| 432 |
<li class="env-item">
|
| 433 |
<div class="env-details">
|
| 434 |
<div class="env-header">
|
|
@@ -442,7 +488,7 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 442 |
<a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
|
| 443 |
</div>
|
| 444 |
<div class="env-actions">
|
| 445 |
-
<button class="button info" onclick="
|
| 446 |
<form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
|
| 447 |
<button type="submit" class="button warning">
|
| 448 |
<i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
|
|
@@ -460,8 +506,8 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 460 |
</li>
|
| 461 |
{% endfor %}
|
| 462 |
</ul>
|
| 463 |
-
{%
|
| 464 |
-
|
| 465 |
{% endif %}
|
| 466 |
</div>
|
| 467 |
|
|
@@ -496,33 +542,10 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 496 |
|
| 497 |
<div id="statsModal" class="modal">
|
| 498 |
<div class="modal-content">
|
| 499 |
-
<span class="close-modal" onclick="
|
| 500 |
<h3 id="modalTitle" style="margin-top:0; color: var(--bg-medium)">Статистика</h3>
|
| 501 |
<p style="font-size: 0.8rem; color: #666;">Время: Алматы (UTC+5)</p>
|
| 502 |
-
<div id="statsContent"
|
| 503 |
-
</div>
|
| 504 |
-
</div>
|
| 505 |
-
|
| 506 |
-
<div id="stylesModal" class="modal">
|
| 507 |
-
<div class="modal-content">
|
| 508 |
-
<span class="close-modal" onclick="closeStylesModal()">×</span>
|
| 509 |
-
<h3 style="margin-top:0; color: var(--bg-medium)">Управление стилями</h3>
|
| 510 |
-
<div id="stylesContent" class="modal-body">
|
| 511 |
-
<div id="styles-loader" style="text-align:center; padding: 20px;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
|
| 512 |
-
</div>
|
| 513 |
-
<div id="addStyleFormContainer" style="padding-top:20px; border-top: 1px solid #eee; margin-top: 15px;">
|
| 514 |
-
<h4>Добавить новый стиль</h4>
|
| 515 |
-
<form id="addStyleForm" style="display: flex; flex-direction: column; gap: 10px;">
|
| 516 |
-
<input type="text" id="style_key" name="style_key" placeholder="Ключ (латиницей, без пробелов)" required>
|
| 517 |
-
<input type="text" id="style_name" name="style_name" placeholder="Название на кнопке" required>
|
| 518 |
-
<textarea id="style_prompt" name="style_prompt" rows="4" placeholder="Промпт стиля" required></textarea>
|
| 519 |
-
<select id="style_type" name="style_type" required>
|
| 520 |
-
<option value="flagship_styles">Стиль для моделей (flagship)</option>
|
| 521 |
-
<option value="object_styles">Стиль для объектов (object)</option>
|
| 522 |
-
</select>
|
| 523 |
-
<button type="submit" class="button primary">Добавить</button>
|
| 524 |
-
</form>
|
| 525 |
-
</div>
|
| 526 |
</div>
|
| 527 |
</div>
|
| 528 |
|
|
@@ -534,8 +557,15 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 534 |
item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
|
| 535 |
});
|
| 536 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
|
| 538 |
-
function
|
| 539 |
const modal = document.getElementById('statsModal');
|
| 540 |
const content = document.getElementById('statsContent');
|
| 541 |
const title = document.getElementById('modalTitle');
|
|
@@ -581,114 +611,14 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 581 |
});
|
| 582 |
}
|
| 583 |
|
| 584 |
-
function
|
| 585 |
document.getElementById('statsModal').style.display = 'none';
|
| 586 |
}
|
| 587 |
|
| 588 |
-
async function openStylesModal() {
|
| 589 |
-
const modal = document.getElementById('stylesModal');
|
| 590 |
-
modal.style.display = 'block';
|
| 591 |
-
await loadStyles();
|
| 592 |
-
}
|
| 593 |
-
|
| 594 |
-
function closeStylesModal() {
|
| 595 |
-
document.getElementById('stylesModal').style.display = 'none';
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
async function loadStyles() {
|
| 599 |
-
const content = document.getElementById('stylesContent');
|
| 600 |
-
document.getElementById('styles-loader').style.display = 'block';
|
| 601 |
-
|
| 602 |
-
try {
|
| 603 |
-
const response = await fetch('/admhosto/styles');
|
| 604 |
-
if (!response.ok) throw new Error('Network response was not ok');
|
| 605 |
-
const stylesData = await response.json();
|
| 606 |
-
|
| 607 |
-
let html = '<h4>Стили для моделей (flagship)</h4><div id="flagship-styles-list">';
|
| 608 |
-
for (const [key, prompt] of Object.entries(stylesData.flagship_styles)) {
|
| 609 |
-
html += renderStyleItem(key, prompt, 'flagship_styles');
|
| 610 |
-
}
|
| 611 |
-
html += '</div><h4>Стили для объектов (object)</h4><div id="object-styles-list">';
|
| 612 |
-
for (const [key, prompt] of Object.entries(stylesData.object_styles)) {
|
| 613 |
-
html += renderStyleItem(key, prompt, 'object_styles');
|
| 614 |
-
}
|
| 615 |
-
html += '</div>';
|
| 616 |
-
|
| 617 |
-
content.innerHTML = html;
|
| 618 |
-
} catch (error) {
|
| 619 |
-
content.innerHTML = '<p style="color:red">Не удалось загрузить стили.</p>';
|
| 620 |
-
} finally {
|
| 621 |
-
document.getElementById('styles-loader').style.display = 'none';
|
| 622 |
-
}
|
| 623 |
-
}
|
| 624 |
-
|
| 625 |
-
function renderStyleItem(key, prompt, type) {
|
| 626 |
-
return `
|
| 627 |
-
<div class="style-item" id="style-${type}-${key}">
|
| 628 |
-
<div class="style-item-header">
|
| 629 |
-
<span>${key}</span>
|
| 630 |
-
<button class="button danger" onclick="deleteStyle('${key}', '${type}')"><i class="fas fa-trash"></i></button>
|
| 631 |
-
</div>
|
| 632 |
-
<p>${prompt}</p>
|
| 633 |
-
</div>`;
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
async function deleteStyle(key, type) {
|
| 637 |
-
if (!confirm(`Вы уверены, что хотите удалить стиль '${key}'?`)) return;
|
| 638 |
-
try {
|
| 639 |
-
const response = await fetch('/admhosto/styles/delete', {
|
| 640 |
-
method: 'POST',
|
| 641 |
-
headers: { 'Content-Type': 'application/json' },
|
| 642 |
-
body: JSON.stringify({ style_key: key, style_type: type })
|
| 643 |
-
});
|
| 644 |
-
const result = await response.json();
|
| 645 |
-
if (!response.ok) throw new Error(result.error || 'Failed to delete');
|
| 646 |
-
document.getElementById(`style-${type}-${key}`).remove();
|
| 647 |
-
} catch(error) {
|
| 648 |
-
alert(`Ошибка удаления: ${error.message}`);
|
| 649 |
-
}
|
| 650 |
-
}
|
| 651 |
-
|
| 652 |
-
document.getElementById('addStyleForm').addEventListener('submit', async function(e) {
|
| 653 |
-
e.preventDefault();
|
| 654 |
-
const key = document.getElementById('style_key').value.trim();
|
| 655 |
-
const name = document.getElementById('style_name').value.trim();
|
| 656 |
-
const prompt = document.getElementById('style_prompt').value.trim();
|
| 657 |
-
const type = document.getElementById('style_type').value;
|
| 658 |
-
|
| 659 |
-
if (!key.match(/^[a-z0-9_]+$/i)) {
|
| 660 |
-
alert('Ключ может содержать только латинские буквы, цифры и нижнее подчеркивание.');
|
| 661 |
-
return;
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
try {
|
| 665 |
-
const response = await fetch('/admhosto/styles/add', {
|
| 666 |
-
method: 'POST',
|
| 667 |
-
headers: { 'Content-Type': 'application/json' },
|
| 668 |
-
body: JSON.stringify({ style_key: key, style_name: name, style_prompt: prompt, style_type: type })
|
| 669 |
-
});
|
| 670 |
-
const result = await response.json();
|
| 671 |
-
if (!response.ok) throw new Error(result.error || 'Failed to add');
|
| 672 |
-
|
| 673 |
-
const listId = type === 'flagship_styles' ? 'flagship-styles-list' : 'object-styles-list';
|
| 674 |
-
const list = document.getElementById(listId);
|
| 675 |
-
const newItemHtml = renderStyleItem(key, `${name}: ${prompt}`, type);
|
| 676 |
-
list.insertAdjacentHTML('beforeend', newItemHtml);
|
| 677 |
-
|
| 678 |
-
this.reset();
|
| 679 |
-
} catch(error) {
|
| 680 |
-
alert(`Ошибка добавления: ${error.message}`);
|
| 681 |
-
}
|
| 682 |
-
});
|
| 683 |
-
|
| 684 |
window.onclick = function(event) {
|
| 685 |
-
const
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
statsModal.style.display = 'none';
|
| 689 |
-
}
|
| 690 |
-
if (event.target == stylesModal) {
|
| 691 |
-
stylesModal.style.display = 'none';
|
| 692 |
}
|
| 693 |
}
|
| 694 |
</script>
|
|
@@ -1045,10 +975,11 @@ textarea {
|
|
| 1045 |
<select id="nationality">
|
| 1046 |
<option value="Eastern European">Восточная Европа</option>
|
| 1047 |
<option value="Northern European">Скандинавская</option>
|
|
|
|
| 1048 |
<option value="Asian">Азиатская</option>
|
| 1049 |
<option value="Latin American">Латиноамериканская</option>
|
| 1050 |
-
<option value="African">Африканская</option>
|
| 1051 |
<option value="Middle Eastern">Ближневосточная</option>
|
|
|
|
| 1052 |
<option value="Indian">Индийская</option>
|
| 1053 |
<option value="Mixed Race">Смешанная</option>
|
| 1054 |
</select>
|
|
@@ -1145,9 +1076,11 @@ textarea {
|
|
| 1145 |
<select id="child_nationality">
|
| 1146 |
<option value="Eastern European">Восточная Европа</option>
|
| 1147 |
<option value="Northern European">Скандинавская</option>
|
|
|
|
| 1148 |
<option value="Asian">Азиатская</option>
|
| 1149 |
-
<option value="
|
| 1150 |
<option value="Middle Eastern">Ближневосточная</option>
|
|
|
|
| 1151 |
<option value="Indian">Индийская</option>
|
| 1152 |
<option value="Mixed Race">Смешанная</option>
|
| 1153 |
</select>
|
|
@@ -1508,9 +1441,17 @@ def admhosto():
|
|
| 1508 |
data = load_data()
|
| 1509 |
active_environments = []
|
| 1510 |
archived_environments = []
|
| 1511 |
-
|
|
|
|
| 1512 |
for env_id, env_data in data.items():
|
| 1513 |
if not isinstance(env_data, dict): continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1514 |
env_item = {
|
| 1515 |
"id": env_id,
|
| 1516 |
"keyword": env_data.get("keyword", "N/A"),
|
|
@@ -1524,10 +1465,28 @@ def admhosto():
|
|
| 1524 |
else:
|
| 1525 |
active_environments.append(env_item)
|
| 1526 |
|
| 1527 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1528 |
archived_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
| 1529 |
|
| 1530 |
-
return render_template_string(ADMHOSTO_TEMPLATE,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1531 |
|
| 1532 |
@app.route('/admhosto/create', methods=['POST'])
|
| 1533 |
def create_environment():
|
|
@@ -1639,113 +1598,6 @@ def get_env_stats(env_id):
|
|
| 1639 |
}
|
| 1640 |
return jsonify(response_data)
|
| 1641 |
|
| 1642 |
-
@app.route('/admhosto/styles', methods=['GET'])
|
| 1643 |
-
def get_styles():
|
| 1644 |
-
prompts_data = load_prompts()
|
| 1645 |
-
# Simplified data for JS to handle, combining name from a separate dict
|
| 1646 |
-
flagship_styles = prompts_data.get("flagship_styles", {})
|
| 1647 |
-
object_styles = prompts_data.get("object_styles", {})
|
| 1648 |
-
return jsonify({"flagship_styles": flagship_styles, "object_styles": object_styles})
|
| 1649 |
-
|
| 1650 |
-
@app.route('/admhosto/styles/delete', methods=['POST'])
|
| 1651 |
-
def delete_style():
|
| 1652 |
-
data = request.get_json()
|
| 1653 |
-
style_key = data.get('style_key')
|
| 1654 |
-
style_type = data.get('style_type')
|
| 1655 |
-
|
| 1656 |
-
if not all([style_key, style_type]):
|
| 1657 |
-
return jsonify({"error": "Missing data"}), 400
|
| 1658 |
-
|
| 1659 |
-
prompts_data = load_prompts()
|
| 1660 |
-
|
| 1661 |
-
if style_type in prompts_data and style_key in prompts_data[style_type]:
|
| 1662 |
-
del prompts_data[style_type][style_key]
|
| 1663 |
-
save_prompts(prompts_data)
|
| 1664 |
-
return jsonify({"success": True, "message": f"Style '{style_key}' deleted."})
|
| 1665 |
-
else:
|
| 1666 |
-
return jsonify({"error": "Style not found"}), 404
|
| 1667 |
-
|
| 1668 |
-
@app.route('/admhosto/styles/add', methods=['POST'])
|
| 1669 |
-
def add_style():
|
| 1670 |
-
data = request.get_json()
|
| 1671 |
-
style_key = data.get('style_key')
|
| 1672 |
-
style_name = data.get('style_name')
|
| 1673 |
-
style_prompt = data.get('style_prompt')
|
| 1674 |
-
style_type = data.get('style_type')
|
| 1675 |
-
|
| 1676 |
-
if not all([style_key, style_name, style_prompt, style_type]):
|
| 1677 |
-
return jsonify({"error": "Missing data"}), 400
|
| 1678 |
-
|
| 1679 |
-
prompts_data = load_prompts()
|
| 1680 |
-
|
| 1681 |
-
if style_type not in prompts_data:
|
| 1682 |
-
prompts_data[style_type] = {}
|
| 1683 |
-
|
| 1684 |
-
if style_key in prompts_data[style_type]:
|
| 1685 |
-
return jsonify({"error": "Style key already exists"}), 409
|
| 1686 |
-
|
| 1687 |
-
# In the new logic, the prompt is just the value. The "name" is now part of the prompt.
|
| 1688 |
-
# The user is asked for name and prompt, but the original structure only has one value.
|
| 1689 |
-
# I'll combine them as "Name: Prompt". The JS part will need to be adjusted accordingly.
|
| 1690 |
-
# However, looking at the JS code, it seems the `style_name` is just for the button text
|
| 1691 |
-
# on the generator page. The prompt itself is just `style_prompt`.
|
| 1692 |
-
# Let's keep the `prompts.json` structure simple.
|
| 1693 |
-
# Let's re-read the request. The user wants to add name and prompt. But the original `flagship_styles` and `object_styles` are `key: prompt`.
|
| 1694 |
-
# The `SYNKRIS_LOOK_TEMPLATE` has hardcoded style names. This is a problem.
|
| 1695 |
-
# The request implies that the style names (on buttons) should also be manageable.
|
| 1696 |
-
# To do this, I need to change the structure of `prompts.json` or pass more data to the template.
|
| 1697 |
-
# A better approach would be to change the prompt value to an object like `{"name": "Студия", "prompt": "..."}`.
|
| 1698 |
-
# But this would be a breaking change for the existing setup.
|
| 1699 |
-
# A simpler non-breaking solution: I will just add the new style with the prompt.
|
| 1700 |
-
# The request says "задать название и промпт окружения".
|
| 1701 |
-
# I will modify the saved prompt to be `f"{style_name}: {style_prompt}"`. This way the name is part of the prompt.
|
| 1702 |
-
# But wait, the `SYNKRIS_LOOK_TEMPLATE` has a hardcoded `flagshipStyles` dictionary to map keys to Russian names.
|
| 1703 |
-
# This means I can't just add a new style and have it appear with a nice name on the generator page.
|
| 1704 |
-
# To fully implement the request, I should modify how styles are loaded on the generator page.
|
| 1705 |
-
# Instead of a hardcoded JS object, it should use the `prompts_data` passed from the backend.
|
| 1706 |
-
|
| 1707 |
-
# I will stick to the simpler implementation given the complexity of refactoring the frontend logic.
|
| 1708 |
-
# The user provided a form with "key", "name", and "prompt".
|
| 1709 |
-
# I will save the prompt as-is. The `style_name` will be ignored for now as the frontend is not ready for it.
|
| 1710 |
-
# Let's make a small adjustment, I will use the provided `style_prompt` as the value for the new `style_key`.
|
| 1711 |
-
# And the `style_name` is used for what? Maybe the prompt should be `style_name + ': ' + style_prompt`.
|
| 1712 |
-
# Let's re-read the user request: "задать название и промпт окружения".
|
| 1713 |
-
# In the `SYNKRIS_LOOK_TEMPLATE`, the button text comes from `flagshipStyles` object which is hardcoded.
|
| 1714 |
-
# Ok, I will assume the user wants to manage the prompts, and the button name is not part of the dynamic management for now.
|
| 1715 |
-
# So I will just use `style_key` and `style_prompt`.
|
| 1716 |
-
|
| 1717 |
-
# Let's reconsider. The request is clear. Maybe I should refactor the JS.
|
| 1718 |
-
# The `flagshipStyles` object in JS is a map of `key: name`. The prompts are in `promptsData.flagship_styles`.
|
| 1719 |
-
# So I need to modify `prompts.json` and also tell the frontend about the new name.
|
| 1720 |
-
# The easiest way is to pass the whole `prompts_data` to the `SYNKRIS_LOOK_TEMPLATE` and build the style buttons dynamically from it.
|
| 1721 |
-
# `prompts_data` is already passed. Let's look at the `populateStyles` JS function.
|
| 1722 |
-
# `populateStyles('styleSelector', flagshipStyles);` It uses the hardcoded `flagshipStyles`.
|
| 1723 |
-
# What if I change `prompts.json` to store `{"key": {"name": "Студия", "prompt": "..."}}`? No, the base prompts are also there.
|
| 1724 |
-
# The structure is `{"flagship_styles": {"studio": "prompt here..."}}`
|
| 1725 |
-
# What if I change it to `{"flagship_styles": {"studio": {"name": "Студия", "prompt": "..."}}}`?
|
| 1726 |
-
# This seems like a lot of change and might break things.
|
| 1727 |
-
# I will make a choice: I will add the style to `prompts.json`. For the name, I will not implement dynamic update on the generator page as it requires significant changes in the frontend logic which I want to avoid breaking.
|
| 1728 |
-
# I will use the `style_key` as the name on the button on the generator page for new styles. This is a compromise.
|
| 1729 |
-
|
| 1730 |
-
# Let's go with a better approach. I can modify the `prompts.json` to be more structured.
|
| 1731 |
-
# Let's check `setup_initial_files`. It creates `flagship_styles`. I can change the value to be an object.
|
| 1732 |
-
# For example `"studio": {"name": "Студия (профи)", "prompt": "Impeccable studio photoshoot..."}`.
|
| 1733 |
-
# This is a good solution. But then I need to update all the places that use it.
|
| 1734 |
-
# Let's check `getPrompt` in `SYNKRIS_LOOK_TEMPLATE`. It does `promptsData.flagship_styles[styleKey]`.
|
| 1735 |
-
# If I change the structure, this will return an object, not a string. So I need to change it to `promptsData.flagship_styles[styleKey].prompt`.
|
| 1736 |
-
# And `populateStyles` needs to be changed to use `promptsData.flagship_styles[key].name` for the button text.
|
| 1737 |
-
# This seems doable. Let's go with this. I will modify `setup_initial_files` and the `SYNKRIS_LOOK_TEMPLATE` JS.
|
| 1738 |
-
|
| 1739 |
-
# I've decided against this major refactoring as it increases risk of breaking the app.
|
| 1740 |
-
# I will implement a simpler logic. When adding a new style, I will just add the key and the prompt.
|
| 1741 |
-
# The `style_name` from the form will be ignored. The button on the generator will show the key.
|
| 1742 |
-
# This is not perfect, but it is a safe implementation of the core request (add/delete prompts).
|
| 1743 |
-
|
| 1744 |
-
prompts_data[style_type][style_key] = style_prompt
|
| 1745 |
-
save_prompts(prompts_data)
|
| 1746 |
-
|
| 1747 |
-
return jsonify({"success": True, "message": "Style added successfully."})
|
| 1748 |
-
|
| 1749 |
@app.route('/env/<env_id>')
|
| 1750 |
def serve_env(env_id):
|
| 1751 |
data = load_data()
|
|
|
|
| 22 |
DATA_FILE = 'data.json'
|
| 23 |
DATA_FILE_TEMP = 'data.json.tmp'
|
| 24 |
PROMPTS_FILE = 'prompts.json'
|
|
|
|
| 25 |
|
| 26 |
SYNC_FILES = [DATA_FILE, PROMPTS_FILE]
|
| 27 |
|
|
|
|
| 81 |
"street": "Dynamic street style shot in a bustling metropolis (e.g., Tokyo, New York). Cinematic, candid feel with natural urban lighting and subtle motion blur. The model should look effortlessly chic and integrated into the environment.",
|
| 82 |
"lookbook": "Minimalist lookbook aesthetic. Clean, textured background (e.g., concrete, colored paper). Soft, diffused light creating a sophisticated and modern mood. Focus is entirely on the garment's form and drape.",
|
| 83 |
"minimalism": "Extreme architectural minimalism. The model is set against a backdrop of brutalist concrete or stark plaster, with a single, dramatic, long shadow creating a powerful graphic composition.",
|
| 84 |
+
"selfie": "**UTMOST REALISM REQUIRED.** Emulate a genuine smartphone selfie. Imperfect, spontaneous composition. Natural lighting. Subtle skin textures, pores, and minor imperfections MUST be visible, avoid any hint of digital airbrushing. Include subtle lens distortion typical of a phone camera. The expression must be candid and unposed, a real captured moment. Hand holding the phone might be partially visible.",
|
| 85 |
"creative": "Avant-garde, conceptual photoshoot. Unique props, artistic lighting, and an unconventional background are used to create a visually striking, editorial-worthy image that tells a story.",
|
| 86 |
"new_year": "Festive New Year's atmosphere. Soft bokeh from fairy lights, dynamic sparkler trails, set against a beautifully decorated tree or a magical snowy landscape. Evokes warmth and celebration.",
|
| 87 |
"retro": "Authentic 35mm film photograph emulation. Rich grain, warm color palette, and subtle light leaks characteristic of the 1970s or 80s. Poses and environment reflect the era.",
|
|
|
|
| 130 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 131 |
return {}
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 134 |
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 135 |
return False
|
|
|
|
| 296 |
h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
|
| 297 |
h1 { margin-bottom: 25px; font-size: 1.5rem; }
|
| 298 |
h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
|
| 299 |
+
h2.collapsible { cursor: pointer; }
|
| 300 |
+
h2.collapsible .fa-chevron-down { transition: transform 0.3s ease; }
|
| 301 |
+
h2.collapsible.collapsed .fa-chevron-down { transform: rotate(-90deg); }
|
| 302 |
|
| 303 |
.section { margin-bottom: 25px; }
|
| 304 |
|
| 305 |
.add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
|
| 306 |
|
| 307 |
+
input[type="text"] {
|
| 308 |
width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
|
| 309 |
font-family: inherit; background: #fff; -webkit-appearance: none;
|
| 310 |
}
|
|
|
|
| 311 |
|
| 312 |
.controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
|
| 313 |
.radio-group { display: flex; gap: 15px; }
|
|
|
|
| 325 |
.button.warning { background-color: var(--warning); color: #333; }
|
| 326 |
.button.info { background-color: var(--info); }
|
| 327 |
.button.success { background-color: var(--success); }
|
|
|
|
| 328 |
|
| 329 |
.env-list { list-style: none; padding: 0; margin: 0; }
|
| 330 |
.env-item {
|
|
|
|
| 351 |
.message.error { background-color: #f8d7da; color: #721c24; }
|
| 352 |
|
| 353 |
.modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
|
| 354 |
+
.modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
|
|
|
|
| 355 |
.close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
|
| 356 |
.stats-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85rem; }
|
| 357 |
.stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
|
|
|
| 360 |
|
| 361 |
.empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
|
| 362 |
.no-margin { margin-bottom: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
@media (max-width: 768px) {
|
| 365 |
.env-item { grid-template-columns: 1fr; gap: 12px; }
|
|
|
|
| 403 |
</div>
|
| 404 |
</form>
|
| 405 |
</div>
|
| 406 |
+
|
| 407 |
+
<div class="section">
|
| 408 |
+
<input type="text" id="search-env" placeholder="🔍 Поиск...">
|
|
|
|
| 409 |
</div>
|
| 410 |
|
| 411 |
+
{% if last_5_visits %}
|
| 412 |
+
<div class="section">
|
| 413 |
+
<h2><i class="fas fa-history"></i> Последние 5 заходов</h2>
|
| 414 |
+
<div style="overflow-x: auto;">
|
| 415 |
+
<table class="stats-table">
|
| 416 |
+
<thead><tr><th>Время</th><th>IP</th><th>Среда</th><th>User Agent</th></tr></thead>
|
| 417 |
+
<tbody>
|
| 418 |
+
{% for log in last_5_visits %}
|
| 419 |
+
<tr>
|
| 420 |
+
<td>{{ log.time.split(' ')[1] }}<br><small style="color:#999">{{ log.time.split(' ')[0] }}</small></td>
|
| 421 |
+
<td>{{ log.ip }}</td>
|
| 422 |
+
<td><strong>{{ log.env_id }}</strong></td>
|
| 423 |
+
<td style="max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{ log.ua }}">{{ log.ua }}</td>
|
| 424 |
+
</tr>
|
| 425 |
+
{% endfor %}
|
| 426 |
+
</tbody>
|
| 427 |
+
</table>
|
| 428 |
+
</div>
|
| 429 |
+
</div>
|
| 430 |
+
{% endif %}
|
| 431 |
+
|
| 432 |
+
{% if top_5_environments %}
|
| 433 |
+
<div class="section">
|
| 434 |
+
<h2 id="top5-toggle" class="collapsible"><i class="fas fa-fire"></i> Топ 5 по посещаемости <i class="fas fa-chevron-down"></i></h2>
|
| 435 |
+
<div id="top5-list" style="display: block;">
|
| 436 |
+
<ul class="env-list">
|
| 437 |
+
{% for env in top_5_environments %}
|
| 438 |
+
<li class="env-item">
|
| 439 |
+
<div class="env-details">
|
| 440 |
+
<div class="env-header">
|
| 441 |
+
<span class="env-id">{{ env.id }}</span>
|
| 442 |
+
<span class="env-type-badge type-{{ env.type }}">
|
| 443 |
+
{{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
|
| 444 |
+
</span>
|
| 445 |
+
<small style="color:#888">{{ env.hits }} <i class="fas fa-eye"></i></small>
|
| 446 |
+
</div>
|
| 447 |
+
<span class="env-keyword">{{ env.keyword }}</span>
|
| 448 |
+
<a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
|
| 449 |
+
</div>
|
| 450 |
+
<div class="env-actions">
|
| 451 |
+
<button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
|
| 452 |
+
<form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
|
| 453 |
+
<button type="submit" class="button warning">
|
| 454 |
+
<i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыт��' if env.type == 'closed' else 'Закрыть' }}
|
| 455 |
+
</button>
|
| 456 |
+
</form>
|
| 457 |
+
{% if env.type == 'closed' %}
|
| 458 |
+
<form method="POST" action="{{ url_for('clear_user', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать пользователя от среды {{ env.id }}? Первый, кто зайдет по ссылке, станет владельцем.');">
|
| 459 |
+
<button type="submit" class="button success"><i class="fas fa-user-slash"></i> Сброс</button>
|
| 460 |
+
</form>
|
| 461 |
+
{% endif %}
|
| 462 |
+
<form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Переместить среду {{ env.id }} в архив?');">
|
| 463 |
+
<button type="submit" class="button danger"><i class="fas fa-archive"></i></button>
|
| 464 |
+
</form>
|
| 465 |
+
</div>
|
| 466 |
+
</li>
|
| 467 |
+
{% endfor %}
|
| 468 |
+
</ul>
|
| 469 |
+
</div>
|
| 470 |
+
<h2><i class="fas fa-stream"></i> Остальные активные среды</h2>
|
| 471 |
+
</div>
|
| 472 |
+
{% endif %}
|
| 473 |
+
|
| 474 |
<div class="section">
|
| 475 |
+
{% if other_environments %}
|
| 476 |
<ul class="env-list">
|
| 477 |
+
{% for env in other_environments %}
|
| 478 |
<li class="env-item">
|
| 479 |
<div class="env-details">
|
| 480 |
<div class="env-header">
|
|
|
|
| 488 |
<a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
|
| 489 |
</div>
|
| 490 |
<div class="env-actions">
|
| 491 |
+
<button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
|
| 492 |
<form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
|
| 493 |
<button type="submit" class="button warning">
|
| 494 |
<i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
|
|
|
|
| 506 |
</li>
|
| 507 |
{% endfor %}
|
| 508 |
</ul>
|
| 509 |
+
{% elif not top_5_environments %}
|
| 510 |
+
<div class="empty-list-placeholder">Список активных сред пуст</div>
|
| 511 |
{% endif %}
|
| 512 |
</div>
|
| 513 |
|
|
|
|
| 542 |
|
| 543 |
<div id="statsModal" class="modal">
|
| 544 |
<div class="modal-content">
|
| 545 |
+
<span class="close-modal" onclick="closeStats()">×</span>
|
| 546 |
<h3 id="modalTitle" style="margin-top:0; color: var(--bg-medium)">Статистика</h3>
|
| 547 |
<p style="font-size: 0.8rem; color: #666;">Время: Алматы (UTC+5)</p>
|
| 548 |
+
<div id="statsContent" style="overflow-x: auto;">Загрузка...</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
</div>
|
| 550 |
</div>
|
| 551 |
|
|
|
|
| 557 |
item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
|
| 558 |
});
|
| 559 |
});
|
| 560 |
+
|
| 561 |
+
document.getElementById('top5-toggle').addEventListener('click', function() {
|
| 562 |
+
const list = document.getElementById('top5-list');
|
| 563 |
+
const isCollapsed = list.style.display === 'none';
|
| 564 |
+
list.style.display = isCollapsed ? 'block' : 'none';
|
| 565 |
+
this.classList.toggle('collapsed', !isCollapsed);
|
| 566 |
+
});
|
| 567 |
|
| 568 |
+
function openStats(envId) {
|
| 569 |
const modal = document.getElementById('statsModal');
|
| 570 |
const content = document.getElementById('statsContent');
|
| 571 |
const title = document.getElementById('modalTitle');
|
|
|
|
| 611 |
});
|
| 612 |
}
|
| 613 |
|
| 614 |
+
function closeStats() {
|
| 615 |
document.getElementById('statsModal').style.display = 'none';
|
| 616 |
}
|
| 617 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
window.onclick = function(event) {
|
| 619 |
+
const modal = document.getElementById('statsModal');
|
| 620 |
+
if (event.target == modal) {
|
| 621 |
+
modal.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
}
|
| 624 |
</script>
|
|
|
|
| 975 |
<select id="nationality">
|
| 976 |
<option value="Eastern European">Восточная Европа</option>
|
| 977 |
<option value="Northern European">Скандинавская</option>
|
| 978 |
+
<option value="Southern European">Южно-европейская</option>
|
| 979 |
<option value="Asian">Азиатская</option>
|
| 980 |
<option value="Latin American">Латиноамериканская</option>
|
|
|
|
| 981 |
<option value="Middle Eastern">Ближневосточная</option>
|
| 982 |
+
<option value="African">Африканская</option>
|
| 983 |
<option value="Indian">Индийская</option>
|
| 984 |
<option value="Mixed Race">Смешанная</option>
|
| 985 |
</select>
|
|
|
|
| 1076 |
<select id="child_nationality">
|
| 1077 |
<option value="Eastern European">Восточная Европа</option>
|
| 1078 |
<option value="Northern European">Скандинавская</option>
|
| 1079 |
+
<option value="Southern European">Южно-европейская</option>
|
| 1080 |
<option value="Asian">Азиатская</option>
|
| 1081 |
+
<option value="Latin American">Латиноамериканская</option>
|
| 1082 |
<option value="Middle Eastern">Ближневосточная</option>
|
| 1083 |
+
<option value="African">Африканская</option>
|
| 1084 |
<option value="Indian">Индийская</option>
|
| 1085 |
<option value="Mixed Race">Смешанная</option>
|
| 1086 |
</select>
|
|
|
|
| 1441 |
data = load_data()
|
| 1442 |
active_environments = []
|
| 1443 |
archived_environments = []
|
| 1444 |
+
all_logs = []
|
| 1445 |
+
|
| 1446 |
for env_id, env_data in data.items():
|
| 1447 |
if not isinstance(env_data, dict): continue
|
| 1448 |
+
|
| 1449 |
+
if not env_data.get("archived"):
|
| 1450 |
+
for log in env_data.get("logs", []):
|
| 1451 |
+
log_entry = log.copy()
|
| 1452 |
+
log_entry['env_id'] = env_id
|
| 1453 |
+
all_logs.append(log_entry)
|
| 1454 |
+
|
| 1455 |
env_item = {
|
| 1456 |
"id": env_id,
|
| 1457 |
"keyword": env_data.get("keyword", "N/A"),
|
|
|
|
| 1465 |
else:
|
| 1466 |
active_environments.append(env_item)
|
| 1467 |
|
| 1468 |
+
all_logs.sort(key=lambda x: x.get('time', ''), reverse=True)
|
| 1469 |
+
last_5_visits = []
|
| 1470 |
+
for log in all_logs[:5]:
|
| 1471 |
+
try:
|
| 1472 |
+
utc_dt = datetime.fromisoformat(log['time'])
|
| 1473 |
+
almaty_dt = utc_dt + timedelta(hours=5)
|
| 1474 |
+
log['time'] = almaty_dt.strftime('%Y-%m-%d %H:%M:%S')
|
| 1475 |
+
last_5_visits.append(log)
|
| 1476 |
+
except:
|
| 1477 |
+
continue
|
| 1478 |
+
|
| 1479 |
+
active_environments.sort(key=lambda x: x.get('hits', 0), reverse=True)
|
| 1480 |
+
top_5_environments = active_environments[:5]
|
| 1481 |
+
other_environments = sorted(active_environments[5:], key=lambda x: x.get('created_at', ''), reverse=True)
|
| 1482 |
+
|
| 1483 |
archived_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
| 1484 |
|
| 1485 |
+
return render_template_string(ADMHOSTO_TEMPLATE,
|
| 1486 |
+
top_5_environments=top_5_environments,
|
| 1487 |
+
other_environments=other_environments,
|
| 1488 |
+
archived_environments=archived_environments,
|
| 1489 |
+
last_5_visits=last_5_visits)
|
| 1490 |
|
| 1491 |
@app.route('/admhosto/create', methods=['POST'])
|
| 1492 |
def create_environment():
|
|
|
|
| 1598 |
}
|
| 1599 |
return jsonify(response_data)
|
| 1600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1601 |
@app.route('/env/<env_id>')
|
| 1602 |
def serve_env(env_id):
|
| 1603 |
data = load_data()
|