<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><title>Hyteck</title><link href="https://hyteck.de/"/><generator>Hugo -- gohugo.io</generator><language>en-us</language><id>https://hyteck.de/</id><updated>2025-12-18T20:30:00+02:00</updated><link href="https://hyteck.de/index.xml" rel="self" type="application/rss+xml"/><entry><title>Commitments</title><link href="https://hyteck.de/notes/commitments/" type="application/octet-stream"/><updated>2025-12-18T20:30:00+02:00</updated><id>https://hyteck.de/notes/commitments/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>In the past I have worked with various non-profit association, helping them with their IT. I have amd mixed experiences
how productive and rewarding that was. After two particularly bad experiences I have decided I need a change,
specifically I need more &lt;strong>commitment&lt;/strong>.&lt;/p>
&lt;h1 id="the-problem">The problem&lt;/h1>
&lt;p>The main problem boils down to indecision and missing capacity/priority. As an example: A group wrote an extensive
google doc, detailing about 15 pages on a website that is to be created. After implementing this in a few weeks (mainly
due to necessary changes of already outdated information) a section of the website was ready for final approval, another
one was in a Proof-of-Concept phase. This was finished in September. Since then, I repeatedly asked for review so the
site could go live.
One person reviewed parts, but wanted another person to give feedback as well.
Now, end of December, the finished section is still not live. I refuse to put more work in until the existing work is
reviewed.&lt;/p>
&lt;h1 id="the-solution">The solution&lt;/h1>
&lt;p>Tackle such projects, like I&amp;rsquo;d tackle a project in my day-job: Define stakeholders, a timeline and force decisions when
necessary. Demand commitments.&lt;/p>
&lt;h2 id="scenario-1-basic-website">Scenario 1: Basic Website&lt;/h2>
&lt;p>I believe the most common first step for organizations is to set up a website and be reachable via e-mail. For a first
website there need to be the following things:&lt;/p>
&lt;ul>
&lt;li>Homepage with short description and one Call-to-Action&lt;/li>
&lt;li>Imprint&lt;/li>
&lt;li>Data privacy statement&lt;/li>
&lt;/ul>
&lt;p>A typical process to set up such a website should be&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Kick-Off call&lt;/strong>: Discuss basic content and style elements, clarify technology, cost and timeline&lt;/li>
&lt;li>&lt;strong>Basic implementation&lt;/strong>: The developer will set up a website with the basic style elements and sections for the
content (not filled with real text yet)&lt;/li>
&lt;li>&lt;strong>Content generation&lt;/strong> The organization provides text for the website&lt;/li>
&lt;li>&lt;strong>Implementation&lt;/strong>: Content is added to the website by the developer&lt;/li>
&lt;li>&lt;strong>Testing&lt;/strong>: The organization checks the result, the dev will make minor adjustments&lt;/li>
&lt;li>&lt;strong>Go-Live&lt;/strong>: After confirming a successful test, the website will go-live&lt;/li>
&lt;/ul>
&lt;p>In order to keep risk low, the time between Kick-Off and Go-Live should be planned to last not longer than 6 weeks.
Otherwise, there is a risk that one side puts in a lot of work that is then not rewarded with a Go-Live.&lt;/p>
&lt;p>Here are some other commitments, one could think of:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Organization&lt;/th>
&lt;th>Developer&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Provide text within two weeks after basic implementation&lt;/td>
&lt;td>Provide basic implementation within two weeks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Take not more than one week for testing&lt;/td>
&lt;td>Make minor tweaks within a week&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Provide actionable items when test result hinders Go-Live&lt;/td>
&lt;td>Document infrastructure&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Define central person for functional questions&lt;/td>
&lt;td>Only implement what others could also maintain&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="scenario-2-joining-an-existing-it-team">Scenario 2: Joining an existing IT team&lt;/h2>
&lt;p>Another common scenario is joining an existing IT team. I&amp;rsquo;ve been on both sides of that process and here is how I think
it should work&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Welcome call:&lt;/strong> Who is everyone, who is responsible for what, how is the team organized? Define a first task that
the
new person can reasonably do. The smaller, the better.&lt;/li>
&lt;li>&lt;strong>Technical onboarding:&lt;/strong> Create admin users, invite to password manager, put SSH keys on server&lt;/li>
&lt;li>&lt;strong>Getting started:&lt;/strong> Newbie starts the task, knows who to ask when they need help - someone needs to be available to
answer questions, otherwise frustration and mental overload will settle in soon&lt;/li>
&lt;li>&lt;strong>Integration in regular work&lt;/strong> Newbie is assigned responsibilities and part of the group&lt;/li>
&lt;/ul>
&lt;p>Ideally documentation already exists for the new person to learn. If not, this is a great opportunity to add it! If the
Newbie has a question: Answer the question in detail and past the answer in your documentation. It will benefit you in
the future, I promise!&lt;/p>
&lt;p>So this seems reasonable right? But there are many ways to fuck it up.&lt;/p>
&lt;h3 id="reasons-for-failure">Reasons for failure&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Missing access:&lt;/strong> This is something I have experienced time and time again. From data protection concerns to trust
issues, the problem was always power: Existing admins did trust the newbies with the power they have. And that is the
core problem: if someone has access, they can mess up things. Even the most experienced admins. That&amp;rsquo;s why you should
have backups and written agreements on data protection. If you don&amp;rsquo;t then your existing admin is more of a threat than
the newbie.&lt;/li>
&lt;li>&lt;strong>Unclear processes:&lt;/strong> So often it&amp;rsquo;s unclear how decisions are made, whome to communicate a downtime, and so on.
Newbies are overwhelmed because they don&amp;rsquo;t know what to do, existing teams are annoyed by newbies because they &amp;ldquo;don&amp;rsquo;t
fit in&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>Missing capacity:&lt;/strong> When you onboard people, this will take extra effort. There must be time made for that, you need
new people to succeed. If you don&amp;rsquo;t do onboarding right, the new person will never become part of the team, will
eventually leave and your team will shrink more and more until it fails.&lt;/li>
&lt;/ul>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>New contributors are valuable. Don&amp;rsquo;t waste their time, don&amp;rsquo;t let them feel helpless. Then you will succeed.&lt;/p></content></entry><entry><title>Adding another post type to Hugo</title><link href="https://hyteck.de/notes/adding-post-type-hugo/" type="application/octet-stream"/><updated>2025-12-14T07:30:00+02:00</updated><id>https://hyteck.de/notes/adding-post-type-hugo/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>Notes are short posts that focus on a small bit of information. Other than my blogposts they do not try to give a
complete picture or a full explanation. They are more of a brain-dump of mine with a low bar of typing them out.&lt;/p>
&lt;p>So how did I add them?&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Create a notes folder:&lt;/strong> The folder in &lt;code>content/notes&lt;/code> will hold the notes&lt;/li>
&lt;li>&lt;strong>Add rendering template:&lt;/strong> Hugo needs to know what to do here, so add a rendering template in
&lt;code>layouts/notes/list.html&lt;/code>. A sample is provided below.&lt;/li>
&lt;li>Add &lt;code>content/notes/_index.md&lt;/code> with a short explanation of the&lt;/li>
&lt;/ol>
&lt;h3 id="styling-template">Styling template&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>{{ define &amp;#34;main&amp;#34; }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">h1&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">&amp;#34;&lt;/span>&amp;gt;Notes&amp;lt;/&lt;span style="color:#f92672">h1&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ .Content }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#f92672">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">ul&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ range .Pages.ByDate.Reverse }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">li&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#960050;background-color:#1e0010">&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">h2&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">a&lt;/span> &lt;span style="color:#a6e22e">href&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;{{ .Permalink }} &amp;#34;&lt;/span>&amp;gt; {{ .Title }} &amp;lt;/&lt;span style="color:#f92672">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#f92672">h2&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">small&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">&amp;#34;&lt;/span>&amp;gt;{{ .Date.Format &amp;#34;2006-01-02&amp;#34; }}&amp;lt;/&lt;span style="color:#f92672">small&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ if .Params.subtitle }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">p&lt;/span>&amp;gt;{{ .Params.subtitle }}&amp;lt;/&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ end }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#f92672">li&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ end }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#f92672">ul&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{{ end }}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As you can see, I added an optional subtitel. They make it much easier to find a note that is interesting, but should
not be necessary to lower the bar for the author. Subtitles can be specified like this&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>title: &amp;#34;Adding another post type to Hugo&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date: 2025-12-14T7:30:00+02:00
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>draft: false
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>subtitle: &amp;#34;How I added notes as content type to hugo in addition to the standard posts and pages.&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Notes are short posts
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content></entry><entry><title>How to manually check hundreds of animal shelters - every 14 days</title><link href="https://hyteck.de/post/checking-shelters/" type="application/octet-stream"/><updated>2025-11-08T12:05:10+02:00</updated><id>https://hyteck.de/post/checking-shelters/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I run a website called &lt;a href="https://notfellchen.org">Notfellchen&lt;/a> that list animals that are waiting for adoption. It&amp;rsquo;s
currently restricted to fancy rats in Germany and that for good reason: Running this website involves &lt;strong>checking every
shelter every two weeks manually&lt;/strong>. You need to visit the website, check if there are new animals, contact the shelter
and add them to notfellchen if they allow it. This takes time. A lot.&lt;/p>
&lt;p>This blog post will outline some of the things I did in order to streamline this and make it possible to &lt;strong>check every
german shelter in 2.5 hours&lt;/strong>.&lt;/p>
&lt;h2 id="general-process">General process&lt;/h2>
&lt;p>When you establish a process. want others to help you or if you want to find inefficiencies, it&amp;rsquo;s a good idea to
formalize it. So here is a rough BPMN diagram of the whole process.&lt;/p>
&lt;div class="embedded-html">
&lt;!--[if IE]>&lt;meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" >&lt;![endif]-->
&lt;!DOCTYPE html>
&lt;html>
&lt;head>
&lt;title>Rescue save process&lt;/title>
&lt;meta charset="utf-8"/>
&lt;/head>
&lt;body>&lt;div class="mxgraph" style="max-width:100%;border:1px solid transparent;" data-mxgraph="{&amp;quot;highlight&amp;quot;:&amp;quot;#0000ff&amp;quot;,&amp;quot;nav&amp;quot;:true,&amp;quot;resize&amp;quot;:true,&amp;quot;dark-mode&amp;quot;:&amp;quot;auto&amp;quot;,&amp;quot;toolbar&amp;quot;:&amp;quot;zoom layers tags lightbox&amp;quot;,&amp;quot;edit&amp;quot;:&amp;quot;_blank&amp;quot;,&amp;quot;xml&amp;quot;:&amp;quot;&amp;lt;mxfile host=\&amp;quot;app.diagrams.net\&amp;quot; agent=\&amp;quot;Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0\&amp;quot; version=\&amp;quot;28.2.7\&amp;quot;&amp;gt;\n &amp;lt;diagram name=\&amp;quot;Page-1\&amp;quot; id=\&amp;quot;ZPuKLs6SsL8PyHUOEfSV\&amp;quot;&amp;gt;\n &amp;lt;mxGraphModel dx=\&amp;quot;1099\&amp;quot; dy=\&amp;quot;1436\&amp;quot; grid=\&amp;quot;1\&amp;quot; gridSize=\&amp;quot;10\&amp;quot; guides=\&amp;quot;1\&amp;quot; tooltips=\&amp;quot;1\&amp;quot; connect=\&amp;quot;1\&amp;quot; arrows=\&amp;quot;1\&amp;quot; fold=\&amp;quot;1\&amp;quot; page=\&amp;quot;1\&amp;quot; pageScale=\&amp;quot;1\&amp;quot; pageWidth=\&amp;quot;850\&amp;quot; pageHeight=\&amp;quot;1100\&amp;quot; math=\&amp;quot;0\&amp;quot; shadow=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;root&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;0\&amp;quot; /&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;1\&amp;quot; parent=\&amp;quot;0\&amp;quot; /&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-1\&amp;quot; value=\&amp;quot;\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=standard;symbol=general;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;50\&amp;quot; y=\&amp;quot;135\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-2\&amp;quot; value=\&amp;quot;\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=end;symbol=terminate2;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1780\&amp;quot; y=\&amp;quot;85\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-3\&amp;quot; value=\&amp;quot;Open animal shelter website\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;130\&amp;quot; y=\&amp;quot;120\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-4\&amp;quot; value=\&amp;quot;\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=end;symbol=terminate2;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;620\&amp;quot; y=\&amp;quot;20\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-5\&amp;quot; value=\&amp;quot;Mark organization as checked\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;450\&amp;quot; y=\&amp;quot;5\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-6\&amp;quot; value=\&amp;quot;Notfellchen-relevant&amp;amp;lt;br&amp;amp;gt;animals there?\&amp;quot; style=\&amp;quot;points=[[0.25,0.25,0],[0.5,0,0],[0.75,0.25,0],[1,0.5,0],[0.75,0.75,0],[0.5,1,0],[0.25,0.75,0],[0,0.5,0]];shape=mxgraph.bpmn.gateway2;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=rhombusPerimeter;outlineConnect=0;outline=none;symbol=none;gwType=exclusive;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;290\&amp;quot; y=\&amp;quot;135\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-7\&amp;quot; value=\&amp;quot;Animals&amp;amp;lt;br&amp;amp;gt;&amp;amp;lt;div&amp;amp;gt;already&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div&amp;amp;gt;listed on&amp;amp;lt;br&amp;amp;gt;Notfellchen?&amp;amp;lt;/div&amp;amp;gt;\&amp;quot; style=\&amp;quot;points=[[0.25,0.25,0],[0.5,0,0],[0.75,0.25,0],[1,0.5,0],[0.75,0.75,0],[0.5,1,0],[0.25,0.75,0],[0,0.5,0]];shape=mxgraph.bpmn.gateway2;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=rhombusPerimeter;outlineConnect=0;outline=none;symbol=none;gwType=exclusive;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;400\&amp;quot; y=\&amp;quot;135\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-8\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-7\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-5\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-9\&amp;quot; value=\&amp;quot;Yes\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-8\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.3565\&amp;quot; y=\&amp;quot;-3\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;-3\&amp;quot; y=\&amp;quot;22\&amp;quot; as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-10\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-6\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-5\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-11\&amp;quot; value=\&amp;quot;No\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-10\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.84\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-12\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-5\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-4\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-13\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-3\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-6\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-14\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-6\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-7\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-15\&amp;quot; value=\&amp;quot;Yes\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-14\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.3333\&amp;quot; y=\&amp;quot;1\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-16\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-18\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-25\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-17\&amp;quot; value=\&amp;quot;No\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-16\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.088\&amp;quot; y=\&amp;quot;3\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-18\&amp;quot; value=\&amp;quot;Organization&amp;amp;lt;br&amp;amp;gt;allows&amp;amp;lt;br&amp;amp;gt;listing?\&amp;quot; style=\&amp;quot;points=[[0.25,0.25,0],[0.5,0,0],[0.75,0.25,0],[1,0.5,0],[0.75,0.75,0],[0.5,1,0],[0.25,0.75,0],[0,0.5,0]];shape=mxgraph.bpmn.gateway2;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=rhombusPerimeter;outlineConnect=0;outline=none;symbol=none;gwType=exclusive;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;510\&amp;quot; y=\&amp;quot;135\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-19\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-7\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-18\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-20\&amp;quot; value=\&amp;quot;No\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-19\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.2\&amp;quot; y=\&amp;quot;2\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-21\&amp;quot; value=\&amp;quot;Add animals to Notfellchen\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1610\&amp;quot; y=\&amp;quot;70\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-22\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-18\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-21\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;Array as=\&amp;quot;points\&amp;quot;&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;610\&amp;quot; y=\&amp;quot;160\&amp;quot; /&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;610\&amp;quot; y=\&amp;quot;110\&amp;quot; /&amp;gt;\n &amp;lt;/Array&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-23\&amp;quot; value=\&amp;quot;Yes\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-22\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.4\&amp;quot; y=\&amp;quot;1\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;-230\&amp;quot; y=\&amp;quot;26\&amp;quot; as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-24\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-1\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-3\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-25\&amp;quot; value=\&amp;quot;Mark organization as &amp;amp;quot;Ongoing conversation&amp;amp;quot;\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;550\&amp;quot; y=\&amp;quot;265\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-26\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-21\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-2\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-27\&amp;quot; value=\&amp;quot;Send E-Mail asking for listing permission\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;710\&amp;quot; y=\&amp;quot;265\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-28\&amp;quot; value=\&amp;quot;\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=standard;symbol=message;\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-27\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;80\&amp;quot; y=\&amp;quot;60\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-29\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-25\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-27\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-30\&amp;quot; value=\&amp;quot;Answer received\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=catching;symbol=message;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;880\&amp;quot; y=\&amp;quot;370\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-31\&amp;quot; value=\&amp;quot;min. 1 day\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=standard;symbol=timer;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;880\&amp;quot; y=\&amp;quot;280\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-32\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-27\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-31\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-33\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-34\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-36\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-34\&amp;quot; value=\&amp;quot;Call\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;970\&amp;quot; y=\&amp;quot;265\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-35\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-31\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-34\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-36\&amp;quot; value=\&amp;quot;phone was&amp;amp;lt;br&amp;amp;gt;answered?\&amp;quot; style=\&amp;quot;points=[[0.25,0.25,0],[0.5,0,0],[0.75,0.25,0],[1,0.5,0],[0.75,0.75,0],[0.5,1,0],[0.25,0.75,0],[0,0.5,0]];shape=mxgraph.bpmn.gateway2;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=rhombusPerimeter;outlineConnect=0;outline=none;symbol=none;gwType=exclusive;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1120\&amp;quot; y=\&amp;quot;280\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-37\&amp;quot; value=\&amp;quot;any\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=standard;symbol=timer;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1005\&amp;quot; y=\&amp;quot;185\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-38\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-36\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-37\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-39\&amp;quot; value=\&amp;quot;nein\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-38\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.713\&amp;quot; y=\&amp;quot;1\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-40\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-37\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-34\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;Array as=\&amp;quot;points\&amp;quot;&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;950\&amp;quot; y=\&amp;quot;210\&amp;quot; /&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;950\&amp;quot; y=\&amp;quot;305\&amp;quot; /&amp;gt;\n &amp;lt;/Array&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-41\&amp;quot; value=\&amp;quot;Answered\&amp;quot; style=\&amp;quot;points=[[0.25,0.25,0],[0.5,0,0],[0.75,0.25,0],[1,0.5,0],[0.75,0.75,0],[0.5,1,0],[0.25,0.75,0],[0,0.5,0]];shape=mxgraph.bpmn.gateway2;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=rhombusPerimeter;outlineConnect=0;outline=none;symbol=none;gwType=exclusive;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1320\&amp;quot; y=\&amp;quot;280\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-42\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitDx=0;exitDy=0;exitPerimeter=0;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;entryX=1;entryY=0.5;exitX=0.5;exitY=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-41\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-44\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;1190\&amp;quot; y=\&amp;quot;200\&amp;quot; as=\&amp;quot;targetPoint\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-43\&amp;quot; value=\&amp;quot;unclear\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-42\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.7988\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-44\&amp;quot; value=\&amp;quot;Log call in internal comment including date\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1170\&amp;quot; y=\&amp;quot;170\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-45\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-44\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-37\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-46\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-36\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-41\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-47\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-41\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-49\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-48\&amp;quot; value=\&amp;quot;Usage&amp;amp;lt;br&amp;amp;gt;allowed\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-47\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.2077\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-49\&amp;quot; value=\&amp;quot;Log status and add internal comment including date and medium\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1430\&amp;quot; y=\&amp;quot;170\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-50\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-49\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-21\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-51\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-52\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-55\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-52\&amp;quot; value=\&amp;quot;Log denial status and in internal comment including the reason of denial\&amp;quot; style=\&amp;quot;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];shape=mxgraph.bpmn.task2;whiteSpace=wrap;rectStyle=rounded;size=10;html=1;container=1;expand=0;collapsible=0;taskMarker=abstract;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1440\&amp;quot; y=\&amp;quot;360\&amp;quot; width=\&amp;quot;120\&amp;quot; height=\&amp;quot;80\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-53\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-41\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-52\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-54\&amp;quot; value=\&amp;quot;Usage&amp;amp;lt;br&amp;amp;gt;denied\&amp;quot; style=\&amp;quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\&amp;quot; parent=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-53\&amp;quot; vertex=\&amp;quot;1\&amp;quot; connectable=\&amp;quot;0\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;-0.2282\&amp;quot; relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;mxPoint as=\&amp;quot;offset\&amp;quot; /&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-55\&amp;quot; value=\&amp;quot;\&amp;quot; style=\&amp;quot;points=[[0.145,0.145,0],[0.5,0,0],[0.855,0.145,0],[1,0.5,0],[0.855,0.855,0],[0.5,1,0],[0.145,0.855,0],[0,0.5,0]];shape=mxgraph.bpmn.event;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;align=center;perimeter=ellipsePerimeter;outlineConnect=0;aspect=fixed;outline=end;symbol=terminate2;\&amp;quot; parent=\&amp;quot;1\&amp;quot; vertex=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry x=\&amp;quot;1600\&amp;quot; y=\&amp;quot;375\&amp;quot; width=\&amp;quot;50\&amp;quot; height=\&amp;quot;50\&amp;quot; as=\&amp;quot;geometry\&amp;quot; /&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;mxCell id=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-56\&amp;quot; style=\&amp;quot;edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;curved=0;\&amp;quot; parent=\&amp;quot;1\&amp;quot; source=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-30\&amp;quot; target=\&amp;quot;kHcOzL8m4RCkJ4hDhTre-41\&amp;quot; edge=\&amp;quot;1\&amp;quot;&amp;gt;\n &amp;lt;mxGeometry relative=\&amp;quot;1\&amp;quot; as=\&amp;quot;geometry\&amp;quot;&amp;gt;\n &amp;lt;Array as=\&amp;quot;points\&amp;quot;&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;1280\&amp;quot; y=\&amp;quot;395\&amp;quot; /&amp;gt;\n &amp;lt;mxPoint x=\&amp;quot;1280\&amp;quot; y=\&amp;quot;305\&amp;quot; /&amp;gt;\n &amp;lt;/Array&amp;gt;\n &amp;lt;/mxGeometry&amp;gt;\n &amp;lt;/mxCell&amp;gt;\n &amp;lt;/root&amp;gt;\n &amp;lt;/mxGraphModel&amp;gt;\n &amp;lt;/diagram&amp;gt;\n&amp;lt;/mxfile&amp;gt;\n&amp;quot;}">&lt;/div>
&lt;script type="text/javascript" src="https://hyteck.de/js/viewer-static.min.js">&lt;/script>
&lt;/body>
&lt;/html>
&lt;/div>
&lt;style>
.geAdaptiveAsset {
width: unset;
}
&lt;/style>
&lt;h2 id="list-of-animal-shelters">List of animal shelters&lt;/h2>
&lt;p>Focusing on the first step: We want to check the website of an animal shelter - but where do we get a list of animal
shelters from? Luckily there is an easy answer: &lt;a href="https://openstreetmap.org">OpenStreetMap&lt;/a> and I wrote a
whole &lt;a href="https://hyteck.de/post/improve-osm-by-using-it/">other blog post on how I imported and improved this data&lt;/a>.&lt;/p>
&lt;h2 id="species-specific-link">Species-specific link&lt;/h2>
&lt;p>Importing this data provides us (most of the time) with a link to the shelter&amp;rsquo;s website. However, rats are usually not
listed on the home page but on a subsite.
In order to save time, I introduced the concept of a species-specific link per organization and species.&lt;/p>
&lt;p>So for the Tierheim Entenhausen this might look like this&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Species&lt;/th>
&lt;th>Species specific link&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cat&lt;/td>
&lt;td>&lt;a href="https://tierheim-entenhausen.de/adoption/cats">https://tierheim-entenhausen.de/adoption/cats&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rats&lt;/td>
&lt;td>&lt;a href="https://tierheim-entenhausen.de/adoption/small-mammals">https://tierheim-entenhausen.de/adoption/small-mammals&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>As animal shelter pages look very different from each other, clicking this link provides an enormous time benefit
compared to clicking through a homepage manually.&lt;/p>
&lt;h1 id="org-check-page">Org check page&lt;/h1>
&lt;p>I set up a special page to make it most efficient to check shelters. It&amp;rsquo;s structured in four parts:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Stats&lt;/strong>: The stats show how many animal shelters are checked in the last two weeks and how many to go.&lt;/li>
&lt;li>&lt;strong>Not checked for the longest period&lt;/strong>: Shows the animal shelters to check next, it&amp;rsquo;s therefore sorted by the date
they were last checked&lt;/li>
&lt;li>&lt;strong>In active communication&lt;/strong>: A overview of the organizations where there is communication (or an attempt thereof).
This can take multiple das or even weeks so the internal comment field is very useful to keep track.&lt;/li>
&lt;li>&lt;strong>Last checked&lt;/strong> It sometimes happens that I accidentally set a organization to &amp;ldquo;Checked&amp;rdquo; by accident. I added this
section to make it easier to revert that.&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="screenshot-checking-site.png" alt="A screenshot of the Notfellchen-Website showing the mentiond sections. Each rescue organization is listed with their website, species specific urls and shows the internal comment. There are three color coded buttons: &amp;ldquo;Organization checked&amp;rdquo;, &amp;ldquo;Active Communication&amp;rdquo; and &amp;ldquo;Exclude from check&amp;rdquo;">&lt;/p>
&lt;h2 id="shortcuts">Shortcuts&lt;/h2>
&lt;p>To make it even faster to work through the organizations I added some shortcuts for the most common functionality and
documented the browser own shortcut to close a tab.&lt;/p>
&lt;ul>
&lt;li>&lt;code>O&lt;/code>: Open website of the first organization&lt;/li>
&lt;li>&lt;code>CTRL+W&lt;/code>: Close tab (Firefox, Chrome)&lt;/li>
&lt;li>&lt;code>C&lt;/code>: Mark first organization as checked&lt;/li>
&lt;/ul>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>After implementing all this, how long does it take now to check all organizations? Here are the numbers&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Measurement&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Time to check one organization (avg.)&lt;/td>
&lt;td>12.1s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Organization checked per minute&lt;/td>
&lt;td>4.96 org/min&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Time to check all (eligible) german animal shelters (429)&lt;/td>
&lt;td>1 h 16 min&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This excludes the time, it takes to add animals or contact rescue organizations. One of these actions must be taken
whenever an eligible animal is found on a website. Here you can see how this interrupts the process:&lt;/p>
&lt;p>&lt;img src="progress.png" alt="A diagramm showing time on the x axis and number of shelters checked on the y axis. In the period from 11am to 12:15pm there are over 200 shelters checked at a relativly constanct rate. The checking is interrupted two times by adding animals and three times by contacting">&lt;/p>
&lt;p>And here is the breakdown of time per activity. A big caveat here is, that I did not follow up on previous conversations
here, therefore the contacting number is likely an underestimation.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Activity&lt;/th>
&lt;th>Time spent&lt;/th>
&lt;th>Percentage&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Checking&lt;/td>
&lt;td>54 min 44s&lt;/td>
&lt;td>72.3%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adding&lt;/td>
&lt;td>11 min 15s&lt;/td>
&lt;td>14.9%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Contacting&lt;/td>
&lt;td>9min 41s&lt;/td>
&lt;td>12.8%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>To me, this looks like a pretty good result. I can&amp;rsquo;t say which optimizations brought how much improvement, but I&amp;rsquo;d argue
they all play a role in reaching the 12s per rescue organizations that is checked.&lt;/p>
&lt;p>In order to check all german animal shelters, one needs to put in about 2 and a half hours every two weeks. That seems
reasonable to me. Further improvements of the likely do not lie in the organization check page but the contact process
and adoption notice form.&lt;/p>
&lt;p>For now, I&amp;rsquo;m happy with the results.&lt;/p>
&lt;h1 id="addendum-common-annoyances">Addendum: Common annoyances&lt;/h1>
&lt;p>When doing this over the last few months I encountered some recurring issues that not only were annoying but also take
up a majority of the time. Here are some that stood out&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Broken SSL encryption&lt;/strong> So many animal shelters do not have a functioning SSL certificate. It takes time to work
around the warnings.&lt;/li>
&lt;li>&lt;strong>No results not indicated&lt;/strong> More often than not, animal shelters do not have rats. However, when you visit a page
like &lt;a href="https://tierschutzliga.de/tierheime/tierparadies-oberdinger-moos/tiervermittlung/#?THM=TOM&amp;amp;Tierart=Kleintiere">this&lt;/a>
it&amp;rsquo;s hard to know if there is a technical issue or if there are no animals for your search.&lt;/li>
&lt;li>&lt;strong>No static links&lt;/strong> Sites where you have to click through a menu to get to the right page, but you can not link
directly to it.&lt;/li>
&lt;li>&lt;strong>No website&lt;/strong> Seriously, there are some animal shelters that only use Instagram or Facebook to tell people about the
animals they have. This is not only a privacy nightmare, it&amp;rsquo;s also incredibly hard to find out which information is
up-to-date. Furthermore, there exists no data structure, so posts about animals often miss crucial information like
the sex.&lt;/li>
&lt;/ul>
&lt;p>While I obviously have some grievances here, I know the organizations never have enough resources, and they&amp;rsquo;d
usually love to have a nicer website. Just keep that in mind too.&lt;/p></content></entry><entry><title>Postmortem - how to completely screw up an update</title><link href="https://hyteck.de/post/postmortem-gpa/" type="application/octet-stream"/><updated>2025-10-19T12:05:10+02:00</updated><id>https://hyteck.de/post/postmortem-gpa/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>The fediverse instance &lt;a href="https://gay-pirate-assassins.de">gay-pirate-assassins.de&lt;/a> was down for a couple of days. This
postmortem will outline what went wrong and what I did to prevent things from going that wrong in the future.&lt;/p>
&lt;h1 id="timeline">Timeline&lt;/h1>
&lt;ul>
&lt;li>2025-10-05 17:26: &lt;a href="https://gay-pirate-assassins.de/@moanos/statuses/01K6TFQ1HVPAR6AYN08XYQ7XFV">Update announcement&lt;/a>&lt;/li>
&lt;li>2025-10-05 ~17:45: Update started&lt;/li>
&lt;li>2025-10-05 ~18:00: Services restart&lt;/li>
&lt;li>2025-10-05 ~18:00: GoToSocial doesn&amp;rsquo;t come up&lt;/li>
&lt;li>2025-10-12 ~10:00: Issue is found&lt;/li>
&lt;li>2025-10-12 10:30: Issue is fixed&lt;/li>
&lt;li>2025-10-12 10:31: GoToSocial is started, migrations start&lt;/li>
&lt;li>2025-10-12 15:38: Migrations finished successfully&lt;/li>
&lt;li>2025-10-12 15:38: Service available again&lt;/li>
&lt;li>2025-10-12 18:36:&lt;a href="https://gay-pirate-assassins.de/@moanos/statuses/01K7CMGF7S2TE39792CMADGEPJ">Announcement sent&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>All times are given in CEST.&lt;/p>
&lt;h2 id="the-beginning-an-update-goes-wrong">The beginning: An update goes wrong&lt;/h2>
&lt;p>I run a small fediverse server with a few users called. &lt;a href="https://gay-pirate-assassins.de/">gay-pirate-assassins&lt;/a> which is powered by &lt;a href="https://gotosocial.org/">GoToSocial&lt;/a>.
The (amazing) GoToSocial devs released &lt;code>v0.20.0-rc1&lt;/code> and &lt;code>v0.20.0-rc2&lt;/code>. As the new features seemed pretty cool, I&amp;rsquo;m
inpatient and the second release candidate seemed stable,
I decided to update to &lt;code>v0.20.0-rc2&lt;/code>. So I stared a backup (via borgmatic), waited for it to finish and confirmed it ran
successfully.
Then I changed the version number in the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook">mash&lt;/a>-ansible
playbook I use. Then I pulled the newest version of the playbook and it&amp;rsquo;s roles because I wanted to update all services
that run on the server. I checked
the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/CHANGELOG.md">Changelog&lt;/a>,
didn&amp;rsquo;t see anything and then started the update. It went through and GoToSocial started up just fine.&lt;/p>
&lt;p>But the instance start page showed me 0 users, 0 posts and 0 federated instances. &lt;strong>Something has gone horribly wrong!&lt;/strong>&lt;/p>
&lt;h2 id="migrations">Migrations&lt;/h2>
&lt;p>It was pretty clear to me, that the migrations went wrong.
The &lt;a href="https://codeberg.org/superseriousbusiness/gotosocial/releases/tag/v0.20.0-rc1">GoToSocial Migration notes&lt;/a>
specifically mentioned long-running migrations that could take several hours. I assumed that somehow, during the running
database migration, the service must have restarted and left the DB in a broken state. This issue happened to me before.&lt;/p>
&lt;p>Well, that&amp;rsquo;s what backups are for, so let&amp;rsquo;s pull it.&lt;/p>
&lt;h2 id="backups">Backups&lt;/h2>
&lt;p>Backups for this server are done two ways:&lt;/p>
&lt;ul>
&lt;li>via postgres-backup: Backups of the database are written to disk&lt;/li>
&lt;li>via &lt;a href="https://torsion.org/borgmatic/">borgmatic&lt;/a>: Backups via borg are written to backup nodes, one of them at my home&lt;/li>
&lt;/ul>
&lt;p>They run every night automatically, monitored by &lt;a href="https://healthchecks.io/">Healthchecks&lt;/a>. I triggered a manual run
before the update so that is the one I mounted using &lt;a href="https://vorta.borgbase.com/">Vorta&lt;/a>.&lt;/p>
&lt;p>And then the realization.&lt;/p>
&lt;pre tabindex="0">&lt;code>
mash-postgres:5432 $ ls -lh
total 2.1M
-r-------- 1 moanos root 418K Oct 05 04:03 gitea
-r-------- 1 moanos root 123K Oct 05 04:03 healthchecks
-r-------- 1 moanos root 217K Oct 05 04:03 ilmo
-r-------- 1 moanos root 370K Oct 05 04:03 notfellchen
-r-------- 1 moanos root 67K Oct 05 04:03 oxitraffic
-r-------- 1 moanos root 931 Oct 05 04:03 prometheus_postgres_exporter
-r-------- 1 moanos root 142K Oct 05 04:03 semaphore
-r-------- 1 moanos root 110K Oct 05 04:03 vaultwarden
-r-------- 1 moanos root 669K Oct 05 04:03 woodpecker_ci_server
&lt;/code>&lt;/pre>&lt;p>Fuck. The database gay-pirate-assassins is not there. Why?&lt;/p>
&lt;p>To explain that I have to tell you how it &lt;em>should&lt;/em> work: Services deployed by the mash-playbook are automatically wired
to the database and reverse proxy by a complex set of Ansible variables. This is great, because adding a service can
therefore be as easy as adding&lt;/p>
&lt;pre tabindex="0">&lt;code>healthchecks_enabled: true
healthchecks_hostname: health.hyteck.de
&lt;/code>&lt;/pre>&lt;p>to the &lt;code>vars.yml&lt;/code> file.&lt;/p>
&lt;p>This will then configure the postgres database automatically, based on the &lt;code>group_vars&lt;/code>. They look like this&lt;/p>
&lt;pre tabindex="0">&lt;code>mash_playbook_postgres_managed_databases_auto_itemized:
- |-
{{
({
&amp;#39;name&amp;#39;: healthchecks_database_name,
&amp;#39;username&amp;#39;: healthchecks_database_username,
&amp;#39;password&amp;#39;: healthchecks_database_password,
} if healthchecks_enabled and healthchecks_database_hostname == postgres_connection_hostname and healthchecks_database_type == &amp;#39;postgres&amp;#39; else omit)
}}
&lt;/code>&lt;/pre>&lt;p>Note that a healthchecks database is only added to the managed databases if &lt;code>healthchecks_enabled&lt;/code> is &lt;code>True&lt;/code>.&lt;/p>
&lt;p>This is really useful for backups because the borgmatic configuration also pulls the list
&lt;code>mash_playbook_postgres_managed_databases_auto_itemized&lt;/code>. Therefore, you do not need to specify which databases to back
up, it just backs up all managed databases.&lt;/p>
&lt;p>However, the database for gay-pirate assassins was not managed. In the playbook it&amp;rsquo;s only possible to configure a
service once. You can not manage multiple GoToSocial instances in the same &lt;code>vars.yml&lt;/code>. In the past, I had two instances
of GoToSocial running on the server. I therefore
followed &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/running-multiple-instances.md">the how-to of &amp;ldquo;Running multiple instances of the same service on the same host&amp;rdquo;&lt;/a>.&lt;/p>
&lt;p>Basically this means that an additional &lt;code>vars.yml&lt;/code> must be created that is treated as a completely different server.
Databases must be created manually as they are not managed.&lt;/p>
&lt;p>With that knowledge you can understand that when I say that the database for gay-pirate-assassins was not managed,
this means it was not included in the list of databases to be backed up. The backup service thought it ran successfully,
because it backed up everything it knew of.&lt;/p>
&lt;p>So this left me with a three-month-old backup. Unacceptable.&lt;/p>
&lt;h2 id="investigating">Investigating&lt;/h2>
&lt;p>So the existing database needed to be rescued. I SSHed into the server and checked the database. It looked completely
normal.
I asked the devs if they could me provide me with the migrations as they already did in the past. However, they pointed
out that the migrations are too difficult for that approach. They suggested to delete the oldest migration to force a
re-run of the migrations.&lt;/p>
&lt;p>Here is where I was confused, because this was the &lt;code>bun_migrations&lt;/code> table:&lt;/p>
&lt;pre tabindex="0">&lt;code>gay-pirate-assassins=# SELECT * FROM bun_migrations ORDER BY id DESC LIMIT 5;
id | name | group_id | migrated_at
-----+----------------+----------+-------------------------------
193 | 20250324173534 | 20 | 2025-04-23 20:00:33.955776+00
192 | 20250321131230 | 20 | 2025-04-23 19:58:06.873134+00
191 | 20250318093828 | 20 | 2025-04-23 19:57:50.540568+00
190 | 20250314120945 | 20 | 2025-04-23 19:57:30.677481+00
&lt;/code>&lt;/pre>&lt;p>The last migration ran in April, when I updated to &lt;code>v0.19.1&lt;/code>. Strange.&lt;/p>
&lt;p>At this point I went on vacation and paused investigations, not only because the vacation was great, but also because I
bamboozeld by this state.&lt;/p>
&lt;hr>
&lt;p>After my vacation I came back, and did some backups of the database.&lt;/p>
&lt;pre tabindex="0">&lt;code>$ docker run -e PGPASSWORD=&amp;#34;XXXX&amp;#34; -it --rm --network mash-postgres postgres pg_dump -U gay-pirate-assassins -h mash-postgres gay-pirate-assassins &amp;gt; manual-backup/gay-pirate-assassins-2025-10-13.sql
&lt;/code>&lt;/pre>&lt;p>Then I deleted the last migration, as I was advised&lt;/p>
&lt;pre tabindex="0">&lt;code>DELETE FROM bun_migration WHERE id=193;
&lt;/code>&lt;/pre>&lt;p>and restarted the server. While watching the server come up it hit me in the face:&lt;/p>
&lt;pre tabindex="0">&lt;code>Oct 12 08:31:29 s3 mash-gpa-gotosocial[2251925]: timestamp=&amp;#34;12/10/2025 08:31:29.905&amp;#34; func=bundb.sqliteConn level=INFO msg=&amp;#34;connected to SQLITE database with address file:/opt/gotosocial/sqlite.db?_pragma=busy_timeout%281800000%29&amp;amp;_pragma=journal_mode%&amp;gt;
Oct 12 13:38:46 s3 mash-gpa-gotosocial[2304549]: timestamp=&amp;#34;12/10/2025 13:38:46.588&amp;#34; func=router.(*Router).Start.func1 level=INFO msg=&amp;#34;listening on 0.0.0.0:8080&amp;#34;
&lt;/code>&lt;/pre>&lt;p>The server is &lt;strong>starting from a completely different database&lt;/strong>! That explains why&lt;/p>
&lt;ul>
&lt;li>the last migration was never done&lt;/li>
&lt;li>the server showed me 0 users, 0 posts and 0 federated instances even though the postgres database had plenty of those&lt;/li>
&lt;/ul>
&lt;p>All of a sudden a SQlite database was configured. This happened because
of &lt;a href="https://github.com/mother-of-all-self-hosting/ansible-role-gotosocial/commit/df34af385f9765bda8f160f6985a47cb7204fe96">this commit&lt;/a>
which introduced SQlite support and set it as default. This was not mentioned in
the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/CHANGELOG.md">Changelog&lt;/a>.&lt;/p>
&lt;p>So what happened is, that the config changed and then the server was restarted and an empty DB was initialized. The
postgres DB never started to migrate.&lt;/p>
&lt;h2 id="fixing">Fixing&lt;/h2>
&lt;p>To fix it, I did the following&lt;/p>
&lt;ol>
&lt;li>Configure the playbook to use postgres for GoToSocial:&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code># vars.yml
gotosocial_database_type: postgres
&lt;/code>&lt;/pre>&lt;ol start="2">
&lt;li>Run the playbook to configure GoToSocial (but not starting the service)&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>just run-tags install-gotosocial
&lt;/code>&lt;/pre>&lt;ol start="3">
&lt;li>Check the configuration is correct&lt;/li>
&lt;li>Start the service&lt;/li>
&lt;/ol>
&lt;p>The migrations took several hours but after that, everything looked stable again. I don&amp;rsquo;t think there are any lasting
consequences. However, the server was unavailable for several days.&lt;/p>
&lt;h2 id="learnings">Learnings&lt;/h2>
&lt;p>I believe the main issue here was not the change in the config that went unnoticed by me. While I&amp;rsquo;d ideally notice stuff
like this, the server is a hobby, and I&amp;rsquo;ll continue to not check every config option that changed.&lt;/p>
&lt;p>The larger issue was the backup. Having a backup would have made this easy to solve. And there are other, less lucky
problems where I&amp;rsquo;d be completely lost without a backup. So to make sure this doesn&amp;rsquo;t happen again, I did/will do the
following:&lt;/p>
&lt;h3 id="1-mainstream-the-config">1. Mainstream the config&lt;/h3>
&lt;p>As explained, I used a specific non-mainstream setup in the ansible playbook because, in the past, I ran two instances
of GoToSocial on the server. After shutting down one of them, I never moved gay-pirate-assassins to be part of the main
config. This means important parts of the configuration had to be done manually, which I botched.&lt;/p>
&lt;p>So in the past week I cleaned up and gay-pirate-assassins is now part of the main &lt;code>vars.yml&lt;/code> and will benefit from all
relevant automations.&lt;/p>
&lt;h3 id="2-checking-backups">2. Checking backups&lt;/h3>
&lt;p>I was confident in my backups because&lt;/p>
&lt;ul>
&lt;li>they run every night very consistently. If they fail e.g. because of a network outage I reliably get a warning.&lt;/li>
&lt;li>I verified successfully run of the backup job prior to upgrading&lt;/li>
&lt;/ul>
&lt;p>The main problem was me assuming that a successful run of the backup command, meant a successful backup. Everyone will
tell you that a backup that is not tested is not to be trusted. And they are right. However, doing frequent
test-restores
exceeds my time and server capacity. So what I&amp;rsquo;ll do instead is the following:&lt;/p>
&lt;ul>
&lt;li>mount the backup before an upgrade&lt;/li>
&lt;li>&lt;code>tail&lt;/code> the backup file as created by postgres-backup and ensure the data is from the same day&lt;/li>
&lt;li>check media folders for the last changed image&lt;/li>
&lt;/ul>
&lt;p>This is not a 100% guarantee, but I&amp;rsquo;d argue it&amp;rsquo;s a pretty good compromise for now. As the frequency of mounting backups
increases and therefore becomes faster, I&amp;rsquo;ll re-evaluate to do a test-restore at least semi-regulary.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>I fucked up, but I was lucky that my error was recoverable and no data was lost. Next time this will hopefully be not due
to luck, but better planning!&lt;/p>
&lt;p>Any questions? Let me know!&lt;/p></content></entry><entry><title>Trying Twenty: How does an Open Source CRM work?</title><link href="https://hyteck.de/post/trying-twenty/" type="application/octet-stream"/><updated>2025-08-03T06:10:10+02:00</updated><id>https://hyteck.de/post/trying-twenty/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I spend my day working with Salesforce, a very, very feature-rich CRM that you pay big money to use.
Salesforce is the opposite of OpenSource and the many features are expensive. Salesforce business model is based on this
and on the lock-in effect.
If your company invested in implementing Salesforce, they&amp;rsquo;ll likely pay a lot to keep it.&lt;/p>
&lt;p>So what does an alternative look like? Let&amp;rsquo;s have a look at &lt;a href="https://twenty.com">Twenty&lt;/a>, an OpenSource CRM that
recently reached the magic 1.0 version.&lt;/p>
&lt;h1 id="getting-started">Getting started&lt;/h1>
&lt;p>There are two options of getting started: Register at &lt;a href="https://app.twenty.com">app.twenty.com&lt;/a> and start right away on
the devs instance or self-host Twenty on your own server.
I did the ladder, so let&amp;rsquo;s discuss how that. The basic steps I took were&lt;/p>
&lt;ul>
&lt;li>point twenty.hyteck.de to a server&lt;/li>
&lt;li>Install traefik on the server (I cheated, traefik was already installed)&lt;/li>
&lt;li>Deploy &lt;a href="docker-compose.yml">this docker-compose.yml&lt;/a> with &lt;a href="env">this env file&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Then visit the domain and set up the first user.&lt;/p>
&lt;h1 id="features">Features&lt;/h1>
&lt;p>Twenty offers an initial datamodel that you should be familiar from other CRMs. the standards objects are&lt;/p>
&lt;p>&lt;img src="person-model.png" alt="A screenshot of the person model in Twenty">&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Persons&lt;/strong> A individual person. You can attach notes, E-Mails, etc..&lt;/li>
&lt;li>&lt;strong>Companies&lt;/strong> The same for organizations. Organization websites must be unique&lt;/li>
&lt;li>&lt;strong>Opportunities&lt;/strong> The classic opportunity with customizable stages&lt;/li>
&lt;li>&lt;strong>Notes&lt;/strong> They can be attached to any of the objects above&lt;/li>
&lt;li>&lt;strong>Tasks&lt;/strong> Items to work on&lt;/li>
&lt;li>&lt;strong>Workflows&lt;/strong> Automations similar to Salesforce flows. E.g. you can create a task every time an Opportunity is
created.&lt;/li>
&lt;/ul>
&lt;p>The basic datamodel can be extended in the GUI. Here is how my &amp;ldquo;Company&amp;rdquo; model looks like&lt;/p>
&lt;p>&lt;img src="organization_dm.png" alt="A screenshot of twenty. It shows the company model being renamed to Organizations and deactivated fields such as Twitter links or number of employees.">&lt;/p>
&lt;p>You can add any of the following fields to an object.&lt;/p>
&lt;p>&lt;img src="fields.png" alt="A list of fields: Text, Number, True/False, Date and Time, Date, Select, Multi-Select, Rating, Currency, E-Mails, Links, Phones, Full Name, Address, Relation and the Advanced fields called Unique ID, JSON and Array">&lt;/p>
&lt;h3 id="workflows">Workflows&lt;/h3>
&lt;p>Workflows are Twenty&amp;rsquo;s way of allowing users to build automations. You can start a Workflow when a Record is created,
updated or deleted. In addition, they can be started manually, on a schedule and via Webhook (yeah!).&lt;/p>
&lt;p>&lt;img src="workflow1.png" alt="A workflow in twenty. After the Trigger &amp;ldquo;Organization&amp;rdquo; created there is a new task generated, a webhook send and a form used.">&lt;/p>
&lt;p>You can then add nodes that trigger actions. Available right now are&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Creating, updating or deleting a record&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Searching records&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Sending E-Mails&lt;/strong> This is the only option to trigger e-mails so far&lt;/li>
&lt;li>&lt;strong>Code&lt;/strong> Serverless Javascript functions&lt;/li>
&lt;li>&lt;strong>Form&lt;/strong> The form will pop up on the user&amp;rsquo;s screen when the workflow is launched from a manual trigger. For other
types of triggers, it will be displayed in the Workflow run record page.&lt;/li>
&lt;li>&lt;strong>HTTP request&lt;/strong> Although possible via Code, this is a handy shortcut to trigger HTTP requests&lt;/li>
&lt;/ul>
&lt;p>What is currently completely missing are Foreach-loops
and &lt;a href="https://github.com/twentyhq/core-team-issues/issues/1265">conditions&lt;/a>. I can not say &amp;ldquo;If Opportunity stage is
updated to X do Y else, do Z&amp;rdquo;.
Without this, Workflows are really limited in their power.&lt;/p>
&lt;p>What already seems quite mature though is the code option. It allows to put in arbitrary code and output a result.&lt;/p>
&lt;p>&lt;img src="serverless_function.png" alt="Screenshot of a javascript function in Twenty that adds two numbers together">&lt;/p>
&lt;p>I did not try a lot, but I assume most basic Javascript works. I successfully built an http request that send data to a
server.&lt;/p>
&lt;p>If what you&amp;rsquo;re doing is straightforward enough to not use loops and conditions or if you are okay with doing all of them
in the Code node, you can do basically anything.&lt;/p>
&lt;h2 id="api">API&lt;/h2>
&lt;p>Twenty offers an extensive API that allows you to basically do everything. It&amp;rsquo;s well documented and easy to use.&lt;/p>
&lt;p>Here is an example of me, syncing Rescue Organizations from &lt;a href="https://notfellchen.org">notfellchen.org&lt;/a> to Twenty.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> requests
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> fellchensammlung.models &lt;span style="color:#f92672">import&lt;/span> RescueOrganization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">sync_rescue_org_to_twenty&lt;/span>(rescue_org: RescueOrganization, base_url, token: str):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> rescue_org&lt;span style="color:#f92672">.&lt;/span>twenty_id:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> update &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> update &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> payload &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;eMails&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;primaryEmail&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>email,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;additionalEmails&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;domainName&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;primaryLinkLabel&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>website,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;primaryLinkUrl&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>website,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;additionalLinks&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> rescue_org&lt;span style="color:#f92672">.&lt;/span>location:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> payload[&lt;span style="color:#e6db74">&amp;#34;address&amp;#34;&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;addressStreet1&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>street&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>housenumber&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;addressCity&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>city,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;addressPostcode&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>postcode,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;addressCountry&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>countrycode,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;addressLat&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>latitude,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;addressLng&amp;#34;&lt;/span>: rescue_org&lt;span style="color:#f92672">.&lt;/span>location&lt;span style="color:#f92672">.&lt;/span>longitude,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Content-Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Authorization&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Bearer &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>token&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> update:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>base_url&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/rest/companies/&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>rescue_org&lt;span style="color:#f92672">.&lt;/span>twenty_id&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response &lt;span style="color:#f92672">=&lt;/span> requests&lt;span style="color:#f92672">.&lt;/span>patch(url, json&lt;span style="color:#f92672">=&lt;/span>payload, headers&lt;span style="color:#f92672">=&lt;/span>headers)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">assert&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>status_code &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">200&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>base_url&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/rest/companies&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response &lt;span style="color:#f92672">=&lt;/span> requests&lt;span style="color:#f92672">.&lt;/span>post(url, json&lt;span style="color:#f92672">=&lt;/span>payload, headers&lt;span style="color:#f92672">=&lt;/span>headers)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">assert&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>status_code &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">201&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rescue_org&lt;span style="color:#f92672">.&lt;/span>twenty_id &lt;span style="color:#f92672">=&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>json()[&lt;span style="color:#e6db74">&amp;#34;data&amp;#34;&lt;/span>][&lt;span style="color:#e6db74">&amp;#34;createCompany&amp;#34;&lt;/span>][&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rescue_org&lt;span style="color:#f92672">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="the-company-business-model-and-paid-features">The Company, Business Model and Paid Features&lt;/h1>
&lt;p>The company behind Twenty is called &amp;ldquo;Twenty.com PBC&amp;rdquo; and mostly seems to consist of former AirBnB employees in Paris.
The company is probably backed by Venture Capital.
The current business model is to charge for using the company&amp;rsquo;s instance of Twenty. It starts at 9$/user/month without
enterprise features. SSO and support will cost you 19$/user/month.&lt;/p>
&lt;p>Selfhosting is free but SSO is locked behind an enterprise badge with seemingly no way to pay for activating it.
I suspect that in the future more features will become &amp;ldquo;Enterprise only&amp;rdquo; even when self-hosting. All contributors must
agree
to &lt;a href="https://github.com/twentyhq/twenty/blob/main/.github/CLA.md">a Contributor License Agreement (CLA)&lt;/a>, therefore I
believe they could change the License in the future, including switching away from Open Source.&lt;/p>
&lt;h1 id="ai-usage">AI Usage&lt;/h1>
&lt;p>The repo contains a &lt;code>.cursor&lt;/code> directory and &lt;code>CLAUDE.md&lt;/code> so I assume the devs make (heavy?) use of LLM generated code.
The ethical and legal problems with this are for you to decide. I don&amp;rsquo;t know what effect this has on code quality, for
now I&amp;rsquo;d say things are sometimes buggy (failed upgrades) and UX could be better tested (looking at the e-mail
integration) - if this is due to AI slop I don&amp;rsquo;t know.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Twenty is a really promising start of building a good CRM. The ease of customizing the datamodel,
using the API and a solid beginning to Flows allows users to get a lot of value from it already.
Flows need some more work to become as powerful as they should be and the E-Mail integration needs to get better.&lt;/p>
&lt;p>Stating the obvious: This is not something that could ever replace Salesforce. However, there are many organizations
that would benefit a lot from a CRM like Twenty, they simply don&amp;rsquo;t need, can&amp;rsquo;t handle or don&amp;rsquo;t want to pay for all the
features other CRMs like Salesforce offer.&lt;/p>
&lt;p>If Twenty continues to focus on small to medium companies and the right mix of standard features vs. custom development
options I see a path where it becomes a solid choice for these companies. On the other hand there are the usual problems
of VC-backed OSS development, and we shall see how it goes for them. Unless there is a strong userbase that credibly
threatens a hard fork, enshittification could start soon.&lt;/p>
&lt;h1 id="addendum-important-features">Addendum: Important Features&lt;/h1>
&lt;p>Here is a short list of features I missed and their place on the roadmap if they have one&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compose &amp;amp; Send E-Mails&lt;/strong>
Planned &lt;a href="https://github.com/orgs/twentyhq/projects/1?pane=issue&amp;amp;itemId=106097937&amp;amp;issue=twentyhq%7Ccore-team-issues%7C811">Q4 2025&lt;/a>&lt;/li>
&lt;li>&lt;strong>Foreach loops in Workflows&lt;/strong> &lt;a href="https://github.com/orgs/twentyhq/projects/1/views/33?pane=issue&amp;amp;itemId=93150024&amp;amp;issue=twentyhq%7Ccore-team-issues%7C21">Q3 2025&lt;/a>&lt;/li>
&lt;li>&lt;strong>Conditions in Flows&lt;/strong> &lt;a href="https://github.com/orgs/twentyhq/projects/1/views/33?pane=issue&amp;amp;itemId=121287765&amp;amp;issue=twentyhq%7Ccore-team-issues%7C1265">Q4 2025&lt;/a>&lt;/li>
&lt;/ul></content></entry><entry><title>Thoughts on HTML mails</title><link href="https://hyteck.de/post/about-html-mails/" type="application/octet-stream"/><updated>2025-07-12T12:05:10+02:00</updated><id>https://hyteck.de/post/about-html-mails/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>Lately I worked on notification e-mails for &lt;a href="https://notfellchen.org">notfellchen.org&lt;/a>. Initially I just sent text
notifications without links to the site. Terrible idea! An E-Mail notification I send always has Call-to-Action or at
minimum a link to more information.&lt;/p>
&lt;p>I left the system like this for half a year because it kinda worked for me (didn&amp;rsquo;t suck enough for me to care), and I was the main receiver of these notifications.
However, as the platform is developed further and more users join I need to think about more user-centric notifications.&lt;/p>
&lt;p>So what do I imagine is important to a user?
*&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Information benefit&lt;/strong>: An e-mail has the purpose to inform a user. This information should be immediately visible &amp;amp; understandable.&lt;/li>
&lt;li>&lt;strong>Actionables&lt;/strong>: Users should be able to act on the information received. This is the bright red button &amp;ldquo;DO SOMETHING NOW!&amp;rdquo; you see so often.&lt;/li>
&lt;li>&lt;strong>Unsubscribing&lt;/strong>: Informing e-mails stop is not only a legal requirement and morally the right thing to do but it also gives users agency and - I hope - increases the User Experience&lt;/li>
&lt;/ul>
&lt;p>With these I naturally came to the next question: Plaintext or HTML?&lt;/p>
&lt;p>Some people would say &lt;a href="https://useplaintext.email/">Plaintext is inherently better&lt;/a> than HTML e-mails. Many of these reasons resonate with me including:&lt;/p>
&lt;ul>
&lt;li>Privacy invasion and tracking&lt;/li>
&lt;li>HTML emails are less accessible&lt;/li>
&lt;li>Some clients can&amp;rsquo;t display HTML emails at all&lt;/li>
&lt;li>Mail client vulnerabilities&lt;/li>
&lt;/ul>
&lt;p>These are all valid points and are a reason I generally enjoy plaintext e-mails when I receive them.
But this is not about me but users. And there are some real benefits of HTML e-mails:&lt;/p>
&lt;ul>
&lt;li>Visually appealing: This is subjective but generally most users seem to agree on that&lt;/li>
&lt;li>User guidance: Rich text provides a real benefit when searching for the relevant information&lt;/li>
&lt;/ul>
&lt;p>Be honest: Do you read automated e-mails you receive completely? Or do you just skim for important information?&lt;/p>
&lt;p>And here HTML-mails shine: &lt;strong>Information can easily be highlighted&lt;/strong> and big button can lead the user to do the right action.
Some might argue that you can also a highlight a link in plaintext but that nearly always will worsen accessibility for screen-reader user.&lt;/p>
&lt;h1 id="the-result">The result&lt;/h1>
&lt;p>In the end, I decided that providing plaintext-only e-mails was not enough. I set up html mails, mostly using
&lt;a href="https://docs.djangoproject.com/en/5.2/topics/email/#send-mail">djangos send_mail&lt;/a> function where I can pass the html message and attattching it correctly is done for me.&lt;/p>
&lt;p>&lt;img src="mail_screenshot.png" alt="A screenshot of an e-mail in thunderbird. The e-mail is structured in header, body and footer. The header says &amp;ldquo;Notfellchen.org&amp;rdquo;, the body shows a message that a new user was registered and a bright green button to show the user. The footer offers a link to unsubscribe">&lt;/p>
&lt;p>For anyone that is interested, here is how most my notifications are sent&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">send_notification_email&lt;/span>(notification_pk):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> notification &lt;span style="color:#f92672">=&lt;/span> Notification&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>get(pk&lt;span style="color:#f92672">=&lt;/span>notification_pk)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subject &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>notification&lt;span style="color:#f92672">.&lt;/span>title&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context &lt;span style="color:#f92672">=&lt;/span> {&lt;span style="color:#e6db74">&amp;#34;notification&amp;#34;&lt;/span>: notification, }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> notification&lt;span style="color:#f92672">.&lt;/span>notification_type &lt;span style="color:#f92672">==&lt;/span> NotificationTypeChoices&lt;span style="color:#f92672">.&lt;/span>NEW_REPORT_COMMENT &lt;span style="color:#f92672">or&lt;/span> notification&lt;span style="color:#f92672">.&lt;/span>notification_type &lt;span style="color:#f92672">==&lt;/span> NotificationTypeChoices&lt;span style="color:#f92672">.&lt;/span>NEW_REPORT_AN:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> html_message &lt;span style="color:#f92672">=&lt;/span> render_to_string(&lt;span style="color:#e6db74">&amp;#39;fellchensammlung/mail/notifications/report.html&amp;#39;&lt;/span>, context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> plain_message &lt;span style="color:#f92672">=&lt;/span> render_to_string(&lt;span style="color:#e6db74">&amp;#39;fellchensammlung/mail/notifications/report.txt&amp;#39;&lt;/span>, context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&lt;span style="color:#f92672">...&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> notification&lt;span style="color:#f92672">.&lt;/span>notification_type &lt;span style="color:#f92672">==&lt;/span> NotificationTypeChoices&lt;span style="color:#f92672">.&lt;/span>NEW_COMMENT:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> html_message &lt;span style="color:#f92672">=&lt;/span> render_to_string(&lt;span style="color:#e6db74">&amp;#39;fellchensammlung/mail/notifications/new-comment.html&amp;#39;&lt;/span>, context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> plain_message &lt;span style="color:#f92672">=&lt;/span> render_to_string(&lt;span style="color:#e6db74">&amp;#39;fellchensammlung/mail/notifications/new-comment.txt&amp;#39;&lt;/span>, context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">NotImplementedError&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Unknown notification type&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#e6db74">&amp;#34;plain_message&amp;#34;&lt;/span> &lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#f92672">in&lt;/span> locals():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> plain_message &lt;span style="color:#f92672">=&lt;/span> strip_tags(html_message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mail&lt;span style="color:#f92672">.&lt;/span>send_mail(subject, plain_message, settings&lt;span style="color:#f92672">.&lt;/span>DEFAULT_FROM_EMAIL,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [notification&lt;span style="color:#f92672">.&lt;/span>user_to_notify&lt;span style="color:#f92672">.&lt;/span>email],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> html_message&lt;span style="color:#f92672">=&lt;/span>html_message)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Yes this could be made more efficient - for now it works. I made the notification framework too complicated initially, so I&amp;rsquo;m still tyring out what works and what doesn&amp;rsquo;t.&lt;/p>
&lt;p>Here is the html template&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>{% extends &amp;#34;fellchensammlung/mail/base.html&amp;#34; %}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{% load i18n %}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{% block title %}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {% translate &amp;#39;Neuer User&amp;#39; %}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{% endblock %}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{% block content %}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">p&lt;/span>&amp;gt;Moin,&amp;lt;/&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> es wurde ein neuer Useraccount erstellt.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Details findest du hier
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">a&lt;/span> &lt;span style="color:#a6e22e">href&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;{{ notification.user_related.get_full_url }}&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">class&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;cta-button&amp;#34;&lt;/span>&amp;gt;{% translate &amp;#39;User anzeigen&amp;#39; %}&amp;lt;/&lt;span style="color:#f92672">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#f92672">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{% endblock %}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and here the plaintext&lt;/p>
&lt;pre tabindex="0">&lt;code>{% extends &amp;#34;fellchensammlung/mail/base.txt&amp;#34; %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
es wurde ein neuer Useraccount erstellt.
User anzeigen: {{ new_user_url }}
{% endblocktranslate %}{% endblock %}
&lt;/code>&lt;/pre>&lt;p>Works pretty well for now. People that prefer plaintext will get these and most users will have skimmable html e-mail where the
styling will help them recognize where it&amp;rsquo;s from and what to do. Accessibility-wise this seems like the best option.&lt;/p>
&lt;p>And while adding a new notification will force me to create&lt;/p>
&lt;ul>
&lt;li>a new notification type,&lt;/li>
&lt;li>two new e-mail templates and&lt;/li>
&lt;li>a proper rendering on the website&lt;/li>
&lt;/ul>
&lt;p>this seems okay. Notifications are useful, but I don&amp;rsquo;t want to shove them everywhere. I&amp;rsquo;m not running facebook or linkedin after all.&lt;/p>
&lt;p>So for now I&amp;rsquo;m pretty happy with the new shiny e-mails and will roll out the changes soon (if I don&amp;rsquo;t find any more wired bugs).&lt;/p>
&lt;p>PS: I wrote this post after reading &lt;a href="https://blog.avas.space/blog-website-eval/">blog &amp;amp; website in the age of containerized socials&lt;/a> by ava.
Maybe this &amp;ldquo;Thoughts on&amp;rdquo; format will stay and I will post these in addition to more structured deep dives.&lt;/p>
&lt;h1 id="update">Update&lt;/h1>
&lt;p>I did a rework of the notification function and it&amp;rsquo;s now much cleaner now. However, it&amp;rsquo;s less readable so this blogpost will stay as-is.
If you want to check out the new code have a look &lt;a href="https://codeberg.org/moanos/notfellchen/src/commit/a4b8486bd489dacf8867b49d04f70f091556dc9d/src/fellchensammlung/mail.py">on Codeberg&lt;/a>.&lt;/p></content></entry><entry><title>Improve OpenStreetMap data by using it</title><link href="https://hyteck.de/post/improve-osm-by-using-it/" type="application/octet-stream"/><updated>2025-06-28T14:05:10+02:00</updated><id>https://hyteck.de/post/improve-osm-by-using-it/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>In the last month I improved the mapping of about 100 german animal shelters - not only out of the goodness of my heart, but because it helped me.&lt;/p>
&lt;p>Let me explain why: I develop &lt;a href="https://notfellchen.org/">notfellchen.org&lt;/a>, where users can search animals in animal shelters, specifically rats, they might want to adopt.
The idea is to have a central website that allows you to search for rats in your area.&lt;/p>
&lt;p>This is necessary because only a small percentage of animal shelters has rats. As a user, just checking your next
shelter doesn&amp;rsquo;t work. Some users will stop after checking the second or third one and just buy from a pet shop (which is a very, very bad idea).&lt;/p>
&lt;p>Now a central platform for is nice for users but has one problem: How do I, as operator of notfellchen, know where rats are?&lt;/p>
&lt;p>I need to &lt;strong>manually check every animal shelter in the country&lt;/strong> and if they have rats, ask them for permission to use
images of the rats on my site.
So wait I need to have is a list of animal shelters in germany and have their website, e-mail and phone number.&lt;/p>
&lt;p>The source for all of this: You guessed it - OpenStreetMap 🥳&lt;/p>
&lt;h1 id="getting-the-data">Getting the data&lt;/h1>
&lt;p>Downloading all german animal shelters is surprisingly easy: You use &lt;a href="https://overpass-turbo.eu/">Overpass Turbo&lt;/a> and get a &lt;code>.geojson&lt;/code> to download.&lt;/p>
&lt;p>here is the query I used:&lt;/p>
&lt;pre tabindex="0">&lt;code>[out:json][timeout:25];
// fetch area “Germany” to search in
{{geocodeArea:Germany}}-&amp;gt;.searchArea;
// Check search area for all objects with animal shelter tag
nwr[&amp;#34;amenity&amp;#34;=&amp;#34;animal_shelter&amp;#34;](area.searchArea);
// print results
out geom;
&lt;/code>&lt;/pre>&lt;p>Now upload it to notfellchen.org and I&amp;rsquo;ll be fine right?&lt;/p>
&lt;h1 id="data-issues">Data Issues&lt;/h1>
&lt;p>Yeah well, this only &lt;em>mostly&lt;/em> works. There were two main problems:&lt;/p>
&lt;p>&lt;strong>Missing contact data&lt;/strong> is annoying because I quickly want to check the website of animal shelters.&lt;/p>
&lt;p>More annoying were what I&amp;rsquo;d call &lt;strong>mapping errors&lt;/strong>.
Most commonly an animal shelter had multiple nodes/ways tagged as &lt;code>amenity:animal_shelter&lt;/code>.
The highlight was the &amp;ldquo;Tierheim München&amp;rdquo; where about 10 buildings were tagged as &lt;code>amenity:animal_shelter&lt;/code> and the contact
data was sitting on the building with name &amp;ldquo;Katzenhaus&amp;rdquo; (&amp;ldquo;cat house&amp;rdquo;).&lt;/p>
&lt;p>Now the &amp;ldquo;Tierheim München&amp;rdquo; appeared in my list 10 times but 9 of them had no contact data at all.&lt;/p>
&lt;h1 id="correcting-it">Correcting it&lt;/h1>
&lt;p>I could have corrected this only in the notfellchen database. It would have been faster and I could even automate parts of it.
But I didn&amp;rsquo;t.&lt;/p>
&lt;p>For each issue I found, I opened OpenStreetMap and added websites, phone numbers or even re-mapped the area.
For &amp;ldquo;Tierheim München&amp;rdquo; I even &lt;a href="https://community.openstreetmap.org/t/mapping-of-multiple-related-buildings-animal-shelters/131801">opened a thread in the forum&lt;/a>
to discuss a proper tagging.&lt;/p>
&lt;p>That makes sense for me because I get one important thing:&lt;/p>
&lt;h1 id="what-i-get-out-of-it-updates">What I get out of it: Updates&lt;/h1>
&lt;p>What if a new shelter was added later or a shelter changed? I already profit a lot from the time people spend adding information, so why stop?&lt;/p>
&lt;p>My database stores the OSM ID, so I can regularly query the data again to get updates.
But that only works if I take an &amp;ldquo;upstream&amp;rdquo; approach: Fix the data in OSM, then load it into notfellchen.
Otherwise, any change in my database will be overwritten by &amp;ldquo;old&amp;rdquo; OSM data.&lt;/p>
&lt;h1 id="result">Result&lt;/h1>
&lt;p>In the last month, I made 86 changes to OSM adding the following information&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Type of information&lt;/th>
&lt;th>Number of times added&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Website&lt;/td>
&lt;td>66&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Phone Numbers&lt;/td>
&lt;td>65&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Operator&lt;/td>
&lt;td>63&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>E-Mail&lt;/td>
&lt;td>49&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fax&lt;/td>
&lt;td>9&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Yes I sometimes even added fax numbers. It was easy enough to add and maybe there is someone might use it.&lt;/p>
&lt;h1 id="looking-forward">Looking forward&lt;/h1>
&lt;p>I&amp;rsquo;m of course not done. Only half of the rescues known to OSM in germany are currently checked, so I&amp;rsquo;ll continue that work.&lt;/p>
&lt;p>After that I&amp;rsquo;ll start adding the shelters that are just in my database.
Currently, 33 animal shelters are known to notfellchen that are not known to OSM. This number will likely grow, maybe double.&lt;/p>
&lt;p>A lot to do. And luckily, this work both benefits me and everyone using OSM. Happy mapping!&lt;/p></content></entry><entry><title>Meine SciFi und Fantasy Empfehlungen - Schriftgelehrte gegen KI</title><link href="https://hyteck.de/post/librarian/" type="application/octet-stream"/><updated>2025-03-03T18:05:10+02:00</updated><id>https://hyteck.de/post/librarian/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h2 id="einführung">Einführung&lt;/h2>
&lt;p>KI ist überall. Große Teile des Internets werden gerade durch KI-generierten Inhalten überschwemmt.
Teilweise zeigen KI-generierte Artikel auch gefälschte Daten (vor 2022) an um so den Anschein zu erwecken nicht KI-generiert zu sein.
Verlässliche und authentische Informationen zu finden ist daher um so schwerer.&lt;/p>
&lt;p>Deshalb beginnt &lt;strong>das Zeitalter der Schriftgelehrten&lt;/strong>, denn Informationen zu sammeln und aufzubereiten, das ist, was sie tun. Schriftgelehrte*r verwende ich hier als Übersetzung des englischen Wortes &amp;ldquo;Librarian&amp;rdquo;.
Und so maße auch ich mir an mich (nur) dafür Schriftgelehrte*r zu nennen und versuche auf diesem
Blog immer wieder Informationen zu sammeln und zu teilen (ironischerweise in dem Wissen, dass dieser Blog von
KI-Unternehmen gescrapt wird).&lt;/p>
&lt;p>Starten möchte ich mit Folgendem:&lt;/p>
&lt;h2 id="sci-fi-und-fantasy">Sci-Fi und Fantasy&lt;/h2>
&lt;p>Wenn ich nach Sci-Fi und Fantasy suche, will ich weg von profit-optimierten Listen von Online-Buchhändler*innen,
und hin zu echten Empfehlungen. Viele solche Empfehlungen bekomme ich im lokalen Buchladen. Gerade junge Buchhändler*innen können oft super Empfehlungen geben.
Leider sind diese wenigen Mitarbeiter*innen selten und in Buchläden sind SciFi und Fantasy oft nur wenig vertreten und die Regale viel zu oft voll mit Büchern weißer Männer.&lt;/p>
&lt;p>Deshalb jetzt zu den Empfehlungen die versuchen, das anders zu machen!&lt;/p>
&lt;p>&lt;em>Die Liste beschreibt die Bücher nur kurz und sollte ohne Spoiler auskommen.
In allen Büchern kommen queere Charaktere vor. Alle Links zum führen zu einem lokaln Buchladen oder Websites der Autor*innen oder des Verlags.&lt;/em>&lt;/p>
&lt;h3 id="becky-chambers-der-lange-weg-zu-einem-kleinen-zorningen-planeten">Becky Chambers: &amp;ldquo;Der lange Weg zu einem kleinen zorningen Planeten&amp;rdquo;&lt;/h3>
&lt;p>Dieses Buch ist eine wirklich wunderschön geschriebene Space-Opera mit Charakteren, die man ins Herz schließt.
Auf dem kleinen Schiff ist viel Alltag und auf dem langen Weg lässt einen jede Zwischenstation in eine weitere Welt eintauchen, egal ob in einen trubeligen Markt oder einen abgeschiedenen Eisplanet.&lt;/p>
&lt;p>Der zweite und dritte Teil handeln im gleichen Universum, sind jedoch nur über wenige Personen/geteilte Themen mit der ersten Geschichte verbunden.&lt;/p>
&lt;p>Alle anderen Bücher von Becky Chambers sind auch sehr empfehlenswert &amp;ldquo;A Psalm for the Wild-Built&amp;rdquo; ist eine kurze Solarpunk Utopie.&lt;/p>
&lt;p>&lt;a href="https://frauenbuchladen.buchkatalog.de/der-lange-weg-zu-einem-kleinen-zornigen-planeten-9783596035687">Link zum Buch beim Frauenbuchladen Thalestris&lt;/a>&lt;/p>
&lt;h3 id="judith--christian-vogt-ace-in-space">Judith &amp;amp; Christian Vogt: Ace in Space&lt;/h3>
&lt;p>Ace in Space ist eine tolle Erzählung von einer Raumjäger-Pilotin, einer Gruppe Space-Punks die ihr Leben auf Social Media teilen.
Angesiedelt ist das Buch eher im Cyber-Punk. Es geht gegen Großkonzerne, es geht um schnelle Flieger und Bars in engen Quartieren.
Außerdem hat es eine der besten Sexszenen, die ich je lesen durfte.&lt;/p>
&lt;p>Judith schreibt oft dystopischere Geschichten als ich normalerweise lese. Aber Laylayland und Wasteland zwei fantastische Bücher die ich nicht missen will.
Die Bücher sind feministisch, queer und tollerweise auch mit Haupt(Charaktere) mit Behinderungen. Progressive Phanastik der höchsten Klassen!&lt;/p>
&lt;p>&lt;a href="https://amrun-verlag.de/produkt/aceinspace1/">Verlagsshop&lt;/a>&lt;/p>
&lt;p>Wer eine ganze Kurzgeschichte &amp;ldquo;der Vögte&amp;rdquo; (also Judith und Christian Vogt) als Leseprobe haben will, findet diese am Ende des Artikels als PDF.&lt;/p>
&lt;h3 id="tj-klune-mr-parnassus-heim-für-magisch-begabte">T.J. Klune: Mr. Parnassus&amp;rsquo; Heim für magisch Begabte&lt;/h3>
&lt;p>Wunderschöne Geschichte über einen Beamten der aus der Stadt rauskommt und ein Haus an der See.
Mehr verrate ich nicht, ist aber eins meiner Lieblingsbücher.&lt;/p>
&lt;p>&lt;a href="https://frauenbuchladen.buchkatalog.de/mr-parnassus-heim-fuer-magisch-begabte-9783453321366">Link zum Buch beim Frauenbuchladen Thalestris&lt;/a>&lt;/p>
&lt;h3 id="lena-richter-dies-ist-mein-letztes-lied">Lena Richter: Dies ist mein letztes Lied&lt;/h3>
&lt;p>Eine mitreisende Geschichte in der die Hauptperson durch ihre Musik von Welt zu Welt gerissen wird. Was sie in den einzelnen Episoden erlebt ist schön und herzzerreißend.
Ich habe die Novelle am Stück verschlungen und mit der Hauptperson gelacht und geweint.&lt;/p>
&lt;p>&lt;a href="https://www.ohneohren.com/shop/Lena-Richter-Dies-ist-mein-letztes-Lied-p520843015">Verlagsshop&lt;/a>&lt;/p>
&lt;h3 id="rebecca-thorne-cant-spell-treason-without-tea">Rebecca Thorne: &amp;ldquo;Can&amp;rsquo;t spell treason without tea&amp;rdquo;&lt;/h3>
&lt;p>In einer Fantasy Welt brennt eine Leibwächterin der Königin mit einer Magierin durch und sie eröffnen einen Teeladen.
Es gibt auch den Nachfolger &amp;ldquo;A Pirate&amp;rsquo;s Life for Tea&amp;rdquo;, den hab ich aber noch nicht gelesen&lt;/p>
&lt;p>&lt;a href="https://frauenbuchladen.buchkatalog.de/cant-spell-treason-without-tea-9783492706896">Link zum Buch beim Frauenbuchladen Thalestris (Deutsche Übersetzung)&lt;/a>&lt;/p>
&lt;h3 id="travis-baldree-legendslatte">Travis Baldree: &amp;ldquo;Legends&amp;amp;Latte&amp;rdquo;&lt;/h3>
&lt;p>Eine Ork, die lange Jahre mit einer Gruppe Abenteuer unterwegs war versucht nun einen Buch &amp;amp; Teeladen aufzumachen.
Sehr unterhaltsam, unerwartet friedlich und macht unglaublich Lust mehr in Cafes zu gehen!&lt;/p>
&lt;p>&amp;ldquo;Bookshops &amp;amp; Bonedust&amp;rdquo; ist die Vorgeschichte, die aber gut nach Legends &amp;amp; Latte gelesen werden kann und auch später geschrieben wurde.&lt;/p>
&lt;p>&lt;a href="https://frauenbuchladen.buchkatalog.de/magie-und-milchschaum-9783423263566">Link zum Buch beim Frauenbuchladen Thalestris&lt;/a>&lt;/p>
&lt;h3 id="sammlung-sonnenseiten---street-art-trifft-solarpunk">Sammlung: Sonnenseiten - Street Art trifft Solarpunk&lt;/h3>
&lt;p>22 Autor*innen haben Geschichten zusammengetragen die die beiden Kunstformen Street Art und Solarpunk verbinden.
Besonders empfehlen kann ich die Geschichte &amp;ldquo;Uferlos&amp;rdquo; von Lena Richter die eine schwimmende Stadt zum Denken anregt und &amp;ldquo;Cloudart&amp;rdquo; von Dominik Windgätter in der ein &amp;ldquo;Maskenmädchen&amp;rdquo; Kunstwerke in den Himmel zeichnet.&lt;/p>
&lt;p>&lt;a href="https://frauenbuchladen.buchkatalog.de/sonnenseiten-9783756803972">Link zum Buch beim Frauenbuchladen Thalestris&lt;/a>&lt;/p>
&lt;h2 id="schluss">Schluss&lt;/h2>
&lt;p>Ich hoffe die Empfehlungen machen, trotz ihrer Kürze, Lust aufs Lesen! Kauft bei eurem lokalen Buchladen und unterstützt kleine Verlage!&lt;/p>
&lt;p>&lt;em>Falls das nicht sowieso schon klar war: Ich bekomme für diese Empfehlungen kein Geld, die Links haben kein Tracking, keine Referral Codes oder sonst etwas.&lt;/em>&lt;/p>
&lt;h3 id="kurzgeschichte-fiatlux">Kurzgeschichte FiatLux&lt;/h3>
&lt;p>Die Kurzgeschichte ist von Judith und Christian Vogt und steht unter der Lizenz &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC-BY-NC-SA&lt;/a>, darf also mit Namensnennung, nichtkommerziell und unter gleichen Bedingungen geteilt werden (wie schön ist das denn bitte?!).&lt;/p>
&lt;object class="fitvidsignore" data="/uploads/FiatLuxVogt.pdf" type="application/pdf" width="100%" height="500px">
&lt;p>&lt;a href="https://hyteck.de/uploads/FiatLuxVogt.pdf">Download the PDF!&lt;/a>&lt;/p>
&lt;/object></content></entry><entry><title>I did something naughty: Circumventing Authorized-Fetch as implemented by GoToSocial</title><link href="https://hyteck.de/post/public-posts-with-authorized-fetch/" type="application/octet-stream"/><updated>2024-12-11T06:10:10+02:00</updated><id>https://hyteck.de/post/public-posts-with-authorized-fetch/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>Yes the title is correct, but I had nothing malicious in mind!&lt;/p>
&lt;h2 id="what-this-is-about">What this is about&lt;/h2>
&lt;p>For &lt;a href="https://social.queereszentrumtuebingen.de/@qzt">@qzt@queereszentrumtuebingen.de&lt;/a> we include the public feed &lt;a href="https://queereszentrumtuebingen.de/">in a sidbar on the homepage&lt;/a>. Initially this was done using the standard API to fetch statuses &lt;code>/api/v1/accounts/{account_id}/statuses&lt;/code> and worked like a charm. The problem started when &lt;a href="https://gotosocial.org/">GoToSocial&lt;/a> (the fediverse server we use, similar to mastodon) implemented authorized fetch. This is a a good thing! Authorized fetch means, that every call to a endpoint needs to be authorized by an &lt;code>access_token&lt;/code>. You get an access token from a fedi account. It&amp;rsquo;s what fediverse clients like Tusky or Phanpy do on your behalf to get the posts that make up you timeline.&lt;/p>
&lt;p>Authorized fetch has major advantages as&lt;/p>
&lt;ul>
&lt;li>data scraping can only be done by other fediaccounts&lt;/li>
&lt;li>blocking can not be circumvented by using the public API&lt;/li>
&lt;/ul>
&lt;p>and much more. Sadly it also broke our website integration.&lt;/p>
&lt;h2 id="possible-solutions">Possible Solutions&lt;/h2>
&lt;p>So what now? I initially wanted to turn of authorized fetch for &lt;a href="https://social.queereszentrumtuebingen.de/@qzt">@qzt@queereszentrumtuebingen.de&lt;/a> by messing with the GoToSocial code and turning it off for the whole server. This would have been possible as this is the only user on the server. The GoToSocial devs helped me manage to find where to do that. But it&amp;rsquo;s not ideal and would make me build a custom docker image fore each update.&lt;/p>
&lt;p>Next idea: The whole point of authorized fetch is, that only fedi-accounts (and apps they authorized) can access the API. So lets do that! Set up a new account, add app and authorize it &lt;a href="https://docs.gotosocial.org/en/latest/api/authentication/">as described in the GoToSocial documentation&lt;/a>. I used #Bruno for that, that was much more comfortable than using curl for me.
With that authorization code you can now get an access token for your app. Put that in the Javascript that loads posts and we are good right? Sadly no. It would totally work. But it would also allow anyone to read and post on behalf of the account. That calls for malicious actors using this for scraping or spamming.&lt;/p>
&lt;p>So instead, we need a proxy that stores the access token securely and restricts the actions.&lt;/p>
&lt;h2 id="the-proxy">The proxy&lt;/h2>
&lt;p>Such a proxie must&lt;/p>
&lt;ul>
&lt;li>offer the endpoint that provides the same data as the FediverseAPI&lt;/li>
&lt;li>authorize itself to the FediverseAPI via &lt;code>access_token&lt;/code>&lt;/li>
&lt;li>restrict to read access of consenting accounts&lt;/li>
&lt;/ul>
&lt;p>The last point is really important, as we don&amp;rsquo;t want to allow others to use this endpoint to scrape data unauthorized.&lt;/p>
&lt;p>I wrote a short FastAPI server that offers this. It only implements one method&lt;/p>
&lt;pre tabindex="0">&lt;code>@app.get(&amp;#34;/api/v1/accounts/{account_id}/statuses&amp;#34;)
async def fetch_data(account_id):
if account_id not in ALLOWED_ACCOUNTS:
raise HTTPException(status_code=401, detail=&amp;#34;You can only use this proxy to access configured accounts&amp;#34;)
headers = {&amp;#34;Authorization&amp;#34;: f&amp;#34;Bearer {ACCESS_TOKEN}&amp;#34;}
response = requests.get(f&amp;#34;{EXTERNAL_API_BASE_URL}/api/v1/accounts/{account_id}/statuses&amp;#34;, headers=headers)
return response.json()
&lt;/code>&lt;/pre>&lt;p>Basically this is the whole API code, I only trimmed a few checks and error handling.&lt;/p>
&lt;h2 id="deployment">Deployment&lt;/h2>
&lt;p>To deploy, I put it in a docker container and started it via docker-compose. Reverse proxing is handled by Traefik, I won&amp;rsquo;t go into detail here.&lt;/p>
&lt;pre tabindex="0">&lt;code>services:
fediproxy.example.org:
image: docker.io/moanos/fediproxy
container_name: &amp;#34;fediproxy.example.org&amp;#34;
restart: unless-stopped
environment:
EXTERNAL_API_BASE_URL: ${EXTERNAL_API_BASE_URL}
ACCESS_TOKEN: ${ACCESS_TOKEN}
ALLOWED_ACCOUNTS: ${ALLOWED_ACCOUNTS}
labels:
- &amp;#34;traefik.enable=true&amp;#34;
- &amp;#34;traefik.docker.network=traefik&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.rule=Host(`fediproxy.example.org`)&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.service=fediproxy-service&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.entrypoints=web-secure&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.tls=true&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.tls.certResolver=default&amp;#34;
- &amp;#34;traefik.http.services.fediproxy-service.loadbalancer.server.port=8000&amp;#34;
networks:
- traefik
networks:
traefik:
name: &amp;#34;traefik&amp;#34;
external: true
&lt;/code>&lt;/pre>&lt;p>I added a short &lt;code>.env&lt;/code> to configure:&lt;/p>
&lt;pre tabindex="0">&lt;code>ACCESS_TOKEN=VERYSECRETTOKENTHATISDEFINETLYREAL
EXTERNAL_API_BASE_URL=https://gay-pirate-assassins.de
ALLOWED_ACCOUNTS=ZGGZF4G8NNOTREAL81Z8G7RTC
&lt;/code>&lt;/pre>&lt;h2 id="results">Results&lt;/h2>
&lt;p>Now I can again use something like &lt;a href="https://wordpress.org/plugins/include-mastodon-feed/#installation">the wordpress plugin Include Mastodon Feed&lt;/a> just by pointing to the proxy: &lt;code>[include-mastodon-feed instance=&amp;quot;fediproxy.example.org.de&amp;quot; account=&amp;quot;ZGGZF4G8NNOTREAL81Z8G7RTC&amp;quot;]&lt;/code>&lt;/p>
&lt;p>Hope you enjoyed the read. Source code for the proxy can be found here: &lt;a href="https://git.hyteck.de/moanos/FediProxy">https://git.hyteck.de/moanos/FediProxy&lt;/a>
If you want to play around a bit you can use &lt;a href="https://git.hyteck.de/moanos/include-fedi">https://git.hyteck.de/moanos/include-fedi&lt;/a>&lt;/p>
&lt;p>Sloth logo of GTS by &lt;a href="https://abramek.art/">Anna Abramek&lt;/a>, &lt;a href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons BY-SA license&lt;/a>.&lt;/p></content></entry><entry><title>Where are you? - Part 2 - Geocoding with Django to empower area search</title><link href="https://hyteck.de/post/django-geocoding/" type="application/octet-stream"/><updated>2024-10-04T14:05:10+02:00</updated><id>https://hyteck.de/post/django-geocoding/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>In the &lt;a href="geocoding-with-django/">previous post&lt;/a> I outlined how to set up a Nominatim server that allows us to find a geolocation for any address on the planet. Now let&amp;rsquo;s use our newfound power in Django. Again, all code snippets are &lt;a href="https://creativecommons.org/public-domain/cc0/">CC0&lt;/a> so make free use of them. But I&amp;rsquo;d be very happy if you tell me if you use them for something cool!&lt;/p>
&lt;h2 id="prerquisites">Prerquisites&lt;/h2>
&lt;ul>
&lt;li>You have a working geocoding server or use a public one&lt;/li>
&lt;li>You have a working django app&lt;/li>
&lt;/ul>
&lt;p>If you want to do geocoding in a different environment you will still be able to use a lot of the the following examples, just skip the Django-specifics and configure the &lt;code>GEOCODING_API_URL&lt;/code> according to your needs.&lt;/p>
&lt;h1 id="using-the-geocoding-api">Using the Geocoding API&lt;/h1>
&lt;p>First of all, let&amp;rsquo;s define the geocoding API URL in our settings. This enables us to switch easily if a service is not available. Add the following to you &lt;code>settings.py&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># appname/settings.py&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34; GEOCODING &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>GEOCODING_API_URL &lt;span style="color:#f92672">=&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#34;geocoding&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;api_url&amp;#34;&lt;/span>, fallback&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://nominatim.hyteck.de/search&amp;#34;&lt;/span>) &lt;span style="color:#75715e"># Adjust if needed&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can then add a class that interacts with the API.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> logging
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> requests
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> APPNAME &lt;span style="color:#f92672">import&lt;/span> __version__ &lt;span style="color:#66d9ef">as&lt;/span> app_version
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> APPNAME &lt;span style="color:#f92672">import&lt;/span> settings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">GeoAPI&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> api_url &lt;span style="color:#f92672">=&lt;/span> settings&lt;span style="color:#f92672">.&lt;/span>GEOCODING_API_URL
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Set User-Agent headers as required by most usage policies (and it&amp;#39;s the nice thing to do)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;User-Agent&amp;#39;&lt;/span>: &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;APPNAME &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>app_version&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;From&amp;#39;&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;info@example.org&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">__init__&lt;/span>(self, debug&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">False&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>requests &lt;span style="color:#f92672">=&lt;/span> requests &lt;span style="color:#75715e"># ignore why we do this for now&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_coordinates_from_query&lt;/span>(self, location_string):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>requests&lt;span style="color:#f92672">.&lt;/span>get(self&lt;span style="color:#f92672">.&lt;/span>api_url, {&lt;span style="color:#e6db74">&amp;#34;q&amp;#34;&lt;/span>: location_string, &lt;span style="color:#e6db74">&amp;#34;format&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;jsonv2&amp;#34;&lt;/span>}, headers&lt;span style="color:#f92672">=&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>headers)&lt;span style="color:#f92672">.&lt;/span>json()[&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> result[&lt;span style="color:#e6db74">&amp;#34;lat&amp;#34;&lt;/span>], result[&lt;span style="color:#e6db74">&amp;#34;lon&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">_get_raw_response&lt;/span>(self, location_string):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>requests&lt;span style="color:#f92672">.&lt;/span>get(self&lt;span style="color:#f92672">.&lt;/span>api_url, {&lt;span style="color:#e6db74">&amp;#34;q&amp;#34;&lt;/span>: location_string, &lt;span style="color:#e6db74">&amp;#34;format&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;jsonv2&amp;#34;&lt;/span>}, headers&lt;span style="color:#f92672">=&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>headers)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> result&lt;span style="color:#f92672">.&lt;/span>content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_geojson_for_query&lt;/span>(self, location_string):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>requests&lt;span style="color:#f92672">.&lt;/span>get(self&lt;span style="color:#f92672">.&lt;/span>api_url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">&amp;#34;q&amp;#34;&lt;/span>: location_string,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;format&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;jsonv2&amp;#34;&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#f92672">=&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>headers)&lt;span style="color:#f92672">.&lt;/span>json()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span> &lt;span style="color:#66d9ef">as&lt;/span> e:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging&lt;span style="color:#f92672">.&lt;/span>warning(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Exception &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>e&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> when querying Nominatim&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> len(result) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging&lt;span style="color:#f92672">.&lt;/span>warning(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Couldn&amp;#39;t find a result for &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>location_string&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> when querying Nominatim&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> result
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The wrapper is a synchronous interface to our geocoding server and will wait until the server returns a response or times out. This impacts the user experienc, as a site will take longer to load. But it&amp;rsquo;s much easier to code, so here we are. If anyone wants to write a async interface for this I&amp;rsquo;ll not stop them!&lt;/p>
&lt;p>Fornow, let&amp;rsquo;s start by adding &lt;code>Location&lt;/code> to our &lt;code>models.py&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Location&lt;/span>(models&lt;span style="color:#f92672">.&lt;/span>Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> place_id &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>IntegerField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latitude &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>FloatField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> longitude &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>FloatField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">2000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">__str__&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>name&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>latitude&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">.5&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">, &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>longitude&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">.5&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@staticmethod&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_location_from_string&lt;/span>(location_string):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> geo_api &lt;span style="color:#f92672">=&lt;/span> geo&lt;span style="color:#f92672">.&lt;/span>GeoAPI()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> geojson &lt;span style="color:#f92672">=&lt;/span> geo_api&lt;span style="color:#f92672">.&lt;/span>get_geojson_for_query(location_string)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> geojson &lt;span style="color:#f92672">is&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result &lt;span style="color:#f92672">=&lt;/span> geojson[&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span> &lt;span style="color:#f92672">in&lt;/span> result:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#f92672">=&lt;/span> result[&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#f92672">=&lt;/span> result[&lt;span style="color:#e6db74">&amp;#34;display_name&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> location &lt;span style="color:#f92672">=&lt;/span> Location&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>create(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> place_id&lt;span style="color:#f92672">=&lt;/span>result[&lt;span style="color:#e6db74">&amp;#34;place_id&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latitude&lt;span style="color:#f92672">=&lt;/span>result[&lt;span style="color:#e6db74">&amp;#34;lat&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> longitude&lt;span style="color:#f92672">=&lt;/span>result[&lt;span style="color:#e6db74">&amp;#34;lon&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name&lt;span style="color:#f92672">=&lt;/span>name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> location
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;em>Don&amp;rsquo;t forget to make&amp;amp;run migrations after this&lt;/em>&lt;/p>
&lt;p>An finally we can use the API!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>location &lt;span style="color:#f92672">=&lt;/span> Location&lt;span style="color:#f92672">.&lt;/span>get_location_from_string(&lt;span style="color:#e6db74">&amp;#34;Berlin&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(location)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Berlin, Deutschland (52.51, 13.38)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Looking good!&lt;/p>
&lt;h1 id="area-search">Area search&lt;/h1>
&lt;p>Now wee have the coordinates - great! But how can we get the distance between coordinates? Lukily we are not the first people with that question and there is the &lt;a href="https://en.wikipedia.org/wiki/Haversine_formula">Haversine Formula&lt;/a> that we can use. It&amp;rsquo;s not a perfect fomula, for example it assumes the erth is perfectly round which the earth is not. But for most use cases of area search this should be irrelevant for the final result.&lt;/p>
&lt;p>Here is my implementation&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">calculate_distance_between_coordinates&lt;/span>(position1, position2):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> Calculate the distance between two points identified by coordinates
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> It expects the coordinates to be a tuple (lat, lon)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> Based on https://en.wikipedia.org/wiki/Haversine_formula
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> earth_radius_km &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">6371&lt;/span> &lt;span style="color:#75715e"># As per https://en.wikipedia.org/wiki/Earth_radius&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latitude1 &lt;span style="color:#f92672">=&lt;/span> float(position1[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> longitude1 &lt;span style="color:#f92672">=&lt;/span> float(position1[&lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latitude2 &lt;span style="color:#f92672">=&lt;/span> float(position2[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> longitude2 &lt;span style="color:#f92672">=&lt;/span> float(position2[&lt;span style="color:#ae81ff">1&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> distance_lat &lt;span style="color:#f92672">=&lt;/span> radians(latitude2 &lt;span style="color:#f92672">-&lt;/span> latitude1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> distance_long &lt;span style="color:#f92672">=&lt;/span> radians(longitude2 &lt;span style="color:#f92672">-&lt;/span> longitude1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> a &lt;span style="color:#f92672">=&lt;/span> pow(sin(distance_lat &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>), &lt;span style="color:#ae81ff">2&lt;/span>) &lt;span style="color:#f92672">+&lt;/span> cos(radians(latitude1)) &lt;span style="color:#f92672">*&lt;/span> cos(radians(latitude2)) &lt;span style="color:#f92672">*&lt;/span> pow(sin(distance_long &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> atan2(sqrt(a), sqrt(&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">-&lt;/span> a))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> distance_in_km &lt;span style="color:#f92672">=&lt;/span> earth_radius_km &lt;span style="color:#f92672">*&lt;/span> c
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> distance_in_km
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And with that we have a functioning area search 🎉&lt;/p></content></entry><entry><title>Where are you? - Part 1 - Geocoding with Nominatim to empower area search</title><link href="https://hyteck.de/post/geocoding-with-nominatim/" type="application/octet-stream"/><updated>2024-09-28T12:05:10+02:00</updated><id>https://hyteck.de/post/geocoding-with-nominatim/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Geocoding is the process of translating a text input like &lt;code>Ungewitterweg, Berlin&lt;/code> into a location with longitude and latitude such as &lt;code>52.544022/13.147589&lt;/code>. So whenever you search in OpenStreetMap or Google Maps for a location, it does exactly that (and sometimes more, but we don&amp;rsquo;t focus on that now).&lt;/p>
&lt;p>For a pet project of mine (&lt;a href="https://notfellchen.org">notfellchen.org&lt;/a>) I wanted to do exactly that: When a animal is added there to be adopted, the user must input a location that is geocoded and saved with it&amp;rsquo;s coordinates. When another user visits the site, that wants to adopt a pet in their area, they input their location and it will search for all animals in a specific radius.&lt;/p>
&lt;p>How is that done? I&amp;rsquo;ll show you!&lt;/p>
&lt;h1 id="nominatim">Nominatim&lt;/h1>
&lt;p>Nominatim is a software that uses OpenStreetMap data for geocoding. It can also do the reverse, find an address for any location on the planet. It is used for the geocoding on &lt;a href="https://openstreetmap.org">OpenStreetMap&lt;/a>, so it&amp;rsquo;s quite production-ready. We could use the public API (while obeying the &lt;a href="https://operations.osmfoundation.org/policies/nominatim/">usage policy&lt;/a>) but it&amp;rsquo;s nicer to have our own instance, so we don&amp;rsquo;t stress the resources of a donation funded organization and to improve user privacy.&lt;/p>
&lt;p>Nominatim works by importing geodate from a &lt;a href="https://wiki.openstreetmap.org/wiki/PBF_Format">PBF&lt;/a>-file into a postgres database. This database will later be queried to provide location data. The process is described below.&lt;/p>
&lt;h2 id="dns-records">DNS records&lt;/h2>
&lt;p>Se let&amp;rsquo;s start by setting the DNS records so that the domain &lt;code>geocoding.example.org&lt;/code> points to your server. Adjust as needed.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Value&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Target&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>geocoding.example.org&lt;/td>
&lt;td>CNAME&lt;/td>
&lt;td>server1.example.org&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="docker-compose-configuration">Docker-compose Configuration&lt;/h2>
&lt;p>We will use Docker Compose to run the official &lt;a href="https://hub.docker.com/r/mediagis/nominatim">Nominatim Docker image&lt;/a>.&lt;/p>
&lt;p>It bundles nominatim together with the database postgres. I usually prefere to have a central database for multiple services (e.g. allows easier backups) but for nominatim a seperate database is good for two reasons&lt;/p>
&lt;ul>
&lt;li>import process (described later) will not slow the database for other services&lt;/li>
&lt;li>it&amp;rsquo;s easier to nuke everything if things go wrong&lt;/li>
&lt;/ul>
&lt;p>The following environment variables will be used to configure the container&lt;/p>
&lt;ul>
&lt;li>&lt;code>PBF_URL&lt;/code>: The URL from where to download the PBF file that contains the geodate we will import. They can be obtained from &lt;a href="https://download.geofabrik.de/">Geofabrik&lt;/a>. It is highly recommended to first download the file to a local server and then set this URL to that server so that the ressources from Geofabrik are not affected if something goes wrong. Feel free to use the pre-set URL for germany while it works if you want to test around.&lt;/li>
&lt;li>&lt;code>REPLICATION_URL&lt;/code>: Where to get updates from. For example Geofabrik&amp;rsquo;s update for the Europe extract are available at &lt;code>https://download.geofabrik.de/europe-updates/&lt;/code> Other places at Geofabrik follow the pattern &lt;code>https://download.geofabrik.de/$CONTINENT/$COUNTRY-updates/&lt;/code>&lt;/li>
&lt;li>&lt;code>POSTGRES_&lt;/code> Postgres tuning data, the current setting allows imports on a ressource constrained system. See &lt;a href="https://github.com/mediagis/nominatim-docker/tree/master/4.4#postgresql-tuning">postgres tuning docs&lt;/a> for more info&lt;/li>
&lt;li>&lt;code>NOMINATIM_PASSWORD&lt;/code>: Database password.&lt;/li>
&lt;li>&lt;code>IMPORT_STYLE&lt;/code>: See below&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Import Styles&lt;/strong>&lt;/p>
&lt;p>Import styles will determin how much &amp;ldquo;resolution&amp;rdquo; the geocoding has. It has the following options&lt;/p>
&lt;ul>
&lt;li>&lt;code>admin&lt;/code>: Only import administrative boundaries and places.&lt;/li>
&lt;li>&lt;code>street&lt;/code>: Like the admin style but also adds streets.&lt;/li>
&lt;li>&lt;code>address&lt;/code>: Import all data necessary to compute addresses down o house number level.&lt;/li>
&lt;li>&lt;code>full&lt;/code>: Default style that also includes points of interest.&lt;/li>
&lt;li>&lt;code>extratags&lt;/code>: Like the full style but also adds most of the OSM tags into the extratags column.&lt;/li>
&lt;/ul>
&lt;p>It has a huge impact on how long the import will take and how much space it will require. Be aware that the import time is on a machine with 32GB RAM, 4 CPUS and SSDs, these are not fixed numbers. My import of &lt;code>admin&lt;/code> took 12 hours.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Style&lt;/th>
&lt;th>Import time&lt;/th>
&lt;th>DB size&lt;/th>
&lt;th>after drop&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>admin&lt;/td>
&lt;td>4h&lt;/td>
&lt;td>215 GB&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>street&lt;/td>
&lt;td>22h&lt;/td>
&lt;td>440 GB&lt;/td>
&lt;td>185 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>address&lt;/td>
&lt;td>36h&lt;/td>
&lt;td>545 GB&lt;/td>
&lt;td>260 GB&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Explaining &lt;em>after drop&lt;/em> (from the &lt;a href="https://nominatim.org/release-docs/3.3/admin/Import-and-Update/">docs&lt;/a>)&lt;/p>
&lt;blockquote>
&lt;p>About half of the data in Nominatim&amp;rsquo;s database is not really used for serving the API. It is only there to allow the data to be updated from the latest changes from OSM. For many uses these dynamic updates are not really required. If you don&amp;rsquo;t plan to apply updates, the dynamic part of the database can be safely dropped using the following command: &lt;code>./utils/setup.php --drop&lt;/code>&lt;/p>&lt;/blockquote>
&lt;p>I have not done this, so I don&amp;rsquo;t have any experince with that. But probably it&amp;rsquo;s a good idea if you don&amp;rsquo;t need up-to-date data.&lt;/p>
&lt;h2 id="reverse-proxy">Reverse Proxy&lt;/h2>
&lt;p>As with most of my projects, it runs on a server where the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook">mash-playbook&lt;/a> has deployed a &lt;a href="https://doc.traefik.io/traefik/">Traefik&lt;/a>, as &lt;em>Application Proxy&lt;/em>. I&amp;rsquo;ll therefore use trafik labels to configure the revers proxy but the same could be achieved with Caddy or Nginx.&lt;/p>
&lt;h2 id="complete-configuration">Complete configuration&lt;/h2>
&lt;pre tabindex="0">&lt;code>services:
nominatim:
environment:
- PBF_URL=https://cdn.hyteck.de/osm/germany-latest.osm.pbf
- REPLICATION_URL=https://download.geofabrik.de/europe/germany-updates/
- POSTGRES_SHARED_BUFFERS=1GB
- POSTGRES_MAINTENANCE_WORK_MEM=1GB
- POSTGRES_AUTOVACUUM_WORK_MEM=500MB
- POSTGRES_EFFECTIVE_CACHE_SIZE=1GB
- IMPORT_STYLE=admin
- NOMINATIM_PASSWORD=VERYSECRET
labels:
- &amp;#34;traefik.enable=true&amp;#34;
- &amp;#34;traefik.docker.network=traefik&amp;#34;
- &amp;#34;traefik.http.routers.nominatim.rule=Host(`geocoding.example.org`)&amp;#34;
- &amp;#34;traefik.http.routers.nominatim.service=nominatim-service&amp;#34;
- &amp;#34;traefik.http.routers.nominatim.entrypoints=web-secure&amp;#34;
- &amp;#34;traefik.http.routers.nominatim.tls=true&amp;#34;
- &amp;#34;traefik.http.routers.nominatim.tls.certResolver=default&amp;#34;
- &amp;#34;traefik.http.services.nominatim-service.loadbalancer.server.port=8080&amp;#34;
container_name: nominatim
image: mediagis/nominatim:4.4
restart: always
networks:
- traefik
volumes:
- nominatim-data:/var/lib/postgresql/14/main
- nominatim-flatnode:/nominatim/flatnode
shm_size: 1gb
volumes:
nominatim-flatnode:
nominatim-data:
networks:
traefik:
name: &amp;#34;traefik&amp;#34;
external: true
&lt;/code>&lt;/pre>&lt;h2 id="importing">Importing&lt;/h2>
&lt;p>Now we are ready to go! Before you type &lt;code>docker-compose up -d&lt;/code> let me explain what it will do&lt;/p>
&lt;ol>
&lt;li>Start the database&lt;/li>
&lt;li>Download the PBF file from the given URL&lt;/li>
&lt;li>Import the PBF file into the database. Here you are most likely to run into errors because of ressource constraints&lt;/li>
&lt;li>Start the Nominatim server&lt;/li>
&lt;/ol>
&lt;p>If you are ready, lets go: &lt;code>docker-compose up -d&lt;/code>. Monitor what nominatim is doing with &lt;code>docker logs -f nominatim&lt;/code> and make a cup of tea. This will take a while (proably several hours).&lt;/p>
&lt;h2 id="testing">Testing&lt;/h2>
&lt;p>You can test your server by visiting the domain. Try &lt;code>/?q=CITYNAME&lt;/code> to see an actual search result.&lt;/p>
&lt;p>Example: &lt;code>https://geocoding.example.org/?q=tuebingen&lt;/code>&lt;/p>
&lt;h1 id="result">Result&lt;/h1>
&lt;p>You should now have a running Nominatim instance that you can use for geocoding 🎉. Initially I wanted to show in the same post how you&amp;rsquo;d use this server to power area search in django but that will be in part 2. Feel free to ping me for questions, preferably at &lt;a href="https://gay-pirate-assassins.de/@moanos">@moanos@gay-pirate-assassins.de&lt;/a>&lt;/p>
&lt;p>Oh and one last thing:&lt;/p>
&lt;h2 id="legal-requirements">Legal requirements&lt;/h2>
&lt;p>Data from OpenStreetMap is licenced under the &lt;a href="https://opendatacommons.org/licenses/odbl/">Open Database License&lt;/a>. The ODbL allows you to use the OSM data for any purpose you like but &lt;strong>attribution is required&lt;/strong>. For showing map data, you&amp;rsquo;d usually display a small badge in the bottom left corner of the map. But geocoding also needs attribution, &lt;a href="https://osmfoundation.org/wiki/Licence/Attribution_Guidelines#Geocoding_(search)">as per this guideline&lt;/a>.&lt;/p></content></entry><entry><title>Styling an Django RSS Feed</title><link href="https://hyteck.de/post/django-rss/" type="application/octet-stream"/><updated>2024-04-16T12:10:10+02:00</updated><id>https://hyteck.de/post/django-rss/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>RSS is amazing! While not everyone thinks that, most people that &lt;em>understand&lt;/em> RSS, like it. This presents a problem, as most people don&amp;rsquo;t have chance to learn about it. Unless there is a person in the community that doesn&amp;rsquo;t shut up about how great RSS is (maybe that person is you), they might not even know what it is, let alone use it.&lt;/p>
&lt;p>One big reason for this is, that when you click an link to an RSS feed you download a strange file that most people don&amp;rsquo;t know how to deal with. Maybe your browser is nice and renders some XML which is also not meant for human consumption. Wouldn&amp;rsquo;t it be better if people clicked on the RSS link and were greeted by a text explaining RSS and how to use it? And if the site would still be a valid RSS feed?&lt;/p>
&lt;p>Luckily you don&amp;rsquo;t have to imagine that - it&amp;rsquo;s possible! You can even try it on this blog by clicking the RSS link in the menu (&lt;a href="https://hyteck.de/index.xml">direct link&lt;/a>).&lt;/p>
&lt;p>Doing this has not been my idea. Darek Kay described this in the blog post &lt;a href="https://darekkay.com/blog/rss-styling/">Style your RSS feed&lt;/a> and I just copied most of their work! This was fairly easy for this Hugo blog and is &lt;a href="https://github.com/moan0s/hugo-nederburg-theme">available in my fork of the hugo-nederburg-theme&lt;/a>. However, in a Django project it get&amp;rsquo;s a bit more complicated. Let me explain.&lt;/p>
&lt;h2 id="the-problem">The Problem&lt;/h2>
&lt;p>Django has the great &lt;a href="https://docs.djangoproject.com/en/5.0/ref/contrib/syndication/">Syndication feed framework&lt;/a>, a high level framework to create RSS and Atom Feeds. This is great as we only need a few lines of code to create a feed. Here is an example from &lt;a href="https://notfellchen.org">notfellchen.org&lt;/a> that list animals that are in search for a new home. People should be able to follow the RSS feed to see new adoption notices. So lets do it&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># in src/fellchennasen/feeds.py&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.contrib.syndication.views &lt;span style="color:#f92672">import&lt;/span> Feed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> .models &lt;span style="color:#f92672">import&lt;/span> AdoptionNotice
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LatestAdoptionNoticesFeed&lt;/span>(Feed):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> title &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Notfellchen&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> link &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/rss/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> description &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Updates zu neuen Vermittlungen.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">items&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> AdoptionNotice&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>order_by(&lt;span style="color:#e6db74">&amp;#34;-created_at&amp;#34;&lt;/span>)[:&lt;span style="color:#ae81ff">5&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">item_title&lt;/span>(self, item):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> item&lt;span style="color:#f92672">.&lt;/span>name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">item_description&lt;/span>(self, item):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> item&lt;span style="color:#f92672">.&lt;/span>description
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># in src/fellchennasen/urls.py&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>urlpatterns &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path(&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>, views&lt;span style="color:#f92672">.&lt;/span>index, name&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;index&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> path(&lt;span style="color:#e6db74">&amp;#34;rss/&amp;#34;&lt;/span>, LatestAdoptionNoticesFeed(), name&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;rss&amp;#34;&lt;/span>), &lt;span style="color:#75715e"># &amp;lt;--- Added&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait that&amp;rsquo;s it? Yeah! We have a working RSS feed. And it was very convenient, Django allows us to create by just pointing it to the right model and fields we want to display.&lt;/p>
&lt;p>But here is the problem: How do we style this? We can&amp;rsquo;t just add a link to a stylesheet here.&lt;/p>
&lt;h2 id="the-solution">The solution&lt;/h2>
&lt;p>First we need to add our styling files. I&amp;rsquo;ll not go into detail how they work her, just refer &lt;a href="https://darekkay.com/blog/rss-styling/">Darek&amp;rsquo;s blog post&lt;/a> for that. In Django we add them to our static files&lt;/p>
&lt;ul>
&lt;li>&lt;code>static/rss.xsl&lt;/code> will be adjusted based on &lt;a href="rss.xsl">this file&lt;/a>. It is responsible for creating a html rendering of your XML file&lt;/li>
&lt;li>for &lt;code>static/css/rss-styles&lt;/code> you can drop in &lt;a href="rss-styles.css">this file&lt;/a>, which is a basic CSS file you can edit to your liking.&lt;/li>
&lt;/ul>
&lt;p>After that comes the hard part. How do tweak this wonderfully simple Feed class to include a link to our style sheet? I first thought &amp;ldquo;that must be easy, just follow the docs on &lt;a href="https://docs.djangoproject.com/en/5.0/ref/contrib/syndication/#custom-feed-generators">custom feed generators&lt;/a> and add a root element. Something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">FormattedFeed&lt;/span>(Rss201rev2Feed):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">add_root_elements&lt;/span>(self, handler):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> super()&lt;span style="color:#f92672">.&lt;/span>add_root_elements(handler)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># We want &amp;lt;?xml-stylesheet href=&amp;#34;/static/rss.xsl&amp;#34; type=&amp;#34;text/xsl&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler&lt;span style="color:#f92672">.&lt;/span>addQuickElement(&lt;span style="color:#e6db74">&amp;#34;?xml-stylesheet&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;href=&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>static(&lt;span style="color:#e6db74">&amp;#34;rss.xsl&amp;#34;&lt;/span>)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LatestAdoptionNoticesFeed&lt;/span>(Feed):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> feed_type &lt;span style="color:#f92672">=&lt;/span> FormattedFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> title &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Notfellchen&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Looks good. Let&amp;rsquo;s try. Oh no what is this?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&amp;lt;&lt;/span>?xml-stylesheet href=&amp;#34;/static/rss.xsl&amp;#34;/&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Yes, we can&amp;rsquo;t correctly close this tag. There is (to my knowledge) no easy way to do this. So let&amp;rsquo;s take the hard road an implement a custom write function. In the following the write function will be copied from &lt;code>django.utils.feedgenerator.RssFeed&lt;/code>. We make two important changes to the class:&lt;/p>
&lt;ol>
&lt;li>Changing the content type from &lt;code>content_type = &amp;quot;application/rss+xml; charset=utf-8&amp;quot;&lt;/code> to &lt;code>content_type = &amp;quot;text/rss+xml; charset=utf-8&lt;/code>. This will make a browser display the content rather than opening it in a app.&lt;/li>
&lt;li>Adding our xml-stylsheet information. This is done in the &lt;code>write()&lt;/code> function with this line&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>handler&lt;span style="color:#f92672">.&lt;/span>_write(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;&amp;lt;?xml-stylesheet href=&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>static(&lt;span style="color:#e6db74">&amp;#34;rss.xsl&amp;#34;&lt;/span>)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34; type=&amp;#34;text/xsl&amp;#34;?&amp;gt;&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Putting it all together we still have a relativly simple solution with only the necessary adjustments. Here is the full &lt;code>feeds.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.contrib.syndication.views &lt;span style="color:#f92672">import&lt;/span> Feed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.utils.feedgenerator &lt;span style="color:#f92672">import&lt;/span> Rss201rev2Feed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.templatetags.static &lt;span style="color:#f92672">import&lt;/span> static
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.utils.xmlutils &lt;span style="color:#f92672">import&lt;/span> SimplerXMLGenerator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> .models &lt;span style="color:#f92672">import&lt;/span> AdoptionNotice
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">FormattedFeed&lt;/span>(Rss201rev2Feed):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content_type &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;text/xml; charset=utf-8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">write&lt;/span>(self, outfile, encoding):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler &lt;span style="color:#f92672">=&lt;/span> SimplerXMLGenerator(outfile, encoding, short_empty_elements&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler&lt;span style="color:#f92672">.&lt;/span>startDocument()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler&lt;span style="color:#f92672">.&lt;/span>_write(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;&amp;lt;?xml-stylesheet href=&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>static(&lt;span style="color:#e6db74">&amp;#34;rss.xsl&amp;#34;&lt;/span>)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34; type=&amp;#34;text/xsl&amp;#34;?&amp;gt;&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler&lt;span style="color:#f92672">.&lt;/span>startElement(&lt;span style="color:#e6db74">&amp;#34;rss&amp;#34;&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>rss_attributes())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler&lt;span style="color:#f92672">.&lt;/span>startElement(&lt;span style="color:#e6db74">&amp;#34;channel&amp;#34;&lt;/span>, self&lt;span style="color:#f92672">.&lt;/span>root_attributes())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>add_root_elements(handler)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>write_items(handler)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>endChannelElement(handler)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> handler&lt;span style="color:#f92672">.&lt;/span>endElement(&lt;span style="color:#e6db74">&amp;#34;rss&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">LatestAdoptionNoticesFeed&lt;/span>(Feed):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> feed_type &lt;span style="color:#f92672">=&lt;/span> FormattedFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> title &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Notfellchen&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> link &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/rss/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> description &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Updates zu neuen Vermittlungen.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">items&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> AdoptionNotice&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>order_by(&lt;span style="color:#e6db74">&amp;#34;-created_at&amp;#34;&lt;/span>)[:&lt;span style="color:#ae81ff">5&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">item_title&lt;/span>(self, item):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> item&lt;span style="color:#f92672">.&lt;/span>name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">item_description&lt;/span>(self, item):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> item&lt;span style="color:#f92672">.&lt;/span>description
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And finally we have what we want! A RSS feed displayed in the browser, with beginner-friendly explanation and still completely spec-compliant.&lt;/p>
&lt;p>&lt;img src="screenshot1.jpeg" alt="Screenshot of a website">&lt;/p>
&lt;h2 id="outlook">Outlook&lt;/h2>
&lt;p>Now you may recognize I&amp;rsquo;m not a frontend person. The style could be prettier and provide a better overview. But I&amp;rsquo;d argue the improvement is immense and might help a user to get started with RSS.&lt;/p>
&lt;p>There are still a couple things to improve:&lt;/p>
&lt;ul>
&lt;li>Translation: The current text is only displayed in english&lt;/li>
&lt;li>The &lt;code>rss.xsl&lt;/code> file has a hard-coded link to the css stylesheet in it&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">link&lt;/span> &lt;span style="color:#a6e22e">rel&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">type&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;text/css&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">href&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;/static/fellchensammlung/css/rss-styles.css&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Both can be solved by templating the &lt;code>rss.xsl&lt;/code> instead of serving it as static file.&lt;/p>
&lt;p>So have fun playing around! If you have created or found a nice-looking RSS feed let me know. Let&amp;rsquo;s keep RSS alive and thriving!&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "django-rss"
})
&lt;/script></content></entry><entry><title>Tracking blog readers with OxiTraffic</title><link href="https://hyteck.de/post/oxitraffic-setup/" type="application/octet-stream"/><updated>2023-11-10T12:10:10+02:00</updated><id>https://hyteck.de/post/oxitraffic-setup/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I recently stumbled upon &lt;a href="https://codeberg.org/mo8it/oxitraffic">OxiTraffic&lt;/a>, a self-hosted, simple and privacy respecting website traffic tracker which is well suited for blogs. What that means is&lt;/p>
&lt;ul>
&lt;li>No personal data is logged&lt;/li>
&lt;li>one binary or simple docker container&lt;/li>
&lt;li>Readers are only counted if they spend &amp;gt;20s per site&lt;/li>
&lt;/ul>
&lt;p>As I currently have no analytics on my blog and I am not inclined to use anything that adds more than 2 sentences to my privacy disclaimer I thought I give it a try. Naturally I wrote an ansible role for this, which can be found under &lt;a href="https://github.com/mother-of-all-self-hosting/ansible-role-oxitraffic">mother-of-all-self-hosting/ansible-role-oxitraffic&lt;/a>. I now have this neat graph.&lt;/p>
&lt;p>&lt;img src="oxitraffic_screenshot.jpeg" alt="A screenshot of OxiTraffic that shows low readership on hyteck.de">&lt;/p>
&lt;p>As the main prupose of a blog is to describe how to host the blog, I&amp;rsquo;ll continue in this tradition and describe my process below.&lt;/p>
&lt;h1 id="the-ansible-role--playbook-integration">The Ansible Role &amp;amp; Playbook Integration&lt;/h1>
&lt;p>The ansible role is pretty simple so I won&amp;rsquo;t go into detail. It set&amp;rsquo;s up the configuration file based on your environment variables and sensible defaults and adds a labels file for traefik to use later. The systemd service that starts the container ensures it runs read-only and as non-root user (which worked out of the box, kudos to the developer).&lt;/p>
&lt;p>The &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook">mash-playbook&lt;/a> integration is wiring the OxiTraffic to the Traefik reverse proxy and the Postgres database.&lt;/p>
&lt;p>After running &lt;code>just install-all&lt;/code> everything was set up*.&lt;/p>
&lt;p>* Actually I &lt;a href="https://codeberg.org/mo8it/oxitraffic/issues/7">found a bug which was fixed very fast&lt;/a>&lt;/p>
&lt;h1 id="hugo-theme-integration">Hugo Theme Integration&lt;/h1>
&lt;p>I maintain a fork of the &lt;a href="https://github.com/moan0s/hugo-nederburg-theme">hugo-nederburg-theme&lt;/a> by Appernetic and naturally wanted to include it there. Adding the following to &lt;code>themes/hugo-nederburg-theme/layouts/partials/head.html&lt;/code> is all I needed&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>{{ with .Site.Params.oxitraffic_url }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#f92672">script&lt;/span> &lt;span style="color:#a6e22e">src&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;{{ . }}&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">defer&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{{ end }}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I could then make us of this by setting the Oxitraffic URL in the theme settings&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-toml" data-lang="toml">&lt;span style="display:flex;">&lt;span>[&lt;span style="color:#a6e22e">params&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">slogan&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;Blog of Julian-Samuel Gebühr&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">description&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;Blog of Julian-Samuel Gebühr&amp;#34;&lt;/span> &lt;span style="color:#75715e"># meta description&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [...]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">oxitraffic_url&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;https://traffic.hyteck.de/count.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And that was it. You can have a look at the traffic of this blog at &lt;a href="https://traffic.hyteck.de">traffic.hyteck.de&lt;/a>.&lt;/p>
&lt;h1 id="advanced-setting-up-multiple-sites-in-on-one-mash-host">Advanced: Setting up multiple sites in on one MASH host&lt;/h1>
&lt;p>You might have multiple sites that need tracking, but an instance of OxiTraffic can only monitor one site. Setting up multiple instances of OxiTraffic is more complicated in MASH, but can be done. Here is how (always replace &lt;code>s3&lt;/code> and &lt;code>other&lt;/code> with you own names):&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Re-Do your Inventory as described in &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/running-multiple-instances.md#re-do-your-inventory-to-add-supplementary-hosts">running-multiple-instances&lt;/a>. I&amp;rsquo;ll use &lt;code>s3&lt;/code> as my &amp;ldquo;main&amp;rdquo; host here and &lt;code>s3.other&lt;/code> as new host.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Add the following in &lt;code>inventory/host_vars/s3.other&lt;/code>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># PLAYBOOK STUFF&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mash_playbook_generic_secret_key&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;LONGSECRET&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mash_playbook_service_identifier_prefix&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;mash-other-&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mash_playbook_service_base_directory_name_prefix&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;other-&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># OXITRAFFIC configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_hostname&lt;/span>: &lt;span style="color:#ae81ff">traffic.other-service.de&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_tracked_origin&lt;/span>: &lt;span style="color:#ae81ff">https://other-service.de&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_database_hostname&lt;/span>: &lt;span style="color:#ae81ff">mash-postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_database_port&lt;/span>: &lt;span style="color:#ae81ff">5432&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_database_name&lt;/span>: &lt;span style="color:#ae81ff">other-oxitraffic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_database_password&lt;/span>: &lt;span style="color:#ae81ff">VERYSECRET&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_database_username&lt;/span>: &lt;span style="color:#ae81ff">other-oxitraffic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_systemd_required_services_list&lt;/span>: |&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> {{
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ([&amp;#39;docker.service&amp;#39;])
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> +
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ([&amp;#39;mash-postgres.service&amp;#39;])
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_container_additional_networks&lt;/span>: |&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> {{
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ([&amp;#39;traefik&amp;#39;])
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> +
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ([&amp;#39;mash-postgres&amp;#39;])
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_container_labels_traefik_enabled&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_container_labels_traefik_docker_network&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;traefik&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_container_labels_traefik_entrypoints&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;web-secure&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">oxitraffic_container_labels_traefik_tls_certResolver&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;default&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>Create the database&lt;/li>
&lt;/ol>
&lt;p>Unlike for other mash services th database will not be created automatically. You therefore need to set it up yourself. Here are the steps that you need to run in the postgres CLI (which cou can access by running &lt;code>/mash/postgres/bin/cli&lt;/code>)&lt;/p>
&lt;ul>
&lt;li>Create a user: &lt;code>CREATE USER &amp;quot;other-oxitraffic&amp;quot; with ENCRYPTED PASSWORD 'PASSWORD_FROM_ABOVE';&lt;/code>&lt;/li>
&lt;li>Create database: &lt;code>CREATE DATABASE other-oxitraffic;&lt;/code>&lt;/li>
&lt;li>Grant privileges: &lt;code>GRANT ALL PRIVILEGES ON DATABASE &amp;quot;other-oxitraffic&amp;quot; TO &amp;quot;other-oxitraffic&amp;quot;;&lt;/code>&lt;/li>
&lt;li>Grant ownership: &lt;code>ALTER DATABASE &amp;quot;other-oxitraffic&amp;quot; OWNER TO &amp;quot;other-oxitraffic&amp;quot;;&lt;/code>&lt;/li>
&lt;/ul></content></entry><entry><title>Deploying a django app with docker, ansible and traefik</title><link href="https://hyteck.de/post/deploying-django-with-docker-and-ansible/" type="application/octet-stream"/><updated>2023-07-24T22:10:10+02:00</updated><id>https://hyteck.de/post/deploying-django-with-docker-and-ansible/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>This blog post will try to outline the process of deploying &lt;a href="https://github.com/moan0s/ILMO2">ILMO&lt;/a> (a &lt;a href="https://www.djangoproject.com/">Django&lt;/a> app) by building a &lt;a href="https://www.docker.com/">docker&lt;/a> image, using &lt;a href="https://www.ansible.com/">ansible&lt;/a> to install&amp;amp;configure it on our server and use &lt;a href="https://traefik.io/">Traefik&lt;/a> as webserver that is readily configured and obtains certificates for us.&lt;/p>
&lt;p>I will go through the steps one by one and link more extensive documentation.&lt;/p>
&lt;h1 id="building-the-docker-image">Building the docker image&lt;/h1>
&lt;p>Building the docker image is pretty straightforward as it closely resembles the steps of &lt;a href="https://ilmo2.readthedocs.io/en/latest/dev/deployment.html#manual-deployment">manual deployment&lt;/a>. The docker file is probably terribly inefficient as it is to large and should be build in stages. Consider this a working example, not a best practice. Also feel free to give me pointers on how to improve it. Specifics I want to point out are:&lt;/p>
&lt;ul>
&lt;li>static files are collected when building the image&lt;/li>
&lt;li>&lt;code>pip install -e .&lt;/code> is used to install the python package. Without &lt;code>-e&lt;/code> the apps static files will not be collected correctly. I haven&amp;rsquo;t figured out why.&lt;/li>
&lt;li>the CMD &lt;code>ilmo&lt;/code> is executed when starting the container and maps to the script in &lt;code>docker/ilmo.bash&lt;/code> (see below).&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Dockerfile" data-lang="Dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>&lt;span style="color:#e6db74"> python:3-slim&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">MAINTAINER&lt;/span>&lt;span style="color:#e6db74"> Julian-Samuel Gebühr&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">ENV&lt;/span> DOCKER_BUILD&lt;span style="color:#f92672">=&lt;/span>true
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">RUN&lt;/span> apt update&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> apt install gettext -y&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">ENV&lt;/span> VIRTUAL_ENV&lt;span style="color:#f92672">=&lt;/span>/var/ilmo/venv&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> python -m venv $VIRTUAL_ENV&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">ENV&lt;/span> PATH&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$VIRTUAL_ENV&lt;span style="color:#e6db74">/bin:&lt;/span>$PATH&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">COPY&lt;/span> src/requirements.txt requirements.txt&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> pip install -r requirements.txt&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">WORKDIR&lt;/span>&lt;span style="color:#e6db74"> /var/ilmo&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">COPY&lt;/span> . .&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> pip install -e . &lt;span style="color:#75715e"># Without the -e the library static folder will not be copied by collectstatic!&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> mkdir /ilmo&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> mkdir /ilmo/static&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> ilmo-manage collectstatic --noinput&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> ilmo-manage compilemessages --ignore venv&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">COPY&lt;/span> docker/ilmo.bash $VIRTUAL_ENV/bin/ilmo&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">EXPOSE&lt;/span>&lt;span style="color:#e6db74"> 8345&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">CMD&lt;/span> [&lt;span style="color:#e6db74">&amp;#34;ilmo&amp;#34;&lt;/span>]&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The standard command of the container is a small bash script located at &lt;code>docker/ilmo.bash&lt;/code> that&lt;/p>
&lt;ul>
&lt;li>activates the virtual environment&lt;/li>
&lt;li>sets a number of workers based on the available CPU cores&lt;/li>
&lt;li>applies migrations to the database&lt;/li>
&lt;li>executes &lt;a href="https://gunicorn.org/">gunicorn&lt;/a> as &lt;a href="https://de.wikipedia.org/wiki/Web_Server_Gateway_Interface">WSGI&lt;/a> HTTP Server on port 8345&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -eux
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /var/ilmo/src
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>export DATA_DIR&lt;span style="color:#f92672">=&lt;/span>/var/ilmo/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source /var/ilmo/venv/bin/activate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AUTOMIGRATE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>AUTOMIGRATE&lt;span style="color:#66d9ef">:-&lt;/span>yes&lt;span style="color:#e6db74">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NUM_WORKERS_DEFAULT&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$((&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">$(&lt;/span>nproc --all&lt;span style="color:#66d9ef">)))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>export NUM_WORKERS&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>NUM_WORKERS&lt;span style="color:#66d9ef">:-&lt;/span>$NUM_WORKERS_DEFAULT&lt;span style="color:#e6db74">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">[&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$AUTOMIGRATE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> !&lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;skip&amp;#34;&lt;/span> &lt;span style="color:#f92672">]&lt;/span>; &lt;span style="color:#66d9ef">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ilmo-manage migrate --noinput
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>exec gunicorn ilmo.wsgi &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --name ilmo &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --workers $NUM_WORKERS &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --max-requests &lt;span style="color:#ae81ff">1200&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --max-requests-jitter &lt;span style="color:#ae81ff">50&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --log-level&lt;span style="color:#f92672">=&lt;/span>info &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> --bind 0.0.0.0:8345
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="using-whitenoise-to-serve-static-files">Using WhiteNoise to serve static files&lt;/h1>
&lt;p>Django apps usually put their static files in the directory you define in &lt;code>STATIC_ROOT&lt;/code> after running &lt;code>python manage.py collectstatic&lt;/code> and expect a webserver like nginx to serve theses files. Now as &lt;a href="https://hyteck.de/post/static-sites-with-mash/">discussed before&lt;/a> traefik does not easily serve static files. Luckily there is a solution for that: &lt;a href="https://whitenoise.readthedocs.io">WhiteNoise&lt;/a>. It allows a django app to serve it&amp;rsquo;s own static files &lt;a href="https://whitenoise.readthedocs.io/en/latest/#isn-t-serving-static-files-from-python-horribly-inefficient">pretty efficiently&lt;/a> while it also takes care of best-practices for you, for instance:&lt;/p>
&lt;ul>
&lt;li>Serving compressed content (gzip and Brotli formats, handling Accept-Encoding and Vary headers correctly)&lt;/li>
&lt;li>Setting far-future cache headers on content which won’t change (useful if working with CDNs).&lt;/li>
&lt;/ul>
&lt;p>To get it to work we have to:&lt;/p>
&lt;ul>
&lt;li>add WhiteNoise to the dependencies (see my &lt;a href="https://github.com/moan0s/ILMO2/blob/main/pyproject.toml">pyproject.toml&lt;/a>)&lt;/li>
&lt;li>add the WhiteNoise middleware directly after the SecurityMiddleware&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>MIDDLEWARE &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;django.middleware.security.SecurityMiddleware&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;whitenoise.middleware.WhiteNoiseMiddleware&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>define the &lt;a href="https://docs.djangoproject.com/en/4.2/ref/settings/#storages">storage backend&lt;/a> (this is new for django &amp;gt;4.2, for previous version use &lt;a href="https://docs.djangoproject.com/en/4.2/ref/settings/#staticfiles-storage">&lt;code>STATICFILES_STORAGE&lt;/code>&lt;/a>). This is not strictly necessary but improves performance.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>STORAGES &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;staticfiles&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;BACKEND&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;whitenoise.storage.CompressedManifestStaticFilesStorage&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When testing if the new configuration works you should test with &lt;code>DEBUG=False&lt;/code>. Otherwise django will serve static files by itself (which is not safe for production). If you encounter problems check the &lt;a href="https://whitenoise.readthedocs.io/en/latest/django.html">Whitenoise Documentation&lt;/a>.&lt;/p>
&lt;h1 id="traefik-as-webserver">Traefik as webserver&lt;/h1>
&lt;p>&lt;a href="https://traefik.io/">Traefik&lt;/a> is a HTTP(S) reverse proxy and load balancer. It is focused on containers and supports dynamic configuration. This means we can spin up a docker container with the &lt;code>--label /path/to/label_file&lt;/code> flag and traefik will use the configuration in the label file to register a new service and router, obtain SSL certificates and start routing traffic to your application.&lt;/p>
&lt;p>For ILMO our traefik configuration adds some sensible response headers, defines an entrypoint (&lt;code>web-secure&lt;/code> stands for HTTPS via port 443), add a SSL certificate resolver (&lt;code>default&lt;/code> is here LetsEncrypt) and tells traefik where to send traefik to &lt;code>traefik.docker.network=traefik&lt;/code> and &lt;code>traefik.http.services.mash-ilmo.loadbalancer.server.port=8345&lt;/code>. It assumes traefik and the application are both in the docker network called &lt;code>traefik&lt;/code>.&lt;/p>
&lt;p>Everything together looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-cfg" data-lang="cfg">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.docker.network&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">traefik&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.middlewares.mash-ilmo-add-response-headers.headers.customresponseheaders.X-XSS-Protection&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">1; mode=block&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.middlewares.mash-ilmo-add-response-headers.headers.customresponseheaders.X-Frame-Options&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">SAMEORIGIN&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.middlewares.mash-ilmo-add-response-headers.headers.customresponseheaders.X-Content-Type-Options&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">nosniff&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.middlewares.mash-ilmo-add-response-headers.headers.customresponseheaders.Content-Security-Policy&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">frame-ancestors &amp;#39;self&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.middlewares.mash-ilmo-add-response-headers.headers.customresponseheaders.Permission-Policy&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">interest-cohort=()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.middlewares.mash-ilmo-add-response-headers.headers.customresponseheaders.Strict-Transport-Security&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">max-age=31536000; includeSubDomains&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.enable&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.routers.mash-ilmo.rule&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">Host(&amp;#34;ilmo.example.com&amp;#34;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.routers.mash-ilmo.middlewares&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">mash-ilmo-add-response-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.routers.mash-ilmo.service&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">mash-ilmo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.routers.mash-ilmo.entrypoints&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">web-secure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.routers.mash-ilmo.tls&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.routers.mash-ilmo.tls.certResolver&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">traefik.http.services.mash-ilmo.loadbalancer.server.port&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">8345&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="ansible-to-deploy">Ansible to deploy&lt;/h1>
&lt;p>The ansible role will set up everything we did so far on the server. I will not discuss the inner workings of the role in detail as the role is mostly derived from the generic role layout we use in &lt;a href="https://github.com/mother-of-all-self-hosting">MASH&lt;/a> for a large variety of services.&lt;/p>
&lt;p>The role features: Install, uninstall and creating the first user. It does so by installing a config and data path, configuring the traefik labels and configuration file, pulling the docker image and finally setting up a systemd service to start the container.&lt;/p>
&lt;p>Used together with the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook">MASH playbook&lt;/a> it will also set up a database user and database and install traefik.&lt;/p>
&lt;p>The full role can be found at &lt;a href="https://github.com/moan0s/ansible-role-ilmo">ansible-role-ilmo&lt;/a>&lt;/p>
&lt;h1 id="final-thoughts">Final thoughts&lt;/h1>
&lt;p>The process of deploying a django app via docker sure is somewhat complicated. In the end I am still glad to have done it as I think it a) will make deployment more reliable &amp;amp; easier to maintain b) encouraged me to make some design decisions that improved the app itself.&lt;/p>
&lt;p>Reach out if you have questions or think this blog post could be improved!&lt;/p></content></entry><entry><title>Hosting static sites with Traefik</title><link href="https://hyteck.de/post/static-sites-with-mash/" type="application/octet-stream"/><updated>2023-07-16T15:10:10+02:00</updated><id>https://hyteck.de/post/static-sites-with-mash/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="hosting-static-sites-with-traefik-and-static-web-server">Hosting Static Sites with &lt;a href="https://traefik.io/">Traefik&lt;/a> and &lt;a href="https://static-web-server.net/features/docker/#run-a-container">Static Web Server&lt;/a>&lt;/h1>
&lt;p>Traefik is amazing to host complex services like with containers. On the other hand it&amp;rsquo;s harder than you&amp;rsquo;d think to host a simple static html site. I wanted to share my current approach that is based on &lt;a href="https://static-web-server.net/features/docker/#run-a-container">Static Web Server Project&lt;/a>.&lt;/p>
&lt;h2 id="static-web-server-sws">Static Web Server (SWS)&lt;/h2>
&lt;p>Static Web Server (or SWS abbreviated) is a simple and really fast web server with the goal to serve static web files or assets. The tiny docker image is only 4 MB with a small memory footprint. We can therefore afford to run a container for each static site.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>On the server we set up all static sites in one folder called &lt;code>static-sites&lt;/code>. As we run the SWS with &lt;a href="https://docs.docker.com/compose/">docker-compose&lt;/a> we add the &lt;code>docker-compose.yml&lt;/code> to this folder too. The following is an example setup with two static sites on seperate domains.&lt;/p>
&lt;pre tabindex="0">&lt;code>| static-sites
|
| docker-compose.yml
| - domain1.example.org
| - index.html
| - domain2.example.org
| - index.html
| - fonts
| - Open-Dyslexic.odf
&lt;/code>&lt;/pre>&lt;p>For each domain we add a folder. I like to name them by the domain name but this is not necessary (just remember the volume in the &lt;code>docker-compose.yml&lt;/code> too).&lt;/p>
&lt;p>With this done we can now fill the appropriate information in the &lt;code>docker-compose.yml&lt;/code>. Copy the following and replace &lt;code>domain1.example.org&lt;/code>, &lt;code>domain2.example.org&lt;/code>, &lt;code>site-one&lt;/code> and &lt;code>site-two&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3.3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">domain1.example.org&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">joseluisq/static-web-server:2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;domain1.example.org&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Note: those envs are customizable but also optional&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SERVER_PORT=8080&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SERVER_ROOT=/public&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SERVER_LOG_LEVEL=info&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./domain1.example.org:/public&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">labels&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.docker.network=traefik&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-one.rule=Host(`domain1.example.org`)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-one.service=site-one&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-one.entrypoints=web-secure&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-one.tls=true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-one.tls.certResolver=default&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.services.site-one.loadbalancer.server.port=8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">networks&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">traefik&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">domain2.example.org&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">joseluisq/static-web-server:2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;domain2.example.org&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Note: those envs are customizable but also optional&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SERVER_PORT=8080&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SERVER_ROOT=/public&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SERVER_LOG_LEVEL=info&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./domain2.example.org:/public&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">labels&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.docker.network=traefik&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-two.rule=Host(`domain2.example.org`)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-two.service=site-two&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-two.entrypoints=web-secure&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-two.tls=true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.routers.site-two.tls.certResolver=default&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;traefik.http.services.site-two.loadbalancer.server.port=8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">networks&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">traefik&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">networks&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">traefik&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">traefik&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">external&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This assumes traefik runs in a docker-network called traefik. This network must already exist.&lt;/p>
&lt;p>As a last step add at least a &lt;code>index.html&lt;/code> in the appropriate folder. Then you can start the webserver with &lt;code>docker-compose up&lt;/code>. Add &lt;code>-d&lt;/code> to run it in the background.&lt;/p>
&lt;h1 id="deploying-static-sites">Deploying static sites&lt;/h1>
&lt;p>Deploying files manually (via Filezilla, scp or rsync) is not something I like to do. I therefore normally set up a CI job to automatically deploy the site when I push a new commit to GitHub, either via GitHub Actions or my &lt;a href="https://woodpecker-ci.org/docs/usage/intro">Woodpecker CI&lt;/a> instance.&lt;/p>
&lt;p>I order to do that I&lt;/p>
&lt;ul>
&lt;li>create a new user on the server, specifically for that purpose (one per site). The command is &lt;code>useradd USERNAME -m&lt;/code>&lt;/li>
&lt;li>create a SSH key without a password &lt;code>ssh-keygen -t ed25519 -a 100 -C &amp;quot;COMMENT&amp;quot; -f FILENAME&lt;/code>&lt;/li>
&lt;li>copy the public key that was just created at &lt;code>FILENAME.pub&lt;/code> on the server in the textfile &lt;code>/home/USERNAME/.ssh/authorized_keys&lt;/code>&lt;/li>
&lt;li>Add the private key to the secrets of your CI&lt;/li>
&lt;/ul>
&lt;p>A typical CI configuration will look like this with a static site and GitHub Actions&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Deploy Production Website via SSH&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">push&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">branches&lt;/span>: [&lt;span style="color:#ae81ff">main]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">jobs&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">runs-on&lt;/span>: &lt;span style="color:#ae81ff">ubuntu-latest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">steps&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">uses&lt;/span>: &lt;span style="color:#ae81ff">actions/checkout@v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Deploy to Server&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">uses&lt;/span>: &lt;span style="color:#ae81ff">easingthemes/ssh-deploy@main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">env&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">SSH_PRIVATE_KEY&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.SSH_PRIVATE_KEY }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ARGS&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;-rltgoDzvO --delete&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">SOURCE&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">REMOTE_HOST&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.REMOTE_HOST }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">REMOTE_USER&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.REMOTE_USER }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">TARGET&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.REMOTE_PRODUCTION_TARGET }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">EXCLUDE&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;.github/, .gitignore&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or this, when using &lt;a href="https://gohugo.io/">HUGO&lt;/a> and Woodpecker CI&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">pipeline&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">klakegg/hugo &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">commands&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">hugo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">deploy&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">appleboy/drone-scp&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">settings&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">strip_components&lt;/span>: &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">host&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">example.org&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">username&lt;/span>: &lt;span style="color:#ae81ff">moanos&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">target&lt;/span>: &lt;span style="color:#ae81ff">/home/USERNAME/static-sites/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">source&lt;/span>: &lt;span style="color:#ae81ff">public/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">key&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">from_secret&lt;/span>: &lt;span style="color:#ae81ff">ssh_key&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>not manually put the files on the server&lt;/p></content></entry><entry><title>Moitoring IoT devices with MASH - Part 1</title><link href="https://hyteck.de/post/monitoring-iot-devices-with-mash/" type="application/octet-stream"/><updated>2023-07-07T15:10:10+02:00</updated><id>https://hyteck.de/post/monitoring-iot-devices-with-mash/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>This article is about monitoring some IoT devices (e.g. a CO2 sensor) with a combination of &lt;a href="https://mosquitto.org/">Mosquitto&lt;/a> (a MQTT broker), &lt;a href="https://www.influxdata.com/time-series-platform/telegraf/">Telegraf&lt;/a> (a metric collector), &lt;a href="https://www.influxdata.com/">InfluxDB&lt;/a> (a time-series database) and &lt;a href="grafana.com/">Grafana&lt;/a> (for displaying everything nicely).
All mentioned services should run on a server that can be reached from the monitoring device(s) and your PC where you want to check the data. We will use MASH (see below) to deploy the services.&lt;/p>
&lt;p>In the end this will enable you to get to something like this:&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/monitoring-iot-devices-with-MASH/grafana_screenshot1.png" alt="A screenshot of a grafana dashboard. It shows one panel with temperature and humindity values over an hour, one panel showing CO2 data and one showing energy consumption">&lt;/p>
&lt;p>While writing, I decided to split this into two parts. Part 1 will set up our server infrastructure, Part 2 will show how to integrate some real sensors.&lt;/p>
&lt;p>Let&amp;rsquo;s get started with Part 1 and answer:&lt;/p>
&lt;h1 id="what-is-mash">What is MASH?&lt;/h1>
&lt;p>MASH is short for &amp;ldquo;Mother of all self-hosting&amp;rdquo;. It&amp;rsquo;s a collection of &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/ansible.md">ansible&lt;/a> roles, tied together by a playbook that can help deploy and maintain a large number of services. It was inspired by the &lt;a href="https://github.com/spantaleev/matrix-docker-ansible-deploy/">matrix-docker-ansible-deploy&lt;/a> follows the same philosophy and is maintained partly by the same people.&lt;/p>
&lt;p>MASH includes services like&lt;/p>
&lt;ul>
&lt;li>Nextcloud&lt;/li>
&lt;li>Authentik&lt;/li>
&lt;li>Gitea&lt;/li>
&lt;li>Peertube&lt;/li>
&lt;li>Funkwhale&lt;/li>
&lt;/ul>
&lt;p>and more than 50 others at time of writing this blogpost (&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/supported-services.md">List of all supported services&lt;/a>).&lt;/p>
&lt;p>The motivation for this playbook is to have all services run in docker, be as configurable as possible, while maintaining easy upgrades and reproducible deployments.
The documentation IMHO is very good, so if people are willing to use ansible this might be a lot easier than writing roles themself.&lt;/p>
&lt;h1 id="architecture">Architecture&lt;/h1>
&lt;p>In the end we will have a IoT device that sends data to a MQTT topic on the MQTT broker Mosquitto. Telegraf will listen to this topic and write this into InfluxDB. When you want to have a look at the data, you can access Grafan which will query InfluxDB.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/monitoring-iot-devices-with-MASH/mash-monitoring-mqtt.png" alt="A schema showing a setup where an IoT sensor sends data via MQTT to an MQTT broker. Telegraf subscribes to this broker and writes the data into InfluxDB. Grafana reads from InfluxDB when you want to view the data">&lt;/p>
&lt;h1 id="prerequisits">Prerequisits&lt;/h1>
&lt;p>To have everything it needs to use MASH you should ensure the following things are done and working&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/prerequisites.md">Prerequisits&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/getting-the-playbook.md">Getting the playbook&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Configure DNS&lt;/strong>&lt;/p>
&lt;p>By following this guide we will set up some services which will be reachable from via web interface (Grafana and InfluxDB) while others will not (Telegraf, Mosquitto). We will set a domain name for the server itself, one for Grafana and one for InfluxDB. This allows you to move services more easily. Feel free to adjust the setup to your needs (but don&amp;rsquo;t forget to change tha &lt;code>_hostname&lt;/code> variables accordingly later).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Domain&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Target&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Server general&lt;/td>
&lt;td>s0.example.com&lt;/td>
&lt;td>A&lt;/td>
&lt;td>&lt;code>&amp;lt;IPv4-IP&amp;gt;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Server general&lt;/td>
&lt;td>s0.example.com&lt;/td>
&lt;td>AAAA&lt;/td>
&lt;td>&lt;code>&amp;lt;IPv6-IP&amp;gt;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Grafana&lt;/td>
&lt;td>grafana.example.com&lt;/td>
&lt;td>CNAME&lt;/td>
&lt;td>s0.example.com&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>InfluxDB&lt;/td>
&lt;td>influxdb.example.com&lt;/td>
&lt;td>CNAME&lt;/td>
&lt;td>s0.example.com&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="set-up-the-services">Set up the services&lt;/h1>
&lt;p>Setting up the services will require three steps. The first will set up the general server with Mosquitto and Influxdb. Then we configure mosquitto and influxdb and use this to set up Telegraf and Grafana.&lt;/p>
&lt;h2 id="setting-up-the-basis-mosquitto-and-influxdb">Setting up the basis, Mosquitto and InfluxDB&lt;/h2>
&lt;p>Setting up will be based on one configuration file of the playbook. &lt;code>&amp;lt;your-domain&amp;gt;&lt;/code> is the one you set up as Server General (&lt;code>s0.example.com&lt;/code>).&lt;/p>
&lt;p>Execute the following these steps inside the playbook directory:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>create a directory to hold your configuration (&lt;code>mkdir -p inventory/host_vars/&amp;lt;your-domain&amp;gt;&lt;/code>)&lt;/p>
&lt;/li>
&lt;li>
&lt;p>copy the sample configuration file (&lt;code>cp examples/vars.yml inventory/host_vars/&amp;lt;your-domain&amp;gt;/vars.yml&lt;/code>)&lt;/p>
&lt;/li>
&lt;li>
&lt;p>copy the sample inventory hosts file (&lt;code>cp examples/hosts inventory/hosts&lt;/code>)&lt;/p>
&lt;/li>
&lt;li>
&lt;p>edit the inventory hosts file (&lt;code>inventory/hosts&lt;/code>) to your liking&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Now you are ready to modify the main configuration file at&lt;code>inventory/host_vars/&amp;lt;your-domain&amp;gt;/vars.yml&lt;/code>.&lt;/p>
&lt;p>Add the following to your playbook and replace the default values (&lt;code>IN_CAPS&lt;/code>)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## Basics ##&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Put a strong secret below, generated with `pwgen -s 64 1` or in another way&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Various other secrets will be derived from this secret automatically.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mash_playbook_generic_secret_key&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mash_playbook_docker_installation_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">devture_docker_sdk_for_python_installation_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># To ensure the server&amp;#39;s clock is synchronized (using systemd-timesyncd/ntpd),&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># we enable the timesync service.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">devture_timesync_installation_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## traefik ##&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Traefik will be our revers proxy that makes grafana and influxdb accessible from the outside. It will automatically obtain SSL certificates for us&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mash_playbook_reverse_proxy_type&lt;/span>: &lt;span style="color:#ae81ff">playbook-managed-traefik&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># The E-Mail address that traefik will use to obtain certificates with&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">devture_traefik_config_certificatesResolvers_acme_email&lt;/span>: &lt;span style="color:#ae81ff">certs@example.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">##############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## influxdb ##&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">##############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_hostname&lt;/span>: &lt;span style="color:#ae81ff">influxdb.example.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_init&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_init_username&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;USERNAME&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_init_password&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;SECURE-PASSWORD&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_init_org&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;YOUR-ORG&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">influxdb_init_bucket&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;monitoring&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#################&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## Mosquitto ##&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#################&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">mosquitto_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Installing&lt;/strong>&lt;/p>
&lt;p>Before installing and each time you update the playbook in the future, you will need to update the Ansible roles in this playbook by running &lt;code>just roles&lt;/code>. &lt;code>just roles&lt;/code> is a shortcut to download the latest Ansible roles. If you don&amp;rsquo;t have &lt;code>just&lt;/code>, you can also manually run the &lt;code>roles&lt;/code> commands seen in the &lt;code>justfile&lt;/code> (this gets tedious fast).&lt;/p>
&lt;p>To install you should can &lt;code>just install-all&lt;/code>. That&amp;rsquo;s it.&lt;/p>
&lt;p>Congratulations! You installed your first services and can now visit influxdb.example.com and login with the credentials you set above!&lt;/p>
&lt;h2 id="configuring-mosquitto-and-influxdb">Configuring Mosquitto and InfluxDB&lt;/h2>
&lt;p>&lt;strong>Mosquitto&lt;/strong>&lt;/p>
&lt;p>To configure mosquitto you only need to set up users. I would recommend to set up seperate users for telegraf and your IoT devices as you might want to restrict the IoT users permissions (not covered here).&lt;/p>
&lt;p>Setting up mosquitto users can be done &lt;code>just run-tags mosquitto-add-user --extra-vars=username=&amp;lt;username&amp;gt; --extra-vars=password=&amp;lt;password&amp;gt;&lt;/code>. For the setting to take effect, you must restart the container. To do that you can use &lt;code>just start-group mosquitto&lt;/code>.&lt;/p>
&lt;p>&lt;strong>InfluxDB&lt;/strong>&lt;/p>
&lt;p>Log in on influxdb.example.com with the credentials you configured in your &lt;code>vars.yml&lt;/code>.&lt;/p>
&lt;p>We will now create a configuration that will automatically be read by telegraf.&lt;/p>
&lt;p>Got to &lt;code>Load Data -&amp;gt; Telegraf&lt;/code> and press &lt;code>Create configuration&lt;/code>. Choose the bucket monitoring and search for the MQTT consumer as data source.&lt;/p>
&lt;p>Add the following replace the &lt;code>[[inputs.mqtt_consumer]]&lt;/code> with the following configuration (and make sure that you replace the example values).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-toml" data-lang="toml">&lt;span style="display:flex;">&lt;span>[[&lt;span style="color:#a6e22e">inputs&lt;/span>.&lt;span style="color:#a6e22e">mqtt_consumer&lt;/span>]]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">servers&lt;/span> = [&lt;span style="color:#e6db74">&amp;#34;tcp://s0.example.com:1883&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">## Topics that will be subscribed to.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">topics&lt;/span> = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;sensors/#&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">data_format&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;value&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">data_type&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;float&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">username&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;USERNAME_SET_WHEN_CONFIGURING_MOSQUITTO&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">password&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;PASSWORD_SET_WHEN_CONFIGURING_MOSQUITTO&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now got to &lt;code>Setup Instructions&lt;/code> and copy the &lt;code>INFLUX_TOKEN&lt;/code>. It will only show once! Also copy the config URL. You will need both in the following step.&lt;/p>
&lt;h2 id="setting-up-telegraf-and-grafana">Setting up Telegraf and Grafana&lt;/h2>
&lt;p>You now have everything for the last step: Setting up Telegraf and grafana. We did not do this in the first step as wee needed the access tokes/configuration link which we have now. Therefore you should now add two new sections to your &lt;code>vars.yml&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">##############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## telegraf ##&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">##############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">telegraf_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">telegraf_influx_token&lt;/span>: &lt;span style="color:#ae81ff">TOKEN-YOU-GOT-FROM-INFLUX&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">telegraf_config_link&lt;/span>: &lt;span style="color:#ae81ff">https://influxdb.example.com/api/v2/telegrafs/0b6d11ca6bb1b000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## grafana ##&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#############&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">grafana_enabled&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">grafana_hostname&lt;/span>: &lt;span style="color:#ae81ff">grafana.example.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">grafana_default_admin_user&lt;/span>: &lt;span style="color:#ae81ff">USERNAME&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">grafana_default_admin_password&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;SECURE-PASSWORD&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">grafana_provisioning_datasources&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">InfluxDBs3&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">type&lt;/span>: &lt;span style="color:#ae81ff">influxdb&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">access&lt;/span>: &lt;span style="color:#ae81ff">proxy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">url&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;https://{{ influxdb_hostname }}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">jsonData&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#ae81ff">Flux&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">organization&lt;/span>: &lt;span style="color:#ae81ff">YOUR-ORG&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">defaultBucket&lt;/span>: &lt;span style="color:#ae81ff">monitoring&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">secureJsonData&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">token&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;TOKEN-YOU-GOT-FROM-INFLUXDB&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After that, once again do &lt;code>just install-all&lt;/code>. You should now have a working setup of all services. Now&lt;/p>
&lt;h1 id="lets-put-some-data-in-and-display-it">Let&amp;rsquo;s put some data in and display it&lt;/h1>
&lt;p>To send some data that you can display you can use the following python script. Save it as &lt;code>cli.py&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> argparse
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> paho.mqtt.client &lt;span style="color:#66d9ef">as&lt;/span> mqtt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> random
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> numpy &lt;span style="color:#66d9ef">as&lt;/span> np
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> time
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">gen_time&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> time&lt;span style="color:#f92672">.&lt;/span>strftime(&lt;span style="color:#e6db74">&amp;#34;%H:%M:%S&amp;#34;&lt;/span>, time&lt;span style="color:#f92672">.&lt;/span>localtime())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">gen_temp&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> amplitude: amplitude of temperature changes
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> mean: mean of the simulated temperature
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> offset: offset of the signal in seconds
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> period: periond time in seconds
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> amplitude &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mean &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> offset &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> period &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">900&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> temp &lt;span style="color:#f92672">=&lt;/span> np&lt;span style="color:#f92672">.&lt;/span>sin((time&lt;span style="color:#f92672">.&lt;/span>time() &lt;span style="color:#f92672">+&lt;/span> offset) &lt;span style="color:#f92672">%&lt;/span> (period) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> np&lt;span style="color:#f92672">.&lt;/span>pi) &lt;span style="color:#f92672">*&lt;/span> amplitude &lt;span style="color:#f92672">+&lt;/span> mean
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> temp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">fake&lt;/span>(client, data_type&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;temperature&amp;#34;&lt;/span>, topic&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sensors/temperature&amp;#34;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> data_type &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;temperature&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data_generator &lt;span style="color:#f92672">=&lt;/span> gen_temp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> data_type &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;time&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data_generator &lt;span style="color:#f92672">=&lt;/span> gen_time
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Publishing&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> data_type &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;temperature&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>publish(topic, gen_temp())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> data_type &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;time&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>publish(topic, gen_time())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> time&lt;span style="color:#f92672">.&lt;/span>sleep(&lt;span style="color:#ae81ff">2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_message&lt;/span>(client, userdata, msg):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(msg&lt;span style="color:#f92672">.&lt;/span>topic &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#e6db74">&amp;#34; &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> str(msg&lt;span style="color:#f92672">.&lt;/span>payload))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_connect&lt;/span>(client, userdata, flags, rc):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Connected with result code &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> str(rc))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> rc &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">ConnectionError&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;MQTT server refused connection&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">connect_broker&lt;/span>(server, port, username&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>, password&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_id &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;python-mqtt-&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>random&lt;span style="color:#f92672">.&lt;/span>randint(&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1000&lt;/span>)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client &lt;span style="color:#f92672">=&lt;/span> mqtt&lt;span style="color:#f92672">.&lt;/span>Client(client_id)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>on_connect &lt;span style="color:#f92672">=&lt;/span> on_connect
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>username_pw_set(username&lt;span style="color:#f92672">=&lt;/span>username,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> password&lt;span style="color:#f92672">=&lt;/span>password)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>connect(server, port, &lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> client
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">cli&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser &lt;span style="color:#f92672">=&lt;/span> argparse&lt;span style="color:#f92672">.&lt;/span>ArgumentParser(description&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#39;Do basic MQTT operations&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;action&amp;#39;&lt;/span>, choices&lt;span style="color:#f92672">=&lt;/span>[&lt;span style="color:#e6db74">&amp;#39;sub&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;pub&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;fake&amp;#39;&lt;/span>], help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;-t&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;--topic&amp;#39;&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;The MQTT topic&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;--type&amp;#39;&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;The type of data to fake&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;-b&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;--broker&amp;#39;&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Hostname of the MQTT broker&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;--port&amp;#39;&lt;/span>, type&lt;span style="color:#f92672">=&lt;/span>int, default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1883&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;The MQTT brokers port (default: 1883)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;-u&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;--user&amp;#39;&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;User for the MQTT broker&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;-p&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;--password&amp;#39;&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Password of the MQTT broker&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parser&lt;span style="color:#f92672">.&lt;/span>add_argument(&lt;span style="color:#e6db74">&amp;#39;-d&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;--payload&amp;#39;&lt;/span>, help&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;The payload to send with a set command&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> args &lt;span style="color:#f92672">=&lt;/span> parser&lt;span style="color:#f92672">.&lt;/span>parse_args()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client &lt;span style="color:#f92672">=&lt;/span> connect_broker(args&lt;span style="color:#f92672">.&lt;/span>broker, args&lt;span style="color:#f92672">.&lt;/span>port, args&lt;span style="color:#f92672">.&lt;/span>user, args&lt;span style="color:#f92672">.&lt;/span>password)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> args&lt;span style="color:#f92672">.&lt;/span>action &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;sub&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>subscribe(args&lt;span style="color:#f92672">.&lt;/span>topic)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>on_message &lt;span style="color:#f92672">=&lt;/span> on_message
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>loop_forever()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> args&lt;span style="color:#f92672">.&lt;/span>action &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;pub&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r &lt;span style="color:#f92672">=&lt;/span> client&lt;span style="color:#f92672">.&lt;/span>publish(args&lt;span style="color:#f92672">.&lt;/span>topic, payload&lt;span style="color:#f92672">=&lt;/span>args&lt;span style="color:#f92672">.&lt;/span>payload)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(r)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> args&lt;span style="color:#f92672">.&lt;/span>action &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;fake&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fake(client, args&lt;span style="color:#f92672">.&lt;/span>topic)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> __name__ &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;__main__&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cli()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Start the script with&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python cli.py fake -t sensors/temperature -b s0.example.com -u USERNAME -p SECURE_PASSWORD&lt;span style="color:#e6db74">`&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will fake a temperature sensor and publish the results to the MQTT topic sensors/temperature. To make sure the MQTT broker works correctly you can subscribe with&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>python cli.py sub -t sensors/temperature -b stats.hyteck.de -u qzt -p fisch-salz-hof
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now let&amp;rsquo;s display this data in Grafana. Add a new dashboard and then use &lt;code>Add-&amp;gt;Visualization&lt;/code>.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/monitoring-iot-devices-with-MASH/grafana_new_query.jpeg" alt="Screenshot of the grafana interface with numbers indicating where to click for the following sentence">
In the panel select InfluxDB as datasource (1+2) put in the folowing query (3).&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-flux" data-lang="flux">from(bucket: &amp;#34;monitoring&amp;#34;)
|&amp;gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
|&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_measurement&amp;#34;] == &amp;#34;mqtt_consumer&amp;#34;)
|&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;_field&amp;#34;] == &amp;#34;value&amp;#34;)
|&amp;gt; filter(fn: (r) =&amp;gt; r[&amp;#34;topic&amp;#34;] == &amp;#34;sensors/temperature&amp;#34;)
|&amp;gt; group(columns: [&amp;#34;topic&amp;#34;])
|&amp;gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|&amp;gt; yield(name: &amp;#34;mean&amp;#34;)
&lt;/code>&lt;/pre>&lt;p>add a nice title (4) and apply to see tha data (5). You should now have a nice display of your fake sensor!&lt;/p>
&lt;h1 id="some-last-words">Some last words&lt;/h1>
&lt;p>Please let me know when you follow this guide - wheter sucessful or not! I would love to improve it.&lt;/p>
&lt;p>I know this seems like a lot of effort to get a few numbers to display a few numbers nicely. On the other hands, once set up this setup can do a lot of stuff, from monitoring, to alerting. Upgrades should be taken care of by MASH, check out how to do that &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/maintenance-upgrading-services.md">here&lt;/a>. Also setting up more services is a breeze. Either check the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/supported-services.md">list of existing services&lt;/a>, or add a new role to the playbook. If you don&amp;rsquo;t know how, I am happy to help!&lt;/p>
&lt;p>So make sure to follow my &lt;a href="https://hyteck.de/index.xml">RSS feed&lt;/a> or any of my socials to get notified when Part 2 comes around.&lt;/p>
&lt;h1 id="troubleshooting">Troubleshooting&lt;/h1>
&lt;p>When something doesn&amp;rsquo;t work feel free to&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Consult the docs&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/mosquitto.md">Mosquitto&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/telegraf.md">Telegraf&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/influxdb.md">InfluxDB&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/grafana.md">Grafana&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;a href="https://hyteck.de/about/#contact">Message me&lt;/a>&lt;/strong>&lt;/p>
&lt;/li>
&lt;/ul></content></entry><entry><title>Garage distributed object storage via Ansible</title><link href="https://hyteck.de/post/garage/" type="application/octet-stream"/><updated>2023-02-27T10:12:54+02:00</updated><id>https://hyteck.de/post/garage/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I recently build a beginner-friendly ansible playbook for Garage, a S3 compatible distributed object storage.&lt;/p>
&lt;h1 id="what-is-garage-docker-ansible-deploy">What is garage-docker-ansible-deploy?&lt;/h1>
&lt;p>Garage is an open-source distributed object storage service tailored for self-hosting. The ansible playbook &lt;a href="https://github.com/moan0s/garage-docker-ansible-deploy">garage-docker-ansible-deploy&lt;/a> helps you to set up such a cluster.&lt;/p>
&lt;p>It comes with &amp;ldquo;batteries included&amp;rdquo; so it will automatically install docker and set up a reverse proxy (traefik).&lt;/p>
&lt;p>You may be familiar with some related ansible playbooks that this playbook is based on&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/spantaleev/matrix-docker-ansible-deploy">matrix-docker-ansible-deploy&lt;/a> - for deploying a fully-featured &lt;a href="https://matrix.org">Matrix&lt;/a> homeserver&lt;/li>
&lt;li>&lt;a href="https://github.com/spantaleev/gitea-docker-ansible-deploy">gitea-docker-ansible-deploy&lt;/a> - for deploying a &lt;a href="https://gitea.io">Gitea&lt;/a> (self-hosted &lt;a href="https://git-scm.com/">Git&lt;/a> service) server&lt;/li>
&lt;li>&lt;a href="https://github.com/spantaleev/vaultwarden-docker-ansible-deploy">vaultwarden-docker-ansible-deploy&lt;/a> - for deploying a &lt;a href="https://github.com/dani-garcia/vaultwarden">Vaultwarden&lt;/a> password manager server (unofficial &lt;a href="https://bitwarden.com/">Bitwarden&lt;/a> compatible server)&lt;/li>
&lt;/ul>
&lt;p>These playbooks are masterfully maintained by &lt;em>spantaleev&lt;/em> and community. I copied the design und re-use roles e.g. to install traefik.&lt;/p>
&lt;h1 id="opinionated-design">Opinionated Design&lt;/h1>
&lt;p>Garage is a very flexible software that can server a lot of use-cases. The playbook is opinionated in the sense that it
reduces the flexibility of garage in favor of an easy deployment that should serve common use cases.
The playbook currently encourages a layout where&lt;/p>
&lt;ul>
&lt;li>1 garage data node is used per physical drive that should be used by the cluster&lt;/li>
&lt;li>1 gateway node is used per host to make redundant setups possible&lt;/li>
&lt;/ul>
&lt;p>Each host is assumed to habe a public IPv4/IPv6 address and every node should have a dedicated subdomain + one subdomain per gateway on the host.&lt;/p>
&lt;p>When all of this comes together a garage host might look something like this&lt;/p>
&lt;figure>&lt;img src="https://hyteck.de/uploads/design_scheme.png"
alt="A garage node with one gatway node and 2 data nodes that expose the ports 3901, 3911 and 3912. A trafik server exposes port 443. Everything is contained within server1 that has IP 42.42.42.42" width="100%">&lt;figcaption>
&lt;p>Example layout with one host that has 2 nodes (as it has two drives where data will be stored)&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>The playbook will need you to configure the DNS records to point to server1 and will make everything else happen with the following configuration.&lt;/p>
&lt;pre tabindex="0">&lt;code>garage_garage_node1_base_path: &amp;#34;/media/drive1/garage/node1&amp;#34;
garage_garage_node2_base_path: &amp;#34;/media/drive2/garage/node2&amp;#34;
garage_garage_nodes:
- name: &amp;#34;gateway1&amp;#34;
metadata_path: &amp;#34;{{ garage_garage_meta_path }}/gw1&amp;#34;
data_path: &amp;#34;{{ garage_garage_data_path }}/gw1&amp;#34;
gateway: true
rpc_bind_port: 3901
node_addr: &amp;#34;garage-gw1.example.com&amp;#34;
s3_api_addr: &amp;#34;s3.example.com&amp;#34;
- name: &amp;#34;node1&amp;#34;
gateway: false
capacity: 3
metadata_path: &amp;#34;{{ garage_garage_node1_base_path }}/metadata&amp;#34;
data_path: &amp;#34;{{ garage_garage_node1_base_path }}/data&amp;#34;
rpc_bind_port: 3911
node_addr: &amp;#34;garage-node1.example.com&amp;#34;
- name: &amp;#34;node2&amp;#34;
gateway: false
capacity: 3
metadata_path: &amp;#34;{{ garage_garage_node2_base_path }}/metadata&amp;#34;
data_path: &amp;#34;{{ garage_garage_node2_base_path }}/data&amp;#34;
rpc_bind_port: 3921
node_addr: &amp;#34;garage-node2.example.com&amp;#34;
&lt;/code>&lt;/pre>&lt;h1 id="limitations">Limitations&lt;/h1>
&lt;p>While the playbook should of course be reusable and fairly modular it will never be a solution to all use cases. The playbook does not cover&lt;/p>
&lt;ul>
&lt;li>Setting up domains (but there are instructions)&lt;/li>
&lt;li>Detailed management of the buckets and keys: There are basic features to create buckets and access keys but management will not be in the scope of the playbook&lt;/li>
&lt;li>connecting nodes via (mesh) VPN as metioned in the &lt;a href="https://garagehq.deuxfleurs.fr/documentation/cookbook/real-world/">project documentation&lt;/a>&lt;/li>
&lt;/ul>
&lt;h1 id="getting-started">Getting started&lt;/h1>
&lt;ul>
&lt;li>Go to &lt;a href="https://github.com/moan0s/garage-docker-ansible-deploy">the garage-docker-ansible-deploy README&lt;/a> with detailed installation instructions&lt;/li>
&lt;li>For any problems you can find help in &lt;a href="https://matrix.to/#/#garage-docker-ansible-deploy:hyteck.de">#garage-docker-ansible-deploy:hyteck.de&lt;/a>&lt;/li>
&lt;li>If you think there is something wrong/missing open a GitHub issues: &lt;a href="https://github.com/moan0s/garage-docker-ansible-deploy/issues">moan0s/garage-docker-ansible-deploy/issues&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Be aware that the playbook is not yet used widely so I don&amp;rsquo;t have much more than my own experiences. I am happy to help if you experience bumps in the road&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "garage"
})
&lt;/script></content></entry><entry><title>Rules, Terms and Privacy for GoToSocial: gay-pirate-assassins.de</title><link href="https://hyteck.de/gay-pirate-assassins/" type="application/octet-stream"/><updated>2022-11-20T04:56:10+02:00</updated><id>https://hyteck.de/gay-pirate-assassins/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="rules">Rules&lt;/h1>
&lt;p>&lt;em>&amp;quot;Be excellent to each other&amp;quot;&lt;/em> is easier said than done, and means
different things to different people.&lt;/p>
&lt;p>The following rules are a (non-exhaustive) list of behaviours that may
lead to deletion of toots, silencing or suspension of accounts, at the
descretion of the instance administrators, as described in our
&lt;a href="#terms-of-service">Terms&lt;/a>&lt;/p>
&lt;p>Please report behaviour that bothers you. We will keep your report
confidential.&lt;/p>
&lt;ul>
&lt;li>We do not tolerate discriminatory behaviour and content promoting or
advocating the oppression of members of marginalised groups. These
groups may be characterised by any of the following (though this
list is naturally incomplete):
&lt;ul>
&lt;li>ethnicity&lt;/li>
&lt;li>gender identity or expression&lt;/li>
&lt;li>sexual identity or expression&lt;/li>
&lt;li>physical characteristics or age&lt;/li>
&lt;li>disability or illness&lt;/li>
&lt;li>nationality, residency, citizen status&lt;/li>
&lt;li>wealth or education&lt;/li>
&lt;li>religious affiliation, agnosticism or atheism&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>We do not tolerate threatening behaviour, stalking, and
&lt;a href="https://en.wikipedia.org/wiki/Doxxing">doxxing&lt;/a>&lt;/li>
&lt;li>We do not tolerate harassment, including brigading, dogpiling, or
any other form of contact with a user who has stated that they do
not wish to be contacted.&lt;/li>
&lt;li>We do not tolerate mobbing, including name-calling, intentional
misgendering or deadnaming.&lt;/li>
&lt;li>We do not tolerate violent nationalist propaganda, Nazi symbolism or
promoting the ideology of National Socialism.&lt;/li>
&lt;li>We do not tolerate conspiracy narratives or other reactionary myths
supporting or leading to the above-mentioned (and/or similar)
behavior.&lt;/li>
&lt;li>Actions intended to damage this instance or its performance may lead
to immediate account suspension.&lt;/li>
&lt;li>Content that is illegal in Germany will be deleted and may lead to
immediate account suspension.&lt;/li>
&lt;/ul>
&lt;h2 id="best-practices">Best practices&lt;/h2>
&lt;p>The list below is a collection of behaviour that we expect to see from
our users. If you see a user go against these best practices in a way
that bothers you, please file a report and we will talk to them. While
these best practices are designed to be guidelines for a good communal
instance, &lt;strong>repeated malicious unwillingness&lt;/strong> to follow the best
practices will be considered just like breaking a rule.&lt;/p>
&lt;ul>
&lt;li>In general, use the tools provided to foster a considerate and
accessible atmosphere. This includes the liberal use of content
warnings (especially on potentially disturbing or controversial
topics), and alt-text captioning of media files.&lt;/li>
&lt;li>When possible, provide credit for creative works in your posts that
are not your own.&lt;/li>
&lt;li>Uninvited comments about another user's personal choices, lifestyle
or family are strongly discouraged and may be considered harassment.
Inappropriate sexual attention, comments about appearance and
implication of physical contact will not be tolerated toward any
non-consenting user.&lt;/li>
&lt;li>If you post sexual content or gore, use content warnings.&lt;/li>
&lt;li>If you post advertisements, use a content warning. Advertisements
should not be excessive or automated.&lt;/li>
&lt;li>Bots may only interact with a user when they're invited by that
user to do so.&lt;/li>
&lt;li>Automated posts and high-frequency posts should be &lt;strong>unlisted&lt;/strong>
(rendering visible to everybody, but not appearing on the local
timeline) to keep the local timeline of our instance a place of
community dialogue and human interaction. This extends to bots, feed
posters, Twitter &amp;quot;retweets&amp;quot; and Twitter crossposts with broken
mentions (&amp;quot;&amp;hellip;@twitter.com&amp;quot;). Crossposter accounts that stop being
active participants in our community may be removed at the
discretion of the moderators.&lt;/li>
&lt;li>In discussions, please remain civil, do not insult the people
you're talking to. Note that irony, sarcasm, or similar modes of
language don't translate well to written language and tend to
escalate discussions or misunderstandings.&lt;/li>
&lt;/ul>
&lt;p>&lt;em>(These best practices were inspired by the Terms of bsd.network. Thank
you!)&lt;/em>&lt;/p>
&lt;h2 id="resources">Resources&lt;/h2>
&lt;p>I am maintaining this instance on my spare time, hardware and nerves.
Don't push either of those.&lt;/p>
&lt;h1 id="terms-of-service">Terms of Service&lt;/h1>
&lt;p>adapted from chaos.social which was in turn adepted by the bsd.network ToS&lt;/p>
&lt;p>It is our intention that you use this service for personal enjoyment and
respectful, friendly interaction. To that end, we hope to foster a
welcoming and inclusive environment.&lt;/p>
&lt;p>The server is privately owned and open to users voluntarily, not a
public space. Users wishing to join this community are expected to act
without malice and in good faith. Doing otherwise may lead to removal
from the service, independent of whether a user violates any rules
outlined below.&lt;/p>
&lt;p>The administrators and moderators of this instance are&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://chaos.social/@moanos">moanos&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>The server hosting the instance is located in Germany.&lt;/p>
&lt;p>The following statements apply regardless of privacy level and instance
of the users involved. In rare cases, public or private offline conduct
or conduct on a separate instance may constitute grounds for removal
from the service.&lt;/p>
&lt;hr>
&lt;h3 id="policies-and-rules">Policies and Rules&lt;/h3>
&lt;p>Our instance is subject to a set of rules governing user behaviour. The
rules are defined above.&lt;/p>
&lt;p>These rules are designed to maintain a friendly and open atmosphere, and
to prevent harassment and discrimination. As such, they are a set of
guidelines, but by necessity incomplete. Users violating the spirit of
these rules will be treated no differently than users violating a
specific rule.&lt;/p>
&lt;p>Please note that our rules contain a section on best practices, and
users who repeatedly and despite warnings disregard these best practices
may be seen to be in violation of our rules.&lt;/p>
&lt;p>The moderators may remove accounts who spam the instance, or are
suspected of camping just to reserve an account name. Violation of the
policies and rules may also lead to account removal at the discretion of
the moderators.&lt;/p>
&lt;h3 id="data-access">Data Access&lt;/h3>
&lt;p>Content on this instance must not be used for the purposes of machine
learning or other &amp;quot;research&amp;quot; purposes without the explicit consent of
the users involved.&lt;/p>
&lt;p>Content on this instance beyond this page must not be archived or
indexed wholesale by automated means by any user or service. Active
users may export their following lists and posts through the export
provided on their settings page, or the API.&lt;/p>
&lt;hr>
&lt;h1 id="privacy">Privacy Policy&lt;/h1>
&lt;h3 id="collect">Information collection&lt;/h3>
&lt;ul>
&lt;li>&lt;em>Mandatory account information&lt;/em>: Username (always public), e-mail
address, and password.&lt;/li>
&lt;li>&lt;em>Optional account information&lt;/em>: Display name, biography, profile
information fields, profile picture, and header image. Display name,
biography, profile picture and header image will always be public.&lt;/li>
&lt;li>&lt;em>Statuses and interactions&lt;/em>: We retain all your posts including
attachments, and other interactions (such as favourites, follows and
reblogs). In addition to the content and people involved, we also
store the timestamps for all of the listed data entries. If these
interactions impact another server (eg. following, boosting, or
messaging a user on a different server), this other server will
receive all required information. Public, unlisted, and pinned posts
are available publicly. Follower-only posts are available to your
followers, and direct messages are available to you and all people
mentioned in the message. Please note that since we cannot control
other servers, this means that we cannot guarantee the privacy
status of your messages as soon as they leave our server.&lt;/li>
&lt;li>&lt;em>Cookies&lt;/em>: We use cookies to keep you logged in and save your
preferences for future visits.&lt;/li>
&lt;li>&lt;em>Other metadata&lt;/em>: We do log and store your IP address.
We retain the name of your browser application to allow you
to review your currently logged in sessions for security reasons.&lt;/li>
&lt;/ul>
&lt;h3 id="use">Information usage&lt;/h3>
&lt;p>Any of the information we collect from you may be used in the following
ways:&lt;/p>
&lt;ul>
&lt;li>To provide the core functionality of GoToSocial. You can only interact
with other people's content and post your own content when you are
logged in. For example, you may follow other people to view their
combined posts in your own personalized home timeline.&lt;/li>
&lt;li>To aid moderation of the community &amp;ndash; when a status or account is
reported, we will look into the matter as part of our moderation
tasks.&lt;/li>
&lt;li>The email address you provide may be used to send you information,
notifications about other people interacting with your content or
sending you messages, and to respond to inquiries, and/or other
requests or questions.&lt;/li>
&lt;li>To aid debugging and reliable providing service.&lt;/li>
&lt;/ul>
&lt;h3 id="protect">Information protection&lt;/h3>
&lt;p>We implement a variety of security measures to maintain the safety of
your personal information when you enter, submit, or access your
personal information. Among other things, your browser session, as well
as the traffic between your applications and the API, are secured with
HTTPS, and your password is hashed using a strong one-way algorithm. You
may enable two-factor authentication to further secure access to your
account.&lt;/p>
&lt;h3 id="data-retention">Information deletion and retention&lt;/h3>
&lt;p>You can request and download an archive of your content, including your
posts, media attachments, profile picture, and header image.&lt;/p>
&lt;p>You may irreversibly delete your account at any time.&lt;/p>
&lt;p>If we judge you to be breaking our instance rules, we may irreversibly
delete your account at any time.&lt;/p>
&lt;h3 id="disclose">Information disclosure&lt;/h3>
&lt;p>Information is not disclosed unless you explicitly permit it. The only
exception is the provider of our server, who is a trusted and
unavoidable third party.&lt;/p>
&lt;p>Contacting or permitting contact from a user from a different instance
implies your consent that the required data is shared with the server in
question.&lt;/p>
&lt;p>Authorization of a third-party application grants information access
depending on the scope of permissions you approve. The application may
access your public profile information, your following list, your
followers, your lists, all your posts, and your favourites. Applications
can never access your e-mail address or password.&lt;/p>
&lt;hr>
&lt;h1 id="attribution-">Attribution &lt;a href="#attribution">¶&lt;/a>&lt;/h1>
&lt;p>This text was adapted from &lt;a href="https://chaos.social/about">https://chaos.social/about&lt;/a> and is free to be
adapted and remixed under the terms of the &lt;a href="https://creativecommons.org/licenses/by/4.0/">CC-BY (Attribution 4.0
International)&lt;/a>.&lt;/p></content></entry><entry><title>Raspberry Pi as Offsite Backup</title><link href="https://hyteck.de/post/raspi-backup/" type="application/octet-stream"/><updated>2022-10-23T10:12:54+02:00</updated><id>https://hyteck.de/post/raspi-backup/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="use-case">Use Case&lt;/h1>
&lt;p>You have one (or more) servers at a hosting provider and a raspberry pi at home. You want to have an offsite backup of the websites, apps and databases at home.&lt;/p>
&lt;h1 id="prerequesits">Prerequesits&lt;/h1>
&lt;p>You configure your raspberry pi to be reachable from &lt;em>the internet&lt;/em> using DynDNS. In the following we assume that it is reachable at offsite.example.com.&lt;/p>
&lt;h1 id="preparing-your-backup-raspberry-pi">Preparing your backup raspberry pi&lt;/h1>
&lt;p>We want to make sure that backups on the raspberry pi can come from multiple sources and one source can not delete another.&lt;/p>
&lt;h2 id="create-an-additional-user">Create an additional user&lt;/h2>
&lt;p>and change to that user afterwards. You can change service1 to the name of the service that this user should backup.&lt;/p>
&lt;pre tabindex="0">&lt;code>sudo useradd service1_backup
sudo su service1_backup
cd ~
&lt;/code>&lt;/pre>&lt;h2 id="create-an-ssh-key-for-the-user">Create an SSH key for the user&lt;/h2>
&lt;p>This SSH key will later be used by your server to push backups automatically. Therefore you should not set a passphrase for the key (just press enter until the key is generated)&lt;/p>
&lt;pre tabindex="0">&lt;code>$ ssh-keygen -t ed25519
&lt;/code>&lt;/pre>&lt;h2 id="create-your-backup-directory">Create your backup directory&lt;/h2>
&lt;pre tabindex="0">&lt;code>mkdir backup &amp;amp;&amp;amp; cd backup
&lt;/code>&lt;/pre>&lt;p>If you want to use an external drive you can mount it to this users home directory.&lt;/p>
&lt;h2 id="initialzie-the-borg-repository">Initialzie the borg repository&lt;/h2>
&lt;pre tabindex="0">&lt;code>borg init --encryption=repokey ./
&lt;/code>&lt;/pre>&lt;p>Make sure to set a strong passphrase and note it down somewhere safe. Without it you will not be able to access you backup!&lt;/p>
&lt;h2 id="make-sure-the-user-can-only-access-the-backup-directory">Make sure the user can only access the backup directory&lt;/h2>
&lt;p>Put the following in &lt;code>~/.ssh/authorized_keys&lt;/code> and make sure everything is in one line. The last values are simply your public key that can be found in &lt;code>~/.ssh/id_ed25519.pub&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code>command=&amp;#34;borg serve --restrict-to-repository /home/&amp;lt;user&amp;gt;/backup&amp;#34;,restrict &amp;lt;key type&amp;gt; &amp;lt;key&amp;gt; &amp;lt;key host&amp;gt;
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Done with the raspberry pi&lt;/strong>&lt;/p>
&lt;h1 id="configure-your-server">Configure your server&lt;/h1>
&lt;p>In this guide we will use &lt;a href="https://torsion.org/borgmatic/">borgmatic&lt;/a> to configure and automatically run the backup in the server.&lt;/p>
&lt;h2 id="install-borgmatic">Install borgmatic&lt;/h2>
&lt;pre tabindex="0">&lt;code>sudo pip3 install --user --upgrade borgmatic
&lt;/code>&lt;/pre>&lt;h2 id="configure-borgmatic">Configure borgmatic&lt;/h2>
&lt;p>The following is a small configuration example. Place it in &lt;code>/etc/borgmatic.d/servic1.yaml&lt;/code>. If you need more options check out the &lt;a href="https://torsion.org/borgmatic/docs/reference/configuration/">full configuration file reference&lt;/a>&lt;/p>
&lt;pre tabindex="0">&lt;code>location:
source_directories:
- /home/service1/static
repositories:
- ssh://service1_backup@offsite1.example.com/./backup
storage:
encryption_passphrase: &amp;#34;ThePassphraseouUsedOnYourRaspi&amp;#34;
ssh_command: ssh -i /etc/borgmatic.d/service1_backup_key
retention:
# Number of daily archives to keep.
keep_daily: 7
hooks:
# List of one or more shell commands or scripts to execute
# before creating a backup, run once per configuration file.
before_backup:
- echo &amp;#34;Starting a backup.&amp;#34;
# List of one or more shell commands or scripts to execute
# after creating a backup, run once per configuration file.
after_backup:
- echo &amp;#34;Finished a backup.&amp;#34;
after_everything:
- echo &amp;#34;Completed actions.&amp;#34;
postgresql_databases:
- name: service1
# mysql_databases:
# - name: users
&lt;/code>&lt;/pre>&lt;h2 id="place-the-private-ssh-key">Place the private SSH key&lt;/h2>
&lt;p>The server will need the private SSH key so connect to your raspberry pi&lt;/p>
&lt;p>On the raspberry pi use&lt;/p>
&lt;pre tabindex="0">&lt;code>cat ~/.ssh/id_ed25519
&lt;/code>&lt;/pre>&lt;p>to get the private key and place it on your server in the file &lt;code>/etc/borgmatic.d/service1_backup_key&lt;/code>.
As this is a private SSH key it must only be readable by the user. Ro change its permissions correctly use&lt;/p>
&lt;pre tabindex="0">&lt;code>chown 600 service1_backup_key
&lt;/code>&lt;/pre>&lt;h2 id="check-if-the-backup-works">Check if the backup works&lt;/h2>
&lt;p>Create your backup with&lt;/p>
&lt;pre tabindex="0">&lt;code>sudo borgmatic create --verbosity 1 --list --stats
&lt;/code>&lt;/pre>&lt;p>Now check out the &lt;a href="https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot">borgmatic configuration&lt;/a> on how to properly set up automated backups&lt;/p>
&lt;h1 id="done">Done&lt;/h1>
&lt;p>Congrats, you should now have a fully functioning backup configuration!&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "raspi-backup"
})
&lt;/script></content></entry><entry><title>Vim shortcuts to execute current line</title><link href="https://hyteck.de/post/easy_execute_in_vim/" type="application/octet-stream"/><updated>2022-05-25T15:15:55+02:00</updated><id>https://hyteck.de/post/easy_execute_in_vim/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I recently had to write a lot of SQL code and thought it would be very neat to have some vim shortcuts to execute the current line or the current command.
I want to share this with everyone as the second command needed some try-and-error on my part.&lt;/p>
&lt;p>Adding the following to &lt;code>~/.vimrc&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code>map &amp;lt;F2&amp;gt; :.w !psql&amp;lt;CR&amp;gt;
map &amp;lt;F3&amp;gt; :.,/;/w !psql&amp;lt;CR&amp;gt;
map &amp;lt;F4&amp;gt; :w !psql&amp;lt;CR&amp;gt;
&lt;/code>&lt;/pre>&lt;p>will enable you to execute the current line in psql with &lt;code>F2&lt;/code>. &lt;code>F3&lt;/code> executes the current line and the next lines until a&lt;code>;&lt;/code>.
&lt;code>F4&lt;/code> executes the whole file.&lt;/p>
&lt;p>This can easily adapted to your needs. If you have any questions or improvemnts feel free to use the chat below.&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "vim-shortcuts"
})
&lt;/script></content></entry><entry><title>Disappearing messages with matrix</title><link href="https://hyteck.de/post/matrix-forget/" type="application/octet-stream"/><updated>2021-12-31T20:00:00+02:00</updated><id>https://hyteck.de/post/matrix-forget/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>I am a HUGE fan of matrix. It allows me to organize my chats in a sensible way, it works with multiple identities and completly anonymous if I want it to. &lt;a href="https://element.io/blog/spaces-blast-out-of-beta/">Spaces&lt;/a> made Matrix my favourite messenger by far. Yet, there is one feature I have been missing: Disappearing messages!&lt;/p>
&lt;p>Regarding the security and usability, only Signal is comparable to matrix. But: Signal offers the possibilty to define disappearing messages for groups and direct messages ranging from 30 seconds to 4 weeks. No Matrix client (to my knowledge) offers this functionality. Nevertheless, it is possible to configure matrix rooms to have the same feature. This needs a special server configuration and the sending of a special event in the room. This post tries to show both steps. If you do not administer a server you can probably skip to &lt;a href="#room-configuration">Room configuration&lt;/a>&lt;/p>
&lt;p>Be aware that this blogpost was written at the end of 2021 - Matrix develops fast and this could be subejct to changes.&lt;/p>
&lt;h1 id="instance-configuration">Instance configuration&lt;/h1>
&lt;p>To make disappearing messages possible you need to enable retention on your matrix instance.
&lt;a href="https://github.com/matrix-org/matrix-doc/blob/matthew/msc1763/proposals/1763-configurable-retention-periods.md">Retention&lt;/a> allows server and room admins to configure how long messages should be kept in the instances database before being purged from it. It is not part of the matrix specification, yet it is supported by synapse.&lt;/p>
&lt;p>A client SHOULD not display these messages anymore after the max_lifetime is exceeded. This was NOT true for element web an desktop while staying logged in. Nevertheless, a newly logged in client did not have access to the messages.&lt;/p>
&lt;p>To configure Synapse to make use of retention you will need to enable it in your &lt;code>homeserver.yaml&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code>retention:
enabled: true # enables the retention, is enough to enforce it once per day
purge_jobs: # configures a job that delete the events from the database after some tome
- longest_max_lifetime: 3d
interval: 1h
- shortest_max_lifetime: 3d
interval: 1d
&lt;/code>&lt;/pre>&lt;p>The example configuration creates two jobs that delete messages from the database. One only focuses on events that should be deleted after three days or less. These events will be deleted every hour. It is therefore possible for a message that was send in a room with a &lt;code>max_lifetime=7200000&lt;/code> (equals 2h) to be deleted one hour after the maximum lifetime.&lt;/p>
&lt;h2 id="ansible">Ansible&lt;/h2>
&lt;p>If you use the &lt;a href="https://github.com/spantaleev/matrix-docker-ansible-deploy">Ansible/Docker setup&lt;/a> to deploy your server you can add the following to &lt;code>inventory/host_vars/matrix.example.com/vars.yml&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code>matrix_synapse_configuration_extension_yaml: |
retention:
enabled: true
purge_jobs:
- longest_max_lifetime: 1d
interval: 2h
- shortest_max_lifetime: 1d
interval: 1d
&lt;/code>&lt;/pre>&lt;h1 id="room-configuration">Room configuration&lt;/h1>
&lt;p>If you are a user on a server that has retention enabled, you can enable disappearing messages yourself for each room. Sadly, this is still experimental - but managable! You have to craft a &lt;code>m.room.retention&lt;/code> event that defines the maximum lifetime of a message. You will need to access the rooms settings in order to do this.&lt;/p>
&lt;p>First you need to open the developer tools in the rooms settings.
&lt;img src="https://hyteck.de/uploads/matrix-retention/retention_dev.png" alt="Screenshot of the element room settings">&lt;/p>
&lt;p>Then click &amp;ldquo;Send custom event&amp;rdquo; to create your event
&lt;img src="https://hyteck.de/uploads/matrix-retention/retention_event_button.png" alt="Screenshot of element marking the button &amp;ldquo;Send custom event&amp;rdquo; in the developer tools">&lt;/p>
&lt;p>And fill the event with the appropriate &lt;code>max_liftime&lt;/code>. The time is an integer in milliseconds. X hours is therefore a value of &lt;code>X*3 600 000&lt;/code>. Make sure to click the red event button. The &lt;code>State Key&lt;/code> can be left empty nevertheless.
&lt;img src="https://hyteck.de/uploads/matrix-retention/retention_event.png" alt="Screenshot of the creation of creating a custom event. The field event type is filled with m.room.retention the event content is &amp;ldquo;max_lifetime&amp;rdquo;: 3600000 and the button event was clicked">&lt;/p>
&lt;p>Depending on your choosen lifetime the client should not show the messages anymore.&lt;/p>
&lt;h1 id="limitations">Limitations&lt;/h1>
&lt;p>The process of deleting messages can not be enforced. A malicious server or chat partner could ignore the request to delete the messages or they could have saved them elsewere. You should not rely on a deletion actually happening. Nevertheless I think this is a good step to take to improve your security in real life.&lt;/p>
&lt;h1 id="further-information">Further Information&lt;/h1>
&lt;ul>
&lt;li>Relevant part of the Synapse configuration file: &lt;a href="https://github.com/matrix-org/synapse/blob/v1.36.0/docs/sample_config.yaml#L451-L518">https://github.com/matrix-org/synapse/blob/v1.36.0/docs/sample_config.yaml#L451-L518&lt;/a>&lt;/li>
&lt;li>Synapse documenation on message retention policies &lt;a href="https://matrix-org.github.io/synapse/v1.41/message_retention_policies.html">https://matrix-org.github.io/synapse/v1.41/message_retention_policies.html&lt;/a>&lt;/li>
&lt;/ul>
&lt;h1 id="what-else">What else?&lt;/h1>
&lt;p>Thanks to &lt;a href="https://tastytea.de/">Tastytea&lt;/a> for helping me get this to work!&lt;/p>
&lt;h1 id="comments">Comments&lt;/h1>
&lt;p>If you have questions, corrections or want to leave something else, please feel free to use the comments!&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "matrix-disappearing-messages"
})
&lt;/script></content></entry><entry><title>Cactus comments via Matrix</title><link href="https://hyteck.de/post/cactus-chat/" type="application/octet-stream"/><updated>2021-08-25T11:08:55+02:00</updated><id>https://hyteck.de/post/cactus-chat/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="integration">Integration&lt;/h1>
&lt;p>Following the &lt;a href="https://cactus.chat/docs/getting-started/quick-start/">quickstart documentation&lt;/a> I tried to add cactus comments to this blog. I currently rely on infrastructure by cactus.chat as I do not host a private synapse server.&lt;/p>
&lt;p>I currently implemented this as a shortcode with hard-coded site title and a variable room name.&lt;/p>
&lt;h1 id="quickstart-with-hugo">Quickstart with HUGO&lt;/h1>
&lt;h2 id="register-your-site">Register your site&lt;/h2>
&lt;p>There is a registration system, that ensures that you are moderater in your comment section(s). I order to register your site you have to send a message to @cactusbot:cactus.chat . First try &lt;code>help&lt;/code> to ensure that the bot answers you, then register your site e.g. &lt;code>register hyteck&lt;/code>. The bot should inform you of success and add you to a moderation room.&lt;/p>
&lt;h2 id="embedd-comment-section-via-shortcode">Embedd comment section via Shortcode&lt;/h2>
&lt;p>HUGO, the static site generator I use, has an option to use &lt;em>shortcodes&lt;/em> that provide a nice interface to hide some HTML, CSS and JavaScript.&lt;/p>
&lt;p>The shortcode &lt;code>chat.html&lt;/code> must be added to &lt;code>layouts/shortcodes/&lt;/code> and looks like this&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;lt;script type=&amp;#34;text/javascript&amp;#34; src=&amp;#34;https://latest.cactus.chat/cactus.js&amp;#34;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;https://latest.cactus.chat/style.css&amp;#34; type=&amp;#34;text/css&amp;#34;&amp;gt;
&amp;lt;div id=&amp;#34;comment-section&amp;#34;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
initComments({
node: document.getElementById(&amp;#34;comment-section&amp;#34;),
defaultHomeserverUrl: &amp;#34;https://matrix.cactus.chat:8448&amp;#34;,
serverName: &amp;#34;cactus.chat&amp;#34;,
siteName: &amp;#34;hyteck&amp;#34;,
commentSectionId: &amp;#34;{{ index .Params 0 }}&amp;#34;
})
&amp;lt;/script&amp;gt;
&lt;/code>&lt;/pre>&lt;p>If you want to use this, replace the site name with the one you registered in the previous step.&lt;/p>
&lt;p>You can then use it as simple as&lt;/p>
&lt;pre tabindex="0">&lt;code>{{&amp;lt; chat cactus-comments &amp;gt;}}
&lt;/code>&lt;/pre>&lt;p>where &lt;code>cactus-comments&lt;/code> is the name of the chatroom. You can decide if you want to create a new one for each post (change the name to somthing like the post title) or if you want to use only one (keep the same name).&lt;/p>
&lt;h1 id="organisation">Organisation&lt;/h1>
&lt;p>I try to make a comment with my main matrix account, so that I am automatically joined to the room (tbh. I don&amp;rsquo;t know if this is necessary). Then I organize the rooms by adding them to a private space.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/element_screenshot.PNG" alt="Element screenshot">&lt;/p>
&lt;h1 id="community">Community&lt;/h1>
&lt;p>CW: Homophobia, Slur&lt;/p>
&lt;p>The community in the official chat seemed helpful and nice. Nevertheless I want to mention, that the first time I tried the demo page a user (that does not seem to be associated with the develoipment or involved in the community) insulted me with a homophobic slur. The &amp;ldquo;reason&amp;rdquo; behind this were the prounouns I had in my name. I reported the comment and it was removed by a moderator. The moderator made it clear that this behaviour is agains the &lt;a href="https://cactus.chat/docs/community/coc/">Code of Conduct&lt;/a> and that the user is not active in the community. For me this is a very good indication of a functioning community, cheers!&lt;/p>
&lt;h1 id="comments">Comments&lt;/h1>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "cactus-comments"
})
&lt;/script></content></entry><entry><title>Owncast &amp; Streaming a talk</title><link href="https://hyteck.de/post/owncast/" type="application/octet-stream"/><updated>2021-05-20T16:08:55+02:00</updated><id>https://hyteck.de/post/owncast/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I recently installed an Owncast server and wanted to share my experience. Here it is:&lt;/p>
&lt;h1 id="what-is-owncast">What is owncast?&lt;/h1>
&lt;p>Owncast is a streaming server that you can selfhost, a &lt;em>Twicht in a box&lt;/em> as the developers call it.
You host owncast on your server (a small VM with good downlink is enough) and can stream your own own content like you would do on Twicht, YouTube etc&amp;hellip;&lt;/p>
&lt;p>It has a chat, a admin panel for customization and thats it! You don&amp;rsquo;t need more to e.g. stream while you are playing minecraft or want to share a talk.&lt;/p>
&lt;h1 id="getting-started">Getting started&lt;/h1>
&lt;p>Get the latest release on &lt;a href="https://github.com/owncast/owncast/releases">GitHub&lt;/a> by using&lt;/p>
&lt;pre tabindex="0">&lt;code>$ mkdir owncast
$ cd owncast
$ wget https://github.com/owncast/owncast/releases/download/v0.0.7/owncast-0.0.7-linux-64bit.zip
$ unzip owncast-0.0.7-linux-64bit.zip
$ rm owncast-0.0.7-linux-64bit.zip
&lt;/code>&lt;/pre>&lt;p>And move the webroot to your document root and make sure the permissions fit&lt;/p>
&lt;pre tabindex="0">&lt;code>$ cd ..
$ mv owncast /var/www/owncast
$ cd /var/www/
$ chown -R www-data:www-data owncast
&lt;/code>&lt;/pre>&lt;p>Now create a new NGINX site e.g. &lt;code>/etc/nginx/sites-enabled/owncast&lt;/code> with the following content&lt;/p>
&lt;pre tabindex="0">&lt;code>map $http_upgrade $connection_upgrade {
default upgrade;
&amp;#39;&amp;#39; close;
}
server {
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/stream.hyteck.de/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/stream.hyteck.de/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name stream.hyteck.de;
# Set header
add_header X-Clacks-Overhead &amp;#34;GNU Terry Pratchett&amp;#34;;
add_header Permissions-Policy interest-cohort=(); #Anti FLoC
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:8080;
}
}
server {
listen 80;
listen [::]:80;
server_name stream.hyteck.de;
return 301 https://$server_name$request_uri;
}
&lt;/code>&lt;/pre>&lt;p>Make sure to adjust the server name and SSL certificats (I will not go into detail on how to obtain them, but &lt;a href="https://hyteck.de/about/">feel free to ask me!&lt;/a>).&lt;/p>
&lt;h1 id="start-server">Start server&lt;/h1>
&lt;p>Now start owncast to test&lt;/p>
&lt;pre tabindex="0">&lt;code>$ cd /var/www/owncast
$ ./owncast/owncast
&lt;/code>&lt;/pre>&lt;p>and visit &lt;a href="https://yourdomain.org">https://yourdomain.org&lt;/a>! If everything works you should see your site now. By visiting &lt;a href="https://yourdomain.org/admin">https://yourdomain.org/admin&lt;/a> you can configure your server. The default credentials are &lt;code>admin&lt;/code> and your stream key which is &lt;code>abc123&lt;/code>. Change this immediately!&lt;/p>
&lt;p>Before you configure, let&amp;rsquo;s make sure this runs whenever your server starts. Goback in the terminal and cancel with &lt;code>Ctrl+C&lt;/code>.&lt;/p>
&lt;h1 id="run-as-system-service">Run as system service&lt;/h1>
&lt;p>You want to install owncast as a system service. Therfore create &lt;code>/etc/systemd/system/owncast.service&lt;/code> with the following content:&lt;/p>
&lt;pre tabindex="0">&lt;code>[Unit]
Description=Owncast
After=network.target
[Service]
Type=simple
Restart=always
RestartSec=1
WorkingDirectory=/var/www/owncast
ExecStart=/var/www/owncast/owncast
[Install]
WantedBy=multi-user.target
&lt;/code>&lt;/pre>&lt;p>Update the daemon with &lt;code>systemctl daemon-reload&lt;/code> enable &lt;code>systemctl enable owncast&lt;/code> and start with &lt;code>systemctl start owncast&lt;/code>. Make sure everything is correct with &lt;code>systemctl status owncast&lt;/code>.&lt;/p>
&lt;h1 id="configuration">Configuration&lt;/h1>
&lt;p>You can now change back to yourdoiman.org/admin and configure, title, logo and more.&lt;/p>
&lt;h2 id="directory">Directory&lt;/h2>
&lt;p>If you start a stream and have directory enabled, Owncast will publish your activity, e.g. in the owncast RocketChat, on Twitter (by mentioning you if you gave Owncast your Twitter Handle) and in the Fediverse. Turn this of for testing!&lt;/p>
&lt;h1 id="streaming">Streaming&lt;/h1>
&lt;p>You can now use &lt;a href="https://obsproject.com/">OBS&lt;/a> or similar software to start streaming. Got to settings and configure your server.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/stream_config.png" alt="Configuration in the OBS streaming tab. The service is set to Custom.. and the server to rtmp://stream.hyteck.de/live. The stream key is hidden">&lt;/p>
&lt;h1 id="concept-for-talks">Concept for talks&lt;/h1>
&lt;p>To livestream we have a few important things to take care of:&lt;/p>
&lt;ul>
&lt;li>The speaker&lt;/li>
&lt;li>The presentation&lt;/li>
&lt;li>The streaming software&lt;/li>
&lt;li>The streaming server&lt;/li>
&lt;li>The chat&lt;/li>
&lt;/ul>
&lt;p>I suggest the following setup. Moderation and Speaker are in a BigBlueButton conference. The meet 15 minutes in advance, enough time to upload a presentation. Also in this room is a user called stream. This user does only listen and has OBS configured to record audio and the (fullscreen) window of the BBB room.&lt;/p>
&lt;p>Before the talk starts, it is a good opportunity to play some videos via the stream, or simply put on a picture, saing the talk will start soon.&lt;/p>
&lt;p>When the speaker an moderation are ready they tell the streamer and the scene is switched to the BBB room. The recording is started (if needed).&lt;/p>
&lt;p>A weakness of the setup is the streaner. If they loose their network connection, the stream stops.&lt;/p>
&lt;p>There is a neat &lt;a href="https://github.com/aau-zid/BigBlueButton-liveStreaming">project for live streaming directly from a BBB room&lt;/a>, but it is quite complicated to set up and a crash of the BBB server would make stop the stream. Also the start of the stream is not as beautiful.&lt;/p>
&lt;h1 id="performance">Performance&lt;/h1>
&lt;p>It can be hard to estimate the needed server capacities for a livestream that is why I want to share my experiences. All of this was done on a Strato Linux V40 with 16 GB of RAM, 8 vCPUs and 500 MBit/s.&lt;/p>
&lt;p>On the following screenshots you see the configuration and the monitoring data of a talk with 40 participants. We pplayed high-quality videos from 17:43 to 18:05. At 18:05 we changed to the presentation. You see the decrease in CPU load, as the compression of the videos was easier with a steady picture. At 19:05 we invited the participents to join us in the BBB room, some left the stream.&lt;/p>
&lt;p>The network load scales linear with the participants that are watching. You can simply calculate the needed downlink capacity with&lt;/p>
&lt;pre tabindex="0">&lt;code>Downlink capacity = Num. participants x Outbound Video Stream Rate
&lt;/code>&lt;/pre>&lt;p>Here this was&lt;/p>
&lt;pre tabindex="0">&lt;code>40 participants x 1200 kbps = 48 Mbps
&lt;/code>&lt;/pre>&lt;p>which matches the observed network traffic quite well.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/monitoring_vortrag.png" alt="Chart of the CPU load and the network traffic basic. The CPU load was jumpy around 25% from 17:45 to 18:05, after that it was steady around 17.5%. The Network Traffic went up until 18:10 and stayed at 50 Mbps until 19:00" title="Grafana Screenshot">&lt;/p>
&lt;p>The CPU load is determined by the compression the server has to do. The more different stream formats the server has to put out, the higher the load becomes. On the given Screenshot you see that only one downlink format was advertised, so the inbound stream had to be compresse only once. Matching imbound and outbound stream formats help reducing the server load.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/vortrag_eg.png" alt="A screenshot of the Owncast admin panel. The outbound stream details are: 1200 kbps, 24 fps and the Input is H.264@2500kbps at 30 fps" title="Screenshot of the owncast admin panel">&lt;/p>
&lt;p>If your bottleneck is the bandwidth you can try to add a second low streamrate, maybe ome clients will take that one.&lt;/p>
&lt;h1 id="final-notes">Final notes&lt;/h1>
&lt;p>Owncast is a great project. Be aware that it is still not in version 1.0 so expect some limitations. This is e.g. the unability to block a user from chat. If you have any questions let me know in the comments below.&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "owncast"
})
&lt;/script></content></entry><entry><title>Transparency</title><link href="https://hyteck.de/transparency/" type="application/octet-stream"/><updated>2021-02-08T15:00:10+02:00</updated><id>https://hyteck.de/transparency/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="deutsch">Deutsch&lt;/h1>
&lt;p>Wir haben die bisherigen Kosten für Server-Hardware in der Tabelle unten angegeben. Das beinhaltet nicht die Kosten für die Domain und unseren Monitoring-Server.&lt;/p>
&lt;p>An alle die bisher gespendet habe: Ganz ganz vielen Dank euch!&lt;/p>
&lt;h1 id="english">English&lt;/h1>
&lt;p>We summarized the cost of hardware in the table below. This does not include costs of the domain and our monitoring server.&lt;/p>
&lt;p>To everyone that contributed: Thank you so so much!&lt;/p>
&lt;h1 id="überblickoverview">Überblick/Overview&lt;/h1>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Kosten&lt;/th>
&lt;th>Spenden&lt;/th>
&lt;th>Saldo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>-321.63 €&lt;/td>
&lt;td>235.5 €&lt;/td>
&lt;td>- 86.13 €&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="kostenaufstellung--exact-cost">Kostenaufstellung / Exact cost&lt;/h1>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Date&lt;/th>
&lt;th>Cost (€)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>27.04.2021&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.04.2021&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>26.02.2021&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.01.2021&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.12.2020&lt;/td>
&lt;td>-24.78&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>25.11.2020&lt;/td>
&lt;td>-24.37&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.10.2020&lt;/td>
&lt;td>-24.37&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.09.2020&lt;/td>
&lt;td>-24.37&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.08.2020&lt;/td>
&lt;td>-24.37&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.07.2020&lt;/td>
&lt;td>-24.37&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.06.2020&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.05.2020&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>22.04.2020&lt;/td>
&lt;td>-25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total&lt;/strong>&lt;/td>
&lt;td>&lt;strong>-321.63&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Die wechselnden monatlichen Kosten sind durch die temporär gesenkte Mehrwertsteuer bedingt.&lt;/p>
&lt;p>The altering monthly costs are explained by a decrease in sales tax in Germany.&lt;/p>
&lt;h1 id="spenden--donations">Spenden / Donations&lt;/h1>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Date&lt;/th>
&lt;th>Donation (€)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>03.05.2021&lt;/td>
&lt;td>7.50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>06.04.2021&lt;/td>
&lt;td>7.50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>02.03.2021&lt;/td>
&lt;td>7.50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>01.03.2021&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12.02.2021&lt;/td>
&lt;td>33&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12.02.2021&lt;/td>
&lt;td>20&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>09.02.2021&lt;/td>
&lt;td>50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>09.02.2021&lt;/td>
&lt;td>10&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total&lt;/strong>&lt;/td>
&lt;td>&lt;strong>235.5&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="letzte-aktualisierung--last-update">Letzte Aktualisierung / Last update&lt;/h1>
&lt;p>21.05.2021&lt;/p>
&lt;h1 id="kontak--contact">Kontak / Contact&lt;/h1>
&lt;p>Bei Fragen meldet euch gerne bei: &lt;a href="mailto:bbb@hyteck.de">bbb@hyteck.de&lt;/a>&lt;/p>
&lt;p>If you have any questions regarding this ask us via: &lt;a href="mailto:bbb@hyteck.de">bbb@hyteck.de&lt;/a>&lt;/p></content></entry><entry><title>Set up and secure an MQTT broker on Ubuntu</title><link href="https://hyteck.de/post/mqtt-telegraf-influx/" type="application/octet-stream"/><updated>2021-01-01T18:18:10+02:00</updated><id>https://hyteck.de/post/mqtt-telegraf-influx/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>I had some IoT devices that I wanted to integrate in my monitoring. For this I set up a MQTT broker as the MQTT protocol is a simple solution to send data from IoT devices to a server. This tutorial is focusing on setting up the server, but I also introduce a Python based MQTT client to test our installation.&lt;/p>
&lt;p>On your server, first install mosquitto, our MQTT server/broker.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt-get install mosquitto
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Allow standard mqtt port in firewall (if you have ufw installed)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">1883&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now on the client side connect to the server and publish some fake sensor values.
First install the mqtt client&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo pip install phao-mqtt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and then use the following python code on your client side to send fake values to your server. You only need to change &lt;code>mqtt.example.com&lt;/code> to your servers IP/domain.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> time
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> paho.mqtt.client &lt;span style="color:#66d9ef">as&lt;/span> mqtt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> numpy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> numpy &lt;span style="color:#66d9ef">as&lt;/span> np
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">calc_temp&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> temp &lt;span style="color:#f92672">=&lt;/span> np&lt;span style="color:#f92672">.&lt;/span>sin(time&lt;span style="color:#f92672">.&lt;/span>time()&lt;span style="color:#f92672">%&lt;/span>(&lt;span style="color:#ae81ff">3600&lt;/span>)&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>&lt;span style="color:#f92672">*&lt;/span>np&lt;span style="color:#f92672">.&lt;/span>pi)&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">5&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#ae81ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> temp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_connect&lt;/span>(client, userdata, flags, rc):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Connected with result code &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> str(rc))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client &lt;span style="color:#f92672">=&lt;/span> mqtt&lt;span style="color:#f92672">.&lt;/span>Client()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#client.username_pw_set(username=&amp;#34;username&amp;#34;,password=&amp;#34;my_super_secret_pw&amp;#34;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>on_connect &lt;span style="color:#f92672">=&lt;/span> on_connect
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>connect(&lt;span style="color:#e6db74">&amp;#34;mqtt.example.com&amp;#34;&lt;/span>, &lt;span style="color:#ae81ff">1883&lt;/span>, &lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>loop_start()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> time&lt;span style="color:#f92672">.&lt;/span>sleep(&lt;span style="color:#ae81ff">2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>publish(&lt;span style="color:#e6db74">&amp;#34;test/temperature&amp;#34;&lt;/span>, calc_temp())
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can check if the broker accepts the values by subscribing to the topic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/usr/bin/env python&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> paho.mqtt.client &lt;span style="color:#66d9ef">as&lt;/span> mqtt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_connect&lt;/span>(client, userdata, flags, rc):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Connected with result code &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> str(rc))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client&lt;span style="color:#f92672">.&lt;/span>subscribe(&lt;span style="color:#e6db74">&amp;#34;test/#&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_message&lt;/span>(client, userdata, msg):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(msg&lt;span style="color:#f92672">.&lt;/span>topic &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#e6db74">&amp;#34; &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> str(msg&lt;span style="color:#f92672">.&lt;/span>payload))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client &lt;span style="color:#f92672">=&lt;/span> mqtt&lt;span style="color:#f92672">.&lt;/span>Client()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#client.username_pw_set(username=&amp;#34;username&amp;#34;,password=&amp;#34;my_super_secret_pw&amp;#34;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>on_connect &lt;span style="color:#f92672">=&lt;/span> on_connect
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>on_message &lt;span style="color:#f92672">=&lt;/span> on_message
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>connect(&lt;span style="color:#e6db74">&amp;#34;mqtt.example.com&amp;#34;&lt;/span>, &lt;span style="color:#ae81ff">1883&lt;/span>, &lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client&lt;span style="color:#f92672">.&lt;/span>loop_forever()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now secure your broker by creating a user with a password&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo mosquitto_passwd -c /etc/mosquitto/passwd &amp;lt;username&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and configure mosquitto to use it in &lt;code>/etc/mosquitto/conf.d/default.conf&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>allow_anonymous false
password_file /etc/mosquitto/passwd
&lt;/code>&lt;/pre>&lt;p>Now restart mosquitto to enable the protection&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo systemctl restart mosquitto
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the installation by uncommenting &lt;code>client.username_pw_set(username=&amp;quot;username&amp;quot;,password=&amp;quot;my_super_secret_pw&amp;quot;)&lt;/code> and filling in your credentials.
The result code &lt;code>0&lt;/code> indicates a valid connection. &lt;code>5&lt;/code> indicates a authentication error.&lt;/p>
&lt;p>I hope this helps setting up a MQTT broker. Hopefully I will have the time to write how to connect such a broker to Grafana via Telegraf and Influx DB.&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "monitoring"
})
&lt;/script></content></entry><entry><title>JA zur Corona-Warn-App</title><link href="https://hyteck.de/post/cwa/" type="application/octet-stream"/><updated>2020-12-10T07:00:00+02:00</updated><id>https://hyteck.de/post/cwa/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>JA zur Corona-Warn-App und Nein zu unbegründeter Verunsicherung!&lt;/p>
&lt;p>Ich beziehe mich im Folgenden auf einen &lt;a href="https://schwarzerpfeil.de/2020/12/06/unser-erneuertes-nein-zur-corona-warn-app/">Blogbeitrag der bei Schwarzer Pfeil veröffentlicht wurde&lt;/a>. Gesschrieben wurde er von &lt;a href="https://capulcu.blackblogs.org">capulcu&lt;/a>.&lt;/p>
&lt;p>Beginnen wir mit dem Start:
Die Freiwilligkeit der Installation wird in Frage gestellt. Behauptet wird, dass der Staat hier die Angst der Bevölkerung ausnutze.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/CWA/1.png" alt="Textauszug: Die &amp;ldquo;freiwillige&amp;rdquo; Corona-App und der digitale Immunitäsnachweis: Unter dem Label &amp;ldquo;Zusammen gegen Corona&amp;rdquo; propagiert das Bundesministerium für Gesundheit die allgemeine Nutzung der sogenannten Corona-Warn-App zur nachträglichen Kontaktrekonstruktion Infizierter. Die berechtigte Angst vor dem Virus wird benutzt, um einem Großteil der Bevölkerung &amp;ldquo;freiwillig&amp;rdquo; ein autoritär hochwirksames Werkzeug zu verabreichen.">\&lt;/p>
&lt;p>Es geht weiter mit sehr abstrakten Vorstellungen von &amp;ldquo;prädiktiven Modellen&amp;rdquo; die von solchen &amp;ldquo;Verhaltensdaten&amp;rdquo; trainiert werden sollen. Doch welche sollen das denn sein? Wie im Text später zu lesen ist verbleiben die Corona-IDs auf dem lokalen Gerät.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/CWA/2.png" alt="Selbst wenn das Protokollieren von Kontakten vollständig pseudonym erfolgen würde, müssen wir dringend vor dieser App warnen. In dem Moment, wo (sogar anonyme) Verhaltensdaten flächendeckend anfallen, sind die prädiktiven Modelle, die damit trainiert werden, dazu in der Lage, ganze Populationen in Risikogruppen einzuteilen und algorithmisch zu verwalten. Es ist eine Überwachungsinfrastruktur, die da ausgerollt wird. Deshalb halten wir den Applaus einiger kritischer Datenschützer*innen für unangemessen, ja sogar fahrlässig.">\&lt;/p>
&lt;p>Das gilt auch im Infektionsfall wie sie selbst schreiben. Woher soll also diese &amp;ldquo;Einteilung der Bevölkerung in Risikogruppen&amp;rdquo; und die &amp;ldquo;algorithmische Verwaltung&amp;rdquo; kommen?&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/CWA/3.png" alt="Textauszug: Der Server der Gesundheitsbehörden kann keine Abbildung des sozialen Umfelds ableiten und lernt von Verdachtsfällen nur, wenn die Nutzenden sich nach einer Aufforderung der App beim Gesundheitsamt beziehungsweise einer Ärzt*in melden. Verglichen mit dem zentralen Ansatz bewahren die Nutzenden der App ein größeres Maß an Privatsphäre und Autonomie gegenüber staatlichen Stellen und deren Infrastruktur.">\&lt;/p>
&lt;p>Beschrieben werden außerdem Bluetooth-Sicherheitslücken.
Diese sind zwar durchaus schwerer Natur, stechen jedoch nicht aus der Masse besonders heraus oder stellen Grundlagen des Protokolles infrage. Eine massenhafte Ausnutzung erscheint unwahrscheinlich.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/CWA/4.png" alt="Bluetooth hat eine 20 Jahre alte Geschichte der Unsicherheit. Alle paar Jahre gibt es einen neuen Angriff auf Bluetooths Pairing-Protokoll oder die verwendete Verschlüsselung. Auch aktuell gibt es eine Sicherheitslücke (CVE-2020-0022[9]) und einen Exploit, der diese ausnutzt (Bluetooth zero-click short-distance RCE exploit against Android 8/9 (bei Android 10 keine RCE, aber DoS)). Mit dieser Lücke und dem Exploit lässt sich ein Wurm schreiben, der sich ohne User*innen-Interaktion Über Bluetooth weiterverbreitet und auf den Geräten Schadcode in einem privilegierten Prozess ausführen kann[10].">\&lt;/p>
&lt;p>Zugegeben, Bluetooth ist nicht zwingend die sicherste Technologie. Obwohl die Sicherheitslücke schnell geschlossen wurde ist die Kritik an der Android Update Verbreitung berechtigt.
Das der CWA anzulasten ist jedoch etwas weit hergeholt.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/CWA/5.png" alt="Textauszug: Wer jemandem zu nahe kommt, kann sich nicht nur selbst mit Covid-19 infizieren, sondern mit einem CVE-2020-0022-Wurm , dank der Corona-App, auch sein Smartphone, welches den Wurm dann munter weitergibt. Die Schwachstelle ist in dem Security-Patch von Android Open Source Project (AOSP) vom Februar 2020 behoben. Aber welche Android-Smartphones werden den jemals erhalten?">\&lt;/p>
&lt;p>Google und Apple wird unterstellt die über die Bluetooth-Schnittstelle der CWE gewonnen Daten zur Erstellung von Social Graphs nutzen zu wollen. Aber sind wir ehrlich Standort und Telefonbuch sind da aussagekräftig genug.&lt;/p>
&lt;p>&lt;img src="https://hyteck.de/uploads/CWA/6.png" alt="Textauszug: Des Weiteren haben Google und Apple auch Interesse an Social Graphs.">\&lt;/p>
&lt;p>Dass Google und Apple maßgeblich dafür Verantwortlich sind, dass sich der datenschutzfreundliche dezentrale Ansatz durchgesetzt hat wird als Beweis der Macht der Konzerne angesehen und als den versuch &amp;ldquo;unausweichliche Instanz zu sein&amp;rdquo;.
&lt;img src="https://hyteck.de/uploads/CWA/7.png" alt="Dass Google und Apple maßgeblich dafür Verantwortlich sind, dass sich der datenschutzfreundliche dezentrale Ansatz durchgesetzt hat wird als Beweis der Macht der Konzerne angesehen und als den versuch &amp;ldquo;unausweichliche Instanz zu sein&amp;rdquo;.">\&lt;/p>
&lt;p>Dass dem nicht so ist und der offene, dezentrale Ansatz sogar Macht von Google und Apple sinken lässt zeigt die in Deutschland verfügbare Google-freie CWA:
&lt;a href="https://f-droid.org/en/packages/de.corona.tracing">https://f-droid.org/en/packages/de.corona.tracing&lt;/a>&lt;/p>
&lt;h1 id="fazit">Fazit&lt;/h1>
&lt;p>Skepsis bzgl. Technik bleibt wichtig. Die Corona-Warn App schafft aus meiner Sicht jedoch
eine durchaus bemerkenswerte Leistung, nämlich sennsible Kontaktdaten datensicher zu verarbeiten.
Das ist vielen Datenschützer*innen zu verdanken, die sich für eine dezentrale Open-Source
Lösung eingesetzt haben. Kritik an der App ist teilweise faktisch falsch und überschätzt Risiken
stark. Statt zur Verunsicherung beizutragen, sollte solidarisch für solche guten Lösungen geworben
werden!&lt;/p>
&lt;p>&lt;strong>PS:&lt;/strong> Der Thread/Blogbeitrag erhebt keinen Anspruch auf Vollständigkeit. Eine Beschäftigung mit
dem gesamten Orginal hätte sehr viel länger gedauert und meine Kapazitäten gesprengt.&lt;/p>
&lt;p>Einen guten Thread hat auch &lt;a href="https://chaos.social/@waweic">Waweic&lt;/a> dazu geschrieben: &lt;a href="https://chaos.social/@waweic/105335074439772081">https://chaos.social/@waweic/105335074439772081&lt;/a>&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "cwa"
})
&lt;/script></content></entry><entry><title>Translation for video conferences</title><link href="https://hyteck.de/post/bbb_translation/" type="application/octet-stream"/><updated>2020-07-23T07:30:00+02:00</updated><id>https://hyteck.de/post/bbb_translation/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>Although physical meetings are not possible, many groups and organizations are dependent on multilingual meetings.
In videoconferencing, concepts such as a whispered translation are not intuitive. Therefore a friend and I have developed a concept for translation in videoconferencing.
The concept was planned for a conference with 80 participants who will discuss and make decisions over two days either in large groups or in sub-groups.
The videoconferencing software that was used was BigBlueButton (BBB).&lt;/p>
&lt;h1 id="translation-via-subtitles">Translation via subtitles&lt;/h1>
&lt;p>BBB offers the possibility to write subtitles simultaneously to video conferencing.
This feature is especially aimed at hearing impaired people.
For translation, one person would translate and type in one language at a time.
Due to the limited typing speed, this procedure is associated with large losses of content.
However, it is a great option for hearing-impaired people and can be combined with a simultaneous oral translation.&lt;/p>
&lt;h1 id="simultaneous-translation">Simultaneous translation&lt;/h1>
&lt;p>The concept of simultaneous translation is based on the fact that there is a seperate BBB-room for each language. These are communicated to the participants in advance.&lt;/p>
&lt;p>The room whose language most of the participants speak will be the main room. People who do not speak the language of the main room will still join it, as moderation tasks and the presentation will be only in this room.&lt;/p>
&lt;p>All statements in the main room will be translated by overseers in the other rooms. The translators should be present in the main room with the microphone switched off. If a person from a secondary room wants to speak they indicate this in the chat of the main room, - preferably with &amp;ldquo;* (language)&amp;rdquo;. They will then be allowed to speak by the moderator and speak in their respective room. The translator will unmute their microphone in the main room and translates to the primar language.&lt;/p>
&lt;h2 id="breakout-rooms">Breakout Rooms&lt;/h2>
&lt;p>Breakput rooms are often formed to debate in smaller groups.
To ensure a reasonable amount of translation effort, all participants and translators of one language are assigned to one breakout room (in the main room).
If the Breakout Rooms become too full, Breakout Rooms can also be formed in the secondary rooms, but translators are then required for each room.&lt;/p>
&lt;h1 id="tips-and-tricks">Tips and tricks&lt;/h1>
&lt;ul>
&lt;li>Use two translators per language. One person cannot do this even at a moderate speed of discussion. It also allows for better reproduction of conversations by alternating the translators.&lt;/li>
&lt;li>Ask participants to set their names according to the &amp;ldquo;Name [pronouns] (language)&amp;rdquo; scheme. This makes it easier for moderation and translators.&lt;/li>
&lt;li>Participants do not need to join the audio in the main room, but they can, so that their contributions can be heard in the original. However, they should mute the browser window of the main room.&lt;/li>
&lt;/ul>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Whispered translation is possible in BBB; in order to use it, the possibility must be announced in good time by the moderator and enough translators must be found. The concept of oral translation has already proven itself at a large meeting.&lt;/p>
&lt;p>If you are interested in implementing the concept you are welcome to contact me at &lt;a href="mailto:fluesteruebersetzung@hyteck.de">fluesteruebersetzung@hyteck.de&lt;/a> or in the comments below.&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "bbb-translation"
})
&lt;/script></content></entry><entry><title>Übersetzung in Videokonferenzen</title><link href="https://hyteck.de/post/konzept_fl%C3%BCster%C3%BCbersetzung/" type="application/octet-stream"/><updated>2020-07-23T07:30:00+02:00</updated><id>https://hyteck.de/post/konzept_fl%C3%BCster%C3%BCbersetzung/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>Auch wenn physische Treffen nicht möglich sind sind viele Gruppen und Organisationen abhängig von mehrsprachigen Treffen.
In Videokonferenzen sind Konzepte wie eine Flüsterübersetzung nicht intuitiv umzusetzen.
Ich habe daher mit einer anderen Person zusammen ein Konzept für die Übersetzung in Videokonferenzen erarbeitet.
Das Konzept wurde geplant für eine Konferenz mit 80 Teilnehmenden,
die über zwei Tage hinweg entweder in großer Gruppe oder in Untergruppen diskutieren und Entscheidungen treffen.
Die Videokonferenzsoftware die dabei zum Einsatz kam war BigBlueButton (BBB).&lt;/p>
&lt;h1 id="übersetzung-via-untertitel">Übersetzung via Untertitel&lt;/h1>
&lt;p>BBB bietet die Möglichkeit simultan zu Videokonfernz Untertitel zu schreiben.
Das Feature richtet sich insbesondere an hörgeschädigte Personen.
Für die Übersetzung würde eine Person jeweils eine Sprache übersetzen und eintippen.
Dieses Vorgehen ist aufgrund der begrenzten Tippgeschwindigkeit mit großen inhaltlichen Verlusten verbunden.
Für hörgeschädigte Personen ist es jedoch eine tolle Möglichkeit und lässt sich mit eine mündlichen Simultanübersetzung kombinieren.&lt;/p>
&lt;h1 id="mündliche-übersetzung">Mündliche Übersetzung&lt;/h1>
&lt;p>Das Konzept der mündlichen Überetzung basiert darauf, dass es für jede Sprache einen Raum gibt.
Diese werden vorher an die Teilnehmenden koummuniziert.&lt;/p>
&lt;p>Der Raum, dessen Sprache die meisten Teilnehmenden beherrschen wird dabei der Hauptraum.
Personen die nicht die Sprache des Hauptraums sprechen treten diesem trotzdem bei, da Meldungen und andere Moderationsaufgaben in
diesem Raum koordiniert werden.&lt;/p>
&lt;p>Alle Äuserungen im Hauptraum werden in den anderen Räumen von Übersezer*innen übersetzt.
Die Simultanübersetzer*innen sollten dafür mit ausgeschaltetem Mikro im Hauptraum anwesend sein.
Wenn eine Person aus dem Nebenraum sich meldet tut sie dies auch im Hauptraum,&lt;/p>
&lt;ul>
&lt;li>am besten mit &amp;ldquo;* (Sprache)&amp;rdquo;. Sie wird dann von der Moderation aufgerufen und spricht in ihrem jeweiligen Nebenraum,
die Übersetzerinnen unmuten sich im Hauptraum und bringen den Wortbeitrag dort ein.&lt;/li>
&lt;/ul>
&lt;h2 id="breakout-rooms">Breakout Rooms&lt;/h2>
&lt;p>Oft werden Breakput Rooms gebildet um in kleineren Gruppen zu sprechen.
Um einen vertretbaren Überstzungsaufwand zu gewährleisten werden alle, Teilnehmende und Übersetzende einer Sprache gemeinsam einem Breakout Room (im Hauptraum) zugewiesen.
Falls die Breakout Rooms zu voll werden, können auch im Nebenraum Breakout Rooms gebildet werden, es sind jedoch für jeden Raum Übersetzende notwendig.&lt;/p>
&lt;h1 id="tipps-und-tricks">Tipps und Tricks&lt;/h1>
&lt;ul>
&lt;li>Nutzt zwei Übersetzer*innen pro Sprache. Eine Person kann das selbst in einer moderaten Diskssionsgeschwindigkeit nicht leisten.
Außerdem ermöglicht es Gespräche durch Abwechseln der Übersetzenden besser wiederzugeben.&lt;/li>
&lt;li>Bittet Teilnehmende ihren Namen nach dem Schema &amp;ldquo;Name [Pronomen] (Sprache)&amp;rdquo; einzustellen. Das macht es Moderation und Übersetzenden leichter.&lt;/li>
&lt;li>Teilnehmende müssen im Hauptraum nicht dem Audio beitreten, können dies aber, sodass ihre Wortbeiträge im Orgninal gehört werden können.
Jedoch sollten sie selbst das Browserfenster auf stumm schalten.&lt;/li>
&lt;/ul>
&lt;h1 id="fazit">Fazit&lt;/h1>
&lt;p>Flüsterübersetzung ist in BBB möglich; Um es zu nutzen, muss die
Möglichkeit frühzeitig durch die Moderation angekündigt werden und sich genügend Übersetzer*innen finden.
Das Konzept der mündlichen Übersetzung hat sich bereits bei einem großen Treffen bewährt.&lt;/p>
&lt;p>Wenn ihr Interesse habt das Konzept umzusetzen schreibt mir gerne an &lt;a href="mailto:fluesteruebersetzung@hyteck.de">fluesteruebersetzung@hyteck.de&lt;/a>&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "bbb-translation"
})
&lt;/script></content></entry><entry><title>Antwort auf einen Impfgegner</title><link href="https://hyteck.de/post/impfgegner/" type="application/octet-stream"/><updated>2020-05-04T00:00:00Z</updated><id>https://hyteck.de/post/impfgegner/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="cw">CW&lt;/h1>
&lt;p>Tod, Krankheit, Spritzen&lt;/p>
&lt;h1 id="petition-gegen-masernimpfung">Petition gegen Masernimpfung&lt;/h1>
&lt;p>Ich habe von einem Freund einen Link zu einer &lt;a href="https://www.change.org/p/bundespr%C3%A4sident-frank-walter-steinmeier-sagen-sie-nein-zu-zwangsimpfungen-in-deutschland">Impfgegner Petition&lt;/a> bekommen. Da ich solche Petitionen für hochgefährlich halte habe ich mir Zeit für eine Antwort genommen.&lt;/p>
&lt;h1 id="antwort">Antwort&lt;/h1>
&lt;p>Hi, ich habe mir die Petition angeschaut und werde sie nicht unterschreiben - aus verschiedenen Gründen.&lt;/p>
&lt;p>In der Petition steht: &amp;ldquo;Masern sind eine harmlose Kinderkrankheit, wenn sie richtig behandelt werden.&amp;rdquo; Das stimmt für die Mehrheit. Aber 1 von 1000 erleidet eine Gehirnentzündung und 4-11 von 100.000 gesunden Menschen erkrankt an SSPE das immer tödlich verläuft. (&lt;a href="https://link.springer.com/article/10.1007/s11910-020-1023-y">https://link.springer.com/article/10.1007/s11910-020-1023-y&lt;/a>)&lt;/p>
&lt;p>&amp;ldquo;In Deutschland stirbt daran jährlich ein Mensch&amp;rdquo; Das stimmt eventuell nach akuten Infektionen (kann ich nicht nachprüfen), and den Spätfolgen sind es jedoch 30 Menschen pro Jahr (die Zahl schwankt sehr stark, also mit Vorsicht genießen).&lt;/p>
&lt;p>&amp;ldquo;Der diesjährige Maserntote starb 8 Tage nach einer Masernimpfung in die akute Masernerkrankung hinein.&amp;rdquo; Das stimmt grundsätzlich (wenn damit 2019 gemeint ist). Erwähnt wird aber nicht, dass der Mensch sich impfen lassen hat nachdem es es in der Familie einen Masernfall gab. Die Impfung hat leider nicht schnell genug geholfen (sie ist erst nach 10-18 Tagen wirksam). Allerdings hätte der Mensch aller Wahrschinlichkeit nach überlebt, hätte er sich irgendwann vorher impfen lassen (Bericht zu dem Fall &lt;a href="https://www.n-tv.de/panorama/Masernkranker-stirbt-in-Niedersachsen-article21010218.html)">https://www.n-tv.de/panorama/Masernkranker-stirbt-in-Niedersachsen-article21010218.html)&lt;/a>.
Die niedrige Anzahl der Toten (und durch die Erkrankung dauerhaft geschädigten) ist aber eben auch der hohen Durchimpfungsquote zu verdanken. Sie ist der einzige Faktor dafür, wie viele Erkrankungen es pro Jahr gibt. (Quelle: &lt;a href="https://www.atlantis-press.com/journals/jegh/125919426">https://www.atlantis-press.com/journals/jegh/125919426&lt;/a>)&lt;/p>
&lt;p>&amp;ldquo;Es gibt also keine epidemiologische Begründung, die Impfungen überhaupt auszuweiten. Auch in Ländern mit 100% Durchimpfungsrate gibt es Masernfälle. Die von Herrn Spahn vorgegebene &amp;ldquo;Ausrottung&amp;rdquo; ist gar nicht möglich.&amp;rdquo;
Zunächste einmal: Es kann kein Land eine Durchimpfungsquote von 100% erreichen. In jedem Land gibt es immunschwache Personen die sich nicht impfen lassen können, zB. nach einer Krebserkrankung).
Warum genau eine Ausrottung nicht möglich sein soll verstehe ich nicht ganz. Es gibt sehr klare Beweise, dass mit mehr Impfungen weniger Fälle auftreten (&lt;a href="https://www.atlantis-press.com/journals/jegh/125919426)">https://www.atlantis-press.com/journals/jegh/125919426)&lt;/a>. Polio ist dank der Impung in Europa nahezu ausgerottet. Ich sehe keinen Grund warum das nicht mit den Masern gelingen sollte.&lt;/p>
&lt;p>Außerdem ist das ganze auch eine Sache der Solidarität: Ein krebskrankes Kind wird sich nicht impfen können und eine Erkrankung stellt eine deutlich größere Gefahr da. Damit solche Menschen in die Schule, in die Stadt und so weiter können müssen alle anderen geimpft sein. Deswegen bezog sich der Gesetzentwurf von Spahn auch auf Schulen und KiTas/KiGas.&lt;/p>
&lt;p>Dazu kommt, dass die Petition von 2019 ist, die Impfpflicht ist seit 1.März 2020 schon in Kraft.&lt;/p>
&lt;p>Sorry dass die Quellen auf Englisch sind, aber ich wollte möglichst auf die orginalen Artikel verlinken. Der Wikipedia Artikel ist aber auch ganz gut: &lt;a href="https://de.wikipedia.org/wiki/Masern#Europa">https://de.wikipedia.org/wiki/Masern#Europa&lt;/a>&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "impfen"
})
&lt;/script></content></entry><entry><title>Travelling around Jericoacoara</title><link href="https://hyteck.de/post/from-jericoacoara-to-sao-louis/" type="application/octet-stream"/><updated>2020-02-01T18:00:00+01:00</updated><id>https://hyteck.de/post/from-jericoacoara-to-sao-louis/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="tldr">TL;DR&lt;/h1>
&lt;p>Jeri to Sao Luis is hard by bus. Try the route Jericoacoara -&amp;gt; Jijoca -&amp;gt; Camocim -&amp;gt; Paranaiba -&amp;gt; Sao Luis&lt;/p>
&lt;h1 id="introduction-from-fortaleza-to-jericoacoara">Introduction: From Fortaleza to Jericoacoara&lt;/h1>
&lt;p>As my girlfriend and I were traveling from Salvador
to Sao Luis we mostly relied on bus connections.
Nevertheless we had some difficulties getting from
the beautiful town of Jericoacoara
to the trip&amp;rsquo;s last station Sao Luis.&lt;/p>
&lt;h2 id="jericoacoara">Jericoacoara&lt;/h2>
&lt;p>Jericoacoara is a town surrounded by a
National Park. Therefore anyone visiting Jericoacoara
has to use an authorized vehicle to make the last part of
the trip from Jijoca de Jericoacoara
to Jericoacoara itself. The vehicles that
make this trip must be off-roaders so mainly the
so called D-20 or Buggys are used.
&lt;img src="https://hyteck.de/uploads/brazil/D-20.jpg" alt="Picture of a D-20 with caracteristic back seats">
The trip takes approximately 50 minutes and
should cost not more than 25 real per person.&lt;/p>
&lt;p>On our way to Jeri we drove by bus from Fortaleza to Jijoca.
At the stop in Jijoca there were already Jeeps waiting for us.
The bus company &lt;em>Expresso Guanabara&lt;/em> which operated the bus to Jijoca offered
to buy tickets from Jijoca to Jericoacoara for 30 real.
After paying the tourist fee (5 real per person and night) we drove through forest,
dunes and some annoyed donkeys to Jeri. The driver dropped us
directly at the Pousada where we stayed.&lt;/p>
&lt;h1 id="from-jericoacoara-to-sao-luis">From Jericoacoara to Sao Luis&lt;/h1>
&lt;p>In Jeri we realized that it would be not as easy as we thought to
travel to Sao Luis as there was no bus going west from Jijoca.
After some research we found the following solution:&lt;/p>
&lt;h2 id="1-800-am-jericoacoara---jijoca">1. 8:00 am Jericoacoara -&amp;gt; Jijoca&lt;/h2>
&lt;p>We waited at the main
street in the city center for a Jeep to Jijoca.
There are also several wooden benches around the city where
drivers with Jeeps that are not yet full pick up people.
Ask in your accomodation!
The trip takes about 50 minutes. People will get of at
different locations in Jijoca, tell the driver
you want to go to Camocim!&lt;/p>
&lt;h2 id="2-945-am-jijoca---camocim">2. 9:45 am Jijoca -&amp;gt; Camocim&lt;/h2>
&lt;p>From Jijoca there are regular busses to Camocim,
a town further west. The bus stop is not well known,
you will recognize it by the sign in the following picture
and people waiting. Next to the stop is a small cafe, perfect
for breakfast or buying some water. &lt;a href="https://www.openstreetmap.org/#map=18/-2.89648/-40.44852">Link to the exact location of departure&lt;/a>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Journey&lt;/th>
&lt;th>Manhã (Morning)&lt;/th>
&lt;th>Tarde (Afternoon)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Jijoca -&amp;gt; Camocim&lt;/td>
&lt;td>7:00, 8:00, 9:45&lt;/td>
&lt;td>1:40, 4:00, 6:30&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Camocim -&amp;gt; Jijoca&lt;/td>
&lt;td>5:00, 6:30, 8:30, 11:00&lt;/td>
&lt;td>1:15, 5:30&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Jijoca -&amp;gt; Prea.&lt;/td>
&lt;td>6:45, 8:15, 11:15&lt;/td>
&lt;td>12:45, 3:00, 7:20&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Do not be late! The bus is mainly taken by locals and can be very full.
There is only limited space for luggage in the back. There is no seat reservation.&lt;/p>
&lt;p>If you are not alone: Let one person reserve seats and
the other person cares about the luggage.
The trip is 17.25 real per person which will be paid during the drive.
Camocim is the last stop. We arrived at 11:20 at the
&lt;a href="https://www.openstreetmap.org/node/6357083486#map=18/-2.89992/-40.84264">Prefeitura de Camocim&lt;/a>, the last of three stops in Camocim.
From there it is ~500m to the &lt;a href="https://www.openstreetmap.org/way/130660923#map=18/-2.90019/-40.84664">Terminal Rodoviário de Camocim&lt;/a>.
A nice bus driver might take you there (without charging extra).&lt;/p>
&lt;p>It seems that there are two companies operating
busses to Camocim and that the other
company appareantly has also a bus at 11:30.
Taking this bus would reduce your time waiting in Camocim.
Nevertheless we did NOT try this so the risk is yours.&lt;/p>
&lt;p>If you tried it let me know: &lt;a href="mailto:brazil-trip2020@hyteck.de">E-Mail&lt;/a>&lt;/p>
&lt;h2 id="3-320-pm-camocim---parnaìba">3. 3:20 pm Camocim -&amp;gt; Parnaìba&lt;/h2>
&lt;p>In Camocim we waited for the 3:20 pm bus to Paranaíba operated
by &lt;em>Expresso Guanabara&lt;/em>. The Terminal has WiFi, bathrooms and a small restaurant
offering a typical buffet and cold drinks.
Opposite the street there is a burger shop with inexpensive Burgers.
There was always an employee present in the small office of &lt;em>Expresso Guanabara&lt;/em>&lt;/p>
&lt;p>The bus ride has to be pre-booked. The best website for finding booking options
is &lt;a href="https://www.buscaonibus.com.br/en">buscaonibus.com.br&lt;/a> which will
provide links to booking sites like &lt;a href="https://www.busbud.com/">Busbud&lt;/a> or &lt;a href="https://brazilbybus.com">BrazilByBus&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Attention: This bus is not listed on Busbud!&lt;/strong>&lt;/p>
&lt;h2 id="4-2100-parnaìba---sao-luis">4. 21:00 Parnaìba -&amp;gt; Sao Luis&lt;/h2>
&lt;p>From Paranaíba to Sao Luis there are two busses each day, one at 9 am one at 9 pm.
Therefore this city can be a good place for some rest after the already long journey.
After getting some sleep you can continue the trip in the morning.
Those that want to reach Sao Luis as fast as possible should
wait at the Terminal for the 9pm connection.
It reaches Sao Luis at around 6:30. The bus has &amp;ldquo;normal&amp;rdquo; seats and the
&lt;em>leito&lt;/em> (bed) option. It is not an actual bed, but a very comfortable seat in the lower deck
that can be leaned far back. You feel bumps significantly less in the upper deck. Furthermore this option provides
you with the possibility to recharge your phones via USB.
The WiFi is a game of luck as in all other busses :)&lt;/p>
&lt;p>Important: Overnight busses mostly arrive too early - 30 minutes up to two hours.
My strategy for getting off at the right station was to set an alarm 90 minutes before
scheduled arrival and then check the position on an offline map and setting an alarm accordingly
the bathroom will be used frequently in the morning - plan some waiting time.&lt;/p>
&lt;p>You will arrive at the &lt;a href="https://www.openstreetmap.org/#map=18/-2.56357/-44.24477">Terminal Rodoviario do Sao Luís&lt;/a>&lt;/p>
&lt;p>The bus ride also has to be pre-booked. Check on &lt;a href="https://www.buscaonibus.com.br/en">buscaonibus.com.br&lt;/a>.&lt;/p>
&lt;h1 id="improve-this">Improve this&lt;/h1>
&lt;p>I hope this helps you when planning your trip.&lt;/p>
&lt;p>If you tried this trip yourself or if you have
any comment or questions let me know: &lt;a href="mailto:brazil-trip2020@hyteck.de">E-Mail&lt;/a>&lt;/p>
&lt;h1 id="further-information">Further information:&lt;/h1>
&lt;ul>
&lt;li>Site for booking of bus connections &lt;a href="https://www.buscaonibus.com.br/en">buscaonibus.com.br&lt;/a>&lt;/li>
&lt;/ul>
&lt;h1 id="ps">PS&lt;/h1>
&lt;p>This is not a travel blog or anything.
I just found it hard to navigate this trip an thought this might help others.
So this is more a tutorial that got out of hand :)&lt;/p></content></entry><entry><title>Services</title><link href="https://hyteck.de/services/" type="application/octet-stream"/><updated>2019-11-14T09:56:10+02:00</updated><id>https://hyteck.de/services/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="services">Services&lt;/h1>
&lt;p>This is a non-extensive list of services I offer. All services are hosted in Germany and come with monitoring of service
uptime.&lt;/p>
&lt;h2 id="library-management">Library management&lt;/h2>
&lt;p>The library management software &lt;a href="https://github.com/moan0s/ILMO2">ILMO&lt;/a> is devoloped by me. It is a perfect tool for the
management of small to middle-sized libraries. It offers user management, reminders on loans and an easy borrow procedure.
The software is open source so you can host it yourself. If you do not want the hassle of self-hosting I offer managed
hosting.&lt;/p>
&lt;h2 id="livestreams">Livestreams&lt;/h2>
&lt;p>I offer livestreams for online talks. During the global COVID pandemic, a lot of lectures, seminars and talks had to be
held online. I want to offer you a way to still reach and interact with people while maintaining a high level of data
protection. This is what &lt;a href="https://owncast.online/">Owncast&lt;/a> in combination with other solutions offers: Just reach out
and we can discuss a concept that will work for you!&lt;/p>
&lt;h2 id="list-of-all-services">List of all services&lt;/h2>
&lt;p>I host some services that are publicly available, some that are only for friends and some that are private.
Get in touch if you want me to host a service like this exclusively for yourself or your organization.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Access&lt;/th>
&lt;th>Status&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://nextcloud.hyteck.de">Nextcloud&lt;/a>&lt;/td>
&lt;td>Cloud storage with collaboration suite&lt;/td>
&lt;td>Upon request&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://matrix.hyteck.de">Matrix&lt;/a>&lt;/td>
&lt;td>Encrypted chat for teams&lt;/td>
&lt;td>Upon request/Einladung&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://gay-pirate-assassins.de">GoToSocial&lt;/a>&lt;/td>
&lt;td>Social Media server in the Fediverse (Mastodon-compatible)&lt;/td>
&lt;td>Public&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://noise.hyteck.de">Funkwhale&lt;/a>&lt;/td>
&lt;td>Music sharing &amp;amp; streaming&lt;/td>
&lt;td>Upon request&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://stream.hyteck.de">Owncast&lt;/a>&lt;/td>
&lt;td>Livestreams&lt;/td>
&lt;td>Upon request&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://notfellchen.org">Notfellchen&lt;/a>&lt;/td>
&lt;td>Find animals and give them a loving home. Not available as commercial hosting - happy to do do this non-profit.&lt;/td>
&lt;td>Public&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://code.hyteck.de">Forgjo&lt;/a> &amp;amp; Forgjo actions&lt;/td>
&lt;td>Git hosting and automations&lt;/td>
&lt;td>Invitiation-only&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Grafana&lt;/td>
&lt;td>Display telemetry data&lt;/td>
&lt;td>Private&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prometheus&lt;/td>
&lt;td>Monitoring system &amp;amp; time series database&lt;/td>
&lt;td>Private&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Influx DB&lt;/td>
&lt;td>Time series database&lt;/td>
&lt;td>Private&lt;/td>
&lt;td>Live&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ILMO&lt;/td>
&lt;td>Library management tool&lt;/td>
&lt;td>Test instance offered upon request&lt;/td>
&lt;td>Testing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BigBlueButton&lt;/td>
&lt;td>Videoconferencing software&lt;/td>
&lt;td>Public&lt;/td>
&lt;td>Discontinued April 21. Use &lt;a href="https://senfcall.de">Senfcall&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="usage-policy-german">Usage policy (german)&lt;/h1>
&lt;p>Unless a contract specifies otherwise the services are provided free of charge and as-is, without warranty of any kind,
expressed or implied. The usage of the services can be restricted or blocked permanently immediately and without warning.&lt;/p>
&lt;p>This is especially true for anything that goes against the following rules.
The rules are a (non-exhaustive) list of behaviours that may
lead to deletion of content or suspension of accounts. In some cases, public or private offline conduct
or conduct in using other services may constitute grounds for removal from the service.&lt;/p>
&lt;ul>
&lt;li>We do not tolerate discriminatory behaviour and content promoting or
advocating the oppression of members of marginalised groups. These
groups may be characterised by any of the following (though this
list is naturally incomplete):
&lt;ul>
&lt;li>ethnicity&lt;/li>
&lt;li>gender identity or expression&lt;/li>
&lt;li>sexual identity or expression&lt;/li>
&lt;li>physical characteristics or age&lt;/li>
&lt;li>disability or illness&lt;/li>
&lt;li>nationality, residency, citizen status&lt;/li>
&lt;li>wealth or education&lt;/li>
&lt;li>religious affiliation, agnosticism or atheism&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>We do not tolerate threatening behaviour, stalking, and
&lt;a href="https://en.wikipedia.org/wiki/Doxxing">doxxing&lt;/a>&lt;/li>
&lt;li>We do not tolerate harassment, including brigading, dogpiling, or
any other form of contact with a user who has stated that they do
not wish to be contacted.&lt;/li>
&lt;li>We do not tolerate mobbing, including name-calling, intentional
misgendering or deadnaming.&lt;/li>
&lt;li>We do not tolerate violent nationalist propaganda, Nazi symbolism or
promoting the ideology of National Socialism.&lt;/li>
&lt;li>We do not tolerate conspiracy narratives or other reactionary myths
supporting or leading to the above-mentioned (and/or similar)
behavior.&lt;/li>
&lt;li>Actions intended to damage a service or its performance may lead
to immediate suspension.&lt;/li>
&lt;li>Content that is illegal in Germany will be deleted and may lead to
immediate account suspension.&lt;/li>
&lt;/ul>
&lt;p>You can report content that goes against these rules even if you are not the affected person. The report will be kept
confidential. &lt;a href="https://hyteck.de/about/#contact">Contact&lt;/a>&lt;/p>
&lt;h2 id="availability">Availability&lt;/h2>
&lt;p>There is &lt;strong>no&lt;/strong> guarantee of availability, unless specified in a separate contract. There is no guarantee of data backups.
Suspension of a service will usually be announced but this is not guaranteed. Join &lt;a href="https://matrix.to/#/#announcements:hyteck.de">Announcements&lt;/a> for this purpose.&lt;/p>
&lt;h1 id="contact">Contact&lt;/h1>
&lt;p>See &lt;a href="https://hyteck.de/about/#contact">here&lt;/a>&lt;/p></content></entry><entry><title>Introduction to git</title><link href="https://hyteck.de/post/introduction_to_git/" type="application/octet-stream"/><updated>2019-10-22T19:00:00+02:00</updated><id>https://hyteck.de/post/introduction_to_git/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>As you man now I work in a research lab. Nearly everyone in our lab needs to do at least some programming. There are trained software engineers next to people how write their first line of code here.
As we produce a lot of code that is only scripts not many people used git or any other version control system.
For all the good reasons (some are mentioned in the guide) we everybody to learn git in a way that is tradeoff between good version control and fast development.
As most of us work on separate projects there is not so much need to collaborate which is reflected ind this guide as I completely excluded branching and merge conflicts.
Yet I hope this will maybe help some learners of git or some people that need to introduce git to others.&lt;/p>
&lt;p>I will not be updating this guide, for the newest version please visit &lt;a href="https://github.com/GerJuli/introduction-to-git">https://github.com/GerJuli/introduction-to-git&lt;/a>&lt;/p>
&lt;p>Now enjoy the guide!&lt;/p>
&lt;h1 id="installation">Installation&lt;/h1>
&lt;ul>
&lt;li>On Windows: Download git here: &lt;a href="https://git-scm.com/download/win">https://git-scm.com/download/win&lt;/a> and install it&lt;/li>
&lt;li>On Linux: execute &amp;lsquo;sudo apt install git&amp;rsquo; in the terminal&lt;/li>
&lt;/ul>
&lt;h1 id="configuration">Configuration&lt;/h1>
&lt;h2 id="basics">Basics&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git config --global user.name &lt;span style="color:#e6db74">&amp;#34;John Doe&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git config --global user.email johndoe@example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Run these commands without &amp;ndash;global to apply changes only for local repository.&lt;/p>
&lt;h2 id="advanced">Advanced&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>Change from default editor by executing &lt;code>$ git config --global core.editor vim&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Create aliases by:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;code>git config --global alias.unstage 'reset HEAD --'&lt;/code>:
Introduces new command unstage&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>git config --global alias.hist 'log --pretty=format:&amp;quot;%h - %an, %ar : %s&amp;quot; --graph'&lt;/code>:
Is an pretty alternative to git log.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>Check the configuration by &lt;code>$ git config --list&lt;/code>&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h1 id="getting-started">Getting started&lt;/h1>
&lt;h2 id="create-an-directory">Create an directory&lt;/h2>
&lt;p>Select an directory of your choice e.g. ~/Desktop/git-training/,
open the terminal there and type&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git init
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Congratulations you have your first git repository!&lt;/p>
&lt;h2 id="committing-files">Committing files&lt;/h2>
&lt;p>Create some text files e.g. &lt;code>hallo.txt&lt;/code> or &lt;code>hello_world.py&lt;/code>.
To start version-controlling them add the file to git by using the command&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git add filename
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can also add multiple files. E.g. you want to add all text files the do&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git add *.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now we can commit these changes by typing&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git commit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will open your preferred text editor
where you can type in a commit message.&lt;/p>
&lt;h3 id="commit-messages">Commit messages&lt;/h3>
&lt;p>&lt;em>Commit messages are important&lt;/em>&lt;/p>
&lt;p>Why are the so important? Because it is much easier to track bugs with it,
they describe the process of development, make it easier for others to work
with your code (e.g. code review) and will help you in two years to understand what you did.
They extend your labbook in its function.&lt;/p>
&lt;h4 id="how-to-write-commit-messages">How to write commit messages&lt;/h4>
&lt;ul>
&lt;li>Use present tense and use imperative&lt;/li>
&lt;li>Tell why you did changes.&lt;/li>
&lt;li>Limit yourself to 50 characters in the first line.&lt;/li>
&lt;li>Wrap the body at 72 characters&lt;/li>
&lt;/ul>
&lt;h4 id="good-commit-messages">Good commit messages&lt;/h4>
&lt;p>For short commits that need only few explanation it is convenient to use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git commit -m &lt;span style="color:#e6db74">&amp;#34;Fix typo in introduction to user guide&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For long commit message you use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git commit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>which opens a text editor and lets you describe your changes in detail. Do not forget the line
between header and body of the commit message.&lt;/p>
&lt;pre tabindex="0">&lt;code>Summarize changes in around 50 characters or less
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.
Explain the problem that this commit is solving. Focus on why you
are making this change as opposed to how (the code explains that).
Are there side effects or other unintuitive consequences of this
change? Here&amp;#39;s the place to explain them.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded
by a single space, with blank lines in between, but conventions
vary here
&lt;/code>&lt;/pre>&lt;h4 id="hall-of-shame">Hall of shame&lt;/h4>
&lt;p>Here you see some examples of bad commit messages.&lt;/p>
&lt;ul>
&lt;li>
&lt;p>“bug fix”, &amp;ldquo;more work”, “minor changes”:
Bad because it contains no useful information&lt;/p>
&lt;/li>
&lt;li>
&lt;p>“Change X constant to be 10”:
Bad as it does not tell why. What was changed is easy to find out by
git diff.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>“super long commit message goes here, something like 100 words and lots of characters woohoo!”:
Bad because it is unreadable.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="storing-changes-on-remote-server">Storing changes on remote server&lt;/h2>
&lt;p>To store changes on a remote server e.g. Github you need to push your repository there.
If you are doing this for the first time you first need to define where to push your repository to.
Use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git remote add origin https://github.com/GerJuli/introduction-to-git.git
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The URL can be
replaced with any URL that points to an existing repository. This can even be a USB drive!
Now push by using &lt;code>git push -u origin master&lt;/code>. With this command you created an upstream to your
remote &amp;lsquo;origin&amp;rsquo; where you branch &amp;lsquo;master&amp;rsquo; will be pushed.
As this is set up you can use &lt;code>git push&lt;/code> from now on.&lt;/p>
&lt;h1 id="cloning-existing-repositories">Cloning existing repositories&lt;/h1>
&lt;p>Downloading existing repositories is called &amp;lsquo;cloning&amp;rsquo;. You can do this by using&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git clone https://github.com/GerJuli/introduction-to-git.git
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That is it. Now you can start working on it.&lt;/p>
&lt;h1 id="workflow">Workflow&lt;/h1>
&lt;h2 id="check-status">Check status&lt;/h2>
&lt;p>With&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>you can check the status of your current directory.
This will look like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>On branch master
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Your branch is up-to-date with &lt;span style="color:#e6db74">&amp;#39;origin/master&amp;#39;&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>nothing to commit, working directory clean
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="add-file">Add file&lt;/h2>
&lt;p>If you now add a file to the repository e.g. README then the status changes.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ echo &lt;span style="color:#e6db74">&amp;#39;My Project&amp;#39;&lt;/span> &amp;gt; README
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>On branch master
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Your branch is up-to-date with &lt;span style="color:#e6db74">&amp;#39;origin/master&amp;#39;&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Untracked files:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">(&lt;/span>use &lt;span style="color:#e6db74">&amp;#34;git add &amp;lt;file&amp;gt;...&amp;#34;&lt;/span> to include in what will be committed&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> README
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nothing added to commit but untracked files present &lt;span style="color:#f92672">(&lt;/span>use &lt;span style="color:#e6db74">&amp;#34;git add&amp;#34;&lt;/span> to track&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can also run&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git diff
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>which will show you the changes that where made.&lt;/p>
&lt;p>Now track the file by adding it to the staging area via&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git add README
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want to commit only a part of the changes you made use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>git add -p
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>git will ask you for every &amp;ldquo;hunk&amp;rdquo;(part of the file) if you want to add it to the staging area.
The main options are:&lt;/p>
&lt;ul>
&lt;li>y stage this hunk for the next commit&lt;/li>
&lt;li>n do not stage this hunk for the next commit&lt;/li>
&lt;li>q quit; do not stage this hunk or any of the remaining hunks&lt;/li>
&lt;/ul>
&lt;h2 id="committing">Committing&lt;/h2>
&lt;p>The changes are now ready to be committed&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>On branch master
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Your branch is up-to-date with &lt;span style="color:#e6db74">&amp;#39;origin/master&amp;#39;&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Changes to be committed:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">(&lt;/span>use &lt;span style="color:#e6db74">&amp;#34;git reset HEAD &amp;lt;file&amp;gt;...&amp;#34;&lt;/span> to unstage&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new file: README
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now commit this change with a suitable commit message.
You forgot what you changed and &lt;code>git diff&lt;/code> does not work?
If you already added the file to the staging area you can run&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git diff --staged
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="push-to-remote">Push to remote&lt;/h2>
&lt;p>Push this now to your repository with&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="show-last-changes">Show last changes&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git log
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>shows you a log of recent commit messages.&lt;/p>
&lt;p>If you want to have a look at the changes source use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git log -p
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A much prettier version is&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git log --pretty&lt;span style="color:#f92672">=&lt;/span>format:&lt;span style="color:#e6db74">&amp;#34;%h - %an, %ar : %s&amp;#34;&lt;/span> --graph
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="get-latest-changes">Get latest changes&lt;/h1>
&lt;p>An amazing thing of git is to work together with others. This guide will not go into the details
of this, yet you will often have to get the latest changes of a project.&lt;/p>
&lt;p>You can do this easily by using&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git pull
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>which is a short version of two different commands. It first executes &lt;code>git fetch&lt;/code> that downloads
the latest changes. Then it runs &lt;code>git merge FETCH_HEAD&lt;/code> which tries to apply the newest changes
to your local repository. This is also the difficulty of this as any file that was changed locally
and in the remote repository will cause a &lt;em>merge conflict&lt;/em> as git does not know which file/parts
of the file to keep. The resolution of such a conflict can be complicated and goes beyond the
intentions of this guide.&lt;/p>
&lt;p>A brief overview can be found
&lt;a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-using-the-command-line">here&lt;/a>.&lt;/p>
&lt;h2 id="gitignore">Gitignore&lt;/h2>
&lt;p>Sometimes you do not want git to track every file in your repository.
Typically this applies for log files, configuration files (where maybe passwords are stored)
and compiled sources. For this purpose you can create the file &lt;code>.gitignore&lt;/code> in your repository.
This file will be read by git. An example &lt;code>.gitignore&lt;/code> looks like this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-gitignore" data-lang="gitignore"># ignore all .log files
*.log
# ignore all files in any directory named config
build/
# but do track sample.log, even though you&amp;#39;re ignoring .log files above
!sample.log
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in any directory named build
build/
&lt;/code>&lt;/pre>&lt;p>You can generate gitignore files &lt;a href="https://www.gitignore.io/">here&lt;/a>.&lt;/p>
&lt;h1 id="undoing-things">Undoing things&lt;/h1>
&lt;h2 id="make-changes-to-the-last-commit">Make changes to the last commit&lt;/h2>
&lt;p>You forgot to add one file to your commit?
You are unhappy with your commit message?
Use &lt;code>git --amend&lt;/code>!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git commit -m &lt;span style="color:#e6db74">&amp;#39;Pretty commit message&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git add forgotten_file
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git commit --amend
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Do not do this if you already pushed to remote.&lt;/p>
&lt;h2 id="unmodifying-a-modified-file">Unmodifying a Modified File&lt;/h2>
&lt;p>You made changes to a file but the changes messed everything up?
You can always go back to the version of the last commit by&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>git checkout -- filename
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;em>Warning&lt;/em>: This is dangerous as you delete all changes that you made locally.
Do NOT use this command unless you are absolutely sure what you are doing.&lt;/p>
&lt;h2 id="time-machine">Time machine&lt;/h2>
&lt;p>Something went terribly wrong. When using git this is no problem.
Just use &lt;code>reflog&lt;/code> and select the commit where still everything was alright.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git reflog
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>9bff138 HEAD@&lt;span style="color:#f92672">{&lt;/span>0&lt;span style="color:#f92672">}&lt;/span>: commit: Document limitations of this guide
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>8d9dac8 HEAD@&lt;span style="color:#f92672">{&lt;/span>1&lt;span style="color:#f92672">}&lt;/span>: commit &lt;span style="color:#f92672">(&lt;/span>amend&lt;span style="color:#f92672">)&lt;/span>: Adjust script to nice print
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">[&lt;/span>...&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2b66f53 HEAD@&lt;span style="color:#f92672">{&lt;/span>7&lt;span style="color:#f92672">}&lt;/span>: commit: Introduce git log
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>8835c33 HEAD@&lt;span style="color:#f92672">{&lt;/span>8&lt;span style="color:#f92672">}&lt;/span>: commit: Fix of formating of headings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2f0f500 HEAD@&lt;span style="color:#f92672">{&lt;/span>9&lt;span style="color:#f92672">}&lt;/span>: commit: Improve order of comments on workflow
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>249dc14 HEAD@&lt;span style="color:#f92672">{&lt;/span>10&lt;span style="color:#f92672">}&lt;/span>: commit &lt;span style="color:#f92672">(&lt;/span>amend&lt;span style="color:#f92672">)&lt;/span>: Explain git diff
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>f5ba1d3 HEAD@&lt;span style="color:#f92672">{&lt;/span>11&lt;span style="color:#f92672">}&lt;/span>: commit: Explain git diff
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>0a2ce1c HEAD@&lt;span style="color:#f92672">{&lt;/span>12&lt;span style="color:#f92672">}&lt;/span>: commit: Remove trailing whitspaces
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>735511f HEAD@&lt;span style="color:#f92672">{&lt;/span>13&lt;span style="color:#f92672">}&lt;/span>: commit &lt;span style="color:#f92672">(&lt;/span>initial&lt;span style="color:#f92672">)&lt;/span>: Initial commit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git reset HEAD@&lt;span style="color:#f92672">{&lt;/span>index_where_everything_was_fine&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git reset --hard origin/master
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;em>Warning&lt;/em>: This is dangerous as you delete all changes that you made locally.
Do NOT use this command unless you are absolutely sure what you are doing.&lt;/p>
&lt;h1 id="usb-sticks-as-remote">USB sticks as remote&lt;/h1>
&lt;p>Sometimes (e.g. you are working on a machine that is not connected to the internet) it can be
helpful to use an USB drive as remote.&lt;/p>
&lt;h2 id="how-to-prepare-the-drive">How to prepare the drive&lt;/h2>
&lt;p>Go to the USB drive&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ cd /media/username/git-stick/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create a directory for your repository&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ mkdir my-repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Initialize the repo&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ cd my-repo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ git init --bare
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="how-to-push-to-a-usb-drive">How to push to a USB drive&lt;/h2>
&lt;p>To push to a USB drive you need to add a remote. Go to your local repository and use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git remote add usb /media/username/git-stick/my-repo/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Be aware that your stick will have a different name and will be at a different location, depending
on your operating system. You named the remote &amp;ldquo;usb&amp;rdquo; here, of course you can change that name.&lt;/p>
&lt;p>Now push your repo&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git push usb
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To list all repositories just type&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git remote
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>origin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>usb
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want to push to the USB drive by default you can use&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ git push -u usb master
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Branch &lt;span style="color:#e6db74">&amp;#39;master&amp;#39;&lt;/span> set up to track remote branch &lt;span style="color:#e6db74">&amp;#39;master&amp;#39;&lt;/span> from &lt;span style="color:#e6db74">&amp;#39;usb&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="what-this-guide-does-not-cover">What this guide does not cover&lt;/h1>
&lt;ul>
&lt;li>Branching
&lt;ul>
&lt;li>Creating branches&lt;/li>
&lt;li>Merging branches (and merge conflicts)&lt;/li>
&lt;li>Remote branches&lt;/li>
&lt;li>Tagging&lt;/li>
&lt;li>Forks&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Usage of Github&lt;/li>
&lt;li>Hooks&lt;/li>
&lt;/ul>
&lt;h1 id="further-reading">Further reading&lt;/h1>
&lt;ul>
&lt;li>Official git website &lt;a href="https://git-scm.com/">Git SCM&lt;/a>: Extensiv documentation&lt;/li>
&lt;li>&lt;a href="https://ohshitgit.com/">Oh shit Git&lt;/a>: Help when you messed up.&lt;/li>
&lt;/ul>
&lt;h1 id="logo">Logo&lt;/h1>
&lt;p>Git Logo by Jason Long&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "git"
})
&lt;/script></content></entry><entry><title>Why Hyteck</title><link href="https://hyteck.de/post/why-hyteck/" type="application/octet-stream"/><updated>2019-09-16T18:18:10+02:00</updated><id>https://hyteck.de/post/why-hyteck/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="tldr">TL;DR&lt;/h1>
&lt;ul>
&lt;li>&lt;em>Hy&lt;/em> - Water&lt;/li>
&lt;li>&lt;em>Teck&lt;/em> - Castle nearby&lt;/li>
&lt;li>Summary: Bad name pun&lt;/li>
&lt;/ul>
&lt;h1 id="why-the-name-hyteck">Why the name Hyteck?&lt;/h1>
&lt;p>I am not very good at finding names I really like.
My nickname changed several times, I was never able to stick to one.
As hard as finding a nickname was finding a domainname for me.&lt;/p>
&lt;p>Nevertheless I really started to like the name Hyteck. The idea came during
a hackathon in Tübingen where two friends and I worked as a team on a project to help
elderly adults drinking. We the decided on the teamname HyHack (Hy for Hydration).&lt;/p>
&lt;p>What we first did was not so High - Tech (I will post about this sometime), yet we called it that.&lt;/p>
&lt;p>I later chenged the Tech to Teck which is a local castle (my hometown is called Kirchheim under Teck) that in the german
pronounciation is a Homophone of Tech.&lt;/p>
&lt;p>The Hy stayed as I always had a close connection to water.&lt;/p>
&lt;p>I feel like I overexplained this a bit, so poor you if you read till the end.&lt;/p>
&lt;p>However, you earned a
&lt;script>
function createCookie(name, value, days){
var expires;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
}
else {
expires = "";
}
document.cookie = name + "=" + value + expires + "; path=/; samesite=strict;";
}
&lt;/script>
&lt;button onclick="createCookie('achievement', 'TLDR_restistant', 365)">cookie&lt;/button>
!&lt;/p></content></entry><entry><title>ILMO</title><link href="https://hyteck.de/post/ilmo/" type="application/octet-stream"/><updated>2019-09-15T22:00:55+02:00</updated><id>https://hyteck.de/post/ilmo/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="ilmo">ILMO&lt;/h1>
&lt;p>is my first bigger FOSS-project. It aims at managing small libraries as found in a department of a university. You can finde the project on &lt;a href="https://github.com/GerJuli/ILMO">Github&lt;/a>.&lt;/p>
&lt;h2 id="history">History:&lt;/h2>
&lt;p>I started to create ILMO for the (rather small) library in the study rooms of mediacal engineering.
It contains about 300 books, 120 labcoats and safty glasses and some more stuff.
As I was in charge of the lends I started to get annoyed by our paper-based solution.
Especially when a bunch of people was trying to lend things it took to much time. Also reminding people to return stuff was unnecessary complicated.
Therfore I started to programm this library management tool from scratch. I only reused some basic database and session functions.
The project is still a field of learning how to do things for me.
Nevertheless after about half a year of development and many tests the system went live in Winter 2018.
Since then I fill the bugtracker and slowly improve workflows and add functions.&lt;/p>
&lt;h2 id="license">License&lt;/h2>
&lt;p>Of course the program is open-source and licensed under GPLv3.&lt;/p>
&lt;h2 id="demo">Demo&lt;/h2>
&lt;p>Right now there is no public available demo but if you are interested I will lead you the way to my test server.&lt;/p>
&lt;h2 id="using-ilmo">Using ILMO&lt;/h2>
&lt;p>You know someone who could use somthing like this? I would love to see this software in more libraries.
Right now the programm is optimized to the needs of our student union but I would be happy to work more on this.&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "ILMO"
})
&lt;/script></content></entry><entry><title>Imprint &amp; Privacy</title><link href="https://hyteck.de/imprint/" type="application/octet-stream"/><updated>2019-09-15T20:51:08+02:00</updated><id>https://hyteck.de/imprint/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="impressum">Impressum&lt;/h1>
&lt;p>Angaben gemäß § 5 TMG&lt;/p>
&lt;pre tabindex="0">&lt;code>Hyteck vertreten durch Julian-Samuel Gebühr
Eugenstraße 38
72076 Tübingen
USt-IdNr.: DE348521794
Kontakt:
E-Mail: kontakt@hyteck.de
&lt;/code>&lt;/pre>&lt;h1 id="copyright">Copyright&lt;/h1>
&lt;p>Die meisten der Inhalte dieser Website, so von mir erstellte Texte und Bilder fallen unter die CC-BY Lizenz sofern nicht anders gekennzeichnet oder vereinbart. Das gilt nicht für Inhalte Dritter, so zum Beispiel Logos von Diensten. Sehr gerne einfach kurz Nachfragen, in den meisten Fällen sollte auch CC-0 kein Problem sein.&lt;/p>
&lt;h1 id="haftungsausschluss">Haftungsausschluss&lt;/h1>
&lt;p>Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte auf dieser Seite kann ich keine Gewähr übernehmen. Bei Bekanntwerden von einer Rechtsverletzung auf dieser Seite durch Dritte werden ich diese Inhalte umgehend entfernen.&lt;/p>
&lt;h1 id="datenschutz">Datenschutz&lt;/h1>
&lt;p>Datenschutz ist wichtig. Deshalb sammeln die Dienst so wenig Daten wie möglich und diese werden gut geschützt.
Verantwortlich für die Datenverarbeitung sind die im Impressum genannten Personen.&lt;/p>
&lt;h2 id="blog-hyteckde">Blog hyteck.de&lt;/h2>
&lt;p>Beim Aufruf des Blogs werden folgende Informationen übertragen:&lt;/p>
&lt;ul>
&lt;li>IP-Addressen: Werden übertrtagen, aber nicht gespeichert&lt;/li>
&lt;/ul>
&lt;p>Wenn Beiträge mit Chat-Funktion aufgerufen werden, werden Daten wie IP Addressen an cactus.chat übertragen um Kommentare abzurufen.&lt;/p>
&lt;p>Durch Nutzung der Kommentarfunktion
a) wird ein Profil auf cactus.chat erstellt, dabei werden Daten im Browser gespeichert die das Profil mit dem Broser verknüpfen.
b) oder durch ein Einloggen mit einem bereits bestehenden Account werden Daten mit dem Anbieter des Account und zwischen Anbieter und Cactus.chat ausgetauscht.&lt;/p>
&lt;h2 id="konten">Konten&lt;/h2>
&lt;p>Eine Applikationen dieser Website können die Erstellung eines Kontos erfordern.
Durch Erstellung eines Kontos werden die angegebenen Daten und von die auf dem Server erstellten oder hochgeladen Inhalte auf dem Server gespeichert.
Des weiteren können Trackingdaten erhoben (Seitenaufrufe, Seitenverweildauer) und dem Konto zugeordnet werden. Die Daten werden ausschließlich im eingeloggten Zustand erfasst.&lt;/p>
&lt;h2 id="rechte-von-nutzerinnen">Rechte von Nutzer*innen&lt;/h2>
&lt;p>Nutzer*innen haben das Recht auf Auskunft über die ihnen zugeordneten, personenbezogenen Daten. Ein Auszug über gespeicherte Daten wird innerhalb von 4 Wochen nach Eingang des Auskunftsersuchens erstellt und elektronisch bereitgestellt. Nutzer*innnen haben das Recht auf Korrektur oder Löschung der ihnen zugeordneten, personenbezogenen Daten. In beiden Fällen ist ein entsprechendes Ersuchen an die im Impressum genannten, aktiven und aktuellen Kontaktdaten zu stellen.&lt;/p>
&lt;h2 id="speicherort">Speicherort&lt;/h2>
&lt;p>Die Server werden im Auftrag durch uberspace, strato oder hetzner bereitgestellt und betrieben.
Ein Vertrag zur Auftragsdatenverarbeitung liegt vor.
Eventuelle Backups werden auf Rechnern/Datenträgern der Impressum genannten Person gespeichert.
Auch ist eine Speicherung durch Dritte möglich, in diesem Fall liegt ein Vertrag zur Auftragsdatenverarbeitung vor.&lt;/p></content></entry><entry><title>Hello World</title><link href="https://hyteck.de/post/hello-world/" type="application/octet-stream"/><updated>2019-09-14T16:35:58+02:00</updated><id>https://hyteck.de/post/hello-world/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="hello-world">Hello world&lt;/h1>
&lt;p>Welcome to my new blog! I don&amp;rsquo;t know what this will all be about - but I will sort this out.
As a start I decided to blog in English, but I can imagine that there will be a mix between English and German in the future.&lt;/p>
&lt;p>The blog is created with the static site generator HUGO and the theme &lt;a href="https://themes.gohugo.io/hugo-nederburg-theme/">Nederburg&lt;/a>.&lt;/p>
&lt;script type="text/javascript" src="https://latest.cactus.chat/cactus.js">&lt;/script>
&lt;link rel="stylesheet" href="https://latest.cactus.chat/style.css" type="text/css">
&lt;div id="comment-section">&lt;/div>
&lt;script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat:8448",
serverName: "cactus.chat",
siteName: "hyteck",
commentSectionId: "hello-world"
})
&lt;/script></content></entry><entry><title>About</title><link href="https://hyteck.de/about/" type="application/octet-stream"/><updated>2019-08-20T19:56:10+02:00</updated><id>https://hyteck.de/about/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;h1 id="hyteck">Hyteck&lt;/h1>
&lt;p>Hyteck is a company for online services that I founded. It provides smaller SaaS contracts, e.g. for Hosting ILMO.
Currently available are the &lt;a href="https://hyteck.de/services/" title="Services">services listed here&lt;/a>&lt;/p>
&lt;h1 id="about-me">About me&lt;/h1>
&lt;p>I work at &lt;a href="https://www.dkms.de/">DKMS&lt;/a>, a nonprofit that fights blood cancer by registering potential blood stem cell
donors and raising awareness and funds. My role is &amp;ldquo;Business Analyst&amp;rdquo; in our Salesforce team. That means I
spend my day trying to figure out the business departments need, sketching solutions and translating between product and
business teams.&lt;/p>
&lt;p>After work, I spend my time with programming, activism and my pet rats.&lt;/p>
&lt;p>&lt;strong>My background&lt;/strong>&lt;/p>
&lt;p>After finishing school, I studied Medical Engineering in a joint course at University Stuttgart and University Tübingen.&lt;/p>
&lt;p>In March 2020 I finished my bachelor thesis &lt;em>&amp;ldquo;Real-time EEG analysis - Phase dependent effects of TMS on MEP&amp;rdquo;&lt;/em> at the
Institute for Neuromodulation and Neurotechnology in the University Hospital Tübingen led by Prof. Gharabaghi. After
that I was working there as a researcher.&lt;/p>
&lt;p>In November 2020 I started studying Medical Informatics Tübingen and finished in April 2024 with my master thesis &lt;em>&amp;quot;
Development and Validation of a Software Platform for Classification and Correction of Pathological Movement in Daily
Activities by Multi-modal Sensor Analysis&amp;quot;&lt;/em>. This work focused on helping Ataxia and Parkinson&amp;rsquo;s disease as part of a
larger project in the Section for Computational Sensomotorics at the Hertie Institute for Clinical Brain Research (HIH).
My advisor for this work was Winfried Ilg, and it was examined by Prof. Dr. habil. Michael Menth and Prof. Dr. Martin
Giese.&lt;/p>
&lt;h1 id="open-source-work">Open-source work&lt;/h1>
&lt;p>My work on various Open-Source projects involves&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Project&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://notfellchen.org">Notfellchen&lt;/a>&lt;/td>
&lt;td>An app for helping fancy rats get adopted from rescues&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://hyteck.de/post/ilmo/">ILMO&lt;/a>&lt;/td>
&lt;td>A library management tool, available as SaaS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook">mash-playbook&lt;/a>&lt;/td>
&lt;td>An Ansible playbook which helps you host a large catalog of FOSS services as Docker containers on your own server&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/spantaleev/matrix-docker-ansible-deploy">https://github.com/spantaleev/matrix-docker-ansible-deploy&lt;/a>&lt;/td>
&lt;td>Matrix (An open network for secure, decentralized communication) server setup using Ansible and Docker&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>and many more you can find on &lt;a href="https://github.com/moan0s">GitHub&lt;/a>, &lt;a href="https://codeberg.org/moanos/">Codeberg&lt;/a>.
or &lt;a href="https://git.hyteck.de/">my own Gitea server&lt;/a>.&lt;/p>
&lt;h1 id="commercial-services">Commercial Services&lt;/h1>
&lt;p>I offer to host some Software-as-a-Service. This is especially true for [ILMO], a library management solution I
developed. Commercial support for software I maintain is also available, as well bespoke software. Be aware that I have
a fulltime job, and I&amp;rsquo;m limited in the time I can make for your projects.&lt;/p>
&lt;p>If you are a non-profit organization, I can offer reduced pricing. If you want to see what&amp;rsquo;s possible, have a look at
the documentation of the &lt;a href="https://doku.queereszentrumtuebingen.de/">Queer Center Tübingen&lt;/a> where I set up&lt;/p>
&lt;ul>
&lt;li>a chat server (Matrix)&lt;/li>
&lt;li>a cloud collaboration platform Nextcloud)&lt;/li>
&lt;li>a SocialMediaServer (GoToSocial)&lt;/li>
&lt;li>a shared password manager (Vaultwarden)&lt;/li>
&lt;li>a automatically embedded calendar on the Website&lt;/li>
&lt;/ul>
&lt;p>and all is connected via SingleSignOn (Authentik) and backed up by BorgBackup.&lt;/p>
&lt;p>I also host(ed) some (semi-)public services including BigBlueButton, Matrix, Nextcloud, Funkwhale and Cryptpad. All
current active systems &lt;a href="https://hyteck.de/services/" title="Services">can be found in a list&lt;/a>&lt;/p>
&lt;h2 id="technologies-i-like-to-use">Technologies I like to use&lt;/h2>
&lt;ul>
&lt;li>My favourite framework is &lt;strong>Django&lt;/strong>, powered by Python. It makes development incredibly efficient and enjoyable.&lt;/li>
&lt;li>I have extensive experience in &lt;strong>Ansible&lt;/strong> and use it to deploy nearly all my services&lt;/li>
&lt;li>I use JavaScript sometimes and even dabbled a bit into React&lt;/li>
&lt;li>Coding started with Java, and PHP for me, however my PHP is a bit rusty now, while I regularly used Java during my
studies.&lt;/li>
&lt;li>For programming of real time applications, mostly for medical devices, I learned *
&lt;em>&lt;a href="https://en.wikipedia.org/wiki/Structured_text">Structured Text&lt;/a>&lt;/em>* (a programming language based on pascal focused
on programming &lt;a href="https://en.wikipedia.org/wiki/Programmable_logic_controller">PLCs&lt;/a>) and &lt;strong>C&lt;/strong>. I don&amp;rsquo;t enjoy C that
much, but I&amp;rsquo;m proud of what I managed to do anyway.&lt;/li>
&lt;li>More recently I started learning &lt;strong>Rust&lt;/strong> and implemented a smaller backend service using axum.&lt;/li>
&lt;/ul>
&lt;h1 id="contact">Contact&lt;/h1>
&lt;ul>
&lt;li>E-Mail: &lt;a href="mailto:julian-samuel@gebuehr.net">julian-samuel@gebuehr.net&lt;/a> &lt;a href="https://hyteck.de/julian-samuel@gebuehr.net.pub.asc">Public key&lt;/a>&lt;/li>
&lt;li>Matrix: @moanos:hyteck.de (Deprecated: @moanos:matrix.org)&lt;/li>
&lt;li>Mastodon: @moanos@chaos.social&lt;/li>
&lt;li>Twitter: &lt;a href="mailto:j-s.gebuehr@twitter.com">j-s.gebuehr@twitter.com&lt;/a>&lt;/li>
&lt;li>Signal, Whatsapp, Phone: Available, use one of the other options to get my phone number&lt;/li>
&lt;li>XMPP: &lt;a href="mailto:moanos@anoxinon.me">moanos@anoxinon.me&lt;/a> (Inactive)&lt;/li>
&lt;li>Threema: 2A724TYR (Inactive)&lt;/li>
&lt;/ul></content></entry></feed>