Britzzy commited on
Commit
a3e854d
·
1 Parent(s): 7aa4187

fix: isolate ALTER TABLE in own transaction to prevent PG InFailedSqlTransaction + auto-save CV editor

Browse files
backend/database.py CHANGED
@@ -272,21 +272,24 @@ def _exec_lastid(conn, sql: str, params=()):
272
 
273
  # ── Init ─────────────────────────────────────────────────────────────────────
274
 
 
 
 
 
 
 
 
 
 
275
  def init_db():
276
  if _is_pg():
277
  with get_db() as conn:
278
  with conn.cursor() as cur:
279
  cur.execute(_PG_DDL)
280
- try:
281
- with conn.cursor() as cur:
282
- cur.execute("ALTER TABLE global_jobs ALTER COLUMN date_found TYPE TIMESTAMP USING date_found::timestamp")
283
- except Exception:
284
- pass
285
- try:
286
- with conn.cursor() as cur:
287
- cur.execute("ALTER TABLE user_cv ADD COLUMN raw_text TEXT DEFAULT ''")
288
- except Exception:
289
- pass
290
  _seed_categories(conn)
291
  _seed_admin_user(conn)
292
  else:
 
272
 
273
  # ── Init ─────────────────────────────────────────────────────────────────────
274
 
275
+ def _safe_alter(conn, sql: str):
276
+ try:
277
+ with conn.cursor() as cur:
278
+ cur.execute(sql)
279
+ conn.commit()
280
+ except Exception:
281
+ conn.rollback()
282
+
283
+
284
  def init_db():
285
  if _is_pg():
286
  with get_db() as conn:
287
  with conn.cursor() as cur:
288
  cur.execute(_PG_DDL)
289
+ conn.commit()
290
+ _safe_alter(conn, "ALTER TABLE global_jobs ALTER COLUMN date_found TYPE TIMESTAMP USING date_found::timestamp")
291
+ _safe_alter(conn, "ALTER TABLE user_cv ADD COLUMN IF NOT EXISTS raw_text TEXT DEFAULT ''")
292
+ _safe_alter(conn, "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin INTEGER DEFAULT 0")
 
 
 
 
 
 
293
  _seed_categories(conn)
294
  _seed_admin_user(conn)
295
  else:
backend/main.py CHANGED
@@ -233,16 +233,19 @@ def make_cv(req: MakeCVRequest, current_user: dict = Depends(get_current_user)):
233
  except Exception as e:
234
  raise HTTPException(500, f"Failed to save CV: {str(e)}")
235
 
236
- # Generate preview text + documents
237
- preview = generate_cv_preview_text(cv_data)
238
-
239
- name = (cv_data.get("personal_info") or {}).get("name", "")
240
- slug = _file_slug(name, req.target_jobs[0] if req.target_jobs else "CV", "", number="00")
241
- cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
242
- cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
243
- profile = get_cv_profile(str(user_id))
244
- generate_cv_docx(cv_data, cv_path, target_type=req.target_type, profile=profile)
245
- generate_cv_pdf(cv_data, cv_pdf_path, target_type=req.target_type, profile=profile)
 
 
 
246
 
247
  return {
248
  "status": "ok",
 
233
  except Exception as e:
234
  raise HTTPException(500, f"Failed to save CV: {str(e)}")
235
 
236
+ try:
237
+ # Generate preview text + documents
238
+ preview = generate_cv_preview_text(cv_data)
239
+
240
+ name = (cv_data.get("personal_info") or {}).get("name", "")
241
+ slug = _file_slug(name, req.target_jobs[0] if req.target_jobs else "CV", "", number="00")
242
+ cv_path = str(GENERATED_DIR / f"{slug}_cv.docx")
243
+ cv_pdf_path = str(GENERATED_DIR / f"{slug}_cv.pdf")
244
+ profile = get_cv_profile(str(user_id))
245
+ generate_cv_docx(cv_data, cv_path, target_type=req.target_type, profile=profile)
246
+ generate_cv_pdf(cv_data, cv_pdf_path, target_type=req.target_type, profile=profile)
247
+ except Exception as e:
248
+ raise HTTPException(500, f"Document generation failed: {type(e).__name__}: {e}")
249
 
250
  return {
251
  "status": "ok",
frontend/cv-editor.html CHANGED
@@ -89,6 +89,7 @@
89
  <p class="subtitle">Manage your professional profile</p>
90
  </div>
91
  <div class="actions">
 
92
  <button class="btn btn-primary" onclick="saveCv()" id="saveBtn">Save CV</button>
93
  <button class="btn btn-secondary" onclick="toggleJsonEditor()">&#9998; JSON Mode</button>
94
  </div>
@@ -155,19 +156,19 @@
155
  <div class="cv-form-section">
156
  <h3>&#128188; Experience</h3>
157
  <div id="experienceList"></div>
158
- <button class="add-entry-btn" onclick="addExperience()">+ Add Experience</button>
159
  </div>
160
 
161
  <div class="cv-form-section">
162
  <h3>&#127891; Education</h3>
163
  <div id="educationList"></div>
164
- <button class="add-entry-btn" onclick="addEducation()">+ Add Education</button>
165
  </div>
166
 
167
  <div class="cv-form-section">
168
  <h3>&#128187; Projects</h3>
169
  <div id="projectsList"></div>
170
- <button class="add-entry-btn" onclick="addProject()">+ Add Project</button>
171
  </div>
172
 
173
  <div class="cv-form-section">
@@ -203,6 +204,8 @@ let savedCv = null;
203
  let expCounter = 0;
204
  let eduCounter = 0;
205
  let projCounter = 0;
 
 
206
 
207
  async function loadCv() {
208
  try {
@@ -304,6 +307,7 @@ function collectFormData() {
304
  }
305
 
306
  async function saveCv() {
 
307
  const btn = document.getElementById("saveBtn");
308
  btn.disabled = true;
309
  btn.innerHTML = '<span class="spinner"></span> Saving...';
@@ -323,6 +327,8 @@ async function saveCv() {
323
  } else {
324
  validateCv(data);
325
  }
 
 
326
  showToast("CV saved successfully");
327
  } catch (err) {
328
  if (err instanceof SyntaxError) showToast("Invalid JSON", "error");
@@ -395,7 +401,7 @@ function addExperience(data) {
395
  const d = data || { title: "", company: "", location: "", start_date: "", end_date: "", current: false, description: "", achievements: [] };
396
  const html = `
397
  <div class="entry-card" data-idx="${num}">
398
- <button class="remove-entry" onclick="this.closest('.entry-card').remove()">&times;</button>
399
  <div class="cv-form-row">
400
  <div class="form-group">
401
  <label>Job Title</label>
@@ -443,7 +449,7 @@ function addEducation(data) {
443
  const d = data || { degree: "", institution: "", location: "", start_date: "", end_date: "", gpa: "" };
444
  const html = `
445
  <div class="entry-card" data-idx="${num}">
446
- <button class="remove-entry" onclick="this.closest('.entry-card').remove()">&times;</button>
447
  <div class="cv-form-row">
448
  <div class="form-group">
449
  <label>Degree</label>
@@ -483,7 +489,7 @@ function addProject(data) {
483
  const d = data || { name: "", description: "", technologies: [], url: "" };
484
  const html = `
485
  <div class="entry-card" data-idx="${num}">
486
- <button class="remove-entry" onclick="this.closest('.entry-card').remove()">&times;</button>
487
  <div class="cv-form-row">
488
  <div class="form-group">
489
  <label>Project Name</label>
@@ -637,9 +643,41 @@ async function convertPlainText() {
637
  }
638
  }
639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  if (!getToken()) window.location.href = "login.html";
641
  updateHeader();
642
- loadCv();
 
 
 
 
 
 
643
  </script>
644
  </body>
645
  </html>
 
89
  <p class="subtitle">Manage your professional profile</p>
90
  </div>
91
  <div class="actions">
92
+ <span id="autoSaveStatus" style="font-size:12px;color:var(--text-subtle);margin-right:8px"></span>
93
  <button class="btn btn-primary" onclick="saveCv()" id="saveBtn">Save CV</button>
94
  <button class="btn btn-secondary" onclick="toggleJsonEditor()">&#9998; JSON Mode</button>
95
  </div>
 
156
  <div class="cv-form-section">
157
  <h3>&#128188; Experience</h3>
158
  <div id="experienceList"></div>
159
+ <button class="add-entry-btn" onclick="addExperience();scheduleAutoSave()">+ Add Experience</button>
160
  </div>
161
 
162
  <div class="cv-form-section">
163
  <h3>&#127891; Education</h3>
164
  <div id="educationList"></div>
165
+ <button class="add-entry-btn" onclick="addEducation();scheduleAutoSave()">+ Add Education</button>
166
  </div>
167
 
168
  <div class="cv-form-section">
169
  <h3>&#128187; Projects</h3>
170
  <div id="projectsList"></div>
171
+ <button class="add-entry-btn" onclick="addProject();scheduleAutoSave()">+ Add Project</button>
172
  </div>
173
 
174
  <div class="cv-form-section">
 
204
  let expCounter = 0;
205
  let eduCounter = 0;
206
  let projCounter = 0;
207
+ let autoSaveTimer = null;
208
+ let isLoadingCv = false;
209
 
210
  async function loadCv() {
211
  try {
 
307
  }
308
 
309
  async function saveCv() {
310
+ clearTimeout(autoSaveTimer);
311
  const btn = document.getElementById("saveBtn");
312
  btn.disabled = true;
313
  btn.innerHTML = '<span class="spinner"></span> Saving...';
 
327
  } else {
328
  validateCv(data);
329
  }
330
+ const status = document.getElementById("autoSaveStatus");
331
+ if (status) { status.textContent = "\u2713 Saved"; setTimeout(() => { status.textContent = ""; }, 3000); }
332
  showToast("CV saved successfully");
333
  } catch (err) {
334
  if (err instanceof SyntaxError) showToast("Invalid JSON", "error");
 
401
  const d = data || { title: "", company: "", location: "", start_date: "", end_date: "", current: false, description: "", achievements: [] };
402
  const html = `
403
  <div class="entry-card" data-idx="${num}">
404
+ <button class="remove-entry" onclick="this.closest('.entry-card').remove();scheduleAutoSave()">&times;</button>
405
  <div class="cv-form-row">
406
  <div class="form-group">
407
  <label>Job Title</label>
 
449
  const d = data || { degree: "", institution: "", location: "", start_date: "", end_date: "", gpa: "" };
450
  const html = `
451
  <div class="entry-card" data-idx="${num}">
452
+ <button class="remove-entry" onclick="this.closest('.entry-card').remove();scheduleAutoSave()">&times;</button>
453
  <div class="cv-form-row">
454
  <div class="form-group">
455
  <label>Degree</label>
 
489
  const d = data || { name: "", description: "", technologies: [], url: "" };
490
  const html = `
491
  <div class="entry-card" data-idx="${num}">
492
+ <button class="remove-entry" onclick="this.closest('.entry-card').remove();scheduleAutoSave()">&times;</button>
493
  <div class="cv-form-row">
494
  <div class="form-group">
495
  <label>Project Name</label>
 
643
  }
644
  }
645
 
646
+ function scheduleAutoSave() {
647
+ if (isLoadingCv) return;
648
+ clearTimeout(autoSaveTimer);
649
+ const status = document.getElementById("autoSaveStatus");
650
+ if (status) status.textContent = "Unsaved changes\u2026";
651
+ autoSaveTimer = setTimeout(async () => {
652
+ try {
653
+ let data;
654
+ const jsonView = document.getElementById("jsonView");
655
+ if (jsonView.style.display !== "none") {
656
+ data = JSON.parse(document.getElementById("cvJson").value);
657
+ } else {
658
+ data = collectFormData();
659
+ }
660
+ await apiFetch("PUT", "/api/cv", data);
661
+ savedCv = data;
662
+ if (status) status.textContent = "\u2713 Saved";
663
+ setTimeout(() => { if (status) status.textContent = ""; }, 3000);
664
+ } catch (err) {
665
+ const status = document.getElementById("autoSaveStatus");
666
+ if (status) status.textContent = "Save failed";
667
+ console.error("Auto-save failed:", err);
668
+ }
669
+ }, 2000);
670
+ }
671
+
672
  if (!getToken()) window.location.href = "login.html";
673
  updateHeader();
674
+
675
+ isLoadingCv = true;
676
+ loadCv().then(() => { isLoadingCv = false; });
677
+
678
+ document.getElementById("formView").addEventListener("input", scheduleAutoSave);
679
+ document.getElementById("formView").addEventListener("change", scheduleAutoSave);
680
+ document.getElementById("cvJson").addEventListener("input", scheduleAutoSave);
681
  </script>
682
  </body>
683
  </html>
frontend/make-cv.html CHANGED
@@ -330,15 +330,15 @@ I'm interested in full-stack development roles. I also have a Google Cloud certi
330
  </div>
331
  <div id="generatedPreview" class="cv-preview"></div>
332
  <div id="generatedActions" style="display:none;margin-top:var(--space-3)">
333
- <div style="display:flex;gap:8px;flex-wrap:wrap">
334
- <button class="btn btn-primary" onclick="saveCv()">&#10003; Save &amp; Use for Tailoring</button>
335
- <button class="btn btn-secondary" id="downloadDocxBtn" style="display:none" onclick="downloadFile('docx')">&#128196; Download CV (DOCX)</button>
336
  <button class="btn btn-secondary" id="downloadPdfBtn" style="display:none" onclick="downloadFile('pdf')">&#128196; Download CV (PDF)</button>
337
  <button class="btn btn-secondary" onclick="location.href='cv-editor.html'">&#9998; Edit in CV Editor</button>
338
- </div>
339
- <p style="font-size:12px;color:var(--text-muted);margin-top:12px">
340
- Your CV is automatically saved so you can use it with the <a href="manual-job.html">Manual Entry</a> tool to tailor it to specific jobs.
341
- </p>
342
  </div>
343
  <div id="generatedError" style="display:none"></div>
344
  </div>
@@ -552,16 +552,6 @@ async function generateCV() {
552
  btn.innerHTML = '<span>&#9889;</span> Generate My CV';
553
  }
554
 
555
- async function saveCv() {
556
- if (!generatedCv) return;
557
- try {
558
- await apiFetch("PUT", "/api/cv", generatedCv);
559
- showToast("CV saved! Ready for tailoring.");
560
- } catch (err) {
561
- showToast(err.message, "error");
562
- }
563
- }
564
-
565
  async function downloadFile(format) {
566
  if (!generatedPaths) return;
567
  const key = format === "pdf" ? "cv_pdf" : "cv";
 
330
  </div>
331
  <div id="generatedPreview" class="cv-preview"></div>
332
  <div id="generatedActions" style="display:none;margin-top:var(--space-3)">
333
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
334
+ <span style="font-size:12px;color:var(--color-success-500);font-weight:500">&#10003; CV auto-saved</span>
335
+ <button class="btn btn-secondary" id="downloadDocxBtn" style="display:none" onclick="downloadFile('docx')">&#128196; Download CV (DOCX)</button>
336
  <button class="btn btn-secondary" id="downloadPdfBtn" style="display:none" onclick="downloadFile('pdf')">&#128196; Download CV (PDF)</button>
337
  <button class="btn btn-secondary" onclick="location.href='cv-editor.html'">&#9998; Edit in CV Editor</button>
338
+ </div>
339
+ <p style="font-size:12px;color:var(--text-muted);margin-top:12px">
340
+ Your CV is saved automatically. Use it with the <a href="manual-job.html">Manual Entry</a> tool to tailor it to specific jobs.
341
+ </p>
342
  </div>
343
  <div id="generatedError" style="display:none"></div>
344
  </div>
 
552
  btn.innerHTML = '<span>&#9889;</span> Generate My CV';
553
  }
554
 
 
 
 
 
 
 
 
 
 
 
555
  async function downloadFile(format) {
556
  if (!generatedPaths) return;
557
  const key = format === "pdf" ? "cv_pdf" : "cv";