Трекер целей на Python и Streamlit: дашборд для привычек и стратегических целей | AiManual
AiManual Logo Ai / Manual.
15 Янв 2026 Гайд

Трекер целей на 2026: как создать дашборд на Python и Streamlit для отслеживания привычек и стратегических целей

Пошаговый гайд по созданию data-driven трекера целей на Python, Streamlit и Neon. Классификация high/low frequency целей, архитектура приложения и готовый код.

Почему все трекеры целей — это мусор (и как сделать свой, который не сломается через месяц)

Открою секрет: я ненавижу приложения для трекинга целей. Notion-таблицы превращаются в цифровое кладбище невыполненных планов. Google Sheets обрастают формулами, которые никто не понимает. Мобильные приложения требуют ежедневного внимания, но не дают стратегической картины.

Проблема не в вас. Проблема в архитектуре.

Большинство трекеров смешивают два типа целей: high-frequency (ежедневные привычки, тренировки, чтение) и low-frequency (стратегические цели на квартал, год, карьерные переходы). Первые требуют ежедневного чекинга, вторые — еженедельной рефлексии. Когда вы пытаетесь запихнуть их в одну таблицу, получается каша.

Классическая ошибка: создавать отдельные таблицы для каждой цели. Через месяц у вас 15 разных файлов, и вы не видите взаимосвязей между ежедневными действиями и годовыми результатами.

Решение? Data-driven дашборд, который показывает:

  • Прогресс по ежедневным привычкам (heatmap как в GitHub)
  • Связь между микро-действиями и макро-целями
  • Тренды и выгорание (да, это можно предсказать)
  • Визуальную доску целей (vision board), которая обновляется автоматически

И самое главное — этот дашборд будет жить в браузере, обновляться в реальном времени и хранить данные в нормальной базе, а не в Excel-файле на Google Диске.

Архитектура, которая не развалится: Python + Streamlit + Neon

Почему именно этот стек? Потому что он работает для прототипов, которые превращаются в продакшен. Не для "пет-проектов на выходных", а для систем, которые вы будете использовать годами.

Компонент Зачем нужен Альтернативы (хуже)
Streamlit Создает интерфейс за 50 строк кода. Обновляется при каждом изменении данных. Flask + HTML (в 10 раз больше кода), Django (избыточно)
Neon (PostgreSQL) Бессерверная база с autoscaling. Бесплатный тариф на 3 ГБ — хватит на 10 лет трекинга. SQLite (ломается при concurrent access), Firebase (vendor lock-in)
Plotly Интерактивные графики, которые можно масштабировать и сохранять. Matplotlib (статичные картинки), Seaborn (мало интерактива)

Ключевой момент: мы разделяем логику хранения (база данных) и представления (дашборд). Это позволяет позже добавить мобильное приложение или телеграм-бота без переписывания всей логики. Помните статью про технический долг в AI-разработке? Тот же принцип: начинайте с архитектуры, которая масштабируется.

1 Настраиваем базу данных в Neon

Neon — это Postgres-as-a-service с одним критически важным преимуществом: он не требует управления сервером. Создаем проект за 2 минуты:

# Ничего не устанавливаем локально
# Идем на neon.tech → Sign up → Create project
# Копируем connection string вида:
# postgresql://user:password@ep-cool-bird-123456.us-east-2.aws.neon.tech/dbname?sslmode=require

Создаем таблицы. Вот схема, которая работает для high/low frequency целей:

-- goals.sql
CREATE TABLE goals (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    description TEXT,
    goal_type VARCHAR(20) CHECK (goal_type IN ('high_freq', 'low_freq')),
    target_value NUMERIC, -- например, 10000 шагов
    unit TEXT, -- 'steps', 'minutes', 'books'
    start_date DATE,
    end_date DATE,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE goal_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    goal_id UUID REFERENCES goals(id) ON DELETE CASCADE,
    date DATE NOT NULL,
    value NUMERIC NOT NULL, -- фактическое значение
    notes TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(goal_id, date) -- одна запись в день на цель
);

CREATE INDEX idx_goal_entries_date ON goal_entries(date);
CREATE INDEX idx_goal_entries_goal_id ON goal_entries(goal_id);
💡
Обратите внимание на UNIQUE constraint. Это предотвращает дублирование записей за день — частая ошибка в самописных трекерах. Без этого constraint можно случайно добавить две записи за один день и испортить статистику.

2 Пишем ядро на Python: работа с базой

Создаем файл database.py. Не используем ORM типа SQLAlchemy — для такой простой схемы это overkill.

# database.py
import os
import psycopg2
from psycopg2.extras import RealDictCursor
from datetime import date, datetime
from typing import List, Dict, Optional
import pandas as pd

class GoalTrackerDB:
    def __init__(self):
        # Берем connection string из переменных окружения
        self.conn_string = os.getenv('NEON_DB_URL')
        
    def get_connection(self):
        """Создаем соединение с базой"""
        return psycopg2.connect(self.conn_string, cursor_factory=RealDictCursor)
    
    def add_goal(self, title: str, goal_type: str, 
                 target_value: float = None, unit: str = None,
                 end_date: date = None) -> str:
        """Добавляем новую цель"""
        with self.get_connection() as conn:
            with conn.cursor() as cur:
                cur.execute("""
                    INSERT INTO goals (title, goal_type, target_value, unit, end_date)
                    VALUES (%s, %s, %s, %s, %s)
                    RETURNING id
                """, (title, goal_type, target_value, unit, end_date))
                goal_id = cur.fetchone()['id']
                conn.commit()
                return goal_id
    
    def log_progress(self, goal_id: str, value: float, 
                     log_date: date = None, notes: str = None):
        """Логируем прогресс за день"""
        if log_date is None:
            log_date = date.today()
            
        with self.get_connection() as conn:
            with conn.cursor() as cur:
                # Используем ON CONFLICT для обновления существующей записи
                cur.execute("""
                    INSERT INTO goal_entries (goal_id, date, value, notes)
                    VALUES (%s, %s, %s, %s)
                    ON CONFLICT (goal_id, date) 
                    DO UPDATE SET value = EXCLUDED.value, notes = EXCLUDED.notes
                """, (goal_id, log_date, value, notes))
                conn.commit()
    
    def get_weekly_summary(self, weeks_back: int = 4) -> pd.DataFrame:
        """Получаем сводку за последние N недель"""
        with self.get_connection() as conn:
            query = """
                SELECT 
                    g.title,
                    g.goal_type,
                    g.unit,
                    ge.date,
                    ge.value,
                    g.target_value,
                    EXTRACT(WEEK FROM ge.date) as week_number
                FROM goal_entries ge
                JOIN goals g ON g.id = ge.goal_id
                WHERE ge.date >= CURRENT_DATE - INTERVAL '%s weeks'
                ORDER BY ge.date DESC
            """
            df = pd.read_sql_query(query, conn, params=(weeks_back,))
            return df
    
    def get_goal_streaks(self) -> Dict[str, int]:
        """Вычисляем текущие серии выполнения для каждой цели"""
        with self.get_connection() as conn:
            with conn.cursor() as cur:
                # Этот запрос находит максимальную непрерывную серию дней
                # с хотя бы одной записью для каждой цели
                cur.execute("""
                    WITH consecutive_days AS (
                        SELECT 
                            goal_id,
                            date,
                            date - ROW_NUMBER() OVER 
                                (PARTITION BY goal_id ORDER BY date) * INTERVAL '1 day' as grp
                        FROM goal_entries
                    )
                    SELECT 
                        g.title,
                        COUNT(*) as current_streak
                    FROM consecutive_days cd
                    JOIN goals g ON g.id = cd.goal_id
                    WHERE cd.date >= CURRENT_DATE - INTERVAL '30 days'
                    GROUP BY g.title, cd.grp
                    ORDER BY MIN(cd.date) DESC
                    LIMIT 10
                """)
                return {row['title']: row['current_streak'] for row in cur.fetchall()}

Не храните connection string в коде! Используйте переменные окружения или .env файл. В Streamlit есть st.secrets для этого, но для локальной разработки проще использовать python-dotenv.

3 Создаем дашборд на Streamlit: от данных к инсайтам

Теперь самое интересное — визуализация. Создаем app.py:

# app.py
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import date, timedelta
import calendar
from database import GoalTrackerDB

st.set_page_config(page_title="Трекер целей 2026", layout="wide")
st.title("🎯 Трекер целей 2026")
st.markdown("High-frequency привычки + Low-frequency стратегические цели")

# Инициализируем базу данных
db = GoalTrackerDB()

# Сайдбар для добавления новых записей
with st.sidebar:
    st.header("Добавить прогресс")
    
    # Получаем список целей
    with db.get_connection() as conn:
        goals_df = pd.read_sql_query(
            "SELECT id, title, goal_type, unit FROM goals ORDER BY created_at", conn)
    
    if not goals_df.empty:
        selected_goal = st.selectbox(
            "Выберите цель", 
            goals_df['title'].tolist()
        )
        goal_id = goals_df[goals_df['title'] == selected_goal].iloc[0]['id']
        goal_unit = goals_df[goals_df['title'] == selected_goal].iloc[0]['unit']
        
        value = st.number_input(f"Значение ({goal_unit if goal_unit else 'единицы'})", 
                               min_value=0.0, step=0.1)
        notes = st.text_area("Заметки (опционально)")
        log_date = st.date_input("Дата", value=date.today())
        
        if st.button("Записать прогресс", type="primary"):
            db.log_progress(goal_id, value, log_date, notes)
            st.success(f"Прогресс по '{selected_goal}' записан!")
    else:
        st.info("Сначала добавьте цели во вкладке 'Управление целями'")

# Основная область — вкладки
tab1, tab2, tab3 = st.tabs(["📊 Дашборд", "🔥 Серии", "🎯 Управление целями"])

with tab1:
    col1, col2 = st.columns(2)
    
    with col1:
        st.subheader("Прогресс за месяц")
        # Heatmap как в GitHub
        df = db.get_weekly_summary(weeks_back=12)
        if not df.empty:
            df['date'] = pd.to_datetime(df['date'])
            df['day_of_week'] = df['date'].dt.dayofweek
            df['week_of_year'] = df['date'].dt.isocalendar().week
            
            # Создаем сводную таблицу для heatmap
            pivot_df = df.pivot_table(
                index='day_of_week', 
                columns='week_of_year', 
                values='value', 
                aggfunc='sum', 
                fill_value=0
            )
            
            fig = px.imshow(
                pivot_df,
                labels=dict(x="Неделя", y="День недели", color="Активность"),
                x=[f"W{w}" for w in pivot_df.columns],
                y=["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"],
                color_continuous_scale="Blues"
            )
            st.plotly_chart(fig, use_container_width=True)
    
    with col2:
        st.subheader("Тренды по целям")
        if not df.empty:
            # Группируем по неделям и целям
            weekly_trends = df.groupby(['title', 'week_number'])['value'].sum().reset_index()
            
            fig = px.line(
                weekly_trends, 
                x='week_number', 
                y='value', 
                color='title',
                markers=True,
                title="Динамика по неделям"
            )
            fig.update_layout(hovermode="x unified")
            st.plotly_chart(fig, use_container_width=True)

with tab2:
    st.subheader("Текущие серии выполнения")
    streaks = db.get_goal_streaks()
    
    if streaks:
        for goal_name, streak_days in streaks.items():
            col1, col2, col3 = st.columns([2, 1, 3])
            with col1:
                st.write(f"**{goal_name}**")
            with col2:
                st.metric(label="Дней подряд", value=streak_days)
            with col3:
                # Прогресс-бар
                progress = min(streak_days / 30, 1.0)  # Макс 30 дней для шкалы
                st.progress(progress, 
                           text=f"{streak_days} из 30 дней для формирования привычки")
    else:
        st.info("Серии пока не сформированы. Записывайте прогресс каждый день!")

with tab3:
    st.subheader("Добавить новую цель")
    
    with st.form("new_goal_form"):
        title = st.text_input("Название цели*")
        description = st.text_area("Описание")
        
        col1, col2 = st.columns(2)
        with col1:
            goal_type = st.selectbox(
                "Тип цели*",
                ["high_freq", "low_freq"],
                format_func=lambda x: "Высокая частота (ежедневно)" 
                if x == "high_freq" else "Низкая частота (стратегическая)"
            )
        with col2:
            unit = st.text_input("Единица измерения", 
                                placeholder="шаги, минуты, книги...")
        
        target_value = st.number_input("Целевое значение", min_value=0.0)
        end_date = st.date_input("Планируемая дата завершения")
        
        submitted = st.form_submit_button("Создать цель", type="primary")
        if submitted and title:
            goal_id = db.add_goal(
                title=title,
                goal_type=goal_type,
                target_value=target_value if target_value > 0 else None,
                unit=unit if unit else None,
                end_date=end_date
            )
            st.success(f"Цель '{title}' создана! ID: {goal_id}")
    
    # Список существующих целей
    st.subheader("Мои цели")
    with db.get_connection() as conn:
        goals_list = pd.read_sql_query(
            """
            SELECT 
                g.title,
                g.goal_type,
                g.target_value,
                g.unit,
                g.end_date,
                COUNT(ge.id) as entries_count,
                AVG(ge.value) as avg_value
            FROM goals g
            LEFT JOIN goal_entries ge ON g.id = ge.goal_id
            GROUP BY g.id, g.title, g.goal_type, g.target_value, g.unit, g.end_date
            ORDER BY g.created_at DESC
            """, conn)
    
    if not goals_list.empty:
        st.dataframe(
            goals_list,
            column_config={
                "goal_type": st.column_config.TextColumn(
                    "Тип",
                    help="High frequency или Low frequency"
                ),
                "entries_count": st.column_config.NumberColumn(
                    "Записей",
                    help="Количество записей прогресса"
                ),
                "avg_value": st.column_config.NumberColumn(
                    "Среднее значение",
                    help="Средний прогресс за все записи",
                    format="%.1f"
                )
            },
            hide_index=True,
            use_container_width=True
        )

Как не сломать систему: нюансы, которые никто не рассказывает

Теперь у вас есть работающий трекер. Но чтобы он жил дольше месяца, нужно избежать трех критических ошибок.

Ошибка 1: Слишком много high-frequency целей

Люди добавляют 15 ежедневных привычек. Через неделю устают заполнять все. Решение: ограничьте 5-7 high-frequency целями максимум. Low-frequency цели могут быть в любом количестве — их вы обновляете раз в неделю.

Ошибка 2: Отсутствие "выходных"

Ваш трекер не должен требовать ежедневного заполнения. Добавьте логику пропусков:

# В database.py добавляем метод
def get_missed_days(self, goal_id: str, days_back: int = 30):
    """Находим дни, когда не было прогресса"""
    with self.get_connection() as conn:
        query = """
            SELECT date_series.date
            FROM generate_series(
                CURRENT_DATE - INTERVAL '%s days', 
                CURRENT_DATE, 
                '1 day'::interval
            ) as date_series(date)
            LEFT JOIN goal_entries ge 
                ON ge.goal_id = %s 
                AND ge.date = date_series.date::date
            WHERE ge.id IS NULL
            ORDER BY date_series.date DESC
        """
        df = pd.read_sql_query(query, conn, params=(days_back, goal_id))
        return df['date'].tolist()

Но ключевой момент: не ругайте себя за пропуски. Просто анализируйте паттерны. Если постоянно пропускаете понедельники — может, нужно сделать понедельник "легким днем"?

Ошибка 3: Фетишизация метрик

Не превращайте трекер в игру по набору очков. Цель — не максимизировать цифры, а понять, какие действия ведут к стратегическим результатам. Добавьте поле для рефлексии:

# В форму добавления прогресса
reflection = st.text_area(
    "Что я узнал сегодня?",
    help="Не только цифры, но и инсайты. Например: 'Заметил, что после 18:00 продуктивность падает'"
)

Куда развивать систему: от трекера к AI-ассистенту

Когда накопится 3-6 месяцев данных, можно добавить аналитику, которая предсказывает выгорание или рекомендует корректировки целей.

💡
Используйте эмбеддинги для кластеризации ваших целей. Похожий подход я описывал в статье про data-driven анализ вкусов. Можете сгруппировать цели по темам: здоровье, карьера, обучение, отношения.

Вот что можно добавить:

  1. Автоматические оповещения: "Вы три дня подряд пропускаете тренировки. Хотите снизить цель или изменить время?"
  2. Корреляционный анализ: "Когда вы спите больше 7 часов, продуктивность в работе увеличивается на 40%"
  3. Прогноз завершения: "При текущем темпе вы достигнете цели 'прочитать 50 книг' к 15 ноября 2026"

Для этого понадобится добавить ML-компоненты. Но начинайте с простого. Как я писал в статье про дашборды для стейкхолдеров: сначала сделайте полезное ядро, потом добавляйте "фичи".

Запуск и деплой: чтобы система работала без вас

Локально запускаете так:

# Устанавливаем зависимости
pip install streamlit psycopg2-binary pandas plotly python-dotenv

# Создаем .env файл с NEON_DB_URL
# Запускаем приложение
streamlit run app.py

Для постоянного доступа деплоим на Streamlit Cloud, Hugging Face Spaces или даже на свой сервер. Streamlit Cloud бесплатен для публичных приложений.

Важно: если деплоите публично, добавьте аутентификацию! Streamlit Community Cloud не имеет встроенной авторизации. Используйте st.secrets для хранения паролей или добавьте простую проверку через st.text_input с паролем.

Что дальше? Трекер, который эволюционирует с вами

Самый важный принцип: ваш трекер должен адаптироваться под изменения в жизни. В январе 2026 вы поставите одни цели, в июне — другие. Не бойтесь архивировать старые цели и создавать новые.

Добавьте в базу поле status с вариантами: active, paused, completed, archived. Раз в квартал проводите ревизию.

И последний совет: не делайте трекинг самоцелью. Лучше иметь неполные данные за год, чем идеальные данные за месяц и выгорание. Система должна служить вам, а не вы — системе.

Когда накопите достаточно данных, вы сможете ответить на вопросы, которые большинство людей задают интуитивно: "В какое время дня я наиболее продуктивен?", "Какие привычки действительно влияют на мое настроение?", "Сколько времени на самом деле уходит на большие проекты?"

Это и есть data-driven подход к личному развитию. Не гадание на кофейной гуще, а решения на основе цифр. Только ваших цифр.