#!/usr/bin/env python3 """ Flashcard Learning System - Flask API + Web UI Features: - Create/manage decks and cards (EN/KO) - Spaced repetition scheduling - Study sessions with tracking - User separation (for future SSO) """ from flask import Flask, request, jsonify, render_template_string, session import os import json from datetime import datetime, timedelta app = Flask(__name__) app.secret_key = os.environ.get('FLASH_CARD_SECRET', 'dev-secret-change-in-prod') DATA_DIR = os.environ.get('FLASH_CARD_DATA_DIR', '/home/joungmin/flashcards') def load_data(): os.makedirs(DATA_DIR, exist_ok=True) users_file = os.path.join(DATA_DIR, 'users.json') decks_file = os.path.join(DATA_DIR, 'decks.json') cards_file = os.path.join(DATA_DIR, 'cards.json') sessions_file = os.path.join(DATA_DIR, 'sessions.json') def read_json(f, default): if os.path.exists(f): with open(f, 'r') as file: return json.load(file) return default return { 'users': read_json(users_file, {}), 'decks': read_json(decks_file, {}), 'cards': read_json(cards_file, {}), 'sessions': read_json(sessions_file, {}) } def save_data(data): users_file = os.path.join(DATA_DIR, 'users.json') decks_file = os.path.join(DATA_DIR, 'decks.json') cards_file = os.path.join(DATA_DIR, 'cards.json') sessions_file = os.path.join(DATA_DIR, 'sessions.json') with open(users_file, 'w') as f: json.dump(data['users'], f, indent=2, default=str) with open(decks_file, 'w') as f: json.dump(data['decks'], f, indent=2, default=str) with open(cards_file, 'w') as f: json.dump(data['cards'], f, indent=2, default=str) with open(sessions_file, 'w') as f: json.dump(data['sessions'], f, indent=2, default=str) def get_next_id(data, key): if not data[key]: return 1 return max(int(k) for k in data[key].keys()) + 1 def get_current_user(): return session.get('user_id', 1) HTML_DECKS = """ Flashcards - EN/KO Learning

My Decks

+ New Deck {% if decks %}
{% for deck_id, deck in decks.items() %}
{{ deck.name }}

{{ deck.description or 'No description' }}

{{ deck.language_pair }} • {{ deck.card_count }} cards

{% endfor %}
{% else %}

No decks yet. Create your first deck!

{% endif %}
""" HTML_DECK_EDIT = """ {% if deck_id %}Edit{% else %}New{% endif %} Deck

{% if deck_id %}Edit Deck{% else %}New Deck{% endif %}

{% if deck_id %}

Cards ({{ cards|length }})

+ Add Card {% for card_id, card in cards.items() %}
{{ card.question_en }}
{{ card.answer_en or card.question_ko }}
Edit | Delete
{% endfor %} {% endif %}
""" HTML_CARD_EDIT = """ {% if card_id %}Edit{% else %}New{% endif %} Card

{% if card_id %}Edit Card{% else %}New Card{% endif %}

English
Korean
""" HTML_STUDY = """ Study: {{ deck.name }}

Study: {{ deck.name }}

Total: {{ cards|length }}
Due: {{ due_cards|length }}
Correct: {{ correct }}
Incorrect: {{ incorrect }}
{% if due_cards %}

Question

{{ current_card.question_en }}

{{ deck.language_pair }}

{% else %}

No cards due for review!

Back to Decks {% endif %}
""" @app.route('/') def index(): data = load_data() user_id = get_current_user() user_decks = {k: v for k, v in data['decks'].items() if str(v.get('user_id', 1)) == str(user_id)} for deck_id in user_decks: cards = [c for c in data['cards'].values() if str(c.get('deck_id')) == str(deck_id)] user_decks[deck_id]['card_count'] = len(cards) return render_template_string(HTML_DECKS, decks=user_decks) @app.route('/deck/new', methods=['GET', 'POST']) def new_deck(): data = load_data() if request.method == 'POST': deck_id = get_next_id(data, 'decks') user_id = get_current_user() data['decks'][str(deck_id)] = { 'deck_id': deck_id, 'user_id': user_id, 'name': request.form['name'], 'description': request.form.get('description', ''), 'language_pair': request.form.get('language_pair', 'EN-KO'), 'created_at': datetime.now().isoformat(), 'is_active': 'Y' } save_data(data) return redirect(f'/deck/{deck_id}') return render_template_string(HTML_DECK_EDIT, deck=None, cards={}, deck_id=None) @app.route('/deck/') def view_deck(deck_id): data = load_data() deck = data['decks'].get(str(deck_id)) if not deck: return "Deck not found", 404 cards = {k: v for k, v in data['cards'].items() if str(v.get('deck_id')) == str(deck_id)} return render_template_string(HTML_DECK_EDIT, deck=deck, cards=cards, deck_id=deck_id) @app.route('/deck//edit', methods=['POST']) def edit_deck(deck_id): data = load_data() if str(deck_id) in data['decks']: data['decks'][str(deck_id)].update({ 'name': request.form['name'], 'description': request.form.get('description', ''), 'language_pair': request.form.get('language_pair', 'EN-KO'), 'updated_at': datetime.now().isoformat() }) save_data(data) return redirect(f'/deck/{deck_id}') @app.route('/card/new/', methods=['GET', 'POST']) def new_card(deck_id): data = load_data() if request.method == 'POST': card_id = get_next_id(data, 'cards') data['cards'][str(card_id)] = { 'card_id': card_id, 'deck_id': int(deck_id), 'question_en': request.form['question_en'], 'answer_en': request.form.get('answer_en', ''), 'question_ko': request.form['question_ko'], 'answer_ko': request.form.get('answer_ko', ''), 'example_sentence': request.form.get('example_sentence', ''), 'difficulty_level': int(request.form.get('difficulty_level', 1)), 'created_at': datetime.now().isoformat(), 'is_active': 'Y' } save_data(data) return redirect(f'/deck/{deck_id}') return render_template_string(HTML_CARD_EDIT, card=None, card_id=None) @app.route('/card//edit', methods=['GET', 'POST']) def edit_card(card_id): data = load_data() card = data['cards'].get(str(card_id)) if not card: return "Card not found", 404 if request.method == 'POST': card.update({ 'question_en': request.form['question_en'], 'answer_en': request.form.get('answer_en', ''), 'question_ko': request.form['question_ko'], 'answer_ko': request.form.get('answer_ko', ''), 'example_sentence': request.form.get('example_sentence', ''), 'difficulty_level': int(request.form.get('difficulty_level', 1)), 'updated_at': datetime.now().isoformat() }) save_data(data) return redirect(f"/deck/{card['deck_id']}") return render_template_string(HTML_CARD_EDIT, card=card, card_id=card_id) @app.route('/card//delete') def delete_card(card_id): data = load_data() if str(card_id) in data['cards']: deck_id = data['cards'][str(card_id)]['deck_id'] del data['cards'][str(card_id)] save_data(data) return redirect(f'/deck/{deck_id}') return "Card not found", 404 @app.route('/study/') def study_deck(deck_id): data = load_data() deck = data['decks'].get(str(deck_id)) if not deck: return "Deck not found", 404 now = datetime.now().isoformat() cards = [c for c in data['cards'].values() if str(c.get('deck_id')) == str(deck_id) and c.get('is_active', 'Y') == 'Y'] cards.sort(key=lambda x: (x.get('next_review_at', now), x.get('difficulty_level', 1))) session_id = get_next_id(data, 'sessions') data['sessions'][str(session_id)] = { 'session_id': session_id, 'user_id': get_current_user(), 'deck_id': int(deck_id), 'started_at': now, 'cards_reviewed': 0, 'cards_correct': 0, 'cards_incorrect': 0 } save_data(data) return render_template_string( HTML_STUDY, deck=deck, cards=cards, due_cards=cards[:20], current_card=cards[0] if cards else None, session_id=session_id, correct=0, incorrect=0 ) # API Routes @app.route('/api/decks', methods=['GET']) def api_decks(): data = load_data() user_id = get_current_user() user_decks = [v for k, v in data['decks'].items() if str(v.get('user_id', 1)) == str(user_id)] return jsonify({'decks': user_decks}) @app.route('/api/decks', methods=['POST']) def api_create_deck(): data = load_data() deck_id = get_next_id(data, 'decks') user_id = get_current_user() req = request.json data['decks'][str(deck_id)] = { 'deck_id': deck_id, 'user_id': user_id, 'name': req.get('name'), 'description': req.get('description', ''), 'language_pair': req.get('language_pair', 'EN-KO'), 'created_at': datetime.now().isoformat(), 'is_active': 'Y' } save_data(data) return jsonify({'success': True, 'deck_id': deck_id}) @app.route('/api/cards', methods=['GET']) def api_cards(): data = load_data() deck_id = request.args.get('deck_id') cards = data['cards'].values() if deck_id: cards = [c for c in cards if str(c.get('deck_id')) == str(deck_id)] return jsonify({'cards': list(cards)}) @app.route('/api/cards', methods=['POST']) def api_create_card(): data = load_data() card_id = get_next_id(data, 'cards') req = request.json data['cards'][str(card_id)] = { 'card_id': card_id, 'deck_id': req.get('deck_id'), 'question_en': req.get('question_en'), 'answer_en': req.get('answer_en', ''), 'question_ko': req.get('question_ko'), 'answer_ko': req.get('answer_ko', ''), 'example_sentence': req.get('example_sentence', ''), 'difficulty_level': req.get('difficulty_level', 1), 'created_at': datetime.now().isoformat(), 'is_active': 'Y' } save_data(data) return jsonify({'success': True, 'card_id': card_id}) @app.route('/api/cards/', methods=['DELETE']) def api_delete_card(card_id): data = load_data() if str(card_id) in data['cards']: del data['cards'][str(card_id)] save_data(data) return jsonify({'success': True}) return jsonify({'error': 'Card not found'}), 404 @app.route('/api/session//result', methods=['POST']) def api_session_result(session_id): data = load_data() if str(session_id) in data['sessions']: req = request.json session_data = data['sessions'][str(session_id)] session_data['cards_reviewed'] = session_data.get('cards_reviewed', 0) + 1 if req.get('was_correct'): session_data['cards_correct'] = session_data.get('cards_correct', 0) + 1 else: session_data['cards_incorrect'] = session_data.get('cards_incorrect', 0) + 1 card_id = req.get('card_id') if str(card_id) in data['cards']: card = data['cards'][str(card_id)] card['times_reviewed'] = card.get('times_reviewed', 0) + 1 if req.get('was_correct'): card['times_correct'] = card.get('times_correct', 0) + 1 card['last_reviewed_at'] = datetime.now().isoformat() days = card.get('times_correct', 1) if req.get('was_correct') else 0 card['next_review_at'] = (datetime.now() + timedelta(days=days*2)).isoformat() save_data(data) return jsonify({'success': True}) return jsonify({'error': 'Session not found'}), 404 if __name__ == '__main__': port = int(os.environ.get('PORT', 8082)) app.run(host='0.0.0.0', port=port, debug=False)