Почему все трекеры целей — это мусор (и как сделать свой, который не сломается через месяц)
Открою секрет: я ненавижу приложения для трекинга целей. 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);
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 месяцев данных, можно добавить аналитику, которая предсказывает выгорание или рекомендует корректировки целей.
Вот что можно добавить:
- Автоматические оповещения: "Вы три дня подряд пропускаете тренировки. Хотите снизить цель или изменить время?"
- Корреляционный анализ: "Когда вы спите больше 7 часов, продуктивность в работе увеличивается на 40%"
- Прогноз завершения: "При текущем темпе вы достигнете цели 'прочитать 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 подход к личному развитию. Не гадание на кофейной гуще, а решения на основе цифр. Только ваших цифр.