diff --git a/hh_browser.py b/hh_browser.py
index c764d17..668810b 100644
--- a/hh_browser.py
+++ b/hh_browser.py
@@ -164,9 +164,10 @@ def _save_screenshot(page, screenshot_dir, vid):
def open_response_page(account_dir, user, account, vacancy, message,
- resume_id=None, screenshot_dir=None):
- """Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст,
- делает скриншот. Возвращает (ok, note, screenshot_path)."""
+ resume_id=None, screenshot_dir=None, submit=False):
+ """Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст.
+ При submit=True дополнительно жмёт «Отправить» и снимает скриншот результата.
+ Возвращает (ok, note, screenshot_path)."""
key = (user, account)
# Закрываем предыдущую страницу этого аккаунта
prev = _pages.pop(key, None)
@@ -222,10 +223,32 @@ def open_response_page(account_dir, user, account, vacancy, message,
except Exception:
continue
+ if not filled:
+ shot = _save_screenshot(page, screenshot_dir, vacancy["id"])
+ return False, "Не нашёл поле сообщения — проверьте скриншот", shot
+
+ # Одно-кликовый режим: сразу жмём «Отправить»
+ if submit:
+ submitted = False
+ for sel in SUBMIT_BUTTONS:
+ try:
+ btn = page.locator(sel).first
+ if btn.is_visible(timeout=3000):
+ btn.click()
+ submitted = True
+ break
+ except Exception:
+ continue
+ if not submitted:
+ shot = _save_screenshot(page, screenshot_dir, vacancy["id"])
+ return False, "Не нашёл кнопку «Отправить» — проверьте скриншот", shot
+ # Ждём подтверждение и снимаем скриншот результата
+ page.wait_for_timeout(2500)
+ shot = _save_screenshot(page, screenshot_dir, vacancy["id"])
+ return True, "Отклик отправлен", shot
+
shot = _save_screenshot(page, screenshot_dir, vacancy["id"])
- if filled:
- return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot
- return False, "Не нашёл поле сообщения — проверьте скриншот", shot
+ return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot
def confirm_send(user, account, vid):
@@ -270,10 +293,10 @@ def close_context(user, account):
# ---------- Публичные обёртки (можно вызывать из любого потока) ----------
def browser_open(account_dir, user, account, vacancy, message,
- resume_id=None, screenshot_dir=None):
+ resume_id=None, screenshot_dir=None, submit=False):
return run_in_browser(lambda: open_response_page(
account_dir, user, account, vacancy, message,
- resume_id=resume_id, screenshot_dir=screenshot_dir))
+ resume_id=resume_id, screenshot_dir=screenshot_dir, submit=submit))
def browser_send(user, account, vid):
diff --git a/templates/index.html b/templates/index.html
index 5caa479..6075cfc 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -252,7 +252,7 @@
-
+
@@ -263,6 +263,7 @@ let currentAccount = null;
let vacancies = [];
let generatedTexts = {};
let currentShotId = null;
+let sentTexts = new Set();
// ---------- Вкладки ----------
function showTab(name) {
@@ -557,12 +558,15 @@ function renderTexts(errors = {}) {
vacancies.filter(v => generatedTexts[v.id]).forEach(v => {
const div = document.createElement('div');
div.className = 'item';
+ const sent = sentTexts.has(v.id);
div.innerHTML = `
${v.title} — ${v.company || ''}
-
-
+ ${sent
+ ? ''
+ : `
+ `}
`;
list.appendChild(div);
@@ -582,6 +586,16 @@ function escapeHtml(s) {
}
// ---------- Открытие, проверка и отправка ----------
+function showShotModal(id, screenshot, note, sent) {
+ currentShotId = id;
+ document.getElementById('shot-img').src = screenshot + '?t=' + Date.now();
+ document.getElementById('shot-note').textContent = note;
+ document.getElementById('send-note').textContent = '';
+ document.getElementById('btn-send').style.display = sent ? 'none' : '';
+ document.getElementById('btn-done').style.display = sent ? 'none' : '';
+ document.getElementById('shot-modal').classList.remove('hidden');
+}
+
async function openVacancy(id) {
const ta = document.querySelector(`textarea[data-id="${id}"]`);
const message = ta ? ta.value : (generatedTexts[id] || '');
@@ -593,12 +607,7 @@ async function openVacancy(id) {
});
if (note) note.textContent = data.note || '';
if (data.screenshot) {
- currentShotId = id;
- document.getElementById('shot-img').src = data.screenshot + '?t=' + Date.now();
- document.getElementById('shot-note').textContent = data.note || '';
- document.getElementById('send-note').textContent = '';
- document.getElementById('btn-send').disabled = false;
- document.getElementById('shot-modal').classList.remove('hidden');
+ showShotModal(id, data.screenshot, data.note || '', false);
} else {
alert(data.note || 'Не удалось открыть вакансию');
}
@@ -608,6 +617,33 @@ async function openVacancy(id) {
}
}
+async function sendVacancy(id) {
+ if (!currentAccount) { alert('Сначала выберите аккаунт'); return; }
+ const ta = document.querySelector(`textarea[data-id="${id}"]`);
+ const message = ta ? ta.value : (generatedTexts[id] || '');
+ const note = document.getElementById(`note-${id}`);
+ if (note) { note.textContent = 'Отправляю отклик...'; }
+ try {
+ const data = await api(`/api/accounts/${currentAccount}/send-now`, {
+ method: 'POST', body: JSON.stringify({ id, message }),
+ });
+ if (note) note.textContent = data.note || '';
+ if (data.screenshot) {
+ showShotModal(id, data.screenshot, data.note || 'Отклик отправлен', true);
+ sentTexts.add(id);
+ const v = vacancies.find(x => x.id === id);
+ if (v) v.responded = true;
+ renderVacancies();
+ renderTexts();
+ } else {
+ alert(data.note || 'Не удалось отправить отклик');
+ }
+ } catch (e) {
+ if (note) note.textContent = 'Ошибка: ' + e.message;
+ alert('Ошибка: ' + e.message);
+ }
+}
+
function closeShot() {
document.getElementById('shot-modal').classList.add('hidden');
}
@@ -625,7 +661,9 @@ async function confirmSend() {
if (data.ok) {
const v = vacancies.find(x => x.id === currentShotId);
if (v) v.responded = true;
+ sentTexts.add(currentShotId);
renderVacancies();
+ renderTexts();
}
} catch (e) {
note.textContent = 'Ошибка: ' + e.message;
@@ -640,7 +678,9 @@ async function markDone(id) {
});
const v = vacancies.find(x => x.id === id);
if (v) v.responded = true;
+ sentTexts.add(id);
renderVacancies();
+ renderTexts();
const note = document.getElementById(`note-${id}`);
if (note) note.textContent = 'Отмечено как отправленное';
}
diff --git a/webui.py b/webui.py
index 8da55e2..a156a1d 100644
--- a/webui.py
+++ b/webui.py
@@ -168,6 +168,15 @@ def save_progress(acc_dir, progress):
write_json(os.path.join(acc_dir, "progress.json"), progress)
+def mark_responded(acc_dir, vid):
+ """Добавляет вакансию в список откликнутых."""
+ progress = get_progress(acc_dir)
+ responded = set(progress.get("responded", []))
+ responded.add(vid)
+ progress["responded"] = sorted(responded)
+ save_progress(acc_dir, progress)
+
+
def get_cache(user, account):
with _cache_lock:
return _cache.setdefault((user, account), {})
@@ -534,6 +543,40 @@ def api_open(name):
return jsonify({"error": f"Ошибка браузера: {e}"}), 500
+@app.route("/api/accounts//send-now", methods=["POST"])
+@login_required
+def api_send_now(name):
+ """Одно-кликовый режим: открывает вакансию, вставляет текст, жмёт «Отправить»,
+ делает скриншот результата и помечает вакансию как откликнутую."""
+ acc_dir = account_path(name)
+ if not os.path.isdir(acc_dir):
+ return jsonify({"error": "Аккаунт не найден"}), 404
+ data = request.get_json(force=True)
+ vid = data.get("id")
+ message = data.get("message") or ""
+ cfg = read_json(os.path.join(acc_dir, "config.json"), {})
+ resume_id = cfg.get("resume_id")
+ cache = get_cache(current_user(), name)
+ vacancy = cache.get(vid)
+ if not vacancy:
+ return jsonify({"error": "Вакансия не найдена в кэше — обновите поиск"}), 404
+
+ try:
+ shot_dir = os.path.join(acc_dir, "screenshots")
+ ok, note, shot = browser_open(acc_dir, current_user(), name, vacancy, message,
+ resume_id=resume_id, screenshot_dir=shot_dir,
+ submit=True)
+ if ok:
+ mark_responded(acc_dir, vid)
+ return jsonify({
+ "ok": ok,
+ "note": note,
+ "screenshot": f"/api/accounts/{name}/screenshot/{vid}" if shot else None,
+ })
+ except Exception as e:
+ return jsonify({"error": f"Ошибка браузера: {e}"}), 500
+
+
@app.route("/api/accounts//send", methods=["POST"])
@login_required
def api_send(name):
@@ -571,11 +614,7 @@ def api_done(name):
return jsonify({"error": "Аккаунт не найден"}), 404
data = request.get_json(force=True)
vid = data.get("id")
- progress = get_progress(acc_dir)
- responded = set(progress.get("responded", []))
- responded.add(vid)
- progress["responded"] = sorted(responded)
- save_progress(acc_dir, progress)
+ mark_responded(acc_dir, vid)
return jsonify({"ok": True})