fix: add blog pages and fix project schema, remove internal TimeNet CLI
CI/CD Pipeline / Build & Deploy (push) Successful in 23s
CI/CD Pipeline / Build & Deploy (push) Successful in 23s
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "Python Project Template"
|
title: "Python Project Template"
|
||||||
description: "Opinionated Python project template with modern tooling: uv, ruff, mypy, pytest, Docker, and CI/CD ready"
|
description: "Opinionated Python project template with modern tooling: uv, ruff, mypy, pytest, Docker, and CI/CD ready"
|
||||||
pubDate: 2026-05-01
|
url: "https://gitlab.impresion3d.pro/root/python-project-template"
|
||||||
status: "active"
|
status: "active"
|
||||||
stack: ["Python", "Docker", "Gitea Actions", "Portainer"]
|
tags: ["Python", "Docker", "CI/CD", "DevOps"]
|
||||||
|
startDate: 2026-05-01
|
||||||
featured: true
|
featured: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
title: "TimeNet CLI"
|
|
||||||
description: "Command-line tool for managing TimeNet attendance tracking - clock in/out from terminal"
|
|
||||||
pubDate: 2026-04-15
|
|
||||||
status: "active"
|
|
||||||
stack: ["Python", "CLI", "Automation"]
|
|
||||||
featured: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# TimeNet CLI
|
|
||||||
|
|
||||||
A command-line interface for managing TimeNet attendance tracking. Clock in, clock out, and check status without opening the web interface.
|
|
||||||
|
|
||||||
## What It Does
|
|
||||||
|
|
||||||
TimeNet CLI automates daily time tracking workflows:
|
|
||||||
- Clock in/out with a single command
|
|
||||||
- Check current status (in/out, hours worked today)
|
|
||||||
- View work history
|
|
||||||
- Export reports
|
|
||||||
|
|
||||||
## Why I Built This
|
|
||||||
|
|
||||||
My employer uses TimeNet for attendance tracking. Opening a browser, navigating to the site, logging in, and clicking buttons for a simple clock-in felt like unnecessary friction.
|
|
||||||
|
|
||||||
I wanted a faster way: `timenet in` in the morning, `timenet out` when leaving. That's it.
|
|
||||||
|
|
||||||
## Stack
|
|
||||||
|
|
||||||
- **Python** for CLI logic
|
|
||||||
- **Click** for command-line interface
|
|
||||||
- **Requests** for API interaction
|
|
||||||
- **Credential management** for secure token storage
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clock in
|
|
||||||
timenet in
|
|
||||||
|
|
||||||
# Clock out
|
|
||||||
timenet out
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
timenet status
|
|
||||||
|
|
||||||
# View today's hours
|
|
||||||
timenet today
|
|
||||||
```
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Active. I use it daily for work attendance tracking.
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../../components/layout/Header.astro';
|
||||||
|
import Footer from '../../components/layout/Footer.astro';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const blogEntries = await getCollection('blog', ({ data }) => !data.draft);
|
||||||
|
return blogEntries.map(entry => ({
|
||||||
|
params: { slug: entry.slug },
|
||||||
|
props: { entry },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={entry.data.title}
|
||||||
|
description={entry.data.description}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<main class="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article>
|
||||||
|
<header class="mb-8">
|
||||||
|
<time class="text-sm text-text-tertiary block mb-2">
|
||||||
|
{entry.data.publishDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
|
||||||
|
<h1 class="text-4xl font-bold mb-4">{entry.data.title}</h1>
|
||||||
|
|
||||||
|
<p class="text-xl text-text-secondary mb-4">
|
||||||
|
{entry.data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{entry.data.tags.map((tag) => (
|
||||||
|
<span class="inline-block px-3 py-1 rounded-full text-sm font-medium border bg-text-tertiary/10 text-text-secondary border-text-tertiary/20">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="prose prose-invert prose-lg max-w-none">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-12 pt-8 border-t border-text-tertiary/20">
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="text-primary hover:text-secondary transition-colors font-medium"
|
||||||
|
>
|
||||||
|
← Back to all posts
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../../components/layout/Header.astro';
|
||||||
|
import Footer from '../../components/layout/Footer.astro';
|
||||||
|
|
||||||
|
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
|
||||||
|
const sortedPosts = allPosts.sort(
|
||||||
|
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Blog"
|
||||||
|
description="Thoughts on building products, technical learnings, and the indie builder journey"
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<main class="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<h1 class="text-4xl font-bold mb-8">Blog</h1>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
{sortedPosts.map((post) => (
|
||||||
|
<article class="bg-surface rounded-lg border border-text-tertiary/20 p-6 hover:border-primary/40 transition-colors">
|
||||||
|
<a href={`/blog/${post.slug}`} class="block">
|
||||||
|
<time class="text-sm text-text-tertiary block mb-2">
|
||||||
|
{post.data.publishDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-semibold mb-3 text-text-primary hover:text-primary transition-colors">
|
||||||
|
{post.data.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-text-secondary mb-4">
|
||||||
|
{post.data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<span class="inline-block px-3 py-1 rounded-full text-sm font-medium border bg-text-tertiary/10 text-text-secondary border-text-tertiary/20">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-primary font-medium">Read more →</span>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sortedPosts.length === 0 && (
|
||||||
|
<p class="text-text-secondary text-center py-12">
|
||||||
|
No posts yet. Check back soon!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
||||||
Reference in New Issue
Block a user