Skip to content

Commit 5fd4a41

Browse files
committed
chore: bump version to 0.3.3
1 parent e75adeb commit 5fd4a41

15 files changed

Lines changed: 291 additions & 155 deletions

File tree

README.md

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,63 @@
11
# FretLog 🎸
22

3-
FretLog is a personal music practice dashboard designed to help musicians track their progress, manage their repertoire, and maintain a consistent practice routine.
3+
FretLog is a personal music practice dashboard designed to help musicians track their progress, manage their repertoire, and maintain a consistent practice routine. It combines a minimal practice timer with powerful statistics and visual progress trackers.
4+
5+
![Dashboard Preview](public/screenshots/dashboard.png)
46

57
## Project Status: Vibe Coded
68
> [!IMPORTANT]
79
> This project is **heavily vibe coded** and primarily intended for **personal use**.
8-
> It was built with a focus on immediate utility and specific workflow "vibes". While functional and feature-rich, it may not follow traditional professional software architecture patterns. It works for me, and it might work for you!
10+
> It was built with a focus on immediate utility and my specific workflow. While functional and feature-rich, it may not follow traditional professional software architecture patterns. It works for me, and it might work for you!
911
10-
## Features
11-
- **Dashboard**: Quick overview of your practice streak, total time, and recent sessions.
12-
- **Active Timer**: Start a practice session and track time for specific items in your library.
13-
- **Library Management**: Organize your songs, exercises, and techniques by category and artist.
14-
- **Detailed Statistics**: Visualize your progress with GitHub-style activity heatmaps and trend charts.
15-
- **Session History**: Review and manage past practice logs with detailed notes.
16-
- **Customizable**: Add your own instruments and practice categories.
17-
- **Dark Mode**: Sleek, modern interface that's easy on the eyes.
12+
## Key Features
13+
- **📊 Interactive Dashboard**: Quick overview of your practice streak, total time, and recent sessions.
14+
- **⏱️ Active Session Timer**: Start a practice session and track time for specific items in your library with a non-intrusive global timer.
15+
- **📱 PWA Ready**: Install FretLog on your phone or desktop for a native-like experience.
16+
- **📈 Detailed Statistics**: Visualize your progress with GitHub-style activity heatmaps, category distributions, and practice trend charts.
17+
- **📚 Repertoire Library**: Organize your songs, exercises, and techniques with ratings, notes, and artist grouping.
18+
- **🎸 Instrument Management**: Support for multiple instruments (Guitar, Bass, Piano, etc.).
19+
- **☁️ Data Sovereignty**: Full JSON Export/Import capabilities and a single-user SQLite backend for privacy.
20+
- **🌙 Modern UI**: A sleek, dark-mode-first interface optimized for both desktop and mobile use.
1821

19-
## Deployment with Docker
20-
The easiest way to run FretLog is using Docker Compose.
22+
## Screenshots
2123

22-
1. **Clone the repo**:
23-
```bash
24-
git clone https://github.com/aFFekopp/fretlog.git
25-
cd fretlog
24+
| Dashboard | Sessions |
25+
|:---:|:---:|
26+
| ![Dashboard](public/screenshots/dashboard.png) | ![Sessions](public/screenshots/sessions.png) |
27+
28+
| Statistics | Library |
29+
|:---:|:---:|
30+
| ![Statistics](public/screenshots/statistics.png) | ![Library](public/screenshots/library.png) |
31+
32+
## Deployment with Docker Compose
33+
The easiest way to run FretLog is using Docker Compose with the pre-built image.
34+
35+
1. **Create a `docker-compose.yml` file**:
36+
```yaml
37+
services:
38+
fretlog:
39+
image: ghcr.io/affekopp/fretlog:latest
40+
container_name: fretlog
41+
restart: unless-stopped
42+
ports:
43+
- "5000:5000"
44+
volumes:
45+
- ./data:/app/data
2646
```
27-
2. **Start the app**:
47+
48+
2. **Start the application**:
2849
```bash
2950
docker-compose up -d
3051
```
52+
3153
3. **Access FretLog**:
3254
Open [http://localhost:5000](http://localhost:5000) in your browser.
3355

3456
## Tech Stack
3557
- **Backend**: Python (Flask), SQLite
36-
- **Frontend**: Vanilla JS, HTML5, CSS3 (Modern Flex/Grid)
58+
- **Frontend**: Vanilla JS, HTML5, CSS3 (Modern Flex/Grid), Chart.js
59+
- **PWA**: Service Workers, Web Manifest
3760
- **Deployment**: Docker, GHCR, GitHub Actions
3861

3962
## License
40-
Intended for personal use. Feel free to fork and adapt it to your own practice vibes.
63+
Intended for personal use. Feel free to fork and adapt it to your needs.

public/screenshots/dashboard.png

267 KB
Loading

public/screenshots/library.png

152 KB
Loading

public/screenshots/sessions.png

102 KB
Loading

public/screenshots/settings.png

119 KB
Loading

public/screenshots/statistics.png

190 KB
Loading

server.py

Lines changed: 61 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ def init_db():
4040
cursor = conn.cursor()
4141

4242
# Tables
43-
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
44-
id TEXT PRIMARY KEY, name TEXT, email TEXT, avatar TEXT,
45-
default_instrument_id TEXT, created_at TEXT)''')
43+
cursor.execute('DROP TABLE IF EXISTS users')
4644

4745
cursor.execute('''CREATE TABLE IF NOT EXISTS categories (
4846
id TEXT PRIMARY KEY, name TEXT, type TEXT, icon TEXT, color TEXT)''')
@@ -69,7 +67,7 @@ def init_db():
6967
key TEXT PRIMARY KEY, value TEXT)''')
7068

7169
# Default data if empty
72-
cursor.execute('SELECT count(*) FROM users')
70+
cursor.execute('SELECT count(*) FROM instruments')
7371
if cursor.fetchone()[0] == 0:
7472
init_default_data(conn)
7573

@@ -78,9 +76,6 @@ def init_db():
7876

7977
def init_default_data(conn):
8078
cursor = conn.cursor()
81-
user_id = generate_id()
82-
cursor.execute('INSERT INTO users (id, name, created_at) VALUES (?, ?, ?)',
83-
(user_id, 'Musician', datetime.now().isoformat()))
8479

8580
# Default Categories
8681
cats = [
@@ -105,8 +100,8 @@ def init_default_data(conn):
105100
cursor.execute('INSERT INTO instruments (id, name, icon) VALUES (?, ?, ?)',
106101
(inst_id, name, icon))
107102

108-
# Set default instrument for user
109-
cursor.execute('UPDATE users SET default_instrument_id=? WHERE id=?', ('inst-guitar', user_id))
103+
# Set default instrument
104+
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES ('default_instrument_id', ?)", ('inst-guitar',))
110105

111106
conn.commit()
112107

@@ -116,18 +111,20 @@ def inject_user():
116111
conn = get_db()
117112
cursor = conn.cursor()
118113

119-
# Get User
120-
cursor.execute('SELECT * FROM users LIMIT 1')
121-
user_row = cursor.fetchone()
122-
user = dict(user_row) if user_row else None
114+
# Get Current Instrument ID from settings
115+
cursor.execute("SELECT value FROM settings WHERE key='default_instrument_id'")
116+
row = cursor.fetchone()
117+
instrument_id = row[0] if row else 'inst-guitar'
123118

124119
# Get Current Instrument
125120
instrument = None
126-
if user and user.get('default_instrument_id'):
127-
cursor.execute('SELECT * FROM instruments WHERE id=?', (user['default_instrument_id'],))
128-
inst_row = cursor.fetchone()
129-
if inst_row:
130-
instrument = dict(inst_row)
121+
cursor.execute('SELECT * FROM instruments WHERE id=?', (instrument_id,))
122+
inst_row = cursor.fetchone()
123+
if inst_row:
124+
instrument = dict(inst_row)
125+
126+
# current_user is now a dummy object for template compatibility
127+
user = {'id': 'local-user', 'name': 'Musician'}
131128

132129
return dict(current_user=user, current_instrument=instrument)
133130
except Exception as e:
@@ -180,9 +177,11 @@ def get_init_data():
180177
conn = get_db()
181178
cursor = conn.cursor()
182179

183-
# User
184-
cursor.execute('SELECT * FROM users LIMIT 1')
185-
user = dict_from_row(cursor.fetchone())
180+
# User (Dummy for compatibility)
181+
user = {'id': 'local-user', 'name': 'Musician'}
182+
cursor.execute("SELECT value FROM settings WHERE key='default_instrument_id'")
183+
inst_row = cursor.fetchone()
184+
user['default_instrument_id'] = inst_row[0] if inst_row else 'inst-guitar'
186185

187186
# Categories
188187
cursor.execute('SELECT * FROM categories')
@@ -239,34 +238,13 @@ def get_init_data():
239238
# ==========================================
240239
@app.route('/api/user', methods=['GET'])
241240
def get_user():
242-
conn = get_db()
243-
cursor = conn.cursor()
244-
cursor.execute('SELECT * FROM users LIMIT 1')
245-
user = dict_from_row(cursor.fetchone())
246-
conn.close()
241+
user = {'id': 'local-user', 'name': 'Musician'}
247242
return jsonify(user)
248243

249244
@app.route('/api/user', methods=['POST', 'PUT'])
250245
def update_user():
251-
data = request.json
252-
conn = get_db()
253-
cursor = conn.cursor()
254-
255-
cursor.execute('SELECT id FROM users LIMIT 1')
256-
user = cursor.fetchone()
257-
258-
if user:
259-
cursor.execute('''
260-
UPDATE users SET name=?, email=?, avatar=?, default_instrument_id=?
261-
WHERE id=?
262-
''', (data.get('name'), data.get('email'), data.get('avatar'),
263-
data.get('defaultInstrumentId'), user[0]))
264-
265-
conn.commit()
266-
cursor.execute('SELECT * FROM users WHERE id=?', (user[0],))
267-
updated_user = dict_from_row(cursor.fetchone())
268-
conn.close()
269-
return jsonify(updated_user)
246+
# Users table is gone, just return dummy
247+
return jsonify({'id': 'local-user', 'name': 'Musician'})
270248

271249
# ==========================================
272250
# Categories API
@@ -880,6 +858,25 @@ def set_theme():
880858
conn.close()
881859
return jsonify({'theme': data.get('theme')})
882860

861+
@app.route('/api/settings', methods=['POST'])
862+
def update_setting():
863+
data = request.json
864+
key = data.get('key')
865+
value = data.get('value')
866+
867+
if not key:
868+
return jsonify({'error': 'Missing key'}), 400
869+
870+
conn = get_db()
871+
cursor = conn.cursor()
872+
cursor.execute('''
873+
INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)
874+
''', (key, value))
875+
conn.commit()
876+
conn.close()
877+
878+
return jsonify({'status': 'success', 'key': key, 'value': value})
879+
883880
# ==========================================
884881
# Statistics API
885882
# ==========================================
@@ -931,7 +928,7 @@ def export_data():
931928
conn = get_db()
932929
cursor = conn.cursor()
933930

934-
tables = ['users', 'categories', 'instruments', 'artists', 'library_items', 'sessions', 'session_items', 'settings']
931+
tables = ['categories', 'instruments', 'artists', 'library_items', 'sessions', 'session_items', 'settings']
935932
export = {}
936933

937934
for table in tables:
@@ -1017,18 +1014,12 @@ def import_data():
10171014
cursor.execute('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
10181015
(setting['key'], setting['value']))
10191016

1020-
# 8. User (optional, might want to keep current)
1021-
if 'users' in data and data['users']:
1022-
user = data['users'][0]
1023-
cursor.execute('''
1024-
INSERT OR REPLACE INTO users (id, name, email, avatar, default_instrument_id, created_at)
1025-
VALUES (?, ?, ?, ?, ?, ?)
1026-
''', (user['id'], user['name'], user.get('email', ''), user.get('avatar'),
1027-
user.get('default_instrument_id'), user['created_at']))
1017+
# 8. Table 'users' is no longer imported
10281018

10291019
conn.commit()
10301020
return jsonify({'status': 'success', 'message': 'Data imported successfully'})
10311021
except Exception as e:
1022+
print(f"Error importing data: {e}")
10321023
conn.rollback()
10331024
return jsonify({'error': str(e)}), 500
10341025
finally:
@@ -1041,16 +1032,16 @@ def clear_data():
10411032
cursor = conn.cursor()
10421033

10431034
try:
1044-
# Preserve user info
1045-
cursor.execute('SELECT name, email, default_instrument_id FROM users LIMIT 1')
1046-
user_row = cursor.fetchone()
1047-
preserved_user = dict(user_row) if user_row else {'name': 'Musician', 'email': '', 'default_instrument_id': None}
1048-
10491035
# Preserve theme
10501036
cursor.execute("SELECT value FROM settings WHERE key='theme'")
10511037
theme_row = cursor.fetchone()
10521038
preserved_theme = theme_row[0] if theme_row else 'dark'
10531039

1040+
# Preserve instrument
1041+
cursor.execute("SELECT value FROM settings WHERE key='default_instrument_id'")
1042+
inst_row = cursor.fetchone()
1043+
preserved_inst = inst_row[0] if inst_row else 'inst-guitar'
1044+
10541045
# Delete everything
10551046
cursor.execute('DELETE FROM session_items')
10561047
cursor.execute('DELETE FROM sessions')
@@ -1059,26 +1050,18 @@ def clear_data():
10591050
cursor.execute('DELETE FROM categories')
10601051
cursor.execute('DELETE FROM instruments')
10611052
cursor.execute('DELETE FROM settings')
1062-
cursor.execute('DELETE FROM users')
10631053

10641054
conn.commit()
10651055

10661056
# Re-initialize with defaults
10671057
init_default_data(conn)
10681058

1069-
# Restore preserved data
1070-
# Update user (id will be newly generated by init_default_data if we deleted all, but init_db usually handles this)
1071-
# Actually init_default_data adds a NEW user if count is 0.
1072-
# Let's check how many users now
1073-
cursor.execute('SELECT id FROM users LIMIT 1')
1074-
new_user = cursor.fetchone()
1075-
if new_user:
1076-
cursor.execute('UPDATE users SET name=?, email=?, default_instrument_id=? WHERE id=?',
1077-
(preserved_user['name'], preserved_user['email'], preserved_user['default_instrument_id'], new_user[0]))
1078-
10791059
# Restore theme
10801060
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES ('theme', ?)", (preserved_theme,))
10811061

1062+
# Restore instrument
1063+
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES ('default_instrument_id', ?)", (preserved_inst,))
1064+
10821065
conn.commit()
10831066
return jsonify({'status': 'success', 'message': 'All data cleared except defaults and profile'})
10841067
except Exception as e:
@@ -1087,6 +1070,14 @@ def clear_data():
10871070
finally:
10881071
conn.close()
10891072

1073+
@app.route('/manifest.json')
1074+
def serve_manifest():
1075+
return send_from_directory('static', 'manifest.json')
1076+
1077+
@app.route('/sw.js')
1078+
def serve_sw():
1079+
return send_from_directory('static', 'sw.js')
1080+
10901081
if __name__ == '__main__':
10911082
with app.app_context():
10921083
init_db()

static/js/app.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,11 @@ function updateUserInfo() {
9393
} catch (e) { console.warn('Failed to parse cached instruments', e); }
9494
}
9595

96-
const userName = document.getElementById('user-name');
9796
const userInstrument = document.getElementById('user-instrument');
9897
const userAvatar = document.getElementById('user-avatar');
9998

100-
if (userName) userName.textContent = user?.name || 'Musician';
10199
if (userInstrument) userInstrument.textContent = instrument?.name || 'Guitar';
102-
if (userAvatar) userAvatar.textContent = (user?.name || 'M').charAt(0).toUpperCase();
100+
if (userAvatar && instrument) userAvatar.textContent = instrument.icon || '🎸';
103101
}
104102

105103
// ==========================================

0 commit comments

Comments
 (0)