├── .github └── workflows │ └── deploy.yaml ├── .gitignore ├── README.md ├── content ├── about │ └── _index.md ├── better-than-estimates │ └── index.md ├── how-to-get-unstuck-and-make-progress │ └── index.md ├── lean-monitoring │ └── index.md ├── on-estimates │ ├── dev-maintenance.png │ ├── hill-concept.png │ ├── index.md │ ├── man-month.png │ └── peopleware-estimates.png ├── posts │ └── _index.md ├── router-or-moderator │ ├── index.md │ ├── manager-moderator.png │ └── manager-router.png ├── three-steps │ ├── balsamiq.png │ ├── dbdiagram.png │ └── index.md ├── why-go-fullstack │ └── index.md ├── why-public-chats-are-better-than-direct-messages │ └── index.md ├── why-you-should-be-careful-with-developer-metrics │ └── index.md ├── you-might-not-need-staging │ └── index.md ├── zero-downtime-db-migrations │ └── index.md └── zero-downtime-deployment │ └── index.md ├── hugo.toml ├── layouts ├── _default │ ├── _markup │ │ ├── render-heading.html │ │ ├── render-image.html │ │ └── render-image.rss.xml │ ├── section.html │ ├── section.xml │ └── single.html ├── index.html ├── index.xml ├── partials │ ├── footer.html │ ├── image.html │ ├── meta.html │ ├── nav.html │ └── post-preview.html └── section │ └── posts.html └── static ├── external.svg ├── feed.svg ├── lightbox.css ├── lightbox.js ├── linkedin.svg ├── main.css ├── photo.jpeg ├── prettify.js └── x.svg /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [master] 5 | workflow_dispatch: 6 | jobs: 7 | notify-build-start: 8 | if: ${{ github.event_name == 'push' }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Send build notifications to Slack 12 | - uses: ivelum/github-action-slack-notify-build@v1.6.0 13 | id: slack 14 | with: 15 | channel_id: C0528Q7QK3M 16 | status: STARTED 17 | color: '#ee9b00' 18 | env: 19 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 20 | outputs: 21 | status_message_id: ${{ steps.slack.outputs.message_id }} 22 | 23 | deploy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Setup Hugo 28 | env: 29 | HUGO_VERSION: 0.147.3 30 | run: | 31 | mkdir ~/hugo 32 | cd ~/hugo 33 | curl -L "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_withdeploy_${HUGO_VERSION}_Linux-64bit.tar.gz" --output hugo.tar.gz 34 | tar -xvzf hugo.tar.gz 35 | sudo mv hugo /usr/local/bin 36 | - env: 37 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 38 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 39 | run: hugo && hugo deploy 40 | 41 | # Send notification on build or deploy failure 42 | - name: Notify slack fail 43 | uses: ivelum/github-action-slack-notify-build@v1.6.0 44 | if: failure() 45 | env: 46 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 47 | with: 48 | channel_id: C0528Q7QK3M 49 | status: FAILED 50 | color: '#d7263d' 51 | 52 | notify-build-success: 53 | if: ${{ github.event_name == 'push' }} 54 | needs: [ deploy, notify-build-start ] 55 | runs-on: ubuntu-latest 56 | steps: 57 | # Send notification on build success 58 | - name: Notify slack success 59 | uses: ivelum/github-action-slack-notify-build@v1.6.0 60 | env: 61 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 62 | with: 63 | message_id: ${{ needs.notify-build-start.outputs.status_message_id }} 64 | channel_id: C0528Q7QK3M 65 | status: SUCCESS 66 | color: '#16db65' 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hugo_build.lock 2 | .idea 3 | .DS_Store 4 | public 5 | resources 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is the source code for my personal blog at [stebunov.com](https://stebunov.com), which 4 | is built on [Hugo](https://gohugo.io). If you'd like to play with it, make sure 5 | you have Hugo installed, clone the repo, and run: 6 | 7 | ```shell 8 | $ hugo server 9 | ``` 10 | 11 | # License 12 | 13 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) 14 | -------------------------------------------------------------------------------- /content/about/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | url: / 3 | --- 4 | 5 | Hi, I'm Denis. I'm CTO at [ivelum](https://ivelum.com), where we help startups and 6 | enterprises build their software products, and I also work on [Teamplify](https://teamplify.com) – 7 | a team management suite for engineering teams. I've been building software for 8 | more than 20 years as a developer, product manager, and CTO. 9 | I live in Vilnius, 🇱🇹Lithuania, and I travel often. You can find me 10 | on [X](https://x.com/dstebunov), 11 | [LinkedIn](https://www.linkedin.com/in/denis-stebunov/), 12 | [GitHub](https://github.com/stebunovd), 13 | or [Instagram](https://www.instagram.com/stebunovd/). 14 | 15 | I write for the [ivelum blog](https://ivelum.com/blog/), 16 | [Teamplify blog](https://teamplify.com/blog/), and occasionally 17 | for my [personal blog](/posts/) here. I'm also making 18 | 🎥 [YouTube videos](https://www.youtube.com/@ivelum/videos). I'm always open 19 | to new projects or content ideas—feel free to drop me a line at 20 | [denis@stebunov.com](mailto:denis@stebunov.com). 21 | 22 | ## The latest video ([see more...](https://www.youtube.com/@ivelum/videos)) 23 | 24 | {{< youtube cgqMP9G6CEc >}} 25 | -------------------------------------------------------------------------------- /content/better-than-estimates/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Better than estimates 3 | description: People routinely use estimates for planning, prioritization, and managing expectations. But are they really the best tool for the job? 4 | date: 2025-03-13T10:22:00+0200 5 | external: https://ivelum.com/blog/better-than-estimates/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/how-to-get-unstuck-and-make-progress/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to get unstuck and make progress 3 | description: Developers spend an enormous amount of time being stuck. I've been a developer and an engineering manager myself for many years, and I think being stuck is the default state. We, developers, spend most of our time being stuck, and just occasionally, we get unstuck and make progress. 4 | date: 2022-08-04T12:39:00+0200 5 | external: https://teamplify.com/blog/how-to-get-unstuck-and-make-progress/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/lean-monitoring/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lean app monitoring—The Starter Pack 3 | description: An easy, low-cost way to start your monitoring routine that covers the most basic needs. For those who lost in application monitoring, infrastructure monitoring, uptime monitoring, etc. 4 | date: 2025-01-16T14:37:00+0200 5 | external: https://ivelum.com/blog/lean-monitoring/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/on-estimates/dev-maintenance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/on-estimates/dev-maintenance.png -------------------------------------------------------------------------------- /content/on-estimates/hill-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/on-estimates/hill-concept.png -------------------------------------------------------------------------------- /content/on-estimates/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: On Estimates 3 | description: Since we began developing software, we’ve looked for ways to reliably estimate our development time. Now, some 60+ years later, we've gotten no better at it. 4 | date: 2020-05-26T12:05:00+0300 5 | slug: on-estimates 6 | --- 7 | 8 | Since we began developing software, we’ve looked for ways to reliably estimate 9 | our development time. Now, some 60+ years later, we've gotten no better at it. 10 | Maybe the problem is not in how we estimate, but that we're so concerned with 11 | estimates in the first place. 12 | 13 | Take the popular Scrum framework. At its core, Scrum is based on estimating the 14 | future work and taking only as much as you can supposedly do into the next 15 | sprint. At first glance, it sounds reasonable. In reality, more often than not, 16 | it means trading team performance for the illusion of planning. I’ll explain 17 | why. 18 | 19 | 20 | ## Estimates slow us down 21 | 22 | People naturally want to be good at what they're doing. At first, developers 23 | tend to be optimistic, and accordingly tend to underestimate their task times. 24 | What inevitably happens next is they commit to the specific timeline that they 25 | themselves stated and fail. This makes them feel uncomfortable, even when no 26 | one blames them (and sometimes people do blame them). As the process repeats 27 | itself, they slowly learn to overestimate, because they don't want to fail. 28 | 29 | Counterintuitively, overestimating doesn't always help people to finish their 30 | work on time. This effect is known as 31 | [Parkinson's Law](https://en.wikipedia.org/wiki/Parkinson%27s_law), and here's 32 | where psychology plays against us. Let's say you think a task will take a 33 | couple of days, but, given the uncertainty, you might need up to a week. You 34 | estimate a week, and your manager says "fine." Now you think - alright, I have 35 | plenty of time! As a result, in the first half of the week, you either work in 36 | a relaxed mode or shift your priorities to other tasks, because you know that 37 | you have plenty of time. As the timeline approaches, you start to realize that 38 | the task is not as simple as you’d initially thought and remember why you 39 | overestimated in the first place. So you work hard, maybe stay late, and 40 | finally, barely fit the work into the timeline you set for yourself. Next week, 41 | the same story happens again. 42 | 43 | Ultimately, problems happen either way you estimate. If team members are under 44 | time pressure (often because they’ve underestimated), they may have to cut 45 | corners to meet the timeline. Surprisingly, if they overestimate, the same 46 | thing happens, just later. As a rule of thumb, you may think of deadlines as 47 | "the earliest dates when something can be delivered." So no matter whether they 48 | over- or underestimated, nothing ships until the deadline. 49 | 50 | ![Productivity by Estimation Approach](peopleware-estimates.png) 51 | 52 | *Source: [Peopleware by Tom DeMarko and Timothy Lister](https://www.amazon.com/Peopleware-Productive-Projects-Tom-DeMarco/dp/0932633439)* 53 | 54 | 55 | Estimates slow the team down, and the more frequently you ask developers for 56 | estimates, the worse the effect becomes. If a team estimates all the work it’s 57 | doing, then the amount of time and energy it must spend on the estimating 58 | process will be overwhelming. First, people spend time making the estimates. 59 | After that, they spend time discussing why those estimates failed and what they 60 | should do next (usually, the solution is to request more estimates.) Next, 61 | requirements change - and voila, now they have to figure out how the change 62 | affects their estimates. Finally, they deal with the technical debt accumulated 63 | in their numerous attempts to meet the estimated timelines, slowing the project 64 | down in the long term. 65 | 66 | 67 | ## Quick estimates are just guessing 68 | 69 | The nature of software development entails a lot of complexity and uncertainty. 70 | Almost all of the work that developers do is something new. Even if they’ve 71 | done something similar in the past, they're now doing it in new conditions. 72 | There might be a new project, or new requirements, or new knowledge learned 73 | from the last experience. If it were absolutely the same and routinely 74 | repeated, it’d likely already be automated, or be implemented as an 75 | out-of-the-box product, or exist as a library or framework. Therefore, in most 76 | cases, developers have to provide estimates for tasks that have a high degree 77 | of uncertainty. 78 | 79 | ![Figuring out what to do – Getting it done](hill-concept.png) 80 | 81 | *Source: [Show Progress | Shape Up by BaseCamp](https://basecamp.com/shapeup/3.4-chapter-12)* 82 | 83 | It could be better if developers are given some time to work on the problem in 84 | advance; in such cases, they would understand the problem better and be more 85 | confident providing an estimate. Unfortunately, it rarely happens in practice. 86 | Developers usually have very little time to study a problem before someone asks 87 | them for an estimate. As a result, they pluck numbers out of thin air. 88 | 89 | Of course, all experienced managers know that estimates are just estimates and 90 | don't try to treat them as hard deadlines. However, they are perceived as some 91 | form of commitment anyway, by both managers and developers. If estimates mean 92 | absolutely nothing, why ask for them in the first place? So providing an 93 | estimate means being accountable for it. It's no surprise that developers often 94 | don't like them - basically, we ask them to be accountable for something that 95 | they don't fully understand. 96 | 97 | 98 | ## Estimates create dangerous misconceptions 99 | 100 | Uncertainty is not the only problem with estimates. They can also create 101 | misconceptions about the workflow for the project team and stakeholders. As 102 | Fred Brooks famously said, "The bearing of a child takes nine months, no matter 103 | how many women are assigned." However, when people hear that a project will 104 | take nine man-months, they sometimes assume that it probably can be three 105 | months for three random developers. 106 | 107 | ![Mythical Man-Month](man-month.png) 108 | 109 | *Source: [The Mythical Man-Month by Frederick P. Brooks](https://www.amazon.com/Mythical-Man-Month-Anniversary-Software-Engineering-ebook/dp/B00B8USS14/)* 110 | 111 | Men and months are not interchangeable, and not just because some work can't be 112 | done in parallel. Who is doing the work also matters. A senior developer who’s 113 | also familiar with the matter could probably do it much faster than estimated, 114 | and a junior developer with no knowledge of the subject may not just miss the 115 | deadline, but fail the task altogether. 116 | 117 | 118 | ## Initial development is a minor cost 119 | 120 | ![Maintenance vs. initial development](dev-maintenance.png) 121 | 122 | Arguably the biggest problem with estimates is that people routinely ask for 123 | initial development estimates, but rarely ask for estimates about the 124 | subsequent maintenance. Maintenance is huge. It takes up to 125 | [60-90%](https://www.google.com/search?q=software+initial+development+vs.+maintenance+costs) 126 | of the total project cost. Besides that, every feature that you add to the 127 | product increases complexity and makes further development slower. With this 128 | fact in mind, people should be very cautious about adding new features. 129 | Instead, prioritize like a maniac, and work on only the features that make the 130 | most sense. This rarely happens when estimates drive project planning. In such 131 | projects, people tend to push features that take less time and postpone the 132 | ones that will take longer. As a consequence, the most important work can be 133 | postponed and the product becomes bloated with less important features and 134 | quick hacks. 135 | 136 | 137 | ## How is it possible to plan a project without estimates? 138 | 139 | Ok, so estimates take their toll. They can be misleading, they can slow a team 140 | down and make developers uncomfortable, but we need them for project planning 141 | anyway, right? Well, not necessarily. Sometimes estimates are unavoidable, but 142 | in most cases, they are not required for project decisions. 143 | 144 | First, the initial development estimates shouldn't be the driving factor that 145 | determines what to work on next. It would be best if you only worked on what 146 | is crucial, and not on what is fast to implement. If you're not absolutely sure 147 | that a feature is essential, it shouldn't be in the development queue. Use 148 | quick MVPs and product discovery to find out what your customers need. 149 | 150 | Second, when you have a timeline to meet, communicate it to developers instead 151 | of asking for their estimates. There's the excellent 152 | [Fix Time and Budget, Flex Scope](https://basecamp.com/gettingreal/02.4-fix-time-and-budget-flex-scope) 153 | approach. Every task can be completed in thousands of ways. Prioritize the 154 | requirements carefully, work on the most critical items first, and be ready to 155 | ship at any time. In such conditions, timelines become manageable. Even if 156 | you're not lucky enough to ship it fully, the most important features will be 157 | there and the least important will be missing, which quite often aren’t 158 | critical to launching. 159 | 160 | Finally, if you badly need an estimate, give developers a few days to work on 161 | the task before asking. If you're lucky, the task will already have been 162 | completed, and no estimate will be needed. Otherwise, they will understand the 163 | task better, and you'll be able to have much more productive talk about the 164 | task challenges. Make sure that you take into account not only initial 165 | development estimates but also long-term maintenance, which is usually more 166 | important. And once again, if you're not sure that the task is essential, it 167 | shouldn't be in the development queue, even for estimates. 168 | 169 | 170 | ## Managing expectations 171 | 172 | This one is probably the most challenging. As a manager or a team lead, you 173 | will likely deal with stakeholders who will wonder how long a project will take. 174 | If you ask them why they need this information, they'll tell you it's necessary 175 | for planning or that they simply want to know what to expect. We discussed 176 | planning above, now let's turn to expectations. It’s human nature to be 177 | concerned if something you care about is uncertain. It may be your boss 178 | who’s asking, and it may be hard to say, "I don't know." Here's what you can 179 | try: 180 | 181 | 1. if the work’s already in the queue or in progress, ask what plans related to 182 | the completion of this work. Try to help them execute their plans. Sometimes, 183 | you may use this information as an input for work prioritization and 184 | communicate their expectations to the team. Other times, you may find 185 | workarounds that would help those people to achieve their goals while the 186 | task’s still in progress; 187 | 2. if the work is not in the development queue yet, ask about its priority. 188 | Talk about the plans that the development team already has, try to figure 189 | out where this new work can fit. Sometimes you'll find there's no chance 190 | that you'll work on it anytime soon, and if so - there's not much sense in 191 | discussing estimates; 192 | 3. focus on team productivity and work transparency. If your team ships to 193 | production every day and everyone can see the work and the queue, people are 194 | less inclined to ask for estimates; 195 | 4. as a manager or a team lead, track the progress of your team regularly. Know 196 | what was done and what’s remaining, what the blockers are, etc. If you know 197 | what's going on, you can make your own estimates and communicate them to 198 | stakeholders when you think it's necessary. Remember, good managers work as 199 | a "shit umbrella" for the team; bad managers let the shit fall through. 200 | Passing all requests for estimates down to developers is not good management 201 | practice. 202 | 203 | 204 | ## Conclusion 205 | 206 | To summarize, while estimates look appealing and straightforward on the surface, 207 | really they are misleading and obstructive to the workflow. It’s natural for 208 | humans to simplify complicated things, but estimates simplify the perception of 209 | the development process far beyond what’s reasonable. Scrum can take this 210 | over-simplification to the extreme with its planning poker, velocity, and 211 | burn-down charts. 212 | 213 | Most people would agree that, though estimates are imperfect, they are at least 214 | measurable. They can't imagine how to manage a project without having something 215 | measurable. Unfortunately, not everything that counts can be measured, and not 216 | everything that can be measured counts. In this post, I provided some practical 217 | advice from my experience of managing projects without estimates. Of course, I'm 218 | not saying that estimates are entirely useless; I'm just saying that most of the 219 | time, other more important factors should be at play. I’ve used this approach 220 | for years, and it’s worked well for our teams. 221 | 222 | I realize that this advice is not for everyone. I worked with different 223 | organizations, big and small, and most of them used estimates in one form or 224 | another. At the same time, most organizations are far from efficient. The way 225 | the best teams operate is often different from the way the majority does. 226 | Perhaps the emphasis placed on estimates is one place where these differences 227 | lie. 228 | 229 | That's all I have for now. If you have any comments or questions - please feel 230 | free to reach me on [Twitter](https://twitter.com/dstebunov). This post may be 231 | edited later if I have more thoughts on it. In such a case, the history will be 232 | available here on Github. 233 | 234 | ----- 235 | 236 | P.S. More on the topic: 237 | 238 | * [Dave Farley - How To Estimate Software Development Time (video, 17 min)](https://youtu.be/v21jg8wb1eU) 239 | * [BaseCamp - Fix Time and Budget, Flex Scope](https://basecamp.com/gettingreal/02.4-fix-time-and-budget-flex-scope) 240 | * [Richard Clayton - Software Estimation is a Losing Game](https://rclayton.silvrback.com/software-estimation-is-a-losing-game) 241 | -------------------------------------------------------------------------------- /content/on-estimates/man-month.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/on-estimates/man-month.png -------------------------------------------------------------------------------- /content/on-estimates/peopleware-estimates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/on-estimates/peopleware-estimates.png -------------------------------------------------------------------------------- /content/posts/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: posts 3 | url: /posts/ 4 | --- 5 | 6 | # Posts 7 | -------------------------------------------------------------------------------- /content/router-or-moderator/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Is your manager a router or a moderator? 3 | description: In software engineering, communication around the manager organizes itself into two common modes – either a "router", or a "moderator". 4 | date: 2021-09-15T16:21:00+0300 5 | slug: router-or-moderator 6 | --- 7 | 8 | Whether directly or indirectly, software engineering teams need to communicate 9 | with other stakeholders somehow. Often, the organization has a dedicated person 10 | responsible for engineering–stakeholder communication. For simplicity, we’ll 11 | call that person a "manager" (it may also be a project manager, team lead, 12 | business analyst, etc). 13 | 14 | In software engineering, communication around the manager organizes itself into 15 | two common modes, with: 16 | 17 | - The manager as a **router**, relaying messages from stakeholders to the 18 | engineering team and back; 19 | - The manager as a **moderator**, overseeing messages flowing freely between 20 | stakeholders and engineers. 21 | 22 | When the manager is a moderator, it's possible to communicate with better 23 | throughput and higher signal quality. However, it also distributes the 24 | responsibility of communication upon the whole team. 25 | 26 | Each strategy has pros and cons. 27 | 28 | ## Router 29 | 30 | ![Manager as a router](manager-router.png) 31 | 32 | In *router* mode, the manager is responsible for all communication between the 33 | engineering team and the stakeholders. Centralizing communication this way 34 | makes it simpler to understand responsibilities, but it becomes harder to 35 | manage as the project grows. 36 | 37 | Pros: 38 | - **Communication is simpler to navigate**. Neither engineers nor stakeholders 39 | need to guess who is responsible for what. If the manager is responsible for 40 | all communication, then everyone talks to the manager first and lets them 41 | figure it out; 42 | - **The structure is simpler to organize**. Communication is a skill, which not 43 | everyone is good at. Some people have trouble putting together their 44 | thoughts, and others just don't enjoy communicating, or do it inefficiently. 45 | Instead of trying to improve communication across the whole team, it's easier 46 | to just hire one person with good communication skills; 47 | - **The manager has a reliable way to control processes**. When all 48 | communication flows through the manager, the manager has immediate access to 49 | all details. This helps them make sure that the project is consistent and 50 | going in the right direction; 51 | 52 | Cons: 53 | - **A bottleneck emerges**. When one person is the go-between for the work 54 | communication between multiple people, sooner or later, they become a 55 | bottleneck. In software terms, we're replacing parallel processing in 56 | multiple threads with concurrent processing in one thread, which severely 57 | limits bandwidth; 58 | - **Stakeholders play a game of telephone**. All communication loses something 59 | in transmission. Two people never understand each other 100% of the time. If 60 | we need to communicate through mediators, these losses quickly add up; 61 | - **There's a single point of failure**. When people are used to communicating 62 | through a manager, even short managerial absences create problems. What's 63 | more, replacing a manager who is sick or on vacation becomes a challenge, as 64 | the manager has concentrated all the project's information in their heads; 65 | - **It deals poorly with complexity**. When the project is large and complex, 66 | even a very talented person eventually forgets important details and loses 67 | track of some updates. 68 | 69 | ## Moderator 70 | 71 | ![Manager as a moderator](manager-moderator.png) 72 | 73 | An alternative approach is *moderator* mode, where the manager supervises while 74 | stakeholders communicate directly with engineering team members. Everyone is 75 | encouraged to talk to everyone else directly. The manager takes part in these 76 | conversations as an observer or guide. Teammates can ask for the manager's 77 | help, or the manager can join a particular group when they see that their help 78 | is needed. 79 | 80 | Although this distributed mode of communication removes the drawbacks of the 81 | router method, it requires more organizational discipline. 82 | 83 | Pros: 84 | - **Throughput is maximized**. The manager is no longer a bottleneck. Project 85 | members communicate as much and as frequently as they need; 86 | - **No one plays a game of telephone**. When people participate in discussions 87 | directly, they get the fullest information.They can ask questions and get 88 | answers right away; 89 | - **There is no single point of failure**. Since the manager is no longer a 90 | required party in every dialog with stakeholders, the team can more easily 91 | tolerate a manager's temporary absence; 92 | - **Openness facilitates creativity**. At all times, a free flow of knowledge 93 | improves long-term progress. In this model, people get information from more 94 | varied sources, helping generate new ideas. 95 | 96 | Cons: 97 | - **All team members must have reasonably good communication skills**. This 98 | raises the bar for hiring and gives the manager a new responsibility ─ 99 | communications coach. This raises the bar for the manager's role as well; 100 | - **Private conversations can easily drive the system into chaos**. If someone 101 | wants to ask someone else a question, the most natural action is to ask their 102 | peer privately. Unfortunately, private conversations aren't visible to the 103 | manager and to the other team members. At some point, when people start 104 | referring to those hidden discussions and agreements, it may be already too 105 | late. Such situations lead to wasted effort, conflicts, and an overall 106 | slowdown of the team; 107 | - **Teams need to work with more discipline**. To efficiently organize work in 108 | moderator mode, each team member must exert some extra effort. In the next 109 | section, we provide some examples of what that effort means in our case. 110 | - **When changing to moderator mode, teams may have to overcome social 111 | inertia**. For some people, the extra effort might also mean breaking their 112 | old work habits. Not everyone is ready for that. 113 | 114 | ## Our company experience 115 | 116 | Though it requires a bit more team effort, at [ivelum](https://ivelum.com) we 117 | like the moderator mode a lot. It's how all our teams operate by default. For 118 | this mode to work efficiently, we came up with the following guidelines: 119 | 120 | 1. **Avoid private work conversations**. Every team has a dedicated Slack 121 | channel, or multiple channels, and we strongly encourage everyone to discuss 122 | all work topics either there, or in an issue tracker, or in another 123 | collaboration tool that all team members can easily see. Direct private 124 | messages are only for private topics, not for regular work discussions; 125 | 2. **Foster a safe and open environment**. Some people, especially those who 126 | joined recently, may be shy to ask their questions in public. To help them 127 | feel comfortable, it is crucial to ensure that our chats have a safe and 128 | healthy environment. Sarcasm and jokes about "stupid" questions are strictly 129 | prohibited, and we watch this closely. A reasonable number of cat pictures 130 | and funny memes are welcome. Anything related to politics can be discussed 131 | only in the *#politics* channel, which people can join and leave whenever 132 | they want; 133 | 3. **Write down and share meeting notes**. This is a very old and common 134 | recommendation, but still, people sometimes forget. Someone in the team must 135 | watch this and politely ask teammates to share notes after their meetings; 136 | 4. **Remove unnecessary access restrictions**. Unless there's an explicit 137 | reason to restrict access, all work materials, including code, work 138 | documents, issue trackers, etc. should be available for all project members. 139 | 140 | From our experience, new teammates usually adapt to our communication style 141 | within a few weeks. Some of them feel comfortable from day one. Others may need 142 | more time. We try to be welcoming and patient and assist as much as we can. 143 | 144 | However, even in our company we have situations where a manager or team lead 145 | has to switch to router mode. In most cases, this is when we temporarily talk 146 | to people who aren't used to this communication style, and it doesn't make 147 | sense for them to adapt to it. Another notable exception is communication with 148 | users of our products. When a user reports a bug or needs consultation, they 149 | usually have just one point of contact. Users rarely want to speak with an 150 | entire development team. 151 | 152 | ## Bottom line 153 | 154 | As always, there are no silver bullets. Every option has its pros and cons. 155 | Every team, project, and situation are unique. What works for one case might 156 | not work for another. 157 | 158 | It's evident that, as project complexity increases, the router mode becomes 159 | more and more difficult to maintain. Yet, to operate in moderator mode, the 160 | manager has to trust their team's communication judgement, and team members 161 | have to be disciplined about following the communication guidelines. 162 | 163 | Though we use both modes at [ivelum](https://ivelum.com), the moderator mode is 164 | our default. We use router mode only in rare cases. Hopefully this article can 165 | help you find the best solution for your situation. 166 | -------------------------------------------------------------------------------- /content/router-or-moderator/manager-moderator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/router-or-moderator/manager-moderator.png -------------------------------------------------------------------------------- /content/router-or-moderator/manager-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/router-or-moderator/manager-router.png -------------------------------------------------------------------------------- /content/three-steps/balsamiq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/three-steps/balsamiq.png -------------------------------------------------------------------------------- /content/three-steps/dbdiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/content/three-steps/dbdiagram.png -------------------------------------------------------------------------------- /content/three-steps/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Three important steps before jumping to the code 3 | description: As a developer, how do you start building a new feature? You may think, "It depends," and it certainly does. However, there could be frameworks that fit many situations, and I'd like to suggest one. 4 | date: 2024-07-08T14:37:00+0200 5 | external: https://ivelum.com/blog/three-steps/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/why-go-fullstack/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why go full-stack in 2023? 3 | description: What does it mean to be a full-stack web developer today, and what are their pros and cons for team productivity? 4 | date: 2023-02-01T10:50:00+0200 5 | external: https://ivelum.com/blog/why-go-full-stack/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/why-public-chats-are-better-than-direct-messages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why public chats are better than direct messages 3 | description: How we communicate makes an enormous impact on our work. One of the best strategies for improving communication in a team is making it open. 4 | date: 2022-09-07T19:31:00+0200 5 | external: https://ivelum.com/blog/why-public-chats-are-better-than-direct-messages/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/why-you-should-be-careful-with-developer-metrics/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why you should be careful with developer metrics 3 | description: "If you've ever managed any software project, you've probably asked yourself: how could our teams move faster? How fast are we moving today? For these kinds of questions, it's tempting to turn to metrics." 4 | date: 2020-12-06T13:04:00+0200 5 | external: https://teamplify.com/blog/why-you-should-be-careful-with-developer-metrics/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/you-might-not-need-staging/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: You might not need staging 3 | description: Many engineering teams use staging to test new features before pushing them to production. But is it really the best way to test? 4 | date: 2025-01-29T10:30:00+0200 5 | external: https://ivelum.com/blog/you-might-not-need-staging/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/zero-downtime-db-migrations/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Migrating a production database without any downtime 3 | description: In this episode, we'll cover the basic principles of zero-downtime database migrations and provide quick recipes for the most common scenarios. 4 | date: 2024-10-16T11:34:00+0200 5 | external: https://ivelum.com/blog/zero-downtime-DB-migrations/ 6 | --- 7 | -------------------------------------------------------------------------------- /content/zero-downtime-deployment/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Update with no fear — achieving zero-downtime deployment 3 | description: "The key principles of zero-downtime deployment applied to the main components of any web app: the backend, the frontend, and the database." 4 | date: 2025-02-18T12:54:00+0200 5 | external: https://ivelum.com/blog/zero-downtime-deployment/ 6 | --- 7 | -------------------------------------------------------------------------------- /hugo.toml: -------------------------------------------------------------------------------- 1 | baseURL = 'https://stebunov.com/' 2 | languageCode = 'en-us' 3 | title = "Denis Stebunov's blog" 4 | disableKinds = ['taxonomy', 'term'] 5 | 6 | [output] 7 | home = ['html', 'rss'] 8 | section = ['html'] 9 | 10 | [deployment] 11 | [[deployment.targets]] 12 | name = "s3" 13 | URL = "s3://stebunov.com?region=eu-north-1" 14 | cloudFrontDistributionID = "E2KMNX4POSU4ZD" 15 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-heading.html: -------------------------------------------------------------------------------- 1 | {{ .Text | safeHTML }} 2 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-image.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | The maximum image width is set to 680px (matching the maximum width 3 | defined in CSS). 4 | */}} 5 | {{- $previewWidth := 680 }} 6 | {{- $img := .Page.Resources.Get .Destination }} 7 | {{- $width := "" }} 8 | {{- $height := "" }} 9 | {{- if ne $img.MediaType.SubType "svg" }} 10 | {{- $previewWidth2x := math.Mul $previewWidth 2 }} 11 | {{- $previewWidth4x := math.Mul $previewWidth 4 }} 12 | {{- if gt $img.Width $previewWidth4x }} 13 | {{- $img = $img.Resize (printf "%dx" $previewWidth4x) }} 14 | {{- end }} 15 | {{- if lt $img.Width $previewWidth2x }} 16 | {{- $width = $img.Width }} 17 | {{- $height = $img.Height }} 18 | {{- else }} 19 | {{- $width = $previewWidth2x }} 20 | {{- $height = cast.ToInt (math.Mul $img.Height (math.Div (cast.ToFloat $previewWidth2x) $img.Width)) }} 21 | {{- end }} 22 | {{- end }} 23 | 24 | {{- partial "image.html" (dict 25 | "Img" $img 26 | "Alt" .Text 27 | "Width" $previewWidth 28 | ) }} 29 | 30 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-image.rss.xml: -------------------------------------------------------------------------------- 1 | {{ .Title }} 2 | -------------------------------------------------------------------------------- /layouts/_default/section.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/layouts/_default/section.html -------------------------------------------------------------------------------- /layouts/_default/section.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/layouts/_default/section.xml -------------------------------------------------------------------------------- /layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{- if .Page.Params.external }} 4 | 5 | 6 | 7 | {{ .Page.Title }} 8 | 9 | 10 | 11 | 12 |

13 |
14 | You'll be redirected in a moment. If it doesn't happen automatically, 15 | please click here.
16 | 17 | {{- else }} 18 | 19 | {{ partial "meta.html" (dict 20 | "title" .Page.Title 21 | "description" .Page.Description 22 | "url" .Page.Permalink 23 | ) }} 24 | 25 | 26 | {{ partial "nav.html" . }} 27 |
28 |

{{ .Title }}

29 |

30 | 33 |

34 | {{ .Content }} 35 |
36 |

37 | << Back to all posts 38 |

39 | {{ partial "footer.html" . }} 40 | 41 | {{- end }} 42 | 43 | -------------------------------------------------------------------------------- /layouts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ partial "meta.html" (dict 5 | "title" "Denis Stebunov – about" 6 | "description" "CTO at ivelum, Teamplify founder" 7 | "url" .Page.Permalink 8 | ) }} 9 | 10 | 11 | {{ partial "nav.html" . }} 12 |

Denis Stebunov

13 | Denis Stebunov 14 | {{- with .GetPage "about" }} 15 | {{ .Content }} 16 | {{ end }} 17 |


18 | {{- with (index (where .Pages "Layout" "ne" "about") 0) }} 19 |

The latest post (see more...)

20 | {{- partial "post-preview.html" . }} 21 | {{ end }} 22 | 23 | {{ partial "footer.html" . }} 24 | 25 | 26 | -------------------------------------------------------------------------------- /layouts/index.xml: -------------------------------------------------------------------------------- 1 | {{- $pages := (where .RegularPages "Params.external" nil).ByDate.Reverse }} 2 | {{- $pages = where $pages "Layout" "eq" "" }} 3 | {{- if not hugo.IsDevelopment }} 4 | {{- $pages = where $pages "Params.sitemap.disable" "ne" true }} 5 | {{- end }} 6 | 7 | {{- $limit := .Site.Config.Services.RSS.Limit }} 8 | {{- if ge $limit 1 }} 9 | {{- $pages = $pages | first $limit }} 10 | {{- end }} 11 | 12 | {{- printf "" | safeHTML }} 13 | 14 | 15 | {{ .Site.Title }} 16 | {{ .Permalink }} 17 | Thoughts about engineering and engineering management 18 | {{ site.Language.LanguageCode }} 19 | {{ with .Site.Copyright }}{{ . }}{{ end }} 20 | {{ if not .Date.IsZero }}{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}{{ end }} 21 | {{- with .OutputFormats.Get "RSS" }} 22 | {{ printf "" .Permalink .MediaType | safeHTML }} 23 | {{- end }} 24 | {{- range $pages }} 25 | 26 | {{ .Title }} 27 | {{ .Permalink }} 28 | {{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }} 29 | {{ .Permalink }} 30 | {{ .Content | transform.XMLEscape | safeHTML }} 31 | 32 | {{- end }} 33 | 34 | 35 | -------------------------------------------------------------------------------- /layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /layouts/partials/image.html: -------------------------------------------------------------------------------- 1 | {{- $webp := false }} 2 | {{- $preview := false }} 3 | {{- $previewWebp := false }} 4 | {{- if ne .Img.MediaType.SubType "svg" }} 5 | {{- $webp = .Img.Process "webp" }} 6 | {{- if gt .Img.Width .Width }} 7 | {{- $preview = .Img.Resize (printf "%dx" .Width) }} 8 | {{- if $webp }} 9 | {{- $previewWebp = $preview.Process "webp" }} 10 | {{ end }} 11 | {{- end }} 12 | {{- end }} 13 | {{ $imgTag := " 20 | {{- if $preview }} 21 | 22 | {{ $imgTag }} src="{{ $preview.RelPermalink }}" srcset="{{ .Img.RelPermalink }} 2x" alt="{{ .Alt }}"> 23 | {{- else }} 24 | 25 | {{ $imgTag }} src="{{ .Img.RelPermalink }}" alt="{{ .Alt }}"> 26 | {{- end }} 27 | 28 | {{- else }} 29 | {{ $imgTag }} src="{{ .Img.RelPermalink }}" alt="{{ .Alt }}"> 30 | {{- end }} 31 | -------------------------------------------------------------------------------- /layouts/partials/meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ .title }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /layouts/partials/nav.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /layouts/partials/post-preview.html: -------------------------------------------------------------------------------- 1 | {{- $url := .RelPermalink }} 2 | {{- $read_more := "Read more..." }} 3 | {{- if .Params.external }} 4 | {{- $url = .Params.external }} 5 | {{- $host := (urls.Parse $url).Hostname }} 6 | {{- $read_more = printf "Read on %s" $host }} 7 | {{- end }} 8 |

{{ .Title }}

9 | 12 |

{{ .Description }}

13 |

14 | 15 | {{ $read_more }} 16 | 17 |

18 | -------------------------------------------------------------------------------- /layouts/section/posts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{- partial "meta.html" (dict 5 | "title" .Site.Title 6 | "description" "Thoughts about software development and engineering management" 7 | "url" .Page.Permalink 8 | ) }} 9 | 10 | 11 | 12 | {{- partial "nav.html" . }} 13 | {{ .Content }} 14 | {{- range $index, $page := where site.RegularPages "Layout" "eq" "" }} 15 | {{- if $index }}
{{ end }} 16 | {{- partial "post-preview.html" $page }} 17 | {{ end }} 18 | {{ partial "footer.html" . }} 19 | 20 | 21 | -------------------------------------------------------------------------------- /static/external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /static/feed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /static/lightbox.css: -------------------------------------------------------------------------------- 1 | #lightbox {width: 100%; height: 100%; position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.85); z-index: 9999999; line-height: 0; cursor: pointer; display: none;} 2 | #lightbox .img { 3 | position: relative; 4 | top: 50%; 5 | left: 50%; 6 | -ms-transform: translateX(-50%) translateY(-50%); 7 | -webkit-transform: translate(-50%,-50%); 8 | transform: translate(-50%,-50%); 9 | max-width: 100%; 10 | max-height: 100%; 11 | } 12 | #lightbox .img img {opacity: 0; pointer-events: none; width: auto;} 13 | @media screen and (min-width: 1200px) { 14 | #lightbox .img { 15 | max-width: 1200px; 16 | } 17 | } 18 | @media screen and (min-height: 1200px) { 19 | #lightbox .img { 20 | max-height: 1200px; 21 | } 22 | } 23 | #lightbox span {display: block; position: fixed; bottom: 13px; height: 1.5em; line-height: 1.4em; width: 100%; text-align: center; color: white; text-shadow: 24 | -1px -1px 0 #000, 25 | 1px -1px 0 #000, 26 | -1px 1px 0 #000, 27 | 1px 1px 0 #000; 28 | } 29 | 30 | #lightbox span {display: none;} 31 | 32 | #lightbox .videoWrapperContainer { 33 | position: relative; 34 | top: 50%; 35 | left: 50%; 36 | -ms-transform: translateX(-50%) translateY(-50%); 37 | -webkit-transform: translate(-50%,-50%); 38 | transform: translate(-50%,-50%); 39 | max-width: 900px; 40 | max-height: 100%; 41 | } 42 | #lightbox .videoWrapperContainer .videoWrapper { 43 | height: 0; 44 | line-height: 0; 45 | margin: 0; 46 | padding: 0; 47 | position: relative; 48 | padding-bottom: 56.333%; /* custom */ 49 | background: black; 50 | } 51 | #lightbox .videoWrapper iframe { 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | border: 0; 58 | display: block; 59 | } 60 | #lightbox #prev, #lightbox #next {height: 50px; line-height: 36px; display: none; margin-top: -25px; position: fixed; top: 50%; padding: 0 15px; cursor: pointer; text-decoration: none; z-index: 99; color: white; font-size: 60px;} 61 | #lightbox.gallery #prev, #lightbox.gallery #next {display: block;} 62 | #lightbox #prev {left: 0;} 63 | #lightbox #next {right: 0;} 64 | #lightbox #close {height: 50px; width: 50px; position: fixed; cursor: pointer; text-decoration: none; z-index: 99; right: 0; top: 0;} 65 | #lightbox #close:after, #lightbox #close:before {position: absolute; margin-top: 22px; margin-left: 14px; content: ""; height: 3px; background: white; width: 23px; 66 | -webkit-transform-origin: 50% 50%; 67 | -moz-transform-origin: 50% 50%; 68 | -o-transform-origin: 50% 50%; 69 | transform-origin: 50% 50%; 70 | /* Safari */ 71 | -webkit-transform: rotate(-45deg); 72 | /* Firefox */ 73 | -moz-transform: rotate(-45deg); 74 | /* IE */ 75 | -ms-transform: rotate(-45deg); 76 | /* Opera */ 77 | -o-transform: rotate(-45deg); 78 | } 79 | #lightbox #close:after { 80 | /* Safari */ 81 | -webkit-transform: rotate(45deg); 82 | /* Firefox */ 83 | -moz-transform: rotate(45deg); 84 | /* IE */ 85 | -ms-transform: rotate(45deg); 86 | /* Opera */ 87 | -o-transform: rotate(45deg); 88 | } 89 | #lightbox, #lightbox * { 90 | -webkit-user-select: none; 91 | -moz-user-select: none; 92 | -ms-user-select: none; 93 | user-select: none; 94 | } -------------------------------------------------------------------------------- /static/lightbox.js: -------------------------------------------------------------------------------- 1 | function is_youtubelink(url) { 2 | var p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; 3 | return (url.match(p)) ? RegExp.$1 : false; 4 | } 5 | function is_imagelink(url) { 6 | var p = /([a-z\-_0-9\/\:\.]*\.(jpg|jpeg|png|gif))/i; 7 | return (url.match(p)) ? true : false; 8 | } 9 | function is_vimeolink(url,el) { 10 | var id = false; 11 | var xmlhttp = new XMLHttpRequest(); 12 | xmlhttp.onreadystatechange = function() { 13 | if (xmlhttp.readyState == XMLHttpRequest.DONE) { // XMLHttpRequest.DONE == 4 14 | if (xmlhttp.status == 200) { 15 | var response = JSON.parse(xmlhttp.responseText); 16 | id = response.video_id; 17 | console.log(id); 18 | el.classList.add('lightbox-vimeo'); 19 | el.setAttribute('data-id',id); 20 | 21 | el.addEventListener("click", function(event) { 22 | event.preventDefault(); 23 | document.getElementById('lightbox').innerHTML = '
'; 24 | document.getElementById('lightbox').style.display = 'block'; 25 | 26 | setGallery(this); 27 | }); 28 | } 29 | else if (xmlhttp.status == 400) { 30 | alert('There was an error 400'); 31 | } 32 | else { 33 | alert('something else other than 200 was returned'); 34 | } 35 | } 36 | }; 37 | xmlhttp.open("GET", 'https://vimeo.com/api/oembed.json?url='+url, true); 38 | xmlhttp.send(); 39 | } 40 | function setGallery(el) { 41 | var elements = document.body.querySelectorAll(".gallery"); 42 | elements.forEach(element => { 43 | element.classList.remove('gallery'); 44 | }); 45 | if(el.closest('ul, p')) { 46 | var link_elements = el.closest('ul, p').querySelectorAll("a[class*='lightbox-']"); 47 | link_elements.forEach(link_element => { 48 | link_element.classList.remove('current'); 49 | }); 50 | link_elements.forEach(link_element => { 51 | if(el.getAttribute('href') == link_element.getAttribute('href')) { 52 | link_element.classList.add('current'); 53 | } 54 | }); 55 | if(link_elements.length>1) { 56 | document.getElementById('lightbox').classList.add('gallery'); 57 | link_elements.forEach(link_element => { 58 | link_element.classList.add('gallery'); 59 | }); 60 | } 61 | var currentkey; 62 | var gallery_elements = document.querySelectorAll('a.gallery'); 63 | Object.keys(gallery_elements).forEach(function (k) { 64 | if(gallery_elements[k].classList.contains('current')) currentkey = k; 65 | }); 66 | if(currentkey==(gallery_elements.length-1)) var nextkey = 0; 67 | else var nextkey = parseInt(currentkey)+1; 68 | if(currentkey==0) var prevkey = parseInt(gallery_elements.length-1); 69 | else var prevkey = parseInt(currentkey)-1; 70 | document.getElementById('next').addEventListener("click", function() { 71 | gallery_elements[nextkey].click(); 72 | }); 73 | document.getElementById('prev').addEventListener("click", function() { 74 | gallery_elements[prevkey].click(); 75 | }); 76 | } 77 | } 78 | 79 | document.addEventListener("DOMContentLoaded", function() { 80 | 81 | //create lightbox div in the footer 82 | var newdiv = document.createElement("div"); 83 | newdiv.setAttribute('id',"lightbox"); 84 | document.body.appendChild(newdiv); 85 | 86 | //add classes to links to be able to initiate lightboxes 87 | var elements = document.querySelectorAll('a'); 88 | elements.forEach(element => { 89 | var url = element.getAttribute('href'); 90 | if(url) { 91 | if(url.indexOf('vimeo') !== -1 && !element.classList.contains('no-lightbox')) { 92 | is_vimeolink(url,element); 93 | } 94 | if(is_youtubelink(url) && !element.classList.contains('no-lightbox')) { 95 | element.classList.add('lightbox-youtube'); 96 | element.setAttribute('data-id',is_youtubelink(url)); 97 | } 98 | if(is_imagelink(url) && !element.classList.contains('no-lightbox')) { 99 | element.classList.add('lightbox-image'); 100 | var href = element.getAttribute('href'); 101 | var filename = href.split('/').pop(); 102 | var split = filename.split("."); 103 | var name = split[0]; 104 | element.setAttribute('title',name); 105 | } 106 | } 107 | }); 108 | 109 | //remove the clicked lightbox 110 | document.getElementById('lightbox').addEventListener("click", function(event) { 111 | if(event.target.id != 'next' && event.target.id != 'prev'){ 112 | this.innerHTML = ''; 113 | document.getElementById('lightbox').style.display = 'none'; 114 | } 115 | }); 116 | 117 | //add the youtube lightbox on click 118 | var elements = document.querySelectorAll('a.lightbox-youtube'); 119 | elements.forEach(element => { 120 | element.addEventListener("click", function(event) { 121 | event.preventDefault(); 122 | document.getElementById('lightbox').innerHTML = '
'; 123 | document.getElementById('lightbox').style.display = 'block'; 124 | 125 | setGallery(this); 126 | }); 127 | }); 128 | 129 | //add the image lightbox on click 130 | var elements = document.querySelectorAll('a.lightbox-image'); 131 | elements.forEach(element => { 132 | element.addEventListener("click", function(event) { 133 | event.preventDefault(); 134 | document.getElementById('lightbox').innerHTML = '
'+this.getAttribute('title')+'
'+this.getAttribute('title')+''; 135 | document.getElementById('lightbox').style.display = 'block'; 136 | 137 | setGallery(this); 138 | }); 139 | }); 140 | 141 | }); -------------------------------------------------------------------------------- /static/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | } 4 | 5 | body { 6 | -webkit-font-smoothing: antialiased; 7 | background-color: #fff; 8 | color: rgb(36, 36, 36); 9 | font-family: source-serif-pro, Georgia, Cambria, "Times New Roman", Times, serif; 10 | font-size: 20px; 11 | font-style: normal; 12 | font-weight: 400; 13 | letter-spacing: -0.003em; 14 | line-break: auto; 15 | line-height: 32px; 16 | margin: 48px auto; 17 | max-width: 680px; 18 | overflow-wrap: normal; 19 | padding: 0 20px; 20 | text-rendering: optimizeLegibility; 21 | text-transform: none; 22 | word-break: break-word; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | font-family: sohne, "Helvetica Neue", Helvetica, Arial, sans-serif; 27 | font-weight: 600; 28 | line-height: 1.1em; 29 | } 30 | 31 | article > h1 { 32 | font-size: 2em; 33 | margin: 0.67em 0; 34 | } 35 | 36 | article > h2 { 37 | margin: -0.83em 0 0.83em; 38 | padding: 0.83em 0 0; 39 | } 40 | 41 | a { 42 | color: inherit; 43 | cursor: pointer; 44 | } 45 | 46 | a.anchor { 47 | text-decoration: none; 48 | } 49 | 50 | a.anchor:hover { 51 | text-decoration: underline; 52 | } 53 | 54 | a.anchor:hover:after { 55 | color: #c0c0c0; 56 | content: ' ¶'; 57 | } 58 | 59 | a.external { 60 | color: #66c43b; 61 | } 62 | 63 | a.external::after { 64 | background-image: url('/external.svg'); 65 | background-size: 1em 1em; 66 | content: ''; 67 | display: inline-block; 68 | height: 1em; 69 | margin-left: 0.3em; 70 | width: 1em; 71 | } 72 | 73 | p, ul, ol { 74 | margin: 0 0 2em; 75 | } 76 | 77 | p.description { 78 | margin: 0 0 0.5em; 79 | } 80 | 81 | li { 82 | margin: 0 0 1em; 83 | } 84 | img { 85 | max-width: 100%; 86 | } 87 | 88 | blockquote { 89 | background-color: #f0f0f0; 90 | border-left: solid 4px #c0c0c0; 91 | margin: 1em 0 2em 2em; 92 | padding: 0.5em 1em; 93 | } 94 | 95 | blockquote > p:last-child { 96 | margin-bottom: 0; 97 | } 98 | 99 | hr { 100 | border: solid 1px #e5e5e5; 101 | margin: 0 0 2em; 102 | } 103 | 104 | time { 105 | display: inline-block; 106 | font-style: italic; 107 | margin-bottom: 0.5em; 108 | } 109 | 110 | nav:after { 111 | content: "."; 112 | display: block; 113 | clear: both; 114 | visibility: hidden; 115 | line-height: 0; 116 | height: 0; 117 | } 118 | 119 | nav { 120 | zoom: 1; 121 | } 122 | 123 | .github { 124 | border: 0; 125 | color: #fff; 126 | fill: #151513; 127 | position: absolute; 128 | right: 0; 129 | top: 0; 130 | z-index: -1 131 | } 132 | 133 | .topLeftMenu { 134 | float: left; 135 | margin: 0; 136 | padding: 0; 137 | } 138 | 139 | .topLeftMenu > li { 140 | display: inline-block; 141 | margin: 0 1em 0 0; 142 | } 143 | 144 | .topRightMenu { 145 | float: right; 146 | margin: 0; 147 | padding: 0; 148 | } 149 | 150 | .topRightMenu > li { 151 | display: inline-block; 152 | margin: 0 0.5em; 153 | } 154 | 155 | .topRightMenu > li img { 156 | height: 28px; 157 | width: 28px; 158 | vertical-align: middle; 159 | } 160 | 161 | .footer { 162 | float: right; 163 | font-size: 0.9em; 164 | font-style: italic; 165 | margin: 15px 0 40px; 166 | } 167 | 168 | @media (prefers-color-scheme: dark) { 169 | body { 170 | background-color: #000; 171 | color: rgb(219, 219, 219); 172 | } 173 | 174 | a.anchor:hover:after { 175 | color: rgb(110, 110, 110); 176 | } 177 | 178 | blockquote { 179 | background-color: #2c2c2c; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /static/photo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stebunovd/blog/9654792e6ad2284de8db69e3ead8badc6e5c1b9c/static/photo.jpeg -------------------------------------------------------------------------------- /static/prettify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function activateAnchors() { 4 | var article = document.querySelector('article'); 5 | if (!article) { 6 | return; 7 | } 8 | document.querySelectorAll('h2').forEach(function (h) { 9 | var id = h.id; 10 | if (id) { 11 | h.innerHTML = '' + h.innerHTML + ''; 12 | } 13 | }); 14 | } 15 | 16 | activateAnchors(); 17 | -------------------------------------------------------------------------------- /static/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------