app.py
from flask import Flask, request, jsonify, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from datetime import date, datetime, timedelta
import os
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler
import logging
import requests

app = Flask(name)
basedir = os.path.abspath(os.path.dirname(file))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'app.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
migrate = Migrate(app, db)

#####################################################################* ARCHIVING TASKS #####################################################################
def archive_tasks():
with app.app_context():
# Check last archive date
last_archived_date = ArchiveInfo.get_last_archive()
today = date.today()


    # If the last archive was not today, archive and update the record
    if last_archived_date is None or last_archived_date != today:
        # Perform the archive
        tasks = Task.query.filter(Task.status == 'finished')
        for task in tasks:
            task.status = 'archived'
            if task.completion_date == None:
                task.completion_date = date.today() - timedelta(days=1)
        db.session.commit()

        # Update the archive info in the database
        ArchiveInfo.update_last_archive(today)
        logging.info(f"Archived tasks for {today}")
    
Configure logging
logging.basicConfig(level=logging.INFO)
logging.getLogger('apscheduler').setLevel(logging.DEBUG)

Initialize the scheduler
executors = {
'default': ThreadPoolExecutor(20),
'processpool': ProcessPoolExecutor(5)
}

job_defaults = {
'coalesce': True,
'max_instances': 1
}

scheduler = BackgroundScheduler(executors=executors, job_defaults=job_defaults)
scheduler.add_job(archive_tasks, 'cron', hour=0, minute=1) # Runs at 12:01 AM

Periodic job to check every 15 minutes if archiving was missed
scheduler.add_job(archive_tasks, 'interval', minutes=15)
scheduler.start()

#####################################################################* DB CLASSES #####################################################################
class ArchiveInfo(db.Model):
id = db.Column(db.Integer, primary_key=True)
last_archive = db.Column(db.Date, nullable=False)


@staticmethod
def get_last_archive():
    archive_info = ArchiveInfo.query.first()
    if archive_info:
        return archive_info.last_archive
    return None

@staticmethod
def update_last_archive(new_archive_date):
    archive_info = ArchiveInfo.query.first()
    if not archive_info:
        archive_info = ArchiveInfo(last_archive=new_archive_date)
        db.session.add(archive_info)
    else:
        archive_info.last_archive = new_archive_date
    db.session.commit()
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
status = db.Column(db.String(20), nullable=False)
completion_date = db.Column(db.Date)
column = db.Column(db.String(20), default='To Do')
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'status': self.status,
'completion_date': self.completion_date.isoformat() if self.completion_date else None,
'column': self.column
}

class Habit(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
#days that the habit is available
days = db.Column(db.String(100)) # Store as comma-separated string
#list of completed dates
dates = db.relationship('HabitDate', backref='habit', lazy=True)

class HabitDate(db.Model):
id = db.Column(db.Integer, primary_key=True)
habit_id = db.Column(db.Integer, db.ForeignKey('habit.id'), nullable=False)
date = db.Column(db.Date, nullable=False)

class JournalEntry(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
date = db.Column(db.Date, nullable=False)
ai_comment = db.Column(db.Text)

#####################################################################* BASE ROUTES #####################################################################
@app.route('/')
def index():
return render_template('index.html')

@app.route('/yearly-grid')
def yearly_grid():
return render_template('yearly_grid.html')

#####################################################################* API routes #####################################################################
############################################## TASKS ##############################################
@app.route('/api/tasks', methods=['GET', 'POST'])
def handle_tasks():
'''
POST request: Creates a new task in the database and returns its details.
GET request: Retrieves and returns all tasks from the database as a list in JSON format.
'''
if request.method == 'POST':
data = request.json
new_task = Task(
title=data['title'],
status=data['status'],
column=data.get('column', 'To Do')
)
db.session.add(new_task)
db.session.commit()
return jsonify(id=new_task.id, title=new_task.title, status=new_task.status, column=new_task.column)
else:
tasks = Task.query.all()
return jsonify([{
'id': task.id,
'title': task.title,
'status': task.status,
'completion_date': str(task.completion_date) if task.completion_date else None,
'column': task.column
} for task in tasks])

@app.route('/api/tasks/int:task_id', methods=['PUT', 'DELETE'])
def update_task(task_id):
task = Task.query.get_or_404(task_id)
if request.method == 'PUT':
data = request.json
if 'title' in data and not data['title'].strip():
# If the title is empty, delete the task
db.session.delete(task)
db.session.commit()
return jsonify({'success': True, 'deleted': True})
else:
task.title = data.get('title', task.title)
task.column = data.get('column', task.column)
if 'status' in data:
task.status = data['status']
if task.status == 'finished':
# Accept completion_date from client or default to date.today()
completion_date_str = data.get('completion_date')
if completion_date_str:
task.completion_date = datetime.strptime(completion_date_str, '%Y-%m-%d').date()
else:
task.completion_date = date.today()
else:
task.completion_date = None
db.session.commit()
return jsonify({'success': True, 'deleted': False})
elif request.method == 'DELETE':
db.session.delete(task)
db.session.commit()
return jsonify({'success': True})

@app.route('/api/tasks/archive', methods=["POST"])
def archive_completed_tasks():
tasks = Task.query.filter(Task.status == 'finished')
for task in tasks:
task.status = 'archived'
if task.completion_date == None:
task.completion_date = date.today() - timedelta(days=1)
db.session.commit()
return jsonify({'success': True})

############################################## HABITS ##############################################

@app.route('/api/habits', methods=['GET', 'POST'])
def handle_habits():
'''
POST request: Creates a new habit in db and returns related JSON
GET request: Returns a JSON of all habits and related fields
'''
if request.method == 'POST':
data = request.json
new_habit = Habit(name=data['name'], days=','.join(map(str, data.get('days', []))))
db.session.add(new_habit)
db.session.commit()
return jsonify(id=new_habit.id, name=new_habit.name, days=new_habit.days.split(','))
else:
habits_data = []
for habit in Habit.query.all():
habit_data = {
'id': habit.id,
'name': habit.name,
'days': habit.days.split(',') if habit.days else [],
'dates': [habit_date.date.isoformat() for habit_date in habit.dates]
}
habits_data.append(habit_data)


    return jsonify(habits_data)
@app.route('/api/habits/int:habit_id', methods=['PUT', 'DELETE'])
def update_habit(habit_id):
habit = Habit.query.get_or_404(habit_id)
if request.method == 'PUT':
data = request.json
if 'name' in data:
habit.name = data['name']
if 'days' in data:
habit.days = ','.join(map(str, data['days']))
if 'toggle_date' in data:
toggle_date = date.fromisoformat(data['toggle_date'])
existing_date = HabitDate.query.filter_by(habit_id=habit.id, date=toggle_date).first()
if existing_date:
db.session.delete(existing_date)
else:
new_date = HabitDate(habit_id=habit.id, date=toggle_date)
db.session.add(new_date)
db.session.commit()
return jsonify({'success': True})
elif request.method == 'DELETE':
db.session.delete(habit)
db.session.commit()
return jsonify({'success': True})

############################################## JOURNAL ##############################################
@app.route('/api/journal', methods=['GET', 'POST'])
def handle_journal():
if request.method == 'POST':
data = request.json
new_entry = JournalEntry(
title=data['title'],
content=data['content'],
date=date.today(),
ai_comment=data.get('ai_comment', '')
)
db.session.add(new_entry)
db.session.commit()
return jsonify(id=new_entry.id, title=new_entry.title, content=new_entry.content, date=str(new_entry.date), ai_comment=new_entry.ai_comment)
else:
entries = JournalEntry.query.all()
return jsonify([{
'id': entry.id,
'title': entry.title,
'content': entry.content,
'date': str(entry.date),
'ai_comment': entry.ai_comment
} for entry in entries])

@app.route('/api/journal/int:entry_id', methods=['PUT', 'DELETE'])
def update_journal(entry_id):
entry = JournalEntry.query.get_or_404(entry_id)
if request.method == 'PUT':
data = request.json
entry.title = data.get('title', entry.title)
entry.content = data.get('content', entry.content)
entry.ai_comment = data.get('ai_comment', entry.ai_comment)
db.session.commit()
return jsonify({'success': True})
elif request.method == 'DELETE':
db.session.delete(entry)
db.session.commit()
return jsonify({'success': True})

############################################## HISTORY ##############################################

@app.route('/api/history', methods=['GET'])
def get_yearly_data():
year_param = request.args.get('year', default=date.today().year, type=int)


# Define the start and end dates for the specified year
start_date = date(year_param, 1, 1)
end_date = date(year_param, 12, 31)

# Handle future years gracefully
today = date.today()
if end_date > today:
    end_date = today  # Limit end_date to today if in future
    
# Generate the date range for the specified year
total_days = (end_date - start_date).days + 1
yearly_data = {
    (start_date + timedelta(days=i)).isoformat(): {
        "availableHabits": 0,
        "completedHabits": 0,
        "completedTasks": [],
        "completedHabitsList": [],
        "completionRate": 0
    } for i in range(total_days)
}

habits = Habit.query.all()
tasks = Task.query.filter(
    Task.completion_date >= start_date,
    Task.completion_date <= end_date
).all()
journal_entries = JournalEntry.query.all()

# Build a mapping of dates to available habits
for habit in habits:
    habit_days = habit.days.split(',') if habit.days else []
    for date_str in yearly_data.keys():
        date_obj = date.fromisoformat(date_str)
        # Adjusting weekday calculation since strftime('%w') returns 0 for Sunday
        weekday = str((date_obj.weekday() + 1) % 7)
        if weekday in habit_days:
            yearly_data[date_str]["availableHabits"] += 1
            
# Mark completed habits
for habit in habits:
    for habit_date in habit.dates:
        if start_date <= habit_date.date <= end_date:
            date_str = habit_date.date.isoformat()
            if date_str in yearly_data:
                yearly_data[date_str]["completedHabits"] += 1
                yearly_data[date_str]["completedHabitsList"].append(habit.name)

 # Record completed tasks
for task in tasks:
    date_str = task.completion_date.isoformat()
    if date_str in yearly_data:
        yearly_data[date_str]["completedTasks"].append(task.title)
        
# Calculate completion rate
for date_str, data in yearly_data.items():
    available = data["availableHabits"]
    completed = data["completedHabits"]
    data["completionRate"] = completed / available if available > 0 else None

for entry in journal_entries:
    date_str = str(entry.date)
    if date_str not in yearly_data:
        yearly_data[date_str] = {"completedHabits": [], "completedTasks": [], "journalEntries": []}
    yearly_data[date_str]["journalEntries"].append(entry.title)

for date_str, data in yearly_data.items():
    available = data["availableHabits"]
    completed = data["completedHabits"]
    if available > 0:
        data["completionRate"] = completed / available
    else:
        data["completionRate"] = None

# Return the data with date strings as keys
return jsonify(yearly_data)
if name == 'main':
with app.app_context():
db.create_all()
app.run(debug=True)

script.js:
const columnOrder = ['to-do-week', 'to-do-today', 'in-progress', 'finished'];

function createTaskElement(task) {
const taskElement = document.createElement('div');
taskElement.className = 'task-card';
taskElement.dataset.taskId = task.id;

taskElement.innerHTML = <div class="task-title" contenteditable="true" onblur="updateTaskTitle(${task.id}, this)">${task.title}</div> <div class="task-buttons"> ${getButtonsForTask(task)} </div> ;
return taskElement;
}

function getLocalDateString() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2,'0');
const day = String(today.getDate()).padStart(2, '0');
return ${year}-${month}-${day};
}

function initializeSortable() {
columnOrder.forEach(columnId => {
const columnElement = document.getElementById(columnId);


  Sortable.create(columnElement, {
    group: 'columns', // Enable moving items between columns
    animation: 150,
    handle: '.task-card',
    draggable: '.task-card',
    onEnd: function (evt) {
      // Update task order in the client-side state here
      // Optionally, save to localStorage or send to the server when appropriate
    },
  });
});
}

function loadTasks() {
axios.get('/api/tasks')
.then(response => {
const tasks = response.data;


    // Clear columns
    columnOrder.forEach(status => {
      const column = document.getElementById(status);
      column.innerHTML = `
        <div class="column-header">
          <span class="column-title">${column.id.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}</span>
          <span>...</span>
        </div>
      `;
    });

    tasks.forEach(task => {
      const columnElement = document.getElementById(task.status);
      if (!columnElement) return;

      const taskElement = createTaskElement(task);
      columnElement.appendChild(taskElement);
    });

    // Add "Add a card" buttons to columns
    columnOrder.forEach(status => {
      const column = document.getElementById(status);
      const addCard = document.createElement('div');
      addCard.className = 'add-card';
      addCard.textContent = '+ Add a card';
      addCard.onclick = () => addTask(status);
      column.appendChild(addCard);
    });

    // Initialize Sortable
    initializeSortable();
  })
  .catch(error => {
    console.error('Error loading tasks:', error);
  });
}

function getButtonsForTask(task) {
switch (task.status) {
case 'to-do-week':
return <button class="task-button" onclick="moveTask(${task.id}, 'to-do-today')">→ Today</button>;
case 'to-do-today':
return <button class="task-button" onclick="moveTask(${task.id}, 'in-progress')">▶ IP</button> <button class="task-button" onclick="moveTask(${task.id}, 'finished')">✓</button> ;
case 'in-progress':
return <button class="task-button" onclick="moveTask(${task.id}, 'to-do-week')">↺ Week</button> <button class="task-button" onclick="moveTask(${task.id}, 'to-do-today')">↺ Today</button> <button class="task-button" onclick="moveTask(${task.id}, 'finished')">✓</button> ;
case 'finished':
return <button class="task-button" onclick="moveTask(${task.id}, 'to-do-week')">↺ Week</button> <button class="task-button" onclick="moveTask(${task.id}, 'to-do-today')">↺ Today</button> ;
default:
return '';
}
}

function addTask(status) {
const column = document.getElementById(status);


// Hide the "Add a card" button
const addCardButton = column.querySelector('.add-card');
addCardButton.style.display = 'none';

// Create a new task card div
const newCard = document.createElement('div');
newCard.classList.add('task-card');
newCard.contentEditable = 'true';  // Make the new card editable

// Add event listener to handle saving when 'Enter' is pressed
newCard.addEventListener('keydown', function(event) {
    if (event.key === 'Enter') {
        event.preventDefault();  // Prevent new lines when pressing Enter
        const taskTitle = newCard.textContent.trim();
        if (taskTitle) {
            // Send task title and status to the backend
            axios.post('/api/tasks', { title: taskTitle, status })
                .then(() => {
                    loadTasks();  // Reload tasks after adding new one
                    addCardButton.style.display = 'block';  // Show "Add a card" button again
                });
        } else {
            // Remove the card if no text was entered
            newCard.remove();
            addCardButton.style.display = 'block';  // Show "Add a card" button again
        }
    }
});

// Add event listener for "Escape" key to discard the card
newCard.addEventListener('keydown', function(event) {
    if (event.key === 'Escape') {
        newCard.remove();  // Discard the new card
        addCardButton.style.display = 'block';  // Show "Add a card" button again
    }
});

// Append the new card to the column
column.appendChild(newCard);
// Focus on the new card to allow typing immediately
newCard.focus();
}

function moveTask(taskId, newStatus) {
let data = { status: newStatus };
if (newStatus === 'finished') {
data['completion_date'] = getLocalDateString();
} else {
data['completion_date'] = null; // Clear the completion_date if not finished
}
axios.put(/api/tasks/${taskId}, data)
.then(() => loadTasks())
.catch(error => {
console.error('Error moving task:', error);
});
}

function updateTaskTitle(taskId, element) {
const newTitle = element.textContent.trim();
axios.put(/api/tasks/${taskId}, { title: newTitle })
.then((response) => {
if (response.data.deleted) {
// If the task was deleted (empty title), remove it from the UI
element.closest('.task-card').remove();
} else {
// If the task was updated, reload tasks to reflect any changes
loadTasks();
}
})
.catch((error) => {
console.error('Error updating task:', error);
loadTasks(); // Reload tasks to ensure UI is in sync with server
});
}

function loadHabits() {
axios.get('/api/habits')
.then(response => {
const habits = response.data;
const habitsContainer = document.getElementById('habits');
habitsContainer.innerHTML = '';
const todayStr = getLocalDateString(); // 'YYYY-MM-DD'
const todayWeekday = new Date().getDay().toString(); // '0' (Sunday) to '6' (Saturday)


        habits.forEach(habit => {
            const isAvailableToday = habit.days.includes(todayWeekday);
            if (!isAvailableToday) {
                // Skip habits not scheduled for today
                return;
            }

            const habitElement = document.createElement('div');
            const habitDates = habit.dates || [];
            const isCompletedToday = habitDates.includes(todayStr);

            habitElement.className = `habit ${isCompletedToday ? 'habit-complete' : 'habit-incomplete'}`;
            habitElement.innerHTML = `<h3>${habit.name}</h3>`;
            habitElement.onclick = () => toggleHabit(habitElement, habit.id);
            habitsContainer.appendChild(habitElement);
        });
    })
    .catch(error => {
        console.error('Error loading habits:', error);
    });
}

function addHabit() {
const name = document.getElementById('new-habit').value;
const dayCheckboxes = document.querySelectorAll('#day-select input[type="checkbox"]:checked');
const days = Array.from(dayCheckboxes).map(cb => cb.value);
// console.log(days)
if (name) {
axios.post('/api/habits', { name, days })
.then(() => {
document.getElementById('new-habit').value = '';
// Clear checkboxes
document.querySelectorAll('#day-select input[type="checkbox"]').forEach(cb => cb.checked = false);
loadHabits();
});
}
}

function toggleHabit(element, habitId) {
const today = getLocalDateString();


// Toggle the visual state immediately
const isCurrentlyCompleted = element.classList.contains('habit-complete');

if (isCurrentlyCompleted) {
    // Remove 'habit-complete', add 'habit-incomplete'
    element.classList.remove('habit-complete');
    element.classList.add('habit-incomplete');
} else {
    // Add 'habit-complete', remove 'habit-incomplete'
    element.classList.add('habit-complete');
    element.classList.remove('habit-incomplete');
}

// Send the request to the backend
axios.put(`/api/habits/${habitId}`, { toggle_date: today })
    .then(response => {
        if (!response.data.success) {
            console.error(`Failed to toggle habit ${habitId}`);
            // Optionally revert the visual change if the backend fails
            toggleVisualState(element);
        }
    })
    .catch(error => {
        console.error('Error toggling habit:', error.response ? error.response.data : error.message);
        // Optionally revert the visual change if the backend fails
        toggleVisualState(element);
    });
}

// Helper function to revert visual state if needed
function toggleVisualState(element) {
const isCompleted = element.classList.contains('habit-complete');
if (isCompleted) {
element.classList.remove('habit-complete');
element.classList.add('habit-incomplete');
} else {
element.classList.add('habit-complete');
element.classList.remove('habit-incomplete');
}
}

document.addEventListener('DOMContentLoaded', function () {
// Load tasks and habits on page load
loadTasks();
loadHabits();
});

yearly_grid.js:
let currentYear = new Date().getFullYear();
let displayMode = 'combined';
const dayWidth = 30; // Updated width of each day square
const dayHeight = 30; // Updated height of each day square
const gutter = 2; // Space between squares

function getLocalDateString(dateObj) {
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2,'0');
const day = String(dateObj.getDate()).padStart(2, '0');
return ${year}-${month}-${day};
}

document.addEventListener('DOMContentLoaded', function() {
// Set the current year display
document.getElementById('current-year').textContent = currentYear;


// Add event listeners to navigation buttons
document.getElementById('prev-year').addEventListener('click', () => {
    currentYear--;
    updateYearlyGrid();
});

document.getElementById('next-year').addEventListener('click', () => {
    currentYear++;
    updateYearlyGrid();
});

// Add event listener to the display mode toggle
const displayModeSelect = document.getElementById('display-mode');
displayModeSelect.value = displayMode; // Set default value
displayModeSelect.addEventListener('change', () => {
    displayMode = displayModeSelect.value;
    updateYearlyGrid();
});

// Fetch the initial data
updateYearlyGrid();
});

function updateYearlyGrid() {
document.getElementById('current-year').textContent = currentYear;


// Disable the next button if currentYear is the same as the current year
const nextYearButton = document.getElementById('next-year');
if (currentYear >= new Date().getFullYear()) {
    nextYearButton.disabled = true;
} else {
    nextYearButton.disabled = false;
}

fetchHistoryData(currentYear);
}

function fetchHistoryData(year) {
axios.get(/api/history?year=${year})
.then(response => {
const data = response.data;
renderHeatmap(data, year, displayMode); // Pass displayMode
})
.catch(error => {
console.error('Error fetching history data:', error);
alert('No data available for this year.');
});
}

function renderHeatmap(data, year, displayMode) {
const container = document.getElementById('heatmap-container');
container.innerHTML = '';
const currentDate = new Date();


const dataMap = new Map(Object.entries(data));

// Create the main grid container
const grid = document.createElement('div');
grid.classList.add('heatmap-grid');

// Iterate over each month
for (let month = 0; month < 12; month++) {
    if (currentYear >= currentDate.getFullYear() && month > currentDate.getMonth()) {
        break;
    }

    const monthColumn = document.createElement('div');
    monthColumn.classList.add('month-column');

    const monthDate = new Date(year, month, 1);
    const monthName = monthDate.toLocaleString('default', { month: 'short' });

    // Create the month label
    const monthLabel = document.createElement('div');
    monthLabel.classList.add('month-label');
    monthLabel.textContent = monthName;
    monthColumn.appendChild(monthLabel);

    // Optionally, add weekday labels
    const weekdaysRow = document.createElement('div');
    weekdaysRow.classList.add('weekdays-row');
    const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    weekdays.forEach(dayName => {
        const weekdayLabel = document.createElement('div');
        weekdayLabel.classList.add('weekday-label');
        weekdayLabel.textContent = dayName;
        weekdaysRow.appendChild(weekdayLabel);
    });
    monthColumn.appendChild(weekdaysRow);

    // Get the weekday of the first day of the month
    const firstWeekday = monthDate.getDay();

    // Create a grid for days
    const daysGrid = document.createElement('div');
    daysGrid.classList.add('days-grid');

    // Add empty cells for days before the first of the month
    for (let i = 0; i < firstWeekday; i++) {
        const emptyCell = document.createElement('div');
        emptyCell.classList.add('day-cell', 'empty-cell');
        daysGrid.appendChild(emptyCell);
    }

    // Get total days in the month
    const daysInMonth = new Date(year, month + 1, 0).getDate();

    // Create day cells
    for (let day = 1; day <= daysInMonth; day++) {

        if (currentYear >= currentDate.getFullYear() && (month == currentDate.getMonth() && day > currentDate.getDate() || (month > currentDate.getMonth()))) {
            break;
        }

        const date = new Date(year, month, day);
        const dateStr = getLocalDateString(date);

        const dayCell = document.createElement('div');
        dayCell.classList.add('day-cell');
        dayCell.dataset.date = dateStr;

        const dayData = dataMap.get(dateStr);
        let shade = '#444';

        if (dayData) {
            if (displayMode === 'combined') {
                shade = getCombinedShade(dayData);
            } else if (displayMode === 'habits') {
                shade = getHabitShade(dayData);
            } else if (displayMode === 'tasks') {
                shade = getTaskShade(dayData);
            }
        } else {
            shade = '#444'; // Default shade when no data
        }
        dayCell.style.backgroundColor = shade;

        // Make the cell clickable
        dayCell.classList.add('clickable');
        dayCell.addEventListener('click', () => {
            if (dayData) {
                showDetailsModal(dateStr, dayData);
            } else {
                showDetailsModal(dateStr, {
                    availableHabits: 0,
                    completedHabits: 0,
                    completedTasks: [],
                    completedHabitsList: [],
                    completionRate: null
                });
            }
        });

        daysGrid.appendChild(dayCell);
    }

    monthColumn.appendChild(daysGrid);
    grid.appendChild(monthColumn);
}

// Append grid to container
container.appendChild(grid);

// Remove calls to renderMonthLabels and renderWeekdayLabels
// renderMonthLabels(container, startDate, endDate, totalWeeks);
// renderWeekdayLabels(container);
}

////////////////////* https://yeun.github.io/open-color/ for colors *////////////////////////
function getCombinedShade(dayData) {
const shades = [
'#3b3b3b', // No tasks completed
'#106030', // 1 task
'#2b8a3e', // 2 tasks
'#2f9e44', // 3 tasks
'#37b24d', // 4 tasks
'#40c057', // 5 tasks
'#51cf66', // 6 tasks
'#69db7c', // 7 tasks
'#8ce99a', // 8 tasks
'#b2f2bb', // 9 tasks
'#d3f9d8', // 10 tasks
'#ebfbee', // 11+ tasks
];


if (!dayData || dayData.completedTasks.length + dayData.completedHabits === 0) {
    return shades[0];
} else {
    const combinedCompleted = dayData.completedTasks.length + dayData.completedHabits;
    if (combinedCompleted === 1) {
        return shades[1];
    } else if (combinedCompleted === 2) {
        return shades[2];
    } else if (combinedCompleted === 3) {
        return shades[3];
    } else if (combinedCompleted === 4) {
        return shades[4];
    } else if (combinedCompleted === 5) {
        return shades[5];
    } else if (combinedCompleted === 6) {
        return shades[6];
    } else if (combinedCompleted === 7) {
        return shades[7];
    } else if (combinedCompleted === 8) {
        return shades[8];
    } else if (combinedCompleted === 9) {
        return shades[9];
    } else if (combinedCompleted === 10) {
        return shades[10];
    } else {
        return shades[11];
    }
}
}
function getHabitShade(dayData) {
const shades = [
'#2c2c2c', // No habits available (darkest)
'#3b3b3b', // 0% completion
'#1864ab', // 10% completion
'#1971c2', // 25% completion
'#1c7ed6', // 40% completion
'#228be6', // 65% completion
'#339af0', // 75% completion
'#4dabf7', // 85% completion
'#74c0fc' // 100% completion
];


if (!dayData || dayData.completionRate === null) {
    return shades[0]; // No habits available
} else {
    const completionRate = dayData.completionRate;
    if (completionRate === 0) {
        return shades[1];
    } else if (completionRate <= 0.1) {
        return shades[2];
    } else if (completionRate <= 0.25) {
        return shades[3];
    } else if (completionRate <= 0.40) {
        return shades[4];
    } else if (completionRate <= 0.65) {
        return shades[5];
    } else if (completionRate <= 0.75) {
        return shades[6];
    } else if (completionRate <= 0.85) {
        return shades[7];
    } else {
        return shades[8];
    }
}
}
function getTaskShade(dayData) {
const shades = [
'#3b3b3b', // No tasks completed
'#3E0101', // 1 task
'#8B0000', // 2 tasks
'#C92A2A', // 3 tasks
'#E03030', // 4 tasks
'#F03E3E', // 5 tasks
'#FA5353', // 6 tasks
'#FE6A6B', // 7 tasks
'#FF8787', // 8 tasks
'#FFC9C9', // 9 tasks
'#FFE2E2', // 10 tasks
'#FFF5F5', // 11+ tasks
];


if (!dayData || dayData.completedTasks.length === 0) {
    return shades[0];
} else {
    const tasksCompleted = dayData.completedTasks.length;
    if (tasksCompleted === 1) {
        return shades[1];
    } else if (tasksCompleted === 2) {
        return shades[2];
    } else if (tasksCompleted === 3) {
        return shades[3];
    } else if (tasksCompleted === 4) {
        return shades[4];
    } else if (tasksCompleted === 5) {
        return shades[5];
    } else if (tasksCompleted === 6) {
        return shades[6];
    } else if (tasksCompleted === 7) {
        return shades[7];
    } else if (tasksCompleted === 8) {
        return shades[8];
    } else if (tasksCompleted === 9) {
        return shades[9];
    } else if (tasksCompleted === 10) {
        return shades[10];
    } else {
        return shades[11];
    }
}
}

function renderMonthLabels(container, startDate, endDate, totalWeeks) {
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];


const monthsRow = document.createElement('div');
monthsRow.classList.add('months-row');

let currentMonth = -1;
let weeksInMonth = 0;
let weekIndex = 0;
let dateIterator = new Date(startDate);

while (dateIterator <= endDate) {
    const month = dateIterator.getMonth();
    if (month !== currentMonth) {
        // New month
        if (currentMonth !== -1) {
            // Set the width of the previous month label
            monthsRow.children[monthsRow.children.length - 1].style.width = (weeksInMonth * (dayWidth + gutter)) + 'px';
        }
        // Create new month label
        currentMonth = month;
        weeksInMonth = 1;

        const monthLabel = document.createElement('div');
        monthLabel.classList.add('month-label');
        monthLabel.textContent = monthNames[currentMonth];
        monthsRow.appendChild(monthLabel);
    } else {
        // Same month
        weeksInMonth++;
    }

    // Move to the next week
    dateIterator.setDate(dateIterator.getDate() + 7);
    weekIndex++;
}

 // Set the width for the last month label
monthsRow.children[monthsRow.children.length - 1].style.width = (weeksInMonth * (dayWidth + gutter)) + 'px';

// Append monthsRow to container
container.insertBefore(monthsRow, container.firstChild);
}

function renderWeekdayLabels(container) {
const weekdayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];


const weekdaysColumn = document.createElement('div');
weekdaysColumn.classList.add('weekdays-column');

for (let i = 0; i < 7; i++) {
    const dayLabel = document.createElement('div');
    dayLabel.classList.add('weekday-label');

    // Show labels for Sun, Tue, Thu (or adjust as desired)
    if (i === 0 || i === 2 || i === 4) {
        dayLabel.textContent = weekdayNames[i];
    } else {
        dayLabel.textContent = ''; // Empty label for alignment
    }

    dayLabel.style.height = dayHeight + 'px';
    dayLabel.style.margin = (gutter / 2) + 'px 0'; // Adjust vertical margin

    weekdaysColumn.appendChild(dayLabel);
}

// Append the weekdaysColumn to container
container.appendChild(weekdaysColumn);
}

function showDetailsModal(dateStr, dayData) {
const modal = document.getElementById('details-modal');
const modalDate = document.getElementById('modal-date');
const modalContent = document.getElementById('modal-content');
const closeButton = document.getElementById('modal-close');


modalDate.textContent = dateStr;
modalContent.innerHTML = '';

// Display completed habits
const habitsDiv = document.createElement('div');
habitsDiv.innerHTML = `<h3>Completed Habits (${dayData.completedHabits}/${dayData.availableHabits})</h3>`;
const habitsList = document.createElement('ul');

dayData.completedHabitsList.forEach(habitName => {
    const li = document.createElement('li');
    li.textContent = habitName;
    habitsList.appendChild(li);
});

if (dayData.completedHabitsList.length > 0) {
    habitsDiv.appendChild(habitsList);
} else {
    const noHabits = document.createElement('p');
    noHabits.textContent = 'No habits completed on this day.';
    habitsDiv.appendChild(noHabits);
}

modalContent.appendChild(habitsDiv);

// Display completed tasks
if (dayData.completedTasks.length > 0) {
    const tasksDiv = document.createElement('div');
    tasksDiv.innerHTML = `<h3>Completed Tasks</h3>`;
    const tasksList = document.createElement('ul');

    dayData.completedTasks.forEach(taskTitle => {
        const li = document.createElement('li');
        li.textContent = taskTitle;
        tasksList.appendChild(li);
    });
    tasksDiv.appendChild(tasksList);
    modalContent.appendChild(tasksDiv);
} else {
    const noTasks = document.createElement('p');
    noTasks.textContent = 'No tasks completed on this day.';
    modalContent.appendChild(noTasks);
}

// Show the modal
modal.style.display = 'block';

// Close modal when clicking on the close button
closeButton.onclick = function() {
    modal.style.display = 'none';
};

// Close modal when clicking outside the modal content
window.onclick = function(event) {
    if (event.target === modal) {
        modal.style.display = 'none';
    }
};
}

The tasks that are archived at 12:01 am show up as completed today instead of yesterday on the yearly_grid page, which is a behavior I don't want.


The issue you're encountering stems from a mismatch between server time zones and the way dates are handled in your application. Specifically, the date.today() function in your server code uses the server's local time zone, which might be in UTC or another time zone different from your desired local time zone. This can lead to tasks being assigned incorrect completion_date values, causing them to appear under the wrong date in your yearly grid.

Here's how you can fix this problem:

Set the Time Zone in Your Scheduler and Application: You need to specify the time zone you want your application to use throughout the code. This ensures consistency between date calculations on the server and the user's expectations. You can do this by setting the time zone in your scheduler and using time zone-aware datetime objects in your functions. Install pytz if not already installed:

pip install pytz
Update your imports:

from datetime import datetime
from pytz import timezone
Adjust the Scheduler Time Zone: When initializing your scheduler, specify the desired time zone. For example, if you want to use Pacific Time (America/Los_Angeles):

scheduler = BackgroundScheduler(executors=executors, job_defaults=job_defaults, timezone='America/Los_Angeles')
Modify the Archive Function to Use Time Zone-Aware Dates: Update the archive_tasks function to calculate dates using the specified time zone:

def archive_tasks():
    with app.app_context():
        # Set the timezone
        tz = timezone('America/Los_Angeles')
        today = datetime.now(tz).date()
        last_archived_date = ArchiveInfo.get_last_archive()

        if last_archived_date is None or last_archived_date != today:
            tasks = Task.query.filter(Task.status == 'finished')
            for task in tasks:
                task.status = 'archived'
                if task.completion_date is None:
                    task.completion_date = today - timedelta(days=1)
            db.session.commit()

            ArchiveInfo.update_last_archive(today)
            logging.info(f"Archived tasks for {today}")
Ensure Consistent Date Handling in get_yearly_data: Similarly, adjust the get_yearly_data function to use time zone-aware dates:

@app.route('/api/history', methods=['GET'])
def get_yearly_data():
    tz = timezone('America/Los_Angeles')
    today = datetime.now(tz).date()
    year_param = request.args.get('year', default=today.year, type=int)

    # Define the start and end dates for the specified year
    start_date = date(year_param, 1, 1)
    end_date = date(year_param, 12, 31)

    # Handle future years gracefully
    if end_date > today:
        end_date = today  # Limit end_date to today if in future

    # Rest of your code...
Adjust Date Strings in get_yearly_data: Ensure that when you're generating date strings (date_str), they match the format of your keys in yearly_data. Replace the use of isoformat() with strftime('%Y-%m-%d') to guarantee consistency:

for task in tasks:
    date_str = task.completion_date.strftime('%Y-%m-%d')
    if date_str in yearly_data:
        yearly_data[date_str]["completedTasks"].append(task.title)
Handle Time Zones in Your JavaScript Code (Optional): If your users are in different time zones, you might also want to adjust your JavaScript code to account for that or ensure that the server always communicates dates in UTC and adjusts for the user's local time on the client side.
Test Your Application: After making these changes, test your application thoroughly:
Mark tasks as finished late at night and see if they appear on the correct day.
Check if archiving tasks at 12:01 AM assigns the correct completion_date.
Verify that the yearly grid displays tasks under the correct dates.
Summary:

The root cause of your issue is that the date.today() function and the scheduler's execution time are based on the server's time zone, which may not align with your desired local time zone. By explicitly setting the time zone in your scheduler and using time zone-aware date calculations throughout your code, you can ensure that dates are handled consistently and tasks appear under the correct dates in your yearly grid.

Note: Replace 'America/Los_Angeles' with the appropriate time zone for your application or user base.


The server is my laptop, which should be using the same time zone I'm using right?


You're absolutely right—since your server and your local machine are the same, time zones should not be the issue here. The problem lies elsewhere in how the dates are being assigned during the archiving process, especially around the transition between days at midnight.

Understanding the Issue:

Tasks Marked as Finished Near Midnight:
When you mark tasks as 'finished' late at night (e.g., 11:59 PM) or shortly after midnight (e.g., 12:05 AM), the completion_date of the task is set to the date when the status change occurs.
For tasks marked as 'finished' after midnight but before the archiving process at 12:01 AM, the completion_date is set to the new day (e.g., '2023-10-11' instead of '2023-10-10').
During the archiving process, these tasks are archived and their completion_date remains set to the new day.
As a result, on the yearly grid, these tasks appear as completed on the new day ('today'), which may not reflect your actual workflow or expectations.
Archiving Process Timing:
The archiving function runs at 12:01 AM, just after midnight.
Any tasks marked as 'finished' after midnight but before this archiving process are included and processed with a completion_date of the new day.
Desired Behavior:

You likely want tasks that are marked as 'finished' in the late-night hours (e.g., between midnight and some early morning cutoff like 3:00 AM or 6:00 AM) to be considered as completed on the previous day. This aligns with how many people perceive their daily activities—if you're working late into the night, you still consider it part of the previous day's work.

Solution: Adjust Completion Date Based on Time

To align the completion_date with your expectations, we can adjust the code to set the completion_date to the previous day if the task is marked as 'finished' during the late-night hours. Here's how you can implement this:

Step 1: Define a Late-Night Cutoff Time
Decide on a cutoff time that signifies the end of your day. For example, you might choose 3:00 AM. Any tasks marked as 'finished' between midnight and 3:00 AM will have their completion_date set to the previous day.

Step 2: Modify the update_task Function
In your update_task function, adjust the logic for setting the completion_date when the task status changes to 'finished'.

Original Code Snippet:


if task.status == 'finished':
    # Accept completion_date from client or default to date.today()
    completion_date_str = data.get('completion_date')
    if completion_date_str:
        task.completion_date = datetime.strptime(completion_date_str, '%Y-%m-%d').date()
    else:
        task.completion_date = date.today()
else:
    task.completion_date = None
Updated Code:


from datetime import datetime, time

if task.status == 'finished':
    # Accept completion_date from client or default to date.today()
    completion_date_str = data.get('completion_date')
    if completion_date_str:
        task.completion_date = datetime.strptime(completion_date_str, '%Y-%m-%d').date()
    else:
        current_time = datetime.now().time()
        # Define your cutoff time (e.g., 3:00 AM)
        cutoff_time = time(3, 0)  # Change to desired cutoff hour
        if current_time < cutoff_time:
            # After midnight but before cutoff_time, set to previous day
            task.completion_date = date.today() - timedelta(days=1)
        else:
            task.completion_date = date.today()
else:
    task.completion_date = None
This code checks the current time when a task is marked as 'finished'. If it's after midnight but before the cutoff time, it sets the completion_date to the previous day.

Step 3: Adjust the Archiving Process (If Necessary)
Since we're now setting the correct completion_date when the task is marked as 'finished', we might not need to adjust the archiving process. However, ensure that the archiving process doesn't overwrite the completion_date if it's already set.

Original Archiving Code:


if task.completion_date == None:
    task.completion_date = date.today() - timedelta(days=1)
Updated Archiving Code:


# Only set completion_date if it's None
if task.completion_date is None:
    task.completion_date = date.today() - timedelta(days=1)
This ensures that the archiving process doesn't alter the completion_date if it was already set during the update_task function.

Step 4: Ensure Consistency in Client-Side Code
Since the client sends the completion_date when moving a task to 'finished', ensure that the client-side code aligns with the server logic.

Client-Side moveTask Function in script.js:


function moveTask(taskId, newStatus) {
    let data = { status: newStatus };
    if (newStatus === 'finished') {
        data['completion_date'] = getLocalDateString();
    } else {
        data['completion_date'] = null; // Clear the completion_date if not finished
    }
    axios.put(`/api/tasks/${taskId}`, data)
        .then(() => loadTasks())
        .catch(error => {
            console.error('Error moving task:', error);
        });
}
Issue:

The client uses the user's local time to set the completion_date.
If the user's clock is not synchronized, or if users in different time zones interact with the server, this could cause inconsistencies.
Recommendation:

Let the server handle the completion_date based on server time.
Modify the client-side code to not send the completion_date unless the user explicitly sets it.
Updated Client-Side Code:


function moveTask(taskId, newStatus) {
    let data = { status: newStatus };
    axios.put(`/api/tasks/${taskId}`, data)
        .then(() => loadTasks())
        .catch(error => {
            console.error('Error moving task:', error);
        });
}
Step 5: Update Task Completion on the Server
With the client no longer sending the completion_date, the server will handle setting it based on the new logic we added in update_task.

Step 6: Test the Changes
Before Midnight Test:
Mark a task as 'finished' before midnight.
Ensure the completion_date is set to the current date.
Verify that the task appears on the correct day in the yearly grid.
After Midnight Test:
Mark a task as 'finished' after midnight but before the cutoff time (e.g., 1:00 AM).
Ensure the completion_date is set to the previous day.
Verify that the task appears on the previous day in the yearly grid.
After Cutoff Time Test:
Mark a task as 'finished' after the cutoff time (e.g., 4:00 AM).
Ensure the completion_date is set to the current date.
Verify that the task appears on the correct day in the yearly grid.
Additional Considerations:
User Preferences:
If multiple users with different schedules are using the app, you might want to make the cutoff time configurable per user.
Timezone Awareness:
While your server and client are on the same machine, consider future scenarios where the server or clients might be in different time zones.
Use timezone-aware datetime objects if necessary.
Archiving Time:
As an alternative to adjusting the completion_date, you could move the archiving process to a later time (e.g., 3:05 AM) to ensure that tasks marked as 'finished' shortly after midnight are archived the next day.
This approach is less precise and might not align with desired behavior, especially for tasks marked during late-night hours.
Summary:
The issue arises because tasks marked as 'finished' after midnight are assigned the completion_date of the new day.
By adjusting the server logic to set the completion_date to the previous day during late-night hours, tasks will appear on the desired day in the yearly grid.
Remove the reliance on client-side time for setting the completion_date to avoid inconsistencies.
Test thoroughly to ensure the new behavior aligns with your expectations.
Updated update_task Function Example:


def update_task(task_id):
    task = Task.query.get_or_404(task_id)
    if request.method == 'PUT':
        data = request.json
        if 'title' in data and not data['title'].strip():
            # If the title is empty, delete the task
            db.session.delete(task)
            db.session.commit()
            return jsonify({'success': True, 'deleted': True})
        else:
            task.title = data.get('title', task.title)
            task.column = data.get('column', task.column)
            if 'status' in data:
                task.status = data['status']
                if task.status == 'finished':
                    # Remove this line: completion_date_str = data.get('completion_date')
                    current_time = datetime.now().time()
                    cutoff_time = time(3, 0)  # Set your desired cutoff time here
                    if current_time < cutoff_time:
                        task.completion_date = date.today() - timedelta(days=1)
                    else:
                        task.completion_date = date.today()
                else:
                    task.completion_date = None
            db.session.commit()
            return jsonify({'success': True, 'deleted': False})
    elif request.method == 'DELETE':
        db.session.delete(task)
        db.session.commit()
        return jsonify({'success': True})
Note on Timezones:

If all operations are on the same machine and users are in the same timezone, you don't need to adjust for timezones.
However, it's good practice to be aware of timezone issues, especially if the app might grow or be used in different environments.
I hope this helps solve the issue you're experiencing! Let me know if you have any questions or need further assistance.

Hm if tasks are set to time 3,0, then I want habits to be set that way too