├── .gitignore ├── scores.gif ├── filtering.gif ├── logbook.png ├── settings.ndtl ├── scripts ├── logbook.js ├── lib │ ├── tablatal.js │ ├── indental.js │ └── helpers.js ├── interface │ ├── summary.js │ ├── balance.js │ ├── scores.js │ └── interface.js ├── day.js └── database.js ├── LICENSE ├── README.md ├── index.html ├── CODE_OF_CONDUCT.md ├── links └── main.css └── logbook.tbtl /.gitignore: -------------------------------------------------------------------------------- 1 | database/ 2 | .DS* 3 | *.plist -------------------------------------------------------------------------------- /scores.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rostiger/logbook/HEAD/scores.gif -------------------------------------------------------------------------------- /filtering.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rostiger/logbook/HEAD/filtering.gif -------------------------------------------------------------------------------- /logbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rostiger/logbook/HEAD/logbook.png -------------------------------------------------------------------------------- /settings.ndtl: -------------------------------------------------------------------------------- 1 | DATABASE.settings = ` 2 | CATEGORIES 3 | WORK : #FF68A3 4 | LIFE : #EBD33F 5 | SLEEP : #42C2EE 6 | PROJECTS 7 | Company 8 | CAT : WORK 9 | NAME : Company 10 | TimeTracker 11 | CAT : WORK 12 | NAME : Time Tracker 13 | Website 14 | CAT : WORK 15 | NAME : Website 16 | Household 17 | CAT : LIFE 18 | NAME : Household 19 | Cooking 20 | CAT : LIFE 21 | NAME : Cooking 22 | Personal 23 | CAT : LIFE 24 | NAME : Personal 25 | Family 26 | CAT : LIFE 27 | NAME : Family 28 | SLEEP 29 | CAT : SLEEP 30 | NAME : Sleep 31 | SCORES 32 | BODY : #FF68A3 33 | MIND : #EBD33F 34 | MOOD : #42C2EE 35 | ` -------------------------------------------------------------------------------- /scripts/logbook.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const database = {} 3 | const dates = { from : 0, to : 0 } 4 | const page = { url : '' } 5 | 6 | function Logbook () { 7 | this.errors = [] 8 | this.database = new Database() 9 | this.interface = new Interface() 10 | 11 | this.install = function (host) { 12 | this.database.install() 13 | this.interface.install(host) 14 | } 15 | 16 | this.start = function () { 17 | // stop at errors 18 | if (this.errors.length > 0) this.interface.update() 19 | else { 20 | this.database.start() 21 | this.update() 22 | } 23 | } 24 | 25 | this.update = function (updateDB = true, input = window.location.hash) { 26 | page.url = input.toUrl().toUpperCase() 27 | setTimeout(() => { window.scrollTo(0, 0) }, 250) 28 | if (updateDB) this.database.update(dates) 29 | this.interface.update() 30 | } 31 | } -------------------------------------------------------------------------------- /scripts/lib/tablatal.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function tablatal (data, Type) { 4 | function makeKey (segs) { 5 | segs.pop() 6 | const keys = {} 7 | let counter = 0 8 | for (const id in segs) { 9 | const key = segs[id].trim().toLowerCase() 10 | const len = id < segs.length - 1 ? segs[id].length : -1 11 | keys[key] = [counter, len - 1] 12 | counter += len 13 | } 14 | return keys 15 | } 16 | const a = [] 17 | const lines = data.trim().split('\n') 18 | const key = makeKey(lines.shift().match(/(\w*\W*)/g)) 19 | for (const id in lines) { 20 | if (lines[id].trim() === '') { continue } 21 | if (lines[id].substr(0, 1) === ';') { continue } 22 | const entry = {} 23 | for (const i in key) { 24 | entry[i] = lines[id].substr(key[i][0], key[i][1] < 0 ? lines[id].length : key[i][1]).trim() 25 | } 26 | a.push(Type ? new Type(entry) : entry) 27 | } 28 | return a 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Clemens Scott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logbook 2 | [Logbook](https://rostiger.github.io/logbook/) is a personal tracking tool I use to document and analyse how I spend my time. 3 | 4 | **Logbook** is a subset of **Anchors**, an ecosystem that hosts an array of personal tools, such as [Chronicle](https://github.com/Rostiger/chronicle) and Lexicon. 5 | 6 | Anchors was born from the desire to better know and understand myself and gather data on where I come from, where I am and where I am going. It is an attempt to store and visualize this data in a single place where it is easily accessible and maintainable. 7 | 8 | Logbook is inspired by members of the [Merveilles](https://merveilles.town) community who create and use their own time tracking tools. 9 | 10 | # Screenshots 11 | 12 | 13 | # Usage 14 | Logbook creates an interface from text based data using [neauoire](https://github.com/neauoire)'s _Tablatal_ and _Indental_ formats and parsers. 15 | 16 | More description coming soon! 17 | 18 | # Extras 19 | - See the [License](https://github.com/Rostiger/logbook/blob/master/LICENSE) file for license rights and limitations (MIT). 20 | - Live Version: (https://rostiger.github.io/logbook/) 21 | - Pull Requests are welcome! 22 | -------------------------------------------------------------------------------- /scripts/interface/summary.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function Summary (host) { 4 | const total = document.createElement('section') 5 | total.id = 'total' 6 | host.appendChild(total) 7 | 8 | this.update = function (categories) { 9 | 10 | const bars = document.createElement('figure') 11 | bars.id = 'bars' 12 | total.appendChild(bars) 13 | 14 | const legend = document.createElement('figure') 15 | legend.id = 'legend' 16 | total.appendChild(legend) 17 | 18 | for (const c in database.categories) { 19 | const cat = categories[c] 20 | if (!cat) continue 21 | const color = database.categories[c].COLOR 22 | const width = cat.percentage === 0 ? 1 : cat.percentage 23 | const display = cat.percentage === 0 ? "none" : "block" 24 | bars.innerHTML += 25 | `
26 |
${cat.percentage}%
27 | 28 | 29 | 30 |
` 31 | legend.innerHTML += 32 | `
33 |
34 | 35 | 36 | 37 |
38 |
${c}
39 |
` 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /scripts/lib/indental.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function indental (data, Type) { 4 | function formatLine (line) { 5 | const a = [] 6 | const h = {} 7 | for (const child of line.children) { 8 | if (child.key) { 9 | if (h[child.key]) { console.warn(`Redefined key: ${child.key}.`) } 10 | h[child.key] = child.value 11 | } else if (child.children.length === 0 && child.content) { 12 | a[a.length] = child.content 13 | } else { 14 | h[child.content.toUpperCase()] = formatLine(child) 15 | } 16 | } 17 | return a.length > 0 ? a : h 18 | } 19 | 20 | function makeLine (line) { 21 | return line.indexOf(' : ') > -1 ? { 22 | indent: line.search(/\S|$/), 23 | content: line.trim(), 24 | key: line.split(' : ')[0].trim().toUpperCase(), 25 | value: line.split(' : ')[1].trim() 26 | } : { 27 | indent: line.search(/\S|$/), 28 | content: line.trim(), 29 | children: [] 30 | } 31 | } 32 | 33 | function skipLine (line) { 34 | return line.trim() !== '' && line.trim().substr(0, 1) !== ';' && line.trim().slice(-1) !== '`' 35 | } 36 | 37 | const lines = data.split('\n').filter(skipLine).map(makeLine) 38 | 39 | // Assoc 40 | const stack = {} 41 | for (const line of lines) { 42 | const target = stack[line.indent - 2] 43 | if (target) { target.children.push(line) } 44 | stack[line.indent] = line 45 | } 46 | 47 | // Format 48 | const h = {} 49 | for (const id in lines) { 50 | const line = lines[id] 51 | if (line.indent > 0) { continue } 52 | const key = line.content.toUpperCase() 53 | if (h[key]) { console.warn(`Redefined key: ${key}, line ${id}.`) } 54 | h[key] = Type ? new Type(key, formatLine(line)) : formatLine(line) 55 | } 56 | return h 57 | } 58 | 59 | // module.exports = indental 60 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | Logbook 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aliceffekt@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /scripts/interface/balance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function Balance () { 4 | const balance = document.createElement('section') 5 | balance.id = 'balance' 6 | const header = document.createElement('header') 7 | const bars = document.createElement('section') 8 | bars.id = 'bars' 9 | const footer = document.createElement('footer') 10 | 11 | const dailyDetails = document.createElement('section') 12 | dailyDetails.id = 'dailyDetails' 13 | 14 | this.install = function (host) { 15 | balance.appendChild(header) 16 | balance.appendChild(bars) 17 | balance.appendChild(footer) 18 | host.appendChild(balance) 19 | } 20 | 21 | this.update = function (project = null) { 22 | this.createBars(project) 23 | } 24 | 25 | this.createBars = function (project) { 26 | const today = document.createElement('span') 27 | today.id = 'today' 28 | 29 | header.innerHTML = 30 | `

Logs

` 31 | 32 | // first & last days date 33 | const dur = database.stats.filter.days 34 | const to = toTimeStamp(database.stats.filter.lastEntry) 35 | let from = to 36 | const width = 100 / dur 37 | 38 | for (let i = dur; i > 0 ; i--) { 39 | const bar = document.createElement('a') 40 | const day = new Date(to) 41 | day.setDate(to.getDate() - i + 1) 42 | const date = convertDateToDB(day) 43 | if (i == dur) from = date 44 | 45 | bar.href = `#${date}` 46 | bar.id = date 47 | bar.style = `width: ${width}%;` 48 | bar.title = date 49 | bars.appendChild(bar) 50 | 51 | // assemble daily overview 52 | const title = {} 53 | const cats = database.categories 54 | if (database.days[date] && i <= dur) { 55 | if (project) { 56 | //get project 57 | if (!database.days[date].projects[project]) continue 58 | const day = database.days[date] 59 | const cat = database.projects[project].CAT 60 | const hours = day.projects[project].hours 61 | const percent = parseFloat((hours * 100 / 24).toFixed(1)) 62 | const color = database.categories[cat].COLOR 63 | bar.innerHTML = this.addSegment(percent, color) 64 | bar.title += ` / ${dayNames[toTimeStamp(date).getDay()]} / ${database.days[date].projects[project].hours}h / ${database.days[date].projects[project].count}e` 65 | } else { 66 | // get categories 67 | for (const c in cats) { 68 | const cat = database.days[date].categories[c] 69 | if (cat && cat.percentage > 0) { 70 | bar.innerHTML += this.addSegment(Math.ceil(cat.percentage), database.categories[c].COLOR) 71 | title[c] = `\n${c}: ${cat.percentage}% / ${cat.hours}h` 72 | } 73 | } 74 | bar.title += ` / ${dayNames[toTimeStamp(date).getDay()]} / ${database.days[date].entries.length}e` 75 | } 76 | for (const t in title) bar.title += title[t] 77 | } 78 | 79 | // mouse events 80 | bar.addEventListener("mouseover", function( event ) { 81 | let el = event.target 82 | while (el.parentElement.id !== 'bars') el = el.parentElement 83 | today.innerHTML = `${el.id}` 84 | }, false); 85 | } 86 | 87 | const firstDateContainer = document.createElement('span') 88 | const lastDateContainer = document.createElement('span') 89 | firstDateContainer.id = 'firstDate' 90 | lastDateContainer.id = 'lastDate' 91 | firstDateContainer.innerHTML += from 92 | lastDateContainer.innerHTML += convertDateToDB(to) 93 | 94 | footer.appendChild(firstDateContainer) 95 | footer.appendChild(today) 96 | footer.appendChild(lastDateContainer) 97 | } 98 | 99 | this.addSegment = function (height, color) { 100 | const html = 101 | ` 102 | 103 | ` 104 | return html 105 | } 106 | 107 | this.diff = function (start, end) { 108 | return Math.round((start - end)/60/60/1000 * 2) * 0.5 109 | } 110 | } -------------------------------------------------------------------------------- /scripts/lib/helpers.js: -------------------------------------------------------------------------------- 1 | // Transforms 2 | 3 | String.prototype.toTitleCase = function () { return this.toLowerCase().split(' ').map((s) => s.charAt(0).toUpperCase() + s.substring(1)).join(' ') } 4 | String.prototype.toUrl = function () { return this.toLowerCase().replace(/ /g, '+').replace(/[^0-9a-z\(\)\+\:\-\.\/\~]/gi, '').trim() } 5 | String.prototype.toEntities = function () { return this.replace(/[\u00A0-\u9999<>\&]/gim, function (i) { return `&#${i.charCodeAt(0)}` }) } 6 | String.prototype.toAlpha = function () { return this.replace(/[^a-z ]/gi, '').trim() } 7 | String.prototype.toAlphanum = function () { return this.replace(/[^0-9a-z ]/gi, '') } 8 | String.prototype.isAlphanum = function () { return !!this.match(/^[A-Za-z0-9 ]+$/) } 9 | String.prototype.toLink = function (name, cl) { return this.indexOf('(') === 0 ? this.toReplLink(name, cl) : this.indexOf('//') > -1 ? this.toExternalLink(name, cl) : this.toLocalLink(name, cl) } 10 | String.prototype.toLocalLink = function (name, cl = '') { return `${name || this}` } 11 | String.prototype.toExternalLink = function (name, cl = '') { return `${name || this}` } 12 | String.prototype.toReplLink = function (name, cl = '') { return `${name || this}` } 13 | String.prototype.stripHTML = function () { return this.replace(/<(?:.|\n)*?>/gm, '') } 14 | String.prototype.replaceAll = function (search, replacement) { return `${this}`.split(search).join(replacement) } 15 | String.prototype.isUrl = function () { return this.substr(0, 4) === 'http' } 16 | String.prototype.insert = function (s, i) { return [this.slice(0, i), s, this.slice(i)].join('') } 17 | String.prototype.toPath = function () { return this.toLowerCase().replace(/ /g, '.').replace(/[^0-9a-z\.]/gi, '').trim() } 18 | 19 | const convertDateToDB = function(date) { 20 | const zero = "0" 21 | const yy = date.getFullYear().toString().slice(2,4) 22 | let mm = date.getMonth() + 1 23 | mm = mm < 10 ? zero.concat(mm.toString()) : mm.toString() 24 | let dd = date.getDate() 25 | dd = dd < 10 ? zero.concat(dd.toString()) : dd.toString() 26 | return yy.concat(mm).concat(dd) 27 | } 28 | 29 | const toTimeStamp = function(date, time = '0000') { 30 | const year = parseInt("20" + date.slice(0,2)) 31 | const month = parseInt(date.slice(2,4),10) - 1 32 | const day = parseInt(date.slice(4,6),10) 33 | const hour = parseInt(time.slice(0,2),10) 34 | const minute = parseInt(time.slice(2,4),10) 35 | const d = new Date(year,month,day,hour,minute) 36 | return d 37 | } 38 | 39 | const prettyDate = function(timeStamp, long = false) { 40 | const d = new Date(timeStamp) 41 | const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] 42 | const day = d.getDate() 43 | const month = long ? months[d.getMonth()] : months[d.getMonth()].slice(0,3).concat('.') 44 | const year = d.getFullYear() 45 | const date = `${day}. ${month} ${year}` 46 | return date 47 | } 48 | 49 | const getToday = function() { 50 | const today = new Date() 51 | return convertDateToDB(today) 52 | } 53 | 54 | const longestString = function(strings) { 55 | let longest = strings[0].toString().length 56 | for (const id in strings) { 57 | const length = strings[id].toString().length 58 | if (length > longest) longest = length 59 | } 60 | return longest 61 | } 62 | 63 | const fillWithChar = function(oldLength, newLength, char) { 64 | let chars = "" 65 | while (oldLength < newLength) { 66 | chars += char 67 | oldLength++ 68 | } 69 | return chars 70 | } 71 | 72 | const leadingZero = function(number, zeros = 1) { 73 | const one = "1" 74 | let leading = "" 75 | let counter = 0 76 | while (counter < zeros) { 77 | leading = leading.concat("0") 78 | counter++ 79 | } 80 | return number < parseInt(one.concat(leading)) ? leading.concat(number.toString()) : number.toString() 81 | } 82 | 83 | const getDuration = function(start, end) { 84 | const staHrs = leadingZero(start.getHours()) 85 | const staMin = leadingZero(start.getMinutes()) 86 | const endHrs = leadingZero(end.getHours()) 87 | const endMin = leadingZero(end.getMinutes()) 88 | // console.info(`Start: ${convertDateToDB(start)} ${staHrs}:${staMin} - End: ${convertDateToDB(end)} ${endHrs}:${endMin} - Duration: ${(end - start)/1000/60/60}`) 89 | return (end - start)/1000/60/60 90 | } 91 | -------------------------------------------------------------------------------- /scripts/day.js: -------------------------------------------------------------------------------- 1 | 'const strict' 2 | 3 | function Day (date) { 4 | this.date = date 5 | this.entries = [] 6 | this.categories = {} 7 | this.projects = {} 8 | this.firstEntry = {} 9 | this.timeline = [] 10 | this.trackedHours = 0 11 | 12 | this.update = function () { 13 | // parse entries 14 | for (const e in this.entries) { 15 | const entry = this.entries[e] 16 | let hours = 0 17 | 18 | // store the entries timestamp 19 | entry.timeStamp = toTimeStamp(this.date, entry.time) 20 | 21 | // pick the previous entries timestamp (midnight if it's the first entry) 22 | const prevEntry = e == 0 ? { timeStamp : toTimeStamp(entry.date) } : this.entries[e-1] 23 | let project = e == 0 ? this.firstEntry.project : prevEntry.project 24 | let tags = e == 0 ? this.firstEntry.tags : prevEntry.tags 25 | let score = e == 0 ? this.firstEntry.scr : prevEntry.scr 26 | let time = e == 0 ? "0000" : prevEntry.time 27 | 28 | if (prevEntry.project === "!") continue 29 | hours = (entry.timeStamp - prevEntry.timeStamp) / (60 * 60 * 1000) 30 | if (hours > 24) console.warn(`Logging error detected in logbook at ${this.date}: ${entry.time}`) 31 | else if (project) { 32 | this.addHours(hours, project, tags) 33 | this.addTimeLineEntry(time, hours, project, tags) 34 | } 35 | if (score && database.scores) this.scores.add(score, time) 36 | 37 | // calculate hours and add the to the project 38 | if (e == this.entries.length - 1) { 39 | // add the days remaining hours to the last entry 40 | const nextDay = { timeStamp : toTimeStamp(this.date)} 41 | nextDay.timeStamp.setDate(nextDay.timeStamp.getDate() + 1) 42 | 43 | // check if the next day is in the future 44 | let diff = nextDay.timeStamp - entry.timeStamp 45 | const isFuture = new Date() - nextDay.timeStamp < 0 ? true : false 46 | if (isFuture) diff = new Date() - entry.timeStamp 47 | 48 | // calculate the hours 49 | hours = parseFloat((diff / (60 * 60 * 1000)).toFixed(1)) 50 | this.addHours(hours, entry.project, entry.tags) 51 | this.addTimeLineEntry(entry.time, hours, entry.project, entry.tags) 52 | } 53 | 54 | // end at current time 55 | if (new Date() - entry.timeStamp < 0) break 56 | } 57 | // update stats 58 | database.stats.total.hours += this.trackedHours 59 | 60 | // calculate category percentages 61 | for (const c in this.categories) { 62 | const cat = this.categories[c] 63 | cat.percentage = parseFloat((cat.hours * 100 / 24).toFixed(1)) 64 | } 65 | 66 | // calculate score averages 67 | this.scores.getAverages() 68 | } 69 | 70 | this.scores = { 71 | scores : {}, 72 | average : {}, 73 | add : function (score, time) { 74 | for (let i = 0; i < score.length; i++) { 75 | const s = parseInt(score[i]) 76 | if (!database.scores[i]) { console.warn(`Can't map score to category. Add more categories to scores.ndtl`); continue } 77 | const cat = database.scores[i].category 78 | if (!this.scores[cat]) this.scores[cat] = [] 79 | if (!isNaN(s)) { 80 | const entry = { score: 10 - s - 5, time : time } 81 | this.scores[cat].push(entry) 82 | } 83 | } 84 | }, 85 | getAverages : function () { 86 | for (const s in this.scores) { 87 | const cat = this.scores[s] 88 | const avrg = parseFloat((this.getSum(cat) / cat.length).toFixed(2)) 89 | this.average[s] = isNaN(avrg) ? 0 : avrg 90 | } 91 | }, 92 | getSum : function (array) { 93 | let sum = 0 94 | let i = array.length 95 | while (i--) sum += array[i].score 96 | return sum 97 | } 98 | } 99 | 100 | this.addHours = function (hours, project, tags, time) { 101 | // check if project exists 102 | project = project.toUpperCase() 103 | const prj = database.projects[project] 104 | if (prj) { 105 | const cat = prj.CAT 106 | // add hours to project 107 | if (!this.projects[project]) this.projects[project] = { hours : 0, count: 0, tags : {}, NAME : project, CAT : cat, } 108 | this.projects[project].hours += hours 109 | this.projects[project].count += 1 110 | // add tag 111 | const tag = this.parseTag(tags) 112 | if (tag) this.addTag(tag, hours, this.projects[project].tags) 113 | // add hours to categories 114 | if (!this.categories[cat]) this.categories[cat] = { hours : 0, percentage : 0 } 115 | this.categories[cat].hours += hours 116 | // add hours to checkSum 117 | this.trackedHours += hours 118 | } else console.warn(`Project ${project} on day ${this.date} doesn't exist in projects database.`) 119 | } 120 | 121 | this.addTimeLineEntry = function (time, hours, project, tags) { 122 | // add data to timeline 123 | const data = { time : time, duration : hours, project : project, tags : tags } 124 | this.timeline.push(data) 125 | } 126 | 127 | this.addTag = function (tag, hours, object) { 128 | if (!object[tag]) object[tag] = { count : 0, hours : 0 } 129 | object[tag].count += 1 130 | object[tag].hours += hours 131 | } 132 | 133 | this.parseTag = function (string) { 134 | let tag = string.split('#')[1] 135 | if (tag) tag = tag.split(' ')[0] 136 | else return null 137 | return tag 138 | } 139 | } -------------------------------------------------------------------------------- /links/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #29272b; 3 | --f_inv: #43423E; 4 | --b_high: #E8E9E2; 5 | --b_med: #C2C1BB; 6 | --b_low: #4B4B49; 7 | --b_inv: #0C0B0; 8 | --rounded: 2px; 9 | } 10 | * { margin:0; padding:0; border:0; outline:0; border-spacing:0; text-decoration:none; font-weight:inherit; font-style:inherit; color:inherit; font-size:100%; font-family:inherit; vertical-align:baseline; list-style:none; border-collapse:collapse; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale } 11 | *:focus { outline: none} 12 | body { background:var(--background); color:white; padding:16px; overflow-x: hidden; transition: opacity 150ms; opacity: 1 !important; font-family: var(--mono); font-size: 12px } 13 | h1 { font-size: 32px; font-weight: bold; text-transform: uppercase; border-bottom: 2px solid var(--b_med); margin-bottom: 8px; } 14 | h2 { font-size: 24px; font-weight: bold; text-transform: uppercase; } 15 | h3 { font-size: 16px; font-weight: bold; text-transform: uppercase; } 16 | p { margin-bottom: 10px; line-height: 2; } 17 | b { font-weight:bold } 18 | i { font-style:italic } 19 | a { cursor: pointer } 20 | ul { display: block } 21 | ul li { display: block } 22 | hr { clear:both } 23 | code { display: block; white-space: pre } 24 | svg { stroke-width: 10; stroke: white; stroke-linecap: round } 25 | small { font-size: 80%; } 26 | 27 | #logbook section { padding: 8px; margin-bottom: 32px; } 28 | #logbook section section { padding: 0; margin: 0; } 29 | #logbook #stats { display: flex; justify-content: space-between; align-items: baseline; } 30 | #logbook #overview #total #bars { display: flex; width:100%; } 31 | #logbook #overview section { margin-top: 16px; } 32 | #logbook #overview .bar { margin-right: 4px; } 33 | #logbook #overview .bar:last-child { margin:0; } 34 | #logbook #overview .bar svg { height: 8px; border-radius: var(--rounded) } 35 | 36 | #logbook #legend { display: flex; justify-content: flex-start; } 37 | #logbook #legend .item { display: flex; align-items: stretch; margin-right: 8px; padding: 0 4px 0 4px; border-radius: 2px; } 38 | #logbook #legend a.item:hover { background-color: var(--b_low); } 39 | #logbook #legend .box { width: 8px; height: 8px; margin-right: 4px; } 40 | #logbook #legend .box svg { border-radius: var(--rounded); } 41 | #logbook #legend .cat { text-transform: uppercase; } 42 | 43 | #logbook #balance header { margin-bottom: 8px; } 44 | #logbook #balance #bars { display:flex; height: 128px; } 45 | #logbook #balance #bars a { display: flex; flex-direction: column; justify-content: flex-end; margin-right: 2px; background-color: var(--b_low); border-radius: var(--rounded); box-sizing: border-box; } 46 | #logbook #balance #bars a:hover { background-color: white; } 47 | #logbook #balance #bars a:hover { opacity: 0.5; } 48 | #logbook #balance #bars a svg:first-child { border-radius: var(--rounded) var(--rounded) 0px 0px; } 49 | #logbook #balance #bars a svg:last-child { border-radius: 0px 0px var(--rounded) var(--rounded); } 50 | #logbook #balance section { margin-top: 16px; } 51 | #logbook #balance section>section { margin: 0; } 52 | #logbook #balance #past figure:last-child { margin: 0; } 53 | #logbook #balance footer { margin-top: 4px; border-top: 2px solid var(--b_low); display: flex; justify-content: space-between; color: var(--b_med); } 54 | 55 | #logbook #projects #categories { display: flex; } 56 | #logbook #projects #categories section { width: 100%; margin-right: 32px; } 57 | #logbook #projects #categories section:last-child { margin-right: 0; } 58 | #logbook #projects #categories figure { margin: 8px 0px 4px 0px; padding-bottom: 8px; border-bottom: 1px solid var(--b_low); } 59 | 60 | #logbook #graph { height: 128px; position: relative; background-color: var(--b_low); border-radius: var(--rounded); margin-bottom: 4px; } 61 | #logbook #graph svg { position: absolute; top: 0; left: 0; } 62 | #logbook #graph #bars { display: flex; } 63 | #logbook #graph #bars a { height: 128px; margin-right: 2px; z-index: 1; } 64 | #logbook #graph #bars a:last-child { margin: 0; border: 0; } 65 | #logbook #graph #bars a:hover { border-right: 3px solid var(--b_high); background-color: rgba(255,255,255,0.1); } 66 | 67 | #logbook #datepicker { display: flex; } 68 | #logbook #datepicker form { font-size: 16px; font-weight: bold; font-family:inherit; } 69 | #logbook #datepicker form select { color: var(--b_high); background-color: var(--background); width: 100%; box-sizing: border-box; border-radius: 0; -moz-appearance: none; -webkit-appearance: none; appearance: none; } 70 | #logbook #datepicker form select:hover { background-color: var(--b_low); } 71 | #logbook #datepicker form select option { font-family: monospace; font-weight: normal; } 72 | 73 | #logbook #timeline #ruler { width: 100%; height:16px; margin-bottom:4px; display: flex; align-items: center; border-bottom: 2px solid var(--b_low); } 74 | #logbook #timeline .hour { height: 100%; position: relative; display: flex; padding-left:2px; color: var(--b_low); border-left: 2px solid var(--b_low); } 75 | #logbook #timeline .hour:last-child { border-right: 2px solid var(--b_low); } 76 | #logbook #timeline #label { position: absolute; top:0; left:2px; color: var(--b_med); } 77 | #logbook #timeline #items { width: 100%; height:16px; margin-bottom:2px; display: flex; justify-content: flex-start; background-color:var(--b_low); border-radius: var(--rounded); } 78 | #logbook #timeline .item { height: 100%; margin-right: 2px; border-radius: var(--rounded); } 79 | #logbook #timeline .item:last-child { margin: 0; } 80 | 81 | .empty { fill: var(--b_low); stroke: var(--b_low); color: var(--b_low); } 82 | .stroke { stroke-width: 2px; stroke-linecap: round; stroke-linejoin: round; fill: none; } 83 | .lc { text-transform: lowercase; } 84 | .rounded { border-radius: var(--rounded); } 85 | .small { font-size: 80%; } -------------------------------------------------------------------------------- /scripts/interface/scores.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function Scores () { 4 | const height = 128 5 | const h = 9 6 | const center = h * 0.5 7 | const offset = 0.1 8 | this.isHome = true 9 | 10 | const scores = document.createElement('section') 11 | scores.id = 'scores' 12 | 13 | this.install = function (host) { 14 | host.appendChild(scores) 15 | } 16 | 17 | this.update = function () { 18 | this.isHome = logbook.interface.isHome 19 | 20 | scores.innerHTML = '' 21 | const header = document.createElement('header') 22 | header.innerHTML = `

Rhythm

` 23 | scores.appendChild(header) 24 | 25 | if (this.isHome) this.createGraph() 26 | else this.createDailyGraph() 27 | this.createLegend() 28 | } 29 | 30 | this.createGraph = function () { 31 | const graph = document.createElement('figure') 32 | graph.id = 'graph' 33 | 34 | const dur = database.stats.filter.days 35 | const width = 100 / dur 36 | let lines = '' 37 | 38 | const bars = document.createElement('div') 39 | bars.id = 'bars' 40 | 41 | this.points = [] 42 | 43 | const d = toTimeStamp(database.stats.filter.lastEntry) 44 | for (let i = 0; i <= dur; i++) { 45 | const x = i 46 | 47 | let date = new Date(d) 48 | date.setDate(d.getDate() - dur + i) 49 | const day = dayNames[date.getDay()] 50 | date = convertDateToDB(date) 51 | 52 | const bar = document.createElement('a') 53 | bar.href = `#${date}` 54 | bar.id = date 55 | bar.title = `${date} / ${day}` 56 | bar.style = `width: ${width}%;` 57 | if (i <= dur && i != 0) bars.appendChild(bar) 58 | 59 | // add segmentation 60 | lines += `` 61 | // get scores as position data 62 | for (const s in database.scores) { 63 | const cat = database.scores[s].category 64 | if (!this.points[s]) this.points[s] = '' 65 | // average scores over the filtered amount of days 66 | if (database.days[date]) { 67 | if (!database.days[date].scores.scores[cat]) continue 68 | const y = center - database.days[date].scores.average[cat] 69 | this.points[s] += `${x} ${y} ` 70 | bar.title += `\n${cat}: ${database.days[date].scores.average[cat]} / ${database.days[date].scores.scores[cat].length}e` 71 | } else { 72 | this.points[s] += `${x} ${center + offset * s} ` 73 | } 74 | } 75 | } 76 | const data = { height: height, dur : dur, h : h, center : center, points : this.points, lines : lines } 77 | const svg = this.createSVG(data) 78 | graph.appendChild(svg) 79 | graph.appendChild(bars) 80 | scores.appendChild(graph) 81 | } 82 | 83 | this.createDailyGraph = function () { 84 | const graph = document.createElement('figure') 85 | graph.id = 'graph' 86 | 87 | const dur = 24 88 | const width = 100 / dur 89 | let lines = '' 90 | 91 | this.points = [] 92 | this.day = database.days[page.url] 93 | 94 | const d = toTimeStamp(page.url) 95 | for (let x = 0; x <= dur; x++) lines += `` 96 | 97 | // get scores as position data 98 | for (const c in database.scores) { 99 | const cat = database.scores[c].category 100 | if (!this.points[c]) this.points[c] = '' 101 | if (!this.day.scores.scores[cat]) continue 102 | for (const s in this.day.scores.scores[cat]) { 103 | this.entry = this.day.scores.scores[cat][s] 104 | console.log(this.entry.time) 105 | this.x = 0 //this.entry.time 106 | this.y = center + this.entry.score 107 | this.points[s] += `${this.x} ${this.y} ` 108 | } 109 | } 110 | const data = { height: height, dur : dur, h : h, center : center, points : this.points, lines : lines } 111 | const svg = this.createSVG(data) 112 | graph.appendChild(svg) 113 | scores.appendChild(graph) 114 | } 115 | 116 | this.createSVG = function ( data ) { 117 | let polylines = '' 118 | for (const c in data.points) { 119 | polylines += `` 120 | } 121 | const svg = document.createElement('div') 122 | svg.id = 'svg' 123 | svg.innerHTML = 124 | ` 125 | ${data.lines} 126 | 127 | ${polylines} 128 | ` 129 | return svg 130 | } 131 | 132 | this.createLegend = function () { 133 | const legend = document.createElement('figure') 134 | legend.id = 'legend' 135 | 136 | for (let i = 0; i < database.scores.length; i++) { 137 | const c = database.scores[i] 138 | 139 | const item = document.createElement('a') 140 | item.className = 'item' 141 | item.id = c.category 142 | item.onclick = function() { logbook.interface.scores.toggle(c.category) } 143 | 144 | const box = document.createElement('div') 145 | box.className = 'box' 146 | box.innerHTML = 147 | ` 148 | 149 | ` 150 | 151 | const cat = document.createElement('div') 152 | cat.className = 'cat' 153 | cat.innerHTML = c.category.toUpperCase() 154 | 155 | item.appendChild(box) 156 | item.appendChild(cat) 157 | legend.appendChild(item) 158 | } 159 | scores.appendChild(legend) 160 | } 161 | 162 | this.toggle = function ( category ) { 163 | const rect = document.querySelector(`section#scores figure#legend a#${category} div.box svg rect`) 164 | const line = document.querySelector(`figure#graph div#svg svg polyline#${category}`) 165 | if (line.style.display === 'none') { 166 | line.style.display = 'unset' 167 | rect.style.fill = database.settings.SCORES[category] 168 | } else { 169 | line.style.display = 'none' 170 | rect.style.fill = 'var(--b_med)' 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /scripts/database.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function Database () { 4 | 5 | this.install = function () { 6 | database.settings = indental(DATABASE.settings) 7 | if (DATABASE.logs) database.logs = tablatal(DATABASE.logs) 8 | else this.error(`Couldn't find 'Logbook' database!`,`Please make sure there is a file named logbook.tbtl in the database folder that starts with 'DATABASE.logs ='`) 9 | 10 | // initialise categories & projects 11 | database.categories = {} 12 | for (const c in database.settings.CATEGORIES) database.categories[c] = { COLOR : database.settings.CATEGORIES[c] } 13 | database.projects = {} 14 | for (const p in database.settings.PROJECTS) { 15 | const project = database.settings.PROJECTS[p] 16 | const name = project.NAME == undefined ? p : project.NAME 17 | if (!project.CAT) this.error(`Missing category:`,`Project '${project}' has no category. Please update settings.ndtl.`) 18 | else if (!database.categories[project.CAT]) this.error(`Couldn't match category '${project.CAT}'`,`Project '${name}' has the category '${project.CAT}'.
This category doesn't exist in settings.ndtl. Please update the settings file.`) 19 | database.projects[p] = { CAT : project.CAT, NAME : name } 20 | } 21 | 22 | // initialise scores 23 | database.scores = [] 24 | for (const s in database.settings.SCORES) { 25 | const cat = { category : s, color : database.settings.SCORES[s] } 26 | database.scores.push(cat) 27 | } 28 | 29 | database.days = {} 30 | database.tags = {} 31 | database.stats = {} 32 | database.stats.total = { 33 | firstEntry : 0, 34 | lastEntry : 0, 35 | days : 0, 36 | hours : 0, 37 | entries : 0 38 | } 39 | } 40 | 41 | this.start = function () { 42 | 43 | // loop through logbook entries starting with the oldest entry 44 | const time = performance.now() 45 | const entries = database.logs 46 | const first = entries.length - 1 47 | let day 48 | 49 | for (let id = first; id >= 0; id--) { 50 | const entry = entries[id] 51 | if (id === first) { 52 | // first entry 53 | // check validity 54 | if (entry.date.charAt(0) === "-") { console.info(`First entry in Logs Database doesn't have a date!`); return } 55 | if (entry.project.charAt(0) === "-") { console.info(`First entry in Log doesn't have a project!`); return } 56 | database.stats.total.firstEntry = entry.date 57 | this.addDay(entry) 58 | day = database.days[entry.date] 59 | } else { 60 | const prevEntry = day.entries[day.entries.length - 1] 61 | const diff = toTimeStamp(prevEntry.date) - toTimeStamp(entry.date) 62 | // TODO: add check for proper times 63 | const isPrevDay = diff > 0 ? true : false 64 | const isSameDay = diff == 0 || entry.date.charAt(0) === "-" ? true : false 65 | if (isSameDay) day.entries.push(entry) 66 | else { 67 | day.update() 68 | this.addDay(entry) 69 | day = database.days[entry.date] 70 | if (!isPrevDay) day.firstEntry = prevEntry 71 | } 72 | } 73 | if (id == 0) { 74 | day.update() 75 | database.stats.total.lastEntry = day.date 76 | } 77 | // update stats 78 | database.stats.total.entries += 1 79 | } 80 | for (const cat in database.categories) database.categories[cat].percentage = parseFloat((database.categories[cat].hours * 100 / database.stats.hours).toFixed(2)) 81 | console.info(`Found ${database.stats.total.entries} entries in database, in ${(performance.now() - time).toFixed(2)}ms.`) 82 | } 83 | 84 | this.update = function (params) { 85 | 86 | const time = performance.now() 87 | database.stats.filter = { 88 | firstEntry : 0, 89 | lastEntry : 0, 90 | days : 0, 91 | hours : 0, 92 | entries : 0 93 | } 94 | 95 | // check date validity 96 | this.to = this.isValidDate(dates.to) ? convertDateToDB(toTimeStamp(dates.to.toString())) : convertDateToDB(new Date()) 97 | this.from = this.isValidDate(dates.from) ? convertDateToDB(toTimeStamp(dates.from.toString())) : this.shiftDate(this.to, -31) 98 | // odd date combincation handling 99 | if (parseInt(this.to) < parseInt(this.from)) this.from = this.shiftDate(this.to, -30) 100 | 101 | dates.from = this.from 102 | dates.to = this.to 103 | 104 | 105 | const from = toTimeStamp(this.from) 106 | const to = toTimeStamp(this.to) 107 | const dayCount = Math.round((to - from) / (60 * 60 * 1000 * 24)) 108 | const days = {} 109 | 110 | // update stats 111 | database.stats.filter.firstEntry = this.from 112 | database.stats.filter.lastEntry = this.to 113 | database.stats.filter.entries = 0 114 | database.stats.filter.hours = 0 115 | database.stats.filter.days = dayCount + 1 116 | 117 | // reset categories 118 | for (const c in database.categories) { database.categories[c].hours = 0; database.categories[c].percentage = 0 } 119 | 120 | //reset projects 121 | for (const p in database.projects) { database.projects[p].hours = 0; database.projects[p].count = 0; database.projects[p].tags = {} } 122 | 123 | // reset tags 124 | for (const t in database.tags) { database.tags[t].hours = 0; database.tags[t].count = 0; } 125 | 126 | // get data between from - to dates 127 | for (let i = 0; i <= dayCount; i++) { 128 | let day = new Date(to) 129 | day.setDate(to.getDate() - i) 130 | day = convertDateToDB(day) 131 | day = database.days[day] 132 | if (!day) continue 133 | 134 | // add hours to projects 135 | this.filterEntries(day) 136 | 137 | // add hours to categories 138 | for (const c in day.categories) { 139 | const cat = database.categories[c] 140 | cat.hours += day.categories[c].hours 141 | } 142 | 143 | // add hours to stats 144 | database.stats.filter.hours += day.trackedHours 145 | database.stats.filter.entries += day.entries.length 146 | } 147 | 148 | // calculate category percentages 149 | for (const c in database.categories) { 150 | const cat = database.categories[c] 151 | if (!cat.hours) cat.hours = 0 152 | cat.percentage = parseFloat((cat.hours * 100 / database.stats.filter.hours).toFixed(2)) 153 | if (isNaN(cat.percentage)) cat.percentage = 0 154 | } 155 | console.info(`Filtered ${database.stats.filter.entries} entries from ${this.from} to ${this.to}, in ${(performance.now() - time).toFixed(2)}ms.`) 156 | console.info(database) 157 | } 158 | 159 | this.isValidDate = function (date, filtered = false) { 160 | if (date === undefined || isNaN(date) || date === 0) return false 161 | return true 162 | } 163 | 164 | this.addDay = function (entry) { 165 | database.days[entry.date] = new Day(entry.date) 166 | database.days[entry.date].entries.push(entry) 167 | database.stats.total.days += 1 168 | } 169 | 170 | this.filterEntries = function (day) { 171 | if (!day.projects) return 172 | for (const p in day.projects) { 173 | if (database.projects[p]) { 174 | const project = database.projects[p] 175 | 176 | if (!project.hours) project.hours = 0 177 | if (!project.count) project.count = 0 178 | if (!project.firstEntry) project.firstEntry = day.date 179 | if (!project.tags) project.tags = {} 180 | 181 | // update the project hours 182 | project.hours += day.projects[p].hours 183 | project.count += day.projects[p].count 184 | 185 | // update the project tags 186 | this.filterTags(day, p) 187 | } else console.warn(`Couldn't find project in Projects Database at entry ${day.date}: ${p}`) 188 | } 189 | } 190 | 191 | this.shiftDate = function (date, amount, format = true) { 192 | this.dateOld = toTimeStamp(date) 193 | this.dateNew = new Date(this.dateOld) 194 | this.dateNew.setDate(this.dateOld.getDate() + amount) 195 | return format ? convertDateToDB(this.dateNew) : this.date 196 | } 197 | 198 | this.filterTags = function (day, project) { 199 | const p = project 200 | for (const t in day.projects[p].tags) { 201 | const tag = day.projects[p].tags[t] 202 | // add tags to project 203 | if (!database.projects[p].tags[t]) database.projects[p].tags[t] = { count : 0, hours : 0 } 204 | const filterTag = database.projects[p].tags[t] 205 | filterTag.hours += tag.hours 206 | filterTag.count += 1 207 | // add totals to tags 208 | if (!database.tags[t]) database.tags[t] = { count : 0, hours : 0 } 209 | const totalTag = database.tags[t] 210 | totalTag.hours += tag.hours 211 | totalTag.count += 1 212 | // track categories 213 | if (!totalTag.categories) totalTag.categories = {} 214 | if (!totalTag.categories[database.projects[p].CAT]) totalTag.categories[database.projects[p].CAT] = { hours : 0, count : 0} 215 | const catTag = totalTag.categories[database.projects[p].CAT] 216 | catTag.hours += tag.hours 217 | catTag.count += 1 218 | } 219 | } 220 | 221 | this.error = function (title, message) { 222 | const error = {} 223 | error.title = title 224 | error.text = message 225 | logbook.errors.push(error) 226 | } 227 | } -------------------------------------------------------------------------------- /logbook.tbtl: -------------------------------------------------------------------------------- 1 | DATABASE.logs = ` 2 | DATE TIME SCR PROJECT TAGS 3 | ------ 2100 --6 Sleep --- 4 | ------ 1900 --- Personal #Idle 5 | ------ 1730 577 Personal #Read Book XYZ 6 | ------ 1700 486 Personal #Commute Home 7 | ------ 0900 323 Company #Code Working on XYZ 8 | ------ 0830 --- Company #Commute 9 | ------ 0730 129 Cooking #Breakfast Porrige 10 | 200130 0700 543 Household #Routines 11 | ------ 2100 --- Sleep --- 12 | ------ 1900 --- Personal #Idle 13 | ------ 1730 513 Personal #Read Book XYZ 14 | ------ 1700 412 Personal #Commute Home 15 | ------ 0900 964 Company #Code Working on XYZ 16 | ------ 0830 --- Company #Commute 17 | ------ 0730 423 Cooking #Breakfast Porrige 18 | 200129 0700 523 Household #Routines 19 | ------ 2100 121 Sleep --- 20 | ------ 1900 --- Personal #Idle 21 | ------ 1730 574 Personal #Read Book XYZ 22 | ------ 1700 455 Personal #Commute Home 23 | ------ 0900 324 Company #Code Working on XYZ 24 | ------ 0830 --- Company #Commute 25 | ------ 0730 423 Cooking #Breakfast Porrige 26 | 200128 0700 543 Household #Routines 27 | ------ 2100 --- Sleep --- 28 | ------ 1900 --- Personal #Idle 29 | ------ 1730 574 Personal #Read Book XYZ 30 | ------ 1700 455 Personal #Commute Home 31 | ------ 0900 324 Company #Code Working on XYZ 32 | ------ 0830 --- Company #Commute 33 | ------ 0730 423 Cooking #Breakfast Porrige 34 | 200127 0700 543 Household #Routines 35 | ------ 2100 --- Sleep --- 36 | ------ 1900 --- Personal #Idle 37 | ------ 1730 574 Personal #Read Book XYZ 38 | ------ 1700 455 Personal #Commute Home 39 | ------ 0900 324 Company #Code Working on XYZ 40 | ------ 0830 --- Company #Commute 41 | ------ 0730 423 Cooking #Breakfast Porrige 42 | 200126 0700 754 Household #Routines 43 | ------ 2100 422 Sleep --- 44 | ------ 1900 --- Personal #Idle 45 | ------ 1730 574 Personal #Read Book XYZ 46 | ------ 1700 465 Personal #Commute Home 47 | ------ 0900 324 Company #Code Working on XYZ 48 | ------ 0830 --- Company #Commute 49 | ------ 0730 423 Cooking #Breakfast Porrige 50 | 200125 0700 753 Household #Routines 51 | ------ 2100 --- Sleep --- 52 | ------ 1900 2-- Personal #Idle 53 | ------ 1730 274 Personal #Read Book XYZ 54 | ------ 1700 255 Personal #Commute Home 55 | ------ 0900 324 Company #Code Working on XYZ 56 | ------ 0830 423 Company #Commute 57 | ------ 0730 543 Cooking #Breakfast Porrige 58 | 200124 0700 --- Household #Routines 59 | ------ 2100 --- Sleep --- 60 | ------ 1900 574 Personal #Idle 61 | ------ 1730 455 Personal #Read Book XYZ 62 | ------ 1700 324 Personal #Commute Home 63 | ------ 0900 --- Company #Code Working on XYZ 64 | ------ 0830 423 Company #Commute 65 | ------ 0730 543 Cooking #Breakfast Porrige 66 | 200123 0700 --- Household #Routines 67 | ------ 2100 --- Sleep --- 68 | ------ 1900 574 Personal #Idle 69 | ------ 1730 455 Personal #Read Book XYZ 70 | ------ 1700 324 Personal #Commute Home 71 | ------ 0900 --- Company #Code Working on XYZ 72 | ------ 0830 423 Company #Commute 73 | ------ 0730 754 Cooking #Breakfast Porrige 74 | 200122 0700 --- Household #Routines 75 | ------ 2100 --- Sleep --- 76 | ------ 1900 574 Personal #Idle 77 | ------ 1730 465 Personal #Read Book XYZ 78 | ------ 1700 324 Personal #Commute Home 79 | ------ 0900 --- Company #Code Working on XYZ 80 | ------ 0830 423 Company #Commute 81 | ------ 0730 753 Cooking #Breakfast Porrige 82 | 200121 0700 --- Household #Routines 83 | ------ 2100 2-- Sleep --- 84 | ------ 1900 274 Personal #Idle 85 | ------ 1730 255 Personal #Read Book XYZ 86 | ------ 1700 455 Personal #Commute Home 87 | ------ 0900 324 Company #Code Working on XYZ 88 | ------ 0830 --- Company #Commute 89 | ------ 0730 423 Cooking #Breakfast Porrige 90 | 200120 0700 543 Household #Routines 91 | ------ 2100 --- Sleep --- 92 | ------ 1900 --- Personal #Idle 93 | ------ 1730 574 Personal #Read Book XYZ 94 | ------ 1700 455 Personal #Commute Home 95 | ------ 0900 324 Company #Code Working on XYZ 96 | ------ 0830 423 Company #Commute 97 | ------ 0730 543 Cooking #Breakfast Porrige 98 | 200119 0700 --- Household #Routines 99 | ------ 2100 --- Sleep --- 100 | ------ 1900 574 Personal #Idle 101 | ------ 1730 455 Personal #Read Book XYZ 102 | ------ 1700 324 Personal #Commute Home 103 | ------ 0900 --- Company #Code Working on XYZ 104 | ------ 0830 423 Company #Commute 105 | ------ 0730 543 Cooking #Breakfast Porrige 106 | 200118 0700 --- Household #Routines 107 | ------ 2100 --- Sleep --- 108 | ------ 1900 574 Personal #Idle 109 | ------ 1730 455 Personal #Read Book XYZ 110 | ------ 1700 324 Personal #Commute Home 111 | ------ 0900 --- Company #Code Working on XYZ 112 | ------ 0830 423 Company #Commute 113 | ------ 0730 754 Cooking #Breakfast Porrige 114 | 200117 0700 --- Household #Routines 115 | ------ 2100 --- Sleep --- 116 | ------ 1900 574 Personal #Idle 117 | ------ 1730 465 Personal #Read Book XYZ 118 | ------ 1700 324 Personal #Commute Home 119 | ------ 0900 423 Company #Code Working on XYZ 120 | ------ 0830 543 Company #Commute 121 | ------ 0730 --- Cooking #Breakfast Porrige 122 | 200116 0700 --- Household #Routines 123 | ------ 2100 574 Sleep --- 124 | ------ 1900 455 Personal #Idle 125 | ------ 1730 324 Personal #Read Book XYZ 126 | ------ 1700 --- Personal #Commute Home 127 | ------ 0900 423 Company #Code Working on XYZ 128 | ------ 0830 543 Company #Commute 129 | ------ 0730 --- Cooking #Breakfast Porrige 130 | 200115 0700 --- Household #Routines 131 | ------ 2100 574 Sleep --- 132 | ------ 1900 455 Personal #Idle 133 | ------ 1730 324 Personal #Read Book XYZ 134 | ------ 1700 423 Personal #Commute Home 135 | ------ 0900 543 Company #Code Working on XYZ 136 | ------ 0830 --- Company #Commute 137 | ------ 0730 --- Cooking #Breakfast Porrige 138 | 200114 0700 --- Household #Routines 139 | ------ 2100 455 Sleep --- 140 | ------ 1900 324 Personal #Idle 141 | ------ 1730 --- Personal #Read Book XYZ 142 | ------ 1700 423 Personal #Commute Home 143 | ------ 0900 543 Company #Code Working on XYZ 144 | ------ 0830 --- Company #Commute 145 | ------ 0730 --- Cooking #Breakfast Porrige 146 | 200113 0700 --- Household #Routines 147 | ------ 2100 455 Sleep --- 148 | ------ 1900 324 Personal #Idle 149 | ------ 1730 --- Personal #Read Book XYZ 150 | ------ 1700 423 Personal #Commute Home 151 | ------ 0900 543 Company #Code Working on XYZ 152 | ------ 0830 --- Company #Commute 153 | ------ 0730 --- Cooking #Breakfast Porrige 154 | 200112 0700 --- Household #Routines 155 | ------ 2100 455 Sleep --- 156 | ------ 1900 324 Personal #Idle 157 | ------ 1730 --- Personal #Read Book XYZ 158 | ------ 1700 423 Personal #Commute Home 159 | ------ 0900 423 Company #Code Working on XYZ 160 | ------ 0830 543 Company #Commute 161 | ------ 0730 --- Cooking #Breakfast Porrige 162 | 200111 0700 --- Household #Routines 163 | ------ 2300 574 Sleep --- 164 | ------ 1900 455 Personal #Idle 165 | ------ 1730 324 Personal #Read Book XYZ 166 | ------ 1700 --- Personal #Commute Home 167 | ------ 0930 423 Company #Code Working on XYZ 168 | ------ 0830 543 Company #Commute 169 | ------ 0730 --- Cooking #Breakfast Porrige 170 | 200110 0700 423 Household #Routines 171 | ------ 2330 423 Sleep --- 172 | ------ 1900 543 Personal #Idle 173 | ------ 1730 --- Personal #Read Book XYZ 174 | ------ 1400 --- Personal #Commute Home 175 | ------ 0900 --- Company #Code Working on XYZ 176 | ------ 0830 574 Company #Commute 177 | ------ 0730 455 Cooking #Breakfast Porrige 178 | 200109 0700 324 Household #Routines 179 | ------ 2200 423 Sleep --- 180 | ------ 1900 543 Personal #Idle 181 | ------ 1730 --- Personal #Read Book XYZ 182 | ------ 1200 --- Personal #Commute Home 183 | ------ 0930 --- Company #Code Working on XYZ 184 | ------ 0830 574 Company #Commute 185 | ------ 0730 423 Cooking #Breakfast Porrige 186 | 200108 0700 543 Household #Routines 187 | ------ 2130 --- Sleep --- 188 | ------ 1900 --- Personal #Idle 189 | ------ 1730 574 Personal #Read Book XYZ 190 | ------ 1730 455 Personal #Commute Home 191 | ------ 0900 324 Company #Code Working on XYZ 192 | ------ 0830 --- Company #Commute 193 | ------ 0730 423 Cooking #Breakfast Porrige 194 | 200107 0700 543 Household #Routines 195 | ------ 2100 543 Sleep --- 196 | ------ 1900 --- Personal #Idle 197 | ------ 1730 --- Personal #Read Book XYZ 198 | ------ 1700 --- Personal #Commute Home 199 | ------ 0900 574 Company #Code Working on XYZ 200 | ------ 0830 455 Company #Commute 201 | ------ 0730 423 Cooking #Breakfast Porrige 202 | 200106 0700 543 Household #Routines 203 | ------ 2130 --- Sleep --- 204 | ------ 1900 --- Personal #Idle 205 | ------ 1730 574 Personal #Read Book XYZ 206 | ------ 1800 455 Personal #Commute Home 207 | ------ 0900 324 Company #Code Working on XYZ 208 | ------ 0830 --- Company #Commute 209 | ------ 0730 423 Cooking #Breakfast Porrige 210 | 200105 0700 543 Household #Routines 211 | ------ 2100 --- Sleep --- 212 | ------ 1900 --- Personal #Idle 213 | ------ 1730 574 Personal #Read Book XYZ 214 | ------ 1700 455 Personal #Commute Home 215 | ------ 0900 324 Company #Code Working on XYZ 216 | ------ 0830 --- Company #Commute 217 | ------ 0730 423 Cooking #Breakfast Porrige 218 | 200104 0700 754 Household #Routines 219 | ------ 2100 422 Sleep --- 220 | ------ 1930 --- Personal #Idle 221 | ------ 1730 574 Personal #Read Book XYZ 222 | ------ 1700 465 Personal #Commute Home 223 | ------ 0900 324 Company #Code Working on XYZ 224 | ------ 0830 --- Company #Commute 225 | ------ 0730 423 Cooking #Breakfast Porrige 226 | 200103 0700 753 Household #Routines 227 | ------ 2100 --- Sleep --- 228 | ------ 1900 2-- Personal #Idle 229 | ------ 1730 274 Personal #Read Book XYZ 230 | ------ 1700 255 Personal #Commute Home 231 | ------ 0930 324 Company #Code Working on XYZ 232 | ------ 0830 --- Company #Commute 233 | ------ 0730 426 Cooking #Breakfast Porrige 234 | 200102 0700 547 Household #Routines 235 | ------ 2100 --- Sleep --- 236 | ------ 1900 --- Personal #Idle 237 | ------ 1730 574 Personal #Read Book XYZ 238 | ------ 1700 456 Personal #Commute Home 239 | ------ 0900 324 Company #Code Working on XYZ 240 | ------ 0830 --- Company #Commute 241 | ------ 0730 423 Cooking #Breakfast Porrige 242 | ------ 0700 543 Household #Routines 243 | 200101 0000 --- Sleep --- 244 | ` -------------------------------------------------------------------------------- /scripts/interface/interface.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] 3 | const dayNamesLong = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'] 4 | 5 | function Interface () { 6 | this.logbook = document.createElement('div') 7 | this.logbook.id = 'logbook' 8 | this.scores = new Scores() 9 | this.isHome = true 10 | 11 | this.install = function (host) { 12 | host.appendChild(this.logbook) 13 | } 14 | 15 | this.update = function () { 16 | if (logbook.errors.length > 0) { this.errors(); return } 17 | const time = performance.now() 18 | this.logbook.innerHTML = '' 19 | this.isHome = page.url.length === 0 ? true : false 20 | 21 | if (this.isHome) this.home() 22 | else this.subPage() 23 | 24 | // drop down data selectors 25 | const fromDate = document.querySelector('#fromDate') 26 | const toDate = document.querySelector('#toDate') 27 | if (fromDate) fromDate.onchange = function () { dates.from = fromDate.value; logbook.update() } 28 | if (toDate) toDate.onchange = function () { dates.to = toDate.value; logbook.update() } 29 | console.info(`Logbook built interface in ${(performance.now() - time).toFixed(2)}ms.`) 30 | } 31 | 32 | this.home = function () { 33 | this.overview() 34 | 35 | if (database.stats.filter.entries > 0) { 36 | // only display if there are entries 37 | if (database.categories) { 38 | // build balance UI 39 | this.logs = new Balance() 40 | this.logs.install(this.logbook) 41 | this.logs.update() 42 | } 43 | if (database.scores) { 44 | // build score graph 45 | this.scores.install(this.logbook) 46 | this.scores.update() 47 | } 48 | // build projects list and tags 49 | this.projects() 50 | this.tags() 51 | } 52 | } 53 | 54 | this.overview = function () { 55 | const overview = document.createElement('section') 56 | overview.id = 'overview' 57 | this.logbook.appendChild(overview) 58 | 59 | const header = document.createElement('header') 60 | header.innerHTML = `

Overview

` 61 | overview.appendChild(header) 62 | 63 | const stats = database.stats.filter 64 | const statsEl = document.createElement('figure') 65 | statsEl.id = 'stats' 66 | overview.appendChild(statsEl) 67 | statsEl.innerHTML += this.datepickers() 68 | statsEl.innerHTML += `

${stats.days}d / ${stats.hours.toFixed(2)}h / ${stats.entries}e

` 69 | 70 | if (database.stats.filter.entries > 0) { 71 | const summary = new Summary(overview) 72 | summary.update(database.categories) 73 | } else { 74 | const msg = document.createElement('section') 75 | msg.innerHTML = '

There are no logged entries within the specified dates.

' 76 | this.logbook.appendChild(msg) 77 | } 78 | } 79 | 80 | this.subPage = function () { 81 | // OVERVIEW 82 | const overview = document.createElement('section') 83 | overview.id = 'overview' 84 | overview.innerHTML = `

<Logbook

` 85 | this.logbook.appendChild(overview) 86 | 87 | this.p = database.projects[page.url] 88 | this.d = database.days[page.url] 89 | // Project or day doesn't exist? 90 | if (!this.p && !this.d) { 91 | overview.innerHTML += `

404

` 92 | overview.innerHTML += `

The project or date '${page.url}' couldn't be found in the database.

` 93 | return 94 | } 95 | 96 | if (this.d) this.dailyDetails(this.d) 97 | else this.projectDetails() 98 | } 99 | 100 | this.dailyDetails = function (day) { 101 | this.day = day 102 | overview.innerHTML += `

${page.url}

` 103 | const statsEl = document.createElement('figure') 104 | statsEl.id = 'stats' 105 | overview.appendChild(statsEl) 106 | statsEl.innerHTML += `

${dayNamesLong[toTimeStamp(page.url).getDay()]}

` 107 | statsEl.innerHTML += `

${this.day.trackedHours}h / ${this.day.entries.length}e

` 108 | 109 | const summary = new Summary(overview) 110 | summary.update(this.day.categories) 111 | this.timeLine(overview, page.url) 112 | 113 | const dailyScore = new Scores() 114 | dailyScore.install(this.logbook) 115 | dailyScore.update() 116 | 117 | this.projects() 118 | } 119 | 120 | this.projectDetails = function () { 121 | this.project = database.projects[page.url] 122 | const entries = this.project.count 123 | let stats = 'This project has no entries.' 124 | if (entries > 0) { 125 | stats = `
` 126 | stats += this.datepickers() 127 | stats += ` 128 |

${this.project.hours}h / ${this.project.count}e

129 |
130 | ` 131 | } 132 | overview.innerHTML += 133 | `

${this.project.NAME}

134 | ${stats} 135 | ` 136 | // ENTRIES 137 | this.logs = new Balance() 138 | this.logs.install(this.logbook) 139 | this.logs.update(page.url) 140 | 141 | // TAGS 142 | this.tags() 143 | } 144 | 145 | this.projects = function () { 146 | this.database = this.isHome ? database.projects : database.days[page.url].projects 147 | const projects = document.createElement('section') 148 | projects.id = 'projects' 149 | this.logbook.appendChild(projects) 150 | 151 | projects.innerHTML = `

Projects

` 152 | 153 | const categories = document.createElement('figure') 154 | categories.id = 'categories' 155 | projects.appendChild(categories) 156 | 157 | for (const c in database.categories) { 158 | const el = document.createElement('section') 159 | el.id = c.toLowerCase() 160 | el.innerHTML = `

${c}

` 161 | categories.appendChild(el) 162 | } 163 | 164 | const tmp = [] 165 | // create a temp array for sorting projects by hours 166 | for (const id in this.database) { 167 | if (!this.database[id].hours) continue 168 | const project = this.database[id] 169 | project.name = id 170 | tmp.push(project) 171 | } 172 | tmp.sort(this.sortByHours) 173 | // create html from sorted projects 174 | for (const id in tmp) { 175 | const project = tmp[id] 176 | const data = this.database[project.name] 177 | const el = document.createElement('figure') 178 | el.id = project.name 179 | const hours = this.isHome ? database.stats.filter.hours : 24 180 | const percent = (project.hours * 100 / hours).toFixed(1) 181 | el.innerHTML += 182 | `

${data.NAME}

183 |

${project.hours.toFixed(2)}h / ${project.count}e / ${percent}%

` 184 | const parent = projects.querySelector(`#${(database.projects[project.name].CAT).toLowerCase()}`) 185 | if (parent !== null) parent.appendChild(el) 186 | } 187 | } 188 | 189 | this.tags = function () { 190 | const tagList = this.tagList(this.data) 191 | if (tagList.length < 1) return 192 | const section = document.createElement('section') 193 | section.id = 'tags' 194 | this.logbook.appendChild(section) 195 | section.innerHTML = `

Tags

` 196 | tags.innerHTML = 197 | `
198 |

Tags

199 |
200 | ${tagList} 201 |
202 |
203 | ` 204 | } 205 | 206 | this.tagList = function () { 207 | this.data = this.isHome ? database : database.projects[page.url] 208 | 209 | const tmp = [] 210 | // create a temp array for sorting tags by hours 211 | for (const id in this.data.tags) { 212 | const tag = this.data.tags[id] 213 | if (tag.hours) { 214 | tmp.push(tag) 215 | tmp[tmp.length - 1].name = id 216 | } 217 | } 218 | 219 | // const untagged = {} 220 | // if () 221 | // untagged.name = "untagged" 222 | // untagged.count = this.data.count - this.data.taggedEntries 223 | // untagged.hours = this.data.hours - this.data.taggedHours 224 | 225 | tmp.sort(this.sortByHours) 226 | // if (untagged.count > 0) tmp.push(untagged) 227 | 228 | let tagList = '' 229 | for (const tag in tmp) { 230 | const w = Math.round(tmp[tag].hours * 100 / tmp[0].hours) 231 | const c = this.isHome ? "#FF68A3" : database.categories[this.data.CAT].COLOR 232 | tagList += 233 | `
234 |

${tmp[tag].name}

235 |

${tmp[tag].hours}h / ${tmp[tag].count}e

236 |
237 |
238 |
239 |
` 240 | } 241 | return tagList 242 | } 243 | 244 | this.sortByHours = function (a, b) { 245 | return b.hours - a.hours; 246 | } 247 | 248 | this.datepickers = function () { 249 | let html = 250 | `
251 |
252 | 258 |
259 |

-

260 |
261 | 267 |
268 |
` 269 | return html 270 | } 271 | 272 | this.errors = function () { 273 | // error messages 274 | const overview = document.createElement('section') 275 | overview.id = 'overview' 276 | this.logbook.appendChild(overview) 277 | 278 | const header = document.createElement('header') 279 | header.innerHTML = `

Error!

` 280 | overview.appendChild(header) 281 | 282 | const errors = document.createElement('figure') 283 | errors.id = 'errors' 284 | overview.appendChild(errors) 285 | for (const e in logbook.errors) errors.innerHTML += `

${logbook.errors[e].title}

${logbook.errors[e].text}

` 286 | return 287 | } 288 | 289 | this.timeLine = function (host) { 290 | const date = page.url 291 | const timeLine = document.createElement('section') 292 | timeLine.id = 'timeline' 293 | host.appendChild(timeLine) 294 | 295 | const header = document.createElement('header') 296 | header.innerHTML = `

Timeline

` 297 | timeLine.appendChild(header) 298 | 299 | const ruler = document.createElement('figure') 300 | ruler.id = 'ruler' 301 | 302 | let h = 0 303 | const width = 100/24 304 | while (h < 24) { 305 | const hour = document.createElement('div') 306 | hour.id = `hour_${h}` 307 | hour.className = 'hour' 308 | hour.style = `width: ${width}%;` 309 | const label = document.createElement('div') 310 | label.id = 'label' 311 | label.innerHTML =`${leadingZero(h)}` 312 | hour.appendChild(label) 313 | ruler.appendChild(hour) 314 | h++ 315 | } 316 | timeLine.appendChild(ruler) 317 | 318 | const items = document.createElement('figure') 319 | items.id = 'items' 320 | timeLine.appendChild(items) 321 | 322 | const day = database.days[date] 323 | const entries = day.entries 324 | const now = new Date() 325 | const dayId = toTimeStamp(day.date).getDay() 326 | const hours = now.getMinutes() > 30 ? leadingZero(now.getHours()+1) : leadingZero(now.getHours()) 327 | const minutes = now.getMinutes() > 30 ? '00' : '30' 328 | 329 | for (const t in day.timeline) { 330 | const entry = day.timeline[t] 331 | const time = parseInt(entry.time.slice(0,2)) + parseFloat(entry.time.slice(2)) / 60 332 | const x = Math.round(100 / 24 * time) 333 | const w = Math.round(entry.duration * 100 / 24) 334 | const project = entry.project.toUpperCase() 335 | const cat = database.projects[project].CAT 336 | const tag = entry.tags 337 | const duration = entry.duration 338 | const color = database.categories[cat].COLOR 339 | items.innerHTML += `
` 340 | } 341 | 342 | const desc = document.createElement('figure') 343 | desc.id = 'desc' 344 | desc.style = 'width: 100%; display: flex; justify-content: end;' 345 | desc.innerHTML = `

 

` 346 | timeLine.appendChild(desc) 347 | 348 | // mouse events 349 | items.addEventListener("mouseover", function( event ) { 350 | const el = event.target 351 | if (el.id.slice(0,5) === 'item_') { 352 | el.style.backgroundColor = 'white' 353 | const i = el.id.slice(5) 354 | const entry = day.timeline[i] 355 | desc.innerHTML = `

${entry.project}: ${entry.tags} / ${entry.duration}h

` 356 | } 357 | }) 358 | 359 | items.addEventListener("mouseout", function( event ) { 360 | const el = event.target 361 | if (el.id.slice(0,5) === 'item_') { 362 | el.style.backgroundColor = el.attributes.color.nodeValue 363 | desc.innerHTML = `

 

` 364 | } 365 | }) 366 | } 367 | } --------------------------------------------------------------------------------